@graphenedata/cli 0.0.1

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 (123) hide show
  1. package/LICENSE.md +100 -0
  2. package/THIRD_PARTY_NOTICES.md +12 -0
  3. package/cli.ts +157 -0
  4. package/dist/cli/cli.js +43 -0
  5. package/dist/docs/data_apps/components/charts/annotations.md +673 -0
  6. package/dist/docs/data_apps/components/charts/area-chart.md +202 -0
  7. package/dist/docs/data_apps/components/charts/bar-chart.md +317 -0
  8. package/dist/docs/data_apps/components/charts/box-plot.md +190 -0
  9. package/dist/docs/data_apps/components/charts/bubble-chart.md +151 -0
  10. package/dist/docs/data_apps/components/charts/calendar-heatmap.md +112 -0
  11. package/dist/docs/data_apps/components/charts/custom-echarts.md +308 -0
  12. package/dist/docs/data_apps/components/charts/echarts-options.md +217 -0
  13. package/dist/docs/data_apps/components/charts/funnel-chart.md +106 -0
  14. package/dist/docs/data_apps/components/charts/heatmap.md +180 -0
  15. package/dist/docs/data_apps/components/charts/histogram.md +107 -0
  16. package/dist/docs/data_apps/components/charts/line-chart.md +265 -0
  17. package/dist/docs/data_apps/components/charts/mixed-type-charts.md +240 -0
  18. package/dist/docs/data_apps/components/charts/sankey-diagram.md +301 -0
  19. package/dist/docs/data_apps/components/charts/scatter-plot.md +134 -0
  20. package/dist/docs/data_apps/components/charts/sparkline.md +68 -0
  21. package/dist/docs/data_apps/components/data/big-value.md +153 -0
  22. package/dist/docs/data_apps/components/data/delta.md +89 -0
  23. package/dist/docs/data_apps/components/data/table.md +470 -0
  24. package/dist/docs/data_apps/components/data/value.md +97 -0
  25. package/dist/docs/data_apps/components/inputs/button-group.md +154 -0
  26. package/dist/docs/data_apps/components/inputs/checkbox.md +52 -0
  27. package/dist/docs/data_apps/components/inputs/date-input.md +131 -0
  28. package/dist/docs/data_apps/components/inputs/date-range.md +124 -0
  29. package/dist/docs/data_apps/components/inputs/dimension-grid.md +67 -0
  30. package/dist/docs/data_apps/components/inputs/dropdown.md +199 -0
  31. package/dist/docs/data_apps/components/inputs/index.md +3 -0
  32. package/dist/docs/data_apps/components/inputs/slider.md +126 -0
  33. package/dist/docs/data_apps/components/inputs/text-input.md +86 -0
  34. package/dist/docs/data_apps/components/maps/area-map.md +397 -0
  35. package/dist/docs/data_apps/components/maps/base-map.md +269 -0
  36. package/dist/docs/data_apps/components/maps/bubble-map.md +361 -0
  37. package/dist/docs/data_apps/components/maps/point-map.md +326 -0
  38. package/dist/docs/data_apps/components/maps/us-map.md +167 -0
  39. package/dist/docs/data_apps/components/ui/accordion.md +116 -0
  40. package/dist/docs/data_apps/components/ui/alert.md +37 -0
  41. package/dist/docs/data_apps/components/ui/big-link.md +19 -0
  42. package/dist/docs/data_apps/components/ui/details.md +58 -0
  43. package/dist/docs/data_apps/components/ui/download-data.md +41 -0
  44. package/dist/docs/data_apps/components/ui/embed.md +47 -0
  45. package/dist/docs/data_apps/components/ui/grid.md +45 -0
  46. package/dist/docs/data_apps/components/ui/image.md +61 -0
  47. package/dist/docs/data_apps/components/ui/info.md +47 -0
  48. package/dist/docs/data_apps/components/ui/last-refreshed.md +28 -0
  49. package/dist/docs/data_apps/components/ui/link-button.md +20 -0
  50. package/dist/docs/data_apps/components/ui/link.md +40 -0
  51. package/dist/docs/data_apps/components/ui/modal.md +57 -0
  52. package/dist/docs/data_apps/components/ui/note.md +32 -0
  53. package/dist/docs/data_apps/components/ui/print-format-components.md +85 -0
  54. package/dist/docs/data_apps/components/ui/tabs.md +122 -0
  55. package/dist/docs/graphene.md +129 -0
  56. package/dist/ui/app.css +332 -0
  57. package/dist/ui/assets/favicon.ico +0 -0
  58. package/dist/ui/component-utilities/autoFormatting.js +301 -0
  59. package/dist/ui/component-utilities/builtInFormats.js +482 -0
  60. package/dist/ui/component-utilities/chartContext.js +12 -0
  61. package/dist/ui/component-utilities/chartWindowDebug.js +21 -0
  62. package/dist/ui/component-utilities/checkInputs.js +95 -0
  63. package/dist/ui/component-utilities/convert.js +15 -0
  64. package/dist/ui/component-utilities/dateParsing.js +57 -0
  65. package/dist/ui/component-utilities/dropdownContext.ts +1 -0
  66. package/dist/ui/component-utilities/echarts.js +262 -0
  67. package/dist/ui/component-utilities/echartsThemes.js +453 -0
  68. package/dist/ui/component-utilities/formatTitle.js +24 -0
  69. package/dist/ui/component-utilities/formatting.js +258 -0
  70. package/dist/ui/component-utilities/getColumnExtents.js +79 -0
  71. package/dist/ui/component-utilities/getColumnSummary.js +67 -0
  72. package/dist/ui/component-utilities/getCompletedData.js +114 -0
  73. package/dist/ui/component-utilities/getDistinctCount.js +7 -0
  74. package/dist/ui/component-utilities/getDistinctValues.js +15 -0
  75. package/dist/ui/component-utilities/getSeriesConfig.js +236 -0
  76. package/dist/ui/component-utilities/getSortedData.js +7 -0
  77. package/dist/ui/component-utilities/getStackPercentages.js +43 -0
  78. package/dist/ui/component-utilities/getStackedData.js +17 -0
  79. package/dist/ui/component-utilities/getYAxisIndex.js +15 -0
  80. package/dist/ui/component-utilities/globalContexts.js +1 -0
  81. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +119 -0
  82. package/dist/ui/component-utilities/inputUtils.ts +25 -0
  83. package/dist/ui/component-utilities/replaceNulls.js +14 -0
  84. package/dist/ui/component-utilities/tableUtils.ts +120 -0
  85. package/dist/ui/component-utilities/themeStores.ts +116 -0
  86. package/dist/ui/components/Area.svelte +174 -0
  87. package/dist/ui/components/AreaChart.svelte +150 -0
  88. package/dist/ui/components/Bar.svelte +326 -0
  89. package/dist/ui/components/BarChart.svelte +194 -0
  90. package/dist/ui/components/BigValue.svelte +69 -0
  91. package/dist/ui/components/Chart.svelte +1070 -0
  92. package/dist/ui/components/Column.svelte +172 -0
  93. package/dist/ui/components/DateRange.svelte +324 -0
  94. package/dist/ui/components/Dropdown.svelte +738 -0
  95. package/dist/ui/components/DropdownOption.svelte +21 -0
  96. package/dist/ui/components/ECharts.svelte +77 -0
  97. package/dist/ui/components/ErrorChart.svelte +54 -0
  98. package/dist/ui/components/GrapheneQuery.svelte +12 -0
  99. package/dist/ui/components/InlineDelta.svelte +150 -0
  100. package/dist/ui/components/Line.svelte +210 -0
  101. package/dist/ui/components/LineChart.svelte +178 -0
  102. package/dist/ui/components/PieChart.svelte +36 -0
  103. package/dist/ui/components/QueryLoad.svelte +82 -0
  104. package/dist/ui/components/Row.svelte +14 -0
  105. package/dist/ui/components/SortIcon.svelte +32 -0
  106. package/dist/ui/components/Table.svelte +19 -0
  107. package/dist/ui/components/TableCell.svelte +75 -0
  108. package/dist/ui/components/TableGroupRow.svelte +136 -0
  109. package/dist/ui/components/TableGroupToggle.svelte +42 -0
  110. package/dist/ui/components/TableHeader.svelte +242 -0
  111. package/dist/ui/components/TableRow.svelte +283 -0
  112. package/dist/ui/components/TableSubtotalRow.svelte +62 -0
  113. package/dist/ui/components/TableTotalRow.svelte +88 -0
  114. package/dist/ui/components/TextInput.svelte +92 -0
  115. package/dist/ui/components/_Table.svelte +516 -0
  116. package/dist/ui/internal/clientCache.ts +43 -0
  117. package/dist/ui/internal/queryEngine.ts +169 -0
  118. package/dist/ui/internal/telemetry.ts +28 -0
  119. package/dist/ui/internal/theme.ts +88 -0
  120. package/dist/ui/layout.svelte +3 -0
  121. package/dist/ui/playwright.config.ts +30 -0
  122. package/dist/ui/web.js +106 -0
  123. package/package.json +71 -0
@@ -0,0 +1,516 @@
1
+ <script lang="ts">
2
+ import {writable} from 'svelte/store'
3
+ import {setContext} from 'svelte'
4
+ import {propKey, strictBuild} from '../component-utilities/chartContext.js'
5
+ import getColumnSummary from '../component-utilities/getColumnSummary.js'
6
+ import {convertColumnToDate} from '../component-utilities/dateParsing.js'
7
+ import checkInputs from '../component-utilities/checkInputs.js'
8
+ import ErrorChart from './ErrorChart.svelte'
9
+ import TableHeader from './TableHeader.svelte'
10
+ import TableRow from './TableRow.svelte'
11
+ import TableGroupRow from './TableGroupRow.svelte'
12
+ import TableSubtotalRow from './TableSubtotalRow.svelte'
13
+ import TableTotalRow from './TableTotalRow.svelte'
14
+ import Column from './Column.svelte'
15
+ import {getFinalColumnOrder} from '../component-utilities/tableUtils'
16
+ import {getThemeStores} from '../component-utilities/themeStores'
17
+ import {toBoolean} from '../component-utilities/convert'
18
+
19
+ const {resolveColor} = getThemeStores()
20
+
21
+ export let data: any[] = []
22
+ export let rows: number | string = 10
23
+ export let title: string | undefined = undefined
24
+ export let subtitle: string | undefined = undefined
25
+ export let rowNumbers: boolean | string | undefined = false
26
+ export let sort: string | undefined = undefined
27
+ export let sortable: boolean | string | undefined = true
28
+ export let groupBy: string | undefined = undefined
29
+ export let groupsOpen: boolean | string | undefined = true
30
+ export let groupType: 'accordion' | 'section' = 'accordion'
31
+ export let accordionRowColor: string | undefined = undefined
32
+ export let groupNamePosition: 'top' | 'middle' | 'bottom' = 'middle'
33
+ export let subtotals: boolean | string | undefined = false
34
+ export let subtotalRowColor: string | undefined = undefined
35
+ export let subtotalFontColor: string | undefined = undefined
36
+ export let rowShading: boolean | string | undefined = false
37
+ export let rowLines: boolean | string | undefined = true
38
+ export let wrapTitles: boolean | string | undefined = false
39
+ export let headerColor: string | undefined = undefined
40
+ export let headerFontColor: string | undefined = undefined
41
+ export let formatColumnTitles: boolean | string | undefined = true
42
+ export let backgroundColor: string | undefined = undefined
43
+ export let compact: boolean | string | undefined = undefined
44
+ export let link: string | undefined = undefined
45
+ export let showLinkCol: boolean | string | undefined = false
46
+ export let totalRow: boolean | string | undefined = false
47
+ export let totalRowColor: string | undefined = undefined
48
+ export let totalFontColor: string | undefined = undefined
49
+ export let emptyMessage: string | undefined = undefined
50
+ export let isFullPage: boolean | string | undefined = undefined
51
+
52
+ rows = Number.parseInt(String(rows), 10)
53
+ if (!Number.isFinite(rows) || rows <= 0) rows = 10
54
+
55
+ rowNumbers = toBoolean(rowNumbers) ?? false
56
+ groupsOpen = toBoolean(groupsOpen) ?? true
57
+ subtotals = toBoolean(subtotals) ?? false
58
+ rowShading = toBoolean(rowShading) ?? false
59
+ rowLines = toBoolean(rowLines) ?? true
60
+ wrapTitles = toBoolean(wrapTitles) ?? false
61
+ formatColumnTitles = toBoolean(formatColumnTitles) ?? true
62
+ compact = toBoolean(compact)
63
+ showLinkCol = toBoolean(showLinkCol) ?? false
64
+ totalRow = toBoolean(totalRow) ?? false
65
+ sortable = toBoolean(sortable) ?? true
66
+ isFullPage = toBoolean(isFullPage) ?? false
67
+
68
+ if (groupType === 'section') rowNumbers = false
69
+
70
+ const props = writable<{data: any[]; columns: any[]; priorityColumns:(string | undefined)[]}>({data, columns: [], priorityColumns: [groupBy]})
71
+ setContext(propKey, props)
72
+
73
+ $: props.update((state) => ({...state, data, priorityColumns: [groupBy]}))
74
+
75
+ $: accordionRowColorStore = resolveColor(accordionRowColor)
76
+ $: subtotalRowColorStore = resolveColor(subtotalRowColor)
77
+ $: subtotalFontColorStore = resolveColor(subtotalFontColor)
78
+ $: totalRowColorStore = resolveColor(totalRowColor)
79
+ $: totalFontColorStore = resolveColor(totalFontColor)
80
+ $: headerColorStore = resolveColor(headerColor)
81
+ $: headerFontColorStore = resolveColor(headerFontColor)
82
+ $: backgroundColorStore = resolveColor(backgroundColor)
83
+
84
+ let error: string | undefined = undefined
85
+ let columnSummary: any[] = []
86
+ let priorityColumns: (string | undefined)[] = [groupBy]
87
+ let finalColumnOrder: string[] = []
88
+ let orderedColumns: any[] = []
89
+ let dataForDisplay: any[] = []
90
+ let displayedRows: any[] = []
91
+
92
+ $: priorityColumns = [groupBy]
93
+ $: props.update((state) => ({...state, priorityColumns}))
94
+ $: finalColumnOrder = getFinalColumnOrder(($props.columns ?? []).map((column: any) => column.id), priorityColumns)
95
+ $: orderedColumns = [...($props.columns ?? [])].sort(
96
+ (a, b) => finalColumnOrder.indexOf(a.id) - finalColumnOrder.indexOf(b.id),
97
+ )
98
+
99
+ let sortObj: {col: string | null; ascending: boolean | null} = {col: null, ascending: null}
100
+ let sortBy: string | undefined
101
+ let sortAsc: boolean | undefined
102
+
103
+ $: if (sort) {
104
+ let [column, direction] = sort.split(/\s+/)
105
+ sortBy = column
106
+ if (direction) {
107
+ sortAsc = direction.toLowerCase() !== 'desc'
108
+ } else {
109
+ sortAsc = true
110
+ }
111
+ sortObj = sortBy ? {col: sortBy, ascending: sortAsc ?? true} : {col: null, ascending: null}
112
+ }
113
+
114
+ $: props.update((state) => ({...state, data}))
115
+
116
+ $: try {
117
+ error = undefined
118
+ checkInputs(Array.isArray(data) ? data : [])
119
+ columnSummary = getColumnSummary(data ?? [], 'array')
120
+
121
+ if (sortBy) {
122
+ let columnNames = columnSummary.map((col) => col.id)
123
+ if (!columnNames.includes(sortBy)) {
124
+ throw new Error(`${sortBy} is not a column in the dataset. sort should contain one column name and optionally a direction (asc or desc).`)
125
+ }
126
+ }
127
+
128
+ let dateCols = columnSummary
129
+ .filter((col) => col.type === 'date' && !(data?.[0]?.[col.id] instanceof Date))
130
+ .map((col) => col.id)
131
+
132
+ for (let columnId of dateCols) {
133
+ data = convertColumnToDate(data, columnId)
134
+ }
135
+
136
+ if (link && !showLinkCol) {
137
+ let linkIndex = columnSummary.findIndex((col) => col.id === link)
138
+ if (linkIndex !== -1) {
139
+ columnSummary = [...columnSummary.slice(0, linkIndex), ...columnSummary.slice(linkIndex + 1)]
140
+ }
141
+ }
142
+ } catch (thrown) {
143
+ let message = thrown instanceof Error ? thrown.message : 'Unable to prepare dataset'
144
+ error = message
145
+ if (strictBuild) throw thrown
146
+ }
147
+
148
+ let paginated = false
149
+ let currentPage = 1
150
+ let pageCount = 1
151
+ let displayedPageLength = 0
152
+
153
+ const goToPage = (page: number) => {
154
+ if (!paginated) return
155
+ let next = Math.min(Math.max(page, 1), pageCount)
156
+ if (Number.isFinite(next)) currentPage = next
157
+ }
158
+
159
+ let groupedData: Record<string, any[]> = {}
160
+ let groupToggleStates: Record<string, boolean> = {}
161
+
162
+ const coerceId = (value: any) => {
163
+ if (value === undefined || value === null || value === '') return undefined
164
+ return String(value)
165
+ }
166
+
167
+ let dataTestId: string | undefined = undefined
168
+
169
+ $: {
170
+ if (!Array.isArray(data)) {
171
+ let raw = data as any
172
+ dataTestId = coerceId(raw?.id)
173
+ let candidate = raw?.rows
174
+ data = Array.isArray(candidate) ? candidate : []
175
+ } else {
176
+ dataTestId = coerceId((data as any)?.id)
177
+ }
178
+ }
179
+
180
+ $: paginated = !groupBy && rows > 0 && (dataForDisplay?.length ?? 0) > rows
181
+ $: pageCount = paginated ? Math.ceil((dataForDisplay?.length ?? 0) / rows) : 1
182
+ $: currentPage = Math.min(Math.max(currentPage, 1), pageCount)
183
+ $: displayedPageLength = paginated
184
+ ? Math.min(rows, (dataForDisplay?.length ?? 0) - rows * (currentPage - 1))
185
+ : dataForDisplay?.length ?? 0
186
+
187
+ $: if (groupBy && data) {
188
+ groupedData = data.reduce<Record<string, any[]>>((acc, row) => {
189
+ let groupName = row[groupBy]
190
+ let key = groupName ?? '(blank)'
191
+ if (!acc[key]) acc[key] = []
192
+ acc[key].push(row)
193
+ return acc
194
+ }, {})
195
+
196
+ for (let groupName of Object.keys(groupedData)) {
197
+ if (!(groupName in groupToggleStates)) groupToggleStates[groupName] = groupsOpen ?? true
198
+ }
199
+ } else {
200
+ groupedData = {}
201
+ }
202
+
203
+ const handleToggle = (event: CustomEvent<{groupName: string}>) => {
204
+ let {groupName} = event.detail
205
+ groupToggleStates = {...groupToggleStates, [groupName]: !groupToggleStates[groupName]}
206
+ }
207
+
208
+ $: {
209
+ if (groupBy) {
210
+ displayedRows = data ?? []
211
+ } else if (paginated) {
212
+ let start = rows * (currentPage - 1)
213
+ let end = start + rows
214
+ displayedRows = dataForDisplay?.slice(start, end) ?? []
215
+ } else {
216
+ displayedRows = dataForDisplay ?? []
217
+ }
218
+ }
219
+
220
+ const applySort = (state: {col: string | null; ascending: boolean | null}) => {
221
+ if (!state.col) return
222
+ let ascending = state.ascending ?? true
223
+ if (groupBy) {
224
+ groupedData = Object.fromEntries(
225
+ Object.entries(groupedData).map(([groupName, rows]) => [
226
+ groupName,
227
+ [...rows].sort((a, b) => compareValues(a[state.col as string], b[state.col as string], ascending)),
228
+ ]),
229
+ )
230
+ } else {
231
+ let source = Array.isArray(data) ? data : []
232
+ dataForDisplay = [...source].sort((a, b) => compareValues(a[state.col as string], b[state.col as string], ascending))
233
+ }
234
+ }
235
+
236
+ const sortClick = (column: string) => () => {
237
+ if (!sortable) return
238
+ if (!column) return
239
+ if (sortObj.col === column) {
240
+ sortObj = {col: column, ascending: !sortObj.ascending}
241
+ } else {
242
+ sortObj = {col: column, ascending: true}
243
+ }
244
+ applySort(sortObj)
245
+ }
246
+
247
+ const normalizeForSort = (value: unknown) => {
248
+ if (value instanceof Date) return value.getTime()
249
+ if (typeof value === 'number') return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY
250
+ if (typeof value === 'bigint') return Number(value)
251
+ if (typeof value === 'boolean') return value ? 1 : 0
252
+ if (typeof value === 'string') {
253
+ let trimmed = value.trim()
254
+ if (!trimmed) return ''
255
+ let numeric = Number(trimmed)
256
+ if (!Number.isNaN(numeric) && /^[-+]?\d*\.?\d+(e[-+]?\d+)?$/i.test(trimmed)) return numeric
257
+ return trimmed.toLowerCase()
258
+ }
259
+ return String(value).toLowerCase()
260
+ }
261
+
262
+ const compareValues = (a: unknown, b: unknown, ascending: boolean) => {
263
+ let modifier = ascending ? 1 : -1
264
+ if (a === b) return 0
265
+ if (a === undefined || a === null) return -1 * modifier
266
+ if (b === undefined || b === null) return 1 * modifier
267
+ let valA = normalizeForSort(a)
268
+ let valB = normalizeForSort(b)
269
+ if (valA < valB) return -1 * modifier
270
+ if (valA > valB) return 1 * modifier
271
+ return 0
272
+ }
273
+
274
+ $: {
275
+ let source = Array.isArray(data) ? data : []
276
+ if (groupBy) {
277
+ dataForDisplay = source
278
+ } else if (sortObj.col) {
279
+ let ascending = sortObj.ascending ?? true
280
+ dataForDisplay = [...source].sort((a, b) => compareValues(a[sortObj.col as string], b[sortObj.col as string], ascending))
281
+ } else {
282
+ dataForDisplay = source
283
+ }
284
+ }
285
+
286
+ $: if (data && sortObj.col) applySort(sortObj)
287
+
288
+ $: sortedGroupNames = groupBy
289
+ ? Object.keys(groupedData).sort((a, b) => a.localeCompare(b))
290
+ : []
291
+
292
+ let groupOffsets: Record<string, number> = {}
293
+ $: if (groupBy) {
294
+ let running = 0
295
+ groupOffsets = {}
296
+ for (let name of sortedGroupNames) {
297
+ groupOffsets[name] = running
298
+ running += groupedData[name]?.length ?? 0
299
+ }
300
+ } else {
301
+ groupOffsets = {}
302
+ }
303
+
304
+ let totalRows = 0
305
+ $: totalRows = dataForDisplay?.length ?? 0
306
+ $: tableData = dataForDisplay ?? []
307
+ </script>
308
+
309
+ {#if !error}
310
+ <slot>
311
+ {#each columnSummary as column (column.id)}
312
+ <Column id={column.id} />
313
+ {/each}
314
+ </slot>
315
+
316
+ <div
317
+ class={`table-container ${paginated ? 'table-container--has-pagination' : ''}`}
318
+ data-testid={isFullPage ? undefined : `DataTable-${dataTestId ?? 'no-id'}`}
319
+ >
320
+ {#if title || subtitle}
321
+ <div class="table-title">
322
+ {#if title}<div class="table-title__headline">{title}</div>{/if}
323
+ {#if subtitle}<div class="table-title__subhead">{subtitle}</div>{/if}
324
+ </div>
325
+ {/if}
326
+
327
+ <div class="scrollbox pretty-scrollbar" style:background-color={$backgroundColorStore}>
328
+ <table>
329
+ <TableHeader
330
+ {rowNumbers}
331
+ headerColor={$headerColorStore}
332
+ headerFontColor={$headerFontColorStore}
333
+ {orderedColumns}
334
+ {columnSummary}
335
+ {sortable}
336
+ {sortClick}
337
+ {formatColumnTitles}
338
+ {sortObj}
339
+ {wrapTitles}
340
+ {compact}
341
+ link={link}
342
+ />
343
+
344
+ {#if groupBy}
345
+ {#each sortedGroupNames as groupName (groupName)}
346
+ <TableGroupRow
347
+ {groupName}
348
+ currentGroupData={groupedData[groupName]}
349
+ toggled={groupToggleStates[groupName]}
350
+ {columnSummary}
351
+ {rowNumbers}
352
+ rowColor={$accordionRowColorStore}
353
+ {subtotals}
354
+ on:toggle={handleToggle}
355
+ {orderedColumns}
356
+ {compact}
357
+ />
358
+ {#if groupToggleStates[groupName]}
359
+ <TableRow
360
+ displayedData={groupedData[groupName]}
361
+ {rowShading}
362
+ {link}
363
+ {rowNumbers}
364
+ {rowLines}
365
+ {compact}
366
+ {columnSummary}
367
+ grouped={true}
368
+ {groupType}
369
+ groupColumn={groupBy}
370
+ groupNamePosition={groupNamePosition}
371
+ orderedColumns={orderedColumns}
372
+ index={groupOffsets[groupName] ?? 0}
373
+ />
374
+ {#if subtotals}
375
+ <TableSubtotalRow
376
+ {groupName}
377
+ currentGroupData={groupedData[groupName]}
378
+ {columnSummary}
379
+ rowColor={$subtotalRowColorStore}
380
+ fontColor={$subtotalFontColorStore}
381
+ groupBy={groupBy}
382
+ groupType={groupType}
383
+ {orderedColumns}
384
+ {compact}
385
+ />
386
+ {/if}
387
+ {/if}
388
+ {/each}
389
+ {:else}
390
+ <TableRow
391
+ displayedData={displayedRows}
392
+ {rowShading}
393
+ {link}
394
+ {rowNumbers}
395
+ {rowLines}
396
+ {compact}
397
+ {columnSummary}
398
+ grouped={false}
399
+ {groupType}
400
+ groupColumn={groupBy}
401
+ groupNamePosition={groupNamePosition}
402
+ orderedColumns={orderedColumns}
403
+ index={rows * (currentPage - 1)}
404
+ />
405
+ {/if}
406
+
407
+ {#if totalRow && !groupBy}
408
+ <TableTotalRow
409
+ data={tableData}
410
+ {rowNumbers}
411
+ {columnSummary}
412
+ rowColor={$totalRowColorStore}
413
+ fontColor={$totalFontColorStore}
414
+ groupType={groupType}
415
+ {orderedColumns}
416
+ {compact}
417
+ />
418
+ {/if}
419
+ </table>
420
+ </div>
421
+
422
+ {#if paginated && pageCount > 1}
423
+ <div class="pagination">
424
+ <button class="pagination__button" disabled={currentPage === 1} on:click={() => goToPage(1)}>First</button>
425
+ <button class="pagination__button" disabled={currentPage === 1} on:click={() => goToPage(currentPage - 1)}>Prev</button>
426
+ <div class="pagination__status">
427
+ Page {currentPage.toLocaleString()} of {pageCount.toLocaleString()}
428
+ </div>
429
+ <button class="pagination__button" disabled={currentPage === pageCount} on:click={() => goToPage(currentPage + 1)}>Next</button>
430
+ <button class="pagination__button" disabled={currentPage === pageCount} on:click={() => goToPage(pageCount)}>Last</button>
431
+ <div class="pagination__meta">{displayedPageLength.toLocaleString()} of {totalRows.toLocaleString()} rows</div>
432
+ </div>
433
+ {/if}
434
+ </div>
435
+ {:else}
436
+ <ErrorChart title="Data Table" error={error ?? emptyMessage ?? 'Unable to render table'} />
437
+ {/if}
438
+
439
+ <style>
440
+ .table-container {
441
+ font-size: 9.5pt;
442
+ margin: 8px 0;
443
+ position: relative;
444
+ color: var(--color-base-content, #1f2937);
445
+ font-family: var(--ui-font-family, system-ui);
446
+ line-height: 1.45;
447
+ }
448
+
449
+ .table-container--has-pagination {
450
+ padding-bottom: 24px;
451
+ }
452
+
453
+ .table-title {
454
+ margin-bottom: 8px;
455
+ }
456
+
457
+ .table-title__headline {
458
+ font-weight: 600;
459
+ font-size: 14px;
460
+ line-height: 1.3;
461
+ }
462
+
463
+ .table-title__subhead {
464
+ color: var(--color-base-content-muted, #6b7280);
465
+ font-size: 13px;
466
+ margin-top: 2px;
467
+ }
468
+
469
+ .scrollbox {
470
+ width: 100%;
471
+ overflow-x: auto;
472
+ scrollbar-width: thin;
473
+ }
474
+
475
+ table {
476
+ width: 100%;
477
+ border-collapse: collapse;
478
+ font-variant-numeric: tabular-nums;
479
+ }
480
+
481
+ .pagination {
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 8px;
485
+ margin-top: 12px;
486
+ font-size: 12px;
487
+ color: var(--color-base-content-muted, #6b7280);
488
+ }
489
+
490
+ .pagination__button {
491
+ padding: 4px 8px;
492
+ border: 1px solid rgba(107, 114, 128, 0.4);
493
+ border-radius: 4px;
494
+ background: transparent;
495
+ color: inherit;
496
+ cursor: pointer;
497
+ transition: background 0.2s ease-in-out;
498
+ }
499
+
500
+ .pagination__button:disabled {
501
+ opacity: 0.4;
502
+ cursor: default;
503
+ }
504
+
505
+ .pagination__button:not(:disabled):hover {
506
+ background: rgba(229, 231, 235, 0.6);
507
+ }
508
+
509
+ .pagination__status {
510
+ margin: 0 8px;
511
+ }
512
+
513
+ .pagination__meta {
514
+ margin-left: auto;
515
+ }
516
+ </style>
@@ -0,0 +1,43 @@
1
+ // clientCache stores query results in cacheStorage keyed by a hash of the compiled sql.
2
+ // Because the server does the compiling, we still need to make a request letting the server
3
+ // know all the hashes we have cached. If one matches, the server 304s (just like an ETag).
4
+
5
+ const TTL_MS = 1000 * 60 * 60 * 2
6
+
7
+ let cache: Cache | null = null
8
+ async function getCache () {
9
+ cache ||= await caches.open('graphene-data')
10
+ return cache
11
+ }
12
+
13
+ export async function getHashes () {
14
+ let store = await getCache()
15
+ let keys = await store.keys()
16
+ return keys.map(k => {
17
+ let url = new URL(k.url)
18
+ let expires = Number(url.searchParams.get('expires') || 0)
19
+ if (expires < Date.now()) {
20
+ store.delete(k)
21
+ return null
22
+ }
23
+ return url.pathname.replace(/^\//, '')
24
+ }).filter(h => !!h)
25
+ }
26
+
27
+ export async function cacheRead (hash: string): Promise<any | null> {
28
+ let store = await getCache()
29
+ let resp = await store.match(`https://graphene-cache/${hash}`, {ignoreSearch: true})
30
+ return await resp?.clone().json()
31
+ }
32
+
33
+ export async function cacheWrite (hash: string, response:Response) {
34
+ if (!hash) return
35
+ let store = await getCache()
36
+
37
+ // remove any older versions of this query from the cache. This can happen if you force the query to ignore cache.
38
+ let existing = await store.keys(`https://graphene-cache/${hash}`, {ignoreSearch: true})
39
+ existing.forEach(key => store.delete(key))
40
+
41
+ let expiresAt = Date.now() + TTL_MS
42
+ await store.put(`https://graphene-cache/${hash}?expires=${expiresAt}`, response)
43
+ }