@malloydata/malloyyo 0.2.17 → 0.2.18
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/dist/frame-entry.tsx +11 -0
- package/dist/frame-inpage-entry.tsx +38 -0
- package/dist/frame-runtime/combine.ts +181 -0
- package/dist/frame-runtime/drill.ts +138 -0
- package/dist/frame-runtime/filters.ts +188 -0
- package/dist/frame-runtime/index.ts +68 -0
- package/dist/frame-runtime/runtime.tsx +943 -0
- package/dist/frame-runtime/ui.tsx +643 -0
- package/dist/frame-runtime/vega-chart.tsx +163 -0
- package/dist/index.js +166 -124
- package/package.json +10 -4
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// <VegaChart> — render a Vega-Lite spec against Malloy query results, entirely
|
|
3
|
+
// inside the sandboxed dashboard frame.
|
|
4
|
+
//
|
|
5
|
+
// The heavy engine (vega + vega-lite's compiler) is bundled ONCE into the
|
|
6
|
+
// frame runtime (dev: by the CLI bundler; hosted: into public/dashboard-vendor.js
|
|
7
|
+
// as window.__DASH_RUNTIME__). A Dashboard.tsx ships only a JSON spec + a Malloy
|
|
8
|
+
// query — the chart code is never loaded per dashboard. See
|
|
9
|
+
// scripts/build-dashboard-vendor.mjs and src/lib/dashboards/bundle.ts.
|
|
10
|
+
//
|
|
11
|
+
// SECURITY — the frame has no cookies and (once the CSP lands) no network. Two
|
|
12
|
+
// properties keep a chart spec from becoming an exfil / eval side-channel:
|
|
13
|
+
// 1. Data comes ONLY from Malloy. sanitizeSpec() strips every `url`/`loader`
|
|
14
|
+
// (remote datasets, transform lookups, remote `image` marks) and forces the
|
|
15
|
+
// dataset to inline `values`. A blocked vega loader rejects any load that
|
|
16
|
+
// slips through, so a hand-written spec can't fetch a third-party origin.
|
|
17
|
+
// 2. Expressions run through vega's AST INTERPRETER (vega-interpreter), never
|
|
18
|
+
// `new Function`, so the chart works under a strict `script-src` CSP.
|
|
19
|
+
import React, { useEffect, useMemo, useRef } from "react";
|
|
20
|
+
import { parse, View, loader } from "vega";
|
|
21
|
+
import { expressionInterpreter } from "vega-interpreter";
|
|
22
|
+
import { compile } from "vega-lite";
|
|
23
|
+
import { useQuery } from "./runtime";
|
|
24
|
+
|
|
25
|
+
// A loader that refuses every fetch — belt-and-suspenders with sanitizeSpec's
|
|
26
|
+
// url stripping. Nothing in a dashboard chart should ever hit the network.
|
|
27
|
+
const blockedLoader = (() => {
|
|
28
|
+
const l = loader();
|
|
29
|
+
const deny = () => Promise.reject(new Error("network access is disabled in dashboard charts"));
|
|
30
|
+
l.load = deny;
|
|
31
|
+
l.http = deny;
|
|
32
|
+
l.file = deny;
|
|
33
|
+
l.sanitize = () => Promise.reject(new Error("remote URLs are not allowed in dashboard charts"));
|
|
34
|
+
return l;
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
// Recursively delete anything that would pull bytes from outside the frame.
|
|
38
|
+
function stripRemote(node) {
|
|
39
|
+
if (Array.isArray(node)) {
|
|
40
|
+
node.forEach(stripRemote);
|
|
41
|
+
} else if (node && typeof node === "object") {
|
|
42
|
+
delete node.url; // remote datasets, transform lookups, remote `image` marks
|
|
43
|
+
delete node.loader;
|
|
44
|
+
for (const k of Object.keys(node)) stripRemote(node[k]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Force the spec's data to our single inlined, named dataset ("table"), strip
|
|
49
|
+
// remote refs, and default to a container-width chart so it fills the panel.
|
|
50
|
+
function sanitizeSpec(spec, rows) {
|
|
51
|
+
const s = JSON.parse(JSON.stringify(spec || {}));
|
|
52
|
+
stripRemote(s);
|
|
53
|
+
s.data = { name: "table", values: rows || [] };
|
|
54
|
+
if (s.width == null) s.width = "container";
|
|
55
|
+
s.autosize = s.autosize ?? { type: "fit", contains: "padding", resize: true };
|
|
56
|
+
return s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderError(container, err) {
|
|
60
|
+
container.innerHTML = "";
|
|
61
|
+
const pre = document.createElement("pre");
|
|
62
|
+
pre.style.cssText = "color:crimson;white-space:pre-wrap;font:12px ui-monospace,monospace;margin:0";
|
|
63
|
+
pre.textContent = "Vega chart error:\n" + ((err && (err.stack || err.message)) || String(err));
|
|
64
|
+
container.appendChild(pre);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Keep ONE vega View alive for a given spec and stream new rows into its "table"
|
|
68
|
+
// dataset in place (view.data + runAsync) — rebuilding the View per data change
|
|
69
|
+
// would blank/flash the chart on every control change, the same problem Panel
|
|
70
|
+
// solves for the Malloy renderer.
|
|
71
|
+
function VegaChartInner({ spec, rows, loading, style }) {
|
|
72
|
+
const ref = useRef(null);
|
|
73
|
+
const viewRef = useRef(null);
|
|
74
|
+
const rowsRef = useRef(rows);
|
|
75
|
+
rowsRef.current = rows;
|
|
76
|
+
const specKey = useMemo(() => JSON.stringify(spec ?? null), [spec]);
|
|
77
|
+
|
|
78
|
+
// (Re)build the View when the spec changes.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!ref.current) return undefined;
|
|
81
|
+
const container = ref.current;
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
let view;
|
|
84
|
+
(async () => {
|
|
85
|
+
try {
|
|
86
|
+
const vgSpec = compile(sanitizeSpec(spec, rowsRef.current)).spec;
|
|
87
|
+
// ast:true + the interpreter = no `new Function`, so charts survive a
|
|
88
|
+
// strict CSP. renderer 'svg' keeps it dependency-light (no canvas).
|
|
89
|
+
view = new View(parse(vgSpec, {}, { ast: true }), {
|
|
90
|
+
expr: expressionInterpreter,
|
|
91
|
+
renderer: "svg",
|
|
92
|
+
container,
|
|
93
|
+
loader: blockedLoader,
|
|
94
|
+
hover: true,
|
|
95
|
+
});
|
|
96
|
+
await view.runAsync();
|
|
97
|
+
if (cancelled) {
|
|
98
|
+
view.finalize();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
viewRef.current = view;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (!cancelled) renderError(container, err);
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
try {
|
|
109
|
+
if (view) view.finalize();
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore */
|
|
112
|
+
}
|
|
113
|
+
viewRef.current = null;
|
|
114
|
+
};
|
|
115
|
+
}, [specKey]);
|
|
116
|
+
|
|
117
|
+
// Stream new rows into the live View — no rebuild, no flash.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const view = viewRef.current;
|
|
120
|
+
if (!view) return;
|
|
121
|
+
try {
|
|
122
|
+
view.data("table", rows || []);
|
|
123
|
+
view.runAsync();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (ref.current) renderError(ref.current, err);
|
|
126
|
+
}
|
|
127
|
+
}, [rows]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
ref={ref}
|
|
132
|
+
style={{
|
|
133
|
+
width: "100%",
|
|
134
|
+
opacity: loading ? 0.4 : 1,
|
|
135
|
+
transition: "opacity .15s",
|
|
136
|
+
...style,
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render a Vega-Lite spec against Malloy data.
|
|
144
|
+
*
|
|
145
|
+
* <VegaChart spec={spec} query="by_year" /> // run a named query
|
|
146
|
+
* <VegaChart spec={spec} malloy="run: flights -> …" givens={givens} />
|
|
147
|
+
* <VegaChart spec={spec} data={rows} /> // already have rows
|
|
148
|
+
*
|
|
149
|
+
* The spec's own `data` is ignored/overridden — rows are inlined as the named
|
|
150
|
+
* dataset "table", so any Vega-Lite example works once you point its encodings
|
|
151
|
+
* at your query's column names. Remote data URLs in the spec are stripped.
|
|
152
|
+
*/
|
|
153
|
+
export function VegaChart({ spec, data, query, malloy, givens, style }) {
|
|
154
|
+
if (data != null) return <VegaChartInner spec={spec} rows={data} style={style} />;
|
|
155
|
+
return <VegaChartQuery spec={spec} query={query} malloy={malloy} givens={givens} style={style} />;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function VegaChartQuery({ spec, query, malloy, givens, style }) {
|
|
159
|
+
const req = malloy ? { malloy, givens } : { query, givens };
|
|
160
|
+
const { rows, loading, error } = useQuery(req);
|
|
161
|
+
if (error) return <pre style={{ color: "crimson", whiteSpace: "pre-wrap" }}>{error}</pre>;
|
|
162
|
+
return <VegaChartInner spec={spec} rows={rows} loading={loading} style={style} />;
|
|
163
|
+
}
|