Skip to content

Commit f47af6c

Browse files
fix: clear stale connectionId after impersonation changes
Root cause: When an admin uses 'View as' to impersonate a user, the backend correctly returns only the connections visible to the target user. However, the frontend's useConnectionManager hook had an early return when connectionId was set, without validating that the connection was actually in the new list. This caused the UI to keep a stale connectionId from before the impersonation, resulting in 403 errors when chat/agent/editor APIs were called with a connection the effective user cannot access. Fix: 1. In useConnectionManager.js: Added validation that connectionId exists in the connections array. If not, clear it and auto-select from valid options. 2. In all section components using useConnectionManager: - Added isLoading check to prevent rendering during connection refresh - Added selectedConnection validation alongside connectionId check - This ensures we never render a section with a stale connection that's not in the effective user's accessible list Affected sections: - AgentChatSection - EditorSection - DashboardsSection - SchemaSection - SchemaDocsSection - SlowQueriesSection - DigestSection - CompanyKnowledgeSection - MonitorSection Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent b4a6165 commit f47af6c

12 files changed

Lines changed: 180 additions & 31 deletions

‎src/components/sections/AgentChatSection.jsx‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@ import { useAuth } from '@/hooks/useAuth'
33
import AgentChatPanel from '@/components/AgentChat/AgentChatPanel'
44

55
export default function AgentChatSection() {
6-
const { connectionId, selectedConnection } = useConnectionManager()
6+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
77
const { username } = useAuth()
88

9-
if (!connectionId) {
9+
// Wait for the connection list to load before rendering anything. Without this,
10+
// we might render the agent panel with a stale connectionId from before an
11+
// impersonation change — the new user's connection list hasn't loaded yet, so
12+
// selectedConnection is undefined, but connectionId is still the old value.
13+
if (isLoading) {
14+
return (
15+
<div style={{ padding: 40, color: '#6b7280', fontSize: 14 }}>
16+
Loading connections…
17+
</div>
18+
)
19+
}
20+
21+
// Either no connection selected, or the selected connectionId is not in the
22+
// current user's connections (stale after impersonation). Both cases mean the
23+
// user needs to pick a valid connection before chatting.
24+
if (!connectionId || !selectedConnection) {
1025
return (
1126
<div style={{ padding: 40, color: '#6b7280', fontSize: 14 }}>
1227
Select a database connection to chat with the DeepSQL Agent.
@@ -20,8 +35,8 @@ export default function AgentChatSection() {
2035
<AgentChatPanel
2136
key={`${username || 'anon'}:${connectionId}`}
2237
connectionId={connectionId}
23-
connectionName={selectedConnection?.connectionName}
24-
canManageContent={Boolean(selectedConnection?.canManageContent)}
38+
connectionName={selectedConnection.connectionName}
39+
canManageContent={Boolean(selectedConnection.canManageContent)}
2540
/>
2641
)
2742
}

‎src/components/sections/CompanyKnowledgeSection.jsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
import { Building2 } from 'lucide-react'
1+
import { Building2, Loader2 } from 'lucide-react'
22
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
33
import CompanyKnowledgePanel from '@/components/company-knowledge/CompanyKnowledgePanel'
44
import styles from './SectionEmpty.module.css'
55

66
export default function CompanyKnowledgeSection() {
7-
const { connectionId } = useConnectionManager()
7+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
88

9-
if (!connectionId) {
9+
if (isLoading) {
10+
return (
11+
<div className={styles.root}>
12+
<Loader2 size={24} color="#9ca3af" className={styles.spin} />
13+
<p className={styles.subtitle}>Loading connections…</p>
14+
</div>
15+
)
16+
}
17+
18+
// Either no connection or stale connectionId not in the effective user's list
19+
if (!connectionId || !selectedConnection) {
1020
return (
1121
<div className={styles.root}>
1222
<div className={styles.iconWrap}><Building2 size={26} color="#9ca3af" /></div>

‎src/components/sections/DashboardsSection.jsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useCallback, useEffect } from 'react'
2-
import { LayoutDashboard } from 'lucide-react'
2+
import { LayoutDashboard, Loader2 } from 'lucide-react'
33
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
44
import { useSetImmersive } from '@/lib/stores/useNavStore'
55
import DashboardsHome from './DashboardsHome'
@@ -10,7 +10,7 @@ import emptyStyles from './SectionEmpty.module.css'
1010
// dashboard, a focused full-bleed builder workspace (sidebar hidden via the
1111
// nav store's immersive flag).
1212
export default function DashboardsSection() {
13-
const { connectionId } = useConnectionManager()
13+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
1414
const setImmersive = useSetImmersive()
1515
const [open_, setOpen] = useState(null) // null = gallery; 'new' | dashboard object = workspace
1616

@@ -22,7 +22,17 @@ export default function DashboardsSection() {
2222
// Safety: never leave the app in immersive mode when this section unmounts.
2323
useEffect(() => () => setImmersive(false), [setImmersive])
2424

25-
if (!connectionId) {
25+
if (isLoading) {
26+
return (
27+
<div className={emptyStyles.root}>
28+
<Loader2 size={24} color="#9ca3af" className={emptyStyles.spin} />
29+
<p className={emptyStyles.subtitle}>Loading connections…</p>
30+
</div>
31+
)
32+
}
33+
34+
// Either no connection or stale connectionId not in the effective user's list
35+
if (!connectionId || !selectedConnection) {
2636
return (
2737
<div className={emptyStyles.root}>
2838
<div className={emptyStyles.iconWrap}><LayoutDashboard size={26} color="#9ca3af" /></div>

‎src/components/sections/DigestSection.jsx‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react'
2-
import { Newspaper, RefreshCw, Settings, Check, Clock, AlertCircle, Zap } from 'lucide-react'
2+
import { Loader2, Newspaper, RefreshCw, Settings, Check, Clock, AlertCircle, Zap } from 'lucide-react'
33
import { slackDigestAPI, digestPreferencesAPI } from '@/lib/api/client'
44
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
55
import DigestPreferencesPanel from './DigestPreferencesPanel'
@@ -165,7 +165,7 @@ function DigestSection({ section }) {
165165
const DIGEST_PREFS_AUTOPEN_KEY = 'deepsql.digestPrefs.autoOpened.v1'
166166

167167
export default function DigestFeedSection() {
168-
const { connectionId, selectedConnection } = useConnectionManager()
168+
const { connectionId, selectedConnection, isLoading: connectionsLoading } = useConnectionManager()
169169
const [digests, setDigests] = useState([])
170170
const [loading, setLoading] = useState(false)
171171
const [triggering, setTriggering] = useState(false)
@@ -257,6 +257,26 @@ export default function DigestFeedSection() {
257257
}
258258
}
259259

260+
// Wait for connection list to load first
261+
if (connectionsLoading) {
262+
return (
263+
<div className={styles.root}>
264+
<div className={styles.topBar}>
265+
<div className={styles.topBarLeft}>
266+
<Newspaper size={17} className={styles.topBarIcon} />
267+
<span className={styles.topBarTitle}>DB Digest</span>
268+
</div>
269+
</div>
270+
<div className={styles.feed}>
271+
<div className={styles.loadingState}>
272+
<Loader2 size={20} className={styles.spinning} color="#9ca3af" />
273+
<span>Loading connections…</span>
274+
</div>
275+
</div>
276+
</div>
277+
)
278+
}
279+
260280
return (
261281
<div className={styles.root}>
262282
{/* Top bar */}
@@ -309,15 +329,16 @@ export default function DigestFeedSection() {
309329
</div>
310330
)}
311331

312-
{!error && !loading && !connectionId && (
332+
{/* Either no connection or stale connectionId not in the effective user's list */}
333+
{!error && !loading && (!connectionId || !selectedConnection) && (
313334
<div className={styles.emptyState}>
314335
<Newspaper size={32} color="#d1d5db" />
315336
<h3>No connection selected</h3>
316337
<p>Select a connection to view its digest history.</p>
317338
</div>
318339
)}
319340

320-
{!error && !loading && !!connectionId && digests.length === 0 && (
341+
{!error && !loading && !!connectionId && !!selectedConnection && digests.length === 0 && (
321342
<div className={styles.emptyState}>
322343
<Newspaper size={32} color="#d1d5db" />
323344
<h3>No digests yet</h3>

‎src/components/sections/EditorSection.jsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
22
import SqlRunnerTab from '@/components/tabs/Core/SqlRunnerTab'
3-
import { Code2 } from 'lucide-react'
3+
import { Code2, Loader2 } from 'lucide-react'
44
import styles from './SectionEmpty.module.css'
55

66
export default function EditorSection() {
7-
const { connectionId } = useConnectionManager()
7+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
88

9-
if (!connectionId) {
9+
if (isLoading) {
10+
return (
11+
<div className={styles.root}>
12+
<Loader2 size={24} color="#9ca3af" className={styles.spin} />
13+
<p className={styles.subtitle}>Loading connections…</p>
14+
</div>
15+
)
16+
}
17+
18+
// Either no connection selected, or stale connectionId not in the current user's list
19+
if (!connectionId || !selectedConnection) {
1020
return (
1121
<div className={styles.root}>
1222
<div className={styles.iconWrap}>

‎src/components/sections/MonitorSection.jsx‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useEffect, useState } from 'react'
2-
import { BarChart2 } from 'lucide-react'
2+
import { BarChart2, Loader2 } from 'lucide-react'
33
import { slowQueriesAPI } from '@/lib/api/client'
44
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
55
import AnalyticsTab from '@/components/tabs/Monitoring/AnalyticsTab'
66
import styles from './SectionEmpty.module.css'
77

88
export default function MonitorSection() {
9-
const { connectionId } = useConnectionManager()
9+
const { connectionId, selectedConnection, isLoading: connectionsLoading } = useConnectionManager()
1010
const [hasData, setHasData] = useState(null) // null = loading
1111
const [loading, setLoading] = useState(true)
1212

@@ -27,7 +27,18 @@ export default function MonitorSection() {
2727
.finally(() => setLoading(false))
2828
}, [connectionId])
2929

30-
if (!connectionId) {
30+
// Wait for connection list to load first
31+
if (connectionsLoading) {
32+
return (
33+
<div className={styles.root}>
34+
<Loader2 size={24} color="#9ca3af" className={styles.spin} />
35+
<p className={styles.subtitle}>Loading connections…</p>
36+
</div>
37+
)
38+
}
39+
40+
// Either no connection or stale connectionId not in the effective user's list
41+
if (!connectionId || !selectedConnection) {
3142
return (
3243
<div className={styles.root}>
3344
<div className={styles.iconWrap}>

‎src/components/sections/SchemaDocsSection.jsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FileText } from 'lucide-react'
1+
import { FileText, Loader2 } from 'lucide-react'
22
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
33
import { useSetActiveSection } from '@/lib/stores/useNavStore'
44
import { useCompanyKnowledgeStore } from '@/lib/stores/useCompanyKnowledgeStore'
@@ -7,11 +7,21 @@ import styles from './SectionEmpty.module.css'
77
import workspaceStyles from './TopLevelSection.module.css'
88

99
export default function SchemaDocsSection() {
10-
const { connectionId } = useConnectionManager()
10+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
1111
const setActiveSection = useSetActiveSection()
1212
const setLinkedFilters = useCompanyKnowledgeStore((state) => state.setLinkedFilters)
1313

14-
if (!connectionId) {
14+
if (isLoading) {
15+
return (
16+
<div className={styles.root}>
17+
<Loader2 size={24} color="#9ca3af" className={styles.spin} />
18+
<p className={styles.subtitle}>Loading connections…</p>
19+
</div>
20+
)
21+
}
22+
23+
// Either no connection or stale connectionId not in the effective user's list
24+
if (!connectionId || !selectedConnection) {
1525
return (
1626
<div className={styles.root}>
1727
<div className={styles.iconWrap}><FileText size={26} color="#9ca3af" /></div>

‎src/components/sections/SchemaSection.jsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
import { Network } from 'lucide-react'
1+
import { Network, Loader2 } from 'lucide-react'
22
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
33
import BrainWorkspace from '@/components/tabs/Brain/BrainWorkspace'
44
import styles from './SectionEmpty.module.css'
55

66
export default function SchemaSection() {
7-
const { connectionId } = useConnectionManager()
7+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
88

9-
if (!connectionId) {
9+
if (isLoading) {
10+
return (
11+
<div className={styles.root}>
12+
<Loader2 size={24} color="#9ca3af" className={styles.spin} />
13+
<p className={styles.subtitle}>Loading connections…</p>
14+
</div>
15+
)
16+
}
17+
18+
// Either no connection or stale connectionId not in the effective user's list
19+
if (!connectionId || !selectedConnection) {
1020
return (
1121
<div className={styles.root}>
1222
<div className={styles.iconWrap}><Network size={26} color="#9ca3af" /></div>

‎src/components/sections/SectionEmpty.module.css‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@
7272
background: #f9fafb;
7373
}
7474

75+
.spin {
76+
animation: sectionSpin 1s linear infinite;
77+
}
78+
79+
@keyframes sectionSpin {
80+
to { transform: rotate(360deg); }
81+
}
82+
7583
.pills {
7684
display: flex;
7785
gap: 8px;

‎src/components/sections/SlowQueriesSection.jsx‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useRef, useState } from 'react'
2-
import { Activity, FileText, LineChart, Settings, Users } from 'lucide-react'
2+
import { Activity, FileText, LineChart, Loader2, Settings, Users } from 'lucide-react'
33
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
44
import { useSlowLogSourceConfig } from '@/lib/hooks/queries'
55
import QueryTrendsTab from '@/components/tabs/Performance/QueryTrendsTab'
@@ -31,7 +31,7 @@ const LOG_SOURCE_HELP = {
3131
* is a single empty state whose CTA opens SlowQuerySourceModal.
3232
*/
3333
export default function SlowQueriesSection() {
34-
const { connectionId, selectedConnection } = useConnectionManager()
34+
const { connectionId, selectedConnection, isLoading } = useConnectionManager()
3535
const [tab, setTab] = useState('trends')
3636
const tabRefs = useRef({})
3737

@@ -59,6 +59,22 @@ export default function SlowQueriesSection() {
5959
const logSourceQ = useSlowLogSourceConfig(connectionId)
6060
const hasLogSource = Boolean(logSourceQ.data?.id)
6161

62+
// Wait for connection list to load before rendering anything
63+
if (isLoading) {
64+
return (
65+
<div className={sectionStyles.page}>
66+
<div className={sectionStyles.header}>
67+
<div className={sectionStyles.eyebrow}>Performance</div>
68+
<h1 className={sectionStyles.title}>Slow queries &amp; workload</h1>
69+
</div>
70+
<div className={styles.empty}>
71+
<Loader2 size={20} className={styles.spinIcon} />
72+
Loading connections…
73+
</div>
74+
</div>
75+
)
76+
}
77+
6278
return (
6379
<div className={sectionStyles.page}>
6480
<div className={sectionStyles.header}>
@@ -70,7 +86,8 @@ export default function SlowQueriesSection() {
7086
</p>
7187
</div>
7288

73-
{!connectionId ? (
89+
{/* Either no connection or stale connectionId not in the effective user's list */}
90+
{(!connectionId || !selectedConnection) ? (
7491
<div className={styles.empty}>
7592
Select a database connection to see performance analytics.
7693
</div>

0 commit comments

Comments
 (0)