@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.
- package/cli.ts +39 -6
- package/dist/cli/cli.js +407 -157
- package/dist/docs/graphene.md +4 -38
- 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 +7 -6
- package/dist/ui/playwright.config.ts +0 -30
- /package/dist/ui/{assets → public}/favicon.ico +0 -0
package/dist/docs/graphene.md
CHANGED
|
@@ -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
|
-
|
|
1
|
+
|
|
2
|
+
@font-face { /* latin-ext */
|
|
3
|
+
font-family: 'Inter';
|
|
4
|
+
font-style: normal;
|
|
5
|
+
font-weight: 100 900;
|
|
6
|
+
font-display: swap;
|
|
7
|
+
src: url(/inter-latin-ext.woff2) format('woff2');
|
|
8
|
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@font-face { /* latin */
|
|
12
|
+
font-family: 'Inter';
|
|
13
|
+
font-style: normal;
|
|
14
|
+
font-weight: 100 900;
|
|
15
|
+
font-display: swap;
|
|
16
|
+
src: url(/inter-latin.woff2) format('woff2');
|
|
17
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
:root {
|
|
4
21
|
/* Layout */
|
|
@@ -97,10 +114,24 @@ html {
|
|
|
97
114
|
body {
|
|
98
115
|
font-family: "Inter", var(--ui-font-family);
|
|
99
116
|
line-height: 1.7;
|
|
117
|
+
margin: 0;
|
|
118
|
+
color: var(--base-heading);
|
|
119
|
+
min-height: 100vh;
|
|
120
|
+
display: flex;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
nav {
|
|
124
|
+
flex: 0 0 200px;
|
|
125
|
+
height: 100vh;
|
|
126
|
+
padding-top: 1rem;
|
|
127
|
+
border-right: 1px solid var(--base-200);
|
|
128
|
+
background: #fbfbfd;
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
main {
|
|
103
|
-
|
|
132
|
+
flex: 1;
|
|
133
|
+
max-width: none;
|
|
134
|
+
padding: 2.5rem 3rem 3.5rem;
|
|
104
135
|
margin: 0 auto;
|
|
105
136
|
}
|
|
106
137
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {registerTheme, init, connect} from 'echarts/dist/echarts.esm.js'
|
|
2
2
|
import {evidenceThemeDark, evidenceThemeLight} from './echartsThemes'
|
|
3
|
-
import debounce from 'debounce'
|
|
4
3
|
import * as chartWindowDebug from './chartWindowDebug'
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -261,4 +260,14 @@ const echartsAction = (node, options) => {
|
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
262
|
|
|
263
|
+
const debounce = (callback, wait) => {
|
|
264
|
+
let timeoutId = null
|
|
265
|
+
return (...args) => {
|
|
266
|
+
window.clearTimeout(timeoutId)
|
|
267
|
+
timeoutId = window.setTimeout(() => {
|
|
268
|
+
callback(...args)
|
|
269
|
+
}, wait)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
264
273
|
export default echartsAction
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import navData from 'virtual:nav'
|
|
3
|
+
|
|
4
|
+
export let currentFile = ''
|
|
5
|
+
|
|
6
|
+
let tree = []
|
|
7
|
+
let flatNodes = []
|
|
8
|
+
let openFolders = new Set()
|
|
9
|
+
let treeSignature = ''
|
|
10
|
+
let lastCurrent = ''
|
|
11
|
+
|
|
12
|
+
$: normalizedFiles = (navData || [])
|
|
13
|
+
.map((file) => file.replace(/^\.\//, '').replace(/\\/g, '/'))
|
|
14
|
+
|
|
15
|
+
$: normalizedCurrent = deriveCurrentFile()
|
|
16
|
+
$: currentRoute = normalizedCurrent ? pathToRoute(normalizedCurrent) : '/'
|
|
17
|
+
|
|
18
|
+
function deriveCurrentFile () {
|
|
19
|
+
let fromProp = normalizeFilePath(currentFile)
|
|
20
|
+
let route = getLocationRoute()
|
|
21
|
+
if (route) {
|
|
22
|
+
let match = normalizedFiles.find((file) => pathToRoute(file) === route)
|
|
23
|
+
if (match) return match
|
|
24
|
+
}
|
|
25
|
+
return fromProp
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeFilePath (filePath) {
|
|
29
|
+
return (filePath || '').replace(/^\.\//, '').replace(/\\/g, '/')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getLocationRoute () {
|
|
33
|
+
if (typeof window === 'undefined') return null
|
|
34
|
+
let route = window.location.pathname || '/'
|
|
35
|
+
route = route.replace(/\/+$/, '') || '/'
|
|
36
|
+
return route
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$: {
|
|
40
|
+
let nextSignature = normalizedFiles.join('|')
|
|
41
|
+
if (nextSignature !== treeSignature) {
|
|
42
|
+
treeSignature = nextSignature
|
|
43
|
+
tree = buildTree(normalizedFiles)
|
|
44
|
+
flatNodes = flattenTree(tree)
|
|
45
|
+
openFolders = createDefaultOpenFolders(tree, normalizedCurrent)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$: {
|
|
50
|
+
if (normalizedCurrent !== lastCurrent) {
|
|
51
|
+
openFolders = mergeAncestorFolders(openFolders, normalizedCurrent)
|
|
52
|
+
lastCurrent = normalizedCurrent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toggleFolder (path) {
|
|
57
|
+
if (!path) return
|
|
58
|
+
let next = new Set(openFolders)
|
|
59
|
+
if (next.has(path)) next.delete(path)
|
|
60
|
+
else next.add(path)
|
|
61
|
+
openFolders = next
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleFolderRowKey (event, path) {
|
|
65
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
66
|
+
event.preventDefault()
|
|
67
|
+
toggleFolder(path)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isOpen (path, openSet = openFolders) {
|
|
71
|
+
if (!path) return true
|
|
72
|
+
return openSet.has(path)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isVisible (node, openSet = openFolders) {
|
|
76
|
+
return node.ancestors.every((path) => isOpen(path, openSet))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildTree (paths) {
|
|
80
|
+
let root = []
|
|
81
|
+
let folderMap = new Map()
|
|
82
|
+
|
|
83
|
+
for (let filePath of paths) {
|
|
84
|
+
let cleanPath = filePath.replace(/^\.\//, '').replace(/^\//, '')
|
|
85
|
+
let segments = cleanPath.split('/')
|
|
86
|
+
if (!segments.length) continue
|
|
87
|
+
let fileName = segments.pop()
|
|
88
|
+
let parentChildren = root
|
|
89
|
+
let parentPath = ''
|
|
90
|
+
|
|
91
|
+
for (let segment of segments) {
|
|
92
|
+
parentPath = parentPath ? `${parentPath}/${segment}` : segment
|
|
93
|
+
if (!folderMap.has(parentPath)) {
|
|
94
|
+
let folderNode = {
|
|
95
|
+
type: 'folder',
|
|
96
|
+
name: segment,
|
|
97
|
+
label: formatLabel(segment, 'folder'),
|
|
98
|
+
path: parentPath,
|
|
99
|
+
children: [],
|
|
100
|
+
route: null,
|
|
101
|
+
}
|
|
102
|
+
folderMap.set(parentPath, folderNode)
|
|
103
|
+
parentChildren.push(folderNode)
|
|
104
|
+
}
|
|
105
|
+
parentChildren = folderMap.get(parentPath).children
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!fileName) continue
|
|
109
|
+
let fullPath = parentPath ? `${parentPath}/${fileName}` : fileName
|
|
110
|
+
|
|
111
|
+
if (fileName.toLowerCase() === 'index.md' && parentPath) {
|
|
112
|
+
let folderNode = folderMap.get(parentPath)
|
|
113
|
+
if (folderNode) folderNode.route = pathToRoute(fullPath)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let exists = parentChildren.find((node) => node.path === fullPath)
|
|
118
|
+
if (exists) continue
|
|
119
|
+
parentChildren.push({
|
|
120
|
+
type: 'file',
|
|
121
|
+
name: fileName,
|
|
122
|
+
label: formatLabel(fileName, 'file'),
|
|
123
|
+
path: fullPath,
|
|
124
|
+
route: pathToRoute(fullPath),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return sortNodes(root)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sortNodes (nodes) {
|
|
132
|
+
return nodes
|
|
133
|
+
.map((node) => {
|
|
134
|
+
if (node.type === 'folder' && node.children?.length) {
|
|
135
|
+
return {...node, children: sortNodes(node.children)}
|
|
136
|
+
}
|
|
137
|
+
return node
|
|
138
|
+
})
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
if (a.label === 'Home') return -1
|
|
141
|
+
if (b.label === 'Home') return 1
|
|
142
|
+
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
|
|
143
|
+
return a.label.localeCompare(b.label)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function flattenTree (nodes, depth = 0, ancestors = []) {
|
|
148
|
+
let list = []
|
|
149
|
+
for (let node of nodes) {
|
|
150
|
+
if (node.type === 'folder') {
|
|
151
|
+
let entry = {...node, depth, ancestors}
|
|
152
|
+
list.push(entry)
|
|
153
|
+
if (node.children?.length) {
|
|
154
|
+
list.push(...flattenTree(node.children, depth + 1, [...ancestors, node.path]))
|
|
155
|
+
}
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
list.push({...node, depth, ancestors})
|
|
159
|
+
}
|
|
160
|
+
return list
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createDefaultOpenFolders (_treeNodes, currentPath) {
|
|
164
|
+
let next = new Set()
|
|
165
|
+
return mergeAncestorFolders(next, currentPath)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function mergeAncestorFolders (openSet, filePath) {
|
|
169
|
+
if (!filePath) return new Set(openSet)
|
|
170
|
+
let parts = filePath.split('/')
|
|
171
|
+
parts.pop()
|
|
172
|
+
let aggregate = []
|
|
173
|
+
let next = new Set(openSet)
|
|
174
|
+
for (let part of parts) {
|
|
175
|
+
aggregate.push(part)
|
|
176
|
+
next.add(aggregate.join('/'))
|
|
177
|
+
}
|
|
178
|
+
return next
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatLabel (value, type) {
|
|
182
|
+
let cleaned = type === 'file' ? value.replace(/\.md$/, '') : value
|
|
183
|
+
if (cleaned.toLowerCase() === 'index') return 'Home'
|
|
184
|
+
return cleaned
|
|
185
|
+
.split(/[\s_-]+/)
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1))
|
|
188
|
+
.join(' ')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pathToRoute (path) {
|
|
192
|
+
let clean = path.replace(/\.md$/, '')
|
|
193
|
+
if (!clean || clean === 'index') return '/'
|
|
194
|
+
return '/' + clean
|
|
195
|
+
}
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<ul>
|
|
199
|
+
{#each flatNodes as node (node.path)}
|
|
200
|
+
{#if node.type === 'folder'}
|
|
201
|
+
<li class={isVisible(node, openFolders) ? '' : 'hidden'} style={`--depth:${node.depth}`} data-folder={node.path}>
|
|
202
|
+
<div
|
|
203
|
+
class={node.route ? 'folder-row' : 'folder-row clickable'}
|
|
204
|
+
role={node.route ? undefined : 'button'}
|
|
205
|
+
aria-expanded={node.route ? undefined : String(isOpen(node.path, openFolders))}
|
|
206
|
+
on:click={node.route ? undefined : () => toggleFolder(node.path)}
|
|
207
|
+
on:keydown={node.route ? undefined : (event) => handleFolderRowKey(event, node.path)}
|
|
208
|
+
>
|
|
209
|
+
<button
|
|
210
|
+
class="toggle"
|
|
211
|
+
type="button"
|
|
212
|
+
data-folder-toggle={node.path}
|
|
213
|
+
aria-expanded={isOpen(node.path, openFolders)}
|
|
214
|
+
on:click={(event) => { event.stopPropagation(); toggleFolder(node.path) }}
|
|
215
|
+
aria-label={(isOpen(node.path, openFolders) ? 'Collapse' : 'Expand') + ' ' + node.label}
|
|
216
|
+
>
|
|
217
|
+
<span class={isOpen(node.path, openFolders) ? 'chevron open' : 'chevron'}>▸</span>
|
|
218
|
+
</button>
|
|
219
|
+
{#if node.route}
|
|
220
|
+
<a
|
|
221
|
+
href={node.route}
|
|
222
|
+
class={node.route === currentRoute ? 'active' : ''}
|
|
223
|
+
aria-current={node.route === currentRoute ? 'page' : undefined}
|
|
224
|
+
>
|
|
225
|
+
{node.label}
|
|
226
|
+
</a>
|
|
227
|
+
{:else}
|
|
228
|
+
<span class="label">{node.label}</span>
|
|
229
|
+
{/if}
|
|
230
|
+
</div>
|
|
231
|
+
</li>
|
|
232
|
+
{:else}
|
|
233
|
+
<li class={isVisible(node, openFolders) ? 'file' : 'file hidden'} style={`--depth:${node.depth}`}>
|
|
234
|
+
<a
|
|
235
|
+
href={node.route}
|
|
236
|
+
class={node.path === normalizedCurrent ? 'active' : ''}
|
|
237
|
+
aria-current={node.path === normalizedCurrent ? 'page' : undefined}
|
|
238
|
+
>
|
|
239
|
+
<span>{node.label}</span>
|
|
240
|
+
</a>
|
|
241
|
+
</li>
|
|
242
|
+
{/if}
|
|
243
|
+
{/each}
|
|
244
|
+
</ul>
|
|
245
|
+
|
|
246
|
+
<style>
|
|
247
|
+
ul {
|
|
248
|
+
list-style: none;
|
|
249
|
+
padding: 0 0.5rem 0 0;
|
|
250
|
+
margin: 0;
|
|
251
|
+
display: flex;
|
|
252
|
+
flex-direction: column;
|
|
253
|
+
gap: 0.1rem;
|
|
254
|
+
overflow: hidden;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
li {
|
|
258
|
+
--indent: calc(var(--depth, 0) * 1rem);
|
|
259
|
+
padding-left: var(--indent);
|
|
260
|
+
width: 100%;
|
|
261
|
+
box-sizing: border-box;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
li.file {
|
|
265
|
+
padding-left: calc(var(--indent) + 1.5rem);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
li.hidden {
|
|
269
|
+
display: none;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.folder-row {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
padding: 0.1rem 0.15rem;
|
|
276
|
+
border-radius: 4px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.folder-row.clickable {
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.folder-row.clickable:focus-visible {
|
|
284
|
+
outline: 2px solid rgba(15, 23, 42, 0.2);
|
|
285
|
+
outline-offset: 2px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.toggle {
|
|
289
|
+
display: inline-flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
justify-content: center;
|
|
292
|
+
width: 1.5rem;
|
|
293
|
+
height: 1.5rem;
|
|
294
|
+
color: var(--base-heading);
|
|
295
|
+
background: transparent;
|
|
296
|
+
border: none;
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
border-radius: 4px;
|
|
299
|
+
opacity: 0;
|
|
300
|
+
pointer-events: none;
|
|
301
|
+
transition: opacity 120ms ease;
|
|
302
|
+
visibility: hidden;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.folder-row:hover .toggle,
|
|
306
|
+
.folder-row:focus-within .toggle,
|
|
307
|
+
.toggle:focus-visible {
|
|
308
|
+
opacity: 1;
|
|
309
|
+
pointer-events: auto;
|
|
310
|
+
visibility: visible;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.toggle:hover,
|
|
314
|
+
.toggle:focus-visible {
|
|
315
|
+
background: rgba(15, 23, 42, 0.1);
|
|
316
|
+
outline: none;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.chevron {
|
|
320
|
+
display: inline-block;
|
|
321
|
+
transition: transform 150ms ease;
|
|
322
|
+
font-size: 0.7rem;
|
|
323
|
+
color: var(--base-content-muted);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.chevron.open {
|
|
327
|
+
transform: rotate(90deg);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.label {
|
|
331
|
+
font-size: 0.85rem;
|
|
332
|
+
padding: 0.2rem 0.35rem;
|
|
333
|
+
white-space: nowrap;
|
|
334
|
+
overflow: hidden;
|
|
335
|
+
text-overflow: ellipsis;
|
|
336
|
+
color: var(--base-heading);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.folder-row a {
|
|
340
|
+
flex: 1;
|
|
341
|
+
display: block;
|
|
342
|
+
font-size: 0.85rem;
|
|
343
|
+
padding: 0.2rem 0.35rem;
|
|
344
|
+
border-radius: 4px;
|
|
345
|
+
color: var(--base-heading);
|
|
346
|
+
text-decoration: none;
|
|
347
|
+
white-space: nowrap;
|
|
348
|
+
overflow: hidden;
|
|
349
|
+
text-overflow: ellipsis;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.folder-row a:hover,
|
|
353
|
+
.folder-row a:focus-visible {
|
|
354
|
+
background: rgba(15, 23, 42, 0.05);
|
|
355
|
+
outline: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
li.file a {
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
font-size: 0.85rem;
|
|
362
|
+
padding: 0.2rem 0.5rem;
|
|
363
|
+
border-radius: 4px;
|
|
364
|
+
color: var(--base-heading);
|
|
365
|
+
text-decoration: none;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
li.file a span {
|
|
369
|
+
white-space: nowrap;
|
|
370
|
+
overflow: hidden;
|
|
371
|
+
text-overflow: ellipsis;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
li.file a:hover,
|
|
375
|
+
li.file a:focus-visible {
|
|
376
|
+
background: rgba(15, 23, 42, 0.05);
|
|
377
|
+
outline: none;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
a.active {
|
|
381
|
+
color: var(--base-900, #0f172a);
|
|
382
|
+
}
|
|
383
|
+
</style>
|
|
Binary file
|
|
Binary file
|
package/dist/ui/web.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {getErrors} from './internal/telemetry.ts'
|
|
2
2
|
import './app.css'
|
|
3
3
|
import {isLoading} from './internal/queryEngine.ts'
|
|
4
|
+
import NavSidebar from './internal/NavSidebar.svelte'
|
|
4
5
|
|
|
5
6
|
import Area from './components/Area.svelte'
|
|
6
7
|
import AreaChart from './components/AreaChart.svelte'
|
|
@@ -32,6 +33,8 @@ import TableSubtotalRow from './components/TableSubtotalRow.svelte'
|
|
|
32
33
|
import TableTotalRow from './components/TableTotalRow.svelte'
|
|
33
34
|
import TextInput from './components/TextInput.svelte'
|
|
34
35
|
|
|
36
|
+
window.$GRAPHENE = window.$GRAPHENE || {}
|
|
37
|
+
|
|
35
38
|
window.$GRAPHENE.components = {
|
|
36
39
|
Area,
|
|
37
40
|
AreaChart,
|
|
@@ -64,35 +67,28 @@ window.$GRAPHENE.components = {
|
|
|
64
67
|
TextInput,
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
|
|
68
70
|
let socket = null
|
|
69
71
|
|
|
72
|
+
if (document.getElementById('nav')) {
|
|
73
|
+
new NavSidebar({target: document.getElementById('nav')})
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
connectWebSocket()
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
await waitForQueriesToFinish()
|
|
74
|
-
let errors = getErrors()
|
|
78
|
+
function captureChart (chartTitle) {
|
|
75
79
|
let escaped = window.CSS.escape(chartTitle)
|
|
76
80
|
let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`)
|
|
77
|
-
|
|
78
|
-
if (!canvas) {
|
|
79
|
-
errors.push({message: `Could not find chart titled "${chartTitle}"`})
|
|
80
|
-
return {stillLoading: isLoading(), screenshot: null, errors}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {stillLoading: isLoading(), screenshot: canvas.toDataURL('image/png'), errors}
|
|
81
|
+
return canvas?.toDataURL('image/png')
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
async function takeScreenshot () {
|
|
87
|
-
await waitForQueriesToFinish()
|
|
88
85
|
if (!window.html2canvas) {
|
|
89
86
|
let html2canvas = await import('@graphenedata/html2canvas')
|
|
90
87
|
window.html2canvas = html2canvas.default
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
let canvas = await window.html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
|
|
94
|
-
|
|
95
|
-
return {stillLoading: isLoading(), screenshot: canvas?.toDataURL('image/png'), errors}
|
|
91
|
+
return canvas?.toDataURL('image/png')
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
async function waitForQueriesToFinish () {
|
|
@@ -115,8 +111,11 @@ function connectWebSocket () {
|
|
|
115
111
|
let {type, requestId, chart} = JSON.parse(event.data)
|
|
116
112
|
|
|
117
113
|
if (type === 'check') {
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
await waitForQueriesToFinish()
|
|
115
|
+
let errors = getErrors().map(e => ({message: e.message, id: e.id}))
|
|
116
|
+
let stillLoading = isLoading()
|
|
117
|
+
let screenshot = chart ? captureChart(chart) : await takeScreenshot()
|
|
118
|
+
socket.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading, screenshot}))
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
121
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"main": "cli.ts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Graphene Systems Inc",
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.8",
|
|
7
7
|
"license": "Elastic-2.0",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=16"
|
|
@@ -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": "
|
|
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
|
|
66
|
-
"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",
|
|
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
|