Skip to content

Commit 5d83f3c

Browse files
prmoore77claude
andcommitted
Add saved connections, query queueing, and configurable timeout
- Fix #7: Add query queueing to prevent concurrent connection errors - Fix #8: Remember host settings with optional password saving - Add configurable query timeout (unlimited by default) - Auto-reconnect saved connections on page load Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3f0f1df commit 5d83f3c

File tree

7 files changed

+495
-80
lines changed

7 files changed

+495
-80
lines changed

app/api/connect/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { setConnection } from '@/lib/connections';
44

55
export async function POST(request: NextRequest) {
66
try {
7-
const { host, port, username, password, useTls, skipTlsVerify } = await request.json();
7+
const { host, port, username, password, useTls, skipTlsVerify, queryTimeout } = await request.json();
88

99
if (!host) {
1010
return NextResponse.json({ error: 'Host is required' }, { status: 400 });
@@ -17,6 +17,7 @@ export async function POST(request: NextRequest) {
1717
password,
1818
useTls: useTls !== false, // Default to true
1919
skipTlsVerify: skipTlsVerify || false,
20+
queryTimeout: queryTimeout || 0, // 0 = unlimited
2021
});
2122

2223
await service.connect();

components/AddServerDialog.module.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,99 @@
158158
color: var(--error-color);
159159
}
160160

161+
.fieldHint {
162+
display: block;
163+
margin-top: 4px;
164+
font-size: 11px;
165+
color: var(--text-muted);
166+
}
167+
161168
.dialogActions {
162169
display: flex;
163170
justify-content: flex-end;
164171
gap: 12px;
165172
margin-top: 24px;
166173
}
174+
175+
/* Saved connections */
176+
.savedConnections {
177+
margin-bottom: 20px;
178+
}
179+
180+
.savedConnections > label {
181+
display: block;
182+
margin-bottom: 8px;
183+
font-size: 13px;
184+
font-weight: 500;
185+
color: var(--text-secondary);
186+
}
187+
188+
.savedConnectionsList {
189+
display: flex;
190+
flex-direction: column;
191+
gap: 4px;
192+
max-height: 150px;
193+
overflow-y: auto;
194+
border: 1px solid var(--border-color);
195+
border-radius: 8px;
196+
padding: 4px;
197+
}
198+
199+
.savedConnectionItem {
200+
display: flex;
201+
align-items: center;
202+
gap: 8px;
203+
padding: 8px 12px;
204+
border-radius: 6px;
205+
cursor: pointer;
206+
transition: background-color 0.15s ease;
207+
}
208+
209+
.savedConnectionItem:hover {
210+
background-color: var(--bg-hover);
211+
}
212+
213+
.savedConnectionItem.selected {
214+
background-color: var(--primary-light);
215+
border: 1px solid var(--primary-color);
216+
}
217+
218+
.savedConnectionName {
219+
font-weight: 500;
220+
font-size: 13px;
221+
color: var(--text-primary);
222+
}
223+
224+
.savedConnectionDetails {
225+
flex: 1;
226+
font-size: 12px;
227+
color: var(--text-muted);
228+
overflow: hidden;
229+
text-overflow: ellipsis;
230+
white-space: nowrap;
231+
}
232+
233+
.deleteSavedBtn {
234+
width: 20px;
235+
height: 20px;
236+
display: flex;
237+
align-items: center;
238+
justify-content: center;
239+
background: transparent;
240+
border: none;
241+
border-radius: 4px;
242+
font-size: 16px;
243+
color: var(--text-muted);
244+
cursor: pointer;
245+
opacity: 0;
246+
transition: opacity 0.15s ease, background-color 0.15s ease;
247+
}
248+
249+
.savedConnectionItem:hover .deleteSavedBtn {
250+
opacity: 1;
251+
}
252+
253+
.deleteSavedBtn:hover {
254+
background-color: var(--error-bg);
255+
color: var(--error-color);
256+
}

components/AddServerDialog.tsx

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, FormEvent } from 'react';
44
import { useApp } from '@/context/AppContext';
5+
import { SavedConnection } from '@/lib/types';
56
import Image from 'next/image';
67
import styles from './AddServerDialog.module.css';
78

@@ -11,34 +12,66 @@ interface AddServerDialogProps {
1112
}
1213

1314
export function AddServerDialog({ onClose, onSuccess }: AddServerDialogProps) {
14-
const { connectServer } = useApp();
15+
const { connectServer, saveConnection, deleteSavedConnection, connectFromSaved, state } = useApp();
1516
const [name, setName] = useState('');
1617
const [host, setHost] = useState('localhost');
1718
const [port, setPort] = useState(31337);
1819
const [username, setUsername] = useState('');
1920
const [password, setPassword] = useState('');
2021
const [useTls, setUseTls] = useState(true);
2122
const [skipTlsVerify, setSkipTlsVerify] = useState(false);
23+
const [queryTimeout, setQueryTimeout] = useState(0); // 0 = unlimited
24+
const [saveThisConnection, setSaveThisConnection] = useState(true);
25+
const [rememberPassword, setRememberPassword] = useState(false);
2226
const [isConnecting, setIsConnecting] = useState(false);
2327
const [error, setError] = useState<string | null>(null);
28+
const [selectedSavedId, setSelectedSavedId] = useState<string | null>(null);
29+
30+
const loadSavedConnection = (saved: SavedConnection) => {
31+
setSelectedSavedId(saved.id);
32+
setName(saved.name);
33+
setHost(saved.host);
34+
setPort(saved.port);
35+
setUsername(saved.username || '');
36+
setPassword(saved.password || '');
37+
setUseTls(saved.useTls);
38+
setSkipTlsVerify(saved.skipTlsVerify);
39+
setQueryTimeout(saved.queryTimeout || 0);
40+
setRememberPassword(!!saved.password);
41+
};
42+
43+
const handleDeleteSaved = (e: React.MouseEvent, savedId: string) => {
44+
e.stopPropagation();
45+
deleteSavedConnection(savedId);
46+
if (selectedSavedId === savedId) {
47+
setSelectedSavedId(null);
48+
}
49+
};
2450

2551
const handleSubmit = async (e: FormEvent) => {
2652
e.preventDefault();
2753
setIsConnecting(true);
2854
setError(null);
2955

56+
const config = {
57+
host,
58+
port,
59+
username: username || undefined,
60+
password: password || undefined,
61+
useTls,
62+
skipTlsVerify,
63+
queryTimeout: queryTimeout || undefined,
64+
};
65+
const displayName = name || `${host}:${port}`;
66+
3067
try {
31-
await connectServer(
32-
{
33-
host,
34-
port,
35-
username: username || undefined,
36-
password: password || undefined,
37-
useTls,
38-
skipTlsVerify,
39-
},
40-
name || `${host}:${port}`
41-
);
68+
await connectServer(config, displayName);
69+
// Save connection if checkbox is checked
70+
if (saveThisConnection) {
71+
// Only include password if user opted to remember it
72+
const configToSave = rememberPassword ? config : { ...config, password: undefined };
73+
saveConnection(configToSave, displayName);
74+
}
4275
onSuccess();
4376
} catch (err) {
4477
setError(err instanceof Error ? err.message : 'Connection failed');
@@ -70,6 +103,34 @@ export function AddServerDialog({ onClose, onSuccess }: AddServerDialogProps) {
70103
</div>
71104
)}
72105

106+
{state.savedConnections.length > 0 && (
107+
<div className={styles.savedConnections}>
108+
<label>Saved Connections</label>
109+
<div className={styles.savedConnectionsList}>
110+
{state.savedConnections.map((saved) => (
111+
<div
112+
key={saved.id}
113+
className={`${styles.savedConnectionItem} ${selectedSavedId === saved.id ? styles.selected : ''}`}
114+
onClick={() => loadSavedConnection(saved)}
115+
>
116+
<span className={styles.savedConnectionName}>{saved.name}</span>
117+
<span className={styles.savedConnectionDetails}>
118+
{saved.username}@{saved.host}:{saved.port}
119+
</span>
120+
<button
121+
type="button"
122+
className={styles.deleteSavedBtn}
123+
onClick={(e) => handleDeleteSaved(e, saved.id)}
124+
title="Delete saved connection"
125+
>
126+
×
127+
</button>
128+
</div>
129+
))}
130+
</div>
131+
</div>
132+
)}
133+
73134
<div className={styles.formGroup}>
74135
<label htmlFor="name">Display Name</label>
75136
<input
@@ -151,6 +212,40 @@ export function AddServerDialog({ onClose, onSuccess }: AddServerDialogProps) {
151212
</label>
152213
</div>
153214

215+
<div className={styles.formGroup}>
216+
<label htmlFor="queryTimeout">Query Timeout (seconds)</label>
217+
<input
218+
type="number"
219+
id="queryTimeout"
220+
value={queryTimeout}
221+
onChange={(e) => setQueryTimeout(parseInt(e.target.value) || 0)}
222+
placeholder="0 (unlimited)"
223+
min={0}
224+
/>
225+
<span className={styles.fieldHint}>0 = unlimited (no timeout)</span>
226+
</div>
227+
228+
<div className={`${styles.formRow} ${styles.checkboxRow}`}>
229+
<label className={styles.checkboxLabel}>
230+
<input
231+
type="checkbox"
232+
checked={saveThisConnection}
233+
onChange={(e) => setSaveThisConnection(e.target.checked)}
234+
/>
235+
<span>Save connection for later</span>
236+
</label>
237+
{saveThisConnection && (
238+
<label className={styles.checkboxLabel}>
239+
<input
240+
type="checkbox"
241+
checked={rememberPassword}
242+
onChange={(e) => setRememberPassword(e.target.checked)}
243+
/>
244+
<span>Remember password</span>
245+
</label>
246+
)}
247+
</div>
248+
154249
<div className={styles.dialogActions}>
155250
<button type="button" className="btn btn-secondary" onClick={onClose}>
156251
Cancel

0 commit comments

Comments
 (0)