From f1a68455e0c81f8d3a9b8f170ca6077c2a4841f5 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sun, 16 Aug 2026 12:35:25 -0700 Subject: [PATCH] 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 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
, not a second
-- every page already renders its own top-level
. --- web/src/lib/api.ts | 30 +- web/src/lib/components/CommandPalette.svelte | 222 +++++++++++++ web/src/lib/components/NavSidebar.svelte | 313 ++++++++++++++++++ web/src/lib/components/ui/Badge.svelte | 42 +++ web/src/lib/components/ui/Button.svelte | 106 ++++++ web/src/lib/components/ui/Card.svelte | 51 +++ web/src/lib/components/ui/EmptyState.svelte | 51 +++ web/src/lib/components/ui/Input.svelte | 57 ++++ web/src/lib/components/ui/Modal.svelte | 101 ++++++ web/src/lib/components/ui/Select.svelte | 60 ++++ .../lib/components/ui/SeverityBadge.svelte | 42 +++ web/src/lib/components/ui/Skeleton.svelte | 34 ++ web/src/lib/components/ui/Table.svelte | 52 +++ web/src/lib/components/ui/Tabs.svelte | 74 +++++ web/src/lib/components/ui/Tooltip.svelte | 46 +++ web/src/lib/components/ui/index.ts | 12 + web/src/routes/+layout.svelte | 80 +++-- web/src/routes/data-sources/+page.svelte | 54 +++ web/src/routes/select-tenant/+page.svelte | 36 +- web/src/routes/settings/+page.svelte | 109 +++++- 20 files changed, 1521 insertions(+), 51 deletions(-) create mode 100644 web/src/lib/components/CommandPalette.svelte create mode 100644 web/src/lib/components/NavSidebar.svelte create mode 100644 web/src/lib/components/ui/Badge.svelte create mode 100644 web/src/lib/components/ui/Button.svelte create mode 100644 web/src/lib/components/ui/Card.svelte create mode 100644 web/src/lib/components/ui/EmptyState.svelte create mode 100644 web/src/lib/components/ui/Input.svelte create mode 100644 web/src/lib/components/ui/Modal.svelte create mode 100644 web/src/lib/components/ui/Select.svelte create mode 100644 web/src/lib/components/ui/SeverityBadge.svelte create mode 100644 web/src/lib/components/ui/Skeleton.svelte create mode 100644 web/src/lib/components/ui/Table.svelte create mode 100644 web/src/lib/components/ui/Tabs.svelte create mode 100644 web/src/lib/components/ui/Tooltip.svelte create mode 100644 web/src/lib/components/ui/index.ts create mode 100644 web/src/routes/data-sources/+page.svelte diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index a3c1dfb..0916e38 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -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 { + 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 { return request(`/dashboards/${id}/export`); } diff --git a/web/src/lib/components/CommandPalette.svelte b/web/src/lib/components/CommandPalette.svelte new file mode 100644 index 0000000..d9dfc2b --- /dev/null +++ b/web/src/lib/components/CommandPalette.svelte @@ -0,0 +1,222 @@ + + + + + +
+ +
    + {#each filtered as item, i (item.id)} +
  • + +
  • + {:else} +
  • No matches
  • + {/each} +
+
+
+ + diff --git a/web/src/lib/components/NavSidebar.svelte b/web/src/lib/components/NavSidebar.svelte new file mode 100644 index 0000000..693b88b --- /dev/null +++ b/web/src/lib/components/NavSidebar.svelte @@ -0,0 +1,313 @@ + + +{#if mobileOpen} + +{/if} + + + + diff --git a/web/src/lib/components/ui/Badge.svelte b/web/src/lib/components/ui/Badge.svelte new file mode 100644 index 0000000..41e72b6 --- /dev/null +++ b/web/src/lib/components/ui/Badge.svelte @@ -0,0 +1,42 @@ + + +{@render children()} + + diff --git a/web/src/lib/components/ui/Button.svelte b/web/src/lib/components/ui/Button.svelte new file mode 100644 index 0000000..eb127fe --- /dev/null +++ b/web/src/lib/components/ui/Button.svelte @@ -0,0 +1,106 @@ + + +{#if href} + + {@render children()} + +{:else} + +{/if} + + diff --git a/web/src/lib/components/ui/Card.svelte b/web/src/lib/components/ui/Card.svelte new file mode 100644 index 0000000..c9999cb --- /dev/null +++ b/web/src/lib/components/ui/Card.svelte @@ -0,0 +1,51 @@ + + +
+ {#if title || actions} +
+ {#if title}

{title}

{/if} + {#if actions}
{@render actions()}
{/if} +
+ {/if} +
+ {@render children()} +
+
+ + diff --git a/web/src/lib/components/ui/EmptyState.svelte b/web/src/lib/components/ui/EmptyState.svelte new file mode 100644 index 0000000..1ee90c9 --- /dev/null +++ b/web/src/lib/components/ui/EmptyState.svelte @@ -0,0 +1,51 @@ + + +
+ +

{title}

+ {#if description}

{description}

{/if} + {#if action}
{@render action()}
{/if} +
+ + diff --git a/web/src/lib/components/ui/Input.svelte b/web/src/lib/components/ui/Input.svelte new file mode 100644 index 0000000..1f5a29f --- /dev/null +++ b/web/src/lib/components/ui/Input.svelte @@ -0,0 +1,57 @@ + + + + + diff --git a/web/src/lib/components/ui/Modal.svelte b/web/src/lib/components/ui/Modal.svelte new file mode 100644 index 0000000..f552337 --- /dev/null +++ b/web/src/lib/components/ui/Modal.svelte @@ -0,0 +1,101 @@ + + + +
+
+ + +
+
+ {@render children()} +
+ {#if footer} +
{@render footer()}
+ {/if} +
+
+ + diff --git a/web/src/lib/components/ui/Select.svelte b/web/src/lib/components/ui/Select.svelte new file mode 100644 index 0000000..440bbf9 --- /dev/null +++ b/web/src/lib/components/ui/Select.svelte @@ -0,0 +1,60 @@ + + +
+ + +
+ + diff --git a/web/src/lib/components/ui/SeverityBadge.svelte b/web/src/lib/components/ui/SeverityBadge.svelte new file mode 100644 index 0000000..8610d83 --- /dev/null +++ b/web/src/lib/components/ui/SeverityBadge.svelte @@ -0,0 +1,42 @@ + + +{severity} + + diff --git a/web/src/lib/components/ui/Skeleton.svelte b/web/src/lib/components/ui/Skeleton.svelte new file mode 100644 index 0000000..4e741cb --- /dev/null +++ b/web/src/lib/components/ui/Skeleton.svelte @@ -0,0 +1,34 @@ + + + + + diff --git a/web/src/lib/components/ui/Table.svelte b/web/src/lib/components/ui/Table.svelte new file mode 100644 index 0000000..eed4266 --- /dev/null +++ b/web/src/lib/components/ui/Table.svelte @@ -0,0 +1,52 @@ + + +
+ + {@render children()} +
+
+ + diff --git a/web/src/lib/components/ui/Tabs.svelte b/web/src/lib/components/ui/Tabs.svelte new file mode 100644 index 0000000..ce63506 --- /dev/null +++ b/web/src/lib/components/ui/Tabs.svelte @@ -0,0 +1,74 @@ + + +
+ {#each tabs as tab (tab.id)} + + {/each} +
+ + diff --git a/web/src/lib/components/ui/Tooltip.svelte b/web/src/lib/components/ui/Tooltip.svelte new file mode 100644 index 0000000..6aed41c --- /dev/null +++ b/web/src/lib/components/ui/Tooltip.svelte @@ -0,0 +1,46 @@ + + + + + {@render children()} + + {text} + + + diff --git a/web/src/lib/components/ui/index.ts b/web/src/lib/components/ui/index.ts new file mode 100644 index 0000000..95247f7 --- /dev/null +++ b/web/src/lib/components/ui/index.ts @@ -0,0 +1,12 @@ +export { default as Button } from './Button.svelte'; +export { default as Input } from './Input.svelte'; +export { default as Select } from './Select.svelte'; +export { default as Badge } from './Badge.svelte'; +export { default as SeverityBadge } from './SeverityBadge.svelte'; +export { default as Table } from './Table.svelte'; +export { default as Card } from './Card.svelte'; +export { default as Modal } from './Modal.svelte'; +export { default as Tooltip } from './Tooltip.svelte'; +export { default as Tabs } from './Tabs.svelte'; +export { default as Skeleton } from './Skeleton.svelte'; +export { default as EmptyState } from './EmptyState.svelte'; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 02999b3..cc1f0fb 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -1,10 +1,12 @@ @@ -12,30 +14,62 @@ - +
+ (paletteOpen = true)} + mobileOpen={mobileNavOpen} + onCloseMobile={() => (mobileNavOpen = false)} + /> +
+ +
+ {@render children()} +
+
+
-{@render children()} + diff --git a/web/src/routes/data-sources/+page.svelte b/web/src/routes/data-sources/+page.svelte new file mode 100644 index 0000000..9a06f75 --- /dev/null +++ b/web/src/routes/data-sources/+page.svelte @@ -0,0 +1,54 @@ + + +
+

Data Sources

+

+ Every tenant has exactly one data source today — its own ClickHouse database and Tantivy + index, provisioned together when the tenant is created. There's nothing to configure yet. +

+ + + {#if enterpriseAuthBase} +

+ Your queries run against your tenant's dedicated ClickHouse database and Tantivy index — + never a shared one. See Settings for SSO and tenant configuration. +

+ {:else} +

+ This deployment isn't running in multi-tenant mode, so there's one data source for the + whole instance — the default ClickHouse database and Tantivy index every query already + runs against. +

+ {/if} +
+ +

+ Multiple data sources per tenant — a second ClickHouse cluster, a read replica, an external + source — is real future work, not something this page is hiding. The + data_sources table this reads from was already built with that in mind (see + /docs/phase-4-rbac-design.md). +

+
+ + diff --git a/web/src/routes/select-tenant/+page.svelte b/web/src/routes/select-tenant/+page.svelte index bd755c9..629e8ef 100644 --- a/web/src/routes/select-tenant/+page.svelte +++ b/web/src/routes/select-tenant/+page.svelte @@ -83,10 +83,8 @@ diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index fc580a6..ebdabf8 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -1,5 +1,7 @@

Settings

+
+

Appearance

+
+ {#each themeOptions as opt (opt.value)} + + {/each} +
+
+ {#each densityOptions as opt (opt.value)} + + {/each} +
+
+

Core

Single-tenant deployment settings live here. Nothing configurable yet.

{#if loading} -

Loading…

+

Loading…

{:else if features.sso_configured}

Single sign-on

@@ -48,19 +92,66 @@