@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 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, { dialect });
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
- let all = JSON.parse(txt2);
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
- table2.name = uppercaseIdentifier(table2.name);
1520
- if (table2.primaryKey) table2.primaryKey = uppercaseIdentifier(table2.primaryKey);
1521
- if (table2.tableName) table2.tableName = uppercaseQualified(table2.tableName);
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 (!field) return;
1528
- if (isJoinField(field)) {
1529
- field.name = uppercaseIdentifier(field.name);
1530
- if (field.tableName) field.tableName = uppercaseQualified(field.tableName);
1531
- if (field.tablePath) field.tablePath = uppercaseQualified(field.tablePath);
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 (Array.isArray(node.path)) node.path = node.path.map(uppercaseIdentifier);
1553
- if (Array.isArray(node.structPath)) node.structPath = node.structPath.map(uppercaseIdentifier);
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
- if (config.dialect == "snowflake") uppercaseMalloyQuery(query);
1637
- let tableQueries = Object.values(contents).map((t) => t.query);
1638
- let joinQueries = Object.values(contents).flatMap((t) => t.fields.map((f) => f.query));
1639
- let allQueries = [...tableQueries, ...joinQueries, query].filter((q) => !!q);
1640
- allQueries.forEach((q) => q.structRef = contents[q.baseTableName]);
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 Promise.resolve();
2457
- throw new Error("Not yet implemented");
2434
+ let res = await this.runQuery("show databases");
2435
+ return res.rows.map((row) => String(row["name"] || ""));
2458
2436
  }
2459
- async listTables() {
2460
- let { database } = getSnowflakeNamespace();
2461
- let tablesRef = `${snowflakeIdent(database)}.${snowflakeIdent("INFORMATION_SCHEMA")}.${snowflakeIdent("TABLES")}`;
2462
- let sql = `
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 ${tablesRef}
2465
- where table_type in ('BASE TABLE', 'VIEW')
2466
- order by table_schema, table_name
2467
- `.trim();
2468
- let res = await this.runQuery(sql);
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 columnsRef = `${snowflakeIdent(database)}.${snowflakeIdent("INFORMATION_SCHEMA")}.${snowflakeIdent("COLUMNS")}`;
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 ${columnsRef}
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
- `.trim();
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
- let server = await createServer({
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
- // optimizeDeps: { // this seems prudent in tests, but currently breaks because ssf needs to be optimized, even in tests
2665
- // noDiscovery: process.env.NODE_ENV == 'test',
2666
- // include: process.env.NODE_ENV == 'test' ? [] : undefined,
2667
- // },
2668
- });
2669
- await optimizeDeps(server.config);
2670
- await server.listen();
2671
- console.log(`Server running at http://localhost:${port}`);
2672
- return server;
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
- <main>
2719
- <div id="content"></div>
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
- return mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
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
- s.watcher.add("**/*.gsql");
2756
- s.watcher.on("change", () => {
2777
+ let refresh = async () => {
2757
2778
  clearWorkspace();
2758
2779
  workspaceLoadPromise = loadWorkspace(config.root, false);
2759
- });
2760
- workspaceLoadPromise = loadWorkspace(config.root, false);
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 mdPath = path8.join(config.root, pathName + ".md");
2777
- if (await fs7.exists(mdPath)) {
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}` : ""}:
@@ -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
- /* Vanilla CSS version of styles (Tailwind removed) */
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
- max-width: 1200px;
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>
@@ -1,6 +1,7 @@
1
1
  type ErrorProvider = () => Error[]
2
2
 
3
- window.$GRAPHENE = {getErrors}
3
+ window.$GRAPHENE ||= {}
4
+ window.$GRAPHENE.getErrors = getErrors
4
5
 
5
6
  let staticErrors: Error[] = []
6
7
  let errorProviders: Record<string, ErrorProvider> = {}
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
- async function captureChart (chartTitle) {
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
- let errors = getErrors().map(e => ({message: e.message, id: e.id}))
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
- let result = chart ? await captureChart(chart) : await takeScreenshot()
119
- socket.send(JSON.stringify({type: 'checkResponse', requestId, ...result}))
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.7",
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": "3.0.5",
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 --reporter=default --reporter=json --outputFile=/tmp/graphene-test-results.json",
67
- "test-one": "DEBUG=1 node --inspect ../scripts/turboTest.js",
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