feat(tracker-web): add admin settings page
This commit is contained in:
parent
3a6ed3a5f8
commit
ccfbfd194a
@ -275,6 +275,25 @@ test.describe('Tracker — Authenticated dashboard', () => {
|
|||||||
await expect(page.getByText('42')).toBeVisible();
|
await expect(page.getByText('42')).toBeVisible();
|
||||||
await expect(page.getByText('admin@example.com')).toBeVisible();
|
await expect(page.getByText('admin@example.com')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders settings for admin configuration', async ({ page }) => {
|
||||||
|
await page.route('**/api/auth/me', (route: Route) =>
|
||||||
|
route.fulfill({
|
||||||
|
json: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.addInitScript(() => localStorage.setItem('tracker_token', 'fake-e2e-token'));
|
||||||
|
await page.goto('/dashboard/settings');
|
||||||
|
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Product context' })).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Default product ID')).toBeVisible();
|
||||||
|
await expect(page.getByText('/api/tracker/[...path]')).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: /open public roadmap/i })).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/roadmap'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Public roadmap (mocked) ─────────────────────────────────────────
|
// ── Public roadmap (mocked) ─────────────────────────────────────────
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const NAV_ITEMS = [
|
|||||||
{ href: '/dashboard/items', label: 'Items' },
|
{ href: '/dashboard/items', label: 'Items' },
|
||||||
{ href: '/dashboard/board', label: 'Board' },
|
{ href: '/dashboard/board', label: 'Board' },
|
||||||
{ href: '/dashboard/fleet', label: 'Fleet' },
|
{ href: '/dashboard/fleet', label: 'Fleet' },
|
||||||
|
{ href: '/dashboard/settings', label: 'Settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Open the ⌘K command palette by replaying the global hotkey. */
|
/** Open the ⌘K command palette by replaying the global hotkey. */
|
||||||
|
|||||||
109
dashboards/tracker-web/src/app/dashboard/settings/page.tsx
Normal file
109
dashboards/tracker-web/src/app/dashboard/settings/page.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const DEFAULT_PRODUCT = 'tracker';
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const [productId, setProductId] = useState(DEFAULT_PRODUCT);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selected = localStorage.getItem('tracker_selected_product');
|
||||||
|
if (selected) setProductId(selected);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function saveProductContext() {
|
||||||
|
localStorage.setItem('tracker_selected_product', productId.trim() || DEFAULT_PRODUCT);
|
||||||
|
window.dispatchEvent(new Event('tracker:product-changed'));
|
||||||
|
setSaved(true);
|
||||||
|
window.setTimeout(() => setSaved(false), 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<header>
|
||||||
|
<p className="text-sm font-medium uppercase tracking-[0.2em] text-muted-foreground">
|
||||||
|
Admin
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-2 text-3xl font-bold tracking-tight">Settings</h1>
|
||||||
|
<p className="mt-2 max-w-3xl text-muted-foreground">
|
||||||
|
Configure the tracker dashboard context, public roadmap links, and integration surfaces
|
||||||
|
used by humans, agents, and external systems.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<div className="rounded-2xl border bg-card p-6 shadow-sm">
|
||||||
|
<h2 className="text-xl font-semibold">Product context</h2>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Tracker is product-aware. This value is sent as the selected product context for
|
||||||
|
internal tracker calls.
|
||||||
|
</p>
|
||||||
|
<label htmlFor="default-product-id" className="mt-5 block text-sm font-medium">
|
||||||
|
Default product ID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="default-product-id"
|
||||||
|
value={productId}
|
||||||
|
onChange={event => setProductId(event.target.value)}
|
||||||
|
className="mt-2 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||||
|
placeholder="tracker"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveProductContext}
|
||||||
|
className="mt-4 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||||
|
>
|
||||||
|
Save product context
|
||||||
|
</button>
|
||||||
|
{saved ? <p className="mt-3 text-sm text-green-500">Saved product context.</p> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border bg-card p-6 shadow-sm">
|
||||||
|
<h2 className="text-xl font-semibold">Public roadmap</h2>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Share the roadmap and submission status pages with public users without requiring a
|
||||||
|
login.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 space-y-3 text-sm">
|
||||||
|
<div className="rounded-lg bg-muted p-3">
|
||||||
|
<span className="font-medium">Roadmap:</span> /roadmap
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-muted p-3">
|
||||||
|
<span className="font-medium">Submission status:</span> /status/<itemId>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/roadmap"
|
||||||
|
className="mt-4 inline-flex rounded-lg border px-4 py-2 text-sm font-semibold hover:bg-muted"
|
||||||
|
>
|
||||||
|
Open public roadmap
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-2xl border bg-card p-6 shadow-sm">
|
||||||
|
<h2 className="text-xl font-semibold">Integration surfaces</h2>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
These are the web-facing route groups that connect the dashboard to platform-service.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 grid gap-3 md:grid-cols-2">
|
||||||
|
{[
|
||||||
|
['/api/tracker/[...path]', 'Authenticated tracker and public roadmap proxy'],
|
||||||
|
['/api/fleet/[...path]', 'Agent/fleet control-plane proxy'],
|
||||||
|
['/api/auth/*', 'Login, MFA, OAuth, and session lookup'],
|
||||||
|
['/api/telemetry/ingest', 'Client telemetry ingestion'],
|
||||||
|
['/api/health', 'Readiness and platform-service dependency probe'],
|
||||||
|
].map(([route, description]) => (
|
||||||
|
<div key={route} className="rounded-xl border bg-background p-4">
|
||||||
|
<code className="text-sm font-semibold">{route}</code>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user