@quantumwake/terminal-ux-dashboard-components 0.1.8 → 0.1.12
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.cjs +135 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -7
- package/dist/index.d.ts +4 -7
- package/dist/index.js +136 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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({
|
|
@@ -1670,76 +1677,167 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
|
|
|
1670
1677
|
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
|
}
|
|
1680
|
+
var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
|
|
1681
|
+
function panelToChartConfig(chartType, config) {
|
|
1682
|
+
const cfg = { chartType, xFields: [], yFields: [], filters: config.filters || [] };
|
|
1683
|
+
switch (chartType) {
|
|
1684
|
+
case "bar":
|
|
1685
|
+
case "grouped-bar":
|
|
1686
|
+
if (config.group) cfg.xFields = [{ name: config.group }];
|
|
1687
|
+
cfg.yFields = config.yFields?.length ? config.yFields : config.value ? [{ name: config.value, agg: config.agg }] : [];
|
|
1688
|
+
break;
|
|
1689
|
+
case "pie":
|
|
1690
|
+
if (config.group) cfg.xFields = [{ name: config.group }];
|
|
1691
|
+
break;
|
|
1692
|
+
case "line":
|
|
1693
|
+
case "scatter":
|
|
1694
|
+
if (config.x) cfg.xFields = [{ name: config.x }];
|
|
1695
|
+
if (config.y) cfg.yFields = [{ name: config.y }];
|
|
1696
|
+
break;
|
|
1697
|
+
case "heatmap":
|
|
1698
|
+
if (config.row) cfg.xFields = [{ name: config.row }];
|
|
1699
|
+
if (config.col) cfg.yFields = [{ name: config.col }];
|
|
1700
|
+
if (config.value) cfg.valueField = { name: config.value, agg: config.agg };
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
return cfg;
|
|
1704
|
+
}
|
|
1705
|
+
function panelSql(chartType, config) {
|
|
1706
|
+
if (config.sql) return config.sql;
|
|
1707
|
+
if (SQL_CHART_TYPES.has(chartType)) return buildChartSQL(panelToChartConfig(chartType, config)) || "";
|
|
1708
|
+
return "";
|
|
1709
|
+
}
|
|
1673
1710
|
function SqlPanel({ panel }) {
|
|
1674
|
-
const { runQuery } = useDashboard();
|
|
1711
|
+
const { runQuery, persistPanelData } = useDashboard();
|
|
1712
|
+
const { canRefresh } = useCapabilities();
|
|
1675
1713
|
const runQueryRef = useRef(runQuery);
|
|
1676
1714
|
runQueryRef.current = runQuery;
|
|
1677
1715
|
const config = panel.config || {};
|
|
1678
1716
|
const chartType = config.chartType || panel.type;
|
|
1679
|
-
const
|
|
1717
|
+
const sql = panelSql(chartType, config);
|
|
1718
|
+
const precomputed = config.data;
|
|
1719
|
+
const [rows, setRows] = useState(precomputed ?? null);
|
|
1680
1720
|
const [error, setError] = useState(null);
|
|
1681
|
-
const [loading, setLoading] = useState(
|
|
1721
|
+
const [loading, setLoading] = useState(!precomputed && canRefresh);
|
|
1722
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
1682
1723
|
useEffect(() => {
|
|
1724
|
+
if (precomputed) {
|
|
1725
|
+
setRows(precomputed);
|
|
1726
|
+
setLoading(false);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (!canRefresh) {
|
|
1730
|
+
setLoading(false);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1683
1733
|
let cancelled = false;
|
|
1684
1734
|
setLoading(true);
|
|
1685
1735
|
setError(null);
|
|
1686
|
-
const sql = config.sql || "";
|
|
1687
1736
|
runQueryRef.current(sql).then(
|
|
1688
1737
|
(res) => {
|
|
1689
|
-
if (cancelled)
|
|
1690
|
-
|
|
1691
|
-
|
|
1738
|
+
if (!cancelled) {
|
|
1739
|
+
setLoading(false);
|
|
1740
|
+
setRows(res?.rows || []);
|
|
1741
|
+
}
|
|
1692
1742
|
},
|
|
1693
1743
|
(err) => {
|
|
1694
|
-
if (cancelled)
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1744
|
+
if (!cancelled) {
|
|
1745
|
+
setLoading(false);
|
|
1746
|
+
setRows(null);
|
|
1747
|
+
setError(err instanceof Error ? err.message : String(err) || "Query failed");
|
|
1748
|
+
}
|
|
1698
1749
|
}
|
|
1699
1750
|
);
|
|
1700
1751
|
return () => {
|
|
1701
1752
|
cancelled = true;
|
|
1702
1753
|
};
|
|
1703
|
-
}, [
|
|
1754
|
+
}, [sql, canRefresh]);
|
|
1755
|
+
const refresh = useCallback(async () => {
|
|
1756
|
+
setRefreshing(true);
|
|
1757
|
+
setError(null);
|
|
1758
|
+
try {
|
|
1759
|
+
const res = await runQueryRef.current(sql);
|
|
1760
|
+
const r = res?.rows || [];
|
|
1761
|
+
setRows(r);
|
|
1762
|
+
persistPanelData?.(panel.id, r, (/* @__PURE__ */ new Date()).toISOString());
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
setError(err instanceof Error ? err.message : String(err) || "Query failed");
|
|
1765
|
+
} finally {
|
|
1766
|
+
setRefreshing(false);
|
|
1767
|
+
}
|
|
1768
|
+
}, [sql, panel.id, persistPanelData]);
|
|
1769
|
+
const refreshBtn = canRefresh ? /* @__PURE__ */ jsx(
|
|
1770
|
+
"button",
|
|
1771
|
+
{
|
|
1772
|
+
onClick: refresh,
|
|
1773
|
+
disabled: refreshing,
|
|
1774
|
+
title: config.refreshedAt ? `Last computed ${new Date(config.refreshedAt).toLocaleString()} \u2014 click to refresh` : "Compute & save this chart\u2019s data",
|
|
1775
|
+
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",
|
|
1776
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: `w-3 h-3 ${refreshing ? "animate-spin" : ""}` })
|
|
1777
|
+
}
|
|
1778
|
+
) : null;
|
|
1704
1779
|
if (loading && !rows) {
|
|
1705
1780
|
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Running\u2026" });
|
|
1706
1781
|
}
|
|
1782
|
+
if (!rows && !error && !canRefresh) {
|
|
1783
|
+
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: [
|
|
1784
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4" }),
|
|
1785
|
+
/* @__PURE__ */ jsx("span", { children: "Chart data not available" }),
|
|
1786
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-midnight-text-subdued", children: "Download the dataset to view this chart." })
|
|
1787
|
+
] });
|
|
1788
|
+
}
|
|
1707
1789
|
if (error) {
|
|
1708
|
-
return /* @__PURE__ */
|
|
1790
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center h-full px-2 text-center text-xs text-red-400", children: [
|
|
1791
|
+
refreshBtn,
|
|
1792
|
+
error
|
|
1793
|
+
] });
|
|
1709
1794
|
}
|
|
1710
1795
|
if (!rows) return null;
|
|
1796
|
+
let chart;
|
|
1711
1797
|
if (chartType === "metric") {
|
|
1712
1798
|
const first = rows[0];
|
|
1713
1799
|
const v = first ? Object.values(first)[0] : 0;
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1800
|
+
chart = /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
|
|
1801
|
+
} else {
|
|
1802
|
+
const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
|
|
1803
|
+
switch (chartType) {
|
|
1804
|
+
case "bar":
|
|
1805
|
+
chart = /* @__PURE__ */ jsx(BarView, { data: shaped, groupColumn: config.group || "", valueColumn: config.value || "", style: config.style });
|
|
1806
|
+
break;
|
|
1807
|
+
case "grouped-bar": {
|
|
1808
|
+
const gb = shaped;
|
|
1809
|
+
chart = /* @__PURE__ */ jsx(GroupedBarChart, { data: gb.data, keys: gb.keys, yFields: config.yFields || [], style: config.style });
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
case "pie":
|
|
1813
|
+
chart = /* @__PURE__ */ jsx(PieView, { data: shaped, groupColumn: config.group || "", style: config.style });
|
|
1814
|
+
break;
|
|
1815
|
+
case "line":
|
|
1816
|
+
chart = /* @__PURE__ */ jsx(LineView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1817
|
+
break;
|
|
1818
|
+
case "scatter":
|
|
1819
|
+
chart = /* @__PURE__ */ jsx(ScatterView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1820
|
+
break;
|
|
1821
|
+
case "heatmap":
|
|
1822
|
+
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" });
|
|
1823
|
+
break;
|
|
1824
|
+
default:
|
|
1825
|
+
chart = /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
|
|
1826
|
+
"Unsupported SQL chart: ",
|
|
1827
|
+
chartType
|
|
1828
|
+
] });
|
|
1723
1829
|
}
|
|
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
1830
|
}
|
|
1831
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full", children: [
|
|
1832
|
+
refreshBtn,
|
|
1833
|
+
chart
|
|
1834
|
+
] });
|
|
1738
1835
|
}
|
|
1739
1836
|
function PanelContent({ panel, records, columns }) {
|
|
1740
1837
|
const { type } = panel;
|
|
1741
1838
|
const config = panel.config || {};
|
|
1742
|
-
|
|
1839
|
+
const effType = config.chartType || type;
|
|
1840
|
+
if (config.sql || SQL_CHART_TYPES.has(effType)) {
|
|
1743
1841
|
return /* @__PURE__ */ jsx(SqlPanel, { panel });
|
|
1744
1842
|
}
|
|
1745
1843
|
if (!records?.length && type !== "insight") {
|