Build the ui/ component library and persistent app shell

Button, Input, Select, Badge, SeverityBadge, Table, Card, Modal,
Tooltip, Tabs, Skeleton, EmptyState -- a shared library so pages stop
hand-rolling markup per page (web/src/lib/components/ui, barrel export
in index.ts). Modal and CommandPalette are built on native <dialog>
for a real focus trap, Escape-to-close, and top-layer stacking instead
of hand-rolling those. Tabs uses roving tabindex with arrow-key nav.

NavSidebar replaces the old top-nav with a persistent sidebar
(Search/Dashboards/Alerts/Data Sources/Settings), a live tenant
indicator (api.ts's new getCurrentSession(), a client for the
already-existing POST /internal/authorize -- zero new backend surface),
theme/density quick-toggles, and a command-palette hint. Collapses to
an off-canvas drawer under 860px. CommandPalette (Cmd/Ctrl+K) indexes
the five static destinations plus live-fetched dashboards/alert rules.

Data Sources is a new, honestly-scoped placeholder page (one data
source per tenant today, no UI needed yet). Settings and Select-tenant
are re-tokened onto the new component library. +layout.svelte's content
wrapper is a plain <div>, not a second <main> -- every page already
renders its own top-level <main>.
This commit is contained in:
2026-08-16 12:35:25 -07:00
parent a6153c5f90
commit f1a68455e0
20 changed files with 1521 additions and 51 deletions
+29 -1
View File
@@ -16,7 +16,7 @@ export type Language = '' | 'sql' | 'spl';
export type QueryResult = { columns: string[]; rows: unknown[][] };
export type VizType = 'table' | 'line' | 'bar' | 'single_stat' | 'top_n';
export type VizType = 'table' | 'line' | 'bar' | 'single_stat' | 'top_n' | 'heatmap';
export type Panel = {
id: string;
@@ -203,6 +203,34 @@ export function selectTenant(tenantId: string): Promise<{ redirect_url: string }
});
}
export type CurrentSession = { tenant_id: string; user_id: string; role: string };
// getCurrentSession backs the sidebar's tenant indicator (Phase 5).
// POST /internal/authorize already exists (api/authz.HTTPAuthorizer and
// alerting's queryclient call it the same way) and is already reachable
// from the browser -- enterprise-auth's whole mux is wrapped in
// WithCredentialedCORS, not just the tenant-picker routes -- so this is
// a client for an existing endpoint, not a new one. Returns null for
// every "no tenant to show" case alike (no enterprise-auth configured,
// not logged in, or a plain single-tenant deployment) rather than
// throwing -- same "absence is a normal deployment shape" posture
// getAuthFeatures already uses, so the sidebar can just render nothing
// instead of branching on error types.
export async function getCurrentSession(): Promise<CurrentSession | null> {
if (!enterpriseAuthBase) return null;
try {
const res = await fetch(`${enterpriseAuthBase}/internal/authorize`, {
method: 'POST',
credentials: 'include'
});
if (!res.ok) return null;
const session = (await res.json()) as CurrentSession;
return session.tenant_id ? session : null;
} catch {
return null;
}
}
export function exportDashboard(id: string): Promise<Dashboard> {
return request(`/dashboards/${id}/export`);
}