diff --git a/src/components/sections/DashboardsSection.jsx b/src/components/sections/DashboardsSection.jsx
index c2476c6..efc770c 100644
--- a/src/components/sections/DashboardsSection.jsx
+++ b/src/components/sections/DashboardsSection.jsx
@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react'
-import { LayoutDashboard } from 'lucide-react'
+import { LayoutDashboard, Loader2 } from 'lucide-react'
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import { useSetImmersive } from '@/lib/stores/useNavStore'
import DashboardsHome from './DashboardsHome'
@@ -10,7 +10,7 @@ import emptyStyles from './SectionEmpty.module.css'
// dashboard, a focused full-bleed builder workspace (sidebar hidden via the
// nav store's immersive flag).
export default function DashboardsSection() {
- const { connectionId } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading } = useConnectionManager()
const setImmersive = useSetImmersive()
const [open_, setOpen] = useState(null) // null = gallery; 'new' | dashboard object = workspace
@@ -22,7 +22,17 @@ export default function DashboardsSection() {
// Safety: never leave the app in immersive mode when this section unmounts.
useEffect(() => () => setImmersive(false), [setImmersive])
- if (!connectionId) {
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ // Either no connection or stale connectionId not in the effective user's list
+ if (!connectionId || !selectedConnection) {
return (
diff --git a/src/components/sections/DigestSection.jsx b/src/components/sections/DigestSection.jsx
index e8f89a5..8bdcaf4 100644
--- a/src/components/sections/DigestSection.jsx
+++ b/src/components/sections/DigestSection.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
-import { Newspaper, RefreshCw, Settings, Check, Clock, AlertCircle, Zap } from 'lucide-react'
+import { Loader2, Newspaper, RefreshCw, Settings, Check, Clock, AlertCircle, Zap } from 'lucide-react'
import { slackDigestAPI, digestPreferencesAPI } from '@/lib/api/client'
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import DigestPreferencesPanel from './DigestPreferencesPanel'
@@ -165,7 +165,7 @@ function DigestSection({ section }) {
const DIGEST_PREFS_AUTOPEN_KEY = 'deepsql.digestPrefs.autoOpened.v1'
export default function DigestFeedSection() {
- const { connectionId, selectedConnection } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading: connectionsLoading } = useConnectionManager()
const [digests, setDigests] = useState([])
const [loading, setLoading] = useState(false)
const [triggering, setTriggering] = useState(false)
@@ -257,6 +257,26 @@ export default function DigestFeedSection() {
}
}
+ // Wait for connection list to load first
+ if (connectionsLoading) {
+ return (
+
+
+
+
+
+ Loading connections…
+
+
+
+ )
+ }
+
return (
{/* Top bar */}
@@ -309,7 +329,8 @@ export default function DigestFeedSection() {
)}
- {!error && !loading && !connectionId && (
+ {/* Either no connection or stale connectionId not in the effective user's list */}
+ {!error && !loading && (!connectionId || !selectedConnection) && (
No connection selected
@@ -317,7 +338,7 @@ export default function DigestFeedSection() {
)}
- {!error && !loading && !!connectionId && digests.length === 0 && (
+ {!error && !loading && !!connectionId && !!selectedConnection && digests.length === 0 && (
No digests yet
diff --git a/src/components/sections/EditorSection.jsx b/src/components/sections/EditorSection.jsx
index 9229c20..a3e1fee 100644
--- a/src/components/sections/EditorSection.jsx
+++ b/src/components/sections/EditorSection.jsx
@@ -1,12 +1,22 @@
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import SqlRunnerTab from '@/components/tabs/Core/SqlRunnerTab'
-import { Code2 } from 'lucide-react'
+import { Code2, Loader2 } from 'lucide-react'
import styles from './SectionEmpty.module.css'
export default function EditorSection() {
- const { connectionId } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading } = useConnectionManager()
- if (!connectionId) {
+ if (isLoading) {
+ return (
+
+
+
Loading connections…
+
+ )
+ }
+
+ // Either no connection selected, or stale connectionId not in the current user's list
+ if (!connectionId || !selectedConnection) {
return (
diff --git a/src/components/sections/MonitorSection.jsx b/src/components/sections/MonitorSection.jsx
index 61022fa..6fe8685 100644
--- a/src/components/sections/MonitorSection.jsx
+++ b/src/components/sections/MonitorSection.jsx
@@ -1,12 +1,12 @@
import { useEffect, useState } from 'react'
-import { BarChart2 } from 'lucide-react'
+import { BarChart2, Loader2 } from 'lucide-react'
import { slowQueriesAPI } from '@/lib/api/client'
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import AnalyticsTab from '@/components/tabs/Monitoring/AnalyticsTab'
import styles from './SectionEmpty.module.css'
export default function MonitorSection() {
- const { connectionId } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading: connectionsLoading } = useConnectionManager()
const [hasData, setHasData] = useState(null) // null = loading
const [loading, setLoading] = useState(true)
@@ -27,7 +27,18 @@ export default function MonitorSection() {
.finally(() => setLoading(false))
}, [connectionId])
- if (!connectionId) {
+ // Wait for connection list to load first
+ if (connectionsLoading) {
+ return (
+
+
+
Loading connections…
+
+ )
+ }
+
+ // Either no connection or stale connectionId not in the effective user's list
+ if (!connectionId || !selectedConnection) {
return (
diff --git a/src/components/sections/SchemaDocsSection.jsx b/src/components/sections/SchemaDocsSection.jsx
index b54deba..19f97a0 100644
--- a/src/components/sections/SchemaDocsSection.jsx
+++ b/src/components/sections/SchemaDocsSection.jsx
@@ -1,4 +1,4 @@
-import { FileText } from 'lucide-react'
+import { FileText, Loader2 } from 'lucide-react'
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import { useSetActiveSection } from '@/lib/stores/useNavStore'
import { useCompanyKnowledgeStore } from '@/lib/stores/useCompanyKnowledgeStore'
@@ -7,11 +7,21 @@ import styles from './SectionEmpty.module.css'
import workspaceStyles from './TopLevelSection.module.css'
export default function SchemaDocsSection() {
- const { connectionId } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading } = useConnectionManager()
const setActiveSection = useSetActiveSection()
const setLinkedFilters = useCompanyKnowledgeStore((state) => state.setLinkedFilters)
- if (!connectionId) {
+ if (isLoading) {
+ return (
+
+
+
Loading connections…
+
+ )
+ }
+
+ // Either no connection or stale connectionId not in the effective user's list
+ if (!connectionId || !selectedConnection) {
return (
diff --git a/src/components/sections/SchemaSection.jsx b/src/components/sections/SchemaSection.jsx
index ea310fb..81488d7 100644
--- a/src/components/sections/SchemaSection.jsx
+++ b/src/components/sections/SchemaSection.jsx
@@ -1,12 +1,22 @@
-import { Network } from 'lucide-react'
+import { Network, Loader2 } from 'lucide-react'
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import BrainWorkspace from '@/components/tabs/Brain/BrainWorkspace'
import styles from './SectionEmpty.module.css'
export default function SchemaSection() {
- const { connectionId } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading } = useConnectionManager()
- if (!connectionId) {
+ if (isLoading) {
+ return (
+
+
+
Loading connections…
+
+ )
+ }
+
+ // Either no connection or stale connectionId not in the effective user's list
+ if (!connectionId || !selectedConnection) {
return (
diff --git a/src/components/sections/SectionEmpty.module.css b/src/components/sections/SectionEmpty.module.css
index 6c9435b..db096c1 100644
--- a/src/components/sections/SectionEmpty.module.css
+++ b/src/components/sections/SectionEmpty.module.css
@@ -72,6 +72,14 @@
background: #f9fafb;
}
+.spin {
+ animation: sectionSpin 1s linear infinite;
+}
+
+@keyframes sectionSpin {
+ to { transform: rotate(360deg); }
+}
+
.pills {
display: flex;
gap: 8px;
diff --git a/src/components/sections/SlowQueriesSection.jsx b/src/components/sections/SlowQueriesSection.jsx
index 44af4c0..7cfbe58 100644
--- a/src/components/sections/SlowQueriesSection.jsx
+++ b/src/components/sections/SlowQueriesSection.jsx
@@ -1,5 +1,5 @@
import { useRef, useState } from 'react'
-import { Activity, FileText, LineChart, Settings, Users } from 'lucide-react'
+import { Activity, FileText, LineChart, Loader2, Settings, Users } from 'lucide-react'
import { useConnectionManager } from '@/lib/hooks/useConnectionManager'
import { useSlowLogSourceConfig } from '@/lib/hooks/queries'
import QueryTrendsTab from '@/components/tabs/Performance/QueryTrendsTab'
@@ -31,7 +31,7 @@ const LOG_SOURCE_HELP = {
* is a single empty state whose CTA opens SlowQuerySourceModal.
*/
export default function SlowQueriesSection() {
- const { connectionId, selectedConnection } = useConnectionManager()
+ const { connectionId, selectedConnection, isLoading } = useConnectionManager()
const [tab, setTab] = useState('trends')
const tabRefs = useRef({})
@@ -59,6 +59,22 @@ export default function SlowQueriesSection() {
const logSourceQ = useSlowLogSourceConfig(connectionId)
const hasLogSource = Boolean(logSourceQ.data?.id)
+ // Wait for connection list to load before rendering anything
+ if (isLoading) {
+ return (
+
+
+
Performance
+
Slow queries & workload
+
+
+
+ Loading connections…
+
+
+ )
+ }
+
return (
@@ -70,7 +86,8 @@ export default function SlowQueriesSection() {
- {!connectionId ? (
+ {/* Either no connection or stale connectionId not in the effective user's list */}
+ {(!connectionId || !selectedConnection) ? (
Select a database connection to see performance analytics.
diff --git a/src/components/sections/SlowQueriesSection.module.css b/src/components/sections/SlowQueriesSection.module.css
index 3b356ad..3f89f5f 100644
--- a/src/components/sections/SlowQueriesSection.module.css
+++ b/src/components/sections/SlowQueriesSection.module.css
@@ -141,3 +141,14 @@
border-color: #d1d5db;
color: #111827;
}
+
+.spinIcon {
+ animation: spin 1s linear infinite;
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 8px;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
diff --git a/src/lib/hooks/useConnectionManager.js b/src/lib/hooks/useConnectionManager.js
index 0f497a0..59a76e2 100644
--- a/src/lib/hooks/useConnectionManager.js
+++ b/src/lib/hooks/useConnectionManager.js
@@ -56,6 +56,11 @@ export function useConnectionManager() {
// selected and a pin that only ran on a blank slate would never actually be the
// default — which is the whole feature. It applies once per page load
// (`pinAppliedThisLoad`), so switching connections mid-session still sticks.
+ //
+ // Critical: if `connectionId` is set but NOT in the connections list (e.g. after
+ // "View as" switches to a user without access to that connection), clear it and
+ // select a valid one. Otherwise the UI keeps a stale connection the effective user
+ // cannot access, and chat/agent calls 403.
useEffect(() => {
if (connections.length === 0) return
@@ -69,12 +74,23 @@ export function useConnectionManager() {
}
}
- if (connectionId) return
+ // Validate the selected connection is actually in the current user's list.
+ // When impersonation changes, the connection list is for the target user, but
+ // connectionId might still be a connection the admin had selected. Clear it
+ // so the user must pick from their own list (or gets auto-selected below).
+ if (connectionId) {
+ const stillValid = connections.some((c) => c.id === connectionId)
+ if (stillValid) return
+ // The selected connection is not in the list — clear it and fall through
+ // to auto-select from the valid options.
+ localStorage.removeItem('selectedConnectionId')
+ setConnectionId(null)
+ }
const savedId = localStorage.getItem('selectedConnectionId')
const saved = savedId ? connections.find((c) => c.id === savedId) : null
selectConnection((pinned || saved || connections[0]).id)
- }, [connections, connectionId, selectConnection])
+ }, [connections, connectionId, selectConnection, setConnectionId])
const changeConnection = useCallback(
(connId) => {