feat(admin-web): adopt @bytelyst/dashboard-components page chrome (UX-4)
- error.tsx -> ErrorPage (keep telemetry on mount; retry wired to Next reset). - (dashboard)/loading.tsx -> LoadingSpinner inside the existing skeleton. - not-found.tsx already used NotFoundPage (confirmed, unchanged). - dashboard overview page.tsx header -> PageHeader (Refresh as actions; the subtitle/last-updated line preserved directly below). Rich detail headers (e.g. users/[id] back-button + plan/status badges) left bespoke on purpose: PageHeader has no subtitle/badge slot, so forcing it would regress them (additive-only rule). dashboard-components reads --color-* which admin maps via @theme inline, so it themes in light + dark. Verify: typecheck+lint+build green (123 routes); vitest 20 files / 168 tests (+3 happy-dom chrome render tests); format:check no new failures; e2e 11 passed / 80 failed (unchanged vs UX-1 baseline — environmental). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
997002e913
commit
94ef3f1c20
@ -152,9 +152,17 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa
|
||||
happy-dom ⌘K/Ctrl-K interaction test (`react-dom/client` + `act`, no new deps; react deduped).
|
||||
test **19 files / 165 tests** (+6); typecheck+lint+build green (123 routes); format:check no new
|
||||
failures; e2e unchanged.
|
||||
- [ ] **UX-4 — Page chrome:** use `@bytelyst/dashboard-components` (`PageHeader`/`ErrorPage`/
|
||||
- [x] **UX-4 — Page chrome:** use `@bytelyst/dashboard-components` (`PageHeader`/`ErrorPage`/
|
||||
`NotFoundPage`/`LoadingSpinner`) on `error.tsx`/`not-found.tsx`/`loading.tsx` + a few high-traffic
|
||||
surfaces where chrome is bespoke. Keep it additive.
|
||||
— **DONE** `<SHA-UX4>` · `error.tsx`→`ErrorPage` (telemetry kept; retry→`reset`);
|
||||
`loading.tsx`→`LoadingSpinner` inside the existing skeleton; `not-found.tsx` already used
|
||||
`NotFoundPage` (confirmed); dashboard overview `page.tsx` header→`PageHeader` (Refresh as
|
||||
`actions`, subtitle preserved below). Rich detail headers (users/[id] back-button + badges)
|
||||
intentionally left bespoke — `PageHeader` has no subtitle/badge slot, so forcing it would
|
||||
regress them (additive rule). dashboard-components reads `--color-*` (admin `@theme inline`),
|
||||
so themes correctly. test **20 files / 168 tests** (+3, happy-dom render of error/not-found/
|
||||
loading chrome); typecheck+lint+build green; format:check no new failures; e2e unchanged.
|
||||
- [ ] **UX-5 — Motion:** add `@bytelyst/motion`; subtle `Reveal`/`Stagger` on the overview dashboard
|
||||
cards + key tables; respect `prefers-reduced-motion`. (Note tracker-web's lesson: do NOT apply
|
||||
motion to surfaces an offline axe gate scans synchronously if transient opacity trips contrast.)
|
||||
@ -173,7 +181,7 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa
|
||||
|
||||
```
|
||||
Setup : UX-1 ✅
|
||||
Adopt : UX-2 ✅ UX-3 ✅ UX-4 ⬜ UX-5 ⬜ UX-6 ⬜(cond)
|
||||
Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ⬜ UX-6 ⬜(cond)
|
||||
Cross : CC.1 ⬜ CC.2 ⬜ CC.3 ⬜ CC.4 ⬜ CC.5 ⬜ CC.6 ⬜
|
||||
```
|
||||
|
||||
|
||||
66
dashboards/admin-web/src/__tests__/page-chrome.test.tsx
Normal file
66
dashboards/admin-web/src/__tests__/page-chrome.test.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
|
||||
// error.tsx fires telemetry on mount — stub it so the chrome can render in isolation.
|
||||
vi.mock('@/lib/telemetry', () => ({ trackEvent: vi.fn() }));
|
||||
|
||||
import GlobalError from '@/app/error';
|
||||
import NotFound from '@/app/not-found';
|
||||
import DashboardLoading from '@/app/(dashboard)/loading';
|
||||
|
||||
/**
|
||||
* UX-4 — page-chrome adoption guard. Verifies admin's error / not-found /
|
||||
* loading surfaces render the shared `@bytelyst/dashboard-components` chrome
|
||||
* and that the error retry handler is wired to Next's `reset`.
|
||||
*/
|
||||
describe('page chrome (dashboard-components)', () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT =
|
||||
true;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('error.tsx renders ErrorPage with the message and a working retry', () => {
|
||||
const reset = vi.fn();
|
||||
act(() => {
|
||||
root.render(
|
||||
<GlobalError error={Object.assign(new Error('kaboom'), { digest: 'd1' })} reset={reset} />
|
||||
);
|
||||
});
|
||||
expect(container.textContent).toContain('Something went wrong');
|
||||
expect(container.textContent).toContain('kaboom');
|
||||
|
||||
const retry = Array.from(container.querySelectorAll('button')).find(b =>
|
||||
/try again|retry/i.test(b.textContent ?? '')
|
||||
);
|
||||
expect(retry).toBeTruthy();
|
||||
act(() => retry!.dispatchEvent(new MouseEvent('click', { bubbles: true })));
|
||||
expect(reset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('not-found.tsx renders the NotFoundPage chrome', () => {
|
||||
act(() => {
|
||||
root.render(<NotFound />);
|
||||
});
|
||||
expect(container.querySelector('a[href="/"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('loading.tsx renders an accessible LoadingSpinner', () => {
|
||||
act(() => {
|
||||
root.render(<DashboardLoading />);
|
||||
});
|
||||
expect(container.querySelector('[role="status"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { LoadingSpinner } from '@bytelyst/dashboard-components';
|
||||
|
||||
export default function DashboardLoading() {
|
||||
return (
|
||||
@ -29,7 +29,7 @@ export default function DashboardLoading() {
|
||||
<div key={i} className="rounded-xl border bg-card p-6">
|
||||
<div className="mb-4 h-5 w-36 animate-pulse rounded bg-muted" />
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<LoadingSpinner size="md" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
type ApiUsageRecord,
|
||||
type RevenueAnalytics,
|
||||
} from '@/lib/api';
|
||||
import { PageHeader } from '@bytelyst/dashboard-components';
|
||||
import { AreaChart, BarChart } from '@/components/charts';
|
||||
import { seriesValues, dateBars } from '@/lib/chart-data';
|
||||
|
||||
@ -275,22 +276,30 @@ export default function DashboardPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Platform overview and key metrics
|
||||
{lastUpdated && (
|
||||
<span className="ml-2 text-xs">
|
||||
· Updated {lastUpdated.toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => fetchData(true)} disabled={refreshing}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
className="!mb-2"
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fetchData(true)}
|
||||
disabled={refreshing}
|
||||
>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground">
|
||||
Platform overview and key metrics
|
||||
{lastUpdated && (
|
||||
<span className="ml-2 text-xs">
|
||||
· Updated {lastUpdated.toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorPage } from '@bytelyst/dashboard-components';
|
||||
import { trackEvent } from '@/lib/telemetry';
|
||||
|
||||
export default function GlobalError({
|
||||
@ -19,19 +20,11 @@ export default function GlobalError({
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<div className="mb-4 text-5xl">⚠</div>
|
||||
<h2 className="mb-2 text-xl font-semibold">Something went wrong</h2>
|
||||
<p className="mb-6 text-sm text-muted-foreground">
|
||||
{error.message || 'An unexpected error occurred.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
<ErrorPage
|
||||
title="Something went wrong"
|
||||
message={error.message || 'An unexpected error occurred.'}
|
||||
onRetry={reset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ export default defineConfig({
|
||||
// The real Next/webpack build already dedupes these to admin-web's React.
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/@bytelyst\/(charts|data-viz|command-palette)/],
|
||||
inline: [/@bytelyst\/(charts|data-viz|command-palette|dashboard-components)/],
|
||||
},
|
||||
},
|
||||
coverage: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user