@malloydata/malloyyo 0.2.34 → 0.2.36
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-runtime/runtime.tsx +76 -0
- package/dist/index.js +147 -102
- package/dist/shared/nav.ts +22 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Injected frame globals (read lazily — script order differs between hosts):
|
|
12
12
|
// window.__DASHBOARD__ { name, query, title, description? } (the # artifact tag)
|
|
13
|
+
// window.__DASHBOARDS__ [{ name, title, description?, href }] (the siblings, nav order)
|
|
13
14
|
// window.__GIVENS__ given specs introspected from the model's given: decls
|
|
14
15
|
// window.__INITIAL_GIVENS__ URL-seeded given values ($-prefixed) for shareable links
|
|
15
16
|
// window.__INITIAL_URLSTATE__ URL-seeded useUrlState view-state (~-prefixed)
|
|
@@ -33,6 +34,13 @@ import { combineTiles } from "./combine";
|
|
|
33
34
|
export { filters };
|
|
34
35
|
|
|
35
36
|
export const dashboardInfo = () => window.__DASHBOARD__ || {};
|
|
37
|
+
|
|
38
|
+
/** The dashboard's SIBLINGS: `[{ name, title, description?, href }]`, in nav
|
|
39
|
+
order, with this one excluded. Every host injects it (dev server, hosted
|
|
40
|
+
frame, static bundle), so a component that links to other dashboards — the
|
|
41
|
+
About page's card list, a "see also" — is written once and works on all
|
|
42
|
+
three. Empty when a host supplies nothing, so reading it is always safe. */
|
|
43
|
+
export const dashboardList = () => window.__DASHBOARDS__ || [];
|
|
36
44
|
export const givenSpecs = () => window.__GIVENS__ || [];
|
|
37
45
|
|
|
38
46
|
// ── host bridge ─────────────────────────────────────────────────────
|
|
@@ -936,6 +944,7 @@ function Root({ Dashboard, extraProps }) {
|
|
|
936
944
|
<UrlStateCtx.Provider value={urlCtx}>
|
|
937
945
|
<Dashboard
|
|
938
946
|
dashboard={dashboardInfo()}
|
|
947
|
+
dashboards={dashboardList()}
|
|
939
948
|
givenSpecs={givenSpecs()}
|
|
940
949
|
givens={committed}
|
|
941
950
|
setGiven={setGiven}
|
|
@@ -966,6 +975,20 @@ function Root({ Dashboard, extraProps }) {
|
|
|
966
975
|
// stays legible; override --dash-panel-bg if your renderer output is dark-safe.
|
|
967
976
|
const THEME_CSS = `
|
|
968
977
|
:root {
|
|
978
|
+
/* The static site's variable names, aliased onto the theme.
|
|
979
|
+
A written page (the About page, most obviously) is authored against the
|
|
980
|
+
bundle's site.css, which names them --bg/--fg/--card/--line/--muted/--accent.
|
|
981
|
+
The same component also runs here — under the dev server and in the hosted
|
|
982
|
+
frame — where only --dash-* exists, so without these it renders with
|
|
983
|
+
transparent cards and invisible borders: technically fine, visibly broken.
|
|
984
|
+
Defined first and with plain names so a page may still override them. */
|
|
985
|
+
--bg: var(--dash-bg, #ffffff);
|
|
986
|
+
--fg: var(--dash-fg, #111418);
|
|
987
|
+
--card: var(--dash-panel-bg, #ffffff);
|
|
988
|
+
--line: var(--dash-border, #e5e7eb);
|
|
989
|
+
--muted: var(--dash-muted, #6b7280);
|
|
990
|
+
--accent: var(--dash-accent, #1573a1);
|
|
991
|
+
|
|
969
992
|
--dash-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
970
993
|
--dash-bg: #ffffff;
|
|
971
994
|
--dash-fg: #171717;
|
|
@@ -1029,8 +1052,61 @@ function injectTheme(bodyReset) {
|
|
|
1029
1052
|
`rootEl` (defaults to #root, the sandboxed iframe's mount node). The tag-only
|
|
1030
1053
|
in-page host passes its own container so the dashboard mounts directly in the
|
|
1031
1054
|
trusted page — no iframe. */
|
|
1055
|
+
/**
|
|
1056
|
+
* Route a click on a sibling link through the host's navigate bridge.
|
|
1057
|
+
*
|
|
1058
|
+
* A written page (the About page) links to its siblings as ordinary anchors —
|
|
1059
|
+
* that is what makes the same component work as a plain static page in a
|
|
1060
|
+
* `dashboard bundle`. Inside a framed host it is NOT a plain page: the document
|
|
1061
|
+
* is a sandboxed iframe on its own origin, so a relative href navigates the
|
|
1062
|
+
* FRAME to a URL that does not exist there (the dev server answers "not found"
|
|
1063
|
+
* on the frame port), and a top-level href would need allow-top-navigation,
|
|
1064
|
+
* which the sandbox deliberately withholds.
|
|
1065
|
+
*
|
|
1066
|
+
* The bridge already solves this for drill: only the trusted parent knows the
|
|
1067
|
+
* environment's dashboard shape (hosted /datasets/:id/dashboard/:slug vs local
|
|
1068
|
+
* /?d=slug). So intercept the click and hand the parent a NAME, exactly as drill
|
|
1069
|
+
* does. The anchor keeps its href, so the static build is untouched and the link
|
|
1070
|
+
* still shows a real target on hover.
|
|
1071
|
+
*
|
|
1072
|
+
* Deliberately capture-phase and non-destructive: modified clicks (new tab,
|
|
1073
|
+
* download) and anchors that aren't siblings fall through untouched.
|
|
1074
|
+
*/
|
|
1075
|
+
/** Installed once per document. `mount()` runs again on every client-side
|
|
1076
|
+
navigation between tag-only dashboards (TagOnlyDashboard unmounts the React
|
|
1077
|
+
root and re-mounts), and a listener added per mount is never removed — so
|
|
1078
|
+
without this each visit would add another handler and one click would fire
|
|
1079
|
+
navigate once per handler. injectTheme guards itself the same way. */
|
|
1080
|
+
let siblingLinksInstalled = false;
|
|
1081
|
+
|
|
1082
|
+
function interceptSiblingLinks() {
|
|
1083
|
+
if (typeof document === "undefined" || siblingLinksInstalled) return;
|
|
1084
|
+
siblingLinksInstalled = true;
|
|
1085
|
+
document.addEventListener(
|
|
1086
|
+
"click",
|
|
1087
|
+
(e: any) => {
|
|
1088
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
1089
|
+
const a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
1090
|
+
if (!a || a.target === "_blank" || a.hasAttribute("download")) return;
|
|
1091
|
+
// dashboardList() is read HERE, per click, not captured at mount: the
|
|
1092
|
+
// header's rule is that injected globals are read lazily because script
|
|
1093
|
+
// order differs between hosts, and a host that sets __DASHBOARDS__ after
|
|
1094
|
+
// the bundle runs would otherwise get no interception at all — silently,
|
|
1095
|
+
// which is the very failure this exists to prevent.
|
|
1096
|
+
// Matched on the authored attribute, which is what the host injected.
|
|
1097
|
+
const href = a.getAttribute("href");
|
|
1098
|
+
const hit = dashboardList().find((d: any) => d && d.href === href);
|
|
1099
|
+
if (!hit) return;
|
|
1100
|
+
e.preventDefault();
|
|
1101
|
+
host.navigate(hit.name, {});
|
|
1102
|
+
},
|
|
1103
|
+
true,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1032
1107
|
export function mount(Dashboard, extraProps, rootEl: any = null, opts: any = {}) {
|
|
1033
1108
|
injectTheme(opts.bodyReset !== false);
|
|
1109
|
+
interceptSiblingLinks();
|
|
1034
1110
|
window.addEventListener("error", (e) => {
|
|
1035
1111
|
if (isBenign(e && e.message)) return;
|
|
1036
1112
|
showFatal((e.error && e.error.stack) || e.message);
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { resolve as resolve3 } from "node:path";
|
|
|
7
7
|
// src/config.ts
|
|
8
8
|
import { readFileSync, existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
var KNOWN_KEYS = /* @__PURE__ */ new Set(["analytics", "targets"]);
|
|
10
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set(["analytics", "targets", "image_hosts"]);
|
|
11
11
|
function isTarget(v) {
|
|
12
12
|
return typeof v === "object" && v !== null && typeof v.url === "string";
|
|
13
13
|
}
|
|
@@ -2443,6 +2443,97 @@ async function makeRunner(root) {
|
|
|
2443
2443
|
};
|
|
2444
2444
|
}
|
|
2445
2445
|
|
|
2446
|
+
// src/discover.ts
|
|
2447
|
+
import fs3 from "node:fs";
|
|
2448
|
+
import path4 from "node:path";
|
|
2449
|
+
import { fileURLToPath } from "node:url";
|
|
2450
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2451
|
+
var require2 = createRequire2(import.meta.url);
|
|
2452
|
+
var HOST_LIBS = [
|
|
2453
|
+
"react",
|
|
2454
|
+
"react-dom",
|
|
2455
|
+
"react-dom/client",
|
|
2456
|
+
"react/jsx-runtime",
|
|
2457
|
+
"react/jsx-dev-runtime",
|
|
2458
|
+
"@malloydata/render",
|
|
2459
|
+
"@malloydata/malloy-filter"
|
|
2460
|
+
];
|
|
2461
|
+
var HOST_ALIAS = {};
|
|
2462
|
+
for (const spec of HOST_LIBS) {
|
|
2463
|
+
try {
|
|
2464
|
+
HOST_ALIAS[spec] = require2.resolve(spec);
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
function resolveRuntimeDir() {
|
|
2469
|
+
const candidates = [
|
|
2470
|
+
new URL("./frame-runtime/", import.meta.url),
|
|
2471
|
+
// src/dashboard.ts (tsx) OR dist/index.js (published)
|
|
2472
|
+
new URL("../src/frame-runtime/", import.meta.url)
|
|
2473
|
+
// built dist/ next to sibling src/ (checkout)
|
|
2474
|
+
].map((u) => fileURLToPath(u));
|
|
2475
|
+
const found = candidates.find((c) => fs3.existsSync(c));
|
|
2476
|
+
if (!found) {
|
|
2477
|
+
throw new Error(
|
|
2478
|
+
"frame-runtime/ not found next to the CLI (looked in ./frame-runtime and ../src/frame-runtime). A published install should ship it in dist/; reinstall the CLI, or rebuild with `npm run build`."
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
return found;
|
|
2482
|
+
}
|
|
2483
|
+
var hostAliasPlugin = {
|
|
2484
|
+
name: "host-alias",
|
|
2485
|
+
setup(b) {
|
|
2486
|
+
b.onResolve(
|
|
2487
|
+
{ filter: /^(react($|\/)|react-dom($|\/)|@malloydata\/(render|malloy-filter)$)/ },
|
|
2488
|
+
(args) => HOST_ALIAS[args.path] ? { path: HOST_ALIAS[args.path] } : void 0
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
async function discoverDashboards(root, runner) {
|
|
2493
|
+
const dir = path4.join(root, "dashboards");
|
|
2494
|
+
if (!fs3.existsSync(dir)) return [];
|
|
2495
|
+
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".malloy")).sort();
|
|
2496
|
+
const dashboards = [];
|
|
2497
|
+
for (const file of files) {
|
|
2498
|
+
const base = file.slice(0, -".malloy".length);
|
|
2499
|
+
const entryFile = path4.join("dashboards", file);
|
|
2500
|
+
const res = await runner.artifactForFile(entryFile, base);
|
|
2501
|
+
if (!res.ok) throw new Error(`dashboard ${file}: ${res.error}`);
|
|
2502
|
+
if (!res.artifact) continue;
|
|
2503
|
+
const component = ["jsx", "tsx"].map((ext) => path4.join(dir, `${base}.${ext}`)).find((p) => fs3.existsSync(p));
|
|
2504
|
+
dashboards.push({ ...res.artifact, name: res.artifact.name || base, entryFile, tsxPath: component });
|
|
2505
|
+
}
|
|
2506
|
+
const about = aboutPage(root);
|
|
2507
|
+
if (about && !dashboards.some((d) => d.name === about.name)) dashboards.unshift(about);
|
|
2508
|
+
return dashboards;
|
|
2509
|
+
}
|
|
2510
|
+
function browserBuildBase() {
|
|
2511
|
+
const shims = path4.join(resolveRuntimeDir(), "..", "shims");
|
|
2512
|
+
return {
|
|
2513
|
+
platform: "browser",
|
|
2514
|
+
jsx: "automatic",
|
|
2515
|
+
loader: { ".css": "empty" },
|
|
2516
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
2517
|
+
alias: {
|
|
2518
|
+
assert: path4.join(shims, "assert.cjs"),
|
|
2519
|
+
util: path4.join(shims, "util.cjs")
|
|
2520
|
+
},
|
|
2521
|
+
banner: {
|
|
2522
|
+
js: "globalThis.process||={env:{},platform:'browser',versions:{},argv:[],cwd:()=>'/'};"
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
var ABOUT_NAME = "index";
|
|
2527
|
+
var ABOUT_TITLE = "About";
|
|
2528
|
+
function aboutPage(root) {
|
|
2529
|
+
const dir = path4.join(root, "dashboards");
|
|
2530
|
+
if (fs3.existsSync(path4.join(dir, "index.malloy"))) return null;
|
|
2531
|
+
const component = ["jsx", "tsx"].map((ext) => path4.join(dir, `index.${ext}`)).find((p) => fs3.existsSync(p));
|
|
2532
|
+
if (!component) return null;
|
|
2533
|
+
return { name: ABOUT_NAME, query: "", title: ABOUT_TITLE, tsxPath: component };
|
|
2534
|
+
}
|
|
2535
|
+
var rendersNoData = (d) => !d.query && (!d.tiles || d.tiles.length === 0);
|
|
2536
|
+
|
|
2446
2537
|
// src/gather.ts
|
|
2447
2538
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
2448
2539
|
function gatherDirectory(dir) {
|
|
@@ -2494,6 +2585,14 @@ async function gatherDashboards(dir) {
|
|
|
2494
2585
|
source: component ? readFileSync2(component, "utf8") : ""
|
|
2495
2586
|
});
|
|
2496
2587
|
}
|
|
2588
|
+
const about = aboutPage(dir);
|
|
2589
|
+
if (about?.tsxPath && !payloads.some((p) => p.name === about.name)) {
|
|
2590
|
+
payloads.unshift({
|
|
2591
|
+
name: about.name,
|
|
2592
|
+
manifest: { title: about.title },
|
|
2593
|
+
source: readFileSync2(about.tsxPath, "utf8")
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2497
2596
|
return payloads;
|
|
2498
2597
|
} finally {
|
|
2499
2598
|
await runner.dispose();
|
|
@@ -2538,6 +2637,16 @@ function componentQueryLiterals(source) {
|
|
|
2538
2637
|
while ((m = re.exec(source)) !== null) out.add(m[2].trim());
|
|
2539
2638
|
return [...out];
|
|
2540
2639
|
}
|
|
2640
|
+
function landingPageErrors(dir, file) {
|
|
2641
|
+
const ext = file.endsWith(".tsx") ? "tsx" : "jsx";
|
|
2642
|
+
try {
|
|
2643
|
+
esbuild.transformSync(readFileSync3(join3(dir, file), "utf8"), { loader: ext, jsx: "automatic" });
|
|
2644
|
+
return [];
|
|
2645
|
+
} catch (e) {
|
|
2646
|
+
const msg = e.errors?.map((x) => x.text).join("; ") ?? String(e);
|
|
2647
|
+
return [`${file}: ${msg}`];
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2541
2650
|
async function lintDashboards(root) {
|
|
2542
2651
|
const abs = resolve(root);
|
|
2543
2652
|
const runner = await makeRunner(abs);
|
|
@@ -2560,13 +2669,16 @@ async function runLint(abs, runner) {
|
|
|
2560
2669
|
const malloyBases = new Set(malloyFiles.map((f) => f.slice(0, -".malloy".length)));
|
|
2561
2670
|
for (const c of entries.filter((f) => /\.(jsx|tsx)$/.test(f)).sort()) {
|
|
2562
2671
|
const cbase = c.replace(/\.(jsx|tsx)$/, "");
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
warnings: []
|
|
2568
|
-
});
|
|
2672
|
+
if (malloyBases.has(cbase)) continue;
|
|
2673
|
+
if (cbase === "index") {
|
|
2674
|
+
dashboards.push({ name: c, errors: landingPageErrors(dir, c), warnings: [] });
|
|
2675
|
+
continue;
|
|
2569
2676
|
}
|
|
2677
|
+
dashboards.push({
|
|
2678
|
+
name: c,
|
|
2679
|
+
errors: [`component "${c}" has no matching "${cbase}.malloy" dashboard`],
|
|
2680
|
+
warnings: []
|
|
2681
|
+
});
|
|
2570
2682
|
}
|
|
2571
2683
|
const seenNames = /* @__PURE__ */ new Map();
|
|
2572
2684
|
for (const file of malloyFiles) {
|
|
@@ -2735,7 +2847,7 @@ function clearCreds(url6) {
|
|
|
2735
2847
|
}
|
|
2736
2848
|
|
|
2737
2849
|
// package.json
|
|
2738
|
-
var version = "0.2.
|
|
2850
|
+
var version = "0.2.36";
|
|
2739
2851
|
|
|
2740
2852
|
// src/http.ts
|
|
2741
2853
|
var USER_AGENT = `malloyyo/${version}`;
|
|
@@ -2917,8 +3029,8 @@ Run: malloyyo login ${target.name}`);
|
|
|
2917
3029
|
}
|
|
2918
3030
|
|
|
2919
3031
|
// src/mcp.ts
|
|
2920
|
-
import
|
|
2921
|
-
import
|
|
3032
|
+
import fs4 from "node:fs";
|
|
3033
|
+
import path5 from "node:path";
|
|
2922
3034
|
import url4 from "node:url";
|
|
2923
3035
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2924
3036
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -3007,7 +3119,7 @@ function defaultConfig(rootUrl) {
|
|
|
3007
3119
|
});
|
|
3008
3120
|
}
|
|
3009
3121
|
async function loadConfig2(root, reader) {
|
|
3010
|
-
const rootUrl = url4.pathToFileURL(root +
|
|
3122
|
+
const rootUrl = url4.pathToFileURL(root + path5.sep);
|
|
3011
3123
|
let discovered;
|
|
3012
3124
|
try {
|
|
3013
3125
|
discovered = await discoverConfig2(rootUrl, rootUrl, reader);
|
|
@@ -3029,13 +3141,13 @@ function fsReader2() {
|
|
|
3029
3141
|
if (u.protocol !== "file:") {
|
|
3030
3142
|
throw new Error(`unsupported URL scheme for import: ${u.href}`);
|
|
3031
3143
|
}
|
|
3032
|
-
return
|
|
3144
|
+
return fs4.promises.readFile(u, "utf8");
|
|
3033
3145
|
}
|
|
3034
3146
|
};
|
|
3035
3147
|
}
|
|
3036
3148
|
function resolveUnderRoot(root, p) {
|
|
3037
|
-
const abs = p.includes("://") ?
|
|
3038
|
-
if (abs !== root && !abs.startsWith(root +
|
|
3149
|
+
const abs = p.includes("://") ? path5.resolve(decodeURIComponent(new URL(p).pathname)) : path5.resolve(root, p);
|
|
3150
|
+
if (abs !== root && !abs.startsWith(root + path5.sep)) {
|
|
3039
3151
|
throw new Error(`path is outside the project root: ${p}`);
|
|
3040
3152
|
}
|
|
3041
3153
|
return abs;
|
|
@@ -3044,7 +3156,7 @@ function makeConfigSource(root) {
|
|
|
3044
3156
|
let cached;
|
|
3045
3157
|
const signature = () => ["malloy-config.json", "malloy-config-local.json"].map((name) => {
|
|
3046
3158
|
try {
|
|
3047
|
-
const st =
|
|
3159
|
+
const st = fs4.statSync(path5.join(root, name));
|
|
3048
3160
|
return `${name}:${st.mtimeMs}:${st.size}`;
|
|
3049
3161
|
} catch {
|
|
3050
3162
|
return `${name}:absent`;
|
|
@@ -3064,7 +3176,7 @@ function makeWithRuntime(root, currentConfig) {
|
|
|
3064
3176
|
return gateConfigProblems(problems, async () => {
|
|
3065
3177
|
const resolved = "url" in input ? { url: resolveUnderRoot(root, input.url) } : {
|
|
3066
3178
|
source: input.source,
|
|
3067
|
-
baseUrl: input.baseUrl ? resolveUnderRoot(root, input.baseUrl) : root +
|
|
3179
|
+
baseUrl: input.baseUrl ? resolveUnderRoot(root, input.baseUrl) : root + path5.sep
|
|
3068
3180
|
};
|
|
3069
3181
|
const { reader, entry, readSource } = prepareSource(fsReader2(), resolved);
|
|
3070
3182
|
const runtime = new Runtime2({ config, urlReader: reader });
|
|
@@ -3078,7 +3190,7 @@ function makeWithRuntime(root, currentConfig) {
|
|
|
3078
3190
|
}
|
|
3079
3191
|
function makeExploreHost(root, currentConfig) {
|
|
3080
3192
|
const withRuntime = makeWithRuntime(root, currentConfig);
|
|
3081
|
-
const published = (ref) => ref === ENTRY2 &&
|
|
3193
|
+
const published = (ref) => ref === ENTRY2 && fs4.existsSync(path5.join(root, ENTRY2));
|
|
3082
3194
|
return {
|
|
3083
3195
|
withModel: (ref, fn) => {
|
|
3084
3196
|
if (!published(ref)) throw new Error(`no published model '${ref}'`);
|
|
@@ -3099,7 +3211,7 @@ function makeDevelopHost(root, currentConfig) {
|
|
|
3099
3211
|
}
|
|
3100
3212
|
async function serveMcp(opts) {
|
|
3101
3213
|
await initConnections();
|
|
3102
|
-
const root =
|
|
3214
|
+
const root = path5.resolve(opts.root ?? process.cwd());
|
|
3103
3215
|
const mode = opts.mode ?? "explore";
|
|
3104
3216
|
const currentConfig = makeConfigSource(root);
|
|
3105
3217
|
const surface = mode === "develop" ? developSurface(makeDevelopHost(root, currentConfig)) : exploreSurface(makeExploreHost(root, currentConfig));
|
|
@@ -3172,84 +3284,13 @@ function navHtml(active, all, href, homeHref = "./") {
|
|
|
3172
3284
|
).join("");
|
|
3173
3285
|
return `<nav class="dash-nav">${brand}<span class="sep"></span>${links}</nav>`;
|
|
3174
3286
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
var HOST_LIBS = [
|
|
3183
|
-
"react",
|
|
3184
|
-
"react-dom",
|
|
3185
|
-
"react-dom/client",
|
|
3186
|
-
"react/jsx-runtime",
|
|
3187
|
-
"react/jsx-dev-runtime",
|
|
3188
|
-
"@malloydata/render",
|
|
3189
|
-
"@malloydata/malloy-filter"
|
|
3190
|
-
];
|
|
3191
|
-
var HOST_ALIAS = {};
|
|
3192
|
-
for (const spec of HOST_LIBS) {
|
|
3193
|
-
try {
|
|
3194
|
-
HOST_ALIAS[spec] = require2.resolve(spec);
|
|
3195
|
-
} catch {
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
function resolveRuntimeDir() {
|
|
3199
|
-
const candidates = [
|
|
3200
|
-
new URL("./frame-runtime/", import.meta.url),
|
|
3201
|
-
// src/dashboard.ts (tsx) OR dist/index.js (published)
|
|
3202
|
-
new URL("../src/frame-runtime/", import.meta.url)
|
|
3203
|
-
// built dist/ next to sibling src/ (checkout)
|
|
3204
|
-
].map((u) => fileURLToPath(u));
|
|
3205
|
-
const found = candidates.find((c) => fs4.existsSync(c));
|
|
3206
|
-
if (!found) {
|
|
3207
|
-
throw new Error(
|
|
3208
|
-
"frame-runtime/ not found next to the CLI (looked in ./frame-runtime and ../src/frame-runtime). A published install should ship it in dist/; reinstall the CLI, or rebuild with `npm run build`."
|
|
3209
|
-
);
|
|
3210
|
-
}
|
|
3211
|
-
return found;
|
|
3212
|
-
}
|
|
3213
|
-
var hostAliasPlugin = {
|
|
3214
|
-
name: "host-alias",
|
|
3215
|
-
setup(b) {
|
|
3216
|
-
b.onResolve(
|
|
3217
|
-
{ filter: /^(react($|\/)|react-dom($|\/)|@malloydata\/(render|malloy-filter)$)/ },
|
|
3218
|
-
(args) => HOST_ALIAS[args.path] ? { path: HOST_ALIAS[args.path] } : void 0
|
|
3219
|
-
);
|
|
3220
|
-
}
|
|
3221
|
-
};
|
|
3222
|
-
async function discoverDashboards(root, runner) {
|
|
3223
|
-
const dir = path5.join(root, "dashboards");
|
|
3224
|
-
if (!fs4.existsSync(dir)) return [];
|
|
3225
|
-
const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".malloy")).sort();
|
|
3226
|
-
const dashboards = [];
|
|
3227
|
-
for (const file of files) {
|
|
3228
|
-
const base = file.slice(0, -".malloy".length);
|
|
3229
|
-
const entryFile = path5.join("dashboards", file);
|
|
3230
|
-
const res = await runner.artifactForFile(entryFile, base);
|
|
3231
|
-
if (!res.ok) throw new Error(`dashboard ${file}: ${res.error}`);
|
|
3232
|
-
if (!res.artifact) continue;
|
|
3233
|
-
const component = ["jsx", "tsx"].map((ext) => path5.join(dir, `${base}.${ext}`)).find((p) => fs4.existsSync(p));
|
|
3234
|
-
dashboards.push({ ...res.artifact, name: res.artifact.name || base, entryFile, tsxPath: component });
|
|
3235
|
-
}
|
|
3236
|
-
return dashboards;
|
|
3237
|
-
}
|
|
3238
|
-
function browserBuildBase() {
|
|
3239
|
-
const shims = path5.join(resolveRuntimeDir(), "..", "shims");
|
|
3240
|
-
return {
|
|
3241
|
-
platform: "browser",
|
|
3242
|
-
jsx: "automatic",
|
|
3243
|
-
loader: { ".css": "empty" },
|
|
3244
|
-
define: { "process.env.NODE_ENV": '"production"' },
|
|
3245
|
-
alias: {
|
|
3246
|
-
assert: path5.join(shims, "assert.cjs"),
|
|
3247
|
-
util: path5.join(shims, "util.cjs")
|
|
3248
|
-
},
|
|
3249
|
-
banner: {
|
|
3250
|
-
js: "globalThis.process||={env:{},platform:'browser',versions:{},argv:[],cwd:()=>'/'};"
|
|
3251
|
-
}
|
|
3252
|
-
};
|
|
3287
|
+
function siblingList(current, all, href) {
|
|
3288
|
+
return all.filter((d) => d.name !== current).map((d) => ({
|
|
3289
|
+
name: d.name,
|
|
3290
|
+
title: d.title || d.name,
|
|
3291
|
+
...d.description ? { description: d.description } : {},
|
|
3292
|
+
href: href(d.name)
|
|
3293
|
+
}));
|
|
3253
3294
|
}
|
|
3254
3295
|
|
|
3255
3296
|
// src/dashboard.ts
|
|
@@ -3330,6 +3371,7 @@ function makeInPageBundler() {
|
|
|
3330
3371
|
};
|
|
3331
3372
|
}
|
|
3332
3373
|
var html = (body, title) => `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(title)}</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>${NAV_CSS}</style></head><body style="margin:0">${body}</body></html>`;
|
|
3374
|
+
var devSiblings = (dash, all) => siblingList(dash.name, all, (n) => `/?d=${encodeURIComponent(n)}`);
|
|
3333
3375
|
function navHtml2(dash, all) {
|
|
3334
3376
|
return navHtml(dash.name, all, (n) => `/?d=${encodeURIComponent(n)}`);
|
|
3335
3377
|
}
|
|
@@ -3346,7 +3388,7 @@ function inPageShell(dash, all, givenSpecs, initialGivens, initialUrlState, tile
|
|
|
3346
3388
|
autorun: dash.autorun
|
|
3347
3389
|
};
|
|
3348
3390
|
return html(
|
|
3349
|
-
navHtml2(dash, all) + `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>try{new EventSource('/events').onmessage=()=>location.reload();}catch(e){}</script><script src="/inpage.js?d=${encodeURIComponent(dash.name)}"></script>`,
|
|
3391
|
+
navHtml2(dash, all) + `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>try{new EventSource('/events').onmessage=()=>location.reload();}catch(e){}</script><script src="/inpage.js?d=${encodeURIComponent(dash.name)}"></script>`,
|
|
3350
3392
|
dash.title
|
|
3351
3393
|
);
|
|
3352
3394
|
}
|
|
@@ -3413,7 +3455,7 @@ function givensFromUrl(url6) {
|
|
|
3413
3455
|
function urlStateFromUrl(url6) {
|
|
3414
3456
|
return urlStateFromSearch(url6.search);
|
|
3415
3457
|
}
|
|
3416
|
-
function frameDoc(dash, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
|
|
3458
|
+
function frameDoc(dash, all, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
|
|
3417
3459
|
const info = {
|
|
3418
3460
|
name: dash.name,
|
|
3419
3461
|
query: dash.query,
|
|
@@ -3428,7 +3470,7 @@ function frameDoc(dash, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
|
|
|
3428
3470
|
autorun: dash.autorun
|
|
3429
3471
|
};
|
|
3430
3472
|
return html(
|
|
3431
|
-
`<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script src="/bundle.js?d=${encodeURIComponent(dash.name)}"></script>`,
|
|
3473
|
+
`<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script src="/bundle.js?d=${encodeURIComponent(dash.name)}"></script>`,
|
|
3432
3474
|
dash.title
|
|
3433
3475
|
);
|
|
3434
3476
|
}
|
|
@@ -3458,6 +3500,7 @@ async function serveDashboard(opts) {
|
|
|
3458
3500
|
const inPageBundle = makeInPageBundler();
|
|
3459
3501
|
const pick = (url6) => byName.get(url6.searchParams.get("d") ?? dashboards[0].name) ?? dashboards[0];
|
|
3460
3502
|
async function resolveGivens(dash) {
|
|
3503
|
+
if (rendersNoData(dash)) return { ok: true, union: [] };
|
|
3461
3504
|
if (dash.tiles && dash.entryFile) {
|
|
3462
3505
|
const t = await runner.dashboardTiles(dash.entryFile, dash.tiles);
|
|
3463
3506
|
return { ok: true, union: t.union, tiles: t.tiles };
|
|
@@ -3510,7 +3553,7 @@ async function serveDashboard(opts) {
|
|
|
3510
3553
|
return send(
|
|
3511
3554
|
200,
|
|
3512
3555
|
"text/html; charset=utf-8",
|
|
3513
|
-
frameDoc(dash, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
|
|
3556
|
+
frameDoc(dash, dashboards, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
|
|
3514
3557
|
);
|
|
3515
3558
|
}
|
|
3516
3559
|
if (url6.pathname === "/bundle.js") {
|
|
@@ -3795,11 +3838,12 @@ window.__GIVENS__ = ${safeJson(givenSpecs)};
|
|
|
3795
3838
|
}
|
|
3796
3839
|
function indexPage(dashboards, title, custom, cleanUrls, analytics) {
|
|
3797
3840
|
const link = (n) => cleanUrls ? `./${encodeURIComponent(n)}` : `./${encodeURIComponent(n)}.html`;
|
|
3841
|
+
const listed = dashboards.filter((d) => !rendersNoData(d));
|
|
3798
3842
|
const body = custom ? `<div id="root"></div>
|
|
3799
3843
|
<script>window.__DASHBOARDS__ = ${safeJson(
|
|
3800
|
-
|
|
3844
|
+
listed.map((d) => ({ name: d.name, title: d.title, description: d.description, href: link(d.name) }))
|
|
3801
3845
|
)};</script>
|
|
3802
|
-
<script type="module" src="./assets/index.js"></script>` : `<main class="index"><h1>${esc3(title)}</h1><ul>` +
|
|
3846
|
+
<script type="module" src="./assets/index.js"></script>` : `<main class="index"><h1>${esc3(title)}</h1><ul>` + listed.map(
|
|
3803
3847
|
(d) => `<li><a href="${link(d.name)}"><strong>${esc3(d.title || d.name)}</strong>` + (d.description ? `<span>${esc3(d.description)}</span>` : "") + `</a></li>`
|
|
3804
3848
|
).join("") + `</ul></main>`;
|
|
3805
3849
|
return `<!doctype html>
|
|
@@ -3960,6 +4004,7 @@ boot(Dashboard);
|
|
|
3960
4004
|
});
|
|
3961
4005
|
fs7.writeFileSync(path8.join(outDir, "assets", "site.css"), SITE_CSS);
|
|
3962
4006
|
for (const d of dashboards) {
|
|
4007
|
+
if (rendersNoData(d)) continue;
|
|
3963
4008
|
let specs = [];
|
|
3964
4009
|
let tileSpecs;
|
|
3965
4010
|
if (d.tiles && d.entryFile) {
|
package/dist/shared/nav.ts
CHANGED
|
@@ -70,3 +70,25 @@ export function navHtml(
|
|
|
70
70
|
.join("");
|
|
71
71
|
return `<nav class="dash-nav">${brand}<span class="sep"></span>${links}</nav>`;
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/** The sibling list a host injects as `window.__DASHBOARDS__`, in nav order and
|
|
75
|
+
excluding the current page.
|
|
76
|
+
*
|
|
77
|
+
* Same shape and same link-shape callback as `navHtml`, because it answers the
|
|
78
|
+
* same question in component form: an About page (or any dashboard) that wants
|
|
79
|
+
* to link to the others gets `props.dashboards` and never has to know whether it
|
|
80
|
+
* is running under the dev server, the hosted frame, or a static bundle. */
|
|
81
|
+
export function siblingList<T extends NavDashboard & { description?: string }>(
|
|
82
|
+
current: string,
|
|
83
|
+
all: readonly T[],
|
|
84
|
+
href: (name: string) => string,
|
|
85
|
+
): Array<{ name: string; title: string; description?: string; href: string }> {
|
|
86
|
+
return all
|
|
87
|
+
.filter((d) => d.name !== current)
|
|
88
|
+
.map((d) => ({
|
|
89
|
+
name: d.name,
|
|
90
|
+
title: d.title || d.name,
|
|
91
|
+
...(d.description ? { description: d.description } : {}),
|
|
92
|
+
href: href(d.name),
|
|
93
|
+
}));
|
|
94
|
+
}
|