@graphenedata/cli 0.0.7 → 0.0.9
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 +6 -8
- package/dist/cli/cli.js +135 -105
- package/dist/docs/graphene.md +8 -8
- 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
|
@@ -24,8 +24,7 @@ program.hook('preAction', async () => {
|
|
|
24
24
|
loadConfig(process.cwd())
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
program
|
|
28
|
-
.command('compile')
|
|
27
|
+
program.command('compile')
|
|
29
28
|
.description('Translate a query to SQL and print it')
|
|
30
29
|
.argument('[input]', 'Path to file, a raw string, or "-" for stdin')
|
|
31
30
|
.action(async (input: string | undefined) => {
|
|
@@ -36,8 +35,7 @@ program
|
|
|
36
35
|
console.log(toSql(queries[0]))
|
|
37
36
|
})
|
|
38
37
|
|
|
39
|
-
program
|
|
40
|
-
.command('run')
|
|
38
|
+
program.command('run')
|
|
41
39
|
.description('Run a query against your database')
|
|
42
40
|
.argument('[input]', 'Path to file, a raw string, or "-" for stdin')
|
|
43
41
|
.action(async (input: string | undefined) => {
|
|
@@ -64,10 +62,12 @@ program.command('schema')
|
|
|
64
62
|
|
|
65
63
|
// figure out if you're wanting to list tables in a schema/dataset
|
|
66
64
|
let dsToList: string | null = null
|
|
65
|
+
let parts = tableArg ? tableArg.split('.') : []
|
|
67
66
|
if (datasets.includes(tableArg)) dsToList = tableArg // you gave the name of a dataset
|
|
68
67
|
else if (!tableArg && datasets.length == 1) dsToList = datasets[0] // only one dataset, and no args
|
|
69
68
|
else if (!tableArg && config.namespace) dsToList = config.namespace // default namespace configured
|
|
70
69
|
else if (!tableArg && config.dialect == 'duckdb') dsToList = '<default>'
|
|
70
|
+
else if (tableArg && config.dialect == 'snowflake' && parts.length == 2) dsToList = tableArg
|
|
71
71
|
|
|
72
72
|
if (dsToList) {
|
|
73
73
|
let tables = await connection.listTables(dsToList)
|
|
@@ -82,8 +82,7 @@ program.command('schema')
|
|
|
82
82
|
console.log(')')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
program
|
|
86
|
-
.command('serve')
|
|
85
|
+
program.command('serve')
|
|
87
86
|
.description('Run the local server')
|
|
88
87
|
.option('--bg', 'Run the server in the background')
|
|
89
88
|
.action(async (options: {bg?: boolean}) => {
|
|
@@ -101,8 +100,7 @@ program.command('stop')
|
|
|
101
100
|
.description('Stop the local server')
|
|
102
101
|
.action(async () => { await stopGrapheneIfRunning() })
|
|
103
102
|
|
|
104
|
-
program
|
|
105
|
-
.command('check')
|
|
103
|
+
program.command('check')
|
|
106
104
|
.description('Check the project for errors, optionally capturing a page screenshot')
|
|
107
105
|
.argument('[mdFile]', 'Markdown file to check (e.g., index.md)')
|
|
108
106
|
.option('-c, --chart <chartTitle>', 'Title of a specific chart to capture')
|
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
|
}
|
|
@@ -1504,63 +1506,33 @@ var init_markdown = __esm({
|
|
|
1504
1506
|
});
|
|
1505
1507
|
|
|
1506
1508
|
// ../lang/snowflake.ts
|
|
1507
|
-
function uppercaseMalloyQuery(query) {
|
|
1508
|
-
query.baseTableName = uppercaseIdentifier(query.baseTableName);
|
|
1509
|
-
for (let stage of query.pipeline || []) {
|
|
1510
|
-
let fields = stage.queryFields || [];
|
|
1511
|
-
fields.forEach((field) => uppercaseColumnField(field));
|
|
1512
|
-
let filters = stage.filterList || [];
|
|
1513
|
-
filters.forEach((filter) => uppercaseExpression(filter?.e));
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
1509
|
function uppercaseTable(table2) {
|
|
1517
1510
|
if (table2.upperCased) return;
|
|
1518
1511
|
table2.upperCased = true;
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
if (table2.tablePath) table2.tablePath = uppercaseQualified(table2.tablePath);
|
|
1523
|
-
table2.fields?.forEach((field) => uppercaseField(field));
|
|
1524
|
-
if (table2.query) uppercaseMalloyQuery(table2.query);
|
|
1512
|
+
if (table2.query) return;
|
|
1513
|
+
table2.tablePath = uppercaseIdentifier(table2.tablePath);
|
|
1514
|
+
table2.fields.forEach(uppercaseField);
|
|
1525
1515
|
}
|
|
1526
1516
|
function uppercaseField(field) {
|
|
1527
|
-
if (
|
|
1528
|
-
if (
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
if (field.structPath) field.structPath = field.structPath.map(uppercaseIdentifier);
|
|
1533
|
-
if (field.path) field.path = field.path.map(uppercaseIdentifier);
|
|
1534
|
-
if (field.onExpression) uppercaseExpression(field.onExpression);
|
|
1535
|
-
uppercaseTable(field);
|
|
1536
|
-
} else {
|
|
1537
|
-
uppercaseColumnField(field);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
function uppercaseColumnField(field) {
|
|
1541
|
-
if (!field) return;
|
|
1517
|
+
if (isJoinField(field)) return uppercaseTable(field);
|
|
1518
|
+
if (field.upperCased) return;
|
|
1519
|
+
field.upperCased = true;
|
|
1520
|
+
if (field.e) return uppercaseExpression(field.e);
|
|
1521
|
+
field.as = field.name;
|
|
1542
1522
|
field.name = uppercaseIdentifier(field.name);
|
|
1543
|
-
if (field.path) field.path = field.path.map(uppercaseIdentifier);
|
|
1544
|
-
if (field.structPath) field.structPath = field.structPath.map(uppercaseIdentifier);
|
|
1545
|
-
if (field.tableName) field.tableName = uppercaseQualified(field.tableName);
|
|
1546
|
-
if (field.tablePath) field.tablePath = uppercaseQualified(field.tablePath);
|
|
1547
|
-
if (field.e) uppercaseExpression(field.e);
|
|
1548
1523
|
}
|
|
1549
1524
|
function uppercaseExpression(expr) {
|
|
1550
1525
|
if (!expr) return;
|
|
1551
1526
|
walkExpression(expr, (node) => {
|
|
1552
|
-
if (
|
|
1553
|
-
|
|
1527
|
+
if (node.type == "field") {
|
|
1528
|
+
node.path = node.path.map(uppercaseIdentifier);
|
|
1529
|
+
}
|
|
1554
1530
|
});
|
|
1555
1531
|
}
|
|
1556
1532
|
function uppercaseIdentifier(value) {
|
|
1557
1533
|
if (!value) return value || "";
|
|
1558
1534
|
return value.toString().toUpperCase();
|
|
1559
1535
|
}
|
|
1560
|
-
function uppercaseQualified(value) {
|
|
1561
|
-
if (!value) return value;
|
|
1562
|
-
return value.split(".").map((part) => part.startsWith('"') && part.endsWith('"') ? part : uppercaseIdentifier(part)).join(".");
|
|
1563
|
-
}
|
|
1564
1536
|
function isJoinField(field) {
|
|
1565
1537
|
return !!field?.join;
|
|
1566
1538
|
}
|
|
@@ -1583,7 +1555,7 @@ function getDiagnostics() {
|
|
|
1583
1555
|
return diagnostics;
|
|
1584
1556
|
}
|
|
1585
1557
|
async function loadWorkspace(dir, includeMd) {
|
|
1586
|
-
let files = await glob(includeMd ? "**/*.{gsql,md}" : "**/*.gsql", { cwd: dir, ignore: ["node_modules/**"] });
|
|
1558
|
+
let files = await glob(includeMd ? "**/*.{gsql,md}" : "**/*.gsql", { cwd: dir, ignore: ["node_modules/**"], follow: false });
|
|
1587
1559
|
for await (let file of files) {
|
|
1588
1560
|
try {
|
|
1589
1561
|
let contents = await readFile(path2.join(dir, file), "utf-8");
|
|
@@ -1633,11 +1605,16 @@ function toSql(query, params = {}) {
|
|
|
1633
1605
|
}));
|
|
1634
1606
|
query = structuredClone(query);
|
|
1635
1607
|
fillInParams(query, params);
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1608
|
+
let visited = /* @__PURE__ */ new Set();
|
|
1609
|
+
function setStructRefs(obj) {
|
|
1610
|
+
if (!obj || typeof obj !== "object" || visited.has(obj)) return;
|
|
1611
|
+
visited.add(obj);
|
|
1612
|
+
if (obj.baseTableName && obj.pipeline) obj.structRef = contents[obj.baseTableName];
|
|
1613
|
+
if (obj.query) setStructRefs(obj.query);
|
|
1614
|
+
if (obj.fields) for (let f of obj.fields) setStructRefs(f);
|
|
1615
|
+
}
|
|
1616
|
+
Object.values(contents).forEach(setStructRefs);
|
|
1617
|
+
setStructRefs(query);
|
|
1641
1618
|
let qm = new QueryModel({
|
|
1642
1619
|
name: "generated_model",
|
|
1643
1620
|
contents,
|
|
@@ -1896,6 +1873,9 @@ async function check(options) {
|
|
|
1896
1873
|
log("No errors found \u{1F48E}");
|
|
1897
1874
|
return true;
|
|
1898
1875
|
}
|
|
1876
|
+
if (process.env.NODE_ENV == "test" && mdFile) {
|
|
1877
|
+
delete FILE_MAP[mdFile];
|
|
1878
|
+
}
|
|
1899
1879
|
let host = `http://localhost:${config.port || Number(process.env.GRAPHENE_PORT) || 4e3}`;
|
|
1900
1880
|
let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "").replace(/\\/g, "/");
|
|
1901
1881
|
if (pageUrl === "/index") pageUrl = "/";
|
|
@@ -2046,6 +2026,7 @@ var init_check = __esm({
|
|
|
2046
2026
|
init_mockFiles();
|
|
2047
2027
|
init_background();
|
|
2048
2028
|
init_util();
|
|
2029
|
+
init_analyze();
|
|
2049
2030
|
browserConnections = [];
|
|
2050
2031
|
pendingRequests = {};
|
|
2051
2032
|
}
|
|
@@ -2382,9 +2363,6 @@ __export(snowflake_exports, {
|
|
|
2382
2363
|
});
|
|
2383
2364
|
import { createPrivateKey } from "node:crypto";
|
|
2384
2365
|
import snowflake from "snowflake-sdk";
|
|
2385
|
-
function getSnowflakeNamespace() {
|
|
2386
|
-
throw new Error("Not yet implemented");
|
|
2387
|
-
}
|
|
2388
2366
|
function snowflakeIdent(value) {
|
|
2389
2367
|
if (!value) throw new Error("Snowflake identifiers cannot be empty");
|
|
2390
2368
|
return `"${value.replace(/"/g, '""')}"`;
|
|
@@ -2453,36 +2431,34 @@ var init_snowflake2 = __esm({
|
|
|
2453
2431
|
});
|
|
2454
2432
|
}
|
|
2455
2433
|
async listDatasets() {
|
|
2456
|
-
await
|
|
2457
|
-
|
|
2434
|
+
let res = await this.runQuery("show databases");
|
|
2435
|
+
return res.rows.map((row) => String(row["name"] || ""));
|
|
2458
2436
|
}
|
|
2459
|
-
async listTables() {
|
|
2460
|
-
let
|
|
2461
|
-
let
|
|
2462
|
-
let
|
|
2437
|
+
async listTables(dataset) {
|
|
2438
|
+
let parts = dataset.split(".");
|
|
2439
|
+
let database = parts.shift() || "";
|
|
2440
|
+
let schema = parts.join(".");
|
|
2441
|
+
let res = await this.runQuery(`
|
|
2463
2442
|
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"] || ""));
|
|
2443
|
+
from ${snowflakeIdent(database)}.INFORMATION_SCHEMA.TABLES
|
|
2444
|
+
where table_type in ('BASE TABLE', 'VIEW') and table_schema = ${sqlStringLiteral3(schema)}
|
|
2445
|
+
order by table_name
|
|
2446
|
+
`);
|
|
2447
|
+
return res.rows.map((row) => `${row["table_schema"]}.${row["table_name"]}`);
|
|
2470
2448
|
}
|
|
2471
2449
|
async describeTable(target) {
|
|
2472
2450
|
let parts = target.split(".");
|
|
2473
|
-
let table2 = parts.pop() || "";
|
|
2474
2451
|
let database = parts.shift() || "";
|
|
2452
|
+
let table2 = parts.pop() || "";
|
|
2475
2453
|
let schema = parts.join(".");
|
|
2476
|
-
let
|
|
2477
|
-
let sql = `
|
|
2454
|
+
let res = await this.runQuery(`
|
|
2478
2455
|
select column_name as "column_name", data_type as "data_type", ordinal_position as ordinal_position
|
|
2479
|
-
from ${
|
|
2456
|
+
from ${snowflakeIdent(database)}.INFORMATION_SCHEMA.COLUMNS
|
|
2480
2457
|
where upper(table_schema) = upper(${sqlStringLiteral3(schema)}) and upper(table_name) = upper(${sqlStringLiteral3(table2)})
|
|
2481
2458
|
order by ordinal_position
|
|
2482
|
-
|
|
2483
|
-
let res = await this.runQuery(sql);
|
|
2459
|
+
`);
|
|
2484
2460
|
return res.rows.map((row) => {
|
|
2485
|
-
return { name: String(row["column_name"]), dataType: String(row["data_type"]) };
|
|
2461
|
+
return { name: String(row["column_name"]).toLowerCase(), dataType: String(row["data_type"]) };
|
|
2486
2462
|
});
|
|
2487
2463
|
}
|
|
2488
2464
|
};
|
|
@@ -2618,20 +2594,28 @@ var init_mdCompile = __esm({
|
|
|
2618
2594
|
// serve2.ts
|
|
2619
2595
|
var serve2_exports = {};
|
|
2620
2596
|
__export(serve2_exports, {
|
|
2597
|
+
prepareDeps: () => prepareDeps,
|
|
2621
2598
|
serve2: () => serve2
|
|
2622
2599
|
});
|
|
2623
|
-
import { createServer, optimizeDeps } from "vite";
|
|
2600
|
+
import { createServer, optimizeDeps, resolveConfig } from "vite";
|
|
2624
2601
|
import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
2625
2602
|
import fs7 from "fs-extra";
|
|
2603
|
+
import { glob as glob2 } from "glob";
|
|
2626
2604
|
import crypto2 from "crypto";
|
|
2627
2605
|
import { mdsvex } from "mdsvex";
|
|
2628
2606
|
import path8 from "path";
|
|
2629
2607
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2630
2608
|
async function serve2() {
|
|
2609
|
+
let server = await createServer(await createConfig());
|
|
2610
|
+
await server.listen();
|
|
2611
|
+
console.log(`Server running at http://localhost:${server.config.server.port}`);
|
|
2612
|
+
return server;
|
|
2613
|
+
}
|
|
2614
|
+
async function createConfig() {
|
|
2631
2615
|
uiRoot = path8.join(fileURLToPath2(import.meta.url), "../../ui");
|
|
2632
2616
|
let port = Number(process.env.GRAPHENE_PORT) || 4e3;
|
|
2633
2617
|
await fs7.ensureDir(path8.resolve(config.root, "node_modules/.graphene"));
|
|
2634
|
-
|
|
2618
|
+
return {
|
|
2635
2619
|
root: config.root,
|
|
2636
2620
|
plugins: [
|
|
2637
2621
|
svelte({
|
|
@@ -2646,11 +2630,15 @@ async function serve2() {
|
|
|
2646
2630
|
injectComponentImports()
|
|
2647
2631
|
]
|
|
2648
2632
|
}),
|
|
2633
|
+
fixSvelteDepsInTests(),
|
|
2649
2634
|
checkVitePlugin(),
|
|
2650
2635
|
handleRequestPlugin,
|
|
2651
2636
|
updateWorkspacePlugin,
|
|
2652
2637
|
mockFilesForTests()
|
|
2653
2638
|
],
|
|
2639
|
+
publicDir: path8.resolve(uiRoot, "public"),
|
|
2640
|
+
// on the fence about this one. This would make it less likely we need to optimize when alternating between dev and tests.
|
|
2641
|
+
// cacheDir: process.env.NODE_ENV == 'test' ? 'node_modules/.vite-tests' : 'node_modules/.vite',
|
|
2654
2642
|
server: {
|
|
2655
2643
|
port,
|
|
2656
2644
|
fs: { strict: false },
|
|
@@ -2660,16 +2648,39 @@ async function serve2() {
|
|
|
2660
2648
|
alias: {
|
|
2661
2649
|
graphene: path8.resolve(uiRoot, "web.js")
|
|
2662
2650
|
}
|
|
2651
|
+
},
|
|
2652
|
+
// vite's pre-bundling won't naturally discover these dependencies since they're transitive.
|
|
2653
|
+
// Instead, we need to list them out here so vite knows where they are.
|
|
2654
|
+
optimizeDeps: {
|
|
2655
|
+
noDiscovery: process.env.NODE_ENV == "test",
|
|
2656
|
+
// tests manually optimize before starting test workers
|
|
2657
|
+
exclude: ["virtual:nav"],
|
|
2658
|
+
include: [
|
|
2659
|
+
"@graphenedata/cli > svelte",
|
|
2660
|
+
"@graphenedata/cli > ssf",
|
|
2661
|
+
"@graphenedata/cli > @tidyjs/tidy",
|
|
2662
|
+
"@graphenedata/cli > chroma-js",
|
|
2663
|
+
"@graphenedata/cli > echarts/dist/echarts.esm.js",
|
|
2664
|
+
"@graphenedata/cli > @graphenedata/html2canvas"
|
|
2665
|
+
]
|
|
2663
2666
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
async function prepareDeps() {
|
|
2670
|
+
let cfg = await resolveConfig(await createConfig(), "serve");
|
|
2671
|
+
await optimizeDeps(cfg, true);
|
|
2672
|
+
}
|
|
2673
|
+
function fixSvelteDepsInTests() {
|
|
2674
|
+
let viteConfig;
|
|
2675
|
+
function configResolved(cfg) {
|
|
2676
|
+
viteConfig = cfg;
|
|
2677
|
+
}
|
|
2678
|
+
function buildStart() {
|
|
2679
|
+
if (process.env.NODE_ENV != "test") return;
|
|
2680
|
+
viteConfig.optimizeDeps.force = false;
|
|
2681
|
+
}
|
|
2682
|
+
buildStart.sequential = true;
|
|
2683
|
+
return { name: "fix-svelte-deps", enforce: "pre", sequential: true, configResolved, buildStart };
|
|
2673
2684
|
}
|
|
2674
2685
|
async function handleQuery(req, res) {
|
|
2675
2686
|
let chunks = [];
|
|
@@ -2700,7 +2711,7 @@ async function handleQuery(req, res) {
|
|
|
2700
2711
|
async function handlePage(server, res, filePath, mount) {
|
|
2701
2712
|
res.setHeader("Content-Type", "text/html");
|
|
2702
2713
|
let mdMount = mount ? `
|
|
2703
|
-
import Page from ${JSON.stringify(filePath)}
|
|
2714
|
+
import Page from ${JSON.stringify(filePath)}
|
|
2704
2715
|
new Page({ target: document.getElementById('content'), props: {} })
|
|
2705
2716
|
` : "";
|
|
2706
2717
|
let html = await server.transformIndexHtml(filePath, `<!doctype html>
|
|
@@ -2710,14 +2721,10 @@ async function handlePage(server, res, filePath, mount) {
|
|
|
2710
2721
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2711
2722
|
<title>Graphene</title>
|
|
2712
2723
|
<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
2724
|
</head>
|
|
2717
2725
|
<body>
|
|
2718
|
-
<
|
|
2719
|
-
|
|
2720
|
-
</main>
|
|
2726
|
+
<nav id="nav"></nav>
|
|
2727
|
+
<main id="content"></main>
|
|
2721
2728
|
<script type="module">
|
|
2722
2729
|
import 'graphene' // do this first so we can track errors caused by importing the md file
|
|
2723
2730
|
${mdMount}
|
|
@@ -2736,11 +2743,12 @@ function mockFilesForTests() {
|
|
|
2736
2743
|
},
|
|
2737
2744
|
load(id) {
|
|
2738
2745
|
if (!id.endsWith("?mock")) return null;
|
|
2739
|
-
|
|
2746
|
+
let content = mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
|
|
2747
|
+
return content;
|
|
2740
2748
|
}
|
|
2741
2749
|
};
|
|
2742
2750
|
}
|
|
2743
|
-
var uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin;
|
|
2751
|
+
var uiRoot, workspaceLoadPromise, mdFiles, updateWorkspacePlugin, handleRequestPlugin;
|
|
2744
2752
|
var init_serve2 = __esm({
|
|
2745
2753
|
"serve2.ts"() {
|
|
2746
2754
|
"use strict";
|
|
@@ -2749,15 +2757,38 @@ var init_serve2 = __esm({
|
|
|
2749
2757
|
init_mdCompile();
|
|
2750
2758
|
init_check();
|
|
2751
2759
|
init_mockFiles();
|
|
2760
|
+
mdFiles = [];
|
|
2752
2761
|
updateWorkspacePlugin = {
|
|
2753
2762
|
name: "updateWorkspace",
|
|
2763
|
+
resolveId(id) {
|
|
2764
|
+
if (id == "virtual:nav") return "\0virtual:nav";
|
|
2765
|
+
},
|
|
2766
|
+
load(id) {
|
|
2767
|
+
if (id != "\0virtual:nav") return;
|
|
2768
|
+
let allFiles = [...mdFiles];
|
|
2769
|
+
if (process.env.NODE_ENV == "test") {
|
|
2770
|
+
for (let key of Object.keys(mockFileMap)) {
|
|
2771
|
+
if (!allFiles.includes(key)) allFiles.push(key);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
return `export default ${JSON.stringify(allFiles)}`;
|
|
2775
|
+
},
|
|
2754
2776
|
configureServer: (s) => {
|
|
2755
|
-
|
|
2756
|
-
s.watcher.on("change", () => {
|
|
2777
|
+
let refresh = async () => {
|
|
2757
2778
|
clearWorkspace();
|
|
2758
2779
|
workspaceLoadPromise = loadWorkspace(config.root, false);
|
|
2759
|
-
|
|
2760
|
-
|
|
2780
|
+
mdFiles = await glob2("**/*.md", { cwd: config.root, ignore: ["node_modules/**"] });
|
|
2781
|
+
mdFiles = mdFiles.filter((f) => !config.ignoredFiles?.includes(path8.basename(f).toLowerCase()));
|
|
2782
|
+
if (process.env.NODE_ENV == "test") {
|
|
2783
|
+
mdFiles.push(...Object.keys(mockFileMap));
|
|
2784
|
+
}
|
|
2785
|
+
let mod = s.moduleGraph.getModuleById("\0virtual:nav");
|
|
2786
|
+
if (!mod) return;
|
|
2787
|
+
s.reloadModule(mod);
|
|
2788
|
+
};
|
|
2789
|
+
s.watcher.add(["**/*.gsql", "**/*.md"]);
|
|
2790
|
+
s.watcher.on("all", refresh);
|
|
2791
|
+
refresh();
|
|
2761
2792
|
}
|
|
2762
2793
|
};
|
|
2763
2794
|
handleRequestPlugin = {
|
|
@@ -2768,19 +2799,16 @@ var init_serve2 = __esm({
|
|
|
2768
2799
|
let [pathName] = (req.url || "").split("?");
|
|
2769
2800
|
if (pathName == "/_api/query") return await handleQuery(req, res);
|
|
2770
2801
|
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
2802
|
if (!pathName || pathName == "/") pathName = "index";
|
|
2776
|
-
let
|
|
2777
|
-
|
|
2803
|
+
let relativeMdPath = pathName.replace(/^\//, "") + ".md";
|
|
2804
|
+
let mdPath = path8.join(config.root, relativeMdPath);
|
|
2805
|
+
if (mockFileMap[relativeMdPath] || await fs7.exists(mdPath)) {
|
|
2778
2806
|
await handlePage(s, res, mdPath, true);
|
|
2779
2807
|
} else {
|
|
2780
2808
|
next();
|
|
2781
2809
|
}
|
|
2782
2810
|
} catch (err) {
|
|
2783
|
-
console.error(err);
|
|
2811
|
+
if (process.env.NODE_ENV != "test") console.error(err);
|
|
2784
2812
|
res.statusCode = 500;
|
|
2785
2813
|
res.end(JSON.stringify([{ message: err.message, stack: err.stack }]));
|
|
2786
2814
|
}
|
|
@@ -2835,10 +2863,12 @@ program.command("schema").description("Inspect database tables or describe a tab
|
|
|
2835
2863
|
${datasets.join("\n")}`);
|
|
2836
2864
|
}
|
|
2837
2865
|
let dsToList = null;
|
|
2866
|
+
let parts = tableArg ? tableArg.split(".") : [];
|
|
2838
2867
|
if (datasets.includes(tableArg)) dsToList = tableArg;
|
|
2839
2868
|
else if (!tableArg && datasets.length == 1) dsToList = datasets[0];
|
|
2840
2869
|
else if (!tableArg && config.namespace) dsToList = config.namespace;
|
|
2841
2870
|
else if (!tableArg && config.dialect == "duckdb") dsToList = "<default>";
|
|
2871
|
+
else if (tableArg && config.dialect == "snowflake" && parts.length == 2) dsToList = tableArg;
|
|
2842
2872
|
if (dsToList) {
|
|
2843
2873
|
let tables = await connection.listTables(dsToList);
|
|
2844
2874
|
return console.log(`Tables${dsToList ? ` in ${dsToList}` : ""}:
|
package/dist/docs/graphene.md
CHANGED
|
@@ -575,8 +575,8 @@ Here's an example:
|
|
|
575
575
|
|----------|-------------|----------|---------|---------|
|
|
576
576
|
| data | Query name, wrapped in curly braces | true | query name | - |
|
|
577
577
|
| x | Column or expression to use for the x-axis of the chart | false | column name, stored expression name, GSQL expression | First column |
|
|
578
|
-
| y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | false | column name, stored expression name, GSQL expression, list of any combination of these | Any non-assigned numeric columns |
|
|
579
|
-
| y2 | Column(s) or expression(s) to include on a secondary y-axis. | false | column name, stored expression name, GSQL expression, list of any combination of these | - |
|
|
578
|
+
| y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | false | column name, stored expression name, GSQL expression, list of any combination of these e.g. `"col1, my_expr"` | Any non-assigned numeric columns |
|
|
579
|
+
| y2 | Column(s) or expression(s) to include on a secondary y-axis. | false | column name, stored expression name, GSQL expression, list of any combination of these e.g. `"col1, my_expr"` | - |
|
|
580
580
|
| y2SeriesType | Chart type to apply to the series on the y2 axis | false | `bar`, `line`, `scatter` | `bar` |
|
|
581
581
|
| series | Column or expression to use to define the series (groups) in a multi-series chart. Use when values of a particular column dictate the multiple series to plot, eg. `country` would create a series for every distinct country in the column. | false | column name, stored expression name, GSQL expression | - |
|
|
582
582
|
| sort | Whether to apply default sort to your data. Default sort is x ascending for number and date x-axes, and y descending for category x-axes | false | `true`, `false` | `true` |
|
|
@@ -598,7 +598,7 @@ Here's an example:
|
|
|
598
598
|
| outlineWidth | Width of line surrounding each bar | number | `0` |
|
|
599
599
|
| outlineColor | Color to use for outline if outlineWidth > 0 | CSS name, hexademical, RGB, HSL | - |
|
|
600
600
|
| colorPalette | List of custom colors to use for the chart | list of color strings (CSS name, hexademical, RGB, HSL) e.g. `"#cf0d06, #eb5752, #e88a87"` | built-in color palette |
|
|
601
|
-
| seriesOrder | Apply a specific order to the series in a multi-series chart. | list of series names in the order they should be used in the chart `"Canada, US"` | default order implied by the data |
|
|
601
|
+
| seriesOrder | Apply a specific order to the series in a multi-series chart. | list of series names in the order they should be used in the chart e.g. `"Canada, US"` | default order implied by the data |
|
|
602
602
|
| leftPadding | Number representing the padding (whitespace) on the left side of the chart. Useful to avoid labels getting cut off | number | - |
|
|
603
603
|
| rightPadding | Number representing the padding (whitespace) on the left side of the chart. Useful to avoid labels getting cut off | number | - |
|
|
604
604
|
| xLabelWrap | Whether to wrap x-axis labels when there is not enough space. Default behaviour is to truncate the labels. | `true`, `false` | `false` |
|
|
@@ -723,8 +723,8 @@ Here's an example:
|
|
|
723
723
|
|------|-------------|----------|---------|---------|
|
|
724
724
|
| data | Query name, wrapped in curly braces | true | query name | - |
|
|
725
725
|
| x | Column or expression to use for the x-axis of the chart | true | column name, stored expression name, GSQL expression | - |
|
|
726
|
-
| y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | true | column name, stored expression name, GSQL expression, list of any combination of these | - |
|
|
727
|
-
| y2 | Column(s) or expression(s) to include on a secondary y-axis. | false | column name, stored expression name, GSQL expression, list of any combination of these | - |
|
|
726
|
+
| y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | true | column name, stored expression name, GSQL expression, list of any combination of these e.g. `"col1, my_expr"` | - |
|
|
727
|
+
| y2 | Column(s) or expression(s) to include on a secondary y-axis. | false | column name, stored expression name, GSQL expression, list of any combination of these e.g. `"col1, my_expr"` | - |
|
|
728
728
|
| y2SeriesType | Chart type to apply to the series on the y2 axis | false | `line`, `bar`, `scatter` | `line` |
|
|
729
729
|
| series | Column or expression to use to define the series (groups) in a multi-series chart. Use when values of a particular column dictate the multiple series to plot, eg. `country` would create a series for every distinct country in the column. | false | column name, stored expression name, GSQL expression | - |
|
|
730
730
|
| sort | Whether to apply default sort to your data. Default is x ascending for number and date x-axes, and y descending for category x-axes | false | `true`, `false` | `true` |
|
|
@@ -750,7 +750,7 @@ Here's an example:
|
|
|
750
750
|
| markerShape | Shape to use if markers=true | false | `circle`, `emptyCircle`, `rect`, `triangle`, `diamond` | `circle` |
|
|
751
751
|
| markerSize | Size of each shape (in pixels) | false | number | `8` |
|
|
752
752
|
| colorPalette | List of custom colors to use for the chart | false | list of color strings (CSS name, hexademical, RGB, HSL) e.g. `"#cf0d06, #eb5752, #e88a87"` | - |
|
|
753
|
-
| seriesOrder | Apply a specific order to the series in a multi-series chart. | false | list of series names in the order they should be used in the chart `"Canada, US"` | default order implied by the data |
|
|
753
|
+
| seriesOrder | Apply a specific order to the series in a multi-series chart. | false | list of series names in the order they should be used in the chart e.g. `"Canada, US"` | default order implied by the data |
|
|
754
754
|
| labels | Show value labels | false | `true`, `false` | `false` |
|
|
755
755
|
| labelSize | Font size of value labels | false | number | `11` |
|
|
756
756
|
| labelPosition | Where label will appear on your series | false | `above`, `middle`, `below` | `above` |
|
|
@@ -832,7 +832,7 @@ Here's an example:
|
|
|
832
832
|
|------|-------------|----------|---------|---------|
|
|
833
833
|
| data | Query name, wrapped in curly braces | true | query name | - |
|
|
834
834
|
| x | Column or expression to use for the x-axis of the chart | true | column name, stored expression name, GSQL expression | First column |
|
|
835
|
-
| y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | true | column name, stored expression name, GSQL expression, list of any combination of these | Any non-assigned numeric columns |
|
|
835
|
+
| y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | true | column name, stored expression name, GSQL expression, list of any combination of these e.g. `"col1, my_expr"` | Any non-assigned numeric columns |
|
|
836
836
|
| series | Column or expression to use to define the series (groups) in a multi-series chart. Use when values of a particular column dictate the multiple series to plot, eg. `country` would create a series for every distinct country in the column. | false | column name, stored expression name, GSQL expression | - |
|
|
837
837
|
| sort | Whether to apply default sort to your data. Default sort is x ascending for number and date x-axes, and y descending for category x-axes | false | `true`, `false` | `true` |
|
|
838
838
|
| type | Grouping method to use for multi-series charts | false | `stacked`, `stacked100` | `stacked` |
|
|
@@ -854,7 +854,7 @@ Here's an example:
|
|
|
854
854
|
| fillOpacity | % of the full color that should be rendered, with remainder being transparent | false | number (0 to 1) | `0.7` |
|
|
855
855
|
| line | Show line on top of the area | false | `true`, `false` | `true` |
|
|
856
856
|
| colorPalette | List of custom colors to use for the chart | false | list of color strings (CSS name, hexademical, RGB, HSL) e.g. `"#cf0d06, #eb5752, #e88a87"` | built-in color palette |
|
|
857
|
-
| seriesOrder | Apply a specific order to the series in a multi-series chart. | false | list of series names in the order they should be used in the chart `"Canada, US"` | default order implied by the data |
|
|
857
|
+
| seriesOrder | Apply a specific order to the series in a multi-series chart. | false | list of series names in the order they should be used in the chart e.g. `"Canada, US"` | default order implied by the data |
|
|
858
858
|
| leftPadding | Number representing the padding (whitespace) on the left side of the chart. Useful to avoid labels getting cut off | false | number | - |
|
|
859
859
|
| rightPadding | Number representing the padding (whitespace) on the left side of the chart. Useful to avoid labels getting cut off | false | number | - |
|
|
860
860
|
| xLabelWrap | Whether to wrap x-axis labels when there is not enough space. Default behaviour is to truncate the labels. | false | `true`, `false` | `false` |
|
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.9",
|
|
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
|