@graphenedata/cli 0.0.14 → 0.0.16

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.
Files changed (121) hide show
  1. package/LICENSE.md +3 -3
  2. package/README.md +138 -0
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/bin.js +2 -2
  5. package/dist/cli/bigQuery-I3F46SC6.js +75 -0
  6. package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
  7. package/dist/cli/chunk-OVWODUTJ.js +12849 -0
  8. package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
  9. package/dist/cli/chunk-QAXEOZ43.js +53 -0
  10. package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
  11. package/dist/cli/cli.js +245 -10290
  12. package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
  13. package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
  14. package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
  15. package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
  16. package/dist/cli/serve2-TNN5EROW.js +447 -0
  17. package/dist/cli/serve2-TNN5EROW.js.map +7 -0
  18. package/dist/cli/snowflake-MOQB5GA4.js +128 -0
  19. package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
  20. package/dist/index.d.ts +63 -0
  21. package/dist/lang/index.d.ts +63 -0
  22. package/dist/skills/graphene/SKILL.md +235 -0
  23. package/dist/skills/graphene/references/big-value.md +20 -0
  24. package/dist/skills/graphene/references/date-range.md +64 -0
  25. package/dist/skills/graphene/references/dropdown.md +62 -0
  26. package/dist/skills/graphene/references/echarts.md +162 -0
  27. package/dist/skills/graphene/references/gsql.md +393 -0
  28. package/dist/skills/graphene/references/model-gsql.md +72 -0
  29. package/dist/skills/graphene/references/table.md +143 -0
  30. package/dist/skills/graphene/references/text-input.md +29 -0
  31. package/dist/ui/app.css +263 -299
  32. package/dist/ui/component-utilities/dataShaping.ts +484 -0
  33. package/dist/ui/component-utilities/dataSummary.ts +57 -0
  34. package/dist/ui/component-utilities/enrich.ts +763 -0
  35. package/dist/ui/component-utilities/format.ts +177 -0
  36. package/dist/ui/component-utilities/inputUtils.ts +48 -9
  37. package/dist/ui/component-utilities/theme.ts +200 -0
  38. package/dist/ui/component-utilities/themeStores.ts +26 -21
  39. package/dist/ui/component-utilities/types.ts +70 -0
  40. package/dist/ui/components/AreaChart.svelte +57 -105
  41. package/dist/ui/components/BarChart.svelte +71 -129
  42. package/dist/ui/components/BigValue.svelte +24 -40
  43. package/dist/ui/components/Column.svelte +11 -19
  44. package/dist/ui/components/DateRange.svelte +71 -34
  45. package/dist/ui/components/Dropdown.svelte +82 -49
  46. package/dist/ui/components/DropdownOption.svelte +1 -2
  47. package/dist/ui/components/ECharts.svelte +179 -60
  48. package/dist/ui/components/InlineDelta.svelte +51 -32
  49. package/dist/ui/components/LineChart.svelte +54 -125
  50. package/dist/ui/components/PieChart.svelte +27 -37
  51. package/dist/ui/components/QueryLoad.svelte +78 -44
  52. package/dist/ui/components/Row.svelte +2 -1
  53. package/dist/ui/components/ScatterPlot.svelte +52 -0
  54. package/dist/ui/components/Skeleton.svelte +32 -0
  55. package/dist/ui/components/Table.svelte +3 -2
  56. package/dist/ui/components/TableGroupRow.svelte +28 -36
  57. package/dist/ui/components/TableHarness.svelte +32 -0
  58. package/dist/ui/components/TableHeader.svelte +34 -59
  59. package/dist/ui/components/TableRow.svelte +15 -39
  60. package/dist/ui/components/TableSubtotalRow.svelte +26 -21
  61. package/dist/ui/components/TableTotalRow.svelte +27 -37
  62. package/dist/ui/components/TextInput.svelte +17 -14
  63. package/dist/ui/components/Value.svelte +25 -0
  64. package/dist/ui/components/_Table.svelte +80 -76
  65. package/dist/ui/internal/ChartGallery.svelte +527 -0
  66. package/dist/ui/internal/ErrorDisplay.svelte +60 -0
  67. package/dist/ui/internal/LocalApp.svelte +87 -19
  68. package/dist/ui/internal/PageNavGroup.svelte +269 -0
  69. package/dist/ui/internal/Sidebar.svelte +178 -0
  70. package/dist/ui/internal/SidebarToggle.svelte +47 -0
  71. package/dist/ui/internal/StyleGallery.svelte +244 -0
  72. package/dist/ui/internal/clientCache.ts +15 -13
  73. package/dist/ui/internal/pageInputs.svelte.js +292 -0
  74. package/dist/ui/internal/queryEngine.ts +124 -132
  75. package/dist/ui/internal/runSocket.ts +59 -0
  76. package/dist/ui/internal/sidebar.svelte.js +18 -0
  77. package/dist/ui/internal/telemetry.ts +52 -17
  78. package/dist/ui/internal/types.d.ts +7 -0
  79. package/dist/ui/web.js +55 -13
  80. package/package.json +40 -41
  81. package/dist/docs/agent-instructions.md +0 -18
  82. package/dist/docs/base.md +0 -98
  83. package/dist/docs/cli.md +0 -22
  84. package/dist/docs/graphene.md +0 -1462
  85. package/dist/ui/component-utilities/autoFormatting.js +0 -301
  86. package/dist/ui/component-utilities/builtInFormats.js +0 -482
  87. package/dist/ui/component-utilities/chartContext.js +0 -12
  88. package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
  89. package/dist/ui/component-utilities/checkInputs.js +0 -95
  90. package/dist/ui/component-utilities/convert.js +0 -15
  91. package/dist/ui/component-utilities/dateParsing.js +0 -57
  92. package/dist/ui/component-utilities/dropdownContext.ts +0 -1
  93. package/dist/ui/component-utilities/echarts.js +0 -272
  94. package/dist/ui/component-utilities/echartsThemes.js +0 -453
  95. package/dist/ui/component-utilities/formatTitle.js +0 -24
  96. package/dist/ui/component-utilities/formatting.js +0 -250
  97. package/dist/ui/component-utilities/getColumnExtents.js +0 -79
  98. package/dist/ui/component-utilities/getColumnSummary.js +0 -67
  99. package/dist/ui/component-utilities/getCompletedData.js +0 -114
  100. package/dist/ui/component-utilities/getDistinctCount.js +0 -7
  101. package/dist/ui/component-utilities/getDistinctValues.js +0 -15
  102. package/dist/ui/component-utilities/getSeriesConfig.js +0 -237
  103. package/dist/ui/component-utilities/getSortedData.js +0 -7
  104. package/dist/ui/component-utilities/getStackPercentages.js +0 -43
  105. package/dist/ui/component-utilities/getStackedData.js +0 -17
  106. package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
  107. package/dist/ui/component-utilities/globalContexts.js +0 -1
  108. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
  109. package/dist/ui/component-utilities/replaceNulls.js +0 -14
  110. package/dist/ui/component-utilities/tableUtils.ts +0 -120
  111. package/dist/ui/components/Area.svelte +0 -214
  112. package/dist/ui/components/Bar.svelte +0 -350
  113. package/dist/ui/components/Chart.svelte +0 -989
  114. package/dist/ui/components/ErrorChart.svelte +0 -118
  115. package/dist/ui/components/Line.svelte +0 -227
  116. package/dist/ui/internal/NavSidebar.svelte +0 -396
  117. package/dist/ui/internal/PageError.svelte +0 -23
  118. package/dist/ui/internal/checkSocket.ts +0 -48
  119. package/dist/ui/internal/theme.ts +0 -88
  120. package/dist/ui/public/inter-latin-ext.woff2 +0 -0
  121. package/dist/ui/public/inter-latin.woff2 +0 -0
@@ -1,21 +1,13 @@
1
1
  // The query engine gathers query requests and inputs from components, and issues requests to the server.
2
2
  // When inputs change, it takes care of notifying affected components and requesting new data.
3
3
 
4
- import {cacheRead, cacheWrite, getHashes} from './clientCache.ts'
5
- import {errorProvider} from './telemetry.ts'
6
-
7
- interface QueryResult {
8
- rows?: any[]
9
- errors?: Error[]
10
- fields?: Field[]
11
- }
4
+ import type {GrapheneError} from '../../lang/index.d.ts'
12
5
 
13
- interface Field {
14
- name: string
15
- type?: string
16
- }
6
+ import {type QueryResult, type Field} from '../component-utilities/types.ts'
7
+ import {cacheRead, cacheWrite, getHashes} from './clientCache.ts'
8
+ import {getActivePageInputs, type ParamSnapshot} from './pageInputs.svelte.js'
17
9
 
18
- type ResultHandler = (res: QueryResult) => void
10
+ type ResultHandler = (res: QueryResult | void) => void
19
11
 
20
12
  interface QueryNode {
21
13
  name?: string
@@ -24,32 +16,39 @@ interface QueryNode {
24
16
  callback?: ResultHandler
25
17
  loading: boolean
26
18
  fields: Map<string, string | string[]>
27
- errors: Error[]
19
+ componentId?: string
20
+ error?: GrapheneError
28
21
  }
29
22
 
23
+ export interface QueryRequest {
24
+ params: ParamSnapshot
25
+ gsql: string
26
+ hashes: string[]
27
+ repoId: string
28
+ }
29
+
30
+ export type QueryFetcher = (req: QueryRequest) => Promise<QueryResult>
31
+
30
32
  let runPending: Promise<void> | null = null
31
- let params = {} as Record<string, any>
32
33
  let queries = [] as QueryNode[]
33
- let queryResults = {} as Record<string, {rows: any[], fields?: Field[]}>
34
-
35
- function registerQuery (name: string, contents: string) {
36
- queries = queries.filter(q => q.name !== name)
37
- queries.push({name, contents, loading: false, fields: new Map(), errors: []})
38
- }
34
+ let queryResults = {} as Record<string, {rows: any[]; fields?: Field[]}>
39
35
 
40
- const getRoutePath = () => typeof window === 'undefined' ? '/' : (window.location.pathname || '/')
36
+ let queryFetcher: QueryFetcher = fetchWithCache
37
+ export const setQueryFetcher = f => (queryFetcher = f)
41
38
 
42
- function updateParam (name: string, value: any) {
43
- params[name] = value
44
- runAll() // for now, do the easy thing and reload it all
39
+ // Called by GrapheneQuery tags to register a named query on the page
40
+ function registerQuery(name: string, contents: string) {
41
+ queries = queries.filter(q => q.name !== name)
42
+ queries.push({name, contents, loading: false, fields: new Map()})
45
43
  }
46
44
 
47
- function query (source: string, fields: Record<string, string | string[]>, callback: ResultHandler) {
45
+ // Called by viz components to request a particular query of data
46
+ function query(source: string, fields: Record<string, string | string[]>, callback: ResultHandler, componentId?: string) {
48
47
  // using Map here because it preserves the order in which we add fields to the select, which we use when we get the result.
49
48
  let map = new Map(Object.entries(fields))
50
49
  let exprs: string[] = []
51
50
  if (map.size > 0) {
52
- map.forEach((value) => {
51
+ map.forEach(value => {
53
52
  if (Array.isArray(value)) exprs.push(...value)
54
53
  else exprs.push(value)
55
54
  })
@@ -57,146 +56,139 @@ function query (source: string, fields: Record<string, string | string[]>, callb
57
56
  exprs = ['*']
58
57
  }
59
58
  let contents = `from ${source} select ${exprs.join(', ')}`
60
- queries.push({contents, callback, loading: false, fields: map, errors: [], source})
59
+ queries.push({contents, callback, loading: false, fields: map, source, componentId})
61
60
  runAll()
61
+ return componentId
62
62
  }
63
63
 
64
- function unsubscribe (callback: ResultHandler) {
64
+ function unsubscribe(callback: ResultHandler) {
65
65
  queries = queries.filter(q => q.callback !== callback)
66
66
  }
67
67
 
68
- async function runNode (n: QueryNode) {
68
+ function resetQueryEngine() {
69
+ queries = []
70
+ Object.keys(queryResults).forEach(key => delete queryResults[key])
71
+ getActivePageInputs().reset()
72
+ }
73
+
74
+ // Actually runs a given query that some frontend component is listening to.
75
+ // This is pretty dumb at the moment, it simply concats all code fenced queries as table statements, then appends the actual query at the end.
76
+ async function runNode(n: QueryNode) {
69
77
  if (!n.callback) throw new Error('running node nobody is listening to')
70
- n.callback({}) // notify that the query is loading
78
+
79
+ n.callback() // notifies listeners we're back in the loading state
71
80
  n.loading = true
72
- n.errors = []
81
+ n.error = undefined
73
82
 
83
+ // build up the request body. Hashes is the list of ETag hashes currently in our browser cache. We send all of them,
84
+ // letting the server determine the hash of this particular query, and whether data we already have is acceptable.
74
85
  let hashes = await getHashes()
75
86
  let tables = queries.filter(q => q.name)
76
- let gsql = [
77
- ...tables.map(q => `table ${q.name} as (${q.contents})`),
78
- n.contents,
79
- ].join('\n')
87
+ let gsql = [...tables.map(q => `table ${q.name} as (${q.contents})`), n.contents].join('\n')
88
+ let params = getActivePageInputs().getParams()
80
89
 
81
90
  try {
82
- let response = await fetch('/_api/query', {
83
- method: 'POST',
84
- headers: {'Content-Type': 'application/json'},
85
- body: JSON.stringify({params, gsql, hashes, routePath: getRoutePath(), repoId: window.$GRAPHENE?.repoId}),
86
- })
87
- let hash = response.headers.get('ETag') || ''
88
-
89
- if (response.status == 304) { // cache hit. Read it out and use that
90
- let body = await cacheRead(hash)
91
- let result = translateData(body, n)
92
- if (n.source) queryResults[n.source] = {rows: result.rows, fields: body.fields}
93
- n.callback(result)
94
- } else if (response.ok) { // cache miss. write it to the cache, and return the data
95
- cacheWrite(hash, response.clone()) // clone allows us to write the raw response into the cache
96
- let body = await response.json()
97
- let fields = body.fields // grab before translateData mutates
98
- let result = translateData(body, n) // nb that translateData modifies in place for performance
99
- if (n.source) queryResults[n.source] = {rows: result.rows, fields}
100
- n.callback(result)
101
- } else { // request failed. Record it
102
- let isJson = response.headers.get('Content-Type') === 'application/json'
103
- let body = isJson ? await response.json() : await response.text()
104
- n.errors = Array.isArray(body) ? body : [{message: body}]
105
-
106
- let fieldIds = Array.from(n.fields.entries()).flatMap(([name, val]) => {
107
- if (Array.isArray(val)) {
108
- if (val.length === 0) return [] as string[]
109
- if (val.length === 1) return [`${name}="${val[0]}"`]
110
- return [`${name}="${val.join(', ')}"`]
111
- }
112
- if (typeof val === 'string' && val.trim().length === 0) return [] as string[]
113
- if (val == null) return [] as string[]
114
- return [`${name}="${val}"`]
115
- })
116
- let idStr = `Query (data="${n.source}" ` + fieldIds.join(' ') + ')'
117
- n.errors.forEach(e => (e as any).id = idStr)
118
- n.callback({errors: n.errors})
119
- }
91
+ let res = await queryFetcher({params, gsql, hashes, repoId: window.$GRAPHENE?.repoId})
92
+ let result = translateData(res, n)
93
+ if (n.source) queryResults[n.source] = result // TODO do we still need queryResults? Seems like a hack
94
+ n.callback(result)
120
95
  } catch (e) {
121
- n.errors = [e as Error]
96
+ let err = typeof e == 'string' ? new Error(e) : (e as Error)
97
+ let grapheneError = err as GrapheneError
98
+ n.error = {...grapheneError, componentId: n.componentId || grapheneError.componentId, message: err.message, stack: err.stack}
99
+ n.callback({rows: [], fields: [], error: n.error, sql: ''})
122
100
  } finally {
123
101
  n.loading = false
124
102
  }
125
103
  }
126
104
 
127
- function runAll () {
105
+ async function fetchWithCache(req: QueryRequest): Promise<QueryResult> {
106
+ let response = await fetch('/_api/query', {
107
+ method: 'POST',
108
+ headers: {'Content-Type': 'application/json'},
109
+ body: JSON.stringify(req),
110
+ })
111
+ let hash = response.headers.get('ETag') || ''
112
+
113
+ // cache hit. Read data out of the browser cache and return it
114
+ if (response.status == 304) {
115
+ return await cacheRead(hash)
116
+ }
117
+
118
+ if (!response.ok) {
119
+ let body = (await response.json()) as GrapheneError
120
+ let err = new Error(body.message)
121
+ Object.assign(err, body)
122
+ throw err
123
+ }
124
+
125
+ cacheWrite(hash, response.clone())
126
+ return await response.json()
127
+ }
128
+
129
+ function runAll() {
128
130
  if (runPending) return runPending
129
- runPending = Promise.resolve().then(_runAll).finally(() => runPending = null)
131
+ runPending = Promise.resolve()
132
+ .then(_runAll)
133
+ .finally(() => (runPending = null))
130
134
  }
131
135
 
132
- async function _runAll () {
133
- await Promise.all(queries.map(async n => {
134
- if (!n.callback) return
135
- await runNode(n)
136
- }))
136
+ async function _runAll() {
137
+ await Promise.all(
138
+ queries.map(async n => {
139
+ if (!n.callback) return
140
+ await runNode(n)
141
+ }),
142
+ )
137
143
  }
138
144
 
139
- function translateData (data: any, node: QueryNode) {
145
+ // This translates results we got back from the server into the format any frontend code expects.
146
+ export function translateData(data: any, node: QueryNode): QueryResult {
140
147
  let rows = data.rows || []
141
- rows.dataLoaded = true // evidence components need this to be set
142
- rows._evidenceColumnTypes = []
143
- let requestFields: string[] = []
144
- node.fields.forEach((value) => {
145
- if (Array.isArray(value)) requestFields.push(...value)
146
- else requestFields.push(value)
147
- })
148
+ let fields: Field[] = []
149
+
150
+ let requestFields = Array.from(node.fields.values()).flatMap(f => f)
148
151
 
149
152
  data.fields.forEach((field, index) => {
150
- let name = field.name
153
+ let requested = requestFields[index]
154
+ let name = requested || field.name
155
+
156
+ // The key in row objects usually matches field.name, except in snowflake where it gets auto-capitalized
157
+ let rowKey = field.name
158
+ if (rows[0] && !Object.hasOwn(rows[0], field.name)) {
159
+ rowKey = Object.keys(rows[0]).find(k => k.toLowerCase() == field.name.toLowerCase())
160
+ }
151
161
 
152
- // server gives names like `col_1` to unnamed expressions but we translate it back into the original expression like `avg(price)`
153
- if (field.name.match(/col_\d+/)) {
154
- name = requestFields[index]
162
+ // Result fields come back in select order, so we can map them back to the requested field names by index.
163
+ // Row objects are still keyed by the warehouse result name, which may differ by alias or by Snowflake uppercasing.
164
+ if (requested && rowKey && rowKey != requested) {
155
165
  rows.forEach(r => {
156
- r[name] = r[field.name]
157
- delete r[field.name]
166
+ r[requested] = r[rowKey]
167
+ delete r[rowKey]
158
168
  })
159
169
  }
160
170
 
161
- // map graphene types down to the ones evidence expects
162
- rows._evidenceColumnTypes.push({name, evidenceType: evidenceType(field.type)})
171
+ fields.push({...field, name})
163
172
  })
164
173
 
165
- return {rows}
174
+ return {rows, fields}
166
175
  }
167
176
 
168
- export const isLoading = () => !!queries.find(q => q.loading)
169
-
170
- errorProvider('queryEngine', () => {
171
- let unique = {}
172
- queries.flatMap(q => q.errors).filter(q => !!q).forEach(e => {
173
- unique[e.message + String((e as any).from?.lineText)] = e
177
+ const isQueryLoading = () => !!queries.find(q => q.loading)
178
+
179
+ if (typeof window !== 'undefined') {
180
+ Object.assign(window.$GRAPHENE, {
181
+ getParam: (name: string) => getActivePageInputs().getParam(name),
182
+ registerQuery,
183
+ subscribeParams: subscriber => getActivePageInputs().subscribeParams(subscriber),
184
+ syncParamsFromUrl: () => getActivePageInputs().syncFromUrl(),
185
+ updateParam: (name: string, value: any) => getActivePageInputs().updateParam(name, value),
186
+ updateParams: (nextParams: Record<string, any>) => getActivePageInputs().updateParams(nextParams),
187
+ query,
188
+ unsubscribe,
189
+ resetQueryEngine,
190
+ rerunQueries: runAll,
191
+ isQueryLoading,
192
+ queryResults,
174
193
  })
175
- return Object.values(unique) as Error[]
176
- })
177
-
178
- async function waitForQueries (timeout = 20_000) {
179
- let end = performance.now() + timeout
180
- while (isLoading() && performance.now() < end) {
181
- await new Promise(resolve => setTimeout(resolve, 25))
182
- }
183
- return !isLoading()
184
- }
185
-
186
- function evidenceType (type: string | undefined) {
187
- if (type === 'string') return 'string'
188
- if (type === 'number') return 'number'
189
- if (type === 'boolean') return 'boolean'
190
- if (type === 'date' || type === 'timestamp') return 'date'
191
- console.warn('Unsupported evidence type ' + type)
192
- return 'string'
193
194
  }
194
-
195
- Object.assign(window.$GRAPHENE, {
196
- registerQuery,
197
- updateParam,
198
- query,
199
- unsubscribe,
200
- waitForQueries,
201
- queryResults,
202
- })
@@ -0,0 +1,59 @@
1
+ // WebSocket connection for the `graphene run` command.
2
+ // Listens for run requests, waits for queries to finish, captures screenshots, and reports errors.
3
+
4
+ import {getErrors} from './telemetry.ts'
5
+
6
+ let socket: WebSocket | null = null
7
+ connect()
8
+
9
+ // html2canvas is dynamically loaded so we don't include it on pages that don't need it.
10
+ let html2canvas: any
11
+ async function loadHtml2Canvas() {
12
+ html2canvas ||= (await import('@graphenedata/html2canvas'))?.default
13
+ }
14
+
15
+ async function captureChart(chart: string) {
16
+ let escaped = window.CSS.escape(chart)
17
+ let chartEl = document.querySelector(`[data-chart-title="${escaped}"]`) as HTMLElement | null
18
+ chartEl ||= document.querySelector(`[data-component-id="${escaped}"]`) as HTMLElement | null
19
+ if (!chartEl) return undefined
20
+
21
+ await loadHtml2Canvas()
22
+ let canvas = await html2canvas(chartEl, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
23
+ return canvas?.toDataURL('image/png')
24
+ }
25
+
26
+ function listComponentIds() {
27
+ return Array.from(document.querySelectorAll('[data-component-id]'))
28
+ .map(el => el.getAttribute('data-component-id') || '')
29
+ .filter(componentId => componentId.trim().length > 0)
30
+ }
31
+
32
+ async function takeScreenshot() {
33
+ await loadHtml2Canvas()
34
+ let canvas = await html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
35
+ return canvas?.toDataURL('image/png')
36
+ }
37
+
38
+ function connect() {
39
+ let wsUrl = `ws://${window.location.host}/_api/ws`
40
+ socket = new WebSocket(wsUrl)
41
+ socket.onclose = () => setTimeout(connect, 2000)
42
+ socket.onopen = () => socket!.send(JSON.stringify({type: 'register', url: window.location.href}))
43
+
44
+ socket.onmessage = async event => {
45
+ let {type, requestId, action, chart} = JSON.parse(event.data)
46
+ if (type !== 'check') return
47
+
48
+ if (action === 'list') {
49
+ await window.$GRAPHENE.pageReady
50
+ await new Promise(resolve => requestAnimationFrame(resolve))
51
+ socket!.send(JSON.stringify({type: 'checkResponse', requestId, componentIds: listComponentIds()}))
52
+ return
53
+ }
54
+
55
+ let finished = await window.$GRAPHENE.waitForLoad(20_000)
56
+ let screenshot = chart ? await captureChart(chart) : await takeScreenshot()
57
+ socket!.send(JSON.stringify({type: 'checkResponse', requestId, errors: getErrors(), stillLoading: !finished, screenshot}))
58
+ }
59
+ }
@@ -0,0 +1,18 @@
1
+ let state = $state({ open: false });
2
+ let closeTimer;
3
+ const sidebar = {
4
+ get open() {
5
+ return state.open;
6
+ },
7
+ enter() {
8
+ clearTimeout(closeTimer);
9
+ state.open = true;
10
+ },
11
+ leave() {
12
+ clearTimeout(closeTimer);
13
+ closeTimer = setTimeout(() => state.open = false, 120);
14
+ }
15
+ };
16
+ export {
17
+ sidebar
18
+ };
@@ -1,30 +1,65 @@
1
- type ErrorProvider = () => Error[]
1
+ import {onDestroy} from 'svelte'
2
+
3
+ import type {GrapheneError} from '../../lang/index.d.ts'
4
+
5
+ // In local development, the same page could have components hot reload many times.
6
+ // A flat list of errors would start to accumulate duplicates, and calling `graphene run` would report many (possibly stale) errors.
7
+ // The current solution is to key errors by their componentId, and remove them when the component is destroyed.
8
+ // This mostly works, but it has the downside that you can only show a single error per component, and does not help with `staticErrors`.
9
+ // The other possible solution is that any error should prevent HMR and force a full refresh.
2
10
 
3
11
  window.$GRAPHENE ||= {}
4
12
  window.$GRAPHENE.getErrors = getErrors
5
13
 
6
- let staticErrors: Error[] = []
7
- let errorProviders: Record<string, ErrorProvider> = {}
14
+ let staticErrors: GrapheneError[] = []
15
+ let componentErrors = new Map<string, GrapheneError>()
8
16
 
9
- window.addEventListener('error', (event) => {
17
+ window.addEventListener('error', event => {
10
18
  if ((event.error?.message || '').match(/Failed to fetch dynamically imported module.*\.md\?import/)) return
11
- staticErrors.push(event.error)
12
- })
13
- window.addEventListener('unhandledrejection', (event) => {
14
- staticErrors.push(event.reason)
19
+ logError(event.error)
15
20
  })
21
+ window.addEventListener('unhandledrejection', event => logError(event.reason))
22
+
23
+ // Logs errors that for whatever reason cannot be attached to a component
24
+ export function logError(error: unknown) {
25
+ let err = error instanceof Error ? error : new Error(String(error))
26
+ staticErrors.push({message: err.message, stack: err.stack})
27
+ }
28
+
29
+ export function setErrorFor(key: string, error: GrapheneError | null) {
30
+ if (error) componentErrors.set(key, error)
31
+ else componentErrors.delete(key)
32
+ }
33
+
34
+ // Creates a logger for one component instance. Each component keeps its latest error
35
+ // across rerenders, and we clear it when the component is destroyed during HMR/navigation.
36
+ export function componentLogger(componentName: string, identifiers: Record<string, unknown> = {}) {
37
+ let id = computeComponentId(componentName, identifiers)
38
+ onDestroy(() => componentErrors.delete(id))
39
+
40
+ return {
41
+ id,
42
+ error(error: unknown, ctx: Partial<GrapheneError> = {}) {
43
+ if (!error) return componentErrors.delete(id)
44
+ let err = error instanceof Error ? error : new Error(String(error))
45
+ componentErrors.set(id, {message: err.message, stack: err.stack, componentId: id, ...ctx})
46
+ },
47
+ }
48
+ }
16
49
 
17
- export function logError (e: Error | string, ctx?: any) {
18
- if (typeof e === 'string') e = new Error(e)
19
- if (ctx) Object.assign(e, ctx)
20
- staticErrors.push(e)
50
+ // Shared helper for logging when a svelte component is given props it did not expect.
51
+ export function logExtraProps(logger: ReturnType<typeof componentLogger>, componentName: string, props: Record<string, unknown>) {
52
+ let unsupported = Object.keys(props).filter(prop => !['children', '$$slots', '$$events', '$$legacy'].includes(prop))
53
+ if (unsupported.length) logger.error(unsupported.map(prop => `Unsupported prop "${prop}" on ${componentName}.`).join(' '))
21
54
  }
22
55
 
23
- export function errorProvider (key:string, fn: ErrorProvider) {
24
- errorProviders[key] = fn
56
+ export function getErrors(): GrapheneError[] {
57
+ return staticErrors.concat(Array.from(componentErrors.values()))
25
58
  }
26
59
 
27
- export function getErrors (): Error[] {
28
- let provided = Object.values(errorProviders).flatMap(p => p())
29
- return staticErrors.concat(provided)
60
+ export function computeComponentId(componentName: string, identifiers: Record<string, unknown> = {}) {
61
+ let attrs = Object.entries(identifiers).flatMap(([name, value]) =>
62
+ value === undefined || value === null || value === '' || (typeof value === 'object' && !Array.isArray(value)) ? [] : [`${name}="${Array.isArray(value) ? value.join(', ') : value}"`],
63
+ )
64
+ return `${componentName}${attrs.length ? ` (${attrs.join(' ')})` : ''}`
30
65
  }
@@ -0,0 +1,7 @@
1
+ interface ImportMetaEnv {
2
+ readonly VITE_TEST: string
3
+ }
4
+
5
+ interface ImportMeta {
6
+ readonly env: ImportMetaEnv
7
+ }
package/dist/ui/web.js CHANGED
@@ -1,48 +1,84 @@
1
1
  import './internal/telemetry.ts'
2
- import './internal/checkSocket.ts'
2
+ import './internal/queryEngine.ts'
3
+ import './internal/runSocket.ts'
4
+ import {getInstanceByDom} from 'echarts'
5
+
3
6
  import './app.css'
4
- import {mount} from 'svelte'
5
- import LocalApp from './internal/LocalApp.svelte'
7
+ import {mount, unmount} from 'svelte'
6
8
 
7
- import Area from './components/Area.svelte'
8
9
  import AreaChart from './components/AreaChart.svelte'
9
- import Bar from './components/Bar.svelte'
10
10
  import BarChart from './components/BarChart.svelte'
11
11
  import BigValue from './components/BigValue.svelte'
12
- import Chart from './components/Chart.svelte'
13
12
  import Column from './components/Column.svelte'
14
13
  import DateRange from './components/DateRange.svelte'
15
14
  import Dropdown from './components/Dropdown.svelte'
16
15
  import DropdownOption from './components/DropdownOption.svelte'
17
16
  import ECharts from './components/ECharts.svelte'
18
- import ErrorChart from './components/ErrorChart.svelte'
19
17
  import GrapheneQuery from './components/GrapheneQuery.svelte'
20
18
  import InlineDelta from './components/InlineDelta.svelte'
21
- import Line from './components/Line.svelte'
22
19
  import LineChart from './components/LineChart.svelte'
23
20
  import PieChart from './components/PieChart.svelte'
24
21
  import QueryLoad from './components/QueryLoad.svelte'
25
22
  import Row from './components/Row.svelte'
23
+ import ScatterPlot from './components/ScatterPlot.svelte'
26
24
  import SortIcon from './components/SortIcon.svelte'
27
25
  import Table from './components/Table.svelte'
28
26
  import TableCell from './components/TableCell.svelte'
29
27
  import TableGroupRow from './components/TableGroupRow.svelte'
30
28
  import TableGroupToggle from './components/TableGroupToggle.svelte'
29
+ import TableHarness from './components/TableHarness.svelte'
31
30
  import TableHeader from './components/TableHeader.svelte'
32
31
  import TableRow from './components/TableRow.svelte'
33
32
  import TableSubtotalRow from './components/TableSubtotalRow.svelte'
34
33
  import TableTotalRow from './components/TableTotalRow.svelte'
35
34
  import TextInput from './components/TextInput.svelte'
35
+ import Value from './components/Value.svelte'
36
+ import ErrorChart from './internal/ErrorDisplay.svelte'
37
+ import LocalApp from './internal/LocalApp.svelte'
36
38
 
39
+ // Having a global $GRAPHENE allows us to provide an api that pages can use without having to import and bundle a bunch of components.
40
+ // That means that as you navigate around, we only have to a very small amount of js for the page itself, and the bulk of the container and component
41
+ // code only has to load once.
42
+ // In theory we could do this with Vite splitting, but then we have a hard dependency on the exact format vite uses. Plus I find the easier to understand.
37
43
  window.$GRAPHENE = window.$GRAPHENE || {}
38
44
 
45
+ let nextRenderId = 0
46
+ let pendingRenders = new Set()
47
+
48
+ window.$GRAPHENE.getChart = domNode => {
49
+ return getInstanceByDom(domNode)
50
+ }
51
+
52
+ window.$GRAPHENE.renderStart = id => {
53
+ let renderId = id == null ? `render:${++nextRenderId}` : String(id)
54
+ pendingRenders.add(renderId)
55
+ return renderId
56
+ }
57
+
58
+ window.$GRAPHENE.renderComplete = id => {
59
+ if (id == null) return
60
+ pendingRenders.delete(String(id))
61
+ }
62
+
63
+ window.$GRAPHENE.waitForLoad = async (timeout = 20_000) => {
64
+ let g = window.$GRAPHENE
65
+ let end = Date.now() + timeout
66
+ while (Date.now() < end) {
67
+ if (!g.isQueryLoading() && pendingRenders.size == 0) {
68
+ if (document.fonts?.ready) await document.fonts.ready
69
+ await new Promise(resolve => setTimeout(resolve, 300))
70
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
71
+ if (!g.isQueryLoading() && pendingRenders.size == 0) return true
72
+ }
73
+ await new Promise(resolve => setTimeout(resolve, 100))
74
+ }
75
+ return false
76
+ }
77
+
39
78
  window.$GRAPHENE.components = {
40
- Area,
41
79
  AreaChart,
42
- Bar,
43
80
  BarChart,
44
81
  BigValue,
45
- Chart,
46
82
  Column,
47
83
  DateRange,
48
84
  Dropdown,
@@ -51,21 +87,27 @@ window.$GRAPHENE.components = {
51
87
  ErrorChart,
52
88
  GrapheneQuery,
53
89
  InlineDelta,
54
- Line,
55
90
  LineChart,
56
91
  PieChart,
57
92
  QueryLoad,
58
93
  Row,
94
+ ScatterPlot,
59
95
  SortIcon,
60
96
  Table,
61
97
  TableCell,
62
98
  TableGroupRow,
63
99
  TableGroupToggle,
64
100
  TableHeader,
101
+ TableHarness,
65
102
  TableRow,
66
103
  TableSubtotalRow,
67
104
  TableTotalRow,
68
105
  TextInput,
106
+ Value,
69
107
  }
70
108
 
71
- mount(LocalApp, {target: document.body})
109
+ window.$GRAPHENE.svelte = {mount, unmount}
110
+
111
+ if (window.location.pathname.replace(/\/+$/, '') !== '/__ct') {
112
+ mount(LocalApp, {target: document.body})
113
+ }