@seed-ship/mcp-ui-solid 6.8.2 → 6.9.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/CHANGELOG.md +34 -0
- package/dist/components/ChartJSRenderer.cjs +27 -13
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +28 -14
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DegradedFallback.cjs +73 -0
- package/dist/components/DegradedFallback.cjs.map +1 -0
- package/dist/components/DegradedFallback.d.ts +37 -0
- package/dist/components/DegradedFallback.d.ts.map +1 -0
- package/dist/components/DegradedFallback.js +73 -0
- package/dist/components/DegradedFallback.js.map +1 -0
- package/dist/components/GraphRenderer.cjs +30 -15
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +31 -16
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/MapRenderer.cjs +128 -107
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +129 -108
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.js +1 -1
- package/dist/utils/degraded-projections.cjs +87 -0
- package/dist/utils/degraded-projections.cjs.map +1 -0
- package/dist/utils/degraded-projections.d.ts +64 -0
- package/dist/utils/degraded-projections.d.ts.map +1 -0
- package/dist/utils/degraded-projections.js +87 -0
- package/dist/utils/degraded-projections.js.map +1 -0
- package/package.json +1 -1
- package/src/components/ChartJSRenderer.tsx +94 -85
- package/src/components/DegradedFallback.test.tsx +61 -0
- package/src/components/DegradedFallback.tsx +93 -0
- package/src/components/GraphRenderer.tsx +26 -4
- package/src/components/MapRenderer.tsx +446 -392
- package/src/utils/degraded-projections.test.ts +113 -0
- package/src/utils/degraded-projections.ts +149 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Degraded-fallback projections (audit 2026-05-30, P2.5).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that turn a heavy renderer's params into a flat
|
|
5
|
+
* `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of
|
|
6
|
+
* the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)
|
|
7
|
+
* is unavailable or throws. No peer deps, no side effects, fully testable.
|
|
8
|
+
*
|
|
9
|
+
* These are best-effort views: they surface the underlying data so the user
|
|
10
|
+
* isn't left with a blank space, not faithful reproductions of the chart/
|
|
11
|
+
* map/graph.
|
|
12
|
+
*/
|
|
13
|
+
export interface DegradedTable {
|
|
14
|
+
columns: string[];
|
|
15
|
+
rows: Array<Array<string | number>>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Graph → edge table when edges exist (Source / Target / Label), else a
|
|
19
|
+
* node list (Node / Label). The graph renderer can therefore degrade to a
|
|
20
|
+
* readable relationship listing instead of a blank canvas.
|
|
21
|
+
*/
|
|
22
|
+
export declare function graphToDegradedTable(params: {
|
|
23
|
+
nodes?: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
}>;
|
|
27
|
+
edges?: Array<{
|
|
28
|
+
source: string;
|
|
29
|
+
target: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
weight?: number;
|
|
32
|
+
}>;
|
|
33
|
+
}): DegradedTable;
|
|
34
|
+
/**
|
|
35
|
+
* Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON
|
|
36
|
+
* features become Type / Lat / Lng (+ a compact properties summary). So a
|
|
37
|
+
* map that can't paint still lists where its points are.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mapToDegradedTable(params: {
|
|
40
|
+
markers?: Array<{
|
|
41
|
+
position: [number, number] | {
|
|
42
|
+
lat: number;
|
|
43
|
+
lng: number;
|
|
44
|
+
};
|
|
45
|
+
tooltip?: string;
|
|
46
|
+
popup?: string;
|
|
47
|
+
}>;
|
|
48
|
+
geojson?: unknown;
|
|
49
|
+
}): DegradedTable;
|
|
50
|
+
/**
|
|
51
|
+
* Chart → a series table: one row per label, one column per dataset. So a
|
|
52
|
+
* chart that can't draw still shows its numbers. Point/object data (scatter,
|
|
53
|
+
* bubble, time series) is stringified per cell.
|
|
54
|
+
*/
|
|
55
|
+
export declare function chartToDegradedTable(params: {
|
|
56
|
+
data?: {
|
|
57
|
+
labels?: Array<string | number>;
|
|
58
|
+
datasets?: Array<{
|
|
59
|
+
label?: string;
|
|
60
|
+
data?: unknown[];
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
}): DegradedTable;
|
|
64
|
+
//# sourceMappingURL=degraded-projections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"degraded-projections.d.ts","sourceRoot":"","sources":["../../src/utils/degraded-projections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;CACrC;AAmBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpF,GAAG,aAAa,CAkBhB;AAoBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,OAAO,CAAC,EAAE,KAAK,CAAC;QACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,aAAa,CA8BhB;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;SAAE,CAAC,CAAC;KACxD,CAAC;CACH,GAAG,aAAa,CAWhB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const MAX_PROJECTED_ROWS = 200;
|
|
2
|
+
function cell(value) {
|
|
3
|
+
if (value == null) return "";
|
|
4
|
+
if (typeof value === "object") {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
} catch {
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return String(value);
|
|
12
|
+
}
|
|
13
|
+
function graphToDegradedTable(params) {
|
|
14
|
+
const edges = params.edges ?? [];
|
|
15
|
+
if (edges.length > 0) {
|
|
16
|
+
return {
|
|
17
|
+
columns: ["Source", "Target", "Label"],
|
|
18
|
+
rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {
|
|
19
|
+
const label = [e.weight != null ? String(e.weight) : "", e.label ?? ""].filter(Boolean).join(" · ");
|
|
20
|
+
return [cell(e.source), cell(e.target), label];
|
|
21
|
+
})
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const nodes = params.nodes ?? [];
|
|
25
|
+
return {
|
|
26
|
+
columns: ["Node", "Label"],
|
|
27
|
+
rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)])
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function firstLngLat(coords) {
|
|
31
|
+
let c = coords;
|
|
32
|
+
while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];
|
|
33
|
+
if (Array.isArray(c) && typeof c[0] === "number" && typeof c[1] === "number") {
|
|
34
|
+
return [c[0], c[1]];
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function mapToDegradedTable(params) {
|
|
39
|
+
var _a, _b, _c, _d;
|
|
40
|
+
const rows = [];
|
|
41
|
+
for (const m of params.markers ?? []) {
|
|
42
|
+
const lat = Array.isArray(m.position) ? m.position[0] : (_a = m.position) == null ? void 0 : _a.lat;
|
|
43
|
+
const lng = Array.isArray(m.position) ? m.position[1] : (_b = m.position) == null ? void 0 : _b.lng;
|
|
44
|
+
rows.push(["marker", cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? "")]);
|
|
45
|
+
}
|
|
46
|
+
const fc = params.geojson;
|
|
47
|
+
const features = Array.isArray(fc == null ? void 0 : fc.features) ? fc.features : [];
|
|
48
|
+
for (const f of features) {
|
|
49
|
+
const ll = firstLngLat((_c = f.geometry) == null ? void 0 : _c.coordinates);
|
|
50
|
+
const props = f.properties ?? {};
|
|
51
|
+
const propSummary = Object.keys(props).slice(0, 3).map((k) => `${k}=${cell(props[k])}`).join(", ");
|
|
52
|
+
rows.push([
|
|
53
|
+
cell(((_d = f.geometry) == null ? void 0 : _d.type) ?? "feature"),
|
|
54
|
+
ll ? cell(ll[1]) : "",
|
|
55
|
+
ll ? cell(ll[0]) : "",
|
|
56
|
+
propSummary
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
columns: ["Type", "Lat", "Lng", "Info"],
|
|
61
|
+
rows: rows.slice(0, MAX_PROJECTED_ROWS)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function chartToDegradedTable(params) {
|
|
65
|
+
var _a, _b;
|
|
66
|
+
const datasets = ((_a = params.data) == null ? void 0 : _a.datasets) ?? [];
|
|
67
|
+
const labels = ((_b = params.data) == null ? void 0 : _b.labels) ?? [];
|
|
68
|
+
const rowCount = Math.max(labels.length, ...datasets.map((d) => {
|
|
69
|
+
var _a2;
|
|
70
|
+
return ((_a2 = d.data) == null ? void 0 : _a2.length) ?? 0;
|
|
71
|
+
}), 0);
|
|
72
|
+
const columns = ["", ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];
|
|
73
|
+
const rows = [];
|
|
74
|
+
for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
|
|
75
|
+
rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => {
|
|
76
|
+
var _a2;
|
|
77
|
+
return cell((_a2 = d.data) == null ? void 0 : _a2[r]);
|
|
78
|
+
})]);
|
|
79
|
+
}
|
|
80
|
+
return { columns, rows };
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
chartToDegradedTable,
|
|
84
|
+
graphToDegradedTable,
|
|
85
|
+
mapToDegradedTable
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=degraded-projections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"degraded-projections.js","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}): DegradedTable {\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: ['Source', 'Target', 'Label'],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: ['Node', 'Label'],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}): DegradedTable {\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push(['marker', cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? 'feature'),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: ['Type', 'Lat', 'Lng', 'Info'],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n */\nexport function chartToDegradedTable(params: {\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}): DegradedTable {\n const datasets = params.data?.datasets ?? [];\n const labels = params.data?.labels ?? [];\n const rowCount = Math.max(labels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = ['', ...datasets.map((d, i) => d.label ?? `Series ${i + 1}`)];\n const rows: Array<Array<string | number>> = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(labels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n }\n return { columns, rows };\n}\n"],"names":["_a"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -8,41 +8,44 @@
|
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js'
|
|
12
|
-
import type { UIComponent, ChartComponentParams } from '../types'
|
|
13
|
-
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
11
|
+
import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js';
|
|
12
|
+
import type { UIComponent, ChartComponentParams } from '../types';
|
|
13
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper';
|
|
14
|
+
import { DegradedFallback } from './DegradedFallback';
|
|
15
|
+
import { chartToDegradedTable } from '../utils/degraded-projections';
|
|
16
|
+
import { useTelemetry } from '../context/MCPUITelemetryContext';
|
|
14
17
|
|
|
15
18
|
// Lazy load Chart.js to avoid bundling if not used
|
|
16
|
-
let ChartJS: any = null
|
|
17
|
-
let chartJSLoadPromise: Promise<any> | null = null
|
|
19
|
+
let ChartJS: any = null;
|
|
20
|
+
let chartJSLoadPromise: Promise<any> | null = null;
|
|
18
21
|
|
|
19
22
|
const loadChartJS = async () => {
|
|
20
|
-
if (ChartJS) return ChartJS
|
|
23
|
+
if (ChartJS) return ChartJS;
|
|
21
24
|
|
|
22
25
|
if (!chartJSLoadPromise) {
|
|
23
26
|
chartJSLoadPromise = import('chart.js/auto')
|
|
24
27
|
.then((module) => {
|
|
25
|
-
ChartJS = module.default || module.Chart
|
|
26
|
-
return ChartJS
|
|
28
|
+
ChartJS = module.default || module.Chart;
|
|
29
|
+
return ChartJS;
|
|
27
30
|
})
|
|
28
31
|
.catch((err) => {
|
|
29
|
-
chartJSLoadPromise = null
|
|
30
|
-
throw err
|
|
31
|
-
})
|
|
32
|
+
chartJSLoadPromise = null;
|
|
33
|
+
throw err;
|
|
34
|
+
});
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
return chartJSLoadPromise
|
|
35
|
-
}
|
|
37
|
+
return chartJSLoadPromise;
|
|
38
|
+
};
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Check if Chart.js is available
|
|
39
42
|
*/
|
|
40
43
|
export async function isChartJSAvailable(): Promise<boolean> {
|
|
41
44
|
try {
|
|
42
|
-
await loadChartJS()
|
|
43
|
-
return true
|
|
45
|
+
await loadChartJS();
|
|
46
|
+
return true;
|
|
44
47
|
} catch {
|
|
45
|
-
return false
|
|
48
|
+
return false;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -50,18 +53,18 @@ export interface ChartJSRendererProps {
|
|
|
50
53
|
/**
|
|
51
54
|
* UIComponent with chart params
|
|
52
55
|
*/
|
|
53
|
-
component: UIComponent
|
|
56
|
+
component: UIComponent;
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
59
|
* Error callback
|
|
57
60
|
*/
|
|
58
|
-
onError?: (error: Error) => void
|
|
61
|
+
onError?: (error: Error) => void;
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).
|
|
62
65
|
* @see ExpandableWrapperProps.toolbarVariant
|
|
63
66
|
*/
|
|
64
|
-
toolbarVariant?: 'hover' | 'always-visible'
|
|
67
|
+
toolbarVariant?: 'hover' | 'always-visible';
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/**
|
|
@@ -87,50 +90,51 @@ export interface ChartJSRendererProps {
|
|
|
87
90
|
* ```
|
|
88
91
|
*/
|
|
89
92
|
export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
90
|
-
const [isLoading, setIsLoading] = createSignal(true)
|
|
91
|
-
const [error, setError] = createSignal<string>()
|
|
92
|
-
let canvasRef: HTMLCanvasElement | undefined
|
|
93
|
-
let chartInstance: any
|
|
93
|
+
const [isLoading, setIsLoading] = createSignal(true);
|
|
94
|
+
const [error, setError] = createSignal<string>();
|
|
95
|
+
let canvasRef: HTMLCanvasElement | undefined;
|
|
96
|
+
let chartInstance: any;
|
|
94
97
|
|
|
95
|
-
const params = () => props.component.params as ChartComponentParams
|
|
96
|
-
const isExpanded = useExpanded()
|
|
98
|
+
const params = () => props.component.params as ChartComponentParams;
|
|
99
|
+
const isExpanded = useExpanded();
|
|
100
|
+
const telemetry = useTelemetry();
|
|
97
101
|
|
|
98
102
|
// v6.1.0 — export visibility :
|
|
99
103
|
// - undefined / true → button shown (new default, was opt-in)
|
|
100
104
|
// - false → button hidden (explicit opt-out, unchanged)
|
|
101
|
-
const exportEnabled = () => params().exportable !== false
|
|
105
|
+
const exportEnabled = () => params().exportable !== false;
|
|
102
106
|
|
|
103
107
|
// v6.1.0 — copy data for the ExpandableWrapper modal-header copy button.
|
|
104
108
|
// Lazy-stringified each time the button is clicked.
|
|
105
|
-
const copyDataJSON = () => JSON.stringify({ type: params().type, data: params().data }, null, 2)
|
|
109
|
+
const copyDataJSON = () => JSON.stringify({ type: params().type, data: params().data }, null, 2);
|
|
106
110
|
|
|
107
111
|
// Chart PNG export
|
|
108
112
|
const handleExportPNG = () => {
|
|
109
|
-
if (!canvasRef) return
|
|
110
|
-
const url = canvasRef.toDataURL('image/png')
|
|
111
|
-
const a = document.createElement('a')
|
|
112
|
-
a.href = url
|
|
113
|
-
a.download = `${(params().title || 'chart').replace(/\s+/g, '-').toLowerCase()}.png
|
|
114
|
-
a.click()
|
|
115
|
-
}
|
|
113
|
+
if (!canvasRef) return;
|
|
114
|
+
const url = canvasRef.toDataURL('image/png');
|
|
115
|
+
const a = document.createElement('a');
|
|
116
|
+
a.href = url;
|
|
117
|
+
a.download = `${(params().title || 'chart').replace(/\s+/g, '-').toLowerCase()}.png`;
|
|
118
|
+
a.click();
|
|
119
|
+
};
|
|
116
120
|
|
|
117
121
|
// Create/update chart when params change
|
|
118
122
|
createEffect(async () => {
|
|
119
|
-
if (!canvasRef) return
|
|
123
|
+
if (!canvasRef) return;
|
|
120
124
|
|
|
121
125
|
// Access params to track dependencies
|
|
122
|
-
const chartParams = params()
|
|
126
|
+
const chartParams = params();
|
|
123
127
|
|
|
124
|
-
setIsLoading(true)
|
|
125
|
-
setError(undefined)
|
|
128
|
+
setIsLoading(true);
|
|
129
|
+
setError(undefined);
|
|
126
130
|
|
|
127
131
|
try {
|
|
128
|
-
const Chart = await loadChartJS()
|
|
132
|
+
const Chart = await loadChartJS();
|
|
129
133
|
|
|
130
134
|
// Destroy previous instance
|
|
131
135
|
if (chartInstance) {
|
|
132
|
-
chartInstance.destroy()
|
|
133
|
-
chartInstance = null
|
|
136
|
+
chartInstance.destroy();
|
|
137
|
+
chartInstance = null;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
// Build options, merging time-axis config if present (v3.1.0)
|
|
@@ -146,11 +150,11 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
146
150
|
...chartParams.options?.plugins?.legend,
|
|
147
151
|
},
|
|
148
152
|
},
|
|
149
|
-
}
|
|
153
|
+
};
|
|
150
154
|
|
|
151
155
|
// Time-series axis (v3.1.0)
|
|
152
156
|
if (chartParams.timeAxis) {
|
|
153
|
-
const ta = chartParams.timeAxis
|
|
157
|
+
const ta = chartParams.timeAxis;
|
|
154
158
|
baseOptions.scales = {
|
|
155
159
|
...baseOptions.scales,
|
|
156
160
|
x: {
|
|
@@ -164,7 +168,7 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
164
168
|
...(ta.min ? { min: ta.min } : {}),
|
|
165
169
|
...(ta.max ? { max: ta.max } : {}),
|
|
166
170
|
},
|
|
167
|
-
}
|
|
171
|
+
};
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
// Create new chart
|
|
@@ -172,24 +176,33 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
172
176
|
type: chartParams.type,
|
|
173
177
|
data: chartParams.data,
|
|
174
178
|
options: baseOptions,
|
|
175
|
-
})
|
|
179
|
+
});
|
|
176
180
|
|
|
177
|
-
setIsLoading(false)
|
|
181
|
+
setIsLoading(false);
|
|
178
182
|
} catch (err) {
|
|
179
|
-
const error = err instanceof Error ? err : new Error('Chart rendering failed')
|
|
180
|
-
setError(error.message)
|
|
181
|
-
setIsLoading(false)
|
|
182
|
-
|
|
183
|
+
const error = err instanceof Error ? err : new Error('Chart rendering failed');
|
|
184
|
+
setError(error.message);
|
|
185
|
+
setIsLoading(false);
|
|
186
|
+
// Fallback ladder (P2.5): record the failure so it's observable, then
|
|
187
|
+
// degrade to the series table below instead of a blank canvas.
|
|
188
|
+
telemetry?.dispatch({
|
|
189
|
+
type: 'render:error',
|
|
190
|
+
errorMessage: error.message,
|
|
191
|
+
id: props.component?.id ?? '',
|
|
192
|
+
componentType: 'chart',
|
|
193
|
+
ts: Date.now(),
|
|
194
|
+
});
|
|
195
|
+
props.onError?.(error);
|
|
183
196
|
}
|
|
184
|
-
})
|
|
197
|
+
});
|
|
185
198
|
|
|
186
199
|
// Cleanup on unmount
|
|
187
200
|
onCleanup(() => {
|
|
188
201
|
if (chartInstance) {
|
|
189
|
-
chartInstance.destroy()
|
|
190
|
-
chartInstance = null
|
|
202
|
+
chartInstance.destroy();
|
|
203
|
+
chartInstance = null;
|
|
191
204
|
}
|
|
192
|
-
})
|
|
205
|
+
});
|
|
193
206
|
|
|
194
207
|
return (
|
|
195
208
|
<ExpandableWrapper
|
|
@@ -198,15 +211,15 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
198
211
|
copyLabel="Copy chart data (JSON)"
|
|
199
212
|
toolbarVariant={props.toolbarVariant}
|
|
200
213
|
>
|
|
201
|
-
<div
|
|
202
|
-
|
|
203
|
-
|
|
214
|
+
<div
|
|
215
|
+
class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden p-4 group ${
|
|
216
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
217
|
+
}`}
|
|
218
|
+
>
|
|
204
219
|
<Show when={params().title || exportEnabled()}>
|
|
205
220
|
<div class="flex items-center justify-between mb-3 flex-shrink-0">
|
|
206
221
|
<Show when={params().title}>
|
|
207
|
-
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
208
|
-
{params().title}
|
|
209
|
-
</h3>
|
|
222
|
+
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{params().title}</h3>
|
|
210
223
|
</Show>
|
|
211
224
|
<Show when={exportEnabled()}>
|
|
212
225
|
<button
|
|
@@ -215,8 +228,18 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
215
228
|
title="Download PNG"
|
|
216
229
|
aria-label="Download chart as PNG"
|
|
217
230
|
>
|
|
218
|
-
<svg
|
|
219
|
-
|
|
231
|
+
<svg
|
|
232
|
+
class="w-3 h-3 text-gray-500 dark:text-gray-400"
|
|
233
|
+
fill="none"
|
|
234
|
+
viewBox="0 0 24 24"
|
|
235
|
+
stroke="currentColor"
|
|
236
|
+
>
|
|
237
|
+
<path
|
|
238
|
+
stroke-linecap="round"
|
|
239
|
+
stroke-linejoin="round"
|
|
240
|
+
stroke-width="2"
|
|
241
|
+
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
242
|
+
/>
|
|
220
243
|
</svg>
|
|
221
244
|
</button>
|
|
222
245
|
</Show>
|
|
@@ -232,28 +255,14 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
232
255
|
</div>
|
|
233
256
|
</Show>
|
|
234
257
|
|
|
258
|
+
{/* Fallback ladder (P2.5): degrade to a series table on render error
|
|
259
|
+
instead of a bare "Chart Error" message. */}
|
|
235
260
|
<Show when={error()}>
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
fill="none"
|
|
242
|
-
viewBox="0 0 24 24"
|
|
243
|
-
stroke="currentColor"
|
|
244
|
-
>
|
|
245
|
-
<path
|
|
246
|
-
stroke-linecap="round"
|
|
247
|
-
stroke-linejoin="round"
|
|
248
|
-
stroke-width="2"
|
|
249
|
-
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
250
|
-
/>
|
|
251
|
-
</svg>
|
|
252
|
-
</div>
|
|
253
|
-
<p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
|
|
254
|
-
<p class="text-gray-600 dark:text-gray-400 text-xs mt-1 max-w-xs">{error()}</p>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
261
|
+
<DegradedFallback
|
|
262
|
+
message={`Chart rendering failed: ${error()}`}
|
|
263
|
+
caption="Showing the chart data as a table — the interactive chart is unavailable."
|
|
264
|
+
{...chartToDegradedTable(params() ?? {})}
|
|
265
|
+
/>
|
|
257
266
|
</Show>
|
|
258
267
|
|
|
259
268
|
<div
|
|
@@ -270,5 +279,5 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
270
279
|
</div>
|
|
271
280
|
</div>
|
|
272
281
|
</ExpandableWrapper>
|
|
273
|
-
)
|
|
274
|
-
}
|
|
282
|
+
);
|
|
283
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for <DegradedFallback> — the middle rung of the renderer fallback
|
|
3
|
+
* ladder (P2.5). Pure presentational component, no peers, no async.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { render, cleanup } from '@solidjs/testing-library';
|
|
8
|
+
import { DegradedFallback } from './DegradedFallback';
|
|
9
|
+
|
|
10
|
+
describe('<DegradedFallback>', () => {
|
|
11
|
+
it('shows the message and a default caption', () => {
|
|
12
|
+
const { getByText, container } = render(() => (
|
|
13
|
+
<DegradedFallback message="Graph rendering failed" />
|
|
14
|
+
));
|
|
15
|
+
expect(getByText('Graph rendering failed')).toBeTruthy();
|
|
16
|
+
expect(container.textContent).toContain('interactive view is unavailable');
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders a table when columns + rows are provided', () => {
|
|
21
|
+
const { container } = render(() => (
|
|
22
|
+
<DegradedFallback
|
|
23
|
+
message="failed"
|
|
24
|
+
columns={['Source', 'Target', 'Label']}
|
|
25
|
+
rows={[
|
|
26
|
+
['a', 'b', 'rel'],
|
|
27
|
+
['b', 'c', ''],
|
|
28
|
+
]}
|
|
29
|
+
/>
|
|
30
|
+
));
|
|
31
|
+
const headers = container.querySelectorAll('th');
|
|
32
|
+
expect(headers).toHaveLength(3);
|
|
33
|
+
expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
|
|
34
|
+
expect(container.textContent).toContain('rel');
|
|
35
|
+
cleanup();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('shows no table when columns are empty', () => {
|
|
39
|
+
const { container } = render(() => <DegradedFallback message="failed" rows={[['x']]} />);
|
|
40
|
+
expect(container.querySelector('table')).toBeNull();
|
|
41
|
+
cleanup();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('truncates rows past maxRows and notes the remainder', () => {
|
|
45
|
+
const rows = Array.from({ length: 5 }, (_, i) => [String(i)]);
|
|
46
|
+
const { container } = render(() => (
|
|
47
|
+
<DegradedFallback message="failed" columns={['n']} rows={rows} maxRows={2} />
|
|
48
|
+
));
|
|
49
|
+
expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
|
|
50
|
+
expect(container.textContent).toContain('+3 more rows');
|
|
51
|
+
cleanup();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses a custom caption when provided', () => {
|
|
55
|
+
const { container } = render(() => (
|
|
56
|
+
<DegradedFallback message="failed" caption="custom caption here" />
|
|
57
|
+
));
|
|
58
|
+
expect(container.textContent).toContain('custom caption here');
|
|
59
|
+
cleanup();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DegradedFallback — the middle rung of the renderer fallback ladder
|
|
3
|
+
* (audit 2026-05-30, P2.5).
|
|
4
|
+
*
|
|
5
|
+
* Each heavy renderer (graph / map / chart) follows the same contract:
|
|
6
|
+
* 1. native render when its peer lib is available and succeeds;
|
|
7
|
+
* 2. **degraded but useful** view when the native render throws — this
|
|
8
|
+
* component: a visible notice + a plain data table so the user still
|
|
9
|
+
* sees the underlying data instead of a blank space;
|
|
10
|
+
* 3. (the caller also emits a `component:error` telemetry event).
|
|
11
|
+
*
|
|
12
|
+
* Pure / presentational — no peer deps, no side effects — so a render-path
|
|
13
|
+
* failure in a heavy lib can never cascade into the fallback itself, and it
|
|
14
|
+
* is trivially unit-testable. Rows/cells are rendered as text; callers are
|
|
15
|
+
* responsible for stringifying complex cell values.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Component, For, Show } from 'solid-js';
|
|
19
|
+
|
|
20
|
+
export interface DegradedFallbackProps {
|
|
21
|
+
/** Short, human-readable reason the native render was skipped/failed. */
|
|
22
|
+
message: string;
|
|
23
|
+
/**
|
|
24
|
+
* Column headers for the degraded data table. When omitted (or empty),
|
|
25
|
+
* only the notice banner is shown.
|
|
26
|
+
*/
|
|
27
|
+
columns?: string[];
|
|
28
|
+
/** Row data — each row is an array of cells aligned to `columns`. */
|
|
29
|
+
rows?: Array<Array<string | number>>;
|
|
30
|
+
/**
|
|
31
|
+
* Caption under the table. Defaults to a generic
|
|
32
|
+
* "interactive view unavailable" line.
|
|
33
|
+
*/
|
|
34
|
+
caption?: string;
|
|
35
|
+
/** Max rows to render before truncating (default 50). */
|
|
36
|
+
maxRows?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DegradedFallback: Component<DegradedFallbackProps> = (props) => {
|
|
40
|
+
const maxRows = () => props.maxRows ?? 50;
|
|
41
|
+
const allRows = () => props.rows ?? [];
|
|
42
|
+
const shownRows = () => allRows().slice(0, maxRows());
|
|
43
|
+
const hiddenCount = () => Math.max(0, allRows().length - shownRows().length);
|
|
44
|
+
const hasTable = () => (props.columns?.length ?? 0) > 0 && allRows().length > 0;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
class="w-full rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20"
|
|
49
|
+
role="alert"
|
|
50
|
+
>
|
|
51
|
+
<p class="text-sm font-medium text-amber-900 dark:text-amber-100">{props.message}</p>
|
|
52
|
+
<p class="mt-0.5 text-xs text-amber-700 dark:text-amber-300">
|
|
53
|
+
{props.caption ?? 'Showing the underlying data — the interactive view is unavailable.'}
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<Show when={hasTable()}>
|
|
57
|
+
<div class="mt-2 max-h-64 overflow-auto rounded border border-amber-200 dark:border-amber-800">
|
|
58
|
+
<table class="w-full border-collapse text-left text-xs">
|
|
59
|
+
<thead class="sticky top-0 bg-amber-100 dark:bg-amber-900/40">
|
|
60
|
+
<tr>
|
|
61
|
+
<For each={props.columns}>
|
|
62
|
+
{(col) => (
|
|
63
|
+
<th class="px-2 py-1 font-medium text-amber-900 dark:text-amber-100">{col}</th>
|
|
64
|
+
)}
|
|
65
|
+
</For>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
<For each={shownRows()}>
|
|
70
|
+
{(row) => (
|
|
71
|
+
<tr class="border-t border-amber-100 dark:border-amber-800/60">
|
|
72
|
+
<For each={props.columns}>
|
|
73
|
+
{(_col, i) => (
|
|
74
|
+
<td class="px-2 py-1 text-amber-800 dark:text-amber-200">
|
|
75
|
+
{String(row[i()] ?? '')}
|
|
76
|
+
</td>
|
|
77
|
+
)}
|
|
78
|
+
</For>
|
|
79
|
+
</tr>
|
|
80
|
+
)}
|
|
81
|
+
</For>
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
<Show when={hiddenCount() > 0}>
|
|
86
|
+
<p class="mt-1 text-[10px] text-amber-600 dark:text-amber-400">
|
|
87
|
+
+{hiddenCount()} more rows not shown.
|
|
88
|
+
</p>
|
|
89
|
+
</Show>
|
|
90
|
+
</Show>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|