@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.
- package/LICENSE.md +3 -3
- package/README.md +138 -0
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/bin.js +2 -2
- 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 +245 -10290
- 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 +235 -0
- package/dist/skills/graphene/references/big-value.md +20 -0
- package/dist/skills/graphene/references/date-range.md +64 -0
- package/dist/skills/graphene/references/dropdown.md +62 -0
- package/dist/skills/graphene/references/echarts.md +162 -0
- package/dist/skills/graphene/references/gsql.md +393 -0
- package/dist/skills/graphene/references/model-gsql.md +72 -0
- package/dist/skills/graphene/references/table.md +143 -0
- package/dist/skills/graphene/references/text-input.md +29 -0
- package/dist/ui/app.css +263 -299
- 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 +48 -9
- package/dist/ui/component-utilities/theme.ts +200 -0
- package/dist/ui/component-utilities/themeStores.ts +26 -21
- 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 +11 -19
- package/dist/ui/components/DateRange.svelte +71 -34
- package/dist/ui/components/Dropdown.svelte +82 -49
- package/dist/ui/components/DropdownOption.svelte +1 -2
- package/dist/ui/components/ECharts.svelte +179 -60
- package/dist/ui/components/InlineDelta.svelte +51 -32
- package/dist/ui/components/LineChart.svelte +54 -125
- package/dist/ui/components/PieChart.svelte +27 -37
- package/dist/ui/components/QueryLoad.svelte +78 -44
- 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 +15 -39
- package/dist/ui/components/TableSubtotalRow.svelte +26 -21
- package/dist/ui/components/TableTotalRow.svelte +27 -37
- package/dist/ui/components/TextInput.svelte +17 -14
- package/dist/ui/components/Value.svelte +25 -0
- package/dist/ui/components/_Table.svelte +80 -76
- package/dist/ui/internal/ChartGallery.svelte +527 -0
- package/dist/ui/internal/ErrorDisplay.svelte +60 -0
- package/dist/ui/internal/LocalApp.svelte +87 -19
- 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 +15 -13
- package/dist/ui/internal/pageInputs.svelte.js +292 -0
- package/dist/ui/internal/queryEngine.ts +124 -132
- package/dist/ui/internal/runSocket.ts +59 -0
- package/dist/ui/internal/sidebar.svelte.js +18 -0
- package/dist/ui/internal/telemetry.ts +52 -17
- package/dist/ui/internal/types.d.ts +7 -0
- package/dist/ui/web.js +55 -13
- package/package.json +40 -41
- package/dist/docs/agent-instructions.md +0 -18
- package/dist/docs/base.md +0 -98
- package/dist/docs/cli.md +0 -22
- package/dist/docs/graphene.md +0 -1462
- package/dist/ui/component-utilities/autoFormatting.js +0 -301
- package/dist/ui/component-utilities/builtInFormats.js +0 -482
- 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 -95
- package/dist/ui/component-utilities/convert.js +0 -15
- package/dist/ui/component-utilities/dateParsing.js +0 -57
- package/dist/ui/component-utilities/dropdownContext.ts +0 -1
- package/dist/ui/component-utilities/echarts.js +0 -272
- package/dist/ui/component-utilities/echartsThemes.js +0 -453
- package/dist/ui/component-utilities/formatTitle.js +0 -24
- package/dist/ui/component-utilities/formatting.js +0 -250
- package/dist/ui/component-utilities/getColumnExtents.js +0 -79
- package/dist/ui/component-utilities/getColumnSummary.js +0 -67
- package/dist/ui/component-utilities/getCompletedData.js +0 -114
- 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 -237
- package/dist/ui/component-utilities/getSortedData.js +0 -7
- package/dist/ui/component-utilities/getStackPercentages.js +0 -43
- package/dist/ui/component-utilities/getStackedData.js +0 -17
- 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 -14
- package/dist/ui/component-utilities/tableUtils.ts +0 -120
- package/dist/ui/components/Area.svelte +0 -214
- package/dist/ui/components/Bar.svelte +0 -350
- package/dist/ui/components/Chart.svelte +0 -989
- package/dist/ui/components/ErrorChart.svelte +0 -118
- package/dist/ui/components/Line.svelte +0 -227
- package/dist/ui/internal/NavSidebar.svelte +0 -396
- package/dist/ui/internal/PageError.svelte +0 -23
- package/dist/ui/internal/checkSocket.ts +0 -48
- package/dist/ui/internal/theme.ts +0 -88
- 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,32 +16,39 @@ interface QueryNode {
|
|
|
24
16
|
callback?: ResultHandler
|
|
25
17
|
loading: boolean
|
|
26
18
|
fields: Map<string, string | string[]>
|
|
27
|
-
|
|
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[]
|
|
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
|
-
|
|
36
|
+
let queryFetcher: QueryFetcher = fetchWithCache
|
|
37
|
+
export const setQueryFetcher = f => (queryFetcher = f)
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
59
|
+
queries.push({contents, callback, loading: false, fields: map, source, componentId})
|
|
61
60
|
runAll()
|
|
61
|
+
return componentId
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function unsubscribe
|
|
64
|
+
function unsubscribe(callback: ResultHandler) {
|
|
65
65
|
queries = queries.filter(q => q.callback !== callback)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
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
|
-
|
|
78
|
+
|
|
79
|
+
n.callback() // notifies listeners we're back in the loading state
|
|
71
80
|
n.loading = true
|
|
72
|
-
n.
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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()
|
|
131
|
+
runPending = Promise.resolve()
|
|
132
|
+
.then(_runAll)
|
|
133
|
+
.finally(() => (runPending = null))
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
async function _runAll
|
|
133
|
-
await Promise.all(
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
let requestFields
|
|
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
|
|
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
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
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[
|
|
157
|
-
delete r[
|
|
166
|
+
r[requested] = r[rowKey]
|
|
167
|
+
delete r[rowKey]
|
|
158
168
|
})
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
window.addEventListener('error',
|
|
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,48 +1,84 @@
|
|
|
1
1
|
import './internal/telemetry.ts'
|
|
2
|
-
import './internal/
|
|
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
|
|
109
|
+
window.$GRAPHENE.svelte = {mount, unmount}
|
|
110
|
+
|
|
111
|
+
if (window.location.pathname.replace(/\/+$/, '') !== '/__ct') {
|
|
112
|
+
mount(LocalApp, {target: document.body})
|
|
113
|
+
}
|