@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.
- package/README.md +138 -0
- package/dist/cli/bigQuery-I3F46SC6.js +75 -0
- package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
- package/dist/cli/chunk-OVWODUTJ.js +12849 -0
- package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
- package/dist/cli/chunk-QAXEOZ43.js +53 -0
- package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
- package/dist/cli/cli.js +234 -11197
- package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
- package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
- package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
- package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
- package/dist/cli/serve2-TNN5EROW.js +447 -0
- package/dist/cli/serve2-TNN5EROW.js.map +7 -0
- package/dist/cli/snowflake-MOQB5GA4.js +128 -0
- package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
- package/dist/index.d.ts +63 -0
- package/dist/lang/index.d.ts +63 -0
- package/dist/skills/graphene/SKILL.md +150 -96
- package/dist/skills/graphene/references/big-value.md +6 -41
- package/dist/skills/graphene/references/date-range.md +64 -0
- package/dist/skills/graphene/references/dropdown.md +3 -4
- package/dist/skills/graphene/references/echarts.md +162 -0
- package/dist/skills/graphene/references/gsql.md +55 -25
- package/dist/skills/graphene/references/model-gsql.md +72 -0
- package/dist/skills/graphene/references/table.md +13 -14
- package/dist/skills/graphene/references/text-input.md +2 -1
- package/dist/ui/app.css +239 -340
- package/dist/ui/component-utilities/dataShaping.ts +484 -0
- package/dist/ui/component-utilities/dataSummary.ts +57 -0
- package/dist/ui/component-utilities/enrich.ts +763 -0
- package/dist/ui/component-utilities/format.ts +177 -0
- package/dist/ui/component-utilities/inputUtils.ts +44 -8
- package/dist/ui/component-utilities/theme.ts +200 -0
- package/dist/ui/component-utilities/themeStores.ts +21 -8
- package/dist/ui/component-utilities/types.ts +70 -0
- package/dist/ui/components/AreaChart.svelte +57 -105
- package/dist/ui/components/BarChart.svelte +71 -129
- package/dist/ui/components/BigValue.svelte +24 -40
- package/dist/ui/components/Column.svelte +10 -18
- package/dist/ui/components/DateRange.svelte +54 -21
- package/dist/ui/components/Dropdown.svelte +47 -26
- package/dist/ui/components/DropdownOption.svelte +1 -2
- package/dist/ui/components/ECharts.svelte +181 -67
- package/dist/ui/components/InlineDelta.svelte +50 -31
- package/dist/ui/components/LineChart.svelte +54 -125
- package/dist/ui/components/PieChart.svelte +27 -37
- package/dist/ui/components/QueryLoad.svelte +77 -45
- package/dist/ui/components/Row.svelte +2 -1
- package/dist/ui/components/ScatterPlot.svelte +52 -0
- package/dist/ui/components/Skeleton.svelte +32 -0
- package/dist/ui/components/Table.svelte +3 -2
- package/dist/ui/components/TableGroupRow.svelte +28 -36
- package/dist/ui/components/TableHarness.svelte +32 -0
- package/dist/ui/components/TableHeader.svelte +34 -59
- package/dist/ui/components/TableRow.svelte +14 -38
- package/dist/ui/components/TableSubtotalRow.svelte +18 -21
- package/dist/ui/components/TableTotalRow.svelte +27 -37
- package/dist/ui/components/TextInput.svelte +13 -12
- package/dist/ui/components/Value.svelte +25 -0
- package/dist/ui/components/_Table.svelte +72 -70
- package/dist/ui/internal/ChartGallery.svelte +527 -0
- package/dist/ui/internal/ErrorDisplay.svelte +22 -97
- package/dist/ui/internal/LocalApp.svelte +80 -17
- package/dist/ui/internal/PageNavGroup.svelte +269 -0
- package/dist/ui/internal/Sidebar.svelte +178 -0
- package/dist/ui/internal/SidebarToggle.svelte +47 -0
- package/dist/ui/internal/StyleGallery.svelte +244 -0
- package/dist/ui/internal/clientCache.ts +2 -2
- package/dist/ui/internal/pageInputs.svelte.js +292 -0
- package/dist/ui/internal/queryEngine.ts +102 -117
- package/dist/ui/internal/runSocket.ts +32 -12
- package/dist/ui/internal/sidebar.svelte.js +18 -0
- package/dist/ui/internal/telemetry.ts +51 -16
- package/dist/ui/internal/types.d.ts +7 -0
- package/dist/ui/web.js +28 -11
- package/package.json +36 -38
- package/dist/skills/graphene/references/area-chart.md +0 -95
- package/dist/skills/graphene/references/bar-chart.md +0 -112
- package/dist/skills/graphene/references/line-chart.md +0 -108
- package/dist/skills/graphene/references/pie-chart.md +0 -29
- package/dist/skills/graphene/references/value-formats.md +0 -104
- package/dist/ui/component-utilities/autoFormatting.js +0 -280
- package/dist/ui/component-utilities/builtInFormats.js +0 -481
- package/dist/ui/component-utilities/chartContext.js +0 -12
- package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
- package/dist/ui/component-utilities/checkInputs.js +0 -84
- package/dist/ui/component-utilities/convert.js +0 -15
- package/dist/ui/component-utilities/dateParsing.js +0 -56
- package/dist/ui/component-utilities/dropdownContext.ts +0 -1
- package/dist/ui/component-utilities/echarts.js +0 -252
- package/dist/ui/component-utilities/echartsThemes.js +0 -443
- package/dist/ui/component-utilities/formatTitle.js +0 -24
- package/dist/ui/component-utilities/formatting.js +0 -241
- package/dist/ui/component-utilities/getColumnExtents.js +0 -79
- package/dist/ui/component-utilities/getColumnSummary.js +0 -62
- package/dist/ui/component-utilities/getCompletedData.js +0 -122
- package/dist/ui/component-utilities/getDistinctCount.js +0 -7
- package/dist/ui/component-utilities/getDistinctValues.js +0 -15
- package/dist/ui/component-utilities/getSeriesConfig.js +0 -231
- package/dist/ui/component-utilities/getSortedData.js +0 -9
- package/dist/ui/component-utilities/getStackPercentages.js +0 -45
- package/dist/ui/component-utilities/getStackedData.js +0 -19
- package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
- package/dist/ui/component-utilities/globalContexts.js +0 -1
- package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
- package/dist/ui/component-utilities/replaceNulls.js +0 -16
- package/dist/ui/component-utilities/tableUtils.ts +0 -107
- package/dist/ui/component-utilities/tidyWithTypes.js +0 -9
- package/dist/ui/components/Area.svelte +0 -214
- package/dist/ui/components/Bar.svelte +0 -347
- package/dist/ui/components/Chart.svelte +0 -995
- package/dist/ui/components/Line.svelte +0 -227
- package/dist/ui/internal/NavSidebar.svelte +0 -396
- package/dist/ui/internal/theme.ts +0 -60
- package/dist/ui/public/inter-latin-ext.woff2 +0 -0
- 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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
78
|
+
|
|
79
|
+
n.callback() // notifies listeners we're back in the loading state
|
|
77
80
|
n.loading = true
|
|
78
|
-
n.
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
let requestFields
|
|
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
|
|
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
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
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[
|
|
168
|
-
delete r[
|
|
166
|
+
r[requested] = r[rowKey]
|
|
167
|
+
delete r[rowKey]
|
|
169
168
|
})
|
|
170
169
|
}
|
|
171
170
|
|
|
172
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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:
|
|
7
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
24
|
-
|
|
56
|
+
export function getErrors(): GrapheneError[] {
|
|
57
|
+
return staticErrors.concat(Array.from(componentErrors.values()))
|
|
25
58
|
}
|
|
26
59
|
|
|
27
|
-
export function
|
|
28
|
-
let
|
|
29
|
-
|
|
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
|
}
|
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)
|
|
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
|
|
109
|
+
window.$GRAPHENE.svelte = {mount, unmount}
|
|
110
|
+
|
|
111
|
+
if (window.location.pathname.replace(/\/+$/, '') !== '/__ct') {
|
|
112
|
+
mount(LocalApp, {target: document.body})
|
|
113
|
+
}
|