@hachej/boring-bi-dashboard 0.1.60
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 +83 -0
- package/dist/front/index.d.ts +28 -0
- package/dist/front/index.js +831 -0
- package/dist/shared/index.d.ts +16 -0
- package/dist/shared/index.js +131 -0
- package/dist/types-BGZKL9Rs.d.ts +146 -0
- package/docs/issues/bi-dashboard-plugin/README.md +349 -0
- package/docs/issues/bi-dashboard-plugin/data-access-unification.md +638 -0
- package/docs/issues/bi-dashboard-plugin/data-bridge.md +425 -0
- package/example/.gitignore +16 -0
- package/example/.pi/extensions/bi-dashboard/front/index.ts +1 -0
- package/example/.pi/extensions/bi-dashboard/package.json +11 -0
- package/example/README.md +10 -0
- package/example/dashboards/people.dashboard.json +178 -0
- package/example/data/people.csv +13 -0
- package/example/eval/bi-dashboard.yaml +31 -0
- package/package.json +85 -0
- package/playground/README.md +25 -0
- package/playground/run-eval.ts +100 -0
- package/playground/smoke-dashboard.ts +101 -0
- package/skills/bi-dashboard-authoring/SKILL.md +78 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { B as BslDashboardSpec } from '../types-BGZKL9Rs.js';
|
|
2
|
+
export { e as BslChartRenderer, a as BslChartSpec, f as BslChartType, b as BslDashboardComponentSpec, c as BslDashboardQuerySpec, g as BslFilterControlSpec, h as BslMetricSpec, d as BslPerspectiveViewerSpec, i as BslTextSpec, D as DashboardGridSpec, P as PerspectiveViewerPlugin } from '../types-BGZKL9Rs.js';
|
|
3
|
+
import '@hachej/boring-generated-pane/shared';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
interface DashboardValidationResult {
|
|
7
|
+
spec: BslDashboardSpec | null;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
declare function parseDashboardSpec(value: unknown): DashboardValidationResult;
|
|
11
|
+
declare function validateDashboardSpec(value: unknown): {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
errors: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { BslDashboardSpec, type DashboardValidationResult, parseDashboardSpec, validateDashboardSpec };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/shared/validation.ts
|
|
2
|
+
import { parseGeneratedPaneSpec } from "@hachej/boring-generated-pane/shared";
|
|
3
|
+
|
|
4
|
+
// src/shared/schemas.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var chartRenderers = ["echarts", "vega-lite", "plotly"];
|
|
7
|
+
var chartTypes = ["bar", "line", "area", "scatter", "heatmap", "pie", "treemap", "sunburst", "gauge", "table"];
|
|
8
|
+
var perspectivePlugins = ["Datagrid", "Y Bar", "X Bar", "Y Line", "Y Area", "Y Scatter", "Y Treemap", "Sunburst", "Heatmap"];
|
|
9
|
+
var metricFormats = ["number", "currency", "percent"];
|
|
10
|
+
var filterControlTypes = ["select", "multiSelect", "dateRange", "numberRange", "search"];
|
|
11
|
+
var sortDirections = ["asc", "desc"];
|
|
12
|
+
var dashboardQuerySchema = z.union([
|
|
13
|
+
z.object({
|
|
14
|
+
id: z.string().min(1),
|
|
15
|
+
model: z.string().min(1),
|
|
16
|
+
query: z.string().min(1),
|
|
17
|
+
limit: z.number().int().safe().min(1).optional()
|
|
18
|
+
}),
|
|
19
|
+
z.object({
|
|
20
|
+
id: z.string().min(1),
|
|
21
|
+
source: z.string().min(1).optional(),
|
|
22
|
+
sql: z.string().min(1),
|
|
23
|
+
params: z.record(z.string(), z.unknown()).optional(),
|
|
24
|
+
limit: z.number().int().safe().min(1).optional()
|
|
25
|
+
})
|
|
26
|
+
]);
|
|
27
|
+
var dashboardGridPropsSchema = z.object({
|
|
28
|
+
title: z.string().optional(),
|
|
29
|
+
columns: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(6), z.literal(12)]).optional()
|
|
30
|
+
});
|
|
31
|
+
var bslMetricPropsSchema = z.object({
|
|
32
|
+
queryId: z.string(),
|
|
33
|
+
valueField: z.string(),
|
|
34
|
+
label: z.string(),
|
|
35
|
+
format: z.enum(metricFormats).optional()
|
|
36
|
+
});
|
|
37
|
+
var bslChartPropsSchema = z.object({
|
|
38
|
+
queryId: z.string(),
|
|
39
|
+
title: z.string().optional(),
|
|
40
|
+
renderer: z.enum(chartRenderers).optional(),
|
|
41
|
+
chartType: z.enum(chartTypes),
|
|
42
|
+
x: z.string().optional(),
|
|
43
|
+
y: z.union([z.string(), z.array(z.string())]).optional(),
|
|
44
|
+
color: z.string().optional(),
|
|
45
|
+
controls: z.array(z.string()).optional()
|
|
46
|
+
});
|
|
47
|
+
var bslPerspectiveViewerPropsSchema = z.object({
|
|
48
|
+
queryId: z.string(),
|
|
49
|
+
title: z.string().optional(),
|
|
50
|
+
plugin: z.enum(perspectivePlugins).optional(),
|
|
51
|
+
columns: z.array(z.string()).optional(),
|
|
52
|
+
groupBy: z.array(z.string()).optional(),
|
|
53
|
+
splitBy: z.array(z.string()).optional(),
|
|
54
|
+
sort: z.array(z.tuple([z.string(), z.enum(sortDirections)])).optional()
|
|
55
|
+
});
|
|
56
|
+
var bslFilterPropsSchema = z.object({
|
|
57
|
+
id: z.string(),
|
|
58
|
+
field: z.string(),
|
|
59
|
+
label: z.string().optional(),
|
|
60
|
+
controlType: z.enum(filterControlTypes),
|
|
61
|
+
targetQueries: z.array(z.string())
|
|
62
|
+
});
|
|
63
|
+
var bslTextPropsSchema = z.object({
|
|
64
|
+
markdown: z.string()
|
|
65
|
+
});
|
|
66
|
+
var componentPropsSchemas = {
|
|
67
|
+
DashboardGrid: dashboardGridPropsSchema,
|
|
68
|
+
BSLMetric: bslMetricPropsSchema,
|
|
69
|
+
BSLChart: bslChartPropsSchema,
|
|
70
|
+
BSLPerspectiveViewer: bslPerspectiveViewerPropsSchema,
|
|
71
|
+
BSLFilter: bslFilterPropsSchema,
|
|
72
|
+
BSLText: bslTextPropsSchema
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/shared/validation.ts
|
|
76
|
+
function isRecord(value) {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
function formatSchemaErrors(prefix, error) {
|
|
80
|
+
return error.issues.map((issue) => `${prefix}${issue.path.length ? `.${issue.path.map(String).join(".")}` : ""}: ${issue.message}`);
|
|
81
|
+
}
|
|
82
|
+
function parseDashboardSpec(value) {
|
|
83
|
+
const base = parseGeneratedPaneSpec(value);
|
|
84
|
+
if (!base.spec) return { spec: null, errors: base.errors };
|
|
85
|
+
const errors = [];
|
|
86
|
+
if (base.spec.profile !== "bi-dashboard") errors.push('dashboard spec profile must be "bi-dashboard"');
|
|
87
|
+
if (typeof base.spec.title !== "string" || base.spec.title.length === 0) errors.push("dashboard spec needs a title");
|
|
88
|
+
if (!isRecord(base.spec.queries)) errors.push("dashboard spec needs a queries object");
|
|
89
|
+
if (errors.length > 0) return { spec: null, errors };
|
|
90
|
+
const queries = base.spec.queries;
|
|
91
|
+
for (const [id, query] of Object.entries(queries)) {
|
|
92
|
+
const parsed = dashboardQuerySchema.safeParse(query);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
errors.push(...formatSchemaErrors(`query ${id}`, parsed.error));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (parsed.data.id !== id) errors.push(`query ${id} must repeat its id field`);
|
|
98
|
+
}
|
|
99
|
+
for (const [id, element] of Object.entries(base.spec.elements)) {
|
|
100
|
+
const type = element.type;
|
|
101
|
+
const schema = componentPropsSchemas[type];
|
|
102
|
+
if (!schema) {
|
|
103
|
+
errors.push(`component ${id} has unsupported type ${String(element.type)}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const props = schema.safeParse(element.props ?? {});
|
|
107
|
+
if (!props.success) errors.push(...formatSchemaErrors(`component ${id}.props`, props.error));
|
|
108
|
+
if (type === "DashboardGrid" && !Array.isArray(element.children)) {
|
|
109
|
+
errors.push(`DashboardGrid ${id} must include string children`);
|
|
110
|
+
}
|
|
111
|
+
if ((type === "BSLMetric" || type === "BSLChart" || type === "BSLPerspectiveViewer") && props.success) {
|
|
112
|
+
const queryId = props.data.queryId;
|
|
113
|
+
if (!queries[queryId]) errors.push(`component ${id} references unknown query ${queryId}`);
|
|
114
|
+
}
|
|
115
|
+
if (type === "BSLFilter" && props.success) {
|
|
116
|
+
for (const queryId of props.data.targetQueries) {
|
|
117
|
+
if (!queries[queryId]) errors.push(`BSLFilter ${id} references unknown query ${queryId}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (errors.length > 0) return { spec: null, errors };
|
|
122
|
+
return { spec: base.spec, errors: [] };
|
|
123
|
+
}
|
|
124
|
+
function validateDashboardSpec(value) {
|
|
125
|
+
const result = parseDashboardSpec(value);
|
|
126
|
+
return { ok: result.spec !== null, errors: result.errors };
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
parseDashboardSpec,
|
|
130
|
+
validateDashboardSpec
|
|
131
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { GeneratedPaneSpec, GeneratedPaneElementSpec } from '@hachej/boring-generated-pane/shared';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
declare const dashboardQuerySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
5
|
+
id: z.ZodString;
|
|
6
|
+
model: z.ZodString;
|
|
7
|
+
query: z.ZodString;
|
|
8
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
10
|
+
id: z.ZodString;
|
|
11
|
+
source: z.ZodOptional<z.ZodString>;
|
|
12
|
+
sql: z.ZodString;
|
|
13
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
14
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
}, z.core.$strip>]>;
|
|
16
|
+
declare const dashboardGridPropsSchema: z.ZodObject<{
|
|
17
|
+
title: z.ZodOptional<z.ZodString>;
|
|
18
|
+
columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<6>, z.ZodLiteral<12>]>>;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
declare const bslMetricPropsSchema: z.ZodObject<{
|
|
21
|
+
queryId: z.ZodString;
|
|
22
|
+
valueField: z.ZodString;
|
|
23
|
+
label: z.ZodString;
|
|
24
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
25
|
+
number: "number";
|
|
26
|
+
currency: "currency";
|
|
27
|
+
percent: "percent";
|
|
28
|
+
}>>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
declare const bslChartPropsSchema: z.ZodObject<{
|
|
31
|
+
queryId: z.ZodString;
|
|
32
|
+
title: z.ZodOptional<z.ZodString>;
|
|
33
|
+
renderer: z.ZodOptional<z.ZodEnum<{
|
|
34
|
+
echarts: "echarts";
|
|
35
|
+
"vega-lite": "vega-lite";
|
|
36
|
+
plotly: "plotly";
|
|
37
|
+
}>>;
|
|
38
|
+
chartType: z.ZodEnum<{
|
|
39
|
+
bar: "bar";
|
|
40
|
+
line: "line";
|
|
41
|
+
area: "area";
|
|
42
|
+
scatter: "scatter";
|
|
43
|
+
heatmap: "heatmap";
|
|
44
|
+
pie: "pie";
|
|
45
|
+
treemap: "treemap";
|
|
46
|
+
sunburst: "sunburst";
|
|
47
|
+
gauge: "gauge";
|
|
48
|
+
table: "table";
|
|
49
|
+
}>;
|
|
50
|
+
x: z.ZodOptional<z.ZodString>;
|
|
51
|
+
y: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
52
|
+
color: z.ZodOptional<z.ZodString>;
|
|
53
|
+
controls: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
declare const bslPerspectiveViewerPropsSchema: z.ZodObject<{
|
|
56
|
+
queryId: z.ZodString;
|
|
57
|
+
title: z.ZodOptional<z.ZodString>;
|
|
58
|
+
plugin: z.ZodOptional<z.ZodEnum<{
|
|
59
|
+
Datagrid: "Datagrid";
|
|
60
|
+
"Y Bar": "Y Bar";
|
|
61
|
+
"X Bar": "X Bar";
|
|
62
|
+
"Y Line": "Y Line";
|
|
63
|
+
"Y Area": "Y Area";
|
|
64
|
+
"Y Scatter": "Y Scatter";
|
|
65
|
+
"Y Treemap": "Y Treemap";
|
|
66
|
+
Sunburst: "Sunburst";
|
|
67
|
+
Heatmap: "Heatmap";
|
|
68
|
+
}>>;
|
|
69
|
+
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
70
|
+
groupBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
71
|
+
splitBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
72
|
+
sort: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodEnum<{
|
|
73
|
+
asc: "asc";
|
|
74
|
+
desc: "desc";
|
|
75
|
+
}>], null>>>;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
declare const bslFilterPropsSchema: z.ZodObject<{
|
|
78
|
+
id: z.ZodString;
|
|
79
|
+
field: z.ZodString;
|
|
80
|
+
label: z.ZodOptional<z.ZodString>;
|
|
81
|
+
controlType: z.ZodEnum<{
|
|
82
|
+
select: "select";
|
|
83
|
+
multiSelect: "multiSelect";
|
|
84
|
+
dateRange: "dateRange";
|
|
85
|
+
numberRange: "numberRange";
|
|
86
|
+
search: "search";
|
|
87
|
+
}>;
|
|
88
|
+
targetQueries: z.ZodArray<z.ZodString>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
declare const bslTextPropsSchema: z.ZodObject<{
|
|
91
|
+
markdown: z.ZodString;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
type DashboardGridProps = z.infer<typeof dashboardGridPropsSchema>;
|
|
94
|
+
type BslMetricProps = z.infer<typeof bslMetricPropsSchema>;
|
|
95
|
+
type BslChartProps = z.infer<typeof bslChartPropsSchema>;
|
|
96
|
+
type BslPerspectiveViewerProps = z.infer<typeof bslPerspectiveViewerPropsSchema>;
|
|
97
|
+
type BslFilterProps = z.infer<typeof bslFilterPropsSchema>;
|
|
98
|
+
type BslTextProps = z.infer<typeof bslTextPropsSchema>;
|
|
99
|
+
type BslDashboardQuerySpec = z.infer<typeof dashboardQuerySchema>;
|
|
100
|
+
|
|
101
|
+
type BslChartRenderer = BslChartProps["renderer"];
|
|
102
|
+
type BslChartType = BslChartProps["chartType"];
|
|
103
|
+
type PerspectiveViewerPlugin = BslPerspectiveViewerProps["plugin"];
|
|
104
|
+
interface BiDashboardSpec extends Omit<GeneratedPaneSpec, "profile" | "queries" | "elements"> {
|
|
105
|
+
profile: "bi-dashboard";
|
|
106
|
+
queries: Record<string, BslDashboardQuerySpec>;
|
|
107
|
+
elements: Record<string, BiDashboardElementSpec>;
|
|
108
|
+
}
|
|
109
|
+
type BslDashboardSpec = BiDashboardSpec;
|
|
110
|
+
type BslDashboardComponentSpec = BiDashboardElementSpec;
|
|
111
|
+
type BiDashboardElementSpec = DashboardGridSpec | BslMetricSpec | BslChartSpec | BslPerspectiveViewerSpec | BslFilterControlSpec | BslTextSpec;
|
|
112
|
+
interface BaseElementSpec extends GeneratedPaneElementSpec {
|
|
113
|
+
props: Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
interface DashboardGridSpec extends BaseElementSpec {
|
|
116
|
+
type: "DashboardGrid";
|
|
117
|
+
props: DashboardGridProps;
|
|
118
|
+
children: string[];
|
|
119
|
+
}
|
|
120
|
+
interface BslMetricSpec extends BaseElementSpec {
|
|
121
|
+
type: "BSLMetric";
|
|
122
|
+
props: BslMetricProps;
|
|
123
|
+
children?: [];
|
|
124
|
+
}
|
|
125
|
+
interface BslChartSpec extends BaseElementSpec {
|
|
126
|
+
type: "BSLChart";
|
|
127
|
+
props: BslChartProps;
|
|
128
|
+
children?: [];
|
|
129
|
+
}
|
|
130
|
+
interface BslPerspectiveViewerSpec extends BaseElementSpec {
|
|
131
|
+
type: "BSLPerspectiveViewer";
|
|
132
|
+
props: BslPerspectiveViewerProps;
|
|
133
|
+
children?: [];
|
|
134
|
+
}
|
|
135
|
+
interface BslFilterControlSpec extends BaseElementSpec {
|
|
136
|
+
type: "BSLFilter";
|
|
137
|
+
props: BslFilterProps;
|
|
138
|
+
children?: [];
|
|
139
|
+
}
|
|
140
|
+
interface BslTextSpec extends BaseElementSpec {
|
|
141
|
+
type: "BSLText";
|
|
142
|
+
props: BslTextProps;
|
|
143
|
+
children?: [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type { BslDashboardSpec as B, DashboardGridSpec as D, PerspectiveViewerPlugin as P, BslChartSpec as a, BslDashboardComponentSpec as b, BslDashboardQuerySpec as c, BslPerspectiveViewerSpec as d, BslChartRenderer as e, BslChartType as f, BslFilterControlSpec as g, BslMetricSpec as h, BslTextSpec as i };
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# BI Dashboard Plugin Runtime Plan
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Follow-up plan for `@hachej/boring-bi-dashboard` after the dashboard authoring
|
|
6
|
+
skeleton PR. This plan assumes WorkspaceBridge RPC v1 from PR #71 and the new
|
|
7
|
+
`@hachej/boring-data-bridge` plugin.
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Turn agent-authored `boring.generated-pane` JSON specs with `profile: "bi-dashboard"` into live dashboards while keeping
|
|
12
|
+
agents and UI components provider-neutral.
|
|
13
|
+
|
|
14
|
+
The BI dashboard plugin owns dashboard authoring/rendering. It does **not** own
|
|
15
|
+
provider credentials, raw SQL execution, BSL server setup, or Perspective server
|
|
16
|
+
transport. Those are supplied by data-bridge adapters.
|
|
17
|
+
|
|
18
|
+
## Current state
|
|
19
|
+
|
|
20
|
+
The current plugin skeleton provides:
|
|
21
|
+
|
|
22
|
+
- controlled component catalog:
|
|
23
|
+
- `DashboardGrid`
|
|
24
|
+
- `BSLMetric`
|
|
25
|
+
- `BSLChart`
|
|
26
|
+
- `BSLPerspectiveViewer`
|
|
27
|
+
- `BSLFilter`
|
|
28
|
+
- `BSLText`
|
|
29
|
+
- shared generated-pane dashboard TypeScript types
|
|
30
|
+
- runtime validation before rendering untyped pane params
|
|
31
|
+
- `bi-dashboard-authoring` skill
|
|
32
|
+
- workspace-playground eval where the agent simply creates a dashboard
|
|
33
|
+
|
|
34
|
+
It currently renders placeholders and a query manifest. It does not execute data
|
|
35
|
+
queries.
|
|
36
|
+
|
|
37
|
+
## Non-goals
|
|
38
|
+
|
|
39
|
+
- Do not add BI-dashboard-specific data endpoints.
|
|
40
|
+
- Do not make dashboard specs provider-specific.
|
|
41
|
+
- Do not ask agents to write raw React, ECharts, Perspective JS, or SQL.
|
|
42
|
+
- Do not duplicate data-bridge adapter logic inside the dashboard plugin.
|
|
43
|
+
- Do not require live Perspective server mode for the first runtime version.
|
|
44
|
+
|
|
45
|
+
## Dashboard contract evolution
|
|
46
|
+
|
|
47
|
+
Keep the authoring contract high-level:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"kind": "boring.generated-pane",
|
|
52
|
+
"profile": "bi-dashboard",
|
|
53
|
+
"version": 1,
|
|
54
|
+
"title": "Orders Revenue",
|
|
55
|
+
"queries": {
|
|
56
|
+
"revenue_by_month": {
|
|
57
|
+
"id": "revenue_by_month",
|
|
58
|
+
"model": "orders",
|
|
59
|
+
"groupBy": ["month"],
|
|
60
|
+
"measures": ["revenue"]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"root": "dashboard",
|
|
64
|
+
"elements": {
|
|
65
|
+
"dashboard": { "type": "DashboardGrid", "props": { "columns": 2 }, "children": ["chart"] },
|
|
66
|
+
"chart": {
|
|
67
|
+
"type": "BSLChart",
|
|
68
|
+
"props": { "queryId": "revenue_by_month", "renderer": "echarts", "chartType": "line", "x": "month", "y": "revenue" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This query shape compiles to data-bridge query input:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
{
|
|
78
|
+
language: "bsl-dashboard",
|
|
79
|
+
model: "orders",
|
|
80
|
+
groupBy: ["month"],
|
|
81
|
+
measures: ["revenue"]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The data-bridge dashboard query shape must stay isomorphic with
|
|
86
|
+
`BslDashboardQuerySpec`: `filters` are `{ field, op, value }[]`, and `orderBy` is
|
|
87
|
+
`[field, direction][]`. The BSL data-bridge adapter can then compile to BSL's
|
|
88
|
+
native query string:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
sm.group_by("month").aggregate("revenue")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use BSL's existing query-string parser/executor rather than creating a second
|
|
95
|
+
BSL execution DSL. Direct `bsl-python` strings are a trusted data-bridge feature,
|
|
96
|
+
not what dashboard browser rendering sends by default.
|
|
97
|
+
|
|
98
|
+
## Runtime architecture
|
|
99
|
+
|
|
100
|
+
```txt
|
|
101
|
+
Dashboard JSON file / pane params
|
|
102
|
+
↓ validate boring.generated-pane + bi-dashboard profile
|
|
103
|
+
BiDashboardPane
|
|
104
|
+
↓ collect queryIds used by visible components
|
|
105
|
+
DataBridge browser/runtime client
|
|
106
|
+
↓ WorkspaceBridge RPC data.v1.*
|
|
107
|
+
DataBridge trusted server plugin
|
|
108
|
+
↓ per-query source resolution by host config/model id or explicit spec dataSource
|
|
109
|
+
↓ selected adapter: BSL / DuckDB / Macro / static file
|
|
110
|
+
Portable result / Perspective descriptor
|
|
111
|
+
↓
|
|
112
|
+
BSLMetric, BSLChart, BSLPerspectiveViewer render live UI
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Data source resolution
|
|
116
|
+
|
|
117
|
+
Runtime work must choose a deterministic source routing rule before fetching
|
|
118
|
+
data. Use this order:
|
|
119
|
+
|
|
120
|
+
1. Optional future `spec.dataSource` if the dashboard author/host provides one.
|
|
121
|
+
2. Host-configured model routing, e.g. `orders -> bsl-main`, `macro_series -> macro`.
|
|
122
|
+
3. A single default data-bridge adapter if the host configured exactly one.
|
|
123
|
+
4. Otherwise fail closed with an actionable "ambiguous data source" error.
|
|
124
|
+
|
|
125
|
+
Do not silently try every adapter for live dashboards unless the host explicitly
|
|
126
|
+
enables adapter probing for that workspace; probing can leak model names and make
|
|
127
|
+
failures non-deterministic. Source resolution is per query, not per dashboard,
|
|
128
|
+
because a valid dashboard may combine models routed to different adapters.
|
|
129
|
+
|
|
130
|
+
## Data access
|
|
131
|
+
|
|
132
|
+
### Metrics and charts
|
|
133
|
+
|
|
134
|
+
Use `data.v1.query.run`:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const query = spec.queries[queryId]
|
|
138
|
+
bridge.call("data.v1.query.run", {
|
|
139
|
+
source: resolveDashboardDataSource(spec, query),
|
|
140
|
+
query: compileDashboardQuery(query),
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Return:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
DataBridgeTableResult
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Rendering rules:
|
|
151
|
+
|
|
152
|
+
- `BSLMetric`: first row + `valueField`.
|
|
153
|
+
- `BSLChart`: rows + declarative chart props → ECharts option generated inside
|
|
154
|
+
plugin.
|
|
155
|
+
- `BSLFilter`: updates local dashboard filter state and re-runs affected query
|
|
156
|
+
ids.
|
|
157
|
+
|
|
158
|
+
### Perspective panels
|
|
159
|
+
|
|
160
|
+
For plain `workspace-file` dashboard queries, use the file data/Perspective endpoint once available. For semantic, BSL, DuckDB, SQL, or remote-adapter queries, use `data.v1.perspective.prepare`:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const query = spec.queries[component.props.queryId]
|
|
164
|
+
if (query.dataRef?.kind === "workspace-file") {
|
|
165
|
+
fileData.preparePerspective({ path: query.dataRef.path, viewer, transport })
|
|
166
|
+
} else {
|
|
167
|
+
bridge.call("data.v1.perspective.prepare", {
|
|
168
|
+
source: resolveDashboardDataSource(spec, query),
|
|
169
|
+
datasetId: component.props.queryId,
|
|
170
|
+
query: compileDashboardQuery(query),
|
|
171
|
+
viewer: {
|
|
172
|
+
plugin: component.props.plugin,
|
|
173
|
+
columns: component.props.columns,
|
|
174
|
+
group_by: component.props.groupBy,
|
|
175
|
+
split_by: component.props.splitBy,
|
|
176
|
+
sort: component.props.sort,
|
|
177
|
+
filter: compilePerspectiveFilters(component.props.filters, activeDashboardFilters),
|
|
178
|
+
},
|
|
179
|
+
transport: {
|
|
180
|
+
preferred: "auto",
|
|
181
|
+
accepted: ["inline", "artifact", "websocket"],
|
|
182
|
+
payloadFormat: "arrow",
|
|
183
|
+
maxInlineBytes: 1_000_000,
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The dashboard advertises supported transports and preferences; the server
|
|
190
|
+
selects the actual safe transport. First runtime version may load inline
|
|
191
|
+
JSON/Arrow into `@finos/perspective` in browser/WASM; larger static results
|
|
192
|
+
should use Arrow artifacts, and live/server mode should use websocket.
|
|
193
|
+
|
|
194
|
+
Perspective filter mapping is explicit: component `props.filters` and active
|
|
195
|
+
`BSLFilter` state are merged as `BslFilterExpression[]`, then converted to
|
|
196
|
+
Perspective viewer filters. Scalar comparisons map directly; `in` expands to the
|
|
197
|
+
adapter-supported equivalent; `between` maps to two filters (`>=` and `<=`); and
|
|
198
|
+
unsupported `contains` behavior must fail visibly instead of being dropped.
|
|
199
|
+
|
|
200
|
+
Later proper client/server replicated mode follows Perspective's architecture:
|
|
201
|
+
|
|
202
|
+
```txt
|
|
203
|
+
server Perspective table
|
|
204
|
+
↔ websocket + Arrow deltas
|
|
205
|
+
browser Perspective websocket client
|
|
206
|
+
→ server_table.view()
|
|
207
|
+
→ local WASM worker.table(server_view)
|
|
208
|
+
→ <perspective-viewer>.load(client_table)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The dashboard plugin should consume a returned descriptor; it should not decide
|
|
212
|
+
how the server table is created.
|
|
213
|
+
|
|
214
|
+
## Frontend components
|
|
215
|
+
|
|
216
|
+
### `useDashboardData(spec, filters)`
|
|
217
|
+
|
|
218
|
+
Responsibilities:
|
|
219
|
+
|
|
220
|
+
- validate spec already passed
|
|
221
|
+
- discover query dependencies by component type
|
|
222
|
+
- dedupe concurrent requests by query id and filter key
|
|
223
|
+
- call data-bridge
|
|
224
|
+
- expose `{ dataByQueryId, loadingByQueryId, errorByQueryId, refresh }`
|
|
225
|
+
|
|
226
|
+
### `BSLMetricRenderer`
|
|
227
|
+
|
|
228
|
+
- receives `DataBridgeTableResult | undefined`
|
|
229
|
+
- formats number/currency/percent
|
|
230
|
+
- shows loading/error/empty states
|
|
231
|
+
|
|
232
|
+
### `BSLChartRenderer`
|
|
233
|
+
|
|
234
|
+
- receives `DataBridgeTableResult | undefined`
|
|
235
|
+
- maps declarative chart props to ECharts options
|
|
236
|
+
- keeps chart option generation local and deterministic
|
|
237
|
+
- never accepts raw ECharts options from dashboard JSON
|
|
238
|
+
|
|
239
|
+
### `BSLPerspectiveRuntimeViewer`
|
|
240
|
+
|
|
241
|
+
- calls `data.v1.perspective.prepare` or consumes prepared descriptor
|
|
242
|
+
- supports inline dataset first
|
|
243
|
+
- later supports websocket descriptor
|
|
244
|
+
- maps camelCase dashboard props to Perspective's `group_by`/`split_by` config
|
|
245
|
+
|
|
246
|
+
## Server integration
|
|
247
|
+
|
|
248
|
+
The dashboard plugin should not register its own data handlers. It should depend
|
|
249
|
+
on data-bridge being installed by the host. If data-bridge is missing, render a
|
|
250
|
+
clear empty state:
|
|
251
|
+
|
|
252
|
+
```txt
|
|
253
|
+
Live data is unavailable because @hachej/boring-data-bridge is not installed.
|
|
254
|
+
The dashboard spec is valid and can still be edited.
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Workspace-playground should install:
|
|
258
|
+
|
|
259
|
+
```txt
|
|
260
|
+
@hachej/boring-bi-dashboard
|
|
261
|
+
@hachej/boring-data-bridge
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
and configure data-bridge with a DuckDB/static adapter over fixture CSV files.
|
|
265
|
+
|
|
266
|
+
BSL-backed apps should configure data-bridge with a BSL adapter using model path
|
|
267
|
+
and profile settings.
|
|
268
|
+
|
|
269
|
+
## Agent skill update
|
|
270
|
+
|
|
271
|
+
The `bi-dashboard-authoring` skill should stay brief. It should teach:
|
|
272
|
+
|
|
273
|
+
- create `dashboards/*.dashboard.json`
|
|
274
|
+
- use `kind: boring.generated-pane and profile: bi-dashboard`, `version: 1`
|
|
275
|
+
- use the controlled components and exact prop names
|
|
276
|
+
- author semantic dashboard queries with `model`, `groupBy`, `measures`, etc.
|
|
277
|
+
- do not write raw React/ECharts/Perspective
|
|
278
|
+
|
|
279
|
+
It should not include provider setup details. Provider details belong in a data
|
|
280
|
+
source/data-bridge skill if needed.
|
|
281
|
+
|
|
282
|
+
## Implementation phases
|
|
283
|
+
|
|
284
|
+
### Phase 0 — Contract validator hardening
|
|
285
|
+
|
|
286
|
+
- Extend `validateDashboardSpec` before any runtime data execution.
|
|
287
|
+
- Validate query `filters`, query `orderBy`, and query `limit` shapes.
|
|
288
|
+
- Validate `BSLPerspectiveViewer.props.filters` with the same `BslFilterExpression` rules.
|
|
289
|
+
- Add regression tests proving malformed filters/order/limits are rejected before data-bridge compilation.
|
|
290
|
+
|
|
291
|
+
### Phase 1 — Data-bridge client seam
|
|
292
|
+
|
|
293
|
+
- Add a tiny dashboard data client abstraction.
|
|
294
|
+
- In tests, mock it rather than mocking `fetch` directly.
|
|
295
|
+
- Add missing-data empty states.
|
|
296
|
+
|
|
297
|
+
### Phase 2 — Query execution for metrics/charts
|
|
298
|
+
|
|
299
|
+
- Compile dashboard query shape to data-bridge `bsl-dashboard` input.
|
|
300
|
+
- Fetch `data.v1.query.run` for `BSLMetric` and `BSLChart`.
|
|
301
|
+
- Render real metric values.
|
|
302
|
+
- Add minimal ECharts runtime for line/bar/heatmap/table, or a placeholder until
|
|
303
|
+
ECharts dependency is explicitly accepted.
|
|
304
|
+
|
|
305
|
+
### Phase 3 — Filters
|
|
306
|
+
|
|
307
|
+
- Implement `BSLFilter` state.
|
|
308
|
+
- Re-run only target queries.
|
|
309
|
+
- Encode filter state in the data-bridge query input.
|
|
310
|
+
|
|
311
|
+
### Phase 4 — Perspective negotiated runtime
|
|
312
|
+
|
|
313
|
+
- Add Perspective dependency behind the BI dashboard plugin.
|
|
314
|
+
- Call `data.v1.perspective.prepare` with `transport.preferred: "auto"`, accepted transports, payload format, and max inline bytes.
|
|
315
|
+
- Load returned inline JSON/Arrow into browser Perspective worker/viewer for small results.
|
|
316
|
+
- Load returned Arrow artifacts for larger static results once artifact support exists.
|
|
317
|
+
|
|
318
|
+
### Phase 5 — Perspective replicated mode
|
|
319
|
+
|
|
320
|
+
- Accept data-bridge websocket descriptors.
|
|
321
|
+
- Use Perspective websocket client + browser WASM replication.
|
|
322
|
+
- Keep client/server table lifecycle in data-bridge/Perspective adapter.
|
|
323
|
+
|
|
324
|
+
### Phase 6 — Evals and validation
|
|
325
|
+
|
|
326
|
+
- Keep the main eval simple: "Create a BI dashboard...".
|
|
327
|
+
- Add separate advanced evals for stress cases, but make them validate generated
|
|
328
|
+
JSON with the shared parser.
|
|
329
|
+
- Add runtime tests for invalid specs, data errors, filter updates, and
|
|
330
|
+
Perspective descriptor loading.
|
|
331
|
+
|
|
332
|
+
## Validation
|
|
333
|
+
|
|
334
|
+
- `@hachej/boring-bi-dashboard` typecheck/test/build.
|
|
335
|
+
- Contract tests for dashboard query compilation.
|
|
336
|
+
- Validator tests for query filters/orderBy/limit and Perspective viewer filters.
|
|
337
|
+
- Component tests with mocked data-bridge client.
|
|
338
|
+
- WorkspaceBridge e2e with data-bridge installed.
|
|
339
|
+
- Generated dashboard eval followed by `validateDashboardSpec`.
|
|
340
|
+
- Advanced dashboard evals for SaaS, supply chain, finance, and operations.
|
|
341
|
+
|
|
342
|
+
## Open questions
|
|
343
|
+
|
|
344
|
+
- Should `dataSource` be added to `BslDashboardSpec` in v1.1, or kept entirely
|
|
345
|
+
host-configured for portability? Runtime v1 should support both explicit and
|
|
346
|
+
host-configured source resolution as described above.
|
|
347
|
+
- Which ECharts subset is accepted for v1: line/bar/heatmap/table only, or more?
|
|
348
|
+
- Should inline Perspective be allowed for large results, or should data-bridge
|
|
349
|
+
force artifact/websocket above a row threshold?
|