feat(tracker-web): add dashboard stats retry state
This commit is contained in:
parent
6c49296d40
commit
c1a88a39e2
@ -278,6 +278,38 @@ test.describe('Tracker — Authenticated dashboard', () => {
|
||||
await expect(page.getByText('admin@example.com')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows a retry path when dashboard stats fail', async ({ page }) => {
|
||||
await page.route('**/api/auth/me', (route: Route) =>
|
||||
route.fulfill({
|
||||
json: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' },
|
||||
})
|
||||
);
|
||||
let allowRecovery = false;
|
||||
await page.route('**/api/tracker/**', (route: Route) => {
|
||||
if (route.request().url().includes('/items/stats')) {
|
||||
return !allowRecovery
|
||||
? route.fulfill({ status: 502, json: { error: 'upstream unavailable' } })
|
||||
: route.fulfill({
|
||||
json: {
|
||||
productId: 'tracker-e2e',
|
||||
total: 7,
|
||||
byType: { bug: 1, feature: 5, task: 1 },
|
||||
byStatus: { open: 4, in_progress: 2, done: 1 },
|
||||
byPriority: { critical: 0, high: 2, medium: 4, low: 1 },
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fulfill({ json: {} });
|
||||
});
|
||||
|
||||
await page.addInitScript(() => localStorage.setItem('tracker_token', 'fake-e2e-token'));
|
||||
await page.goto('/dashboard');
|
||||
await expect(page.getByRole('heading', { name: /could not load dashboard/i })).toBeVisible();
|
||||
allowRecovery = true;
|
||||
await page.getByRole('button', { name: /retry/i }).click();
|
||||
await expect(page.getByTestId('bl-number-flow').filter({ hasText: '7' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('renders settings for admin configuration', async ({ page }) => {
|
||||
await page.route('**/api/auth/me', (route: Route) =>
|
||||
route.fulfill({
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
||||
import { KpiCard } from '@bytelyst/data-viz';
|
||||
import { Reveal, NumberFlow } from '@bytelyst/motion';
|
||||
import { Skeleton, toast } from '@/components/ui/Primitives';
|
||||
import { Button, Skeleton, toast } from '@/components/ui/Primitives';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useProduct } from '@/lib/product-context';
|
||||
import { getStats, type TrackerStats } from '@/lib/tracker-client';
|
||||
import { overviewKpis } from '@/lib/overview-charts';
|
||||
|
||||
@ -18,16 +19,31 @@ const OverviewCharts = dynamic(() => import('@/components/overview-charts'), {
|
||||
|
||||
export default function DashboardOverview() {
|
||||
const { token } = useAuth();
|
||||
const { productId } = useProduct();
|
||||
const [stats, setStats] = useState<TrackerStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await getStats();
|
||||
setStats(data);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setStats(null);
|
||||
setError(message);
|
||||
toast({ type: 'error', title: 'Failed to load stats', description: message });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
getStats()
|
||||
.then(setStats)
|
||||
.catch(err =>
|
||||
toast({ type: 'error', title: 'Failed to load stats', description: err.message })
|
||||
);
|
||||
}, [token]);
|
||||
void loadStats();
|
||||
}, [loadStats, productId]);
|
||||
|
||||
const kpis = stats ? overviewKpis(stats) : null;
|
||||
|
||||
@ -36,7 +52,17 @@ export default function DashboardOverview() {
|
||||
<PageHeader title="Dashboard" />
|
||||
<p className="-mt-4 text-sm text-muted-foreground">Overview of all tracked items</p>
|
||||
|
||||
{stats && kpis ? (
|
||||
{error ? (
|
||||
<div className="rounded-2xl border border-destructive/30 bg-card p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-foreground">Could not load dashboard</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{error}. Check platform-service health or retry the request.
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => void loadStats()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
) : stats && kpis ? (
|
||||
<div className="space-y-4">
|
||||
{/* KPI row */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@ -57,11 +83,11 @@ export default function DashboardOverview() {
|
||||
<OverviewCharts stats={stats} />
|
||||
</Reveal>
|
||||
</div>
|
||||
) : (
|
||||
) : loading ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user