@kaizenreport/kensho-viewer 0.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/assets/app.js +1396 -0
- package/assets/app.jsx +860 -0
- package/assets/charts.js +1156 -0
- package/assets/charts.jsx +593 -0
- package/assets/colors_and_type.css +219 -0
- package/assets/components.js +894 -0
- package/assets/components.jsx +520 -0
- package/assets/data-bridge.js +49 -0
- package/assets/data-bridge.jsx +55 -0
- package/assets/data-loader.js +543 -0
- package/assets/kaizen-mark.svg +5 -0
- package/assets/kensho-wordmark.svg +18 -0
- package/assets/pages.js +1472 -0
- package/assets/pages.jsx +822 -0
- package/assets/test-detail.js +1058 -0
- package/assets/test-detail.jsx +502 -0
- package/assets/tokens.css +357 -0
- package/assets/tree-detail.js +1705 -0
- package/assets/tree-detail.jsx +947 -0
- package/dist/component.d.ts +115 -0
- package/dist/component.js +698 -0
- package/index.html +35 -0
- package/package.json +65 -0
- package/scripts/build.js +117 -0
- package/src/component.d.ts +115 -0
- package/src/component.jsx +340 -0
- package/src/data.js +538 -0
package/index.html
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Kensho · Test Report</title>
|
|
7
|
+
<link rel="icon" href="assets/kaizen-mark.svg">
|
|
8
|
+
<link rel="stylesheet" href="assets/tokens.css">
|
|
9
|
+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
10
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" crossorigin="anonymous"></script>
|
|
11
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" crossorigin="anonymous"></script>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app">
|
|
15
|
+
<noscript>Kensho report requires JavaScript to render.</noscript>
|
|
16
|
+
<div class="kv-boot">
|
|
17
|
+
<div class="kv-boot-mark">
|
|
18
|
+
<img src="assets/kaizen-mark.svg" width="40" height="40" alt="">
|
|
19
|
+
</div>
|
|
20
|
+
<div class="kv-boot-text">Loading report…</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- Pre-compiled by packages/viewer/scripts/build.js — the .jsx sources are
|
|
25
|
+
copied alongside for human readability, but the browser only needs .js. -->
|
|
26
|
+
<script src="assets/data-loader.js"></script>
|
|
27
|
+
<script src="assets/data-bridge.js"></script>
|
|
28
|
+
<script src="assets/components.js"></script>
|
|
29
|
+
<script src="assets/charts.js"></script>
|
|
30
|
+
<script src="assets/test-detail.js"></script>
|
|
31
|
+
<script src="assets/tree-detail.js"></script>
|
|
32
|
+
<script src="assets/pages.js"></script>
|
|
33
|
+
<script src="assets/app.js"></script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaizenreport/kensho-viewer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Static HTML/JS/CSS viewer bundled by the Kensho CLI into each generated report. Also exports an embeddable React component for the Kaizen platform.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.html",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.html",
|
|
9
|
+
"assets",
|
|
10
|
+
"src",
|
|
11
|
+
"scripts",
|
|
12
|
+
"dist",
|
|
13
|
+
"build.js",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.html",
|
|
18
|
+
"./component": {
|
|
19
|
+
"types": "./dist/component.d.ts",
|
|
20
|
+
"import": "./dist/component.js",
|
|
21
|
+
"default": "./dist/component.js"
|
|
22
|
+
},
|
|
23
|
+
"./style.css": "./assets/tokens.css",
|
|
24
|
+
"./data": "./src/data.js",
|
|
25
|
+
"./assets/*": "./assets/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18.0.0",
|
|
30
|
+
"react-dom": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"react": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"react-dom": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@babel/core": "^7.29.7",
|
|
42
|
+
"@babel/preset-react": "^7.27.1",
|
|
43
|
+
"esbuild": "^0.24.0"
|
|
44
|
+
},
|
|
45
|
+
"license": "Apache-2.0",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/brandon1794/kensho.git",
|
|
52
|
+
"directory": "packages/viewer"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/brandon1794/kensho/tree/main/packages/viewer#readme",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/brandon1794/kensho/issues"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "node scripts/build.js",
|
|
63
|
+
"build:no-bundle": "node scripts/build.js --no-bundle"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build pipeline for @kaizenreport/kensho-viewer.
|
|
3
|
+
//
|
|
4
|
+
// Two outputs:
|
|
5
|
+
//
|
|
6
|
+
// 1. Static-report assets — the pre-compiled .js files that index.html
|
|
7
|
+
// references via plain <script src="..."> tags. Each assets/*.jsx is
|
|
8
|
+
// transpiled in place to assets/*.js via @babel/preset-react. The
|
|
9
|
+
// browser does NOT need @babel/standalone at runtime. We also produce
|
|
10
|
+
// assets/data-loader.js which exposes window.__KENSHO_LOAD_DATA so the
|
|
11
|
+
// static `data-bridge.js` shim can call into the shared loader.
|
|
12
|
+
//
|
|
13
|
+
// 2. Embed bundle — dist/component.js + dist/component.d.ts. An ESM build
|
|
14
|
+
// of the React `<KenshoViewer />` component (and the shared loader),
|
|
15
|
+
// bundled with esbuild. React/ReactDOM are external peer-deps so the
|
|
16
|
+
// host app picks them up.
|
|
17
|
+
//
|
|
18
|
+
// Both outputs are produced from the same set of sources so the two modes
|
|
19
|
+
// stay in sync.
|
|
20
|
+
|
|
21
|
+
import { readdirSync, readFileSync, writeFileSync, statSync, mkdirSync, copyFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { transformSync } from '@babel/core';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const ROOT = resolve(__dirname, '..');
|
|
28
|
+
const ASSETS = join(ROOT, 'assets');
|
|
29
|
+
const SRC = join(ROOT, 'src');
|
|
30
|
+
const DIST = join(ROOT, 'dist');
|
|
31
|
+
|
|
32
|
+
mkdirSync(ASSETS, { recursive: true });
|
|
33
|
+
mkdirSync(DIST, { recursive: true });
|
|
34
|
+
|
|
35
|
+
// -------------------------------------------------------------------------
|
|
36
|
+
// 1. Compile every .jsx in assets/ → sibling .js
|
|
37
|
+
// -------------------------------------------------------------------------
|
|
38
|
+
const files = readdirSync(ASSETS).filter(f => f.endsWith('.jsx'));
|
|
39
|
+
let bytesIn = 0, bytesOut = 0;
|
|
40
|
+
for (const f of files) {
|
|
41
|
+
const src = readFileSync(join(ASSETS, f), 'utf8');
|
|
42
|
+
const { code } = transformSync(src, {
|
|
43
|
+
presets: [['@babel/preset-react', { runtime: 'classic' }]],
|
|
44
|
+
babelrc: false,
|
|
45
|
+
configFile: false,
|
|
46
|
+
filename: f,
|
|
47
|
+
sourceMaps: false,
|
|
48
|
+
compact: false,
|
|
49
|
+
comments: true,
|
|
50
|
+
});
|
|
51
|
+
const out = f.replace(/\.jsx$/, '.js');
|
|
52
|
+
const banner = `/* Auto-generated from ${f} by packages/viewer/scripts/build.js. Edit the .jsx — DO NOT edit this file. */\n`;
|
|
53
|
+
writeFileSync(join(ASSETS, out), banner + code);
|
|
54
|
+
bytesIn += Buffer.byteLength(src);
|
|
55
|
+
bytesOut += Buffer.byteLength(code);
|
|
56
|
+
console.log(` ${f.padEnd(20)} → ${out} (${(Buffer.byteLength(code)/1024).toFixed(1)} KB)`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
// 2. Static-report data loader — wraps src/data.js into a non-module script
|
|
61
|
+
// that exposes window.__KENSHO_LOAD_DATA. The static report loads this
|
|
62
|
+
// via a plain <script> tag (no `type="module"`, no bundler at runtime).
|
|
63
|
+
//
|
|
64
|
+
// The loader source itself is plain ESM, so we strip the `export` keywords
|
|
65
|
+
// and wrap in an IIFE that pins the named export onto window.
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
const dataSrc = readFileSync(join(SRC, 'data.js'), 'utf8');
|
|
68
|
+
// Replace `export function name(` and `export async function name(` → `function name(`.
|
|
69
|
+
// Then expose `loadKenshoData` (and a couple helpers) on window at the end.
|
|
70
|
+
const dataNoExport = dataSrc.replace(/^export\s+(async\s+function|function|const|let|var)\b/gm, '$1');
|
|
71
|
+
const dataLoaderJs =
|
|
72
|
+
'/* Auto-generated. Wraps src/data.js for the static-report bootstrap. */\n' +
|
|
73
|
+
'(function () {\n' +
|
|
74
|
+
dataNoExport +
|
|
75
|
+
'\nwindow.__KENSHO_LOAD_DATA = loadKenshoData;\n' +
|
|
76
|
+
'})();\n';
|
|
77
|
+
writeFileSync(join(ASSETS, 'data-loader.js'), dataLoaderJs);
|
|
78
|
+
console.log(` ${'src/data.js'.padEnd(20)} → assets/data-loader.js (${(Buffer.byteLength(dataLoaderJs)/1024).toFixed(1)} KB)`);
|
|
79
|
+
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
// 3. ESM bundle for embedding — dist/component.js + dist/component.d.ts.
|
|
82
|
+
// Lazily required (esbuild is an optional dev dep — if it's not installed,
|
|
83
|
+
// the static-report path still builds). Skip with --no-bundle.
|
|
84
|
+
// -------------------------------------------------------------------------
|
|
85
|
+
const skipBundle = process.argv.includes('--no-bundle');
|
|
86
|
+
if (!skipBundle) {
|
|
87
|
+
let esbuild;
|
|
88
|
+
try {
|
|
89
|
+
esbuild = await import('esbuild');
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn('[kensho-viewer] esbuild not installed — skipping embed bundle.');
|
|
92
|
+
console.warn(' run `pnpm add -D esbuild` inside packages/viewer/ to enable it.');
|
|
93
|
+
}
|
|
94
|
+
if (esbuild) {
|
|
95
|
+
await esbuild.build({
|
|
96
|
+
entryPoints: [join(SRC, 'component.jsx')],
|
|
97
|
+
outfile: join(DIST, 'component.js'),
|
|
98
|
+
bundle: true,
|
|
99
|
+
format: 'esm',
|
|
100
|
+
target: ['es2020'],
|
|
101
|
+
platform: 'browser',
|
|
102
|
+
jsx: 'automatic',
|
|
103
|
+
jsxImportSource: 'react',
|
|
104
|
+
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
|
105
|
+
logLevel: 'info',
|
|
106
|
+
minify: false,
|
|
107
|
+
});
|
|
108
|
+
// Copy the .d.ts (hand-written, kept tiny).
|
|
109
|
+
const dts = join(SRC, 'component.d.ts');
|
|
110
|
+
if (existsSync(dts)) {
|
|
111
|
+
copyFileSync(dts, join(DIST, 'component.d.ts'));
|
|
112
|
+
}
|
|
113
|
+
console.log(` ${'src/component.jsx'.padEnd(20)} → dist/component.js`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`[kensho-viewer] built ${files.length} static asset${files.length === 1 ? '' : 's'} · ${(bytesIn/1024).toFixed(1)} KB → ${(bytesOut/1024).toFixed(1)} KB`);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Type declarations for @kaizenreport/kensho-viewer/component.
|
|
2
|
+
//
|
|
3
|
+
// Hand-written; the implementation is JSX. Kept minimal — covers the public
|
|
4
|
+
// API only.
|
|
5
|
+
|
|
6
|
+
import type * as React from 'react';
|
|
7
|
+
|
|
8
|
+
export interface KenshoSidebarItem {
|
|
9
|
+
/** Stable key, used for React reconciliation. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Visible label. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Lucide icon name (https://lucide.dev/icons). */
|
|
14
|
+
icon: string;
|
|
15
|
+
/** Render the page body when this item is active. */
|
|
16
|
+
render: () => React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KenshoExtraTab {
|
|
20
|
+
/** Stable key, used for React reconciliation. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Visible tab label. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Render the tab body for the given test (`RichTest` shape). */
|
|
25
|
+
render: (test: any) => React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface KenshoViewerProps {
|
|
29
|
+
/**
|
|
30
|
+
* URL to a Kensho `data/` directory. The component will fetch
|
|
31
|
+
* `${dataUrl}/index.json` and (lazily) `${dataUrl}/cases/<id>.json`.
|
|
32
|
+
* Trailing slashes are normalized.
|
|
33
|
+
*/
|
|
34
|
+
dataUrl: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Override for per-case JSON URLs. Default: `${dataUrl}/cases/<id>.json`.
|
|
38
|
+
*/
|
|
39
|
+
caseUrl?: (caseId: string) => string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* URL to the directory containing the viewer's compiled `assets/*.js`
|
|
43
|
+
* files. Required if you don't drop a `<link data-kensho-viewer-assets="…">`
|
|
44
|
+
* marker in your <head>.
|
|
45
|
+
*/
|
|
46
|
+
assetsUrl?: string;
|
|
47
|
+
|
|
48
|
+
/** Fired when a case is opened (deep-link integration). */
|
|
49
|
+
onCaseOpen?: (caseId: string | null) => void;
|
|
50
|
+
|
|
51
|
+
/** Fired when the user navigates to a different sidebar page. */
|
|
52
|
+
onPageChange?: (page: string) => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Optional extra sidebar items rendered after the built-ins.
|
|
56
|
+
*/
|
|
57
|
+
extraSidebar?: KenshoSidebarItem[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Optional extra tabs injected at the end of the test detail tab list.
|
|
61
|
+
*/
|
|
62
|
+
extraTabs?: KenshoExtraTab[];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initial state — useful for SSR / deep-linked URLs.
|
|
66
|
+
*/
|
|
67
|
+
initial?: {
|
|
68
|
+
page?: string;
|
|
69
|
+
caseId?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* When `true`, the viewer suppresses its own hash-router and keyboard
|
|
74
|
+
* shortcuts. The host must update `initial.page` / `initial.caseId` (or
|
|
75
|
+
* remount) in response to the `onPageChange` / `onCaseOpen` callbacks.
|
|
76
|
+
*/
|
|
77
|
+
ownKeyboard?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare function KenshoViewer(props: KenshoViewerProps): JSX.Element;
|
|
81
|
+
export default KenshoViewer;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pure data loader — fetches `${dataUrl}/index.json` and normalizes the
|
|
85
|
+
* Kensho v1 schema into the shape the viewer expects. Exposed for hosts that
|
|
86
|
+
* want to introspect the loaded data themselves.
|
|
87
|
+
*/
|
|
88
|
+
export declare function loadKenshoData(
|
|
89
|
+
dataUrl: string,
|
|
90
|
+
opts?: {
|
|
91
|
+
caseUrl?: (caseId: string) => string;
|
|
92
|
+
fetch?: typeof fetch;
|
|
93
|
+
}
|
|
94
|
+
): Promise<KenshoState>;
|
|
95
|
+
|
|
96
|
+
export interface KenshoState {
|
|
97
|
+
kenshoIndex: any;
|
|
98
|
+
reportType: 'unit' | 'e2e' | 'mixed' | string;
|
|
99
|
+
run: any;
|
|
100
|
+
env: Array<[string, string]>;
|
|
101
|
+
suites: Array<{ name: string; segs: Array<{ k: string; n: number }>; total: number }>;
|
|
102
|
+
tests: any[];
|
|
103
|
+
richTests: Record<string, any>;
|
|
104
|
+
suiteTree: any[];
|
|
105
|
+
behaviorTree: any[];
|
|
106
|
+
categories: any[];
|
|
107
|
+
timelineTests: any[];
|
|
108
|
+
trendRuns: any[];
|
|
109
|
+
histogram: Array<{ label: string; n: number }>;
|
|
110
|
+
historyRuns: any[];
|
|
111
|
+
ensureCaseLoaded: (richTest: any) => Promise<any>;
|
|
112
|
+
loadCase: (id: string) => Promise<any>;
|
|
113
|
+
fmtDuration: (ms: number) => string;
|
|
114
|
+
relTime: (iso: string) => string;
|
|
115
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// =============================================================
|
|
2
|
+
// <KenshoViewer /> — embeddable React component.
|
|
3
|
+
//
|
|
4
|
+
// Same code path as the static report, but:
|
|
5
|
+
// * Fetches data from the URL passed in `props.dataUrl` (instead of
|
|
6
|
+
// hard-coded "data/").
|
|
7
|
+
// * Suppresses hash-router and keyboard shortcuts when the host opts in
|
|
8
|
+
// (`ownKeyboard: true`), bubbling navigation events to callbacks.
|
|
9
|
+
// * Lets the host inject extra sidebar items + extra detail-pane tabs.
|
|
10
|
+
//
|
|
11
|
+
// The static report itself does NOT load this file. It loads the original
|
|
12
|
+
// pre-compiled `assets/*.js` that read from window globals.
|
|
13
|
+
// =============================================================
|
|
14
|
+
|
|
15
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
16
|
+
import { loadKenshoData } from './data.js';
|
|
17
|
+
|
|
18
|
+
// ---- Context ------------------------------------------------------------
|
|
19
|
+
//
|
|
20
|
+
// Components in the static-report bundle continue to read window.RICH_TESTS
|
|
21
|
+
// etc. directly — they aren't aware of this context. The context is only
|
|
22
|
+
// consumed by:
|
|
23
|
+
//
|
|
24
|
+
// * Sidebar/DetailPane — to discover extraSidebar / extraTabs
|
|
25
|
+
// * The internal navigation shims — to bypass hash routing when
|
|
26
|
+
// `ownKeyboard` is set.
|
|
27
|
+
//
|
|
28
|
+
// When useKenshoCtx() returns null (= component used outside a provider, ie.
|
|
29
|
+
// the static-report path), callers fall back to window globals as before.
|
|
30
|
+
|
|
31
|
+
const KenshoContext = React.createContext(null);
|
|
32
|
+
|
|
33
|
+
export function useKenshoCtx() {
|
|
34
|
+
return React.useContext(KenshoContext);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Make it accessible to the global components (they live in window.*) so
|
|
38
|
+
// Sidebar/DetailPane can opt in.
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
window.__KenshoContext = KenshoContext;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- Snapshot of the rendered <App> tree --------------------------------
|
|
44
|
+
//
|
|
45
|
+
// `App`, `Sidebar`, `DetailPane`, etc. are defined by the legacy assets/*
|
|
46
|
+
// scripts (loaded as plain <script> tags by index.html). When this component
|
|
47
|
+
// is bundled for embedding via esbuild, those scripts are NOT loaded.
|
|
48
|
+
//
|
|
49
|
+
// We need *some* way to render the same UI from inside the bundle. There
|
|
50
|
+
// are two viable shapes:
|
|
51
|
+
//
|
|
52
|
+
// A) Re-implement <App> here. Lots of duplication, drifts over time.
|
|
53
|
+
//
|
|
54
|
+
// B) Resolve <App> from window at mount time. Requires the host to load
|
|
55
|
+
// the legacy assets up front. Bigger ask on integrators.
|
|
56
|
+
//
|
|
57
|
+
// We pick (C): the component bundle dynamically injects the legacy compiled
|
|
58
|
+
// scripts into the host page (one-time, idempotent) and then renders the
|
|
59
|
+
// resolved `window.App`. This keeps a single source of truth and makes the
|
|
60
|
+
// embed a true mirror of the static-report. See `loadLegacyScripts()` below.
|
|
61
|
+
|
|
62
|
+
const LEGACY_SCRIPTS = [
|
|
63
|
+
'data-loader.js', // exposes window.__KENSHO_LOAD_DATA
|
|
64
|
+
'components.js',
|
|
65
|
+
'charts.js',
|
|
66
|
+
'test-detail.js',
|
|
67
|
+
'tree-detail.js',
|
|
68
|
+
'pages.js',
|
|
69
|
+
'app.js',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Track which (assetsBaseUrl, scriptName) pairs we've injected so a re-mount
|
|
73
|
+
// with the same base doesn't double-load.
|
|
74
|
+
const _injected = new Set();
|
|
75
|
+
|
|
76
|
+
function injectScript(url) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const existing = document.querySelector(`script[data-kv-src="${url}"]`);
|
|
79
|
+
if (existing) {
|
|
80
|
+
if (existing.dataset.kvLoaded === '1') return resolve();
|
|
81
|
+
existing.addEventListener('load', () => resolve(), { once: true });
|
|
82
|
+
existing.addEventListener('error', () => reject(new Error('failed: ' + url)), { once: true });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const s = document.createElement('script');
|
|
86
|
+
s.src = url;
|
|
87
|
+
s.dataset.kvSrc = url;
|
|
88
|
+
s.async = false; // preserve order
|
|
89
|
+
s.addEventListener('load', () => { s.dataset.kvLoaded = '1'; resolve(); }, { once: true });
|
|
90
|
+
s.addEventListener('error', () => reject(new Error('failed: ' + url)), { once: true });
|
|
91
|
+
document.head.appendChild(s);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function loadLegacyAssets(assetsBaseUrl) {
|
|
96
|
+
const base = String(assetsBaseUrl).replace(/\/+$/, '');
|
|
97
|
+
for (const name of LEGACY_SCRIPTS) {
|
|
98
|
+
const url = `${base}/${name}`;
|
|
99
|
+
if (_injected.has(url)) continue;
|
|
100
|
+
await injectScript(url);
|
|
101
|
+
_injected.add(url);
|
|
102
|
+
}
|
|
103
|
+
// Lucide icons — same as the static report's index.html.
|
|
104
|
+
if (!window.lucide) {
|
|
105
|
+
await injectScript('https://unpkg.com/lucide@latest/dist/umd/lucide.min.js');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `app.js`'s top-level code calls window.__KENSHO_BOOT.then(() => ReactDOM.createRoot(...).render(<App/>)).
|
|
110
|
+
// We don't want THAT to happen — we render <App/> ourselves into our own
|
|
111
|
+
// container. So before `app.js` evaluates, install a shim that swallows the
|
|
112
|
+
// auto-mount.
|
|
113
|
+
function installNoAutoMount() {
|
|
114
|
+
// Resolve __KENSHO_BOOT immediately with `null`. The `.then(() => render…)`
|
|
115
|
+
// body will run, but our shim throws before it can render.
|
|
116
|
+
if (!window.__KENSHO_BOOT) window.__KENSHO_BOOT = Promise.resolve();
|
|
117
|
+
// Swap out ReactDOM.createRoot with a noop the first time it's accessed
|
|
118
|
+
// FROM the legacy app.js. Easiest: monkey-patch `getElementById` to return
|
|
119
|
+
// null for the legacy "app" id, which makes createRoot throw — caught by
|
|
120
|
+
// app.js's `.catch` (it doesn't have one!). So instead we replace
|
|
121
|
+
// ReactDOM.createRoot during the brief window the script evaluates.
|
|
122
|
+
const dom = window.ReactDOM;
|
|
123
|
+
if (!dom) {
|
|
124
|
+
// ReactDOM isn't on window in embed mode (we use the host's react-dom
|
|
125
|
+
// import). Provide a stub so app.js's `ReactDOM.createRoot(...).render`
|
|
126
|
+
// is a noop.
|
|
127
|
+
window.ReactDOM = { createRoot: () => ({ render() {}, unmount() {} }) };
|
|
128
|
+
}
|
|
129
|
+
// Same for React (legacy uses `const { useState, useEffect } = React;`).
|
|
130
|
+
if (!window.React) {
|
|
131
|
+
window.React = React;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Restore real ReactDOM/React after the legacy scripts have evaluated, so
|
|
136
|
+
// we don't permanently mess with the global. (We installed stubs only; if
|
|
137
|
+
// the host had its own globals, we leave them alone via the if-checks
|
|
138
|
+
// above.)
|
|
139
|
+
|
|
140
|
+
// ---- The component itself ------------------------------------------------
|
|
141
|
+
|
|
142
|
+
export function KenshoViewer(props) {
|
|
143
|
+
const {
|
|
144
|
+
dataUrl,
|
|
145
|
+
caseUrl,
|
|
146
|
+
assetsUrl, // optional: where to load the viewer's compiled JS from. Default: same package.
|
|
147
|
+
onCaseOpen,
|
|
148
|
+
onPageChange,
|
|
149
|
+
extraSidebar,
|
|
150
|
+
extraTabs,
|
|
151
|
+
initial,
|
|
152
|
+
ownKeyboard = false,
|
|
153
|
+
} = props;
|
|
154
|
+
|
|
155
|
+
if (!dataUrl) throw new Error('<KenshoViewer dataUrl="..." /> is required');
|
|
156
|
+
|
|
157
|
+
const containerRef = useRef(null);
|
|
158
|
+
const [phase, setPhase] = useState('boot'); // 'boot' | 'loading' | 'ready' | 'error'
|
|
159
|
+
const [errMsg, setErrMsg] = useState('');
|
|
160
|
+
const [state, setState] = useState(null);
|
|
161
|
+
|
|
162
|
+
// Resolve the URL to load the legacy compiled scripts from. By default we
|
|
163
|
+
// assume the host bundled this component; the legacy assets live in the
|
|
164
|
+
// same package. The host can override via `assetsUrl` if they self-host.
|
|
165
|
+
const resolvedAssetsUrl = assetsUrl || guessDefaultAssetsUrl();
|
|
166
|
+
|
|
167
|
+
// 1. Inject the legacy compiled scripts (one-time).
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
let cancelled = false;
|
|
170
|
+
setPhase('loading');
|
|
171
|
+
installNoAutoMount();
|
|
172
|
+
loadLegacyAssets(resolvedAssetsUrl)
|
|
173
|
+
.then(() => loadKenshoData(dataUrl, { caseUrl: caseUrl ? (id) => caseUrl(id) : undefined }))
|
|
174
|
+
.then(s => {
|
|
175
|
+
if (cancelled) return;
|
|
176
|
+
// Push the loaded state onto window.* so the legacy components keep
|
|
177
|
+
// working. (They were written before the context refactor.) This is
|
|
178
|
+
// the documented limitation: 1 embedded viewer per page. Re-mount
|
|
179
|
+
// with a different `dataUrl` works because we re-write the globals.
|
|
180
|
+
applyToWindow(s);
|
|
181
|
+
setState(s);
|
|
182
|
+
setPhase('ready');
|
|
183
|
+
})
|
|
184
|
+
.catch(err => {
|
|
185
|
+
if (cancelled) return;
|
|
186
|
+
console.error('[KenshoViewer] failed to boot:', err);
|
|
187
|
+
setErrMsg(err?.message || String(err));
|
|
188
|
+
setPhase('error');
|
|
189
|
+
});
|
|
190
|
+
return () => { cancelled = true; };
|
|
191
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
|
+
}, [dataUrl]); // intentionally don't depend on caseUrl/assetsUrl — they're stable in practice.
|
|
193
|
+
|
|
194
|
+
// (Step 2 removed.) Navigation is bridged inside the legacy <App>: when its
|
|
195
|
+
// context says `ownKeyboard: true` it calls `ctx.onPageChange` /
|
|
196
|
+
// `ctx.onCaseOpen` instead of pushing to window.location.hash. See
|
|
197
|
+
// assets/app.jsx. We don't override window.__navTo from out here because
|
|
198
|
+
// that would skip the legacy App's local page state update.
|
|
199
|
+
|
|
200
|
+
// 3. Mount the legacy <App/> into our container once data is ready.
|
|
201
|
+
// We do it imperatively because <App/> lives on window and isn't a
|
|
202
|
+
// JSX-importable export.
|
|
203
|
+
//
|
|
204
|
+
// Important: we mount ONCE per `state` (data reload) and re-render the
|
|
205
|
+
// same root with a fresh `ctxValue` when extras / callbacks change. If
|
|
206
|
+
// we unmounted on every prop change, the legacy App's local state
|
|
207
|
+
// (selected page, search filters, splitter width, …) would be wiped.
|
|
208
|
+
const rootRef = useRef(null);
|
|
209
|
+
// (a) Mount on phase=ready / data changes.
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (phase !== 'ready' || !state || !containerRef.current) return;
|
|
212
|
+
const App = window.App;
|
|
213
|
+
if (typeof App !== 'function') {
|
|
214
|
+
console.error('[KenshoViewer] window.App not found after legacy load. Build out of date?');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let cancelled = false;
|
|
218
|
+
import('react-dom/client').then(({ createRoot }) => {
|
|
219
|
+
if (cancelled) return;
|
|
220
|
+
const root = createRoot(containerRef.current);
|
|
221
|
+
rootRef.current = root;
|
|
222
|
+
renderLegacy(root);
|
|
223
|
+
});
|
|
224
|
+
return () => {
|
|
225
|
+
cancelled = true;
|
|
226
|
+
try { rootRef.current?.unmount(); } catch {}
|
|
227
|
+
rootRef.current = null;
|
|
228
|
+
};
|
|
229
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
230
|
+
}, [phase, state]);
|
|
231
|
+
|
|
232
|
+
// (b) Re-render with fresh ctxValue whenever extras / callbacks change.
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (rootRef.current) renderLegacy(rootRef.current);
|
|
235
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
236
|
+
}, [extraSidebar, extraTabs, onCaseOpen, onPageChange, ownKeyboard]);
|
|
237
|
+
|
|
238
|
+
function renderLegacy(root) {
|
|
239
|
+
const App = window.App;
|
|
240
|
+
if (typeof App !== 'function') return;
|
|
241
|
+
const ctxValue = {
|
|
242
|
+
state,
|
|
243
|
+
extraSidebar: extraSidebar || [],
|
|
244
|
+
extraTabs: extraTabs || [],
|
|
245
|
+
onCaseOpen,
|
|
246
|
+
onPageChange,
|
|
247
|
+
ownKeyboard,
|
|
248
|
+
// `page` / `caseId` here are the host-controlled values from
|
|
249
|
+
// `props.initial`. The legacy App reads `ctx?.page` only as an
|
|
250
|
+
// optional one-way sync source (see app.jsx). Local navigation
|
|
251
|
+
// inside the viewer keeps using the App's own `useState`.
|
|
252
|
+
page: initial?.page,
|
|
253
|
+
caseId: initial?.caseId,
|
|
254
|
+
};
|
|
255
|
+
const legacyApp = React.createElement(App, null);
|
|
256
|
+
root.render(
|
|
257
|
+
React.createElement(KenshoContext.Provider, { value: ctxValue }, legacyApp)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 4. Suppress legacy keyboard shortcuts when the host opts in. The legacy
|
|
262
|
+
// shortcut handler is registered on `window` from `app.jsx`. We can't
|
|
263
|
+
// easily un-register it, so instead capture all keydown events at the
|
|
264
|
+
// container and stop propagation before they bubble to window.
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (!ownKeyboard) return;
|
|
267
|
+
const node = containerRef.current;
|
|
268
|
+
if (!node) return;
|
|
269
|
+
const onKeyDown = (e) => {
|
|
270
|
+
// Don't swallow chords inside text inputs the host may add.
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
};
|
|
273
|
+
node.addEventListener('keydown', onKeyDown, true);
|
|
274
|
+
return () => node.removeEventListener('keydown', onKeyDown, true);
|
|
275
|
+
}, [ownKeyboard, phase]);
|
|
276
|
+
|
|
277
|
+
if (phase === 'error') {
|
|
278
|
+
return React.createElement(
|
|
279
|
+
'div',
|
|
280
|
+
{ className: 'kv-embed-error', style: { padding: 24, color: '#E5484D', fontFamily: 'sans-serif' } },
|
|
281
|
+
React.createElement('h3', null, 'Failed to load Kensho report'),
|
|
282
|
+
React.createElement('pre', { style: { whiteSpace: 'pre-wrap', background: '#fcebec', padding: 12, borderRadius: 6 } }, errMsg)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return React.createElement('div', {
|
|
287
|
+
ref: containerRef,
|
|
288
|
+
className: 'kv-embed-root',
|
|
289
|
+
'data-kensho-viewer': '',
|
|
290
|
+
style: { width: '100%', height: '100%', minHeight: 480 },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Where do we load `assets/*.js` from?
|
|
295
|
+
// * If the host imported from `@kaizenreport/kensho-viewer/component`, we
|
|
296
|
+
// don't have a direct way to know the package URL.
|
|
297
|
+
// * Convention: the host should serve the package's `assets/` directory
|
|
298
|
+
// (e.g. via Vite's static asset import) and pass `assetsUrl` explicitly.
|
|
299
|
+
// * As a sane default, we look for a <script data-kensho-viewer-assets>
|
|
300
|
+
// marker tag the host can drop in their <head>.
|
|
301
|
+
function guessDefaultAssetsUrl() {
|
|
302
|
+
if (typeof document === 'undefined') return '/kensho-viewer-assets';
|
|
303
|
+
const marker = document.querySelector('[data-kensho-viewer-assets]');
|
|
304
|
+
if (marker) return marker.getAttribute('data-kensho-viewer-assets') || marker.getAttribute('href');
|
|
305
|
+
// Last-resort default — relative to the page.
|
|
306
|
+
return './kensho-viewer-assets';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Apply a loaded `KenshoState` onto window globals so the legacy components
|
|
310
|
+
// can read them. Mirrors the assignments in `assets/data-bridge.jsx`.
|
|
311
|
+
function applyToWindow(state) {
|
|
312
|
+
Object.assign(window, {
|
|
313
|
+
KENSHO_INDEX: state.kenshoIndex,
|
|
314
|
+
KENSHO_REPORT_TYPE: state.reportType,
|
|
315
|
+
RUN: state.run,
|
|
316
|
+
ENV: state.env,
|
|
317
|
+
SUITES: state.suites,
|
|
318
|
+
TESTS: state.tests,
|
|
319
|
+
RICH_TESTS: state.richTests,
|
|
320
|
+
SUITE_TREE: state.suiteTree,
|
|
321
|
+
BEHAVIOR_TREE: state.behaviorTree,
|
|
322
|
+
CATEGORIES: state.categories,
|
|
323
|
+
TIMELINE_TESTS: state.timelineTests,
|
|
324
|
+
TREND_RUNS: state.trendRuns,
|
|
325
|
+
HISTOGRAM: state.histogram,
|
|
326
|
+
HISTORY_RUNS: state.historyRuns,
|
|
327
|
+
_kenshoEnsureCase: state.ensureCaseLoaded,
|
|
328
|
+
_kenshoLoadCase: state.loadCase,
|
|
329
|
+
_kenshoFmtDuration: state.fmtDuration,
|
|
330
|
+
_kenshoRelTime: state.relTime,
|
|
331
|
+
});
|
|
332
|
+
// Mark __KENSHO_BOOT as resolved so any late-arriving legacy code path
|
|
333
|
+
// (which awaits it) proceeds without waiting for a real fetch.
|
|
334
|
+
window.__KENSHO_BOOT = Promise.resolve();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Re-export for advanced consumers.
|
|
338
|
+
export { loadKenshoData };
|
|
339
|
+
|
|
340
|
+
export default KenshoViewer;
|