From 45e0865a0cc63451f02ec214466aeca7c997201e Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sun, 16 Aug 2026 12:35:49 -0700 Subject: [PATCH] Rebuild dashboard panels on the new chart layer Drag-and-drop grid stays on GridStack (already a Phase 3 dependency -- no new library needed). PanelEditor.svelte (a Modal) replaces the old inline add-panel form: a debounced live preview reuses PanelViz directly, so the preview is pixel-identical to what renders on save instead of drifting from a separate preview renderer. Dashboards list and detail pages get EmptyState/Skeleton for empty/loading states instead of a blank panel or a raw error string, and panel titles are now clickable buttons that open the editor. --- web/src/lib/components/PanelEditor.svelte | 310 ++++++++++++++++++++ web/src/routes/dashboards/+page.svelte | 70 +++-- web/src/routes/dashboards/[id]/+page.svelte | 225 +++++++------- 3 files changed, 477 insertions(+), 128 deletions(-) create mode 100644 web/src/lib/components/PanelEditor.svelte diff --git a/web/src/lib/components/PanelEditor.svelte b/web/src/lib/components/PanelEditor.svelte new file mode 100644 index 0000000..f64e6c9 --- /dev/null +++ b/web/src/lib/components/PanelEditor.svelte @@ -0,0 +1,310 @@ + + + +
+ + + + + + {#if vizType === 'line' || vizType === 'bar'} +
+ vizConfig.x_column ?? '', (v) => setConfig('x_column', v)} /> + vizConfig.value_column ?? '', (v) => setConfig('value_column', v)} + /> + vizConfig.series_column ?? '', (v) => setConfig('series_column', v)} + /> +
+ {#if vizType === 'bar'} + + {/if} + {:else if vizType === 'top_n'} +
+ vizConfig.label_column ?? '', (v) => setConfig('label_column', v)} /> + vizConfig.value_column ?? '', (v) => setConfig('value_column', v)} + /> +
+ {:else if vizType === 'heatmap'} +
+ vizConfig.x_column ?? '', (v) => setConfig('x_column', v)} /> + vizConfig.y_column ?? '', (v) => setConfig('y_column', v)} /> + vizConfig.value_column ?? '', (v) => setConfig('value_column', v)} + /> +
+ {:else if vizType === 'single_stat'} +
+ vizConfig.value_column ?? '', (v) => setConfig('value_column', v)} + /> + vizConfig.unit ?? '', (v) => setConfig('unit', v)} /> +
+ + {/if} + +
+ + +
+ + + + + + + + {#if saveError}

{saveError}

{/if} +
+ + {#snippet footer()} + + + {/snippet} +
+ + diff --git a/web/src/routes/dashboards/+page.svelte b/web/src/routes/dashboards/+page.svelte index e89f6b0..11cc83f 100644 --- a/web/src/routes/dashboards/+page.svelte +++ b/web/src/routes/dashboards/+page.svelte @@ -6,6 +6,7 @@ importDashboard, type Dashboard } from '$lib/api'; + import { Button, Input, EmptyState, Skeleton } from '$lib/components/ui'; let dashboards = $state([]); let loading = $state(true); @@ -65,12 +66,12 @@ {#if error}

Error: {error}

{/if}
- e.key === 'Enter' && create()} + onkeydown={(e: KeyboardEvent) => e.key === 'Enter' && create()} /> - +
{#if loading} -

Loading…

+
+ {#each Array(3) as _, i (i)} + + {/each} +
{:else if dashboards.length === 0} -

No dashboards yet.

+ {:else}
    {#each dashboards as d (d.id)} @@ -96,24 +105,31 @@ diff --git a/web/src/routes/dashboards/[id]/+page.svelte b/web/src/routes/dashboards/[id]/+page.svelte index 55a521c..ef449d6 100644 --- a/web/src/routes/dashboards/[id]/+page.svelte +++ b/web/src/routes/dashboards/[id]/+page.svelte @@ -2,13 +2,13 @@ import { page } from '$app/state'; import { GridStack, type GridStackNode } from 'gridstack'; import 'gridstack/dist/gridstack.min.css'; - import QueryBar from '$lib/QueryBar.svelte'; import PanelViz from '$lib/PanelViz.svelte'; + import PanelEditor from '$lib/components/PanelEditor.svelte'; + import { Button, Card, EmptyState, Skeleton } from '$lib/components/ui'; import { getDashboard, updateDashboard, deleteDashboard as apiDeleteDashboard, - addPanel, deletePanel as apiDeletePanel, updatePanel as apiUpdatePanel, exportDashboard, @@ -17,8 +17,6 @@ injectTimeRange, type Dashboard, type Panel, - type VizType, - type Language, type QueryResult } from '$lib/api'; @@ -37,11 +35,29 @@ let gridEl: HTMLDivElement | undefined = $state(); let grid: GridStack | undefined; - let showAddPanel = $state(false); - let newTitle = $state(''); - let newQuery = $state(''); - let newLanguage = $state(''); - let newVizType = $state('table'); + let editorOpen = $state(false); + let editingPanel = $state(null); + + function openNewPanel() { + editingPanel = null; + editorOpen = true; + } + function openEditPanel(panel: Panel) { + editingPanel = panel; + editorOpen = true; + } + + // Zoom on a time-series panel becomes the dashboard's new global + // range -- the brief's "zoomed range able to feed back into the + // global dashboard time-range picker" requirement. Reuses the exact + // same applyTimeRange() path the manual earliest/latest inputs use, + // so a zoom and a typed range behave identically (persisted, re-runs + // every panel), not two divergent code paths. + async function onPanelZoom(range: { earliest: string; latest: string }) { + earliestInput = range.earliest; + latestInput = range.latest; + await applyTimeRange(); + } async function load() { loading = true; @@ -99,30 +115,6 @@ return Math.max(...dashboard.panels.map((p) => p.position_y + p.height)); } - async function submitAddPanel() { - if (!newQuery.trim()) return; - try { - await addPanel(dashboardId, { - title: newTitle, - query: newQuery, - query_language: newLanguage, - viz_type: newVizType, - position_x: 0, - position_y: nextY(), - width: 6, - height: 4 - }); - showAddPanel = false; - newTitle = ''; - newQuery = ''; - newLanguage = ''; - newVizType = 'table'; - await load(); - } catch (e) { - error = e instanceof Error ? e.message : String(e); - } - } - async function removePanel(panelId: string) { try { await apiDeletePanel(dashboardId, panelId); @@ -193,15 +185,23 @@
    {#if loading} -

    Loading…

    +
    + + +
    +
    + {#each Array(4) as _, i (i)} + + {/each} +
    {:else if !dashboard} -

    Error: {error}

    + {:else}

    {dashboard.name}

    - - + +
    {#if dashboard.description}

    {dashboard.description}

    {/if} @@ -210,8 +210,8 @@
    - - Per-panel overrides win over this default -- see the panel editor. + + Per-panel overrides win over this default, and a time-series panel's zoom updates this automatically.
    {#if dashboard.panels && dashboard.panels.length > 0} @@ -229,8 +229,10 @@ >
    - {panel.title || panel.query} - + +
    {#if panelErrors[panel.id]}

    Error: {panelErrors[panel.id]}

    @@ -239,49 +241,49 @@ result={panelResults[panel.id]} vizType={panel.viz_type} vizConfig={panel.viz_config} + query={panel.query} + onZoom={onPanelZoom} /> {:else} -

    Loading…

    + {/if}
    {/each} {:else} -

    No panels yet. Add one below.

    + + {#snippet action()} + + {/snippet} + {/if} -
    - - {#if showAddPanel} -
    - - - - -
    - {/if} -
    + {#if dashboard.panels && dashboard.panels.length > 0} +
    + +
    + {/if} + + {/if}