@graphenedata/cli 0.0.7 → 0.0.8
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/cli.ts +2 -0
- package/dist/cli/cli.js +122 -62
- package/dist/ui/app.css +33 -2
- package/dist/ui/component-utilities/echarts.js +10 -1
- package/dist/ui/internal/NavSidebar.svelte +383 -0
- package/dist/ui/internal/telemetry.ts +2 -1
- package/dist/ui/public/inter-latin-ext.woff2 +0 -0
- package/dist/ui/public/inter-latin.woff2 +0 -0
- package/dist/ui/web.js +15 -16
- package/package.json +5 -5
- package/dist/ui/playwright.config.ts +0 -30
- /package/dist/ui/{assets → public}/favicon.ico +0 -0
package/cli.ts
CHANGED
|
@@ -64,10 +64,12 @@ program.command('schema')
|
|
|
64
64
|
|
|
65
65
|
// figure out if you're wanting to list tables in a schema/dataset
|
|
66
66
|
let dsToList: string | null = null
|
|
67
|
+
let parts = tableArg ? tableArg.split('.') : []
|
|
67
68
|
if (datasets.includes(tableArg)) dsToList = tableArg // you gave the name of a dataset
|
|
68
69
|
else if (!tableArg && datasets.length == 1) dsToList = datasets[0] // only one dataset, and no args
|
|
69
70
|
else if (!tableArg && config.namespace) dsToList = config.namespace // default namespace configured
|
|
70
71
|
else if (!tableArg && config.dialect == 'duckdb') dsToList = '<default>'
|
|
72
|
+
else if (tableArg && config.dialect == 'snowflake' && parts.length == 2) dsToList = tableArg
|
|
71
73
|
|
|
72
74
|
if (dsToList) {
|
|
73
75
|
let tables = await connection.listTables(dsToList)
|
package/dist/cli/cli.js
CHANGED
|
@@ -150,15 +150,17 @@ function setConfig(cfg) {
|
|
|
150
150
|
else if (cfg.snowflake) dialect = "snowflake";
|
|
151
151
|
else if (cfg.duckdb) dialect = "duckdb";
|
|
152
152
|
Object.keys(config).forEach((key) => delete config[key]);
|
|
153
|
-
Object.assign(config, cfg
|
|
153
|
+
Object.assign(config, cfg);
|
|
154
|
+
config.dialect = dialect;
|
|
155
|
+
config.root ||= process.cwd();
|
|
156
|
+
config.ignoredFiles ||= ["agents.md", "claude.md"];
|
|
154
157
|
}
|
|
155
158
|
function loadConfig(dir) {
|
|
156
159
|
if (config.root) return;
|
|
157
160
|
let packageJsonObject = {};
|
|
158
161
|
try {
|
|
159
162
|
let txt2 = fs.readFileSync(path.join(dir, "package.json"), "utf8");
|
|
160
|
-
|
|
161
|
-
packageJsonObject = all.graphene || {};
|
|
163
|
+
packageJsonObject = JSON.parse(txt2).graphene || {};
|
|
162
164
|
} catch {
|
|
163
165
|
console.warn("No package.json found in current directory");
|
|
164
166
|
}
|
|
@@ -729,7 +731,7 @@ function addColumnField(table2, node) {
|
|
|
729
731
|
table2.primaryKey = name;
|
|
730
732
|
}
|
|
731
733
|
let type = convertDataType(txt(node.getChild("DataType")));
|
|
732
|
-
if (!type) diag(node, `Unsupported data type: ${txt(node.getChild("DataType"))}`);
|
|
734
|
+
if (!type) return diag(node, `Unsupported data type: ${txt(node.getChild("DataType"))}`);
|
|
733
735
|
addFieldToTable(table2, { name, type, metadata: extractLeadingMetadata(node) }, node);
|
|
734
736
|
}
|
|
735
737
|
function addJoinField(table2, node) {
|
|
@@ -1115,7 +1117,7 @@ function lookupTable(name, node) {
|
|
|
1115
1117
|
}
|
|
1116
1118
|
}
|
|
1117
1119
|
function clearWorkspace() {
|
|
1118
|
-
FILE_MAP
|
|
1120
|
+
Object.keys(FILE_MAP).forEach((k) => delete FILE_MAP[k]);
|
|
1119
1121
|
TABLE_NODE_MAP = /* @__PURE__ */ new WeakMap();
|
|
1120
1122
|
diagnostics = [];
|
|
1121
1123
|
}
|
|
@@ -1583,7 +1585,7 @@ function getDiagnostics() {
|
|
|
1583
1585
|
return diagnostics;
|
|
1584
1586
|
}
|
|
1585
1587
|
async function loadWorkspace(dir, includeMd) {
|
|
1586
|
-
let files = await glob(includeMd ? "**/*.{gsql,md}" : "**/*.gsql", { cwd: dir, ignore: ["node_modules/**"] });
|
|
1588
|
+
let files = await glob(includeMd ? "**/*.{gsql,md}" : "**/*.gsql", { cwd: dir, ignore: ["node_modules/**"], follow: false });
|
|
1587
1589
|
for await (let file of files) {
|
|
1588
1590
|
try {
|
|
1589
1591
|
let contents = await readFile(path2.join(dir, file), "utf-8");
|
|
@@ -1634,10 +1636,16 @@ function toSql(query, params = {}) {
|
|
|
1634
1636
|
query = structuredClone(query);
|
|
1635
1637
|
fillInParams(query, params);
|
|
1636
1638
|
if (config.dialect == "snowflake") uppercaseMalloyQuery(query);
|
|
1637
|
-
let
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1639
|
+
let visited = /* @__PURE__ */ new Set();
|
|
1640
|
+
function setStructRefs(obj) {
|
|
1641
|
+
if (!obj || typeof obj !== "object" || visited.has(obj)) return;
|
|
1642
|
+
visited.add(obj);
|
|
1643
|
+
if (obj.baseTableName && obj.pipeline) obj.structRef = contents[obj.baseTableName];
|
|
1644
|
+
if (obj.query) setStructRefs(obj.query);
|
|
1645
|
+
if (obj.fields) for (let f of obj.fields) setStructRefs(f);
|
|
1646
|
+
}
|
|
1647
|
+
Object.values(contents).forEach(setStructRefs);
|
|
1648
|
+
setStructRefs(query);
|
|
1641
1649
|
let qm = new QueryModel({
|
|
1642
1650
|
name: "generated_model",
|
|
1643
1651
|
contents,
|
|
@@ -1896,6 +1904,9 @@ async function check(options) {
|
|
|
1896
1904
|
log("No errors found \u{1F48E}");
|
|
1897
1905
|
return true;
|
|
1898
1906
|
}
|
|
1907
|
+
if (process.env.NODE_ENV == "test" && mdFile) {
|
|
1908
|
+
delete FILE_MAP[mdFile];
|
|
1909
|
+
}
|
|
1899
1910
|
let host = `http://localhost:${config.port || Number(process.env.GRAPHENE_PORT) || 4e3}`;
|
|
1900
1911
|
let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "").replace(/\\/g, "/");
|
|
1901
1912
|
if (pageUrl === "/index") pageUrl = "/";
|
|
@@ -2046,6 +2057,7 @@ var init_check = __esm({
|
|
|
2046
2057
|
init_mockFiles();
|
|
2047
2058
|
init_background();
|
|
2048
2059
|
init_util();
|
|
2060
|
+
init_analyze();
|
|
2049
2061
|
browserConnections = [];
|
|
2050
2062
|
pendingRequests = {};
|
|
2051
2063
|
}
|
|
@@ -2382,9 +2394,6 @@ __export(snowflake_exports, {
|
|
|
2382
2394
|
});
|
|
2383
2395
|
import { createPrivateKey } from "node:crypto";
|
|
2384
2396
|
import snowflake from "snowflake-sdk";
|
|
2385
|
-
function getSnowflakeNamespace() {
|
|
2386
|
-
throw new Error("Not yet implemented");
|
|
2387
|
-
}
|
|
2388
2397
|
function snowflakeIdent(value) {
|
|
2389
2398
|
if (!value) throw new Error("Snowflake identifiers cannot be empty");
|
|
2390
2399
|
return `"${value.replace(/"/g, '""')}"`;
|
|
@@ -2453,34 +2462,32 @@ var init_snowflake2 = __esm({
|
|
|
2453
2462
|
});
|
|
2454
2463
|
}
|
|
2455
2464
|
async listDatasets() {
|
|
2456
|
-
await
|
|
2457
|
-
|
|
2465
|
+
let res = await this.runQuery("show databases");
|
|
2466
|
+
return res.rows.map((row) => String(row["name"] || ""));
|
|
2458
2467
|
}
|
|
2459
|
-
async listTables() {
|
|
2460
|
-
let
|
|
2461
|
-
let
|
|
2462
|
-
let
|
|
2468
|
+
async listTables(dataset) {
|
|
2469
|
+
let parts = dataset.split(".");
|
|
2470
|
+
let database = parts.shift() || "";
|
|
2471
|
+
let schema = parts.join(".");
|
|
2472
|
+
let res = await this.runQuery(`
|
|
2463
2473
|
select table_schema as "table_schema", table_name as "table_name"
|
|
2464
|
-
from ${
|
|
2465
|
-
where table_type in ('BASE TABLE', 'VIEW')
|
|
2466
|
-
order by
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
return res.rows.map((row) => String(row["table_name"] || ""));
|
|
2474
|
+
from ${snowflakeIdent(database)}.INFORMATION_SCHEMA.TABLES
|
|
2475
|
+
where table_type in ('BASE TABLE', 'VIEW') and table_schema = ${sqlStringLiteral3(schema)}
|
|
2476
|
+
order by table_name
|
|
2477
|
+
`);
|
|
2478
|
+
return res.rows.map((row) => `${row["table_schema"]}.${row["table_name"]}`);
|
|
2470
2479
|
}
|
|
2471
2480
|
async describeTable(target) {
|
|
2472
2481
|
let parts = target.split(".");
|
|
2473
|
-
let table2 = parts.pop() || "";
|
|
2474
2482
|
let database = parts.shift() || "";
|
|
2483
|
+
let table2 = parts.pop() || "";
|
|
2475
2484
|
let schema = parts.join(".");
|
|
2476
|
-
let
|
|
2477
|
-
let sql = `
|
|
2485
|
+
let res = await this.runQuery(`
|
|
2478
2486
|
select column_name as "column_name", data_type as "data_type", ordinal_position as ordinal_position
|
|
2479
|
-
from ${
|
|
2487
|
+
from ${snowflakeIdent(database)}.INFORMATION_SCHEMA.COLUMNS
|
|
2480
2488
|
where upper(table_schema) = upper(${sqlStringLiteral3(schema)}) and upper(table_name) = upper(${sqlStringLiteral3(table2)})
|
|
2481
2489
|
order by ordinal_position
|
|
2482
|
-
|
|
2483
|
-
let res = await this.runQuery(sql);
|
|
2490
|
+
`);
|
|
2484
2491
|
return res.rows.map((row) => {
|
|
2485
2492
|
return { name: String(row["column_name"]), dataType: String(row["data_type"]) };
|
|
2486
2493
|
});
|
|
@@ -2618,20 +2625,28 @@ var init_mdCompile = __esm({
|
|
|
2618
2625
|
// serve2.ts
|
|
2619
2626
|
var serve2_exports = {};
|
|
2620
2627
|
__export(serve2_exports, {
|
|
2628
|
+
prepareDeps: () => prepareDeps,
|
|
2621
2629
|
serve2: () => serve2
|
|
2622
2630
|
});
|
|
2623
|
-
import { createServer, optimizeDeps } from "vite";
|
|
2631
|
+
import { createServer, optimizeDeps, resolveConfig } from "vite";
|
|
2624
2632
|
import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
2625
2633
|
import fs7 from "fs-extra";
|
|
2634
|
+
import { glob as glob2 } from "glob";
|
|
2626
2635
|
import crypto2 from "crypto";
|
|
2627
2636
|
import { mdsvex } from "mdsvex";
|
|
2628
2637
|
import path8 from "path";
|
|
2629
2638
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2630
2639
|
async function serve2() {
|
|
2640
|
+
let server = await createServer(await createConfig());
|
|
2641
|
+
await server.listen();
|
|
2642
|
+
console.log(`Server running at http://localhost:${server.config.server.port}`);
|
|
2643
|
+
return server;
|
|
2644
|
+
}
|
|
2645
|
+
async function createConfig() {
|
|
2631
2646
|
uiRoot = path8.join(fileURLToPath2(import.meta.url), "../../ui");
|
|
2632
2647
|
let port = Number(process.env.GRAPHENE_PORT) || 4e3;
|
|
2633
2648
|
await fs7.ensureDir(path8.resolve(config.root, "node_modules/.graphene"));
|
|
2634
|
-
|
|
2649
|
+
return {
|
|
2635
2650
|
root: config.root,
|
|
2636
2651
|
plugins: [
|
|
2637
2652
|
svelte({
|
|
@@ -2646,11 +2661,15 @@ async function serve2() {
|
|
|
2646
2661
|
injectComponentImports()
|
|
2647
2662
|
]
|
|
2648
2663
|
}),
|
|
2664
|
+
fixSvelteDepsInTests(),
|
|
2649
2665
|
checkVitePlugin(),
|
|
2650
2666
|
handleRequestPlugin,
|
|
2651
2667
|
updateWorkspacePlugin,
|
|
2652
2668
|
mockFilesForTests()
|
|
2653
2669
|
],
|
|
2670
|
+
publicDir: path8.resolve(uiRoot, "public"),
|
|
2671
|
+
// on the fence about this one. This would make it less likely we need to optimize when alternating between dev and tests.
|
|
2672
|
+
// cacheDir: process.env.NODE_ENV == 'test' ? 'node_modules/.vite-tests' : 'node_modules/.vite',
|
|
2654
2673
|
server: {
|
|
2655
2674
|
port,
|
|
2656
2675
|
fs: { strict: false },
|
|
@@ -2660,16 +2679,38 @@ async function serve2() {
|
|
|
2660
2679
|
alias: {
|
|
2661
2680
|
graphene: path8.resolve(uiRoot, "web.js")
|
|
2662
2681
|
}
|
|
2682
|
+
},
|
|
2683
|
+
// vite's pre-bundling won't naturally discover these dependencies since they're transitive.
|
|
2684
|
+
// Instead, we need to list them out here so vite knows where they are.
|
|
2685
|
+
optimizeDeps: {
|
|
2686
|
+
noDiscovery: process.env.NODE_ENV == "test",
|
|
2687
|
+
// tests manually optimize before starting test workers
|
|
2688
|
+
include: [
|
|
2689
|
+
"@graphenedata/cli > svelte",
|
|
2690
|
+
"@graphenedata/cli > ssf",
|
|
2691
|
+
"@graphenedata/cli > @tidyjs/tidy",
|
|
2692
|
+
"@graphenedata/cli > chroma-js",
|
|
2693
|
+
"@graphenedata/cli > echarts/dist/echarts.esm.js",
|
|
2694
|
+
"@graphenedata/cli > @graphenedata/html2canvas"
|
|
2695
|
+
]
|
|
2663
2696
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
async function prepareDeps() {
|
|
2700
|
+
let cfg = await resolveConfig(await createConfig(), "serve");
|
|
2701
|
+
await optimizeDeps(cfg, true);
|
|
2702
|
+
}
|
|
2703
|
+
function fixSvelteDepsInTests() {
|
|
2704
|
+
let viteConfig;
|
|
2705
|
+
function configResolved(cfg) {
|
|
2706
|
+
viteConfig = cfg;
|
|
2707
|
+
}
|
|
2708
|
+
function buildStart() {
|
|
2709
|
+
if (process.env.NODE_ENV != "test") return;
|
|
2710
|
+
viteConfig.optimizeDeps.force = false;
|
|
2711
|
+
}
|
|
2712
|
+
buildStart.sequential = true;
|
|
2713
|
+
return { name: "fix-svelte-deps", enforce: "pre", sequential: true, configResolved, buildStart };
|
|
2673
2714
|
}
|
|
2674
2715
|
async function handleQuery(req, res) {
|
|
2675
2716
|
let chunks = [];
|
|
@@ -2700,7 +2741,7 @@ async function handleQuery(req, res) {
|
|
|
2700
2741
|
async function handlePage(server, res, filePath, mount) {
|
|
2701
2742
|
res.setHeader("Content-Type", "text/html");
|
|
2702
2743
|
let mdMount = mount ? `
|
|
2703
|
-
import Page from ${JSON.stringify(filePath)}
|
|
2744
|
+
import Page from ${JSON.stringify(filePath)}
|
|
2704
2745
|
new Page({ target: document.getElementById('content'), props: {} })
|
|
2705
2746
|
` : "";
|
|
2706
2747
|
let html = await server.transformIndexHtml(filePath, `<!doctype html>
|
|
@@ -2710,14 +2751,10 @@ async function handlePage(server, res, filePath, mount) {
|
|
|
2710
2751
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2711
2752
|
<title>Graphene</title>
|
|
2712
2753
|
<link rel="icon" href="/favicon.ico" />
|
|
2713
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
2714
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
2715
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
|
2716
2754
|
</head>
|
|
2717
2755
|
<body>
|
|
2718
|
-
<
|
|
2719
|
-
|
|
2720
|
-
</main>
|
|
2756
|
+
<nav id="nav"></nav>
|
|
2757
|
+
<main id="content"></main>
|
|
2721
2758
|
<script type="module">
|
|
2722
2759
|
import 'graphene' // do this first so we can track errors caused by importing the md file
|
|
2723
2760
|
${mdMount}
|
|
@@ -2736,11 +2773,12 @@ function mockFilesForTests() {
|
|
|
2736
2773
|
},
|
|
2737
2774
|
load(id) {
|
|
2738
2775
|
if (!id.endsWith("?mock")) return null;
|
|
2739
|
-
|
|
2776
|
+
let content = mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
|
|
2777
|
+
return content;
|
|
2740
2778
|
}
|
|
2741
2779
|
};
|
|
2742
2780
|
}
|
|
2743
|
-
var uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin;
|
|
2781
|
+
var uiRoot, workspaceLoadPromise, mdFiles, updateWorkspacePlugin, handleRequestPlugin;
|
|
2744
2782
|
var init_serve2 = __esm({
|
|
2745
2783
|
"serve2.ts"() {
|
|
2746
2784
|
"use strict";
|
|
@@ -2749,15 +2787,38 @@ var init_serve2 = __esm({
|
|
|
2749
2787
|
init_mdCompile();
|
|
2750
2788
|
init_check();
|
|
2751
2789
|
init_mockFiles();
|
|
2790
|
+
mdFiles = [];
|
|
2752
2791
|
updateWorkspacePlugin = {
|
|
2753
2792
|
name: "updateWorkspace",
|
|
2793
|
+
resolveId(id) {
|
|
2794
|
+
if (id == "virtual:nav") return "\0virtual:nav";
|
|
2795
|
+
},
|
|
2796
|
+
load(id) {
|
|
2797
|
+
if (id != "\0virtual:nav") return;
|
|
2798
|
+
let allFiles = [...mdFiles];
|
|
2799
|
+
if (process.env.NODE_ENV == "test") {
|
|
2800
|
+
for (let key of Object.keys(mockFileMap)) {
|
|
2801
|
+
if (!allFiles.includes(key)) allFiles.push(key);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return `export default ${JSON.stringify(allFiles)}`;
|
|
2805
|
+
},
|
|
2754
2806
|
configureServer: (s) => {
|
|
2755
|
-
|
|
2756
|
-
s.watcher.on("change", () => {
|
|
2807
|
+
let refresh = async () => {
|
|
2757
2808
|
clearWorkspace();
|
|
2758
2809
|
workspaceLoadPromise = loadWorkspace(config.root, false);
|
|
2759
|
-
|
|
2760
|
-
|
|
2810
|
+
mdFiles = await glob2("**/*.md", { cwd: config.root, ignore: ["node_modules/**"] });
|
|
2811
|
+
mdFiles = mdFiles.filter((f) => !config.ignoredFiles?.includes(path8.basename(f).toLowerCase()));
|
|
2812
|
+
if (process.env.NODE_ENV == "test") {
|
|
2813
|
+
mdFiles.push(...Object.keys(mockFileMap));
|
|
2814
|
+
}
|
|
2815
|
+
let mod = s.moduleGraph.getModuleById("\0virtual:nav");
|
|
2816
|
+
if (!mod) return;
|
|
2817
|
+
s.reloadModule(mod);
|
|
2818
|
+
};
|
|
2819
|
+
s.watcher.add(["**/*.gsql", "**/*.md"]);
|
|
2820
|
+
s.watcher.on("all", refresh);
|
|
2821
|
+
refresh();
|
|
2761
2822
|
}
|
|
2762
2823
|
};
|
|
2763
2824
|
handleRequestPlugin = {
|
|
@@ -2768,19 +2829,16 @@ var init_serve2 = __esm({
|
|
|
2768
2829
|
let [pathName] = (req.url || "").split("?");
|
|
2769
2830
|
if (pathName == "/_api/query") return await handleQuery(req, res);
|
|
2770
2831
|
if (pathName == "/__ct") return await handlePage(s, res, "__ct", false);
|
|
2771
|
-
if (pathName == "/favicon.ico") {
|
|
2772
|
-
res.setHeader("Content-Type", "image/x-icon");
|
|
2773
|
-
return res.end(await fs7.readFile(path8.resolve(uiRoot, "assets/favicon.ico")));
|
|
2774
|
-
}
|
|
2775
2832
|
if (!pathName || pathName == "/") pathName = "index";
|
|
2776
|
-
let
|
|
2777
|
-
|
|
2833
|
+
let relativeMdPath = pathName.replace(/^\//, "") + ".md";
|
|
2834
|
+
let mdPath = path8.join(config.root, relativeMdPath);
|
|
2835
|
+
if (mockFileMap[relativeMdPath] || await fs7.exists(mdPath)) {
|
|
2778
2836
|
await handlePage(s, res, mdPath, true);
|
|
2779
2837
|
} else {
|
|
2780
2838
|
next();
|
|
2781
2839
|
}
|
|
2782
2840
|
} catch (err) {
|
|
2783
|
-
console.error(err);
|
|
2841
|
+
if (process.env.NODE_ENV != "test") console.error(err);
|
|
2784
2842
|
res.statusCode = 500;
|
|
2785
2843
|
res.end(JSON.stringify([{ message: err.message, stack: err.stack }]));
|
|
2786
2844
|
}
|
|
@@ -2835,10 +2893,12 @@ program.command("schema").description("Inspect database tables or describe a tab
|
|
|
2835
2893
|
${datasets.join("\n")}`);
|
|
2836
2894
|
}
|
|
2837
2895
|
let dsToList = null;
|
|
2896
|
+
let parts = tableArg ? tableArg.split(".") : [];
|
|
2838
2897
|
if (datasets.includes(tableArg)) dsToList = tableArg;
|
|
2839
2898
|
else if (!tableArg && datasets.length == 1) dsToList = datasets[0];
|
|
2840
2899
|
else if (!tableArg && config.namespace) dsToList = config.namespace;
|
|
2841
2900
|
else if (!tableArg && config.dialect == "duckdb") dsToList = "<default>";
|
|
2901
|
+
else if (tableArg && config.dialect == "snowflake" && parts.length == 2) dsToList = tableArg;
|
|
2842
2902
|
if (dsToList) {
|
|
2843
2903
|
let tables = await connection.listTables(dsToList);
|
|
2844
2904
|
return console.log(`Tables${dsToList ? ` in ${dsToList}` : ""}:
|
package/dist/ui/app.css
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
@font-face { /* latin-ext */
|
|
3
|
+
font-family: 'Inter';
|
|
4
|
+
font-style: normal;
|
|
5
|
+
font-weight: 100 900;
|
|
6
|
+
font-display: swap;
|
|
7
|
+
src: url(/inter-latin-ext.woff2) format('woff2');
|
|
8
|
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@font-face { /* latin */
|
|
12
|
+
font-family: 'Inter';
|
|
13
|
+
font-style: normal;
|
|
14
|
+
font-weight: 100 900;
|
|
15
|
+
font-display: swap;
|
|
16
|
+
src: url(/inter-latin.woff2) format('woff2');
|
|
17
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
:root {
|
|
4
21
|
/* Layout */
|
|
@@ -97,10 +114,24 @@ html {
|
|
|
97
114
|
body {
|
|
98
115
|
font-family: "Inter", var(--ui-font-family);
|
|
99
116
|
line-height: 1.7;
|
|
117
|
+
margin: 0;
|
|
118
|
+
color: var(--base-heading);
|
|
119
|
+
min-height: 100vh;
|
|
120
|
+
display: flex;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
nav {
|
|
124
|
+
flex: 0 0 200px;
|
|
125
|
+
height: 100vh;
|
|
126
|
+
padding-top: 1rem;
|
|
127
|
+
border-right: 1px solid var(--base-200);
|
|
128
|
+
background: #fbfbfd;
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
main {
|
|
103
|
-
|
|
132
|
+
flex: 1;
|
|
133
|
+
max-width: none;
|
|
134
|
+
padding: 2.5rem 3rem 3.5rem;
|
|
104
135
|
margin: 0 auto;
|
|
105
136
|
}
|
|
106
137
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {registerTheme, init, connect} from 'echarts/dist/echarts.esm.js'
|
|
2
2
|
import {evidenceThemeDark, evidenceThemeLight} from './echartsThemes'
|
|
3
|
-
import debounce from 'debounce'
|
|
4
3
|
import * as chartWindowDebug from './chartWindowDebug'
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -261,4 +260,14 @@ const echartsAction = (node, options) => {
|
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
262
|
|
|
263
|
+
const debounce = (callback, wait) => {
|
|
264
|
+
let timeoutId = null
|
|
265
|
+
return (...args) => {
|
|
266
|
+
window.clearTimeout(timeoutId)
|
|
267
|
+
timeoutId = window.setTimeout(() => {
|
|
268
|
+
callback(...args)
|
|
269
|
+
}, wait)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
264
273
|
export default echartsAction
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import navData from 'virtual:nav'
|
|
3
|
+
|
|
4
|
+
export let currentFile = ''
|
|
5
|
+
|
|
6
|
+
let tree = []
|
|
7
|
+
let flatNodes = []
|
|
8
|
+
let openFolders = new Set()
|
|
9
|
+
let treeSignature = ''
|
|
10
|
+
let lastCurrent = ''
|
|
11
|
+
|
|
12
|
+
$: normalizedFiles = (navData || [])
|
|
13
|
+
.map((file) => file.replace(/^\.\//, '').replace(/\\/g, '/'))
|
|
14
|
+
|
|
15
|
+
$: normalizedCurrent = deriveCurrentFile()
|
|
16
|
+
$: currentRoute = normalizedCurrent ? pathToRoute(normalizedCurrent) : '/'
|
|
17
|
+
|
|
18
|
+
function deriveCurrentFile () {
|
|
19
|
+
let fromProp = normalizeFilePath(currentFile)
|
|
20
|
+
let route = getLocationRoute()
|
|
21
|
+
if (route) {
|
|
22
|
+
let match = normalizedFiles.find((file) => pathToRoute(file) === route)
|
|
23
|
+
if (match) return match
|
|
24
|
+
}
|
|
25
|
+
return fromProp
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeFilePath (filePath) {
|
|
29
|
+
return (filePath || '').replace(/^\.\//, '').replace(/\\/g, '/')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getLocationRoute () {
|
|
33
|
+
if (typeof window === 'undefined') return null
|
|
34
|
+
let route = window.location.pathname || '/'
|
|
35
|
+
route = route.replace(/\/+$/, '') || '/'
|
|
36
|
+
return route
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$: {
|
|
40
|
+
let nextSignature = normalizedFiles.join('|')
|
|
41
|
+
if (nextSignature !== treeSignature) {
|
|
42
|
+
treeSignature = nextSignature
|
|
43
|
+
tree = buildTree(normalizedFiles)
|
|
44
|
+
flatNodes = flattenTree(tree)
|
|
45
|
+
openFolders = createDefaultOpenFolders(tree, normalizedCurrent)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$: {
|
|
50
|
+
if (normalizedCurrent !== lastCurrent) {
|
|
51
|
+
openFolders = mergeAncestorFolders(openFolders, normalizedCurrent)
|
|
52
|
+
lastCurrent = normalizedCurrent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toggleFolder (path) {
|
|
57
|
+
if (!path) return
|
|
58
|
+
let next = new Set(openFolders)
|
|
59
|
+
if (next.has(path)) next.delete(path)
|
|
60
|
+
else next.add(path)
|
|
61
|
+
openFolders = next
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleFolderRowKey (event, path) {
|
|
65
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
66
|
+
event.preventDefault()
|
|
67
|
+
toggleFolder(path)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isOpen (path, openSet = openFolders) {
|
|
71
|
+
if (!path) return true
|
|
72
|
+
return openSet.has(path)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isVisible (node, openSet = openFolders) {
|
|
76
|
+
return node.ancestors.every((path) => isOpen(path, openSet))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildTree (paths) {
|
|
80
|
+
let root = []
|
|
81
|
+
let folderMap = new Map()
|
|
82
|
+
|
|
83
|
+
for (let filePath of paths) {
|
|
84
|
+
let cleanPath = filePath.replace(/^\.\//, '').replace(/^\//, '')
|
|
85
|
+
let segments = cleanPath.split('/')
|
|
86
|
+
if (!segments.length) continue
|
|
87
|
+
let fileName = segments.pop()
|
|
88
|
+
let parentChildren = root
|
|
89
|
+
let parentPath = ''
|
|
90
|
+
|
|
91
|
+
for (let segment of segments) {
|
|
92
|
+
parentPath = parentPath ? `${parentPath}/${segment}` : segment
|
|
93
|
+
if (!folderMap.has(parentPath)) {
|
|
94
|
+
let folderNode = {
|
|
95
|
+
type: 'folder',
|
|
96
|
+
name: segment,
|
|
97
|
+
label: formatLabel(segment, 'folder'),
|
|
98
|
+
path: parentPath,
|
|
99
|
+
children: [],
|
|
100
|
+
route: null,
|
|
101
|
+
}
|
|
102
|
+
folderMap.set(parentPath, folderNode)
|
|
103
|
+
parentChildren.push(folderNode)
|
|
104
|
+
}
|
|
105
|
+
parentChildren = folderMap.get(parentPath).children
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!fileName) continue
|
|
109
|
+
let fullPath = parentPath ? `${parentPath}/${fileName}` : fileName
|
|
110
|
+
|
|
111
|
+
if (fileName.toLowerCase() === 'index.md' && parentPath) {
|
|
112
|
+
let folderNode = folderMap.get(parentPath)
|
|
113
|
+
if (folderNode) folderNode.route = pathToRoute(fullPath)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let exists = parentChildren.find((node) => node.path === fullPath)
|
|
118
|
+
if (exists) continue
|
|
119
|
+
parentChildren.push({
|
|
120
|
+
type: 'file',
|
|
121
|
+
name: fileName,
|
|
122
|
+
label: formatLabel(fileName, 'file'),
|
|
123
|
+
path: fullPath,
|
|
124
|
+
route: pathToRoute(fullPath),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return sortNodes(root)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sortNodes (nodes) {
|
|
132
|
+
return nodes
|
|
133
|
+
.map((node) => {
|
|
134
|
+
if (node.type === 'folder' && node.children?.length) {
|
|
135
|
+
return {...node, children: sortNodes(node.children)}
|
|
136
|
+
}
|
|
137
|
+
return node
|
|
138
|
+
})
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
if (a.label === 'Home') return -1
|
|
141
|
+
if (b.label === 'Home') return 1
|
|
142
|
+
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
|
|
143
|
+
return a.label.localeCompare(b.label)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function flattenTree (nodes, depth = 0, ancestors = []) {
|
|
148
|
+
let list = []
|
|
149
|
+
for (let node of nodes) {
|
|
150
|
+
if (node.type === 'folder') {
|
|
151
|
+
let entry = {...node, depth, ancestors}
|
|
152
|
+
list.push(entry)
|
|
153
|
+
if (node.children?.length) {
|
|
154
|
+
list.push(...flattenTree(node.children, depth + 1, [...ancestors, node.path]))
|
|
155
|
+
}
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
list.push({...node, depth, ancestors})
|
|
159
|
+
}
|
|
160
|
+
return list
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createDefaultOpenFolders (_treeNodes, currentPath) {
|
|
164
|
+
let next = new Set()
|
|
165
|
+
return mergeAncestorFolders(next, currentPath)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function mergeAncestorFolders (openSet, filePath) {
|
|
169
|
+
if (!filePath) return new Set(openSet)
|
|
170
|
+
let parts = filePath.split('/')
|
|
171
|
+
parts.pop()
|
|
172
|
+
let aggregate = []
|
|
173
|
+
let next = new Set(openSet)
|
|
174
|
+
for (let part of parts) {
|
|
175
|
+
aggregate.push(part)
|
|
176
|
+
next.add(aggregate.join('/'))
|
|
177
|
+
}
|
|
178
|
+
return next
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatLabel (value, type) {
|
|
182
|
+
let cleaned = type === 'file' ? value.replace(/\.md$/, '') : value
|
|
183
|
+
if (cleaned.toLowerCase() === 'index') return 'Home'
|
|
184
|
+
return cleaned
|
|
185
|
+
.split(/[\s_-]+/)
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1))
|
|
188
|
+
.join(' ')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pathToRoute (path) {
|
|
192
|
+
let clean = path.replace(/\.md$/, '')
|
|
193
|
+
if (!clean || clean === 'index') return '/'
|
|
194
|
+
return '/' + clean
|
|
195
|
+
}
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<ul>
|
|
199
|
+
{#each flatNodes as node (node.path)}
|
|
200
|
+
{#if node.type === 'folder'}
|
|
201
|
+
<li class={isVisible(node, openFolders) ? '' : 'hidden'} style={`--depth:${node.depth}`} data-folder={node.path}>
|
|
202
|
+
<div
|
|
203
|
+
class={node.route ? 'folder-row' : 'folder-row clickable'}
|
|
204
|
+
role={node.route ? undefined : 'button'}
|
|
205
|
+
aria-expanded={node.route ? undefined : String(isOpen(node.path, openFolders))}
|
|
206
|
+
on:click={node.route ? undefined : () => toggleFolder(node.path)}
|
|
207
|
+
on:keydown={node.route ? undefined : (event) => handleFolderRowKey(event, node.path)}
|
|
208
|
+
>
|
|
209
|
+
<button
|
|
210
|
+
class="toggle"
|
|
211
|
+
type="button"
|
|
212
|
+
data-folder-toggle={node.path}
|
|
213
|
+
aria-expanded={isOpen(node.path, openFolders)}
|
|
214
|
+
on:click={(event) => { event.stopPropagation(); toggleFolder(node.path) }}
|
|
215
|
+
aria-label={(isOpen(node.path, openFolders) ? 'Collapse' : 'Expand') + ' ' + node.label}
|
|
216
|
+
>
|
|
217
|
+
<span class={isOpen(node.path, openFolders) ? 'chevron open' : 'chevron'}>▸</span>
|
|
218
|
+
</button>
|
|
219
|
+
{#if node.route}
|
|
220
|
+
<a
|
|
221
|
+
href={node.route}
|
|
222
|
+
class={node.route === currentRoute ? 'active' : ''}
|
|
223
|
+
aria-current={node.route === currentRoute ? 'page' : undefined}
|
|
224
|
+
>
|
|
225
|
+
{node.label}
|
|
226
|
+
</a>
|
|
227
|
+
{:else}
|
|
228
|
+
<span class="label">{node.label}</span>
|
|
229
|
+
{/if}
|
|
230
|
+
</div>
|
|
231
|
+
</li>
|
|
232
|
+
{:else}
|
|
233
|
+
<li class={isVisible(node, openFolders) ? 'file' : 'file hidden'} style={`--depth:${node.depth}`}>
|
|
234
|
+
<a
|
|
235
|
+
href={node.route}
|
|
236
|
+
class={node.path === normalizedCurrent ? 'active' : ''}
|
|
237
|
+
aria-current={node.path === normalizedCurrent ? 'page' : undefined}
|
|
238
|
+
>
|
|
239
|
+
<span>{node.label}</span>
|
|
240
|
+
</a>
|
|
241
|
+
</li>
|
|
242
|
+
{/if}
|
|
243
|
+
{/each}
|
|
244
|
+
</ul>
|
|
245
|
+
|
|
246
|
+
<style>
|
|
247
|
+
ul {
|
|
248
|
+
list-style: none;
|
|
249
|
+
padding: 0 0.5rem 0 0;
|
|
250
|
+
margin: 0;
|
|
251
|
+
display: flex;
|
|
252
|
+
flex-direction: column;
|
|
253
|
+
gap: 0.1rem;
|
|
254
|
+
overflow: hidden;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
li {
|
|
258
|
+
--indent: calc(var(--depth, 0) * 1rem);
|
|
259
|
+
padding-left: var(--indent);
|
|
260
|
+
width: 100%;
|
|
261
|
+
box-sizing: border-box;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
li.file {
|
|
265
|
+
padding-left: calc(var(--indent) + 1.5rem);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
li.hidden {
|
|
269
|
+
display: none;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.folder-row {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
padding: 0.1rem 0.15rem;
|
|
276
|
+
border-radius: 4px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.folder-row.clickable {
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.folder-row.clickable:focus-visible {
|
|
284
|
+
outline: 2px solid rgba(15, 23, 42, 0.2);
|
|
285
|
+
outline-offset: 2px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.toggle {
|
|
289
|
+
display: inline-flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
justify-content: center;
|
|
292
|
+
width: 1.5rem;
|
|
293
|
+
height: 1.5rem;
|
|
294
|
+
color: var(--base-heading);
|
|
295
|
+
background: transparent;
|
|
296
|
+
border: none;
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
border-radius: 4px;
|
|
299
|
+
opacity: 0;
|
|
300
|
+
pointer-events: none;
|
|
301
|
+
transition: opacity 120ms ease;
|
|
302
|
+
visibility: hidden;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.folder-row:hover .toggle,
|
|
306
|
+
.folder-row:focus-within .toggle,
|
|
307
|
+
.toggle:focus-visible {
|
|
308
|
+
opacity: 1;
|
|
309
|
+
pointer-events: auto;
|
|
310
|
+
visibility: visible;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.toggle:hover,
|
|
314
|
+
.toggle:focus-visible {
|
|
315
|
+
background: rgba(15, 23, 42, 0.1);
|
|
316
|
+
outline: none;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.chevron {
|
|
320
|
+
display: inline-block;
|
|
321
|
+
transition: transform 150ms ease;
|
|
322
|
+
font-size: 0.7rem;
|
|
323
|
+
color: var(--base-content-muted);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.chevron.open {
|
|
327
|
+
transform: rotate(90deg);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.label {
|
|
331
|
+
font-size: 0.85rem;
|
|
332
|
+
padding: 0.2rem 0.35rem;
|
|
333
|
+
white-space: nowrap;
|
|
334
|
+
overflow: hidden;
|
|
335
|
+
text-overflow: ellipsis;
|
|
336
|
+
color: var(--base-heading);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.folder-row a {
|
|
340
|
+
flex: 1;
|
|
341
|
+
display: block;
|
|
342
|
+
font-size: 0.85rem;
|
|
343
|
+
padding: 0.2rem 0.35rem;
|
|
344
|
+
border-radius: 4px;
|
|
345
|
+
color: var(--base-heading);
|
|
346
|
+
text-decoration: none;
|
|
347
|
+
white-space: nowrap;
|
|
348
|
+
overflow: hidden;
|
|
349
|
+
text-overflow: ellipsis;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.folder-row a:hover,
|
|
353
|
+
.folder-row a:focus-visible {
|
|
354
|
+
background: rgba(15, 23, 42, 0.05);
|
|
355
|
+
outline: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
li.file a {
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
font-size: 0.85rem;
|
|
362
|
+
padding: 0.2rem 0.5rem;
|
|
363
|
+
border-radius: 4px;
|
|
364
|
+
color: var(--base-heading);
|
|
365
|
+
text-decoration: none;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
li.file a span {
|
|
369
|
+
white-space: nowrap;
|
|
370
|
+
overflow: hidden;
|
|
371
|
+
text-overflow: ellipsis;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
li.file a:hover,
|
|
375
|
+
li.file a:focus-visible {
|
|
376
|
+
background: rgba(15, 23, 42, 0.05);
|
|
377
|
+
outline: none;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
a.active {
|
|
381
|
+
color: var(--base-900, #0f172a);
|
|
382
|
+
}
|
|
383
|
+
</style>
|
|
Binary file
|
|
Binary file
|
package/dist/ui/web.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {getErrors} from './internal/telemetry.ts'
|
|
2
2
|
import './app.css'
|
|
3
3
|
import {isLoading} from './internal/queryEngine.ts'
|
|
4
|
+
import NavSidebar from './internal/NavSidebar.svelte'
|
|
4
5
|
|
|
5
6
|
import Area from './components/Area.svelte'
|
|
6
7
|
import AreaChart from './components/AreaChart.svelte'
|
|
@@ -32,6 +33,8 @@ import TableSubtotalRow from './components/TableSubtotalRow.svelte'
|
|
|
32
33
|
import TableTotalRow from './components/TableTotalRow.svelte'
|
|
33
34
|
import TextInput from './components/TextInput.svelte'
|
|
34
35
|
|
|
36
|
+
window.$GRAPHENE = window.$GRAPHENE || {}
|
|
37
|
+
|
|
35
38
|
window.$GRAPHENE.components = {
|
|
36
39
|
Area,
|
|
37
40
|
AreaChart,
|
|
@@ -64,35 +67,28 @@ window.$GRAPHENE.components = {
|
|
|
64
67
|
TextInput,
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
|
|
68
70
|
let socket = null
|
|
69
71
|
|
|
72
|
+
if (document.getElementById('nav')) {
|
|
73
|
+
new NavSidebar({target: document.getElementById('nav')})
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
connectWebSocket()
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
await waitForQueriesToFinish()
|
|
74
|
-
let errors = getErrors()
|
|
78
|
+
function captureChart (chartTitle) {
|
|
75
79
|
let escaped = window.CSS.escape(chartTitle)
|
|
76
80
|
let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`)
|
|
77
|
-
|
|
78
|
-
if (!canvas) {
|
|
79
|
-
errors.push({message: `Could not find chart titled "${chartTitle}"`})
|
|
80
|
-
return {stillLoading: isLoading(), screenshot: null, errors}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {stillLoading: isLoading(), screenshot: canvas.toDataURL('image/png'), errors}
|
|
81
|
+
return canvas?.toDataURL('image/png')
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
async function takeScreenshot () {
|
|
87
|
-
await waitForQueriesToFinish()
|
|
88
85
|
if (!window.html2canvas) {
|
|
89
86
|
let html2canvas = await import('@graphenedata/html2canvas')
|
|
90
87
|
window.html2canvas = html2canvas.default
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
let canvas = await window.html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
|
|
94
|
-
|
|
95
|
-
return {stillLoading: isLoading(), screenshot: canvas?.toDataURL('image/png'), errors}
|
|
91
|
+
return canvas?.toDataURL('image/png')
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
async function waitForQueriesToFinish () {
|
|
@@ -115,8 +111,11 @@ function connectWebSocket () {
|
|
|
115
111
|
let {type, requestId, chart} = JSON.parse(event.data)
|
|
116
112
|
|
|
117
113
|
if (type === 'check') {
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
await waitForQueriesToFinish()
|
|
115
|
+
let errors = getErrors().map(e => ({message: e.message, id: e.id}))
|
|
116
|
+
let stillLoading = isLoading()
|
|
117
|
+
let screenshot = chart ? captureChart(chart) : await takeScreenshot()
|
|
118
|
+
socket.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading, screenshot}))
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
121
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"main": "cli.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Graphene Systems Inc",
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.8",
|
|
7
7
|
"license": "Elastic-2.0",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=16"
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"@types/sanitize-html": "^2.16.0",
|
|
59
59
|
"@types/ws": "^8.18.1",
|
|
60
60
|
"esbuild": "^0.21.5",
|
|
61
|
-
"vitest": "
|
|
61
|
+
"vitest": "4.0.15",
|
|
62
62
|
"vscode-languageserver-types": "^3.17.0"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
|
-
"build": "rm -rf dist && node ./esbuild.mjs",
|
|
66
|
-
"test": "vitest run
|
|
67
|
-
"test-one": "
|
|
65
|
+
"build": "rm -rf dist && rm -f *.tgz && node ./esbuild.mjs",
|
|
66
|
+
"test": "vitest run cli --root ..",
|
|
67
|
+
"test-one": "node ../scripts/turboTest.js",
|
|
68
68
|
"prepack": "pnpm run build"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import {defineConfig, devices} from '@playwright/test'
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
testDir: './tests',
|
|
5
|
-
outputDir: './tests/results',
|
|
6
|
-
timeout: 10_000,
|
|
7
|
-
expect: {
|
|
8
|
-
timeout: process.env.DEBUG ? 0 : 2_000,
|
|
9
|
-
toHaveScreenshot: {
|
|
10
|
-
pathTemplate: '{testDir}/snapshots/{testFilePath}/{arg}{ext}',
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
fullyParallel: false,
|
|
14
|
-
forbidOnly: !!process.env.CI,
|
|
15
|
-
retries: 0, // process.env.CI ? 1 : 0,
|
|
16
|
-
reporter: process.env.CI ? [['list'], ['github']] : 'list',
|
|
17
|
-
use: {
|
|
18
|
-
headless: true,
|
|
19
|
-
actionTimeout: 0,
|
|
20
|
-
trace: 'retain-on-failure',
|
|
21
|
-
video: 'off',
|
|
22
|
-
launchOptions: {devtools: !!process.env.DEBUG},
|
|
23
|
-
},
|
|
24
|
-
projects: [
|
|
25
|
-
{
|
|
26
|
-
name: 'chromium',
|
|
27
|
-
use: {...devices['Desktop Chrome'], browserName: 'chromium'},
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
})
|
|
File without changes
|