@graphenedata/cli 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -379,6 +379,10 @@ FROM `bigquery-public-data.thelook_ecommerce.orders` as base
379
379
 
380
380
  You don't have to understand this; the point is that GSQL is minimizing the chances that naive users aggregate data incorrectly.
381
381
 
382
+ #### Percentile shorthand
383
+
384
+ Graphene provides percentile helpers so you rarely have to remember the SQL form for each warehouse. Anywhere you can call an aggregate, you can also write `pXX(column)` where `XX` is a whole number between 0 and 100. If you need precision finer than a whole percentile, append extra digits—everything after the first two digits is treated as decimals. Examples: `p975` → 97.5th percentile, `p9999` → 99.99th percentile. Graphene rewrites these shorthands to the dialect’s native function (`quantile_cont` on DuckDB, `approx_quantiles` on BigQuery, `PERCENTILE_CONT` on Snowflake) and ensures they behave like other aggregates (automatic grouping, structPath handling, etc.).
385
+
382
386
  ### `table as` statements
383
387
 
384
388
  You can turn the output of any `select` statement into a table with `table foo as (select ...)`. Here's an example of an additional table `user_facts` added to the two tables from earlier:
@@ -1132,44 +1136,6 @@ where email ilike concat('%', $name_of_input, '%')
1132
1136
  | description | Adds an info icon with description tooltip on hover | false | string | - |
1133
1137
 
1134
1138
 
1135
- #### Date range
1136
-
1137
- Creates a date picker that can be used to filter a query. Includes a set of preset ranges for quick selection of common date ranges (relative to the supplied end date). To see how to filter a query using an input component, see Filters.
1138
-
1139
- Here's an example:
1140
-
1141
- ```markdown
1142
- <DateRange
1143
- name=date_range_name
1144
- data=orders_by_day
1145
- dates=day
1146
- />
1147
- ```
1148
-
1149
- The start and end dates for the user-selected range would then be referenced in GSQL as `$date_range_name_start` and `$date_range_name_end` at the end. For example:
1150
-
1151
- ```sql
1152
- select *
1153
- from orders
1154
- where created_at > $date_range_name_start and < $date_range_name_end
1155
- ```
1156
-
1157
- ##### All date range attributes
1158
-
1159
- | Attribute | Description | Required | Options | Default |
1160
- |------|-------------|----------|---------|---------|
1161
- | name | Name of the DateRange, used to reference the selected values elsewhere as `"$name_start"` or `"$name_end"` | true | string | - |
1162
- | data | Query name, wrapped in curly braces | false | query name | - |
1163
- | dates | Column or expression from the query containing date range to span | false | column name, stored expression name, GSQL expression | - |
1164
- | start | A manually specified start date to use for the range | false | string formatted YYYY-MM-DD | - |
1165
- | end | A manually specified end date to use for the range | false | string formatted YYYY-MM-DD | - |
1166
- | title | Title to display in the Date Range component | false | string | - |
1167
- | presetRanges | Customize "Select a Range" drop down, by including preset range options | false | list of values e.g. `"Last 7 Days, Last 30 Days"`. Allowed values: `Last 7 Days`, `Last 30 Days`, `Last 90 Days`, `Last 365 Days`, `Last 3 Months`, `Last 6 Months`, `Last 12 Months`, `Last Month`, `Last Year`, `Month to Date`, `Month to Today`, `Year to Date`, `Year to Today`, `All Time` | - |
1168
- | defaultValue | Accepts preset in string format to apply default value in Date Range picker | false | `"Last 7 Days"`, `"Last 30 Days"`, `"Last 90 Days"`, `"Last 365 Days"`, `"Last 3 Months"`, `"Last 6 Months"`, `"Last 12 Months"`, `"Last Month"`, `"Last Year"`, `"Month to Date"`, `"Month to Today"`, `"Year to Date"`, `"Year to Today"`, `"All Time"` | - |
1169
- | hideDuringPrint | Hide the component when the report is printed | false | `true`, `false` | `true` |
1170
- | description | Adds an info icon with description tooltip on hover | false | string | - |
1171
-
1172
-
1173
1139
  #### Dropdown
1174
1140
 
1175
1141
  Creates a dropdown menu with a list of options that can be selected. The selected option can be used to filter queries or in markdown. To see how to filter a query using a dropdown, see Filters.
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.6",
6
+ "version": "0.0.8",
7
7
  "license": "Elastic-2.0",
8
8
  "engines": {
9
9
  "node": ">=16"
@@ -25,6 +25,7 @@
25
25
  "dependencies": {
26
26
  "@duckdb/node-api": "1.3.2-alpha.26",
27
27
  "@google-cloud/bigquery": "^8.1.1",
28
+ "@graphenedata/html2canvas": "^1.4.1",
28
29
  "@graphenedata/malloy": "0.0.304",
29
30
  "@lezer/common": "^1.2.3",
30
31
  "@lezer/lr": "^1.4.2",
@@ -36,10 +37,10 @@
36
37
  "cli-table3": "^0.6.3",
37
38
  "commander": "^11.0.0",
38
39
  "debounce": "^1.2.1",
40
+ "dotenv": "^17.2.3",
39
41
  "echarts": "^5.5.0",
40
42
  "fs-extra": "11.2.0",
41
43
  "glob": "^11.0.3",
42
- "@graphenedata/html2canvas": "^1.4.1",
43
44
  "marked": "^16.3.0",
44
45
  "mdsvex": "^0.12.6",
45
46
  "nanoid": "3.3.8",
@@ -57,13 +58,13 @@
57
58
  "@types/sanitize-html": "^2.16.0",
58
59
  "@types/ws": "^8.18.1",
59
60
  "esbuild": "^0.21.5",
60
- "vitest": "3.0.5",
61
+ "vitest": "4.0.15",
61
62
  "vscode-languageserver-types": "^3.17.0"
62
63
  },
63
64
  "scripts": {
64
- "build": "rm -rf dist && node ./esbuild.mjs",
65
- "test": "vitest run --reporter=default --reporter=json --outputFile=/tmp/graphene-test-results.json",
66
- "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",
67
68
  "prepack": "pnpm run build"
68
69
  }
69
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