@quantumwake/terminal-ux-dashboard-components 0.1.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/index.cjs +1807 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +477 -0
- package/dist/index.d.ts +477 -0
- package/dist/index.js +1772 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1807 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var bar = require('@nivo/bar');
|
|
6
|
+
var pie = require('@nivo/pie');
|
|
7
|
+
var line = require('@nivo/line');
|
|
8
|
+
var scatterplot = require('@nivo/scatterplot');
|
|
9
|
+
var heatmap = require('@nivo/heatmap');
|
|
10
|
+
var PivotTableUI = require('react-pivottable/PivotTableUI');
|
|
11
|
+
require('react-pivottable/pivottable.css');
|
|
12
|
+
var lucideReact = require('lucide-react');
|
|
13
|
+
|
|
14
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
+
|
|
16
|
+
var PivotTableUI__default = /*#__PURE__*/_interopDefault(PivotTableUI);
|
|
17
|
+
|
|
18
|
+
// src/context/DashboardContext.tsx
|
|
19
|
+
var DashboardContext = react.createContext(null);
|
|
20
|
+
function DashboardProvider({ children, ...value }) {
|
|
21
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DashboardContext.Provider, { value, children });
|
|
22
|
+
}
|
|
23
|
+
function useDashboard() {
|
|
24
|
+
const ctx = react.useContext(DashboardContext);
|
|
25
|
+
if (!ctx) {
|
|
26
|
+
throw new Error("useDashboard must be used within a <DashboardProvider>");
|
|
27
|
+
}
|
|
28
|
+
return ctx;
|
|
29
|
+
}
|
|
30
|
+
function useCapabilities() {
|
|
31
|
+
const c = useDashboard();
|
|
32
|
+
return {
|
|
33
|
+
canRun: true,
|
|
34
|
+
canSave: !!c.saveDashboard,
|
|
35
|
+
canList: !!c.listDashboards,
|
|
36
|
+
canSearch: !!c.searchDashboards,
|
|
37
|
+
canLoad: !!c.loadDashboard,
|
|
38
|
+
canDelete: !!c.deleteDashboard,
|
|
39
|
+
canAnalyze: !!c.analyzeDataset,
|
|
40
|
+
canRefine: !!c.refineDashboard,
|
|
41
|
+
canEditPanels: !!c.removePanel
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/sqlgen.ts
|
|
46
|
+
var TABLE = "data";
|
|
47
|
+
var MAX_GROUPS = 50;
|
|
48
|
+
var MAX_POINTS = 1e3;
|
|
49
|
+
var qIdent = (name) => `"${String(name).replace(/"/g, '""')}"`;
|
|
50
|
+
var qLit = (v) => {
|
|
51
|
+
const s = String(v);
|
|
52
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return s;
|
|
53
|
+
if (s === "true" || s === "false") return s;
|
|
54
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
55
|
+
};
|
|
56
|
+
var qNum = (col) => `TRY_CAST(${qIdent(col)} AS DECIMAL(18,6))`;
|
|
57
|
+
var aggExpr = (agg, col) => {
|
|
58
|
+
const fn = (agg || "count").toLowerCase();
|
|
59
|
+
if (fn === "count") return "COUNT(*)";
|
|
60
|
+
if (!col) return "COUNT(*)";
|
|
61
|
+
const c = qIdent(col);
|
|
62
|
+
switch (fn) {
|
|
63
|
+
case "distinct":
|
|
64
|
+
return `COUNT(DISTINCT ${c})`;
|
|
65
|
+
case "sum":
|
|
66
|
+
return `SUM(${qNum(col)})`;
|
|
67
|
+
case "avg":
|
|
68
|
+
return `AVG(${qNum(col)})`;
|
|
69
|
+
case "min":
|
|
70
|
+
return `MIN(${qNum(col)})`;
|
|
71
|
+
case "max":
|
|
72
|
+
return `MAX(${qNum(col)})`;
|
|
73
|
+
default:
|
|
74
|
+
return "COUNT(*)";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var compileWhere = (filters) => {
|
|
78
|
+
const conds = (filters || []).filter((f) => f && f.column && f.op).map((f) => {
|
|
79
|
+
const c = qIdent(f.column);
|
|
80
|
+
switch (f.op) {
|
|
81
|
+
case "IS NULL":
|
|
82
|
+
return `${c} IS NULL`;
|
|
83
|
+
case "IS NOT NULL":
|
|
84
|
+
return `${c} IS NOT NULL`;
|
|
85
|
+
case "IN":
|
|
86
|
+
case "NOT IN": {
|
|
87
|
+
const items = String(f.value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
88
|
+
if (!items.length) return null;
|
|
89
|
+
return `${c} ${f.op} (${items.map(qLit).join(", ")})`;
|
|
90
|
+
}
|
|
91
|
+
case "BETWEEN": {
|
|
92
|
+
const parts = String(f.value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
93
|
+
if (parts.length !== 2) return null;
|
|
94
|
+
return `${c} BETWEEN ${qLit(parts[0])} AND ${qLit(parts[1])}`;
|
|
95
|
+
}
|
|
96
|
+
case "LIKE":
|
|
97
|
+
return `${c} LIKE ${qLit(f.value)}`;
|
|
98
|
+
default:
|
|
99
|
+
if (f.value == null || f.value === "") return null;
|
|
100
|
+
return `${c} ${f.op} ${qLit(f.value)}`;
|
|
101
|
+
}
|
|
102
|
+
}).filter((x) => Boolean(x));
|
|
103
|
+
return conds.length ? ` WHERE ${conds.join(" AND ")}` : "";
|
|
104
|
+
};
|
|
105
|
+
var buildChartSQL = ({
|
|
106
|
+
chartType,
|
|
107
|
+
xFields = [],
|
|
108
|
+
yFields = [],
|
|
109
|
+
valueField = null,
|
|
110
|
+
filters = []
|
|
111
|
+
}) => {
|
|
112
|
+
const where = compileWhere(filters);
|
|
113
|
+
const x = xFields[0]?.name;
|
|
114
|
+
const y = yFields[0]?.name;
|
|
115
|
+
switch (chartType) {
|
|
116
|
+
case "bar": {
|
|
117
|
+
if (!x) return null;
|
|
118
|
+
const metric = aggExpr(yFields[0]?.agg || "count", yFields[0]?.name);
|
|
119
|
+
return `SELECT ${qIdent(x)} AS g, ${metric} AS v FROM ${TABLE}${where} GROUP BY 1 ORDER BY 2 DESC LIMIT ${MAX_GROUPS}`;
|
|
120
|
+
}
|
|
121
|
+
case "grouped-bar": {
|
|
122
|
+
if (!x || !yFields.length) return null;
|
|
123
|
+
const metrics = yFields.map((yf, i) => `${aggExpr(yf.agg || "count", yf.name)} AS m${i}`);
|
|
124
|
+
return `SELECT ${qIdent(x)} AS g, ${metrics.join(", ")} FROM ${TABLE}${where} GROUP BY 1 ORDER BY 2 DESC LIMIT ${MAX_GROUPS}`;
|
|
125
|
+
}
|
|
126
|
+
case "pie": {
|
|
127
|
+
if (!x) return null;
|
|
128
|
+
return `SELECT ${qIdent(x)} AS g, COUNT(*) AS v FROM ${TABLE}${where} GROUP BY 1 ORDER BY 2 DESC LIMIT ${MAX_GROUPS}`;
|
|
129
|
+
}
|
|
130
|
+
case "line": {
|
|
131
|
+
if (!x || !y) return null;
|
|
132
|
+
const w = where ? `${where} AND ${qIdent(x)} IS NOT NULL AND ${qIdent(y)} IS NOT NULL` : ` WHERE ${qIdent(x)} IS NOT NULL AND ${qIdent(y)} IS NOT NULL`;
|
|
133
|
+
return `SELECT ${qIdent(x)} AS x, ${qIdent(y)} AS y FROM ${TABLE}${w} ORDER BY 1 LIMIT 500`;
|
|
134
|
+
}
|
|
135
|
+
case "scatter": {
|
|
136
|
+
if (!x || !y) return null;
|
|
137
|
+
const w = where ? `${where} AND ${qIdent(x)} IS NOT NULL AND ${qIdent(y)} IS NOT NULL` : ` WHERE ${qIdent(x)} IS NOT NULL AND ${qIdent(y)} IS NOT NULL`;
|
|
138
|
+
return `SELECT ${qIdent(x)} AS x, ${qIdent(y)} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
|
|
139
|
+
}
|
|
140
|
+
case "heatmap": {
|
|
141
|
+
if (!x || !y) return null;
|
|
142
|
+
const metric = valueField ? aggExpr(valueField.agg || "count", valueField.name) : "COUNT(*)";
|
|
143
|
+
return `SELECT ${qIdent(x)} AS r, ${qIdent(y)} AS c, ${metric} AS v FROM ${TABLE}${where} GROUP BY 1, 2`;
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
|
|
150
|
+
rows = rows || [];
|
|
151
|
+
switch (chartType) {
|
|
152
|
+
case "bar":
|
|
153
|
+
return rows.map((r) => ({ group: String(r.g ?? "(null)"), value: Number(r.v) || 0 }));
|
|
154
|
+
case "grouped-bar": {
|
|
155
|
+
const keys = yFields.map((yf) => `${yf.agg || "count"}(${yf.name})`);
|
|
156
|
+
const data = rows.map((r) => {
|
|
157
|
+
const out = { group: String(r.g ?? "(null)") };
|
|
158
|
+
yFields.forEach((_, i) => {
|
|
159
|
+
out[keys[i]] = Number(r[`m${i}`]) || 0;
|
|
160
|
+
});
|
|
161
|
+
return out;
|
|
162
|
+
});
|
|
163
|
+
return { data, keys };
|
|
164
|
+
}
|
|
165
|
+
case "pie":
|
|
166
|
+
return rows.map((r) => ({ id: String(r.g ?? "(null)"), label: String(r.g ?? "(null)"), value: Number(r.v) || 0 }));
|
|
167
|
+
case "line":
|
|
168
|
+
return [{ id: "series", data: rows.map((r) => ({ x: String(r.x), y: Number(r.y) || 0 })) }];
|
|
169
|
+
case "scatter":
|
|
170
|
+
return [{ id: "points", data: rows.map((r) => ({ x: Number(r.x) || 0, y: Number(r.y) || 0 })) }];
|
|
171
|
+
case "heatmap": {
|
|
172
|
+
const rowKeys = [];
|
|
173
|
+
const colKeys = [];
|
|
174
|
+
const cell = {};
|
|
175
|
+
for (const r of rows) {
|
|
176
|
+
const rk = String(r.r ?? "(null)");
|
|
177
|
+
const ck = String(r.c ?? "(null)");
|
|
178
|
+
if (!cell[rk]) {
|
|
179
|
+
cell[rk] = {};
|
|
180
|
+
rowKeys.push(rk);
|
|
181
|
+
}
|
|
182
|
+
if (!colKeys.includes(ck)) colKeys.push(ck);
|
|
183
|
+
cell[rk][ck] = Number(r.v) || 0;
|
|
184
|
+
}
|
|
185
|
+
return rowKeys.map((rk) => ({
|
|
186
|
+
id: rk,
|
|
187
|
+
data: colKeys.map((ck) => ({ x: ck, y: cell[rk][ck] || 0 }))
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/chartStyle.ts
|
|
196
|
+
var DEFAULT_CHART_STYLE = {
|
|
197
|
+
background: "transparent",
|
|
198
|
+
// chart canvas background
|
|
199
|
+
textColor: "#94a3b8",
|
|
200
|
+
// ticks + labels
|
|
201
|
+
legendColor: "#cbd5e1",
|
|
202
|
+
// axis titles (legends)
|
|
203
|
+
fontSize: 11,
|
|
204
|
+
gridColor: "rgba(148,163,184,0.1)",
|
|
205
|
+
tooltipBg: "#1e293b",
|
|
206
|
+
tooltipColor: "#e2e8f0",
|
|
207
|
+
// Axis titles (legends): position along the axis + distance from it.
|
|
208
|
+
xLegendPosition: "middle",
|
|
209
|
+
xLegendOffset: 46,
|
|
210
|
+
yLegendPosition: "middle",
|
|
211
|
+
yLegendOffset: -50,
|
|
212
|
+
// Series legend placement (charts that have one: pie, grouped bar).
|
|
213
|
+
legendAnchor: "right",
|
|
214
|
+
// Panel title alignment (rendered by DashboardRenderer's panel header).
|
|
215
|
+
titleAlign: "left"
|
|
216
|
+
};
|
|
217
|
+
var LEGEND_ANCHORS = ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"];
|
|
218
|
+
var withStyleDefaults = (style) => ({
|
|
219
|
+
...DEFAULT_CHART_STYLE,
|
|
220
|
+
...style || {}
|
|
221
|
+
});
|
|
222
|
+
var buildNivoTheme = (style) => {
|
|
223
|
+
const s = withStyleDefaults(style);
|
|
224
|
+
return {
|
|
225
|
+
background: s.background,
|
|
226
|
+
text: { fill: s.textColor, fontSize: s.fontSize },
|
|
227
|
+
axis: {
|
|
228
|
+
ticks: { text: { fill: s.textColor, fontSize: s.fontSize } },
|
|
229
|
+
legend: { text: { fill: s.legendColor, fontSize: s.fontSize + 2 } }
|
|
230
|
+
},
|
|
231
|
+
grid: { line: { stroke: s.gridColor } },
|
|
232
|
+
crosshair: { line: { stroke: s.textColor, strokeDasharray: "6 6" } },
|
|
233
|
+
labels: { text: { fontSize: s.fontSize } },
|
|
234
|
+
legends: { text: { fill: s.textColor, fontSize: s.fontSize } },
|
|
235
|
+
tooltip: { container: { background: s.tooltipBg, color: s.tooltipColor, border: "1px solid #334155" } }
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
var legendConfig = (style) => {
|
|
239
|
+
const s = withStyleDefaults(style);
|
|
240
|
+
if (s.legendAnchor === "none") return null;
|
|
241
|
+
const base = {
|
|
242
|
+
anchor: s.legendAnchor,
|
|
243
|
+
direction: "column",
|
|
244
|
+
itemWidth: 90,
|
|
245
|
+
itemHeight: 18,
|
|
246
|
+
itemsSpacing: 2,
|
|
247
|
+
symbolSize: 10,
|
|
248
|
+
symbolShape: "circle",
|
|
249
|
+
itemTextColor: s.textColor
|
|
250
|
+
};
|
|
251
|
+
if (s.legendAnchor === "right") {
|
|
252
|
+
return { ...base, translateX: 100 };
|
|
253
|
+
}
|
|
254
|
+
const tx = s.legendAnchor.includes("left") ? 10 : -10;
|
|
255
|
+
const ty = s.legendAnchor.includes("top") ? 10 : -10;
|
|
256
|
+
return { ...base, translateX: tx, translateY: ty };
|
|
257
|
+
};
|
|
258
|
+
var axisLegend = (style, axis, legendText) => {
|
|
259
|
+
const s = withStyleDefaults(style);
|
|
260
|
+
const isX = axis === "x";
|
|
261
|
+
return {
|
|
262
|
+
legend: legendText,
|
|
263
|
+
legendPosition: isX ? s.xLegendPosition : s.yLegendPosition,
|
|
264
|
+
legendOffset: isX ? s.xLegendOffset : s.yLegendOffset
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/dataShape.ts
|
|
269
|
+
var groupBy = (records, column) => {
|
|
270
|
+
const groups = {};
|
|
271
|
+
for (const rec of records) {
|
|
272
|
+
const key = String(rec[column] ?? "(null)");
|
|
273
|
+
if (!groups[key]) groups[key] = [];
|
|
274
|
+
groups[key].push(rec);
|
|
275
|
+
}
|
|
276
|
+
return groups;
|
|
277
|
+
};
|
|
278
|
+
var aggregate = (records, column, fn) => {
|
|
279
|
+
const values = records.map((r) => r[column]).filter((v) => v != null);
|
|
280
|
+
switch (fn) {
|
|
281
|
+
case "count":
|
|
282
|
+
return records.length;
|
|
283
|
+
case "distinct":
|
|
284
|
+
return new Set(values.map(String)).size;
|
|
285
|
+
case "sum":
|
|
286
|
+
return values.reduce((a, b) => a + Number(b), 0);
|
|
287
|
+
case "avg":
|
|
288
|
+
return values.length ? values.reduce((a, b) => a + Number(b), 0) / values.length : 0;
|
|
289
|
+
case "min":
|
|
290
|
+
return Math.min(...values.map(Number));
|
|
291
|
+
case "max":
|
|
292
|
+
return Math.max(...values.map(Number));
|
|
293
|
+
default:
|
|
294
|
+
return records.length;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
function MetricView({ records, config, value: presetValue }) {
|
|
298
|
+
const value = react.useMemo(() => {
|
|
299
|
+
if (presetValue != null) return presetValue;
|
|
300
|
+
if (!records?.length) return 0;
|
|
301
|
+
return aggregate(records, config.column, config.agg || "count");
|
|
302
|
+
}, [records, config, presetValue]);
|
|
303
|
+
const formatted = typeof value === "number" ? value.toLocaleString(void 0, { maximumFractionDigits: 2 }) : value;
|
|
304
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex flex-col items-center justify-center", children: [
|
|
305
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-4xl font-mono text-midnight-accent", children: formatted }),
|
|
306
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-midnight-text-muted mt-2", children: config.label || `${config.agg}(${config.column})` })
|
|
307
|
+
] });
|
|
308
|
+
}
|
|
309
|
+
function InsightView({ config }) {
|
|
310
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full p-4 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-midnight-text-body leading-relaxed whitespace-pre-wrap", children: config.text }) });
|
|
311
|
+
}
|
|
312
|
+
function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: presetData, style }) {
|
|
313
|
+
const computed = react.useMemo(() => {
|
|
314
|
+
if (presetData || !records) return [];
|
|
315
|
+
const groups = groupBy(records, groupColumn);
|
|
316
|
+
return Object.entries(groups).map(([key, recs]) => ({ group: key, value: aggregate(recs, valueColumn, aggFn) })).sort((a, b) => b.value - a.value).slice(0, 50);
|
|
317
|
+
}, [records, groupColumn, valueColumn, aggFn, presetData]);
|
|
318
|
+
const data = presetData || computed;
|
|
319
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
320
|
+
bar.ResponsiveBar,
|
|
321
|
+
{
|
|
322
|
+
data,
|
|
323
|
+
keys: ["value"],
|
|
324
|
+
indexBy: "group",
|
|
325
|
+
margin: { top: 20, right: 20, bottom: 60, left: 60 },
|
|
326
|
+
padding: 0.3,
|
|
327
|
+
colors: ["rgba(74, 222, 128, 0.8)"],
|
|
328
|
+
borderColor: { from: "color", modifiers: [["darker", 1.6]] },
|
|
329
|
+
axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35 },
|
|
330
|
+
axisLeft: { tickSize: 5, tickPadding: 5, format: (v) => Number(v).toLocaleString() },
|
|
331
|
+
labelSkipWidth: 12,
|
|
332
|
+
labelSkipHeight: 12,
|
|
333
|
+
labelTextColor: { from: "color", modifiers: [["darker", 3]] },
|
|
334
|
+
theme: buildNivoTheme(style)
|
|
335
|
+
}
|
|
336
|
+
) });
|
|
337
|
+
}
|
|
338
|
+
function PieView({ records, groupColumn, data: presetData, style }) {
|
|
339
|
+
const computed = react.useMemo(() => {
|
|
340
|
+
if (presetData || !records) return [];
|
|
341
|
+
const groups = groupBy(records, groupColumn);
|
|
342
|
+
const sorted = Object.entries(groups).sort((a, b) => b[1].length - a[1].length);
|
|
343
|
+
const top = sorted.slice(0, 12);
|
|
344
|
+
const otherCount = sorted.slice(12).reduce((sum, [, recs]) => sum + recs.length, 0);
|
|
345
|
+
const result = top.map(([key, recs]) => ({ id: key, label: key, value: recs.length }));
|
|
346
|
+
if (otherCount > 0) result.push({ id: "(other)", label: "(other)", value: otherCount });
|
|
347
|
+
return result;
|
|
348
|
+
}, [records, groupColumn, presetData]);
|
|
349
|
+
const data = presetData || computed;
|
|
350
|
+
const legend = legendConfig(style);
|
|
351
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
352
|
+
pie.ResponsivePie,
|
|
353
|
+
{
|
|
354
|
+
data,
|
|
355
|
+
margin: { top: 20, right: 120, bottom: 20, left: 20 },
|
|
356
|
+
innerRadius: 0.4,
|
|
357
|
+
padAngle: 1,
|
|
358
|
+
cornerRadius: 3,
|
|
359
|
+
activeOuterRadiusOffset: 8,
|
|
360
|
+
colors: { scheme: "set2" },
|
|
361
|
+
borderWidth: 1,
|
|
362
|
+
borderColor: { from: "color", modifiers: [["darker", 0.2]] },
|
|
363
|
+
arcLinkLabelsSkipAngle: 10,
|
|
364
|
+
arcLinkLabelsTextColor: "#94a3b8",
|
|
365
|
+
arcLinkLabelsColor: { from: "color" },
|
|
366
|
+
arcLabelsSkipAngle: 10,
|
|
367
|
+
arcLabelsTextColor: { from: "color", modifiers: [["darker", 3]] },
|
|
368
|
+
legends: legend ? [legend] : [],
|
|
369
|
+
theme: buildNivoTheme(style)
|
|
370
|
+
}
|
|
371
|
+
) });
|
|
372
|
+
}
|
|
373
|
+
function LineView({ records, xColumn, yColumn, data: presetData, style }) {
|
|
374
|
+
const computed = react.useMemo(() => {
|
|
375
|
+
if (presetData || !records) return [];
|
|
376
|
+
const sorted = [...records].filter((r) => r[xColumn] != null && r[yColumn] != null).sort((a, b) => {
|
|
377
|
+
const av = a[xColumn];
|
|
378
|
+
const bv = b[xColumn];
|
|
379
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
380
|
+
}).slice(0, 500);
|
|
381
|
+
return [{
|
|
382
|
+
id: yColumn,
|
|
383
|
+
data: sorted.map((r) => ({ x: String(r[xColumn]), y: Number(r[yColumn]) || 0 }))
|
|
384
|
+
}];
|
|
385
|
+
}, [records, xColumn, yColumn, presetData]);
|
|
386
|
+
const data = presetData || computed;
|
|
387
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
388
|
+
line.ResponsiveLine,
|
|
389
|
+
{
|
|
390
|
+
data,
|
|
391
|
+
margin: { top: 20, right: 20, bottom: 60, left: 60 },
|
|
392
|
+
xScale: { type: "point" },
|
|
393
|
+
yScale: { type: "linear", min: "auto", max: "auto" },
|
|
394
|
+
curve: "monotoneX",
|
|
395
|
+
enableArea: true,
|
|
396
|
+
areaOpacity: 0.15,
|
|
397
|
+
colors: ["rgba(96, 165, 250, 0.9)"],
|
|
398
|
+
pointSize: (data[0]?.data.length ?? 0) > 50 ? 0 : 6,
|
|
399
|
+
pointColor: { theme: "background" },
|
|
400
|
+
pointBorderWidth: 2,
|
|
401
|
+
pointBorderColor: { from: "serieColor" },
|
|
402
|
+
enableGridX: false,
|
|
403
|
+
axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35, ...axisLegend(style, "x", xColumn) },
|
|
404
|
+
axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
|
|
405
|
+
useMesh: true,
|
|
406
|
+
theme: buildNivoTheme(style)
|
|
407
|
+
}
|
|
408
|
+
) });
|
|
409
|
+
}
|
|
410
|
+
function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
|
|
411
|
+
const computed = react.useMemo(() => {
|
|
412
|
+
if (presetData || !records) return [];
|
|
413
|
+
const points = records.filter((r) => r[xColumn] != null && r[yColumn] != null).map((r) => ({ x: Number(r[xColumn]) || 0, y: Number(r[yColumn]) || 0 })).slice(0, 1e3);
|
|
414
|
+
return [{ id: `${xColumn} vs ${yColumn}`, data: points }];
|
|
415
|
+
}, [records, xColumn, yColumn, presetData]);
|
|
416
|
+
const data = presetData || computed;
|
|
417
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
418
|
+
scatterplot.ResponsiveScatterPlot,
|
|
419
|
+
{
|
|
420
|
+
data,
|
|
421
|
+
margin: { top: 20, right: 20, bottom: 60, left: 60 },
|
|
422
|
+
xScale: { type: "linear", min: "auto", max: "auto" },
|
|
423
|
+
yScale: { type: "linear", min: "auto", max: "auto" },
|
|
424
|
+
colors: ["rgba(167, 139, 250, 0.7)"],
|
|
425
|
+
nodeSize: 6,
|
|
426
|
+
axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
|
|
427
|
+
axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
|
|
428
|
+
useMesh: true,
|
|
429
|
+
theme: buildNivoTheme(style)
|
|
430
|
+
}
|
|
431
|
+
) });
|
|
432
|
+
}
|
|
433
|
+
var MAX_AXIS = 30;
|
|
434
|
+
var reduceVals = (vals, agg) => {
|
|
435
|
+
const nums = vals.map(Number).filter((v) => !Number.isNaN(v));
|
|
436
|
+
switch (agg) {
|
|
437
|
+
case "avg":
|
|
438
|
+
return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
|
|
439
|
+
case "min":
|
|
440
|
+
return nums.length ? Math.min(...nums) : 0;
|
|
441
|
+
case "max":
|
|
442
|
+
return nums.length ? Math.max(...nums) : 0;
|
|
443
|
+
case "count":
|
|
444
|
+
return vals.length;
|
|
445
|
+
case "sum":
|
|
446
|
+
default:
|
|
447
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
var fmtNum = (n) => {
|
|
451
|
+
if (typeof n !== "number" || Number.isNaN(n)) return "";
|
|
452
|
+
return Number.isInteger(n) ? n.toLocaleString() : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
453
|
+
};
|
|
454
|
+
var orderIds = (ids, marginals, order) => {
|
|
455
|
+
const sorted = [...ids];
|
|
456
|
+
if (order === "total-desc") sorted.sort((a, b) => (marginals[b] || 0) - (marginals[a] || 0));
|
|
457
|
+
else if (order === "total-asc") sorted.sort((a, b) => (marginals[a] || 0) - (marginals[b] || 0));
|
|
458
|
+
else sorted.sort();
|
|
459
|
+
return sorted;
|
|
460
|
+
};
|
|
461
|
+
function HeatmapView({
|
|
462
|
+
records,
|
|
463
|
+
rowColumn,
|
|
464
|
+
colColumn,
|
|
465
|
+
valueColumn,
|
|
466
|
+
aggFn = "count",
|
|
467
|
+
data: presetData,
|
|
468
|
+
rowOrder = "alpha",
|
|
469
|
+
colOrder = "alpha",
|
|
470
|
+
marginAgg = "sum",
|
|
471
|
+
showTotals = false,
|
|
472
|
+
style
|
|
473
|
+
}) {
|
|
474
|
+
const s = withStyleDefaults(style);
|
|
475
|
+
const base = react.useMemo(() => {
|
|
476
|
+
if (presetData) return presetData;
|
|
477
|
+
if (!records) return [];
|
|
478
|
+
const rowGroups = groupBy(records, rowColumn);
|
|
479
|
+
const colValues = [...new Set(records.map((r) => String(r[colColumn] ?? "(null)")))];
|
|
480
|
+
const fn = valueColumn ? aggFn : "count";
|
|
481
|
+
return Object.keys(rowGroups).map((rowVal) => ({
|
|
482
|
+
id: rowVal,
|
|
483
|
+
data: colValues.map((colVal) => {
|
|
484
|
+
const cellRecords = rowGroups[rowVal]?.filter((r) => String(r[colColumn] ?? "(null)") === colVal) || [];
|
|
485
|
+
return { x: colVal, y: cellRecords.length && valueColumn ? aggregate(cellRecords, valueColumn, fn) : cellRecords.length };
|
|
486
|
+
})
|
|
487
|
+
}));
|
|
488
|
+
}, [records, rowColumn, colColumn, valueColumn, aggFn, presetData]);
|
|
489
|
+
const { data, rowTotals, colTotals } = react.useMemo(() => {
|
|
490
|
+
if (!base.length) return { data: [], rowTotals: {}, colTotals: {} };
|
|
491
|
+
const cell = {};
|
|
492
|
+
const colIdSet = [];
|
|
493
|
+
for (const row of base) {
|
|
494
|
+
cell[row.id] = {};
|
|
495
|
+
for (const d of row.data) {
|
|
496
|
+
cell[row.id][d.x] = Number(d.y) || 0;
|
|
497
|
+
if (!colIdSet.includes(d.x)) colIdSet.push(d.x);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const rowIds = base.map((r) => r.id);
|
|
501
|
+
const rowMarg = {};
|
|
502
|
+
rowIds.forEach((rid) => {
|
|
503
|
+
rowMarg[rid] = reduceVals(colIdSet.map((cid) => cell[rid][cid] ?? 0), marginAgg);
|
|
504
|
+
});
|
|
505
|
+
const colMarg = {};
|
|
506
|
+
colIdSet.forEach((cid) => {
|
|
507
|
+
colMarg[cid] = reduceVals(rowIds.map((rid) => cell[rid][cid] ?? 0), marginAgg);
|
|
508
|
+
});
|
|
509
|
+
const orderedRows = orderIds(rowIds, rowMarg, rowOrder).slice(0, MAX_AXIS);
|
|
510
|
+
const orderedCols = orderIds(colIdSet, colMarg, colOrder).slice(0, MAX_AXIS);
|
|
511
|
+
const shaped = orderedRows.map((rid) => ({
|
|
512
|
+
id: rid,
|
|
513
|
+
data: orderedCols.map((cid) => ({ x: cid, y: cell[rid][cid] ?? 0 }))
|
|
514
|
+
}));
|
|
515
|
+
return { data: shaped, rowTotals: rowMarg, colTotals: colMarg };
|
|
516
|
+
}, [base, rowOrder, colOrder, marginAgg]);
|
|
517
|
+
if (!data.length || !data[0].data.length) {
|
|
518
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-midnight-text-muted", children: "Not enough distinct values for a heatmap" });
|
|
519
|
+
}
|
|
520
|
+
const margin = showTotals ? { top: 60, right: 70, bottom: 60, left: 100 } : { top: 60, right: 20, bottom: 20, left: 100 };
|
|
521
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
522
|
+
heatmap.ResponsiveHeatMap,
|
|
523
|
+
{
|
|
524
|
+
data,
|
|
525
|
+
margin,
|
|
526
|
+
axisTop: { tickSize: 5, tickPadding: 5, tickRotation: -35, legend: colColumn, legendPosition: s.xLegendPosition, legendOffset: -50 },
|
|
527
|
+
axisLeft: { tickSize: 5, tickPadding: 5, legend: rowColumn, legendPosition: s.yLegendPosition, legendOffset: -80 },
|
|
528
|
+
axisRight: showTotals ? { tickSize: 5, tickPadding: 5, format: (id) => fmtNum(rowTotals[id]), legend: `${marginAgg} \u25B8`, legendOffset: 60 } : null,
|
|
529
|
+
axisBottom: showTotals ? { tickSize: 5, tickPadding: 5, tickRotation: -35, format: (id) => fmtNum(colTotals[id]) } : null,
|
|
530
|
+
colors: { type: "sequential", scheme: "blue_green", minValue: 0 },
|
|
531
|
+
emptyColor: "#1e293b",
|
|
532
|
+
borderWidth: 1,
|
|
533
|
+
borderColor: "#334155",
|
|
534
|
+
labelTextColor: { from: "color", modifiers: [["darker", 3]] },
|
|
535
|
+
hoverTarget: "cell",
|
|
536
|
+
theme: buildNivoTheme(style)
|
|
537
|
+
}
|
|
538
|
+
) });
|
|
539
|
+
}
|
|
540
|
+
function PivotView({ records }) {
|
|
541
|
+
const [pivotState, setPivotState] = react.useState({});
|
|
542
|
+
if (!records?.length) {
|
|
543
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-midnight-text-muted", children: "No data for pivot" });
|
|
544
|
+
}
|
|
545
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full overflow-auto pivot-dark-theme", children: [
|
|
546
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: `
|
|
547
|
+
/* Override react-pivottable styles for dark theme */
|
|
548
|
+
.pivot-dark-theme .pvtUi { background: transparent; color: #e2e8f0; }
|
|
549
|
+
.pivot-dark-theme table.pvtTable { font-size: 12px; color: #e2e8f0; border-collapse: collapse; }
|
|
550
|
+
.pivot-dark-theme table.pvtTable thead tr th,
|
|
551
|
+
.pivot-dark-theme table.pvtTable tbody tr th { background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 4px 8px; }
|
|
552
|
+
.pivot-dark-theme table.pvtTable tbody tr td { background: #0f172a; color: #e2e8f0; border: 1px solid #334155; padding: 4px 8px; text-align: right; }
|
|
553
|
+
.pivot-dark-theme .pvtAxisContainer, .pivot-dark-theme .pvtVals { background: #1e293b; border: 1px solid #334155; }
|
|
554
|
+
.pivot-dark-theme .pvtAxisContainer li.pvtAxis { background: #334155; color: #e2e8f0; border: 1px solid #475569; border-radius: 3px; }
|
|
555
|
+
.pivot-dark-theme .pvtFilterBox { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; }
|
|
556
|
+
.pivot-dark-theme .pvtDropdown { background: #1e293b; color: #e2e8f0; border: 1px solid #334155; }
|
|
557
|
+
.pivot-dark-theme .pvtSearch { background: #0f172a; color: #e2e8f0; border: 1px solid #334155; }
|
|
558
|
+
.pivot-dark-theme select, .pivot-dark-theme .pvtAggregator { background: #1e293b; color: #e2e8f0; border: 1px solid #334155; }
|
|
559
|
+
.pivot-dark-theme .pvtRenderers { background: #1e293b; color: #e2e8f0; border: 1px solid #334155; }
|
|
560
|
+
` }),
|
|
561
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
562
|
+
PivotTableUI__default.default,
|
|
563
|
+
{
|
|
564
|
+
data: records,
|
|
565
|
+
onChange: (s) => setPivotState(s),
|
|
566
|
+
...pivotState
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
] });
|
|
570
|
+
}
|
|
571
|
+
function Row({ label, children }) {
|
|
572
|
+
const { theme } = useDashboard();
|
|
573
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
|
|
574
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs ${theme.font} text-midnight-text-muted`, children: label }),
|
|
575
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", children })
|
|
576
|
+
] });
|
|
577
|
+
}
|
|
578
|
+
function ColorControl({ value, onChange }) {
|
|
579
|
+
const { theme } = useDashboard();
|
|
580
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
581
|
+
"input",
|
|
582
|
+
{
|
|
583
|
+
type: "color",
|
|
584
|
+
value: /^#/.test(value) ? value : "#94a3b8",
|
|
585
|
+
onChange: (e) => onChange(e.target.value),
|
|
586
|
+
className: `w-9 h-6 bg-transparent border ${theme.border} cursor-pointer`
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
function NumberControl({ value, onChange, min, max }) {
|
|
591
|
+
const { theme } = useDashboard();
|
|
592
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
593
|
+
"input",
|
|
594
|
+
{
|
|
595
|
+
type: "number",
|
|
596
|
+
value,
|
|
597
|
+
min,
|
|
598
|
+
max,
|
|
599
|
+
onChange: (e) => onChange(Number(e.target.value)),
|
|
600
|
+
className: `w-20 px-2 py-1 text-xs ${theme.font} bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent`
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
function Choice({ value, options, onChange }) {
|
|
605
|
+
const { theme } = useDashboard();
|
|
606
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-28", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
607
|
+
"select",
|
|
608
|
+
{
|
|
609
|
+
value,
|
|
610
|
+
onChange: (e) => onChange(e.target.value),
|
|
611
|
+
className: `w-full px-2 py-1 text-xs ${theme.font} bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent`,
|
|
612
|
+
children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o, children: o }, o))
|
|
613
|
+
}
|
|
614
|
+
) });
|
|
615
|
+
}
|
|
616
|
+
function Group({ children, last }) {
|
|
617
|
+
const { theme } = useDashboard();
|
|
618
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `space-y-2 pb-3 ${last ? "" : `mb-3 border-b ${theme.border}`}`, children });
|
|
619
|
+
}
|
|
620
|
+
function ChartStyleControls({ style, onChange }) {
|
|
621
|
+
const s = withStyleDefaults(style);
|
|
622
|
+
const set = (key, value) => onChange({ ...s, [key]: value });
|
|
623
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
624
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
|
|
625
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Background", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.background, onChange: (v) => set("background", v) }) }),
|
|
626
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Text color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
|
|
627
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
|
|
628
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Grid color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
|
|
629
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.fontSize, min: 6, max: 24, onChange: (v) => set("fontSize", v) }) })
|
|
630
|
+
] }),
|
|
631
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
|
|
632
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X title pos", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
|
|
633
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X title offset", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.xLegendOffset, onChange: (v) => set("xLegendOffset", v) }) }),
|
|
634
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y title pos", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
|
|
635
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y title offset", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.yLegendOffset, onChange: (v) => set("yLegendOffset", v) }) })
|
|
636
|
+
] }),
|
|
637
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Group, { last: true, children: [
|
|
638
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Legend", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }),
|
|
639
|
+
/* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title align", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) })
|
|
640
|
+
] })
|
|
641
|
+
] });
|
|
642
|
+
}
|
|
643
|
+
var DEFAULT_SQL = "SELECT *\nFROM data\nLIMIT 100";
|
|
644
|
+
var renderCell = (v) => {
|
|
645
|
+
if (v == null) return "";
|
|
646
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
647
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
648
|
+
return String(v);
|
|
649
|
+
};
|
|
650
|
+
function SqlConsole({ columns = [], stateId }) {
|
|
651
|
+
const { theme, runQuery } = useDashboard();
|
|
652
|
+
const [sql, setSql] = react.useState(DEFAULT_SQL);
|
|
653
|
+
const [result, setResult] = react.useState(null);
|
|
654
|
+
const [error, setError] = react.useState(null);
|
|
655
|
+
const [running, setRunning] = react.useState(false);
|
|
656
|
+
const run = async () => {
|
|
657
|
+
if (running || !sql.trim()) return;
|
|
658
|
+
setRunning(true);
|
|
659
|
+
setError(null);
|
|
660
|
+
try {
|
|
661
|
+
const data = await runQuery(sql, stateId);
|
|
662
|
+
setResult({ columns: data.columns || [], rows: data.rows || [] });
|
|
663
|
+
} catch (err) {
|
|
664
|
+
setResult(null);
|
|
665
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
666
|
+
} finally {
|
|
667
|
+
setRunning(false);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
const onKeyDown = (e) => {
|
|
671
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
672
|
+
e.preventDefault();
|
|
673
|
+
void run();
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
const resultColumns = result?.columns || [];
|
|
677
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col h-full", children: [
|
|
678
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border-b ${theme.border} bg-midnight-elevated p-3 shrink-0`, children: [
|
|
679
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
680
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs uppercase text-midnight-text-muted font-mono", children: [
|
|
681
|
+
"SQL \u2014 table is ",
|
|
682
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "text-midnight-accent", children: "data" })
|
|
683
|
+
] }),
|
|
684
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
685
|
+
"button",
|
|
686
|
+
{
|
|
687
|
+
onClick: () => void run(),
|
|
688
|
+
disabled: running || !sql.trim(),
|
|
689
|
+
className: `flex items-center gap-1.5 px-3 py-1 border text-xs font-mono transition-colors ${running ? "border-midnight-border opacity-50" : "border-midnight-accent text-midnight-accent hover:bg-midnight-accent/10"}`,
|
|
690
|
+
children: [
|
|
691
|
+
running ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { className: "w-3.5 h-3.5" }),
|
|
692
|
+
"Run ",
|
|
693
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60", children: "\u2318\u21B5" })
|
|
694
|
+
]
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
] }),
|
|
698
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
699
|
+
"textarea",
|
|
700
|
+
{
|
|
701
|
+
value: sql,
|
|
702
|
+
onChange: (e) => setSql(e.target.value),
|
|
703
|
+
onKeyDown,
|
|
704
|
+
spellCheck: false,
|
|
705
|
+
rows: 5,
|
|
706
|
+
className: "w-full bg-midnight-surface border border-midnight-border px-3 py-2 text-sm font-mono outline-none text-midnight-text-body resize-y focus:border-midnight-accent transition-colors",
|
|
707
|
+
placeholder: "SELECT ... FROM data"
|
|
708
|
+
}
|
|
709
|
+
),
|
|
710
|
+
columns.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex flex-wrap gap-1", children: columns.map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
711
|
+
"button",
|
|
712
|
+
{
|
|
713
|
+
title: `Insert ${c.name}`,
|
|
714
|
+
onClick: () => setSql((s) => `${s}${s.endsWith(" ") || s.endsWith("\n") || !s ? "" : " "}${c.name}`),
|
|
715
|
+
className: "px-1.5 py-0.5 border border-midnight-border text-[11px] font-mono text-midnight-text-muted hover:bg-midnight-raised transition-colors",
|
|
716
|
+
children: [
|
|
717
|
+
c.name,
|
|
718
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-50 ml-1", children: c.type })
|
|
719
|
+
]
|
|
720
|
+
},
|
|
721
|
+
c.name
|
|
722
|
+
)) })
|
|
723
|
+
] }),
|
|
724
|
+
(result || error) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex items-center gap-4 px-3 py-1.5 border-b ${theme.border} text-xs font-mono shrink-0 ${error ? "text-red-400" : "text-midnight-text-muted"}`, children: error ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
725
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "w-3.5 h-3.5" }),
|
|
726
|
+
" ",
|
|
727
|
+
error
|
|
728
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
|
|
729
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Rows3, { className: "w-3.5 h-3.5" }),
|
|
730
|
+
" ",
|
|
731
|
+
result.rows.length.toLocaleString(),
|
|
732
|
+
" rows"
|
|
733
|
+
] }) }),
|
|
734
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: result && !error && resultColumns.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full text-xs", children: [
|
|
735
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: resultColumns.map((c) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-2 py-1.5 text-left text-midnight-text-muted font-mono border-b border-midnight-border whitespace-nowrap", children: c }, c)) }) }),
|
|
736
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: result.rows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: resultColumns.map((c) => /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-2 py-1 text-midnight-text-body font-mono truncate max-w-[300px]", children: renderCell(row[c]) }, c)) }, i)) })
|
|
737
|
+
] }) : !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: [
|
|
738
|
+
"Write a query and press ",
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-accent mx-1", children: "Run" }),
|
|
740
|
+
" (\u2318\u21B5)"
|
|
741
|
+
] }) })
|
|
742
|
+
] });
|
|
743
|
+
}
|
|
744
|
+
var CHART_TYPES = [
|
|
745
|
+
{ id: "bar", icon: lucideReact.BarChart3, label: "Bar" },
|
|
746
|
+
{ id: "pie", icon: lucideReact.PieChart, label: "Pie" },
|
|
747
|
+
{ id: "line", icon: lucideReact.TrendingUp, label: "Line" },
|
|
748
|
+
{ id: "scatter", icon: lucideReact.ScatterChart, label: "Scatter" },
|
|
749
|
+
{ id: "heatmap", icon: lucideReact.LayoutGrid, label: "Heatmap" },
|
|
750
|
+
{ id: "grouped-bar", icon: lucideReact.BarChart3, label: "Grouped Bar" }
|
|
751
|
+
];
|
|
752
|
+
var AGG_OPTIONS = ["count", "distinct", "sum", "avg", "min", "max"];
|
|
753
|
+
var FILTER_OPS = ["=", "!=", "<", "<=", ">", ">=", "IN", "NOT IN", "LIKE", "BETWEEN", "IS NULL", "IS NOT NULL"];
|
|
754
|
+
var OPS_NO_VALUE = /* @__PURE__ */ new Set(["IS NULL", "IS NOT NULL"]);
|
|
755
|
+
var SelectControl = ({ value, options, onChange, className = "w-28" }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
756
|
+
"select",
|
|
757
|
+
{
|
|
758
|
+
value,
|
|
759
|
+
onChange: (e) => onChange(e.target.value),
|
|
760
|
+
className: "w-full px-2 py-1 text-xs font-mono bg-midnight-surface border border-midnight-border text-midnight-text-body outline-none focus:border-midnight-accent transition-colors",
|
|
761
|
+
children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o.id, children: o.label }, o.id))
|
|
762
|
+
}
|
|
763
|
+
) });
|
|
764
|
+
var ORDER_OPTIONS = [
|
|
765
|
+
{ id: "alpha", label: "A \u2192 Z" },
|
|
766
|
+
{ id: "total-desc", label: "Total \u2193" },
|
|
767
|
+
{ id: "total-asc", label: "Total \u2191" }
|
|
768
|
+
];
|
|
769
|
+
var MARGIN_AGGS = ["sum", "avg", "min", "max", "count"].map((a) => ({ id: a, label: a }));
|
|
770
|
+
var FILTER_OP_OPTIONS = FILTER_OPS.map((o) => ({ id: o, label: o }));
|
|
771
|
+
var FieldPill = ({ name, type, onRemove, onChangeAgg, agg, showAgg }) => {
|
|
772
|
+
const typeColor = {
|
|
773
|
+
string: "border-green-500/50 text-green-400",
|
|
774
|
+
int64: "border-blue-500/50 text-blue-400",
|
|
775
|
+
float64: "border-purple-500/50 text-purple-400",
|
|
776
|
+
bool: "border-orange-500/50 text-orange-400"
|
|
777
|
+
}[type ?? ""] || "border-midnight-border text-midnight-text-body";
|
|
778
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center gap-1 px-2 py-1 border ${typeColor} bg-midnight-surface text-xs font-mono`, children: [
|
|
779
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "w-3 h-3 opacity-40" }),
|
|
780
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: name }),
|
|
781
|
+
showAgg && /* @__PURE__ */ jsxRuntime.jsx(
|
|
782
|
+
"select",
|
|
783
|
+
{
|
|
784
|
+
value: agg,
|
|
785
|
+
onChange: (e) => onChangeAgg?.(e.target.value),
|
|
786
|
+
className: "bg-transparent border-none text-xs outline-none ml-1 text-midnight-text-muted",
|
|
787
|
+
children: AGG_OPTIONS.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a, children: a }, a))
|
|
788
|
+
}
|
|
789
|
+
),
|
|
790
|
+
onRemove && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onRemove, className: "ml-1 hover:text-red-400", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "w-3 h-3" }) })
|
|
791
|
+
] });
|
|
792
|
+
};
|
|
793
|
+
var DropZone = ({ label, fields, onAdd, onRemove, onChangeAgg, columns, showAgg = false, maxFields = 5 }) => {
|
|
794
|
+
const [showPicker, setShowPicker] = react.useState(false);
|
|
795
|
+
const usedNames = new Set(fields.map((f) => f.name));
|
|
796
|
+
const available = columns.filter((c) => !usedNames.has(c.name));
|
|
797
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
798
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-1 font-mono", children: label }),
|
|
799
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-1 min-h-[32px] p-1.5 border border-dashed border-midnight-border bg-midnight-surface/50", children: [
|
|
800
|
+
fields.map((f, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
801
|
+
FieldPill,
|
|
802
|
+
{
|
|
803
|
+
name: f.name,
|
|
804
|
+
type: f.type,
|
|
805
|
+
agg: f.agg,
|
|
806
|
+
showAgg,
|
|
807
|
+
onRemove: () => onRemove(i),
|
|
808
|
+
onChangeAgg: (agg) => onChangeAgg(i, agg)
|
|
809
|
+
},
|
|
810
|
+
f.name
|
|
811
|
+
)),
|
|
812
|
+
fields.length < maxFields && available.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
813
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
814
|
+
"button",
|
|
815
|
+
{
|
|
816
|
+
onClick: () => setShowPicker(!showPicker),
|
|
817
|
+
className: "flex items-center gap-1 px-2 py-1 border border-dashed border-midnight-border text-xs text-midnight-text-muted hover:bg-midnight-raised transition-colors",
|
|
818
|
+
children: [
|
|
819
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "w-3 h-3" }),
|
|
820
|
+
" Add"
|
|
821
|
+
]
|
|
822
|
+
}
|
|
823
|
+
),
|
|
824
|
+
showPicker && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-full left-0 mt-1 z-20 bg-midnight-elevated border border-midnight-border shadow-lg max-h-[200px] overflow-auto min-w-[150px]", children: available.map((col) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
825
|
+
"button",
|
|
826
|
+
{
|
|
827
|
+
onClick: () => {
|
|
828
|
+
onAdd({ name: col.name, type: col.type, agg: "count" });
|
|
829
|
+
setShowPicker(false);
|
|
830
|
+
},
|
|
831
|
+
className: "block w-full text-left px-3 py-1.5 text-xs font-mono text-midnight-text-body hover:bg-midnight-raised transition-colors",
|
|
832
|
+
children: [
|
|
833
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: col.name }),
|
|
834
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-midnight-text-muted ml-2", children: [
|
|
835
|
+
"(",
|
|
836
|
+
col.type,
|
|
837
|
+
")"
|
|
838
|
+
] })
|
|
839
|
+
]
|
|
840
|
+
},
|
|
841
|
+
col.name
|
|
842
|
+
)) })
|
|
843
|
+
] })
|
|
844
|
+
] })
|
|
845
|
+
] });
|
|
846
|
+
};
|
|
847
|
+
var FiltersSection = ({ filters, columns, onChange }) => {
|
|
848
|
+
const addFilter = () => onChange([...filters, { column: columns[0]?.name || "", op: "=", value: "" }]);
|
|
849
|
+
const updateFilter = (i, patch) => onChange(filters.map((f, idx) => idx === i ? { ...f, ...patch } : f));
|
|
850
|
+
const removeFilter = (i) => onChange(filters.filter((_, idx) => idx !== i));
|
|
851
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
852
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
|
|
853
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs uppercase text-midnight-text-muted font-mono flex items-center gap-1", children: [
|
|
854
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Filter, { className: "w-3 h-3" }),
|
|
855
|
+
" Filters"
|
|
856
|
+
] }),
|
|
857
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
858
|
+
"button",
|
|
859
|
+
{
|
|
860
|
+
onClick: addFilter,
|
|
861
|
+
disabled: !columns.length,
|
|
862
|
+
className: "flex items-center gap-1 px-1.5 py-0.5 border border-dashed border-midnight-border text-xs text-midnight-text-muted hover:bg-midnight-raised transition-colors",
|
|
863
|
+
children: [
|
|
864
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "w-3 h-3" }),
|
|
865
|
+
" Add"
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
] }),
|
|
870
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
871
|
+
filters.map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
872
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
873
|
+
SelectControl,
|
|
874
|
+
{
|
|
875
|
+
value: f.column,
|
|
876
|
+
options: columns.map((c) => ({ id: c.name, label: c.name })),
|
|
877
|
+
onChange: (colId) => updateFilter(i, { column: colId }),
|
|
878
|
+
className: "flex-1 min-w-0"
|
|
879
|
+
}
|
|
880
|
+
),
|
|
881
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
882
|
+
SelectControl,
|
|
883
|
+
{
|
|
884
|
+
value: f.op,
|
|
885
|
+
options: FILTER_OP_OPTIONS,
|
|
886
|
+
onChange: (op) => updateFilter(i, { op }),
|
|
887
|
+
className: "w-24 shrink-0"
|
|
888
|
+
}
|
|
889
|
+
),
|
|
890
|
+
!OPS_NO_VALUE.has(f.op) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
891
|
+
"input",
|
|
892
|
+
{
|
|
893
|
+
value: f.value ?? "",
|
|
894
|
+
onChange: (e) => updateFilter(i, { value: e.target.value }),
|
|
895
|
+
placeholder: f.op === "IN" || f.op === "NOT IN" ? "a,b,c" : f.op === "BETWEEN" ? "lo,hi" : "value",
|
|
896
|
+
className: "w-full px-2 py-1 text-xs font-mono bg-midnight-surface border border-midnight-border text-midnight-text-body outline-none focus:border-midnight-accent transition-colors"
|
|
897
|
+
}
|
|
898
|
+
) }),
|
|
899
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => removeFilter(i), className: "text-midnight-text-muted hover:text-red-400", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "w-3 h-3" }) })
|
|
900
|
+
] }, i)),
|
|
901
|
+
!filters.length && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-midnight-text-muted font-mono px-1", children: "No filters" })
|
|
902
|
+
] })
|
|
903
|
+
] });
|
|
904
|
+
};
|
|
905
|
+
var GroupedBarChart = ({ records, xFields = [], yFields = [], groupField, data: presetData, keys: presetKeys, style }) => {
|
|
906
|
+
const computed = react.useMemo(() => {
|
|
907
|
+
if (presetData || !records || !xFields.length || !yFields.length) return [];
|
|
908
|
+
const xCol = xFields[0].name;
|
|
909
|
+
const groups = groupBy(records, xCol);
|
|
910
|
+
return Object.entries(groups).map(([key, recs]) => {
|
|
911
|
+
const row = { group: key };
|
|
912
|
+
yFields.forEach((yf) => {
|
|
913
|
+
row[`${yf.agg}(${yf.name})`] = aggregate(recs, yf.name, yf.agg ?? "count");
|
|
914
|
+
});
|
|
915
|
+
return row;
|
|
916
|
+
}).sort((a, b) => {
|
|
917
|
+
const firstKey = `${yFields[0].agg}(${yFields[0].name})`;
|
|
918
|
+
return (Number(b[firstKey]) || 0) - (Number(a[firstKey]) || 0);
|
|
919
|
+
}).slice(0, 50);
|
|
920
|
+
}, [records, xFields, yFields, presetData]);
|
|
921
|
+
const data = presetData || computed;
|
|
922
|
+
if (!data.length) return null;
|
|
923
|
+
const keys = presetKeys || yFields.map((yf) => `${yf.agg}(${yf.name})`);
|
|
924
|
+
const colors = ["rgba(74,222,128,0.8)", "rgba(96,165,250,0.8)", "rgba(251,146,60,0.8)", "rgba(167,139,250,0.8)", "rgba(248,113,113,0.8)"];
|
|
925
|
+
const legend = legendConfig(style);
|
|
926
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
927
|
+
bar.ResponsiveBar,
|
|
928
|
+
{
|
|
929
|
+
data,
|
|
930
|
+
keys,
|
|
931
|
+
indexBy: "group",
|
|
932
|
+
groupMode: groupField ? "grouped" : "stacked",
|
|
933
|
+
margin: { top: 20, right: 120, bottom: 60, left: 60 },
|
|
934
|
+
padding: 0.3,
|
|
935
|
+
colors: colors.slice(0, keys.length),
|
|
936
|
+
axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35 },
|
|
937
|
+
axisLeft: { tickSize: 5, tickPadding: 5, format: (v) => Number(v).toLocaleString() },
|
|
938
|
+
labelSkipWidth: 12,
|
|
939
|
+
labelSkipHeight: 12,
|
|
940
|
+
labelTextColor: { from: "color", modifiers: [["darker", 3]] },
|
|
941
|
+
legends: legend ? [{ dataFrom: "keys", ...legend }] : [],
|
|
942
|
+
theme: buildNivoTheme(style)
|
|
943
|
+
}
|
|
944
|
+
) });
|
|
945
|
+
};
|
|
946
|
+
var ChartPreview = ({
|
|
947
|
+
chartType,
|
|
948
|
+
records,
|
|
949
|
+
xFields,
|
|
950
|
+
yFields,
|
|
951
|
+
valueField,
|
|
952
|
+
groupField,
|
|
953
|
+
engineMode,
|
|
954
|
+
sqlRows,
|
|
955
|
+
sqlLoading,
|
|
956
|
+
sqlError,
|
|
957
|
+
heatmapOpts = {},
|
|
958
|
+
style
|
|
959
|
+
}) => {
|
|
960
|
+
const xCol = xFields[0]?.name;
|
|
961
|
+
const yCol = yFields[0]?.name;
|
|
962
|
+
const yAgg = yFields[0]?.agg || "count";
|
|
963
|
+
if (engineMode === "sql") {
|
|
964
|
+
if (sqlError) {
|
|
965
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center h-full text-sm text-red-400 gap-2 px-4 text-center", children: [
|
|
966
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "w-4 h-4 shrink-0" }),
|
|
967
|
+
" ",
|
|
968
|
+
sqlError
|
|
969
|
+
] });
|
|
970
|
+
}
|
|
971
|
+
if (sqlLoading && !sqlRows) {
|
|
972
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted gap-2", children: [
|
|
973
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-4 h-4 animate-spin" }),
|
|
974
|
+
" Running query\u2026"
|
|
975
|
+
] });
|
|
976
|
+
}
|
|
977
|
+
if (!sqlRows) {
|
|
978
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Configure the chart to run a query" });
|
|
979
|
+
}
|
|
980
|
+
if (chartType === "grouped-bar" || chartType === "bar" && (yFields.length > 1 || groupField)) {
|
|
981
|
+
const { data, keys } = shapeChartData("grouped-bar", sqlRows, { yFields });
|
|
982
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GroupedBarChart, { data, keys, yFields, groupField, style });
|
|
983
|
+
}
|
|
984
|
+
const shaped = shapeChartData(chartType, sqlRows, { yFields });
|
|
985
|
+
switch (chartType) {
|
|
986
|
+
case "bar":
|
|
987
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(BarView, { data: shaped, groupColumn: xCol, valueColumn: yCol, style }) });
|
|
988
|
+
case "pie":
|
|
989
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(PieView, { data: shaped, groupColumn: xCol, style }) });
|
|
990
|
+
case "line":
|
|
991
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(LineView, { data: shaped, xColumn: xCol, yColumn: yCol, style }) });
|
|
992
|
+
case "scatter":
|
|
993
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { data: shaped, xColumn: xCol, yColumn: yCol, style }) });
|
|
994
|
+
case "heatmap":
|
|
995
|
+
return shaped.length ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { data: shaped, rowColumn: xCol, colColumn: yCol, ...heatmapOpts, style }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "No data" });
|
|
996
|
+
default:
|
|
997
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (!records?.length) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "No data" });
|
|
1001
|
+
switch (chartType) {
|
|
1002
|
+
case "bar":
|
|
1003
|
+
if (yFields.length > 1 || groupField) {
|
|
1004
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GroupedBarChart, { records, xFields, yFields, groupField, style });
|
|
1005
|
+
}
|
|
1006
|
+
return xCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(BarView, { records, groupColumn: xCol, valueColumn: yCol, aggFn: yAgg, style }) }) : null;
|
|
1007
|
+
case "grouped-bar":
|
|
1008
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GroupedBarChart, { records, xFields, yFields, groupField, style });
|
|
1009
|
+
case "pie":
|
|
1010
|
+
return xCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(PieView, { records, groupColumn: xCol, style }) }) : null;
|
|
1011
|
+
case "line":
|
|
1012
|
+
return xCol && yCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(LineView, { records, xColumn: xCol, yColumn: yCol, style }) }) : null;
|
|
1013
|
+
case "scatter":
|
|
1014
|
+
return xCol && yCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { records, xColumn: xCol, yColumn: yCol, style }) }) : null;
|
|
1015
|
+
case "heatmap":
|
|
1016
|
+
return xCol && yCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { records, rowColumn: xCol, colColumn: yCol, valueColumn: valueField?.name, aggFn: valueField?.agg || "count", ...heatmapOpts, style }) }) : null;
|
|
1017
|
+
default:
|
|
1018
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
function ChartBuilder({ records, columns, stateId, onSave }) {
|
|
1022
|
+
const { theme, runQuery } = useDashboard();
|
|
1023
|
+
const [chartType, setChartType] = react.useState("bar");
|
|
1024
|
+
const [xFields, setXFields] = react.useState([]);
|
|
1025
|
+
const [yFields, setYFields] = react.useState([]);
|
|
1026
|
+
const [valueField, setValueField] = react.useState(null);
|
|
1027
|
+
const [groupField, setGroupField] = react.useState(null);
|
|
1028
|
+
const [colorField, setColorField] = react.useState(null);
|
|
1029
|
+
const [title, setTitle] = react.useState("");
|
|
1030
|
+
const [engineMode, setEngineMode] = react.useState("sql");
|
|
1031
|
+
const [filters, setFilters] = react.useState([]);
|
|
1032
|
+
const [showSql, setShowSql] = react.useState(false);
|
|
1033
|
+
const [rowOrder, setRowOrder] = react.useState("alpha");
|
|
1034
|
+
const [colOrder, setColOrder] = react.useState("alpha");
|
|
1035
|
+
const [marginAgg, setMarginAgg] = react.useState("sum");
|
|
1036
|
+
const [showTotals, setShowTotals] = react.useState(false);
|
|
1037
|
+
const [style, setStyle] = react.useState(DEFAULT_CHART_STYLE);
|
|
1038
|
+
const [showStyle, setShowStyle] = react.useState(false);
|
|
1039
|
+
const [sqlRows, setSqlRows] = react.useState(null);
|
|
1040
|
+
const [sqlLoading, setSqlLoading] = react.useState(false);
|
|
1041
|
+
const [sqlError, setSqlError] = react.useState(null);
|
|
1042
|
+
const generatedSql = react.useMemo(
|
|
1043
|
+
() => buildChartSQL({ chartType, xFields, yFields, valueField, filters }),
|
|
1044
|
+
[chartType, xFields, yFields, valueField, filters]
|
|
1045
|
+
);
|
|
1046
|
+
react.useEffect(() => {
|
|
1047
|
+
if (engineMode !== "sql") return void 0;
|
|
1048
|
+
if (!generatedSql) {
|
|
1049
|
+
setSqlRows(null);
|
|
1050
|
+
setSqlError(null);
|
|
1051
|
+
return void 0;
|
|
1052
|
+
}
|
|
1053
|
+
let cancelled = false;
|
|
1054
|
+
setSqlLoading(true);
|
|
1055
|
+
setSqlError(null);
|
|
1056
|
+
runQuery(generatedSql, stateId).then((res) => {
|
|
1057
|
+
if (cancelled) return;
|
|
1058
|
+
setSqlLoading(false);
|
|
1059
|
+
setSqlRows(res.rows || []);
|
|
1060
|
+
}).catch((err) => {
|
|
1061
|
+
if (cancelled) return;
|
|
1062
|
+
setSqlLoading(false);
|
|
1063
|
+
setSqlRows(null);
|
|
1064
|
+
setSqlError(err instanceof Error ? err.message : String(err) || "Query failed");
|
|
1065
|
+
});
|
|
1066
|
+
return () => {
|
|
1067
|
+
cancelled = true;
|
|
1068
|
+
};
|
|
1069
|
+
}, [engineMode, generatedSql, runQuery, stateId]);
|
|
1070
|
+
const addField = (zone, field) => {
|
|
1071
|
+
switch (zone) {
|
|
1072
|
+
case "x":
|
|
1073
|
+
setXFields((f) => [...f, field]);
|
|
1074
|
+
break;
|
|
1075
|
+
case "y":
|
|
1076
|
+
setYFields((f) => [...f, field]);
|
|
1077
|
+
break;
|
|
1078
|
+
case "value":
|
|
1079
|
+
setValueField(field);
|
|
1080
|
+
break;
|
|
1081
|
+
case "group":
|
|
1082
|
+
setGroupField(field);
|
|
1083
|
+
break;
|
|
1084
|
+
case "color":
|
|
1085
|
+
setColorField(field);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
const removeField = (zone, idx) => {
|
|
1090
|
+
switch (zone) {
|
|
1091
|
+
case "x":
|
|
1092
|
+
setXFields((f) => f.filter((_, i) => i !== idx));
|
|
1093
|
+
break;
|
|
1094
|
+
case "y":
|
|
1095
|
+
setYFields((f) => f.filter((_, i) => i !== idx));
|
|
1096
|
+
break;
|
|
1097
|
+
case "value":
|
|
1098
|
+
setValueField(null);
|
|
1099
|
+
break;
|
|
1100
|
+
case "group":
|
|
1101
|
+
setGroupField(null);
|
|
1102
|
+
break;
|
|
1103
|
+
case "color":
|
|
1104
|
+
setColorField(null);
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
const changeAgg = (zone, idx, agg) => {
|
|
1109
|
+
if (zone === "y") {
|
|
1110
|
+
setYFields((f) => f.map((field, i) => i === idx ? { ...field, agg } : field));
|
|
1111
|
+
} else if (zone === "value") {
|
|
1112
|
+
setValueField((f) => f ? { ...f, agg } : f);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
const handleSaveToDashboard = () => {
|
|
1116
|
+
if (!onSave) return;
|
|
1117
|
+
const config = {};
|
|
1118
|
+
if (xFields.length) config.group = xFields[0].name;
|
|
1119
|
+
if (yFields.length) {
|
|
1120
|
+
config.value = yFields[0].name;
|
|
1121
|
+
config.agg = yFields[0].agg;
|
|
1122
|
+
}
|
|
1123
|
+
if (chartType === "line" || chartType === "scatter") {
|
|
1124
|
+
config.x = xFields[0]?.name;
|
|
1125
|
+
config.y = yFields[0]?.name;
|
|
1126
|
+
}
|
|
1127
|
+
if (chartType === "heatmap") {
|
|
1128
|
+
config.row = xFields[0]?.name;
|
|
1129
|
+
config.col = yFields[0]?.name;
|
|
1130
|
+
if (valueField) {
|
|
1131
|
+
config.value = valueField.name;
|
|
1132
|
+
config.agg = valueField.agg || "count";
|
|
1133
|
+
} else {
|
|
1134
|
+
delete config.value;
|
|
1135
|
+
delete config.agg;
|
|
1136
|
+
}
|
|
1137
|
+
config.rowOrder = rowOrder;
|
|
1138
|
+
config.colOrder = colOrder;
|
|
1139
|
+
config.marginAgg = marginAgg;
|
|
1140
|
+
config.showTotals = showTotals;
|
|
1141
|
+
}
|
|
1142
|
+
if (engineMode === "sql" && generatedSql) {
|
|
1143
|
+
config.sql = generatedSql;
|
|
1144
|
+
config.chartType = chartType;
|
|
1145
|
+
if (filters.length) config.filters = filters;
|
|
1146
|
+
if (yFields.length) config.yFields = yFields.map((f) => ({ name: f.name, agg: f.agg }));
|
|
1147
|
+
}
|
|
1148
|
+
config.style = style;
|
|
1149
|
+
onSave({
|
|
1150
|
+
title: title || `${chartType} chart`,
|
|
1151
|
+
type: chartType === "grouped-bar" ? "bar" : chartType,
|
|
1152
|
+
config,
|
|
1153
|
+
width: 6,
|
|
1154
|
+
height: 2
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full", children: [
|
|
1158
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `w-[280px] shrink-0 border-r ${theme.border} bg-midnight-elevated overflow-y-auto p-3 space-y-4`, children: [
|
|
1159
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1160
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Chart Type" }),
|
|
1161
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 gap-1", children: CHART_TYPES.map((ct) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1162
|
+
"button",
|
|
1163
|
+
{
|
|
1164
|
+
onClick: () => setChartType(ct.id),
|
|
1165
|
+
className: `flex flex-col items-center gap-1 p-2 border text-xs font-mono transition-colors ${chartType === ct.id ? "border-midnight-accent text-midnight-accent bg-midnight-accent/10" : "border-midnight-border text-midnight-text-muted hover:bg-midnight-raised"}`,
|
|
1166
|
+
children: [
|
|
1167
|
+
/* @__PURE__ */ jsxRuntime.jsx(ct.icon, { className: "w-4 h-4" }),
|
|
1168
|
+
ct.label
|
|
1169
|
+
]
|
|
1170
|
+
},
|
|
1171
|
+
ct.id
|
|
1172
|
+
)) })
|
|
1173
|
+
] }),
|
|
1174
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1175
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-1 font-mono", children: "Query Engine" }),
|
|
1176
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex border border-midnight-border", children: [
|
|
1177
|
+
{ id: "sql", label: "SQL (full data)" },
|
|
1178
|
+
{ id: "direct", label: "Direct (sample)" }
|
|
1179
|
+
].map((m) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1180
|
+
"button",
|
|
1181
|
+
{
|
|
1182
|
+
onClick: () => setEngineMode(m.id),
|
|
1183
|
+
className: `flex-1 px-2 py-1 text-xs font-mono transition-colors ${engineMode === m.id ? "bg-midnight-accent/10 text-midnight-accent" : "text-midnight-text-muted hover:bg-midnight-raised"}`,
|
|
1184
|
+
children: m.label
|
|
1185
|
+
},
|
|
1186
|
+
m.id
|
|
1187
|
+
)) })
|
|
1188
|
+
] }),
|
|
1189
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1190
|
+
DropZone,
|
|
1191
|
+
{
|
|
1192
|
+
label: chartType === "heatmap" ? "Rows" : "X Axis / Group By",
|
|
1193
|
+
fields: xFields,
|
|
1194
|
+
columns,
|
|
1195
|
+
onAdd: (f) => addField("x", f),
|
|
1196
|
+
onRemove: (i) => removeField("x", i),
|
|
1197
|
+
onChangeAgg: () => {
|
|
1198
|
+
},
|
|
1199
|
+
maxFields: chartType === "pie" || chartType === "heatmap" ? 1 : 3
|
|
1200
|
+
}
|
|
1201
|
+
),
|
|
1202
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1203
|
+
DropZone,
|
|
1204
|
+
{
|
|
1205
|
+
label: chartType === "heatmap" ? "Columns" : "Y Axis / Values",
|
|
1206
|
+
fields: yFields,
|
|
1207
|
+
columns,
|
|
1208
|
+
onAdd: (f) => addField("y", f),
|
|
1209
|
+
onRemove: (i) => removeField("y", i),
|
|
1210
|
+
onChangeAgg: (i, agg) => changeAgg("y", i, agg),
|
|
1211
|
+
showAgg: chartType !== "heatmap",
|
|
1212
|
+
maxFields: chartType === "heatmap" ? 1 : 5
|
|
1213
|
+
}
|
|
1214
|
+
),
|
|
1215
|
+
chartType === "heatmap" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1216
|
+
DropZone,
|
|
1217
|
+
{
|
|
1218
|
+
label: "Cell Value (optional \u2014 defaults to count)",
|
|
1219
|
+
fields: valueField ? [valueField] : [],
|
|
1220
|
+
columns,
|
|
1221
|
+
onAdd: (f) => addField("value", f),
|
|
1222
|
+
onRemove: () => removeField("value"),
|
|
1223
|
+
onChangeAgg: (i, agg) => changeAgg("value", i, agg),
|
|
1224
|
+
showAgg: true,
|
|
1225
|
+
maxFields: 1
|
|
1226
|
+
}
|
|
1227
|
+
),
|
|
1228
|
+
chartType === "heatmap" && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1229
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Order & Totals" }),
|
|
1230
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
1231
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
|
|
1232
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Order rows" }),
|
|
1233
|
+
/* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: rowOrder, options: ORDER_OPTIONS, onChange: (v) => setRowOrder(v) })
|
|
1234
|
+
] }),
|
|
1235
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
|
|
1236
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Order columns" }),
|
|
1237
|
+
/* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: colOrder, options: ORDER_OPTIONS, onChange: (v) => setColOrder(v) })
|
|
1238
|
+
] }),
|
|
1239
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
|
|
1240
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Total aggregator" }),
|
|
1241
|
+
/* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: marginAgg, options: MARGIN_AGGS, onChange: setMarginAgg })
|
|
1242
|
+
] }),
|
|
1243
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1245
|
+
"input",
|
|
1246
|
+
{
|
|
1247
|
+
type: "checkbox",
|
|
1248
|
+
checked: showTotals,
|
|
1249
|
+
onChange: (e) => setShowTotals(e.target.checked),
|
|
1250
|
+
className: "accent-midnight-accent"
|
|
1251
|
+
}
|
|
1252
|
+
),
|
|
1253
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Show row/column totals" })
|
|
1254
|
+
] })
|
|
1255
|
+
] })
|
|
1256
|
+
] }),
|
|
1257
|
+
(chartType === "bar" || chartType === "grouped-bar") && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1258
|
+
DropZone,
|
|
1259
|
+
{
|
|
1260
|
+
label: "Group / Color",
|
|
1261
|
+
fields: groupField ? [groupField] : [],
|
|
1262
|
+
columns,
|
|
1263
|
+
onAdd: (f) => addField("group", f),
|
|
1264
|
+
onRemove: () => removeField("group"),
|
|
1265
|
+
onChangeAgg: () => {
|
|
1266
|
+
},
|
|
1267
|
+
maxFields: 1
|
|
1268
|
+
}
|
|
1269
|
+
),
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsx(FiltersSection, { filters, columns, onChange: setFilters }),
|
|
1271
|
+
engineMode === "sql" && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1272
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1273
|
+
"button",
|
|
1274
|
+
{
|
|
1275
|
+
onClick: () => setShowSql((s) => !s),
|
|
1276
|
+
className: "flex items-center gap-1 text-xs uppercase text-midnight-text-muted font-mono hover:text-midnight-text-body transition-colors",
|
|
1277
|
+
children: [
|
|
1278
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { className: "w-3 h-3" }),
|
|
1279
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `w-3 h-3 transition-transform ${showSql ? "" : "-rotate-90"}` }),
|
|
1280
|
+
"Generated SQL"
|
|
1281
|
+
]
|
|
1282
|
+
}
|
|
1283
|
+
),
|
|
1284
|
+
showSql && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "mt-1 p-2 bg-midnight-surface border border-midnight-border text-[11px] font-mono text-midnight-text-body whitespace-pre-wrap break-words max-h-[160px] overflow-auto", children: generatedSql || "\u2014 configure the chart \u2014" })
|
|
1285
|
+
] }),
|
|
1286
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1287
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1288
|
+
"button",
|
|
1289
|
+
{
|
|
1290
|
+
onClick: () => setShowStyle((s) => !s),
|
|
1291
|
+
className: "flex items-center gap-1 text-xs uppercase text-midnight-text-muted font-mono hover:text-midnight-text-body transition-colors",
|
|
1292
|
+
children: [
|
|
1293
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `w-3 h-3 transition-transform ${showStyle ? "" : "-rotate-90"}` }),
|
|
1294
|
+
"Appearance"
|
|
1295
|
+
]
|
|
1296
|
+
}
|
|
1297
|
+
),
|
|
1298
|
+
showStyle && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(ChartStyleControls, { style, onChange: setStyle }) })
|
|
1299
|
+
] }),
|
|
1300
|
+
onSave && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4 border-t border-midnight-border", children: [
|
|
1301
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1302
|
+
"input",
|
|
1303
|
+
{
|
|
1304
|
+
value: title,
|
|
1305
|
+
onChange: (e) => setTitle(e.target.value),
|
|
1306
|
+
placeholder: "Chart title...",
|
|
1307
|
+
className: "w-full mb-2 px-2 py-1 text-xs font-mono bg-midnight-surface border border-midnight-border text-midnight-text-body outline-none focus:border-midnight-accent transition-colors"
|
|
1308
|
+
}
|
|
1309
|
+
),
|
|
1310
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1311
|
+
"button",
|
|
1312
|
+
{
|
|
1313
|
+
onClick: handleSaveToDashboard,
|
|
1314
|
+
className: "w-full px-3 py-1.5 border border-midnight-accent text-midnight-accent text-xs font-mono hover:bg-midnight-accent/10 transition-colors",
|
|
1315
|
+
children: "+ Add to Dashboard"
|
|
1316
|
+
}
|
|
1317
|
+
)
|
|
1318
|
+
] }),
|
|
1319
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4 border-t border-midnight-border", children: [
|
|
1320
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Available Fields" }),
|
|
1321
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs font-mono px-1 py-0.5", children: [
|
|
1322
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-text-body", children: col.name }),
|
|
1323
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-text-muted", children: col.type })
|
|
1324
|
+
] }, col.name)) })
|
|
1325
|
+
] })
|
|
1326
|
+
] }),
|
|
1327
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0 p-2", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1328
|
+
ChartPreview,
|
|
1329
|
+
{
|
|
1330
|
+
chartType,
|
|
1331
|
+
records,
|
|
1332
|
+
xFields,
|
|
1333
|
+
yFields,
|
|
1334
|
+
valueField,
|
|
1335
|
+
groupField,
|
|
1336
|
+
colorField,
|
|
1337
|
+
engineMode,
|
|
1338
|
+
sqlRows,
|
|
1339
|
+
sqlLoading,
|
|
1340
|
+
sqlError,
|
|
1341
|
+
heatmapOpts: { rowOrder, colOrder, marginAgg, showTotals },
|
|
1342
|
+
style
|
|
1343
|
+
}
|
|
1344
|
+
) })
|
|
1345
|
+
] });
|
|
1346
|
+
}
|
|
1347
|
+
function ViewLoading() {
|
|
1348
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
|
|
1349
|
+
}
|
|
1350
|
+
function SqlPanel({ panel }) {
|
|
1351
|
+
const { runQuery } = useDashboard();
|
|
1352
|
+
const config = panel.config || {};
|
|
1353
|
+
const chartType = config.chartType || panel.type;
|
|
1354
|
+
const [rows, setRows] = react.useState(null);
|
|
1355
|
+
const [error, setError] = react.useState(null);
|
|
1356
|
+
const [loading, setLoading] = react.useState(true);
|
|
1357
|
+
react.useEffect(() => {
|
|
1358
|
+
let cancelled = false;
|
|
1359
|
+
setLoading(true);
|
|
1360
|
+
setError(null);
|
|
1361
|
+
const sql = config.sql || "";
|
|
1362
|
+
runQuery(sql).then(
|
|
1363
|
+
(res) => {
|
|
1364
|
+
if (cancelled) return;
|
|
1365
|
+
setLoading(false);
|
|
1366
|
+
setRows(res?.rows || []);
|
|
1367
|
+
},
|
|
1368
|
+
(err) => {
|
|
1369
|
+
if (cancelled) return;
|
|
1370
|
+
setLoading(false);
|
|
1371
|
+
setRows(null);
|
|
1372
|
+
setError(err instanceof Error ? err.message : String(err) || "Query failed");
|
|
1373
|
+
}
|
|
1374
|
+
);
|
|
1375
|
+
return () => {
|
|
1376
|
+
cancelled = true;
|
|
1377
|
+
};
|
|
1378
|
+
}, [config.sql, runQuery]);
|
|
1379
|
+
if (loading && !rows) {
|
|
1380
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Running\u2026" });
|
|
1381
|
+
}
|
|
1382
|
+
if (error) {
|
|
1383
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-red-400 px-2 text-center", children: error });
|
|
1384
|
+
}
|
|
1385
|
+
if (!rows) return null;
|
|
1386
|
+
if (chartType === "metric") {
|
|
1387
|
+
const first = rows[0];
|
|
1388
|
+
const v = first ? Object.values(first)[0] : 0;
|
|
1389
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
|
|
1390
|
+
}
|
|
1391
|
+
const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
|
|
1392
|
+
switch (chartType) {
|
|
1393
|
+
case "bar":
|
|
1394
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BarView, { data: shaped, groupColumn: config.group || "", valueColumn: config.value || "", style: config.style });
|
|
1395
|
+
case "grouped-bar": {
|
|
1396
|
+
const gb = shaped;
|
|
1397
|
+
return /* @__PURE__ */ jsxRuntime.jsx(GroupedBarChart, { data: gb.data, keys: gb.keys, yFields: config.yFields || [], style: config.style });
|
|
1398
|
+
}
|
|
1399
|
+
case "pie":
|
|
1400
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PieView, { data: shaped, groupColumn: config.group || "", style: config.style });
|
|
1401
|
+
case "line":
|
|
1402
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LineView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1403
|
+
case "scatter":
|
|
1404
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1405
|
+
case "heatmap":
|
|
1406
|
+
return shaped.length ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
|
|
1407
|
+
default:
|
|
1408
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
|
|
1409
|
+
"Unsupported SQL chart: ",
|
|
1410
|
+
chartType
|
|
1411
|
+
] });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function PanelContent({ panel, records, columns }) {
|
|
1415
|
+
const { type } = panel;
|
|
1416
|
+
const config = panel.config || {};
|
|
1417
|
+
if (config.sql) {
|
|
1418
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SqlPanel, { panel });
|
|
1419
|
+
}
|
|
1420
|
+
if (!records?.length && type !== "insight") {
|
|
1421
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
|
|
1422
|
+
}
|
|
1423
|
+
switch (type) {
|
|
1424
|
+
case "bar":
|
|
1425
|
+
return config.group ? /* @__PURE__ */ jsxRuntime.jsx(BarView, { records, groupColumn: config.group, valueColumn: config.value || "", aggFn: config.agg || "count", style: config.style }) : null;
|
|
1426
|
+
case "pie":
|
|
1427
|
+
return config.group ? /* @__PURE__ */ jsxRuntime.jsx(PieView, { records, groupColumn: config.group, style: config.style }) : null;
|
|
1428
|
+
case "line":
|
|
1429
|
+
return config.x && config.y ? /* @__PURE__ */ jsxRuntime.jsx(LineView, { records, xColumn: config.x, yColumn: config.y, style: config.style }) : null;
|
|
1430
|
+
case "scatter":
|
|
1431
|
+
return config.x && config.y ? /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { records, xColumn: config.x, yColumn: config.y, style: config.style }) : null;
|
|
1432
|
+
case "heatmap":
|
|
1433
|
+
return config.row && config.col ? /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { records, rowColumn: config.row, colColumn: config.col, valueColumn: config.value, aggFn: config.agg || "count", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : null;
|
|
1434
|
+
case "pivot":
|
|
1435
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PivotView, { records });
|
|
1436
|
+
case "metric":
|
|
1437
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MetricView, { records, config: { column: config.column || "", agg: config.agg, label: config.label } });
|
|
1438
|
+
case "insight":
|
|
1439
|
+
return /* @__PURE__ */ jsxRuntime.jsx(InsightView, { config: { text: config.text } });
|
|
1440
|
+
case "table": {
|
|
1441
|
+
const cols = config.columns ? config.columns.map((c) => ({ name: c })) : columns;
|
|
1442
|
+
const colNames = cols?.map((c) => c.name) || (records && records[0] ? Object.keys(records[0]) : []);
|
|
1443
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
|
|
1444
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: colNames.map((n) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-2 py-1 text-left text-midnight-text-muted font-mono border-b border-midnight-border", children: n }, n)) }) }),
|
|
1445
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: (records || []).slice(0, 50).map((r, i) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: colNames.map((n) => {
|
|
1446
|
+
const cellValue = r[n];
|
|
1447
|
+
return /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-2 py-1 text-midnight-text-body truncate max-w-[200px]", children: cellValue == null ? "" : typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue) }, n);
|
|
1448
|
+
}) }, i)) })
|
|
1449
|
+
] }) });
|
|
1450
|
+
}
|
|
1451
|
+
default:
|
|
1452
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
|
|
1453
|
+
"Unknown panel type: ",
|
|
1454
|
+
type
|
|
1455
|
+
] });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
var ROW_HEIGHT = 180;
|
|
1459
|
+
function DashboardRenderer({ dashboard, records, columns }) {
|
|
1460
|
+
const { theme, removePanel } = useDashboard();
|
|
1461
|
+
const { canEditPanels } = useCapabilities();
|
|
1462
|
+
if (!dashboard) return null;
|
|
1463
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 p-2", children: [
|
|
1464
|
+
dashboard.insights && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border ${theme.border} bg-midnight-elevated px-4 py-3 flex items-start gap-3`, children: [
|
|
1465
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "w-5 h-5 text-midnight-accent shrink-0 mt-0.5" }),
|
|
1466
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm ${theme.text} leading-relaxed`, children: dashboard.insights })
|
|
1467
|
+
] }),
|
|
1468
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-12 gap-3", children: dashboard.panels?.map((panel) => {
|
|
1469
|
+
const colSpan = Math.min(Math.max(panel.width || 6, 1), 12);
|
|
1470
|
+
const rowSpan = Math.min(Math.max(panel.height || 2, 1), 4);
|
|
1471
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1472
|
+
"div",
|
|
1473
|
+
{
|
|
1474
|
+
className: `border ${theme.border} bg-midnight-surface flex flex-col`,
|
|
1475
|
+
style: {
|
|
1476
|
+
gridColumn: `span ${colSpan}`,
|
|
1477
|
+
minHeight: `${rowSpan * ROW_HEIGHT}px`
|
|
1478
|
+
},
|
|
1479
|
+
children: [
|
|
1480
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated`, children: [
|
|
1481
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-xs font-mono text-midnight-text-body truncate", style: { textAlign: panel.config?.style?.titleAlign || "left" }, children: panel.title || panel.type }),
|
|
1482
|
+
canEditPanels && removePanel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1483
|
+
"button",
|
|
1484
|
+
{
|
|
1485
|
+
onClick: () => removePanel(panel.id),
|
|
1486
|
+
className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
|
|
1487
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "w-3 h-3" })
|
|
1488
|
+
}
|
|
1489
|
+
)
|
|
1490
|
+
] }),
|
|
1491
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(ViewLoading, {}), children: /* @__PURE__ */ jsxRuntime.jsx(PanelContent, { panel, records, columns }) }) })
|
|
1492
|
+
]
|
|
1493
|
+
},
|
|
1494
|
+
panel.id
|
|
1495
|
+
);
|
|
1496
|
+
}) })
|
|
1497
|
+
] });
|
|
1498
|
+
}
|
|
1499
|
+
function ProfileSummary({ profile, theme }) {
|
|
1500
|
+
const [open, setOpen] = react.useState(false);
|
|
1501
|
+
if (!profile) return null;
|
|
1502
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border ${theme.border} bg-midnight-surface mb-3`, children: [
|
|
1503
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1504
|
+
"button",
|
|
1505
|
+
{
|
|
1506
|
+
onClick: () => setOpen(!open),
|
|
1507
|
+
className: `w-full px-3 py-2 bg-midnight-elevated flex items-center gap-2 hover:bg-midnight-raised transition-colors text-left`,
|
|
1508
|
+
children: [
|
|
1509
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `w-4 h-4 transition-transform ${open ? "" : "-rotate-90"}` }),
|
|
1510
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.BarChart3, { className: "w-4 h-4" }),
|
|
1511
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: `text-sm font-mono ${theme.text}`, children: [
|
|
1512
|
+
profile.total_rows?.toLocaleString(),
|
|
1513
|
+
" rows, ",
|
|
1514
|
+
profile.columns?.length,
|
|
1515
|
+
" columns"
|
|
1516
|
+
] })
|
|
1517
|
+
]
|
|
1518
|
+
}
|
|
1519
|
+
),
|
|
1520
|
+
open && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full text-xs", children: [
|
|
1521
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { className: `border-b ${theme.border} bg-midnight-elevated`, children: ["Column", "Type", "Distinct", "Nulls", "Stats", "Top Values"].map((h) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: `px-3 py-1 text-left uppercase ${theme.textSecondary}`, children: h }, h)) }) }),
|
|
1522
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: profile.columns?.map((col) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: `border-b border-dashed ${theme.border}`, children: [
|
|
1523
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: `px-3 py-1 font-mono ${theme.text}`, children: col.name }),
|
|
1524
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: `px-3 py-1 ${theme.textSecondary}`, children: col.type }),
|
|
1525
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: `px-3 py-1 text-right ${theme.textSecondary}`, children: col.distinct_count }),
|
|
1526
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: `px-3 py-1 text-right ${theme.textSecondary}`, children: col.null_count }),
|
|
1527
|
+
/* @__PURE__ */ jsxRuntime.jsxs("td", { className: `px-3 py-1 ${theme.textSecondary}`, children: [
|
|
1528
|
+
col.type === "string" && (col.avg_length ?? 0) > 0 && `len: ${col.min_length}\u2013${col.max_length}`,
|
|
1529
|
+
(col.type === "int64" || col.type === "float64") && col.min !== void 0 && `${col.min}\u2013${col.max}`
|
|
1530
|
+
] }),
|
|
1531
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: `px-3 py-1 ${theme.textSecondary} max-w-[200px] truncate`, children: col.top_values?.slice(0, 3).join(", ") })
|
|
1532
|
+
] }, col.name)) })
|
|
1533
|
+
] }) })
|
|
1534
|
+
] });
|
|
1535
|
+
}
|
|
1536
|
+
function StateSelector({ states, activeStateId, onSelect, theme }) {
|
|
1537
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border ${theme.border} bg-midnight-surface mb-3`, children: [
|
|
1538
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `px-3 py-2 bg-midnight-elevated flex items-center gap-2`, children: [
|
|
1539
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Database, { className: "w-4 h-4" }),
|
|
1540
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: `text-sm font-mono ${theme.text}`, children: [
|
|
1541
|
+
"States (",
|
|
1542
|
+
states.length,
|
|
1543
|
+
")"
|
|
1544
|
+
] })
|
|
1545
|
+
] }),
|
|
1546
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-h-[140px] overflow-y-auto", children: [
|
|
1547
|
+
states.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1548
|
+
"div",
|
|
1549
|
+
{
|
|
1550
|
+
onClick: () => onSelect(s.state_id),
|
|
1551
|
+
className: `px-3 py-1.5 cursor-pointer flex justify-between items-center border-b border-dashed ${theme.border} ${s.state_id === activeStateId ? "bg-midnight-raised" : ""} hover:bg-midnight-raised transition-colors`,
|
|
1552
|
+
children: [
|
|
1553
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `font-mono text-xs ${theme.text} truncate`, children: s.state_id }),
|
|
1554
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: `text-xs ${theme.textSecondary} ml-2 shrink-0`, children: [
|
|
1555
|
+
s.row_count?.toLocaleString(),
|
|
1556
|
+
" rows"
|
|
1557
|
+
] })
|
|
1558
|
+
]
|
|
1559
|
+
},
|
|
1560
|
+
s.state_id
|
|
1561
|
+
)),
|
|
1562
|
+
!states.length && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `px-3 py-4 text-center text-sm ${theme.textSecondary}`, children: "No states found" })
|
|
1563
|
+
] })
|
|
1564
|
+
] });
|
|
1565
|
+
}
|
|
1566
|
+
function ChatInput({ onSend, loading, theme }) {
|
|
1567
|
+
const [text, setText] = react.useState("");
|
|
1568
|
+
const handleSubmit = (e) => {
|
|
1569
|
+
e.preventDefault();
|
|
1570
|
+
if (!text.trim() || loading) return;
|
|
1571
|
+
onSend(text.trim());
|
|
1572
|
+
setText("");
|
|
1573
|
+
};
|
|
1574
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: `flex gap-2 px-3 py-2 border-t ${theme.border} bg-midnight-elevated shrink-0`, children: [
|
|
1575
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1576
|
+
"input",
|
|
1577
|
+
{
|
|
1578
|
+
value: text,
|
|
1579
|
+
onChange: (e) => setText(e.target.value),
|
|
1580
|
+
placeholder: "Ask AI to refine the dashboard...",
|
|
1581
|
+
disabled: loading,
|
|
1582
|
+
className: "flex-1 bg-midnight-surface border border-midnight-border px-3 py-1.5 text-sm outline-none text-midnight-text-body placeholder:text-midnight-text-muted"
|
|
1583
|
+
}
|
|
1584
|
+
),
|
|
1585
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1586
|
+
"button",
|
|
1587
|
+
{
|
|
1588
|
+
type: "submit",
|
|
1589
|
+
disabled: loading || !text.trim(),
|
|
1590
|
+
className: `px-3 py-1.5 border border-midnight-border text-sm flex items-center gap-1 ${loading ? "opacity-50" : "hover:bg-midnight-raised"} transition-colors`,
|
|
1591
|
+
children: loading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "w-4 h-4" })
|
|
1592
|
+
}
|
|
1593
|
+
)
|
|
1594
|
+
] });
|
|
1595
|
+
}
|
|
1596
|
+
function ModeTab({ active, onClick, icon: Icon, label }) {
|
|
1597
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1598
|
+
"button",
|
|
1599
|
+
{
|
|
1600
|
+
onClick,
|
|
1601
|
+
className: `px-3 py-1 text-xs font-mono border transition-colors ${active ? "border-midnight-accent text-midnight-accent bg-midnight-accent/10" : "border-midnight-border text-midnight-text-muted hover:bg-midnight-raised"}`,
|
|
1602
|
+
children: [
|
|
1603
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "w-3 h-3 inline mr-1" }),
|
|
1604
|
+
label
|
|
1605
|
+
]
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
function DataExplorer({
|
|
1610
|
+
records,
|
|
1611
|
+
columns,
|
|
1612
|
+
states = [],
|
|
1613
|
+
activeStateId,
|
|
1614
|
+
profile,
|
|
1615
|
+
dashboard,
|
|
1616
|
+
dashboardId,
|
|
1617
|
+
savedDashboards = [],
|
|
1618
|
+
loading = false,
|
|
1619
|
+
analyzing = false,
|
|
1620
|
+
onSelectState,
|
|
1621
|
+
onRefreshStates
|
|
1622
|
+
}) {
|
|
1623
|
+
const { theme, saveDashboard, listDashboards, searchDashboards, loadDashboard, deleteDashboard, analyzeDataset, refineDashboard } = useDashboard();
|
|
1624
|
+
const caps = useCapabilities();
|
|
1625
|
+
const [mode, setMode] = react.useState("dashboard");
|
|
1626
|
+
const [fullscreen, setFullscreen] = react.useState(false);
|
|
1627
|
+
const [showSaved, setShowSaved] = react.useState(false);
|
|
1628
|
+
const [saveName, setSaveName] = react.useState("");
|
|
1629
|
+
const [showSaveInput, setShowSaveInput] = react.useState(false);
|
|
1630
|
+
const [searchQuery, setSearchQuery] = react.useState("");
|
|
1631
|
+
const showPersistence = caps.canSave || caps.canList || caps.canLoad || caps.canDelete;
|
|
1632
|
+
const handleAutoAnalyze = () => {
|
|
1633
|
+
setMode("dashboard");
|
|
1634
|
+
analyzeDataset?.();
|
|
1635
|
+
};
|
|
1636
|
+
const handleSave = async () => {
|
|
1637
|
+
if (!caps.canSave) return;
|
|
1638
|
+
if (showSaveInput && saveName.trim()) {
|
|
1639
|
+
await saveDashboard?.(saveName.trim());
|
|
1640
|
+
setShowSaveInput(false);
|
|
1641
|
+
setSaveName("");
|
|
1642
|
+
} else if (dashboardId) {
|
|
1643
|
+
await saveDashboard?.();
|
|
1644
|
+
} else {
|
|
1645
|
+
setShowSaveInput(true);
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
const handleOpenSaved = () => {
|
|
1649
|
+
listDashboards?.();
|
|
1650
|
+
setShowSaved(!showSaved);
|
|
1651
|
+
};
|
|
1652
|
+
const handleSearch = (q) => {
|
|
1653
|
+
setSearchQuery(q);
|
|
1654
|
+
searchDashboards?.(q);
|
|
1655
|
+
};
|
|
1656
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col ${theme.bg} ${theme.text} p-4 ${fullscreen ? "fixed inset-0 z-50" : "h-full"}`, children: [
|
|
1657
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-3 shrink-0", children: [
|
|
1658
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Rows3, { className: "w-5 h-5" }),
|
|
1659
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg font-mono", children: "Data Explorer" }),
|
|
1660
|
+
(loading || analyzing) && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-4 h-4 animate-spin text-midnight-text-muted ml-2" }),
|
|
1661
|
+
onRefreshStates && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onRefreshStates, className: "ml-auto p-1 hover:bg-midnight-raised transition-colors", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "w-4 h-4" }) }),
|
|
1662
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1663
|
+
"button",
|
|
1664
|
+
{
|
|
1665
|
+
onClick: () => setFullscreen((f) => !f),
|
|
1666
|
+
title: fullscreen ? "Exit full screen" : "Full screen",
|
|
1667
|
+
className: `p-1 hover:bg-midnight-raised transition-colors ${onRefreshStates ? "" : "ml-auto"}`,
|
|
1668
|
+
children: fullscreen ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minimize2, { className: "w-4 h-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize2, { className: "w-4 h-4" })
|
|
1669
|
+
}
|
|
1670
|
+
)
|
|
1671
|
+
] }),
|
|
1672
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(StateSelector, { states, activeStateId, onSelect: onSelectState, theme }) }),
|
|
1673
|
+
activeStateId && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1674
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 mb-3 shrink-0", children: [
|
|
1675
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(ProfileSummary, { profile, theme }) }),
|
|
1676
|
+
caps.canAnalyze && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1677
|
+
"button",
|
|
1678
|
+
{
|
|
1679
|
+
onClick: handleAutoAnalyze,
|
|
1680
|
+
disabled: analyzing || !profile,
|
|
1681
|
+
className: `shrink-0 flex items-center gap-2 px-4 py-2 border text-sm font-mono transition-colors ${analyzing ? "border-midnight-border opacity-50" : "border-midnight-accent text-midnight-accent hover:bg-midnight-accent/10"}`,
|
|
1682
|
+
children: [
|
|
1683
|
+
analyzing ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "w-4 h-4" }),
|
|
1684
|
+
analyzing ? "Analyzing..." : "Auto-Analyze"
|
|
1685
|
+
]
|
|
1686
|
+
}
|
|
1687
|
+
)
|
|
1688
|
+
] }),
|
|
1689
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 mb-3 shrink-0", children: [
|
|
1690
|
+
/* @__PURE__ */ jsxRuntime.jsx(ModeTab, { active: mode === "dashboard", onClick: () => setMode("dashboard"), icon: lucideReact.Sparkles, label: "Dashboard" }),
|
|
1691
|
+
/* @__PURE__ */ jsxRuntime.jsx(ModeTab, { active: mode === "builder", onClick: () => setMode("builder"), icon: lucideReact.BarChart3, label: "Chart Builder" }),
|
|
1692
|
+
/* @__PURE__ */ jsxRuntime.jsx(ModeTab, { active: mode === "sql", onClick: () => setMode("sql"), icon: lucideReact.Terminal, label: "SQL" }),
|
|
1693
|
+
showPersistence && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 ml-auto", children: [
|
|
1694
|
+
caps.canSave && showSaveInput && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1695
|
+
"input",
|
|
1696
|
+
{
|
|
1697
|
+
value: saveName,
|
|
1698
|
+
onChange: (e) => setSaveName(e.target.value),
|
|
1699
|
+
onKeyDown: (e) => {
|
|
1700
|
+
if (e.key === "Enter") void handleSave();
|
|
1701
|
+
},
|
|
1702
|
+
placeholder: "Dashboard name...",
|
|
1703
|
+
className: "bg-midnight-surface border border-midnight-border px-2 py-1 text-xs outline-none text-midnight-text-body w-40",
|
|
1704
|
+
autoFocus: true
|
|
1705
|
+
}
|
|
1706
|
+
),
|
|
1707
|
+
caps.canSave && dashboard && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1708
|
+
"button",
|
|
1709
|
+
{
|
|
1710
|
+
onClick: () => void handleSave(),
|
|
1711
|
+
title: dashboardId ? "Save" : "Save as...",
|
|
1712
|
+
className: "p-1 border border-midnight-border text-midnight-text-muted hover:bg-midnight-raised transition-colors",
|
|
1713
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "w-3.5 h-3.5" })
|
|
1714
|
+
}
|
|
1715
|
+
),
|
|
1716
|
+
(caps.canList || caps.canLoad || caps.canDelete) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1717
|
+
"button",
|
|
1718
|
+
{
|
|
1719
|
+
onClick: handleOpenSaved,
|
|
1720
|
+
title: "Saved dashboards",
|
|
1721
|
+
className: `p-1 border transition-colors ${showSaved ? "border-midnight-accent text-midnight-accent" : "border-midnight-border text-midnight-text-muted hover:bg-midnight-raised"}`,
|
|
1722
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpen, { className: "w-3.5 h-3.5" })
|
|
1723
|
+
}
|
|
1724
|
+
)
|
|
1725
|
+
] })
|
|
1726
|
+
] }),
|
|
1727
|
+
showSaved && (caps.canList || caps.canLoad || caps.canDelete) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border ${theme.border} bg-midnight-surface mb-3 shrink-0 max-h-[200px] overflow-y-auto`, children: [
|
|
1728
|
+
caps.canSearch && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1729
|
+
"input",
|
|
1730
|
+
{
|
|
1731
|
+
value: searchQuery,
|
|
1732
|
+
onChange: (e) => handleSearch(e.target.value),
|
|
1733
|
+
placeholder: "Search dashboards...",
|
|
1734
|
+
className: "w-full bg-midnight-surface border-b border-midnight-border px-3 py-1.5 text-xs outline-none text-midnight-text-body placeholder:text-midnight-text-muted sticky top-0"
|
|
1735
|
+
}
|
|
1736
|
+
),
|
|
1737
|
+
savedDashboards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-3 text-center text-xs text-midnight-text-muted", children: "No saved dashboards" }) : savedDashboards.map((d) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b border-dashed ${theme.border} hover:bg-midnight-raised transition-colors`, children: [
|
|
1738
|
+
caps.canLoad ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1739
|
+
"button",
|
|
1740
|
+
{
|
|
1741
|
+
onClick: () => {
|
|
1742
|
+
void loadDashboard?.(d.id);
|
|
1743
|
+
setShowSaved(false);
|
|
1744
|
+
setMode("dashboard");
|
|
1745
|
+
},
|
|
1746
|
+
className: "flex-1 text-left text-xs font-mono text-midnight-text-body truncate",
|
|
1747
|
+
children: d.name
|
|
1748
|
+
}
|
|
1749
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-xs font-mono text-midnight-text-body truncate", children: d.name }),
|
|
1750
|
+
typeof d.updated_at === "string" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-midnight-text-muted mx-2", children: new Date(d.updated_at).toLocaleDateString() }),
|
|
1751
|
+
caps.canDelete && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1752
|
+
"button",
|
|
1753
|
+
{
|
|
1754
|
+
onClick: () => void deleteDashboard?.(d.id),
|
|
1755
|
+
className: "p-0.5 text-midnight-text-muted hover:text-red-400 transition-colors",
|
|
1756
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "w-3 h-3" })
|
|
1757
|
+
}
|
|
1758
|
+
)
|
|
1759
|
+
] }, d.id))
|
|
1760
|
+
] }),
|
|
1761
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `border ${theme.border} bg-midnight-surface flex-1 flex flex-col min-h-0`, children: mode === "sql" ? /* @__PURE__ */ jsxRuntime.jsx(SqlConsole, { columns, stateId: activeStateId ?? void 0 }) : mode === "builder" ? /* @__PURE__ */ jsxRuntime.jsx(ChartBuilder, { records, columns, stateId: activeStateId ?? void 0 }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1762
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: dashboard ? /* @__PURE__ */ jsxRuntime.jsx(DashboardRenderer, { dashboard, records, columns }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-4 text-midnight-text-muted", children: [
|
|
1763
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "w-8 h-8" }),
|
|
1764
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: caps.canAnalyze ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1765
|
+
"Click ",
|
|
1766
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Auto-Analyze" }),
|
|
1767
|
+
" to generate an AI dashboard"
|
|
1768
|
+
] }) : "No dashboard to display" })
|
|
1769
|
+
] }) }),
|
|
1770
|
+
dashboard && caps.canRefine && refineDashboard && /* @__PURE__ */ jsxRuntime.jsx(ChatInput, { onSend: refineDashboard, loading: analyzing, theme })
|
|
1771
|
+
] }) })
|
|
1772
|
+
] })
|
|
1773
|
+
] });
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
exports.BarView = BarView;
|
|
1777
|
+
exports.ChartBuilder = ChartBuilder;
|
|
1778
|
+
exports.ChartStyleControls = ChartStyleControls;
|
|
1779
|
+
exports.DEFAULT_CHART_STYLE = DEFAULT_CHART_STYLE;
|
|
1780
|
+
exports.DashboardProvider = DashboardProvider;
|
|
1781
|
+
exports.DashboardRenderer = DashboardRenderer;
|
|
1782
|
+
exports.DataExplorer = DataExplorer;
|
|
1783
|
+
exports.HeatmapView = HeatmapView;
|
|
1784
|
+
exports.InsightView = InsightView;
|
|
1785
|
+
exports.LEGEND_ANCHORS = LEGEND_ANCHORS;
|
|
1786
|
+
exports.LineView = LineView;
|
|
1787
|
+
exports.MetricView = MetricView;
|
|
1788
|
+
exports.PieView = PieView;
|
|
1789
|
+
exports.PivotView = PivotView;
|
|
1790
|
+
exports.ScatterView = ScatterView;
|
|
1791
|
+
exports.SqlConsole = SqlConsole;
|
|
1792
|
+
exports.aggExpr = aggExpr;
|
|
1793
|
+
exports.aggregate = aggregate;
|
|
1794
|
+
exports.axisLegend = axisLegend;
|
|
1795
|
+
exports.buildChartSQL = buildChartSQL;
|
|
1796
|
+
exports.buildNivoTheme = buildNivoTheme;
|
|
1797
|
+
exports.compileWhere = compileWhere;
|
|
1798
|
+
exports.groupBy = groupBy;
|
|
1799
|
+
exports.legendConfig = legendConfig;
|
|
1800
|
+
exports.qIdent = qIdent;
|
|
1801
|
+
exports.qLit = qLit;
|
|
1802
|
+
exports.shapeChartData = shapeChartData;
|
|
1803
|
+
exports.useCapabilities = useCapabilities;
|
|
1804
|
+
exports.useDashboard = useDashboard;
|
|
1805
|
+
exports.withStyleDefaults = withStyleDefaults;
|
|
1806
|
+
//# sourceMappingURL=index.cjs.map
|
|
1807
|
+
//# sourceMappingURL=index.cjs.map
|