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();
|
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 }) => {
|
test('renders settings for admin configuration', async ({ page }) => {
|
||||||
await page.route('**/api/auth/me', (route: Route) =>
|
await page.route('**/api/auth/me', (route: Route) =>
|
||||||
route.fulfill({
|
route.fulfill({
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
|
||||||
import { KpiCard } from '@bytelyst/data-viz';
|
import { KpiCard } from '@bytelyst/data-viz';
|
||||||
import { Reveal, NumberFlow } from '@bytelyst/motion';
|
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 { useAuth } from '@/lib/auth-context';
|
||||||
|
import { useProduct } from '@/lib/product-context';
|
||||||
import { getStats, type TrackerStats } from '@/lib/tracker-client';
|
import { getStats, type TrackerStats } from '@/lib/tracker-client';
|
||||||
import { overviewKpis } from '@/lib/overview-charts';
|
import { overviewKpis } from '@/lib/overview-charts';
|
||||||
|
|
||||||
@ -18,16 +19,31 @@ const OverviewCharts = dynamic(() => import('@/components/overview-charts'), {
|
|||||||
|
|
||||||
export default function DashboardOverview() {
|
export default function DashboardOverview() {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
const { productId } = useProduct();
|
||||||
const [stats, setStats] = useState<TrackerStats | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!token) return;
|
void loadStats();
|
||||||
getStats()
|
}, [loadStats, productId]);
|
||||||
.then(setStats)
|
|
||||||
.catch(err =>
|
|
||||||
toast({ type: 'error', title: 'Failed to load stats', description: err.message })
|
|
||||||
);
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
const kpis = stats ? overviewKpis(stats) : null;
|
const kpis = stats ? overviewKpis(stats) : null;
|
||||||
|
|
||||||
@ -36,7 +52,17 @@ export default function DashboardOverview() {
|
|||||||
<PageHeader title="Dashboard" />
|
<PageHeader title="Dashboard" />
|
||||||
<p className="-mt-4 text-sm text-muted-foreground">Overview of all tracked items</p>
|
<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">
|
<div className="space-y-4">
|
||||||
{/* KPI row */}
|
{/* KPI row */}
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
@ -57,11 +83,11 @@ export default function DashboardOverview() {
|
|||||||
<OverviewCharts stats={stats} />
|
<OverviewCharts stats={stats} />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : loading ? (
|
||||||
<div className="flex justify-center py-10">
|
<div className="flex justify-center py-10">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user