@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.
- package/LICENSE.md +100 -0
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/cli.ts +157 -0
- package/dist/cli/cli.js +43 -0
- package/dist/docs/data_apps/components/charts/annotations.md +673 -0
- package/dist/docs/data_apps/components/charts/area-chart.md +202 -0
- package/dist/docs/data_apps/components/charts/bar-chart.md +317 -0
- package/dist/docs/data_apps/components/charts/box-plot.md +190 -0
- package/dist/docs/data_apps/components/charts/bubble-chart.md +151 -0
- package/dist/docs/data_apps/components/charts/calendar-heatmap.md +112 -0
- package/dist/docs/data_apps/components/charts/custom-echarts.md +308 -0
- package/dist/docs/data_apps/components/charts/echarts-options.md +217 -0
- package/dist/docs/data_apps/components/charts/funnel-chart.md +106 -0
- package/dist/docs/data_apps/components/charts/heatmap.md +180 -0
- package/dist/docs/data_apps/components/charts/histogram.md +107 -0
- package/dist/docs/data_apps/components/charts/line-chart.md +265 -0
- package/dist/docs/data_apps/components/charts/mixed-type-charts.md +240 -0
- package/dist/docs/data_apps/components/charts/sankey-diagram.md +301 -0
- package/dist/docs/data_apps/components/charts/scatter-plot.md +134 -0
- package/dist/docs/data_apps/components/charts/sparkline.md +68 -0
- package/dist/docs/data_apps/components/data/big-value.md +153 -0
- package/dist/docs/data_apps/components/data/delta.md +89 -0
- package/dist/docs/data_apps/components/data/table.md +470 -0
- package/dist/docs/data_apps/components/data/value.md +97 -0
- package/dist/docs/data_apps/components/inputs/button-group.md +154 -0
- package/dist/docs/data_apps/components/inputs/checkbox.md +52 -0
- package/dist/docs/data_apps/components/inputs/date-input.md +131 -0
- package/dist/docs/data_apps/components/inputs/date-range.md +124 -0
- package/dist/docs/data_apps/components/inputs/dimension-grid.md +67 -0
- package/dist/docs/data_apps/components/inputs/dropdown.md +199 -0
- package/dist/docs/data_apps/components/inputs/index.md +3 -0
- package/dist/docs/data_apps/components/inputs/slider.md +126 -0
- package/dist/docs/data_apps/components/inputs/text-input.md +86 -0
- package/dist/docs/data_apps/components/maps/area-map.md +397 -0
- package/dist/docs/data_apps/components/maps/base-map.md +269 -0
- package/dist/docs/data_apps/components/maps/bubble-map.md +361 -0
- package/dist/docs/data_apps/components/maps/point-map.md +326 -0
- package/dist/docs/data_apps/components/maps/us-map.md +167 -0
- package/dist/docs/data_apps/components/ui/accordion.md +116 -0
- package/dist/docs/data_apps/components/ui/alert.md +37 -0
- package/dist/docs/data_apps/components/ui/big-link.md +19 -0
- package/dist/docs/data_apps/components/ui/details.md +58 -0
- package/dist/docs/data_apps/components/ui/download-data.md +41 -0
- package/dist/docs/data_apps/components/ui/embed.md +47 -0
- package/dist/docs/data_apps/components/ui/grid.md +45 -0
- package/dist/docs/data_apps/components/ui/image.md +61 -0
- package/dist/docs/data_apps/components/ui/info.md +47 -0
- package/dist/docs/data_apps/components/ui/last-refreshed.md +28 -0
- package/dist/docs/data_apps/components/ui/link-button.md +20 -0
- package/dist/docs/data_apps/components/ui/link.md +40 -0
- package/dist/docs/data_apps/components/ui/modal.md +57 -0
- package/dist/docs/data_apps/components/ui/note.md +32 -0
- package/dist/docs/data_apps/components/ui/print-format-components.md +85 -0
- package/dist/docs/data_apps/components/ui/tabs.md +122 -0
- package/dist/docs/graphene.md +129 -0
- package/dist/ui/app.css +332 -0
- package/dist/ui/assets/favicon.ico +0 -0
- package/dist/ui/component-utilities/autoFormatting.js +301 -0
- package/dist/ui/component-utilities/builtInFormats.js +482 -0
- package/dist/ui/component-utilities/chartContext.js +12 -0
- package/dist/ui/component-utilities/chartWindowDebug.js +21 -0
- package/dist/ui/component-utilities/checkInputs.js +95 -0
- package/dist/ui/component-utilities/convert.js +15 -0
- package/dist/ui/component-utilities/dateParsing.js +57 -0
- package/dist/ui/component-utilities/dropdownContext.ts +1 -0
- package/dist/ui/component-utilities/echarts.js +262 -0
- package/dist/ui/component-utilities/echartsThemes.js +453 -0
- package/dist/ui/component-utilities/formatTitle.js +24 -0
- package/dist/ui/component-utilities/formatting.js +258 -0
- package/dist/ui/component-utilities/getColumnExtents.js +79 -0
- package/dist/ui/component-utilities/getColumnSummary.js +67 -0
- package/dist/ui/component-utilities/getCompletedData.js +114 -0
- package/dist/ui/component-utilities/getDistinctCount.js +7 -0
- package/dist/ui/component-utilities/getDistinctValues.js +15 -0
- package/dist/ui/component-utilities/getSeriesConfig.js +236 -0
- package/dist/ui/component-utilities/getSortedData.js +7 -0
- package/dist/ui/component-utilities/getStackPercentages.js +43 -0
- package/dist/ui/component-utilities/getStackedData.js +17 -0
- package/dist/ui/component-utilities/getYAxisIndex.js +15 -0
- package/dist/ui/component-utilities/globalContexts.js +1 -0
- package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +119 -0
- package/dist/ui/component-utilities/inputUtils.ts +25 -0
- package/dist/ui/component-utilities/replaceNulls.js +14 -0
- package/dist/ui/component-utilities/tableUtils.ts +120 -0
- package/dist/ui/component-utilities/themeStores.ts +116 -0
- package/dist/ui/components/Area.svelte +174 -0
- package/dist/ui/components/AreaChart.svelte +150 -0
- package/dist/ui/components/Bar.svelte +326 -0
- package/dist/ui/components/BarChart.svelte +194 -0
- package/dist/ui/components/BigValue.svelte +69 -0
- package/dist/ui/components/Chart.svelte +1070 -0
- package/dist/ui/components/Column.svelte +172 -0
- package/dist/ui/components/DateRange.svelte +324 -0
- package/dist/ui/components/Dropdown.svelte +738 -0
- package/dist/ui/components/DropdownOption.svelte +21 -0
- package/dist/ui/components/ECharts.svelte +77 -0
- package/dist/ui/components/ErrorChart.svelte +54 -0
- package/dist/ui/components/GrapheneQuery.svelte +12 -0
- package/dist/ui/components/InlineDelta.svelte +150 -0
- package/dist/ui/components/Line.svelte +210 -0
- package/dist/ui/components/LineChart.svelte +178 -0
- package/dist/ui/components/PieChart.svelte +36 -0
- package/dist/ui/components/QueryLoad.svelte +82 -0
- package/dist/ui/components/Row.svelte +14 -0
- package/dist/ui/components/SortIcon.svelte +32 -0
- package/dist/ui/components/Table.svelte +19 -0
- package/dist/ui/components/TableCell.svelte +75 -0
- package/dist/ui/components/TableGroupRow.svelte +136 -0
- package/dist/ui/components/TableGroupToggle.svelte +42 -0
- package/dist/ui/components/TableHeader.svelte +242 -0
- package/dist/ui/components/TableRow.svelte +283 -0
- package/dist/ui/components/TableSubtotalRow.svelte +62 -0
- package/dist/ui/components/TableTotalRow.svelte +88 -0
- package/dist/ui/components/TextInput.svelte +92 -0
- package/dist/ui/components/_Table.svelte +516 -0
- package/dist/ui/internal/clientCache.ts +43 -0
- package/dist/ui/internal/queryEngine.ts +169 -0
- package/dist/ui/internal/telemetry.ts +28 -0
- package/dist/ui/internal/theme.ts +88 -0
- package/dist/ui/layout.svelte +3 -0
- package/dist/ui/playwright.config.ts +30 -0
- package/dist/ui/web.js +106 -0
- 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
|
+
}
|