From e8da298603324709578eed5ed146269e878a8e11 Mon Sep 17 00:00:00 2001 From: zuchka Date: Tue, 19 May 2026 17:06:14 -0700 Subject: [PATCH] feat(analytics): add persisted auto-refresh interval to SQL dashboards Adds a `refreshInterval?: number` field to `SqlDashboardConfig` that wires through to React Query's `refetchInterval` on every panel. A Select control in the filter row (Off / 0.5 s / 1 s / 5 s / 10 s / 30 s) lets users set the interval per-dashboard; the value is persisted to the dashboard config so it survives page reloads. When `refetchInterval` is active, `staleTime` is set to 0 so React Query always treats the cache as stale and fires every tick. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../app/components/dashboard/SqlChart.tsx | 4 ++ templates/analytics/app/lib/sql-query.ts | 2 +- .../adhoc/sql-dashboard/SqlChartCard.tsx | 4 ++ .../app/pages/adhoc/sql-dashboard/index.tsx | 55 ++++++++++++++++--- .../app/pages/adhoc/sql-dashboard/types.ts | 3 + 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/templates/analytics/app/components/dashboard/SqlChart.tsx b/templates/analytics/app/components/dashboard/SqlChart.tsx index 510253c36d..94399ffc8f 100644 --- a/templates/analytics/app/components/dashboard/SqlChart.tsx +++ b/templates/analytics/app/components/dashboard/SqlChart.tsx @@ -279,12 +279,15 @@ interface SqlChartProps { resolvedSql?: string; className?: string; onExportCsvChange?: (handler: (() => void) | null) => void; + /** Auto-refresh interval in ms forwarded from the dashboard config. */ + refreshInterval?: number; } export function SqlChart({ panel, resolvedSql, onExportCsvChange, + refreshInterval, }: SqlChartProps) { // Section panels are pure layout — no query, no chart. Render a header with // optional description and skip the SQL pipeline entirely. @@ -305,6 +308,7 @@ export function SqlChart({ ["sql-chart", panel.id, sql, panel.source], sql, panel.source, + refreshInterval ? { refetchInterval: refreshInterval } : undefined, ); const rawRows = result?.rows ?? []; diff --git a/templates/analytics/app/lib/sql-query.ts b/templates/analytics/app/lib/sql-query.ts index bf4e594955..6f49b03cd3 100644 --- a/templates/analytics/app/lib/sql-query.ts +++ b/templates/analytics/app/lib/sql-query.ts @@ -58,6 +58,6 @@ export function useSqlQuery( queryFn: () => executeSqlQuery(sql, source), enabled: options?.enabled ?? true, refetchInterval: options?.refetchInterval, - staleTime: 5 * 60 * 1000, + staleTime: options?.refetchInterval ? 0 : 5 * 60 * 1000, }); } diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx b/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx index 4ae5330529..22c5be04bf 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx @@ -53,6 +53,8 @@ interface SqlChartCardProps { /** Persist a SQL-only edit from the inline View SQL popover. Should throw on * validation failure so the popover can stay open and surface the error. */ onSaveSql?: (sql: string) => Promise; + /** Auto-refresh interval in ms forwarded from the dashboard config. */ + refreshInterval?: number; } export function SqlChartCard({ @@ -63,6 +65,7 @@ export function SqlChartCard({ gridColumns, onEdit, onSaveSql, + refreshInterval, }: SqlChartCardProps) { const { attributes, @@ -283,6 +286,7 @@ export function SqlChartCard({ panel={panel} resolvedSql={resolvedSql} onExportCsvChange={handleExportCsvChange} + refreshInterval={refreshInterval} /> diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx b/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx index 2b1b951e26..2b5a23c42c 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx @@ -37,8 +37,16 @@ import { IconDots, IconPencil, IconPlus, + IconRefresh, IconTrash, } from "@tabler/icons-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { DropdownMenu, DropdownMenuContent, @@ -137,6 +145,7 @@ async function fetchDashboard(id: string): Promise { filters: data.filters, variables: data.variables, columns: typeof data.columns === "number" ? data.columns : undefined, + refreshInterval: typeof data.refreshInterval === "number" ? data.refreshInterval : undefined, panels: data.panels ?? [], }, archivedAt: typeof data.archivedAt === "string" ? data.archivedAt : null, @@ -1005,13 +1014,44 @@ export default function SqlDashboardPage() { )} - {/* Filters */} - {dashboard.filters && dashboard.filters.length > 0 && ( - - )} + {/* Filters + refresh interval */} +
+ {dashboard.filters && dashboard.filters.length > 0 && ( + + )} +
+ + Refresh + +
+
{/* Panels grid */} {dashboard.panels.length === 0 ? ( @@ -1090,6 +1130,7 @@ export default function SqlDashboardPage() { panel={resolved} resolvedSql={interpolate(panel.sql, vars)} gridColumns={group.columns} + refreshInterval={dashboard?.refreshInterval} onRemove={() => removePanel(panel.id)} onToggleWidth={() => toggleWidth(panel.id, group.columns) diff --git a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts index b9d92d057e..8d1f3aab3b 100644 --- a/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +++ b/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts @@ -116,6 +116,9 @@ export interface SqlDashboardConfig { * not only at narrow viewports). Defaults to 2. */ columns?: number; + /** Auto-refresh interval in milliseconds. When set, all panels refetch at + * this cadence. Omit (or set to 0) to disable. */ + refreshInterval?: number; panels: SqlPanel[]; }