@graphenedata/cli 0.0.15 → 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 (117) hide show
  1. package/README.md +138 -0
  2. package/dist/cli/bigQuery-I3F46SC6.js +75 -0
  3. package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
  4. package/dist/cli/chunk-OVWODUTJ.js +12849 -0
  5. package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
  6. package/dist/cli/chunk-QAXEOZ43.js +53 -0
  7. package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
  8. package/dist/cli/cli.js +234 -11197
  9. package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
  10. package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
  11. package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
  12. package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
  13. package/dist/cli/serve2-TNN5EROW.js +447 -0
  14. package/dist/cli/serve2-TNN5EROW.js.map +7 -0
  15. package/dist/cli/snowflake-MOQB5GA4.js +128 -0
  16. package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
  17. package/dist/index.d.ts +63 -0
  18. package/dist/lang/index.d.ts +63 -0
  19. package/dist/skills/graphene/SKILL.md +150 -96
  20. package/dist/skills/graphene/references/big-value.md +6 -41
  21. package/dist/skills/graphene/references/date-range.md +64 -0
  22. package/dist/skills/graphene/references/dropdown.md +3 -4
  23. package/dist/skills/graphene/references/echarts.md +162 -0
  24. package/dist/skills/graphene/references/gsql.md +55 -25
  25. package/dist/skills/graphene/references/model-gsql.md +72 -0
  26. package/dist/skills/graphene/references/table.md +13 -14
  27. package/dist/skills/graphene/references/text-input.md +2 -1
  28. package/dist/ui/app.css +239 -340
  29. package/dist/ui/component-utilities/dataShaping.ts +484 -0
  30. package/dist/ui/component-utilities/dataSummary.ts +57 -0
  31. package/dist/ui/component-utilities/enrich.ts +763 -0
  32. package/dist/ui/component-utilities/format.ts +177 -0
  33. package/dist/ui/component-utilities/inputUtils.ts +44 -8
  34. package/dist/ui/component-utilities/theme.ts +200 -0
  35. package/dist/ui/component-utilities/themeStores.ts +21 -8
  36. package/dist/ui/component-utilities/types.ts +70 -0
  37. package/dist/ui/components/AreaChart.svelte +57 -105
  38. package/dist/ui/components/BarChart.svelte +71 -129
  39. package/dist/ui/components/BigValue.svelte +24 -40
  40. package/dist/ui/components/Column.svelte +10 -18
  41. package/dist/ui/components/DateRange.svelte +54 -21
  42. package/dist/ui/components/Dropdown.svelte +47 -26
  43. package/dist/ui/components/DropdownOption.svelte +1 -2
  44. package/dist/ui/components/ECharts.svelte +181 -67
  45. package/dist/ui/components/InlineDelta.svelte +50 -31
  46. package/dist/ui/components/LineChart.svelte +54 -125
  47. package/dist/ui/components/PieChart.svelte +27 -37
  48. package/dist/ui/components/QueryLoad.svelte +77 -45
  49. package/dist/ui/components/Row.svelte +2 -1
  50. package/dist/ui/components/ScatterPlot.svelte +52 -0
  51. package/dist/ui/components/Skeleton.svelte +32 -0
  52. package/dist/ui/components/Table.svelte +3 -2
  53. package/dist/ui/components/TableGroupRow.svelte +28 -36
  54. package/dist/ui/components/TableHarness.svelte +32 -0
  55. package/dist/ui/components/TableHeader.svelte +34 -59
  56. package/dist/ui/components/TableRow.svelte +14 -38
  57. package/dist/ui/components/TableSubtotalRow.svelte +18 -21
  58. package/dist/ui/components/TableTotalRow.svelte +27 -37
  59. package/dist/ui/components/TextInput.svelte +13 -12
  60. package/dist/ui/components/Value.svelte +25 -0
  61. package/dist/ui/components/_Table.svelte +72 -70
  62. package/dist/ui/internal/ChartGallery.svelte +527 -0
  63. package/dist/ui/internal/ErrorDisplay.svelte +22 -97
  64. package/dist/ui/internal/LocalApp.svelte +80 -17
  65. package/dist/ui/internal/PageNavGroup.svelte +269 -0
  66. package/dist/ui/internal/Sidebar.svelte +178 -0
  67. package/dist/ui/internal/SidebarToggle.svelte +47 -0
  68. package/dist/ui/internal/StyleGallery.svelte +244 -0
  69. package/dist/ui/internal/clientCache.ts +2 -2
  70. package/dist/ui/internal/pageInputs.svelte.js +292 -0
  71. package/dist/ui/internal/queryEngine.ts +102 -117
  72. package/dist/ui/internal/runSocket.ts +32 -12
  73. package/dist/ui/internal/sidebar.svelte.js +18 -0
  74. package/dist/ui/internal/telemetry.ts +51 -16
  75. package/dist/ui/internal/types.d.ts +7 -0
  76. package/dist/ui/web.js +28 -11
  77. package/package.json +36 -38
  78. package/dist/skills/graphene/references/area-chart.md +0 -95
  79. package/dist/skills/graphene/references/bar-chart.md +0 -112
  80. package/dist/skills/graphene/references/line-chart.md +0 -108
  81. package/dist/skills/graphene/references/pie-chart.md +0 -29
  82. package/dist/skills/graphene/references/value-formats.md +0 -104
  83. package/dist/ui/component-utilities/autoFormatting.js +0 -280
  84. package/dist/ui/component-utilities/builtInFormats.js +0 -481
  85. package/dist/ui/component-utilities/chartContext.js +0 -12
  86. package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
  87. package/dist/ui/component-utilities/checkInputs.js +0 -84
  88. package/dist/ui/component-utilities/convert.js +0 -15
  89. package/dist/ui/component-utilities/dateParsing.js +0 -56
  90. package/dist/ui/component-utilities/dropdownContext.ts +0 -1
  91. package/dist/ui/component-utilities/echarts.js +0 -252
  92. package/dist/ui/component-utilities/echartsThemes.js +0 -443
  93. package/dist/ui/component-utilities/formatTitle.js +0 -24
  94. package/dist/ui/component-utilities/formatting.js +0 -241
  95. package/dist/ui/component-utilities/getColumnExtents.js +0 -79
  96. package/dist/ui/component-utilities/getColumnSummary.js +0 -62
  97. package/dist/ui/component-utilities/getCompletedData.js +0 -122
  98. package/dist/ui/component-utilities/getDistinctCount.js +0 -7
  99. package/dist/ui/component-utilities/getDistinctValues.js +0 -15
  100. package/dist/ui/component-utilities/getSeriesConfig.js +0 -231
  101. package/dist/ui/component-utilities/getSortedData.js +0 -9
  102. package/dist/ui/component-utilities/getStackPercentages.js +0 -45
  103. package/dist/ui/component-utilities/getStackedData.js +0 -19
  104. package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
  105. package/dist/ui/component-utilities/globalContexts.js +0 -1
  106. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
  107. package/dist/ui/component-utilities/replaceNulls.js +0 -16
  108. package/dist/ui/component-utilities/tableUtils.ts +0 -107
  109. package/dist/ui/component-utilities/tidyWithTypes.js +0 -9
  110. package/dist/ui/components/Area.svelte +0 -214
  111. package/dist/ui/components/Bar.svelte +0 -347
  112. package/dist/ui/components/Chart.svelte +0 -995
  113. package/dist/ui/components/Line.svelte +0 -227
  114. package/dist/ui/internal/NavSidebar.svelte +0 -396
  115. package/dist/ui/internal/theme.ts +0 -60
  116. package/dist/ui/public/inter-latin-ext.woff2 +0 -0
  117. 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,27 +16,34 @@ 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
21
+ }
22
+
23
+ export interface QueryRequest {
24
+ params: ParamSnapshot
25
+ gsql: string
26
+ hashes: string[]
27
+ repoId: string
28
28
  }
29
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
34
  let queryResults = {} as Record<string, {rows: any[]; fields?: Field[]}>
34
35
 
36
+ let queryFetcher: QueryFetcher = fetchWithCache
37
+ export const setQueryFetcher = f => (queryFetcher = f)
38
+
39
+ // Called by GrapheneQuery tags to register a named query on the page
35
40
  function registerQuery(name: string, contents: string) {
36
41
  queries = queries.filter(q => q.name !== name)
37
- queries.push({name, contents, loading: false, fields: new Map(), errors: []})
38
- }
39
-
40
- const getRoutePath = () => (typeof window === 'undefined' ? '/' : window.location.pathname || '/')
41
-
42
- function updateParam(name: string, value: any) {
43
- params[name] = value
44
- runAll() // for now, do the easy thing and reload it all
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[] = []
@@ -57,8 +56,9 @@ function query(source: string, fields: Record<string, string | string[]>, callba
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
64
  function unsubscribe(callback: ResultHandler) {
@@ -66,71 +66,66 @@ function unsubscribe(callback: ResultHandler) {
66
66
  }
67
67
 
68
68
  function resetQueryEngine() {
69
- params = {}
70
69
  queries = []
71
- queryResults = {}
70
+ Object.keys(queryResults).forEach(key => delete queryResults[key])
71
+ getActivePageInputs().reset()
72
72
  }
73
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.
74
76
  async function runNode(n: QueryNode) {
75
77
  if (!n.callback) throw new Error('running node nobody is listening to')
76
- n.callback({}) // notify that the query is loading
78
+
79
+ n.callback() // notifies listeners we're back in the loading state
77
80
  n.loading = true
78
- n.errors = []
81
+ n.error = undefined
79
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.
80
85
  let hashes = await getHashes()
81
86
  let tables = queries.filter(q => q.name)
82
87
  let gsql = [...tables.map(q => `table ${q.name} as (${q.contents})`), n.contents].join('\n')
88
+ let params = getActivePageInputs().getParams()
83
89
 
84
90
  try {
85
- let response = await fetch('/_api/query', {
86
- method: 'POST',
87
- headers: {'Content-Type': 'application/json'},
88
- body: JSON.stringify({params, gsql, hashes, routePath: getRoutePath(), repoId: window.$GRAPHENE?.repoId}),
89
- })
90
- let hash = response.headers.get('ETag') || ''
91
-
92
- if (response.status == 304) {
93
- // cache hit. Read it out and use that
94
- let body = await cacheRead(hash)
95
- let result = translateData(body, n)
96
- if (n.source) queryResults[n.source] = {rows: result.rows, fields: body.fields}
97
- n.callback(result)
98
- } else if (response.ok) {
99
- // cache miss. write it to the cache, and return the data
100
- cacheWrite(hash, response.clone()) // clone allows us to write the raw response into the cache
101
- let body = await response.json()
102
- let fields = body.fields // grab before translateData mutates
103
- let result = translateData(body, n) // nb that translateData modifies in place for performance
104
- if (n.source) queryResults[n.source] = {rows: result.rows, fields}
105
- n.callback(result)
106
- } else {
107
- // request failed. Record it
108
- let contentType = response.headers.get('Content-Type') || ''
109
- let isJson = contentType.includes('application/json')
110
- let body = isJson ? await response.json() : await response.text()
111
- n.errors = Array.isArray(body) ? body : [{message: body}]
112
-
113
- let fieldIds = Array.from(n.fields.entries()).flatMap(([name, val]) => {
114
- if (Array.isArray(val)) {
115
- if (val.length === 0) return [] as string[]
116
- if (val.length === 1) return [`${name}="${val[0]}"`]
117
- return [`${name}="${val.join(', ')}"`]
118
- }
119
- if (typeof val === 'string' && val.trim().length === 0) return [] as string[]
120
- if (val == null) return [] as string[]
121
- return [`${name}="${val}"`]
122
- })
123
- let idStr = `Query (data="${n.source}" ` + fieldIds.join(' ') + ')'
124
- n.errors.forEach(e => ((e as any).queryId = idStr))
125
- n.callback({errors: n.errors})
126
- }
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)
127
95
  } catch (e) {
128
- 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: ''})
129
100
  } finally {
130
101
  n.loading = false
131
102
  }
132
103
  }
133
104
 
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
+
134
129
  function runAll() {
135
130
  if (runPending) return runPending
136
131
  runPending = Promise.resolve()
@@ -147,63 +142,53 @@ async function _runAll() {
147
142
  )
148
143
  }
149
144
 
150
- 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 {
151
147
  let rows = data.rows || []
152
- rows.dataLoaded = true // evidence components need this to be set
153
- rows._evidenceColumnTypes = []
154
- let requestFields: string[] = []
155
- node.fields.forEach(value => {
156
- if (Array.isArray(value)) requestFields.push(...value)
157
- else requestFields.push(value)
158
- })
148
+ let fields: Field[] = []
149
+
150
+ let requestFields = Array.from(node.fields.values()).flatMap(f => f)
159
151
 
160
152
  data.fields.forEach((field, index) => {
161
- 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
+ }
162
161
 
163
- // server gives names like `col_1` to unnamed expressions but we translate it back into the original expression like `avg(price)`
164
- if (field.name.match(/col_\d+/)) {
165
- 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) {
166
165
  rows.forEach(r => {
167
- r[name] = r[field.name]
168
- delete r[field.name]
166
+ r[requested] = r[rowKey]
167
+ delete r[rowKey]
169
168
  })
170
169
  }
171
170
 
172
- // map graphene types down to the ones evidence expects
173
- rows._evidenceColumnTypes.push({name, evidenceType: evidenceType(field.type)})
171
+ fields.push({...field, name})
174
172
  })
175
173
 
176
- return {rows}
174
+ return {rows, fields}
177
175
  }
178
176
 
179
177
  const isQueryLoading = () => !!queries.find(q => q.loading)
180
178
 
181
- errorProvider('queryEngine', () => {
182
- let unique = {}
183
- queries
184
- .flatMap(q => q.errors)
185
- .filter(q => !!q)
186
- .forEach(e => {
187
- unique[e.message + String((e as any).from?.lineText)] = e
188
- })
189
- return Object.values(unique) as Error[]
190
- })
191
-
192
- function evidenceType(type: string | undefined) {
193
- if (type === 'string') return 'string'
194
- if (type === 'number') return 'number'
195
- if (type === 'boolean') return 'boolean'
196
- if (type === 'date' || type === 'timestamp') return 'date'
197
- console.warn('Unsupported evidence type ' + type)
198
- return 'string'
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,
193
+ })
199
194
  }
200
-
201
- Object.assign(window.$GRAPHENE, {
202
- registerQuery,
203
- updateParam,
204
- query,
205
- unsubscribe,
206
- resetQueryEngine,
207
- isQueryLoading,
208
- queryResults,
209
- })
@@ -6,18 +6,32 @@ import {getErrors} from './telemetry.ts'
6
6
  let socket: WebSocket | null = null
7
7
  connect()
8
8
 
9
- function captureChart(chartTitle: string) {
10
- let escaped = window.CSS.escape(chartTitle)
11
- let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`) as HTMLCanvasElement | null
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})
12
23
  return canvas?.toDataURL('image/png')
13
24
  }
14
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
+
15
32
  async function takeScreenshot() {
16
- if (!(window as any).html2canvas) {
17
- let html2canvas = await import('@graphenedata/html2canvas')
18
- ;(window as any).html2canvas = html2canvas.default
19
- }
20
- let canvas = await (window as any).html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
33
+ await loadHtml2Canvas()
34
+ let canvas = await html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
21
35
  return canvas?.toDataURL('image/png')
22
36
  }
23
37
 
@@ -28,12 +42,18 @@ function connect() {
28
42
  socket.onopen = () => socket!.send(JSON.stringify({type: 'register', url: window.location.href}))
29
43
 
30
44
  socket.onmessage = async event => {
31
- let {type, requestId, chart} = JSON.parse(event.data)
45
+ let {type, requestId, action, chart} = JSON.parse(event.data)
32
46
  if (type !== 'check') return
33
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
+
34
55
  let finished = await window.$GRAPHENE.waitForLoad(20_000)
35
- let errors = getErrors().map((e: any) => ({type: e.type, message: e.message, queryId: e.queryId, file: e.file, line: e.loc?.line, frame: e.frame, from: e.from, to: e.to}))
36
- let screenshot = chart ? captureChart(chart) : await takeScreenshot()
37
- socket!.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading: !finished, screenshot}))
56
+ let screenshot = chart ? await captureChart(chart) : await takeScreenshot()
57
+ socket!.send(JSON.stringify({type: 'checkResponse', requestId, errors: getErrors(), stillLoading: !finished, screenshot}))
38
58
  }
39
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
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,15 +1,14 @@
1
1
  import './internal/telemetry.ts'
2
2
  import './internal/queryEngine.ts'
3
3
  import './internal/runSocket.ts'
4
+ import {getInstanceByDom} from 'echarts'
5
+
4
6
  import './app.css'
5
- import {mount} from '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'
@@ -17,29 +16,39 @@ import DropdownOption from './components/DropdownOption.svelte'
17
16
  import ECharts from './components/ECharts.svelte'
18
17
  import GrapheneQuery from './components/GrapheneQuery.svelte'
19
18
  import InlineDelta from './components/InlineDelta.svelte'
20
- import Line from './components/Line.svelte'
21
19
  import LineChart from './components/LineChart.svelte'
22
20
  import PieChart from './components/PieChart.svelte'
23
21
  import QueryLoad from './components/QueryLoad.svelte'
24
22
  import Row from './components/Row.svelte'
23
+ import ScatterPlot from './components/ScatterPlot.svelte'
25
24
  import SortIcon from './components/SortIcon.svelte'
26
25
  import Table from './components/Table.svelte'
27
26
  import TableCell from './components/TableCell.svelte'
28
27
  import TableGroupRow from './components/TableGroupRow.svelte'
29
28
  import TableGroupToggle from './components/TableGroupToggle.svelte'
29
+ import TableHarness from './components/TableHarness.svelte'
30
30
  import TableHeader from './components/TableHeader.svelte'
31
31
  import TableRow from './components/TableRow.svelte'
32
32
  import TableSubtotalRow from './components/TableSubtotalRow.svelte'
33
33
  import TableTotalRow from './components/TableTotalRow.svelte'
34
34
  import TextInput from './components/TextInput.svelte'
35
+ import Value from './components/Value.svelte'
35
36
  import ErrorChart from './internal/ErrorDisplay.svelte'
36
37
  import LocalApp from './internal/LocalApp.svelte'
37
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.
38
43
  window.$GRAPHENE = window.$GRAPHENE || {}
39
44
 
40
45
  let nextRenderId = 0
41
46
  let pendingRenders = new Set()
42
47
 
48
+ window.$GRAPHENE.getChart = domNode => {
49
+ return getInstanceByDom(domNode)
50
+ }
51
+
43
52
  window.$GRAPHENE.renderStart = id => {
44
53
  let renderId = id == null ? `render:${++nextRenderId}` : String(id)
45
54
  pendingRenders.add(renderId)
@@ -55,19 +64,21 @@ window.$GRAPHENE.waitForLoad = async (timeout = 20_000) => {
55
64
  let g = window.$GRAPHENE
56
65
  let end = Date.now() + timeout
57
66
  while (Date.now() < end) {
58
- if (!g.isQueryLoading() && pendingRenders.size == 0) return true
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
+ }
59
73
  await new Promise(resolve => setTimeout(resolve, 100))
60
74
  }
61
75
  return false
62
76
  }
63
77
 
64
78
  window.$GRAPHENE.components = {
65
- Area,
66
79
  AreaChart,
67
- Bar,
68
80
  BarChart,
69
81
  BigValue,
70
- Chart,
71
82
  Column,
72
83
  DateRange,
73
84
  Dropdown,
@@ -76,21 +87,27 @@ window.$GRAPHENE.components = {
76
87
  ErrorChart,
77
88
  GrapheneQuery,
78
89
  InlineDelta,
79
- Line,
80
90
  LineChart,
81
91
  PieChart,
82
92
  QueryLoad,
83
93
  Row,
94
+ ScatterPlot,
84
95
  SortIcon,
85
96
  Table,
86
97
  TableCell,
87
98
  TableGroupRow,
88
99
  TableGroupToggle,
89
100
  TableHeader,
101
+ TableHarness,
90
102
  TableRow,
91
103
  TableSubtotalRow,
92
104
  TableTotalRow,
93
105
  TextInput,
106
+ Value,
94
107
  }
95
108
 
96
- 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
+ }