@quantumwake/terminal-ux-dashboard-components 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -30,6 +30,7 @@ interface DashboardCapabilities {
30
30
  newDashboard?: () => void;
31
31
  addPanel?: (panel: PanelInput) => void;
32
32
  removePanel?: (panelId: string) => void;
33
+ persistPanelData?: (panelId: string, rows: QueryResult['rows'], refreshedAt: string) => void;
33
34
  }
34
35
  interface DashboardContextValue extends DashboardCapabilities {
35
36
  theme: DashboardTheme;
@@ -52,6 +53,7 @@ declare function useCapabilities(): {
52
53
  canNew: boolean;
53
54
  canAddPanel: boolean;
54
55
  canEditPanels: boolean;
56
+ canRefresh: boolean;
55
57
  };
56
58
 
57
59
  type ChartType = 'bar' | 'grouped-bar' | 'pie' | 'line' | 'scatter' | 'heatmap';
@@ -358,6 +360,8 @@ interface PanelConfig {
358
360
  text?: string;
359
361
  label?: string;
360
362
  column?: string;
363
+ data?: Row[];
364
+ refreshedAt?: string;
361
365
  [key: string]: unknown;
362
366
  }
363
367
  interface DashboardPanel$1 {
@@ -404,13 +408,6 @@ interface ChartBuilderProps {
404
408
  stateId?: string;
405
409
  onSave?: (panel: ChartPanel) => void;
406
410
  }
407
- /**
408
- * ChartBuilder — visual chart authoring over a state's dataset. Generates DuckDB
409
- * SQL from the visual config (SQL engine mode, exact over the full dataset) or
410
- * aggregates the loaded sample client-side (Direct mode). The host injects
411
- * runQuery + theme via <DashboardProvider>; an optional onSave persists the
412
- * assembled panel (absent ⇒ the save affordance is hidden / read-only).
413
- */
414
411
  declare function ChartBuilder({ records, columns, stateId, onSave }: ChartBuilderProps): react.JSX.Element;
415
412
 
416
413
  /** A dataset column descriptor: name + DuckDB-ish type tag. */
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ interface DashboardCapabilities {
30
30
  newDashboard?: () => void;
31
31
  addPanel?: (panel: PanelInput) => void;
32
32
  removePanel?: (panelId: string) => void;
33
+ persistPanelData?: (panelId: string, rows: QueryResult['rows'], refreshedAt: string) => void;
33
34
  }
34
35
  interface DashboardContextValue extends DashboardCapabilities {
35
36
  theme: DashboardTheme;
@@ -52,6 +53,7 @@ declare function useCapabilities(): {
52
53
  canNew: boolean;
53
54
  canAddPanel: boolean;
54
55
  canEditPanels: boolean;
56
+ canRefresh: boolean;
55
57
  };
56
58
 
57
59
  type ChartType = 'bar' | 'grouped-bar' | 'pie' | 'line' | 'scatter' | 'heatmap';
@@ -358,6 +360,8 @@ interface PanelConfig {
358
360
  text?: string;
359
361
  label?: string;
360
362
  column?: string;
363
+ data?: Row[];
364
+ refreshedAt?: string;
361
365
  [key: string]: unknown;
362
366
  }
363
367
  interface DashboardPanel$1 {
@@ -404,13 +408,6 @@ interface ChartBuilderProps {
404
408
  stateId?: string;
405
409
  onSave?: (panel: ChartPanel) => void;
406
410
  }
407
- /**
408
- * ChartBuilder — visual chart authoring over a state's dataset. Generates DuckDB
409
- * SQL from the visual config (SQL engine mode, exact over the full dataset) or
410
- * aggregates the loaded sample client-side (Direct mode). The host injects
411
- * runQuery + theme via <DashboardProvider>; an optional onSave persists the
412
- * assembled panel (absent ⇒ the save affordance is hidden / read-only).
413
- */
414
411
  declare function ChartBuilder({ records, columns, stateId, onSave }: ChartBuilderProps): react.JSX.Element;
415
412
 
416
413
  /** A dataset column descriptor: name + DuckDB-ish type tag. */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useMemo, useState, useEffect, useRef, Suspense } from 'react';
1
+ import { createContext, useContext, useMemo, useState, useEffect, useRef, Suspense, useCallback } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { ResponsiveBar } from '@nivo/bar';
4
4
  import { ResponsivePie } from '@nivo/pie';
@@ -37,7 +37,9 @@ function useCapabilities() {
37
37
  canRefine: !!c.refineDashboard,
38
38
  canNew: !!c.newDashboard,
39
39
  canAddPanel: !!c.addPanel,
40
- canEditPanels: !!c.removePanel
40
+ canEditPanels: !!c.removePanel,
41
+ // Studio can recompute + persist a panel's precomputed result.
42
+ canRefresh: !!c.persistPanelData
41
43
  };
42
44
  }
43
45
 
@@ -1339,6 +1341,7 @@ var ChartPreview = ({
1339
1341
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
1340
1342
  }
1341
1343
  };
1344
+ var MAX_PRECOMPUTE_ROWS = 5e3;
1342
1345
  function ChartBuilder({ records, columns, stateId, onSave }) {
1343
1346
  const { theme, runQuery } = useDashboard();
1344
1347
  const runQueryRef = useRef(runQuery);
@@ -1467,6 +1470,10 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1467
1470
  config.chartType = chartType;
1468
1471
  if (filters.length) config.filters = filters;
1469
1472
  if (yFields.length) config.yFields = yFields.map((f) => ({ name: f.name, agg: f.agg }));
1473
+ if (sqlRows && sqlRows.length <= MAX_PRECOMPUTE_ROWS) {
1474
+ config.data = sqlRows;
1475
+ config.refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
1476
+ }
1470
1477
  }
1471
1478
  config.style = style;
1472
1479
  onSave({
@@ -1671,70 +1678,129 @@ function ViewLoading() {
1671
1678
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1672
1679
  }
1673
1680
  function SqlPanel({ panel }) {
1674
- const { runQuery } = useDashboard();
1681
+ const { runQuery, persistPanelData } = useDashboard();
1682
+ const { canRefresh } = useCapabilities();
1675
1683
  const runQueryRef = useRef(runQuery);
1676
1684
  runQueryRef.current = runQuery;
1677
1685
  const config = panel.config || {};
1678
1686
  const chartType = config.chartType || panel.type;
1679
- const [rows, setRows] = useState(null);
1687
+ const precomputed = config.data;
1688
+ const [rows, setRows] = useState(precomputed ?? null);
1680
1689
  const [error, setError] = useState(null);
1681
- const [loading, setLoading] = useState(true);
1690
+ const [loading, setLoading] = useState(!precomputed && canRefresh);
1691
+ const [refreshing, setRefreshing] = useState(false);
1682
1692
  useEffect(() => {
1693
+ if (precomputed) {
1694
+ setRows(precomputed);
1695
+ setLoading(false);
1696
+ return;
1697
+ }
1698
+ if (!canRefresh) {
1699
+ setLoading(false);
1700
+ return;
1701
+ }
1683
1702
  let cancelled = false;
1684
1703
  setLoading(true);
1685
1704
  setError(null);
1686
- const sql = config.sql || "";
1687
- runQueryRef.current(sql).then(
1705
+ runQueryRef.current(config.sql || "").then(
1688
1706
  (res) => {
1689
- if (cancelled) return;
1690
- setLoading(false);
1691
- setRows(res?.rows || []);
1707
+ if (!cancelled) {
1708
+ setLoading(false);
1709
+ setRows(res?.rows || []);
1710
+ }
1692
1711
  },
1693
1712
  (err) => {
1694
- if (cancelled) return;
1695
- setLoading(false);
1696
- setRows(null);
1697
- setError(err instanceof Error ? err.message : String(err) || "Query failed");
1713
+ if (!cancelled) {
1714
+ setLoading(false);
1715
+ setRows(null);
1716
+ setError(err instanceof Error ? err.message : String(err) || "Query failed");
1717
+ }
1698
1718
  }
1699
1719
  );
1700
1720
  return () => {
1701
1721
  cancelled = true;
1702
1722
  };
1703
- }, [config.sql]);
1723
+ }, [config.sql, canRefresh]);
1724
+ const refresh = useCallback(async () => {
1725
+ setRefreshing(true);
1726
+ setError(null);
1727
+ try {
1728
+ const res = await runQueryRef.current(config.sql || "");
1729
+ const r = res?.rows || [];
1730
+ setRows(r);
1731
+ persistPanelData?.(panel.id, r, (/* @__PURE__ */ new Date()).toISOString());
1732
+ } catch (err) {
1733
+ setError(err instanceof Error ? err.message : String(err) || "Query failed");
1734
+ } finally {
1735
+ setRefreshing(false);
1736
+ }
1737
+ }, [config.sql, panel.id, persistPanelData]);
1738
+ const refreshBtn = canRefresh ? /* @__PURE__ */ jsx(
1739
+ "button",
1740
+ {
1741
+ onClick: refresh,
1742
+ disabled: refreshing,
1743
+ title: config.refreshedAt ? `Last computed ${new Date(config.refreshedAt).toLocaleString()} \u2014 click to refresh` : "Compute & save this chart\u2019s data",
1744
+ className: "absolute top-1 right-1 z-10 p-1 bg-midnight-elevated/80 border border-midnight-border text-midnight-text-muted hover:text-midnight-accent transition-colors",
1745
+ children: /* @__PURE__ */ jsx(RefreshCw, { className: `w-3 h-3 ${refreshing ? "animate-spin" : ""}` })
1746
+ }
1747
+ ) : null;
1704
1748
  if (loading && !rows) {
1705
1749
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Running\u2026" });
1706
1750
  }
1751
+ if (!rows && !error && !canRefresh) {
1752
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-1 px-2 text-center text-xs text-midnight-text-muted", children: [
1753
+ /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4" }),
1754
+ /* @__PURE__ */ jsx("span", { children: "Chart data not available" }),
1755
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-midnight-text-subdued", children: "Download the dataset to view this chart." })
1756
+ ] });
1757
+ }
1707
1758
  if (error) {
1708
- return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-red-400 px-2 text-center", children: error });
1759
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center h-full px-2 text-center text-xs text-red-400", children: [
1760
+ refreshBtn,
1761
+ error
1762
+ ] });
1709
1763
  }
1710
1764
  if (!rows) return null;
1765
+ let chart;
1711
1766
  if (chartType === "metric") {
1712
1767
  const first = rows[0];
1713
1768
  const v = first ? Object.values(first)[0] : 0;
1714
- return /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1715
- }
1716
- const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1717
- switch (chartType) {
1718
- case "bar":
1719
- return /* @__PURE__ */ jsx(BarView, { data: shaped, groupColumn: config.group || "", valueColumn: config.value || "", style: config.style });
1720
- case "grouped-bar": {
1721
- const gb = shaped;
1722
- return /* @__PURE__ */ jsx(GroupedBarChart, { data: gb.data, keys: gb.keys, yFields: config.yFields || [], style: config.style });
1769
+ chart = /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1770
+ } else {
1771
+ const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1772
+ switch (chartType) {
1773
+ case "bar":
1774
+ chart = /* @__PURE__ */ jsx(BarView, { data: shaped, groupColumn: config.group || "", valueColumn: config.value || "", style: config.style });
1775
+ break;
1776
+ case "grouped-bar": {
1777
+ const gb = shaped;
1778
+ chart = /* @__PURE__ */ jsx(GroupedBarChart, { data: gb.data, keys: gb.keys, yFields: config.yFields || [], style: config.style });
1779
+ break;
1780
+ }
1781
+ case "pie":
1782
+ chart = /* @__PURE__ */ jsx(PieView, { data: shaped, groupColumn: config.group || "", style: config.style });
1783
+ break;
1784
+ case "line":
1785
+ chart = /* @__PURE__ */ jsx(LineView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
1786
+ break;
1787
+ case "scatter":
1788
+ chart = /* @__PURE__ */ jsx(ScatterView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
1789
+ break;
1790
+ case "heatmap":
1791
+ chart = shaped.length ? /* @__PURE__ */ jsx(HeatmapView, { data: shaped, rowColumn: config.row || "", colColumn: config.col || "", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
1792
+ break;
1793
+ default:
1794
+ chart = /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1795
+ "Unsupported SQL chart: ",
1796
+ chartType
1797
+ ] });
1723
1798
  }
1724
- case "pie":
1725
- return /* @__PURE__ */ jsx(PieView, { data: shaped, groupColumn: config.group || "", style: config.style });
1726
- case "line":
1727
- return /* @__PURE__ */ jsx(LineView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
1728
- case "scatter":
1729
- return /* @__PURE__ */ jsx(ScatterView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
1730
- case "heatmap":
1731
- return shaped.length ? /* @__PURE__ */ jsx(HeatmapView, { data: shaped, rowColumn: config.row || "", colColumn: config.col || "", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
1732
- default:
1733
- return /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1734
- "Unsupported SQL chart: ",
1735
- chartType
1736
- ] });
1737
1799
  }
1800
+ return /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full", children: [
1801
+ refreshBtn,
1802
+ chart
1803
+ ] });
1738
1804
  }
1739
1805
  function PanelContent({ panel, records, columns }) {
1740
1806
  const { type } = panel;