@bonnard/react 0.1.1
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 +161 -0
- package/dist/bonnard-chart.d.ts +21 -0
- package/dist/bonnard-chart.d.ts.map +1 -0
- package/dist/charts/area-chart.d.ts +3 -0
- package/dist/charts/area-chart.d.ts.map +1 -0
- package/dist/charts/bar-chart.d.ts +3 -0
- package/dist/charts/bar-chart.d.ts.map +1 -0
- package/dist/charts/big-value.d.ts +3 -0
- package/dist/charts/big-value.d.ts.map +1 -0
- package/dist/charts/data-table.d.ts +3 -0
- package/dist/charts/data-table.d.ts.map +1 -0
- package/dist/charts/index.d.ts +7 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/line-chart.d.ts +3 -0
- package/dist/charts/line-chart.d.ts.map +1 -0
- package/dist/charts/pie-chart.d.ts +3 -0
- package/dist/charts/pie-chart.d.ts.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/dashboard/dashboard-viewer.d.ts +11 -0
- package/dist/dashboard/dashboard-viewer.d.ts.map +1 -0
- package/dist/dashboard/dashboard.d.ts +8 -0
- package/dist/dashboard/dashboard.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +6 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/inputs/date-range-input.d.ts +10 -0
- package/dist/dashboard/inputs/date-range-input.d.ts.map +1 -0
- package/dist/dashboard/inputs/dropdown-input.d.ts +11 -0
- package/dist/dashboard/inputs/dropdown-input.d.ts.map +1 -0
- package/dist/dashboard/parser.d.ts +9 -0
- package/dist/dashboard/parser.d.ts.map +1 -0
- package/dist/dashboard/query-load.d.ts +13 -0
- package/dist/dashboard/query-load.d.ts.map +1 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +935 -0
- package/dist/data-table-DQKxzbS3.js +985 -0
- package/dist/hooks/use-dashboard.d.ts +8 -0
- package/dist/hooks/use-dashboard.d.ts.map +1 -0
- package/dist/hooks/use-query.d.ts +13 -0
- package/dist/hooks/use-query.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/lib/apply-inputs.d.ts +19 -0
- package/dist/lib/apply-inputs.d.ts.map +1 -0
- package/dist/lib/build-series.d.ts +33 -0
- package/dist/lib/build-series.d.ts.map +1 -0
- package/dist/lib/date-presets.d.ts +11 -0
- package/dist/lib/date-presets.d.ts.map +1 -0
- package/dist/lib/echarts-series.d.ts +22 -0
- package/dist/lib/echarts-series.d.ts.map +1 -0
- package/dist/lib/format-value.d.ts +16 -0
- package/dist/lib/format-value.d.ts.map +1 -0
- package/dist/lib/types.d.ts +104 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/styles/bonnard.css +68 -0
- package/dist/theme/chart-theme.d.ts +76 -0
- package/dist/theme/chart-theme.d.ts.map +1 -0
- package/dist/theme/use-chart-theme.d.ts +39 -0
- package/dist/theme/use-chart-theme.d.ts.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { a as BarChart, i as LineChart, n as PieChart, o as BigValue, r as AreaChart, t as DataTable, u as useBonnard } from "./data-table-DQKxzbS3.js";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import DOMPurify from "isomorphic-dompurify";
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
import { unified } from "unified";
|
|
6
|
+
import remarkParse from "remark-parse";
|
|
7
|
+
import remarkRehype from "remark-rehype";
|
|
8
|
+
import rehypeStringify from "rehype-stringify";
|
|
9
|
+
import { SKIP, visit } from "unist-util-visit";
|
|
10
|
+
import yaml from "yaml";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
|
|
13
|
+
//#region src/hooks/use-dashboard.ts
|
|
14
|
+
function useDashboard(slug) {
|
|
15
|
+
const { client } = useBonnard();
|
|
16
|
+
const [dashboard, setDashboard] = useState(void 0);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState(void 0);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(void 0);
|
|
23
|
+
client.getDashboard(slug).then((result) => {
|
|
24
|
+
if (!cancelled) {
|
|
25
|
+
setDashboard(result.dashboard);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}, (err) => {
|
|
29
|
+
if (!cancelled) {
|
|
30
|
+
setError(err instanceof Error ? err.message : "Failed to load dashboard");
|
|
31
|
+
setLoading(false);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true;
|
|
36
|
+
};
|
|
37
|
+
}, [client, slug]);
|
|
38
|
+
return {
|
|
39
|
+
dashboard,
|
|
40
|
+
loading,
|
|
41
|
+
error
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/dashboard/parser.ts
|
|
47
|
+
const VALID_QUERY_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
48
|
+
const VALID_COMPONENTS = new Set([
|
|
49
|
+
"BigValue",
|
|
50
|
+
"LineChart",
|
|
51
|
+
"BarChart",
|
|
52
|
+
"AreaChart",
|
|
53
|
+
"DataTable",
|
|
54
|
+
"PieChart",
|
|
55
|
+
"DateRange",
|
|
56
|
+
"Dropdown"
|
|
57
|
+
]);
|
|
58
|
+
/** Input component types that get hoisted into the inputs bar */
|
|
59
|
+
const INPUT_COMPONENTS = new Set(["DateRange", "Dropdown"]);
|
|
60
|
+
/**
|
|
61
|
+
* Regex to extract individual props from an attributes string.
|
|
62
|
+
* Matches:
|
|
63
|
+
* data={name} → key="data", value from group 2 (curly)
|
|
64
|
+
* x="field" → key="x", value from group 3 (double-quoted)
|
|
65
|
+
* x='field' → key="x", value from group 4 (single-quoted)
|
|
66
|
+
* horizontal → key="horizontal", value undefined (boolean shorthand)
|
|
67
|
+
*/
|
|
68
|
+
const PROP_RE = /(\w+)(?:=\{(\w+)\}|="([^"]*)"|='([^']*)')?/g;
|
|
69
|
+
/**
|
|
70
|
+
* Parse a dashboard markdown string into structured output.
|
|
71
|
+
*
|
|
72
|
+
* Collects all validation errors and reports them together rather than
|
|
73
|
+
* failing on the first error (pattern from Evidence.dev).
|
|
74
|
+
*/
|
|
75
|
+
function parseDashboard(markdown) {
|
|
76
|
+
const errors = [];
|
|
77
|
+
const { data: rawFrontmatter, content: bodyWithoutFrontmatter } = matter(markdown);
|
|
78
|
+
const frontmatter = {
|
|
79
|
+
title: typeof rawFrontmatter.title === "string" ? rawFrontmatter.title : "Untitled Dashboard",
|
|
80
|
+
description: typeof rawFrontmatter.description === "string" ? rawFrontmatter.description : void 0
|
|
81
|
+
};
|
|
82
|
+
const queries = /* @__PURE__ */ new Map();
|
|
83
|
+
const tree = unified().use(remarkParse).parse(bodyWithoutFrontmatter);
|
|
84
|
+
const nodesToRemove = [];
|
|
85
|
+
visit(tree, "code", (node, index, parent) => {
|
|
86
|
+
if (node.lang !== "query") return;
|
|
87
|
+
const name = node.meta?.trim();
|
|
88
|
+
if (!name) {
|
|
89
|
+
errors.push("Query block must have a name: ```query my_query_name");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!VALID_QUERY_NAME.test(name)) {
|
|
93
|
+
errors.push(`Invalid query name "${name}": must be a valid identifier (letters, numbers, _, $)`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (queries.has(name)) {
|
|
97
|
+
errors.push(`Duplicate query name "${name}"`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const options = yaml.parse(node.value);
|
|
102
|
+
if (!options || !options.cube) {
|
|
103
|
+
errors.push(`Query "${name}" must specify a cube`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
queries.set(name, options);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
errors.push(`Query "${name}" has invalid YAML: ${e instanceof Error ? e.message : String(e)}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (parent && typeof index === "number") nodesToRemove.push({
|
|
112
|
+
parent,
|
|
113
|
+
index
|
|
114
|
+
});
|
|
115
|
+
return SKIP;
|
|
116
|
+
});
|
|
117
|
+
for (let i = nodesToRemove.length - 1; i >= 0; i--) {
|
|
118
|
+
const { parent, index } = nodesToRemove[i];
|
|
119
|
+
parent.children.splice(index, 1);
|
|
120
|
+
}
|
|
121
|
+
if (errors.length > 0) throw new Error(`Dashboard parse errors:\n- ${errors.join("\n- ")}`);
|
|
122
|
+
const htmlResult = unified().use(remarkRehype, { allowDangerousHtml: true }).use(rehypeStringify, { allowDangerousHtml: true }).stringify(unified().use(remarkRehype, { allowDangerousHtml: true }).runSync(tree));
|
|
123
|
+
const { sections, inputs } = splitIntoSections(String(htmlResult));
|
|
124
|
+
return {
|
|
125
|
+
frontmatter,
|
|
126
|
+
queries,
|
|
127
|
+
inputs,
|
|
128
|
+
sections
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Split HTML string into sections of HTML content, component tags, and grid layouts.
|
|
133
|
+
* Input components (DateRange, Dropdown) are hoisted into a separate inputs array.
|
|
134
|
+
*/
|
|
135
|
+
function splitIntoSections(html) {
|
|
136
|
+
const sections = [];
|
|
137
|
+
const inputs = [];
|
|
138
|
+
let lastIndex = 0;
|
|
139
|
+
const COMBINED_RE = /<Grid\s+([\s\S]*?)>([\s\S]*?)<\/Grid>|<(BigValue|LineChart|BarChart|AreaChart|DataTable|PieChart|DateRange|Dropdown)\s+([\s\S]*?)\/>/g;
|
|
140
|
+
COMBINED_RE.lastIndex = 0;
|
|
141
|
+
let match;
|
|
142
|
+
while ((match = COMBINED_RE.exec(html)) !== null) {
|
|
143
|
+
const before = html.slice(lastIndex, match.index).trim();
|
|
144
|
+
if (before) sections.push({
|
|
145
|
+
kind: "html",
|
|
146
|
+
content: before
|
|
147
|
+
});
|
|
148
|
+
if (match[1] !== void 0) {
|
|
149
|
+
const props = parseProps(match[1]);
|
|
150
|
+
const inner = splitIntoSections(match[2]);
|
|
151
|
+
inputs.push(...inner.inputs);
|
|
152
|
+
sections.push({
|
|
153
|
+
kind: "grid",
|
|
154
|
+
props,
|
|
155
|
+
children: inner.sections
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
const component = parseComponentTag(match[3], match[4]);
|
|
159
|
+
if (component) if (INPUT_COMPONENTS.has(component.type)) inputs.push(component);
|
|
160
|
+
else sections.push({
|
|
161
|
+
kind: "component",
|
|
162
|
+
component
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
lastIndex = match.index + match[0].length;
|
|
166
|
+
}
|
|
167
|
+
const remaining = html.slice(lastIndex).trim();
|
|
168
|
+
if (remaining) sections.push({
|
|
169
|
+
kind: "html",
|
|
170
|
+
content: remaining
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
sections,
|
|
174
|
+
inputs
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Parse an attributes string into a props Record.
|
|
179
|
+
*/
|
|
180
|
+
function parseProps(attrsString) {
|
|
181
|
+
const props = {};
|
|
182
|
+
PROP_RE.lastIndex = 0;
|
|
183
|
+
let propMatch;
|
|
184
|
+
while ((propMatch = PROP_RE.exec(attrsString)) !== null) {
|
|
185
|
+
const key = propMatch[1];
|
|
186
|
+
props[key] = propMatch[2] ?? propMatch[3] ?? propMatch[4] ?? "true";
|
|
187
|
+
}
|
|
188
|
+
return props;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Parse component attributes string into a ComponentTag.
|
|
192
|
+
*/
|
|
193
|
+
function parseComponentTag(name, attrsString) {
|
|
194
|
+
if (!VALID_COMPONENTS.has(name)) return null;
|
|
195
|
+
return {
|
|
196
|
+
type: name,
|
|
197
|
+
props: parseProps(attrsString)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/lib/date-presets.ts
|
|
203
|
+
function fmt(d) {
|
|
204
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
205
|
+
}
|
|
206
|
+
function today() {
|
|
207
|
+
const d = /* @__PURE__ */ new Date();
|
|
208
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
209
|
+
}
|
|
210
|
+
const PRESETS = {
|
|
211
|
+
"last-7-days": {
|
|
212
|
+
label: "Last 7 Days",
|
|
213
|
+
getRange: () => {
|
|
214
|
+
const end = today();
|
|
215
|
+
const start = new Date(end);
|
|
216
|
+
start.setDate(start.getDate() - 6);
|
|
217
|
+
return [fmt(start), fmt(end)];
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"last-30-days": {
|
|
221
|
+
label: "Last 30 Days",
|
|
222
|
+
getRange: () => {
|
|
223
|
+
const end = today();
|
|
224
|
+
const start = new Date(end);
|
|
225
|
+
start.setDate(start.getDate() - 29);
|
|
226
|
+
return [fmt(start), fmt(end)];
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"last-3-months": {
|
|
230
|
+
label: "Last 3 Months",
|
|
231
|
+
getRange: () => {
|
|
232
|
+
const end = today();
|
|
233
|
+
const start = new Date(end);
|
|
234
|
+
start.setMonth(start.getMonth() - 3);
|
|
235
|
+
return [fmt(start), fmt(end)];
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"last-6-months": {
|
|
239
|
+
label: "Last 6 Months",
|
|
240
|
+
getRange: () => {
|
|
241
|
+
const end = today();
|
|
242
|
+
const start = new Date(end);
|
|
243
|
+
start.setMonth(start.getMonth() - 6);
|
|
244
|
+
return [fmt(start), fmt(end)];
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
"last-12-months": {
|
|
248
|
+
label: "Last 12 Months",
|
|
249
|
+
getRange: () => {
|
|
250
|
+
const end = today();
|
|
251
|
+
const start = new Date(end);
|
|
252
|
+
start.setFullYear(start.getFullYear() - 1);
|
|
253
|
+
return [fmt(start), fmt(end)];
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
"month-to-date": {
|
|
257
|
+
label: "Month to Date",
|
|
258
|
+
getRange: () => {
|
|
259
|
+
const end = today();
|
|
260
|
+
return [fmt(new Date(end.getFullYear(), end.getMonth(), 1)), fmt(end)];
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
"year-to-date": {
|
|
264
|
+
label: "Year to Date",
|
|
265
|
+
getRange: () => {
|
|
266
|
+
const end = today();
|
|
267
|
+
return [fmt(new Date(end.getFullYear(), 0, 1)), fmt(end)];
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"last-year": {
|
|
271
|
+
label: "Last Year",
|
|
272
|
+
getRange: () => {
|
|
273
|
+
const end = today();
|
|
274
|
+
const start = new Date(end.getFullYear() - 1, 0, 1);
|
|
275
|
+
const yearEnd = new Date(end.getFullYear() - 1, 11, 31);
|
|
276
|
+
return [fmt(start), fmt(yearEnd)];
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"all-time": {
|
|
280
|
+
label: "All Time",
|
|
281
|
+
getRange: () => ["2000-01-01", fmt(today())]
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/** Resolve a preset key to a concrete [start, end] date range */
|
|
285
|
+
function getPresetRange(preset) {
|
|
286
|
+
const def = PRESETS[preset];
|
|
287
|
+
if (!def) throw new Error(`Unknown date preset: ${preset}`);
|
|
288
|
+
return def.getRange();
|
|
289
|
+
}
|
|
290
|
+
/** Ordered array of preset options for UI rendering */
|
|
291
|
+
const PRESET_OPTIONS = [
|
|
292
|
+
{
|
|
293
|
+
key: "last-7-days",
|
|
294
|
+
label: "Last 7 Days"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
key: "last-30-days",
|
|
298
|
+
label: "Last 30 Days"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: "last-3-months",
|
|
302
|
+
label: "Last 3 Months"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: "last-6-months",
|
|
306
|
+
label: "Last 6 Months"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
key: "last-12-months",
|
|
310
|
+
label: "Last 12 Months"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
key: "month-to-date",
|
|
314
|
+
label: "Month to Date"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
key: "year-to-date",
|
|
318
|
+
label: "Year to Date"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
key: "last-year",
|
|
322
|
+
label: "Last Year"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
key: "all-time",
|
|
326
|
+
label: "All Time"
|
|
327
|
+
}
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/lib/apply-inputs.ts
|
|
332
|
+
/**
|
|
333
|
+
* Apply all active inputs to a single query's options.
|
|
334
|
+
*
|
|
335
|
+
* - DateRange: overrides timeDimension.dateRange on targeted queries
|
|
336
|
+
* - Dropdown: adds/replaces a filter on the specified dimension
|
|
337
|
+
*/
|
|
338
|
+
function applyInputsToQuery(queryName, baseOptions, inputTags, inputsState) {
|
|
339
|
+
let options = structuredClone(baseOptions);
|
|
340
|
+
for (const input of inputTags) {
|
|
341
|
+
const { type, props } = input;
|
|
342
|
+
const inputName = props.name;
|
|
343
|
+
if (!inputName) continue;
|
|
344
|
+
const state = inputsState[inputName];
|
|
345
|
+
if (state === void 0) continue;
|
|
346
|
+
if (type === "DateRange") options = applyDateRange(queryName, options, props, state);
|
|
347
|
+
else if (type === "Dropdown") options = applyDropdown(queryName, options, props, state);
|
|
348
|
+
}
|
|
349
|
+
return options;
|
|
350
|
+
}
|
|
351
|
+
function applyDateRange(queryName, options, props, state) {
|
|
352
|
+
if (!options.timeDimension) return options;
|
|
353
|
+
if (props.queries) {
|
|
354
|
+
if (!props.queries.split(",").map((s) => s.trim()).includes(queryName)) return options;
|
|
355
|
+
}
|
|
356
|
+
if (!state) return options;
|
|
357
|
+
options.timeDimension = {
|
|
358
|
+
...options.timeDimension,
|
|
359
|
+
dateRange: state.range
|
|
360
|
+
};
|
|
361
|
+
return options;
|
|
362
|
+
}
|
|
363
|
+
function applyDropdown(queryName, options, props, state) {
|
|
364
|
+
const dimension = props.dimension;
|
|
365
|
+
if (!dimension) return options;
|
|
366
|
+
if (!props.queries) return options;
|
|
367
|
+
const targets = props.queries.split(",").map((s) => s.trim());
|
|
368
|
+
const dataQuery = props.data;
|
|
369
|
+
if (dataQuery && dataQuery === queryName) return options;
|
|
370
|
+
if (!targets.includes(queryName)) return options;
|
|
371
|
+
if (state === null || state === "") {
|
|
372
|
+
if (options.filters) {
|
|
373
|
+
options.filters = options.filters.filter((f) => f.dimension !== dimension);
|
|
374
|
+
if (options.filters.length === 0) delete options.filters;
|
|
375
|
+
}
|
|
376
|
+
return options;
|
|
377
|
+
}
|
|
378
|
+
const newFilter = {
|
|
379
|
+
dimension,
|
|
380
|
+
operator: "equals",
|
|
381
|
+
values: [state]
|
|
382
|
+
};
|
|
383
|
+
if (!options.filters) options.filters = [newFilter];
|
|
384
|
+
else {
|
|
385
|
+
const idx = options.filters.findIndex((f) => f.dimension === dimension);
|
|
386
|
+
if (idx >= 0) options.filters[idx] = newFilter;
|
|
387
|
+
else options.filters.push(newFilter);
|
|
388
|
+
}
|
|
389
|
+
return options;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Build the initial inputsState from input tags (using their defaults).
|
|
393
|
+
*/
|
|
394
|
+
function buildInitialInputsState(inputs) {
|
|
395
|
+
const state = {};
|
|
396
|
+
for (const input of inputs) {
|
|
397
|
+
const { type, props } = input;
|
|
398
|
+
const name = props.name;
|
|
399
|
+
if (!name) continue;
|
|
400
|
+
if (type === "DateRange") {
|
|
401
|
+
const preset = props.default || "last-6-months";
|
|
402
|
+
state[name] = {
|
|
403
|
+
preset,
|
|
404
|
+
range: getPresetRange(preset)
|
|
405
|
+
};
|
|
406
|
+
} else if (type === "Dropdown") state[name] = props.default || null;
|
|
407
|
+
}
|
|
408
|
+
return state;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/dashboard/query-load.tsx
|
|
413
|
+
/**
|
|
414
|
+
* QueryLoad — wraps chart components to handle loading / error / empty states.
|
|
415
|
+
*/
|
|
416
|
+
function QueryLoad({ data, loading, error, children }) {
|
|
417
|
+
if (loading) return /* @__PURE__ */ jsx("div", {
|
|
418
|
+
style: {
|
|
419
|
+
display: "flex",
|
|
420
|
+
alignItems: "center",
|
|
421
|
+
justifyContent: "center",
|
|
422
|
+
height: 200,
|
|
423
|
+
fontSize: 14,
|
|
424
|
+
color: "var(--bon-text-muted)"
|
|
425
|
+
},
|
|
426
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
427
|
+
style: {
|
|
428
|
+
display: "flex",
|
|
429
|
+
alignItems: "center",
|
|
430
|
+
gap: 8
|
|
431
|
+
},
|
|
432
|
+
children: [/* @__PURE__ */ jsx("div", { style: {
|
|
433
|
+
width: 16,
|
|
434
|
+
height: 16,
|
|
435
|
+
borderRadius: "50%",
|
|
436
|
+
border: "2px solid var(--bon-spinner-color)",
|
|
437
|
+
borderTopColor: "transparent",
|
|
438
|
+
animation: "bonnard-spin 0.6s linear infinite"
|
|
439
|
+
} }), "Loading data…"]
|
|
440
|
+
})
|
|
441
|
+
});
|
|
442
|
+
if (error) return /* @__PURE__ */ jsxs("div", {
|
|
443
|
+
style: {
|
|
444
|
+
borderRadius: "var(--bon-radius)",
|
|
445
|
+
border: "1px solid var(--bon-border-error)",
|
|
446
|
+
backgroundColor: "var(--bon-bg-error)",
|
|
447
|
+
padding: 16,
|
|
448
|
+
fontSize: 14,
|
|
449
|
+
color: "var(--bon-text-error)"
|
|
450
|
+
},
|
|
451
|
+
children: ["Query error: ", error]
|
|
452
|
+
});
|
|
453
|
+
if (!data || data.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
454
|
+
style: {
|
|
455
|
+
display: "flex",
|
|
456
|
+
alignItems: "center",
|
|
457
|
+
justifyContent: "center",
|
|
458
|
+
height: 200,
|
|
459
|
+
fontSize: 14,
|
|
460
|
+
color: "var(--bon-text-muted)"
|
|
461
|
+
},
|
|
462
|
+
children: "No data available"
|
|
463
|
+
});
|
|
464
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/dashboard/inputs/date-range-input.tsx
|
|
469
|
+
function DateRangeInput({ name, label, value, onInputChange }) {
|
|
470
|
+
const handleChange = (e) => {
|
|
471
|
+
const preset = e.target.value;
|
|
472
|
+
onInputChange(name, {
|
|
473
|
+
preset,
|
|
474
|
+
range: getPresetRange(preset)
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
478
|
+
style: {
|
|
479
|
+
display: "flex",
|
|
480
|
+
flexDirection: "column",
|
|
481
|
+
gap: 4
|
|
482
|
+
},
|
|
483
|
+
children: [label && /* @__PURE__ */ jsx("label", {
|
|
484
|
+
style: {
|
|
485
|
+
fontSize: 12,
|
|
486
|
+
fontWeight: 500,
|
|
487
|
+
color: "var(--bon-text-muted)"
|
|
488
|
+
},
|
|
489
|
+
children: label
|
|
490
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
491
|
+
value: value.preset,
|
|
492
|
+
onChange: handleChange,
|
|
493
|
+
style: {
|
|
494
|
+
width: 180,
|
|
495
|
+
height: 32,
|
|
496
|
+
fontSize: 14,
|
|
497
|
+
padding: "0 8px",
|
|
498
|
+
borderRadius: "var(--bon-radius)",
|
|
499
|
+
border: "1px solid var(--bon-input-border)",
|
|
500
|
+
backgroundColor: "var(--bon-input-bg)",
|
|
501
|
+
color: "var(--bon-text)",
|
|
502
|
+
cursor: "pointer"
|
|
503
|
+
},
|
|
504
|
+
children: PRESET_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
505
|
+
value: opt.key,
|
|
506
|
+
children: opt.label
|
|
507
|
+
}, opt.key))
|
|
508
|
+
})]
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/dashboard/inputs/dropdown-input.tsx
|
|
514
|
+
function DropdownInput({ name, dimension, label, data, value, onInputChange }) {
|
|
515
|
+
const options = Array.from(new Set(data.map((row) => String(row[dimension] ?? "")).filter(Boolean))).sort();
|
|
516
|
+
const handleChange = (e) => {
|
|
517
|
+
const val = e.target.value;
|
|
518
|
+
onInputChange(name, val === "" ? null : val);
|
|
519
|
+
};
|
|
520
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
521
|
+
style: {
|
|
522
|
+
display: "flex",
|
|
523
|
+
flexDirection: "column",
|
|
524
|
+
gap: 4
|
|
525
|
+
},
|
|
526
|
+
children: [label && /* @__PURE__ */ jsx("label", {
|
|
527
|
+
style: {
|
|
528
|
+
fontSize: 12,
|
|
529
|
+
fontWeight: 500,
|
|
530
|
+
color: "var(--bon-text-muted)"
|
|
531
|
+
},
|
|
532
|
+
children: label
|
|
533
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
534
|
+
value: value ?? "",
|
|
535
|
+
onChange: handleChange,
|
|
536
|
+
style: {
|
|
537
|
+
width: 180,
|
|
538
|
+
height: 32,
|
|
539
|
+
fontSize: 14,
|
|
540
|
+
padding: "0 8px",
|
|
541
|
+
borderRadius: "var(--bon-radius)",
|
|
542
|
+
border: "1px solid var(--bon-input-border)",
|
|
543
|
+
backgroundColor: "var(--bon-input-bg)",
|
|
544
|
+
color: "var(--bon-text)",
|
|
545
|
+
cursor: "pointer"
|
|
546
|
+
},
|
|
547
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
548
|
+
value: "",
|
|
549
|
+
children: "All"
|
|
550
|
+
}), options.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
551
|
+
value: opt,
|
|
552
|
+
children: opt
|
|
553
|
+
}, opt))]
|
|
554
|
+
})]
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
//#endregion
|
|
559
|
+
//#region src/dashboard/dashboard-viewer.tsx
|
|
560
|
+
const GRID_COL_STYLES = {
|
|
561
|
+
"1": { gridTemplateColumns: "1fr" },
|
|
562
|
+
"2": { gridTemplateColumns: "repeat(2, 1fr)" },
|
|
563
|
+
"3": { gridTemplateColumns: "repeat(3, 1fr)" },
|
|
564
|
+
"4": { gridTemplateColumns: "repeat(4, 1fr)" },
|
|
565
|
+
"5": { gridTemplateColumns: "repeat(5, 1fr)" },
|
|
566
|
+
"6": { gridTemplateColumns: "repeat(6, 1fr)" }
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* DashboardViewer — parses dashboard markdown and renders it.
|
|
570
|
+
*/
|
|
571
|
+
function DashboardViewer({ content, onInputsChange }) {
|
|
572
|
+
const { client } = useBonnard();
|
|
573
|
+
const [parsed, setParsed] = useState(null);
|
|
574
|
+
const [parseError, setParseError] = useState(null);
|
|
575
|
+
const [queryStates, setQueryStates] = useState(/* @__PURE__ */ new Map());
|
|
576
|
+
const [inputsState, setInputsState] = useState({});
|
|
577
|
+
const isInitialRender = useRef(true);
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
try {
|
|
580
|
+
const result = parseDashboard(content);
|
|
581
|
+
setParsed(result);
|
|
582
|
+
setParseError(null);
|
|
583
|
+
setInputsState(buildInitialInputsState(result.inputs));
|
|
584
|
+
isInitialRender.current = true;
|
|
585
|
+
} catch (err) {
|
|
586
|
+
setParseError(err instanceof Error ? err.message : "Failed to parse dashboard");
|
|
587
|
+
setParsed(null);
|
|
588
|
+
}
|
|
589
|
+
}, [content]);
|
|
590
|
+
const executeQueries = useCallback(async (queries, inputs, currentInputsState, queryNames) => {
|
|
591
|
+
const namesToExecute = queryNames ?? Array.from(queries.keys());
|
|
592
|
+
setQueryStates((prev) => {
|
|
593
|
+
const next = new Map(prev);
|
|
594
|
+
for (const name of namesToExecute) next.set(name, { loading: true });
|
|
595
|
+
return next;
|
|
596
|
+
});
|
|
597
|
+
await Promise.all(namesToExecute.map(async (name) => {
|
|
598
|
+
const baseOptions = queries.get(name);
|
|
599
|
+
if (!baseOptions) return;
|
|
600
|
+
const options = applyInputsToQuery(name, baseOptions, inputs, currentInputsState);
|
|
601
|
+
try {
|
|
602
|
+
const result = await client.query(options);
|
|
603
|
+
setQueryStates((prev) => {
|
|
604
|
+
const next = new Map(prev);
|
|
605
|
+
next.set(name, {
|
|
606
|
+
loading: false,
|
|
607
|
+
data: result.data,
|
|
608
|
+
limit: options.limit ?? null
|
|
609
|
+
});
|
|
610
|
+
return next;
|
|
611
|
+
});
|
|
612
|
+
} catch (err) {
|
|
613
|
+
setQueryStates((prev) => {
|
|
614
|
+
const next = new Map(prev);
|
|
615
|
+
next.set(name, {
|
|
616
|
+
loading: false,
|
|
617
|
+
error: err instanceof Error ? err.message : "Query failed"
|
|
618
|
+
});
|
|
619
|
+
return next;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}));
|
|
623
|
+
}, [client]);
|
|
624
|
+
useEffect(() => {
|
|
625
|
+
if (parsed && parsed.queries.size > 0 && isInitialRender.current) {
|
|
626
|
+
isInitialRender.current = false;
|
|
627
|
+
executeQueries(parsed.queries, parsed.inputs, inputsState);
|
|
628
|
+
}
|
|
629
|
+
}, [
|
|
630
|
+
parsed,
|
|
631
|
+
inputsState,
|
|
632
|
+
executeQueries
|
|
633
|
+
]);
|
|
634
|
+
const handleInputChange = useCallback((inputName, value) => {
|
|
635
|
+
if (!parsed) return;
|
|
636
|
+
setInputsState((prev) => {
|
|
637
|
+
const next = {
|
|
638
|
+
...prev,
|
|
639
|
+
[inputName]: value
|
|
640
|
+
};
|
|
641
|
+
const changedInput = parsed.inputs.find((i) => i.props.name === inputName);
|
|
642
|
+
if (!changedInput) return next;
|
|
643
|
+
let affectedQueries;
|
|
644
|
+
if (changedInput.type === "DateRange") if (changedInput.props.queries) affectedQueries = changedInput.props.queries.split(",").map((s) => s.trim());
|
|
645
|
+
else affectedQueries = Array.from(parsed.queries.entries()).filter(([, opts]) => opts.timeDimension).map(([name]) => name);
|
|
646
|
+
else affectedQueries = changedInput.props.queries ? changedInput.props.queries.split(",").map((s) => s.trim()) : [];
|
|
647
|
+
if (affectedQueries.length > 0) executeQueries(parsed.queries, parsed.inputs, next, affectedQueries);
|
|
648
|
+
return next;
|
|
649
|
+
});
|
|
650
|
+
}, [parsed, executeQueries]);
|
|
651
|
+
if (parseError) return /* @__PURE__ */ jsxs("div", {
|
|
652
|
+
style: {
|
|
653
|
+
borderRadius: "var(--bon-radius)",
|
|
654
|
+
border: "1px solid var(--bon-border-error)",
|
|
655
|
+
backgroundColor: "var(--bon-bg-error)",
|
|
656
|
+
padding: 16
|
|
657
|
+
},
|
|
658
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
659
|
+
style: {
|
|
660
|
+
fontWeight: 500,
|
|
661
|
+
color: "var(--bon-text-error)",
|
|
662
|
+
marginBottom: 4,
|
|
663
|
+
margin: 0
|
|
664
|
+
},
|
|
665
|
+
children: "Dashboard Parse Error"
|
|
666
|
+
}), /* @__PURE__ */ jsx("pre", {
|
|
667
|
+
style: {
|
|
668
|
+
fontSize: 14,
|
|
669
|
+
color: "var(--bon-text-error)",
|
|
670
|
+
whiteSpace: "pre-wrap",
|
|
671
|
+
margin: 0
|
|
672
|
+
},
|
|
673
|
+
children: parseError
|
|
674
|
+
})]
|
|
675
|
+
});
|
|
676
|
+
if (!parsed) return null;
|
|
677
|
+
const groupedSections = groupConsecutiveBigValues(parsed.sections);
|
|
678
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
679
|
+
style: {
|
|
680
|
+
display: "flex",
|
|
681
|
+
flexDirection: "column",
|
|
682
|
+
gap: 16
|
|
683
|
+
},
|
|
684
|
+
children: [parsed.inputs.length > 0 && /* @__PURE__ */ jsx(InputsBar, {
|
|
685
|
+
inputs: parsed.inputs,
|
|
686
|
+
inputsState,
|
|
687
|
+
queryStates,
|
|
688
|
+
onInputChange: handleInputChange
|
|
689
|
+
}), groupedSections.map((section, i) => /* @__PURE__ */ jsx(SectionRenderer, {
|
|
690
|
+
section,
|
|
691
|
+
queryStates
|
|
692
|
+
}, i))]
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function InputsBar({ inputs, inputsState, queryStates, onInputChange }) {
|
|
696
|
+
return /* @__PURE__ */ jsx("div", {
|
|
697
|
+
style: {
|
|
698
|
+
display: "flex",
|
|
699
|
+
flexWrap: "wrap",
|
|
700
|
+
alignItems: "flex-end",
|
|
701
|
+
gap: 16,
|
|
702
|
+
borderRadius: "var(--bon-radius)",
|
|
703
|
+
backgroundColor: "var(--bon-bg-muted)",
|
|
704
|
+
padding: "12px 16px"
|
|
705
|
+
},
|
|
706
|
+
children: inputs.map((input) => {
|
|
707
|
+
const { type, props } = input;
|
|
708
|
+
const name = props.name;
|
|
709
|
+
if (!name) return null;
|
|
710
|
+
if (type === "DateRange") {
|
|
711
|
+
const value = inputsState[name];
|
|
712
|
+
if (!value) return null;
|
|
713
|
+
return /* @__PURE__ */ jsx(DateRangeInput, {
|
|
714
|
+
name,
|
|
715
|
+
label: props.label,
|
|
716
|
+
value,
|
|
717
|
+
onInputChange
|
|
718
|
+
}, name);
|
|
719
|
+
}
|
|
720
|
+
if (type === "Dropdown") {
|
|
721
|
+
const dataRef = props.data;
|
|
722
|
+
const data = dataRef ? queryStates.get(dataRef)?.data ?? [] : [];
|
|
723
|
+
return /* @__PURE__ */ jsx(DropdownInput, {
|
|
724
|
+
name,
|
|
725
|
+
dimension: props.dimension,
|
|
726
|
+
label: props.label,
|
|
727
|
+
data,
|
|
728
|
+
value: inputsState[name] ?? null,
|
|
729
|
+
onInputChange
|
|
730
|
+
}, name);
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
})
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function groupConsecutiveBigValues(sections) {
|
|
737
|
+
const result = [];
|
|
738
|
+
let bigValueRun = [];
|
|
739
|
+
const flushRun = () => {
|
|
740
|
+
if (bigValueRun.length > 1) result.push({
|
|
741
|
+
kind: "grid",
|
|
742
|
+
props: { cols: String(Math.min(bigValueRun.length, 4)) },
|
|
743
|
+
children: bigValueRun
|
|
744
|
+
});
|
|
745
|
+
else if (bigValueRun.length === 1) result.push(bigValueRun[0]);
|
|
746
|
+
bigValueRun = [];
|
|
747
|
+
};
|
|
748
|
+
for (const section of sections) if (section.kind === "component" && section.component.type === "BigValue") bigValueRun.push(section);
|
|
749
|
+
else {
|
|
750
|
+
flushRun();
|
|
751
|
+
result.push(section);
|
|
752
|
+
}
|
|
753
|
+
flushRun();
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
function SectionRenderer({ section, queryStates }) {
|
|
757
|
+
if (section.kind === "html") return /* @__PURE__ */ jsx("div", {
|
|
758
|
+
style: {
|
|
759
|
+
color: "var(--bon-text)",
|
|
760
|
+
fontSize: 14,
|
|
761
|
+
lineHeight: 1.6
|
|
762
|
+
},
|
|
763
|
+
dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(section.content) }
|
|
764
|
+
});
|
|
765
|
+
if (section.kind === "grid") return /* @__PURE__ */ jsx("div", {
|
|
766
|
+
style: {
|
|
767
|
+
display: "grid",
|
|
768
|
+
gap: 16,
|
|
769
|
+
...GRID_COL_STYLES[section.props.cols || "2"] || GRID_COL_STYLES["2"]
|
|
770
|
+
},
|
|
771
|
+
children: section.children.map((child, i) => /* @__PURE__ */ jsx(SectionRenderer, {
|
|
772
|
+
section: child,
|
|
773
|
+
queryStates
|
|
774
|
+
}, i))
|
|
775
|
+
});
|
|
776
|
+
return /* @__PURE__ */ jsx(ComponentRenderer, {
|
|
777
|
+
component: section.component,
|
|
778
|
+
queryStates
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function ComponentRenderer({ component, queryStates }) {
|
|
782
|
+
const { type, props } = component;
|
|
783
|
+
const dataRef = props.data;
|
|
784
|
+
if (!dataRef) return /* @__PURE__ */ jsxs("div", {
|
|
785
|
+
style: {
|
|
786
|
+
borderRadius: "var(--bon-radius)",
|
|
787
|
+
border: "1px solid var(--bon-border-warn)",
|
|
788
|
+
backgroundColor: "var(--bon-bg-warn)",
|
|
789
|
+
padding: 12,
|
|
790
|
+
fontSize: 14,
|
|
791
|
+
color: "var(--bon-text-warn)"
|
|
792
|
+
},
|
|
793
|
+
children: [
|
|
794
|
+
"Component <",
|
|
795
|
+
type,
|
|
796
|
+
"> is missing a \"data\" prop"
|
|
797
|
+
]
|
|
798
|
+
});
|
|
799
|
+
const qs = queryStates.get(dataRef);
|
|
800
|
+
return /* @__PURE__ */ jsx(QueryLoad, {
|
|
801
|
+
data: qs?.data,
|
|
802
|
+
loading: qs?.loading ?? true,
|
|
803
|
+
error: qs?.error,
|
|
804
|
+
children: (data) => {
|
|
805
|
+
switch (type) {
|
|
806
|
+
case "BigValue": return /* @__PURE__ */ jsx(BigValue, {
|
|
807
|
+
data,
|
|
808
|
+
value: props.value,
|
|
809
|
+
title: props.title,
|
|
810
|
+
fmt: props.fmt
|
|
811
|
+
});
|
|
812
|
+
case "BarChart": return /* @__PURE__ */ jsx(BarChart, {
|
|
813
|
+
data,
|
|
814
|
+
x: props.x,
|
|
815
|
+
y: props.y,
|
|
816
|
+
title: props.title,
|
|
817
|
+
horizontal: props.horizontal === "true",
|
|
818
|
+
series: props.series,
|
|
819
|
+
type: props.type,
|
|
820
|
+
yFmt: props.yFmt
|
|
821
|
+
});
|
|
822
|
+
case "LineChart": return /* @__PURE__ */ jsx(LineChart, {
|
|
823
|
+
data,
|
|
824
|
+
x: props.x,
|
|
825
|
+
y: props.y,
|
|
826
|
+
title: props.title,
|
|
827
|
+
series: props.series,
|
|
828
|
+
type: props.type,
|
|
829
|
+
yFmt: props.yFmt
|
|
830
|
+
});
|
|
831
|
+
case "AreaChart": return /* @__PURE__ */ jsx(AreaChart, {
|
|
832
|
+
data,
|
|
833
|
+
x: props.x,
|
|
834
|
+
y: props.y,
|
|
835
|
+
title: props.title,
|
|
836
|
+
series: props.series,
|
|
837
|
+
type: props.type,
|
|
838
|
+
yFmt: props.yFmt
|
|
839
|
+
});
|
|
840
|
+
case "PieChart": return /* @__PURE__ */ jsx(PieChart, {
|
|
841
|
+
data,
|
|
842
|
+
name: props.name,
|
|
843
|
+
value: props.value,
|
|
844
|
+
title: props.title
|
|
845
|
+
});
|
|
846
|
+
case "DataTable": return /* @__PURE__ */ jsx(DataTable, {
|
|
847
|
+
data,
|
|
848
|
+
columns: props.columns ? props.columns.split(",").map((c) => c.trim()) : void 0,
|
|
849
|
+
fmt: props.fmt,
|
|
850
|
+
rows: props.rows === "all" ? "all" : props.rows ? Number(props.rows) : void 0,
|
|
851
|
+
queryLimit: qs?.limit
|
|
852
|
+
});
|
|
853
|
+
default: return /* @__PURE__ */ jsxs("div", {
|
|
854
|
+
style: {
|
|
855
|
+
borderRadius: "var(--bon-radius)",
|
|
856
|
+
border: "1px solid var(--bon-border-warn)",
|
|
857
|
+
backgroundColor: "var(--bon-bg-warn)",
|
|
858
|
+
padding: 12,
|
|
859
|
+
fontSize: 14,
|
|
860
|
+
color: "var(--bon-text-warn)"
|
|
861
|
+
},
|
|
862
|
+
children: ["Unknown component: ", type]
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/dashboard/dashboard.tsx
|
|
871
|
+
function Dashboard({ slug, content, onInputsChange, className }) {
|
|
872
|
+
if (content) return /* @__PURE__ */ jsx("div", {
|
|
873
|
+
className,
|
|
874
|
+
children: /* @__PURE__ */ jsx(DashboardViewer, {
|
|
875
|
+
content,
|
|
876
|
+
onInputsChange
|
|
877
|
+
})
|
|
878
|
+
});
|
|
879
|
+
if (slug) return /* @__PURE__ */ jsx(DashboardFromSlug, {
|
|
880
|
+
slug,
|
|
881
|
+
onInputsChange,
|
|
882
|
+
className
|
|
883
|
+
});
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
function DashboardFromSlug({ slug, onInputsChange, className }) {
|
|
887
|
+
const { dashboard, loading, error } = useDashboard(slug);
|
|
888
|
+
if (loading) return /* @__PURE__ */ jsx("div", {
|
|
889
|
+
style: {
|
|
890
|
+
display: "flex",
|
|
891
|
+
alignItems: "center",
|
|
892
|
+
justifyContent: "center",
|
|
893
|
+
height: 200,
|
|
894
|
+
fontSize: 14,
|
|
895
|
+
color: "var(--bon-text-muted)"
|
|
896
|
+
},
|
|
897
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
898
|
+
style: {
|
|
899
|
+
display: "flex",
|
|
900
|
+
alignItems: "center",
|
|
901
|
+
gap: 8
|
|
902
|
+
},
|
|
903
|
+
children: [/* @__PURE__ */ jsx("div", { style: {
|
|
904
|
+
width: 16,
|
|
905
|
+
height: 16,
|
|
906
|
+
borderRadius: "50%",
|
|
907
|
+
border: "2px solid var(--bon-spinner-color)",
|
|
908
|
+
borderTopColor: "transparent",
|
|
909
|
+
animation: "bonnard-spin 0.6s linear infinite"
|
|
910
|
+
} }), "Loading dashboard…"]
|
|
911
|
+
})
|
|
912
|
+
});
|
|
913
|
+
if (error) return /* @__PURE__ */ jsxs("div", {
|
|
914
|
+
style: {
|
|
915
|
+
borderRadius: "var(--bon-radius)",
|
|
916
|
+
border: "1px solid var(--bon-border-error)",
|
|
917
|
+
backgroundColor: "var(--bon-bg-error)",
|
|
918
|
+
padding: 16,
|
|
919
|
+
fontSize: 14,
|
|
920
|
+
color: "var(--bon-text-error)"
|
|
921
|
+
},
|
|
922
|
+
children: ["Failed to load dashboard: ", error]
|
|
923
|
+
});
|
|
924
|
+
if (!dashboard) return null;
|
|
925
|
+
return /* @__PURE__ */ jsx("div", {
|
|
926
|
+
className,
|
|
927
|
+
children: /* @__PURE__ */ jsx(DashboardViewer, {
|
|
928
|
+
content: dashboard.content,
|
|
929
|
+
onInputsChange
|
|
930
|
+
})
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
//#endregion
|
|
935
|
+
export { Dashboard, DashboardViewer, parseDashboard, useDashboard };
|