@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/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@hachej/boring-bi-dashboard",
3
+ "version": "0.1.60",
4
+ "type": "module",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "description": "BI dashboard plugin primitives for Boring workspace: BSL-backed dashboard specs, charts, metrics, filters, and Perspective panels.",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hachej/boring-ui"
11
+ },
12
+ "homepage": "https://github.com/hachej/boring-ui",
13
+ "boring": {
14
+ "id": "bi-dashboard",
15
+ "label": "BI Dashboard",
16
+ "front": "dist/front/index.js",
17
+ "server": false
18
+ },
19
+ "pi": {
20
+ "skills": [
21
+ "skills/bi-dashboard-authoring"
22
+ ],
23
+ "systemPrompt": "Use the bi-dashboard-authoring skill when the user asks to create, edit, or open a BI dashboard. Dashboard files should live under dashboards/*.dashboard.json."
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "skills",
28
+ "example",
29
+ "playground",
30
+ "docs",
31
+ "README.md"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/front/index.d.ts",
36
+ "import": "./dist/front/index.js"
37
+ },
38
+ "./front": {
39
+ "types": "./dist/front/index.d.ts",
40
+ "import": "./dist/front/index.js"
41
+ },
42
+ "./shared": {
43
+ "types": "./dist/shared/index.d.ts",
44
+ "import": "./dist/shared/index.js"
45
+ },
46
+ "./package.json": "./package.json"
47
+ },
48
+ "sideEffects": false,
49
+ "peerDependencies": {
50
+ "react": "^18.0.0 || ^19.0.0",
51
+ "react-dom": "^18.0.0 || ^19.0.0",
52
+ "@hachej/boring-workspace": "0.1.60"
53
+ },
54
+ "dependencies": {
55
+ "lucide-react": "^1.8.0",
56
+ "zod": "^4.3.6",
57
+ "@hachej/boring-generated-pane": "0.1.60",
58
+ "@hachej/boring-ui-kit": "0.1.60"
59
+ },
60
+ "devDependencies": {
61
+ "@duckdb/node-api": "1.5.2-r.1",
62
+ "@json-render/core": "^0.19.0",
63
+ "@json-render/react": "^0.19.0",
64
+ "@types/react": "^19.0.0",
65
+ "@types/react-dom": "^19.0.0",
66
+ "react": "^19.0.0",
67
+ "react-dom": "^19.0.0",
68
+ "tsup": "^8.4.0",
69
+ "tsx": "^4.21.0",
70
+ "typescript": "~5.9.3",
71
+ "vitest": "^3.2.6",
72
+ "@hachej/boring-agent": "0.1.60",
73
+ "@hachej/boring-data-bridge": "0.1.60",
74
+ "@hachej/boring-workspace": "0.1.60"
75
+ },
76
+ "scripts": {
77
+ "build": "tsup",
78
+ "typecheck": "tsc --noEmit",
79
+ "test": "vitest run --passWithNoTests",
80
+ "playground:eval": "tsx playground/run-eval.ts",
81
+ "playground:smoke": "tsx playground/smoke-dashboard.ts",
82
+ "lint": "pnpm run typecheck",
83
+ "clean": "rm -rf dist .tsbuildinfo"
84
+ }
85
+ }
@@ -0,0 +1,25 @@
1
+ # BI dashboard playground
2
+
3
+ This folder contains plugin-local playground helpers. The repo-level `workspace-playground` stays generic; BI dashboard is loaded explicitly when needed.
4
+
5
+ ## Browser playground
6
+
7
+ Run the plugin in the existing workspace playground using the fixture in `../example`. The demo workspace includes `.pi/extensions/bi-dashboard`, which loads the front plugin as a workspace-local extension. For live query data, use the eval runner or another host that loads `@hachej/boring-data-bridge` as a trusted server plugin:
8
+
9
+ ```bash
10
+ pnpm --filter @hachej/boring-data-bridge build
11
+ pnpm --filter @hachej/boring-bi-dashboard build
12
+ BORING_EXTERNAL_PLUGINS=1 \
13
+ BORING_AGENT_WORKSPACE_ROOT="$PWD/plugins/bi-dashboard/example" \
14
+ pnpm --filter workspace-playground dev
15
+ ```
16
+
17
+ ## Eval playground
18
+
19
+ Run the dashboard authoring eval through the plugin-local runner:
20
+
21
+ ```bash
22
+ pnpm --filter @hachej/boring-bi-dashboard playground:eval
23
+ ```
24
+
25
+ The runner seeds a temp workspace from `../example` and loads `@hachej/boring-data-bridge` plus `@hachej/boring-bi-dashboard` explicitly.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env -S tsx
2
+ import { cpSync, mkdtempSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { dirname, join, resolve } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+ import { runEvalSuite, type SuiteReport } from "@hachej/boring-agent/eval"
7
+ import { createWorkspaceAgentServer } from "@hachej/boring-workspace/app/server"
8
+ import { parseDashboardSpec } from "../src/shared/validation"
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+ const PLUGIN_ROOT = resolve(__dirname, "..")
13
+ const EXAMPLE_ROOT = resolve(PLUGIN_ROOT, "example")
14
+ const DEFAULT_EVAL = resolve(EXAMPLE_ROOT, "eval/bi-dashboard.yaml")
15
+
16
+ function seedWorkspace(): string {
17
+ const root = mkdtempSync(join(tmpdir(), "bi-dashboard-playground-"))
18
+ cpSync(EXAMPLE_ROOT, root, { recursive: true })
19
+ return root
20
+ }
21
+
22
+ async function main(): Promise<number> {
23
+ const fixturesPath = resolve(process.argv[2] ?? DEFAULT_EVAL)
24
+ const workspaceRoot = seedWorkspace()
25
+ console.log(`[bi-dashboard playground] running suite: ${fixturesPath}`)
26
+ console.log(`[bi-dashboard playground] seeded workspace: ${workspaceRoot}`)
27
+
28
+ const app = await createWorkspaceAgentServer({
29
+ workspaceRoot,
30
+ appRoot: PLUGIN_ROOT,
31
+ mode: "local",
32
+ logger: false,
33
+ defaultPluginPackages: ["@hachej/boring-data-bridge", "@hachej/boring-bi-dashboard"],
34
+ workspaceBridge: { allowInsecureLocalCliBrowserAuth: true },
35
+ })
36
+
37
+ try {
38
+ const report = await runEvalSuite({ app, fixturesPath, concurrency: 1 })
39
+ console.log(
40
+ `[bi-dashboard playground] ${report.passed}/${report.total} passed (${(report.passRate * 100).toFixed(1)}%) in ${(report.totalDurationMs / 1000).toFixed(1)}s`,
41
+ )
42
+
43
+ const validationErrors = validateWrittenDashboards(report)
44
+ if (validationErrors.length > 0) {
45
+ console.error("\n[bi-dashboard playground] generated dashboard validation failed:")
46
+ for (const error of validationErrors) console.error(` - ${error}`)
47
+ return 1
48
+ }
49
+
50
+ if (!report.allPassed) {
51
+ console.error(`\n[bi-dashboard playground] ${report.failed} prompt(s) failed:`)
52
+ for (const r of report.results) {
53
+ if (r.ok) continue
54
+ console.error(`\n prompt: ${r.prompt}`)
55
+ console.error(` reason: ${r.reason ?? "(no reason)"}`)
56
+ if (r.actual.length > 0) {
57
+ console.error(` actual calls: ${JSON.stringify(r.actual, null, 2).replace(/\n/g, "\n ")}`)
58
+ }
59
+ if (r.text) {
60
+ console.error(` text: ${r.text.slice(0, 200)}${r.text.length > 200 ? "…" : ""}`)
61
+ }
62
+ }
63
+ return 1
64
+ }
65
+
66
+ return 0
67
+ } finally {
68
+ await app.close()
69
+ }
70
+ }
71
+
72
+ function validateWrittenDashboards(report: SuiteReport): string[] {
73
+ const errors: string[] = []
74
+ for (const result of report.results) {
75
+ for (const call of result.actual) {
76
+ if (call.tool !== "write") continue
77
+ const path = typeof call.params.path === "string" ? call.params.path : ""
78
+ if (!/\.dashboard\.json$/i.test(path)) continue
79
+ if (typeof call.params.content !== "string") {
80
+ errors.push(`${path || "dashboard write"}: content is not a string`)
81
+ continue
82
+ }
83
+ try {
84
+ const parsed = parseDashboardSpec(JSON.parse(call.params.content))
85
+ if (!parsed.spec) errors.push(`${path}: ${parsed.errors.join("; ")}`)
86
+ } catch (error) {
87
+ errors.push(`${path}: invalid JSON (${error instanceof Error ? error.message : String(error)})`)
88
+ }
89
+ }
90
+ }
91
+ return errors
92
+ }
93
+
94
+ main().then(
95
+ (code) => process.exit(code),
96
+ (err) => {
97
+ console.error("[bi-dashboard playground] fatal:", err)
98
+ process.exit(2)
99
+ },
100
+ )
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env -S tsx
2
+ import { readFileSync } from "node:fs"
3
+ import { dirname, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { DuckDBConnection, quotedIdentifier, quotedString } from "@duckdb/node-api"
6
+ import { createWorkspaceBridgeRegistry } from "@hachej/boring-workspace/server"
7
+ import { createDataBridgeServerPlugin, type DataBridgeSqlAdapter } from "@hachej/boring-data-bridge/server"
8
+ import type { DataBridgeTableResult } from "@hachej/boring-data-bridge/shared"
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+ const PLUGIN_ROOT = resolve(__dirname, "..")
13
+ const EXAMPLE_ROOT = resolve(PLUGIN_ROOT, "example")
14
+ const DASHBOARD_PATH = resolve(EXAMPLE_ROOT, "dashboards/people.dashboard.json")
15
+
16
+ async function createPeopleDuckDbAdapter(): Promise<{ adapter: DataBridgeSqlAdapter; close: () => void }> {
17
+ const connection = await DuckDBConnection.create()
18
+ await connection.run(
19
+ `create or replace view ${quotedIdentifier("people")} as select * from read_csv_auto(${quotedString(resolve(EXAMPLE_ROOT, "data/people.csv"))}, HEADER = true)`,
20
+ )
21
+ return {
22
+ adapter: {
23
+ maxRows: 100,
24
+ async execute({ sql, limit }) {
25
+ const reader = await connection.runAndReadUntil(sql, limit + 1)
26
+ const rows = reader.getRowObjectsJson().slice(0, limit) as Record<string, unknown>[]
27
+ return {
28
+ kind: "data-bridge.table",
29
+ version: 1,
30
+ columns: reader.columnNames().map((name) => ({ name, type: "string" })),
31
+ rows,
32
+ rowCount: rows.length,
33
+ truncated: !reader.done || reader.currentRowCount > limit,
34
+ source: "people-duckdb",
35
+ }
36
+ },
37
+ },
38
+ close: () => connection.closeSync(),
39
+ }
40
+ }
41
+
42
+ async function main(): Promise<number> {
43
+ const dashboard = JSON.parse(readFileSync(DASHBOARD_PATH, "utf8")) as {
44
+ profile?: unknown
45
+ queries?: Record<string, { id: string; source?: string; sql?: string; params?: Record<string, unknown>; model?: string; query?: string; limit?: number }>
46
+ }
47
+ if (dashboard.profile !== "bi-dashboard" || !dashboard.queries) {
48
+ console.error("[bi-dashboard smoke] invalid dashboard fixture")
49
+ return 1
50
+ }
51
+
52
+ const { adapter, close } = await createPeopleDuckDbAdapter()
53
+ try {
54
+ const plugin = createDataBridgeServerPlugin({
55
+ workspaceRoot: EXAMPLE_ROOT,
56
+ sqlAdapters: { "people-duckdb": adapter },
57
+ })
58
+ const registry = createWorkspaceBridgeRegistry()
59
+ for (const contribution of plugin.workspaceBridgeHandlers ?? []) {
60
+ registry.registerHandler(contribution.definition, contribution.handler)
61
+ }
62
+
63
+ for (const [queryId, query] of Object.entries(dashboard.queries)) {
64
+ const bridgeQuery = query.sql
65
+ ? { language: "sql" as const, source: query.source ?? "default", sql: query.sql, params: query.params, limit: query.limit }
66
+ : { language: "bsl" as const, model: query.model ?? "", query: query.query ?? "", limit: query.limit }
67
+
68
+ const res = await registry.call({
69
+ op: "data.v1.query.run",
70
+ input: { query: bridgeQuery },
71
+ }, {
72
+ callerClass: "runtime",
73
+ workspaceId: "bi-dashboard-smoke",
74
+ capabilities: ["data:read", "data:sql-query"],
75
+ actor: { actorKind: "agent", performedBy: { label: "bi-dashboard smoke" } },
76
+ })
77
+
78
+ if (!res.ok) {
79
+ console.error(`[bi-dashboard smoke] ${queryId} failed: ${res.error.message}`)
80
+ return 1
81
+ }
82
+ const output = res.output as DataBridgeTableResult
83
+ if (!Array.isArray(output.rows) || output.rows.length === 0) {
84
+ console.error(`[bi-dashboard smoke] ${queryId} returned no rows`)
85
+ return 1
86
+ }
87
+ console.log(`[bi-dashboard smoke] ${queryId}: ${output.rows.length} row(s)`)
88
+ }
89
+ return 0
90
+ } finally {
91
+ close()
92
+ }
93
+ }
94
+
95
+ main().then(
96
+ (code) => process.exit(code),
97
+ (err) => {
98
+ console.error("[bi-dashboard smoke] fatal:", err)
99
+ process.exit(2)
100
+ },
101
+ )
@@ -0,0 +1,78 @@
1
+ ---
2
+ description: Create or edit BSL BI dashboard JSON specs for the BI Dashboard plugin.
3
+ ---
4
+
5
+ # bi-dashboard-authoring
6
+
7
+ Use this skill when the user asks to create, edit, or open a BI dashboard for
8
+ `@hachej/boring-bi-dashboard`.
9
+
10
+ ## Files
11
+
12
+ - write dashboard specs as JSON files under `dashboards/`
13
+ - use the suffix `.dashboard.json`
14
+ - when the user asks to open or view the dashboard and `exec_ui` is available,
15
+ open the file with:
16
+
17
+ ```json
18
+ {
19
+ "kind": "openSurface",
20
+ "params": {
21
+ "kind": "workspace.open.path",
22
+ "target": "dashboards/example.dashboard.json"
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## Contract
28
+
29
+ Write provider-neutral BSL dashboard specs. Do not write React. Do not write raw
30
+ ECharts or raw Perspective configs.
31
+
32
+ Use this top-level shape:
33
+
34
+ ```json
35
+ {
36
+ "kind": "boring.generated-pane",
37
+ "profile": "bi-dashboard",
38
+ "version": 1,
39
+ "title": "Dashboard title",
40
+ "queries": {
41
+ "query_id": {
42
+ "id": "query_id",
43
+ "model": "orders",
44
+ "groupBy": [],
45
+ "measures": []
46
+ }
47
+ },
48
+ "root": "dashboard",
49
+ "elements": {
50
+ "dashboard": {
51
+ "type": "DashboardGrid",
52
+ "props": { "columns": 2 },
53
+ "children": []
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Components
60
+
61
+ Use only these component types and prop names:
62
+
63
+ - `DashboardGrid` — layout container with string `children`; optional `props.columns` must be one of `1`, `2`, `3`, `4`, `6`, or `12`
64
+ - `BSLMetric` — KPI card; requires `props.queryId`, `props.label`, and `props.valueField`; optional `props.format` is `number`, `currency`, or `percent`
65
+ - `BSLChart` — chart placeholder; requires `props.queryId` and `props.chartType`; use `props.renderer: "echarts"` for normal charts; allowed chart types are `bar`, `line`, `area`, `scatter`, `heatmap`, `pie`, `treemap`, `sunburst`, `gauge`, and `table`; optional axis props are `props.x`, `props.y`, and `props.color` (not `xField`, `yField`, or `yFields`)
66
+ - `BSLPerspectiveViewer` — exploratory table/pivot; use `props.plugin: "Datagrid"` for detail tables; optional `props.columns`, `props.groupBy`, and `props.splitBy` are string arrays; optional `props.sort` is an array of `[field, "asc" | "desc"]` tuples
67
+ - `BSLFilter` — filter control targeting one or more query IDs; requires `props.id`, `props.field`, `props.controlType`, and `props.targetQueries`; `controlType` is `select`, `multiSelect`, `dateRange`, `numberRange`, or `search`
68
+ - `BSLText` — markdown notes or section text; requires `props.markdown`
69
+
70
+ ## Authoring rules
71
+
72
+ - every component ID referenced in `children` must exist in `elements`
73
+ - every `queryId` and filter `targetQueries` entry must exist in `queries`
74
+ - use the exact validated prop names above; avoid invented aliases such as `xField`, `yField`, or `yFields`
75
+ - keep the spec concise and readable
76
+ - choose sensible query IDs and component IDs from the dashboard domain
77
+ - prefer semantic BSL fields such as `revenue`, `order_count`, `month`, `region`,
78
+ `customer_id`, and `cohort_month` over UI-specific names