Skip to content

Commit 092b491

Browse files
committed
feat: refactor AppSidebar and DataExplorerPageWrapper for improved state management and UI consistency
1 parent fc3d144 commit 092b491

3 files changed

Lines changed: 139 additions & 168 deletions

File tree

frontend/components/AppSidebar.tsx

Lines changed: 117 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
7878
const [showDbPicker, setShowDbPicker] = useState(false);
7979
const [latencyMs, setLatencyMs] = useState<number | null>(null);
8080
const [collectionSort, setCollectionSort] = useState<'name_asc' | 'name_desc' | 'count_desc' | 'count_asc'>('name_asc');
81-
const [isSwitching, setIsSwitching] = useState(false);
8281
const chipRef = useRef<HTMLDivElement>(null);
8382

8483
useEffect(() => {
@@ -92,11 +91,6 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
9291
return () => document.removeEventListener('mousedown', handleClick);
9392
}, [showDbPicker]);
9493

95-
// Reset loading state whenever the connection actually changes
96-
useEffect(() => {
97-
setIsSwitching(false);
98-
}, [accountId, databaseName]);
99-
10094
// Measure API latency by pinging /health every 30s
10195
useEffect(() => {
10296
const measure = async () => {
@@ -139,145 +133,128 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
139133
}}>QueryPal</span>
140134
</Link>
141135

142-
{/* Connection chip */}
143-
{(accountName || isSwitching) && (
144-
<div ref={chipRef} style={{ position: 'relative' }}>
145-
<style>{`
146-
@keyframes qp-spin { to { transform: rotate(360deg); } }
147-
@keyframes qp-pulse { 0%,100% { opacity: 0.45; } 50% { opacity: 0.9; } }
148-
`}</style>
149-
{isSwitching ? (
150-
<div style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '5px 9px', background: 'var(--soft)', borderRadius: 7 }}>
151-
<div style={{ width: 14, height: 14, borderRadius: '50%', border: '2px solid var(--border)', borderTopColor: 'var(--accent)', animation: 'qp-spin 0.7s linear infinite', flexShrink: 0 }} />
136+
{/* Connection chip — always rendered so the brand section never changes height */}
137+
<div ref={chipRef} style={{ position: 'relative' }}>
138+
{!accountName ? (
139+
/* Placeholder keeps the same height as the real chip */
140+
<div style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '5px 9px', background: 'var(--soft)', borderRadius: 7 }}>
141+
<span style={{ width: 14, height: 14, borderRadius: 4, background: 'var(--border)', flexShrink: 0 }} />
142+
<div style={{ minWidth: 0, flex: 1 }}>
143+
<div style={{ height: 11, borderRadius: 3, background: 'var(--border)', width: '65%', marginBottom: 4 }} />
144+
<div style={{ height: 9, borderRadius: 3, background: 'var(--border)', width: '45%' }} />
145+
</div>
146+
</div>
147+
) : (
148+
<>
149+
<div
150+
onClick={() => ((availableAccounts && availableAccounts.length > 1) || (availableDbs && availableDbs.length > 1)) ? setShowDbPicker(v => !v) : undefined}
151+
style={{
152+
display: 'flex', alignItems: 'center', gap: 7,
153+
padding: '5px 9px', background: 'var(--soft)', borderRadius: 7,
154+
cursor: ((availableAccounts && availableAccounts.length > 1) || (availableDbs && availableDbs.length > 1)) ? 'pointer' : 'default',
155+
}}
156+
>
157+
<span style={{ width: 14, height: 14, borderRadius: 4, background: '#1d6cf2', flexShrink: 0 }} />
152158
<div style={{ minWidth: 0, flex: 1 }}>
153-
<div style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
154-
Connecting…
159+
<div style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--fg)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
160+
{accountName}
155161
</div>
156-
<div style={{ height: 9, marginTop: 3, borderRadius: 4, background: 'var(--border)', animation: 'qp-pulse 1.4s ease-in-out infinite', width: '70%' }} />
162+
{databaseName && (
163+
<div style={{ fontSize: 10.5, color: 'var(--muted)', fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
164+
{databaseName}
165+
</div>
166+
)}
157167
</div>
168+
{((availableAccounts && availableAccounts.length > 1) || (availableDbs && availableDbs.length > 1)) && (
169+
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" style={{ color: 'var(--muted)', flexShrink: 0 }}>
170+
<path d="M4 6l4 4 4-4"/>
171+
</svg>
172+
)}
158173
</div>
159-
) : (() => {
160-
const hasMultipleAccounts = availableAccounts && availableAccounts.length > 1;
161-
const hasMultipleDbs = availableDbs && availableDbs.length > 1;
162-
const isPickerEnabled = hasMultipleAccounts || hasMultipleDbs;
163-
return (
164-
<>
165-
<div
166-
onClick={() => isPickerEnabled ? setShowDbPicker(v => !v) : undefined}
167-
style={{
168-
display: 'flex', alignItems: 'center', gap: 7,
169-
padding: '5px 9px', background: 'var(--soft)', borderRadius: 7,
170-
cursor: isPickerEnabled ? 'pointer' : 'default',
171-
}}
172-
>
173-
<span style={{ width: 14, height: 14, borderRadius: 4, background: '#1d6cf2', flexShrink: 0 }} />
174-
<div style={{ minWidth: 0, flex: 1 }}>
175-
<div style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--fg)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
176-
{accountName}
177-
</div>
178-
{databaseName && (
179-
<div style={{ fontSize: 10.5, color: 'var(--muted)', fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
180-
{databaseName}
181-
</div>
182-
)}
183-
</div>
184-
{isPickerEnabled && (
185-
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" style={{ color: 'var(--muted)', flexShrink: 0 }}>
186-
<path d="M4 6l4 4 4-4"/>
187-
</svg>
188-
)}
189-
</div>
190-
191-
{showDbPicker && (
192-
<div style={{
193-
position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, zIndex: 100,
194-
background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8,
195-
boxShadow: '0 4px 16px rgba(0,0,0,0.10)', overflow: 'hidden',
196-
}}>
197-
{/* Accounts section */}
198-
{hasMultipleAccounts && availableAccounts && (
199-
<>
200-
<div style={{ padding: '6px 10px 4px', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--muted)', fontWeight: 500 }}>
201-
Cosmos account
202-
</div>
203-
{availableAccounts.map(acc => {
204-
const isCurrent = acc.id === accountId;
205-
return (
206-
<button
207-
key={acc.id}
208-
onClick={() => { if (!isCurrent) { setIsSwitching(true); setShowDbPicker(false); onSwitchAccount?.(acc); } }}
209-
style={{
210-
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
211-
padding: '7px 10px', border: 'none', textAlign: 'left',
212-
cursor: isCurrent ? 'default' : 'pointer',
213-
background: isCurrent ? 'var(--accent-soft)' : 'transparent',
214-
color: isCurrent ? 'var(--accent)' : 'var(--fg)',
215-
fontSize: 12.5, fontFamily: 'var(--font-body)',
216-
}}
217-
onMouseEnter={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'var(--soft)'; }}
218-
onMouseLeave={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
219-
>
220-
<span style={{ width: 10, height: 10, borderRadius: 3, background: isCurrent ? '#1d6cf2' : 'var(--muted)', flexShrink: 0 }} />
221-
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{acc.name}</span>
222-
{isCurrent && (
223-
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" style={{ color: 'var(--accent)', flexShrink: 0 }}>
224-
<path d="M3 8l4 4 6-6"/>
225-
</svg>
226-
)}
227-
</button>
228-
);
229-
})}
230-
</>
231-
)}
232174

233-
{/* Divider between sections */}
234-
{hasMultipleAccounts && hasMultipleDbs && (
235-
<div style={{ height: 1, background: 'var(--border)', margin: '4px 0' }} />
236-
)}
237-
238-
{/* Databases section */}
239-
{hasMultipleDbs && availableDbs && (
240-
<>
241-
<div style={{ padding: '6px 10px 4px', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--muted)', fontWeight: 500 }}>
242-
Database
243-
</div>
244-
{availableDbs.map(db => {
245-
const isCurrent = db.name === databaseName;
246-
return (
247-
<button
248-
key={db.name}
249-
onClick={() => { if (!isCurrent) { setIsSwitching(true); setShowDbPicker(false); onSwitchDatabase?.(db); } }}
250-
style={{
251-
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
252-
padding: '7px 10px', border: 'none', cursor: 'pointer', textAlign: 'left',
253-
background: isCurrent ? 'var(--accent-soft)' : 'transparent',
254-
color: isCurrent ? 'var(--accent)' : 'var(--fg)',
255-
fontSize: 12.5, fontFamily: 'var(--font-mono)',
256-
}}
257-
onMouseEnter={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'var(--soft)'; }}
258-
onMouseLeave={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
259-
>
260-
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" style={{ color: isCurrent ? 'var(--accent)' : 'var(--muted)', flexShrink: 0 }}>
261-
<ellipse cx="8" cy="4" rx="6" ry="2"/><path d="M2 4v8c0 1.1 2.7 2 6 2s6-.9 6-2V4M2 8c0 1.1 2.7 2 6 2s6-.9 6-2"/>
262-
</svg>
263-
{db.name}
264-
{isCurrent && (
265-
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginLeft: 'auto', color: 'var(--accent)', flexShrink: 0 }}>
266-
<path d="M3 8l4 4 6-6"/>
267-
</svg>
268-
)}
269-
</button>
270-
);
271-
})}
272-
</>
273-
)}
274-
</div>
175+
{showDbPicker && (
176+
<div style={{
177+
position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, zIndex: 100,
178+
background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8,
179+
boxShadow: '0 4px 16px rgba(0,0,0,0.10)', overflow: 'hidden',
180+
}}>
181+
{availableAccounts && availableAccounts.length > 1 && (
182+
<>
183+
<div style={{ padding: '6px 10px 4px', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--muted)', fontWeight: 500 }}>
184+
Cosmos account
185+
</div>
186+
{availableAccounts.map(acc => {
187+
const isCurrent = acc.id === accountId;
188+
return (
189+
<button
190+
key={acc.id}
191+
onClick={() => { if (!isCurrent) { setShowDbPicker(false); onSwitchAccount?.(acc); } }}
192+
style={{
193+
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
194+
padding: '7px 10px', border: 'none', textAlign: 'left',
195+
cursor: isCurrent ? 'default' : 'pointer',
196+
background: isCurrent ? 'var(--accent-soft)' : 'transparent',
197+
color: isCurrent ? 'var(--accent)' : 'var(--fg)',
198+
fontSize: 12.5, fontFamily: 'var(--font-body)',
199+
}}
200+
onMouseEnter={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'var(--soft)'; }}
201+
onMouseLeave={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
202+
>
203+
<span style={{ width: 10, height: 10, borderRadius: 3, background: isCurrent ? '#1d6cf2' : 'var(--muted)', flexShrink: 0 }} />
204+
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{acc.name}</span>
205+
{isCurrent && (
206+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" style={{ color: 'var(--accent)', flexShrink: 0 }}>
207+
<path d="M3 8l4 4 6-6"/>
208+
</svg>
209+
)}
210+
</button>
211+
);
212+
})}
213+
</>
275214
)}
276-
</>
277-
);
278-
})()}
279-
</div>
280-
)}
215+
{availableAccounts && availableAccounts.length > 1 && availableDbs && availableDbs.length > 1 && (
216+
<div style={{ height: 1, background: 'var(--border)', margin: '4px 0' }} />
217+
)}
218+
{availableDbs && availableDbs.length > 1 && (
219+
<>
220+
<div style={{ padding: '6px 10px 4px', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--muted)', fontWeight: 500 }}>
221+
Database
222+
</div>
223+
{availableDbs.map(db => {
224+
const isCurrent = db.name === databaseName;
225+
return (
226+
<button
227+
key={db.name}
228+
onClick={() => { if (!isCurrent) { setShowDbPicker(false); onSwitchDatabase?.(db); } }}
229+
style={{
230+
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
231+
padding: '7px 10px', border: 'none', cursor: 'pointer', textAlign: 'left',
232+
background: isCurrent ? 'var(--accent-soft)' : 'transparent',
233+
color: isCurrent ? 'var(--accent)' : 'var(--fg)',
234+
fontSize: 12.5, fontFamily: 'var(--font-mono)',
235+
}}
236+
onMouseEnter={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'var(--soft)'; }}
237+
onMouseLeave={(e) => { if (!isCurrent) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
238+
>
239+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" style={{ color: isCurrent ? 'var(--accent)' : 'var(--muted)', flexShrink: 0 }}>
240+
<ellipse cx="8" cy="4" rx="6" ry="2"/><path d="M2 4v8c0 1.1 2.7 2 6 2s6-.9 6-2V4M2 8c0 1.1 2.7 2 6 2s6-.9 6-2"/>
241+
</svg>
242+
{db.name}
243+
{isCurrent && (
244+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginLeft: 'auto', color: 'var(--accent)', flexShrink: 0 }}>
245+
<path d="M3 8l4 4 6-6"/>
246+
</svg>
247+
)}
248+
</button>
249+
);
250+
})}
251+
</>
252+
)}
253+
</div>
254+
)}
255+
</>
256+
)}
257+
</div>
281258
</div>
282259

283260
{/* Navigation */}
@@ -347,23 +324,7 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
347324
})}
348325
</div>
349326

350-
{/* Collections — skeleton while switching, real list when loaded */}
351-
{isSwitching && (
352-
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', marginTop: 12 }}>
353-
<div style={{ padding: '0 8px 6px 8px', fontSize: 10.5, fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
354-
Collections
355-
</div>
356-
<div style={{ padding: '0 8px', display: 'flex', flexDirection: 'column', gap: 4 }}>
357-
{[60, 80, 50].map((w, i) => (
358-
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '5px 8px' }}>
359-
<div style={{ width: 11, height: 11, borderRadius: 3, background: 'var(--border)', flexShrink: 0, animation: 'qp-pulse 1.4s ease-in-out infinite' }} />
360-
<div style={{ height: 10, borderRadius: 4, background: 'var(--border)', width: `${w}%`, animation: 'qp-pulse 1.4s ease-in-out infinite', animationDelay: `${i * 0.15}s` }} />
361-
</div>
362-
))}
363-
</div>
364-
</div>
365-
)}
366-
{!isSwitching && collections && collections.length > 0 && (
327+
{collections && collections.length > 0 && (
367328
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', marginTop: 12 }}>
368329
<div style={{
369330
display: 'flex', alignItems: 'center', justifyContent: 'space-between',

0 commit comments

Comments
 (0)