Skip to content

Commit 668fcbb

Browse files
authored
feat(ui): show global banner when API server is unreachable (#97)
.
1 parent 955f321 commit 668fcbb

4 files changed

Lines changed: 63 additions & 1 deletion

File tree

ui/src/api/api-status-events.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type Listener = (isDown: boolean) => void
2+
3+
let listener: Listener | null = null
4+
5+
export function reportApiDown() {
6+
listener?.(true)
7+
}
8+
9+
export function reportApiUp() {
10+
listener?.(false)
11+
}
12+
13+
export function onApiStatusChange(fn: Listener): () => void {
14+
listener = fn
15+
return () => {
16+
if (listener === fn) listener = null
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useState, useEffect } from 'react'
2+
import { WifiOff } from 'lucide-react'
3+
import { useAuth } from '@/hooks/useAuth'
4+
import { onApiStatusChange } from '@/api/api-status-events'
5+
6+
export function ApiDownBanner() {
7+
const { isApiEnabled } = useAuth()
8+
const [isDown, setIsDown] = useState(false)
9+
10+
useEffect(() => onApiStatusChange(setIsDown), [])
11+
12+
if (!isApiEnabled || !isDown) return null
13+
14+
return (
15+
<div className="border-b border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950">
16+
<div className="mx-auto flex max-w-7xl items-center justify-center gap-3 px-4 py-2 text-sm/5 text-amber-800 dark:text-amber-200">
17+
<WifiOff className="size-4 shrink-0" />
18+
<span>
19+
Unable to reach the API server. Data may be stale. The banner will
20+
dismiss automatically once connectivity is restored.
21+
</span>
22+
</div>
23+
</div>
24+
)
25+
}

ui/src/components/layout/RootLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
22
import { Outlet, useNavigate, useLocation } from '@tanstack/react-router'
33
import { useAuth } from '@/hooks/useAuth'
44
import { Header } from '@/components/layout/Header'
5+
import { ApiDownBanner } from '@/components/layout/ApiDownBanner'
56
import { Footer } from '@/components/layout/Footer'
67

78
export function RootLayout() {
@@ -24,6 +25,7 @@ export function RootLayout() {
2425
return (
2526
<div className="flex min-h-dvh flex-col bg-gray-50 dark:bg-gray-900">
2627
{!requiresLogin && <Header />}
28+
<ApiDownBanner />
2729
<main className="mx-auto w-full max-w-7xl flex-1 px-4 py-8">
2830
<Outlet />
2931
</main>

ui/src/main.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { StrictMode } from 'react'
22
import { createRoot } from 'react-dom/client'
3-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3+
import { QueryCache, MutationCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
44
import { RouterProvider } from '@tanstack/react-router'
55
import { AuthProvider } from '@/contexts/auth'
66
import { loadRuntimeConfig } from '@/config/runtime'
7+
import { reportApiDown, reportApiUp } from '@/api/api-status-events'
78
import { router } from './router'
89
import './index.css'
910

@@ -34,8 +35,24 @@ function handleOAuthCallback(): boolean {
3435
return true
3536
}
3637

38+
function isNetworkError(error: unknown): boolean {
39+
return error instanceof TypeError
40+
}
41+
3742
if (!handleOAuthCallback()) {
43+
const queryCache = new QueryCache({
44+
onError: (error) => { if (isNetworkError(error)) reportApiDown() },
45+
onSuccess: () => reportApiUp(),
46+
})
47+
48+
const mutationCache = new MutationCache({
49+
onError: (error) => { if (isNetworkError(error)) reportApiDown() },
50+
onSuccess: () => reportApiUp(),
51+
})
52+
3853
const queryClient = new QueryClient({
54+
queryCache,
55+
mutationCache,
3956
defaultOptions: {
4057
queries: {
4158
staleTime: 30_000,

0 commit comments

Comments
 (0)