datool 0.0.4 → 0.0.6
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 +14 -0
- package/client-dist/assets/index-CU3ksvkv.js +164 -0
- package/client-dist/assets/index-Rn9jSaAz.css +1 -0
- package/client-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/client/App.tsx +115 -4
- package/src/client/components/data-table-cell.tsx +19 -6
- package/src/client/components/data-table.tsx +492 -84
- package/src/client/components/viewer-settings.tsx +67 -0
- package/src/client/index.css +3 -0
- package/src/client/lib/datool-url-state.ts +29 -2
- package/src/node/config.ts +22 -0
- package/src/shared/types.ts +4 -0
- package/client-dist/assets/index-B5MN-j1l.js +0 -164
- package/client-dist/assets/index-BkIiz0aS.css +0 -1
|
@@ -2,6 +2,7 @@ import * as React from "react"
|
|
|
2
2
|
import {
|
|
3
3
|
DownloadIcon,
|
|
4
4
|
EllipsisIcon,
|
|
5
|
+
LayoutGridIcon,
|
|
5
6
|
MoonIcon,
|
|
6
7
|
Settings2Icon,
|
|
7
8
|
SunIcon,
|
|
@@ -38,9 +39,12 @@ type ViewerSettingsColumn = {
|
|
|
38
39
|
|
|
39
40
|
type ViewerSettingsProps = {
|
|
40
41
|
columns: ViewerSettingsColumn[]
|
|
42
|
+
groupedColumnIds: string[]
|
|
41
43
|
isDisabled?: boolean
|
|
42
44
|
onExportCsv: () => void
|
|
43
45
|
onExportMarkdown: () => void
|
|
46
|
+
onClearGrouping: () => void
|
|
47
|
+
onToggleGrouping: (columnId: string, grouped: boolean) => void
|
|
44
48
|
onToggleColumn: (columnId: string, visible: boolean) => void
|
|
45
49
|
className?: string
|
|
46
50
|
}
|
|
@@ -69,14 +73,26 @@ const THEME_OPTIONS: Array<{
|
|
|
69
73
|
|
|
70
74
|
export function ViewerSettings({
|
|
71
75
|
columns,
|
|
76
|
+
groupedColumnIds,
|
|
72
77
|
isDisabled = false,
|
|
73
78
|
onExportCsv,
|
|
74
79
|
onExportMarkdown,
|
|
80
|
+
onClearGrouping,
|
|
81
|
+
onToggleGrouping,
|
|
75
82
|
onToggleColumn,
|
|
76
83
|
className,
|
|
77
84
|
}: ViewerSettingsProps) {
|
|
78
85
|
const { theme, setTheme } = useTheme()
|
|
79
86
|
const canExport = !isDisabled && columns.length > 0
|
|
87
|
+
const groupedLabels = React.useMemo(
|
|
88
|
+
() =>
|
|
89
|
+
groupedColumnIds.flatMap((columnId) => {
|
|
90
|
+
const column = columns.find((candidate) => candidate.id === columnId)
|
|
91
|
+
|
|
92
|
+
return column ? [column.label] : []
|
|
93
|
+
}),
|
|
94
|
+
[columns, groupedColumnIds]
|
|
95
|
+
)
|
|
80
96
|
|
|
81
97
|
return (
|
|
82
98
|
<DropdownMenu>
|
|
@@ -131,6 +147,57 @@ export function ViewerSettings({
|
|
|
131
147
|
))}
|
|
132
148
|
</DropdownMenuSubContent>
|
|
133
149
|
</DropdownMenuSub>
|
|
150
|
+
<DropdownMenuSub>
|
|
151
|
+
<DropdownMenuSubTrigger
|
|
152
|
+
className="min-h-9 text-sm"
|
|
153
|
+
disabled={isDisabled || columns.length === 0}
|
|
154
|
+
>
|
|
155
|
+
<LayoutGridIcon className="size-4 text-muted-foreground" />
|
|
156
|
+
Group rows
|
|
157
|
+
</DropdownMenuSubTrigger>
|
|
158
|
+
<DropdownMenuSubContent className="max-h-80 w-64 overflow-y-auto">
|
|
159
|
+
<DropdownMenuLabel>
|
|
160
|
+
{groupedLabels.length > 0
|
|
161
|
+
? `Grouped by ${groupedLabels.join(", ")}`
|
|
162
|
+
: "Group rows by field"}
|
|
163
|
+
</DropdownMenuLabel>
|
|
164
|
+
<DropdownMenuSeparator />
|
|
165
|
+
<DropdownMenuItem
|
|
166
|
+
className="min-h-9 text-sm"
|
|
167
|
+
disabled={groupedColumnIds.length === 0}
|
|
168
|
+
onSelect={(event) => {
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
onClearGrouping()
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
Clear grouping
|
|
174
|
+
</DropdownMenuItem>
|
|
175
|
+
<DropdownMenuSeparator />
|
|
176
|
+
{columns.map((column) => (
|
|
177
|
+
<DropdownMenuCheckboxItem
|
|
178
|
+
key={column.id}
|
|
179
|
+
checked={groupedColumnIds.includes(column.id)}
|
|
180
|
+
className="min-h-9 text-sm"
|
|
181
|
+
onSelect={(event) => {
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
}}
|
|
184
|
+
onCheckedChange={(checked) =>
|
|
185
|
+
onToggleGrouping(column.id, checked === true)
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
<span className="flex min-w-0 items-center gap-2 pr-4">
|
|
189
|
+
{column.kind ? (
|
|
190
|
+
<DataTableColIcon
|
|
191
|
+
kind={column.kind}
|
|
192
|
+
className="size-4 shrink-0 text-muted-foreground"
|
|
193
|
+
/>
|
|
194
|
+
) : null}
|
|
195
|
+
<span className="truncate">{column.label}</span>
|
|
196
|
+
</span>
|
|
197
|
+
</DropdownMenuCheckboxItem>
|
|
198
|
+
))}
|
|
199
|
+
</DropdownMenuSubContent>
|
|
200
|
+
</DropdownMenuSub>
|
|
134
201
|
<DropdownMenuSub>
|
|
135
202
|
<DropdownMenuSubTrigger className="min-h-9 text-sm">
|
|
136
203
|
<SunIcon className="size-4 text-muted-foreground dark:hidden" />
|
package/src/client/index.css
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
--secondary: oklch(0.967 0.001 286.375);
|
|
18
18
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
19
19
|
--muted: oklch(0.967 0.001 286.375);
|
|
20
|
+
--table-gap: oklch(0.967 0.001 286.375);
|
|
20
21
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
21
22
|
--accent: oklch(0.967 0.001 286.375);
|
|
22
23
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
--secondary: oklch(0.274 0.006 286.033);
|
|
53
54
|
--secondary-foreground: oklch(0.985 0 0);
|
|
54
55
|
--muted: oklch(0.274 0.006 286.033);
|
|
56
|
+
--table-gap: oklch(0.180 0.005 285.823);
|
|
55
57
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
56
58
|
--accent: oklch(0.274 0.006 286.033);
|
|
57
59
|
--accent-foreground: oklch(0.985 0 0);
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
--color-accent: var(--accent);
|
|
98
100
|
--color-muted-foreground: var(--muted-foreground);
|
|
99
101
|
--color-muted: var(--muted);
|
|
102
|
+
--color-table-gap: var(--table-gap);
|
|
100
103
|
--color-secondary-foreground: var(--secondary-foreground);
|
|
101
104
|
--color-secondary: var(--secondary);
|
|
102
105
|
--color-primary-foreground: var(--primary-foreground);
|
|
@@ -5,6 +5,7 @@ type PersistedTableState = {
|
|
|
5
5
|
columnSizing?: Record<string, number>
|
|
6
6
|
columnVisibility?: VisibilityState
|
|
7
7
|
globalFilter?: string
|
|
8
|
+
groupBy?: string[]
|
|
8
9
|
highlightedColumns?: Record<string, boolean>
|
|
9
10
|
sorting?: unknown[]
|
|
10
11
|
}
|
|
@@ -23,7 +24,9 @@ function getTableUrlParam(tableId: string) {
|
|
|
23
24
|
|
|
24
25
|
function isDatoolUrlParam(key: string) {
|
|
25
26
|
return (
|
|
26
|
-
key.startsWith(
|
|
27
|
+
key.startsWith(
|
|
28
|
+
`${DATA_TABLE_URL_PARAM_PREFIX}${LOG_VIEWER_TABLE_ID_PREFIX}`
|
|
29
|
+
) ||
|
|
27
30
|
(key.startsWith(`${LOG_VIEWER_TABLE_ID_PREFIX}-`) && key.endsWith("-search"))
|
|
28
31
|
)
|
|
29
32
|
}
|
|
@@ -57,6 +60,14 @@ function sanitizeColumnVisibility(
|
|
|
57
60
|
)
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function sanitizeGroupBy(groupBy: string[] | undefined, columnIds: string[]) {
|
|
64
|
+
const validIds = new Set(columnIds)
|
|
65
|
+
|
|
66
|
+
return (groupBy ?? []).filter((columnId, index, values) => {
|
|
67
|
+
return validIds.has(columnId) && values.indexOf(columnId) === index
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
function cleanUpDatoolParams(url: URL, tableId: string) {
|
|
61
72
|
const activeSearchParam = getSearchUrlParam(tableId)
|
|
62
73
|
const activeTableParam = getTableUrlParam(tableId)
|
|
@@ -89,7 +100,10 @@ export function readDatoolSearch(tableId: string) {
|
|
|
89
100
|
return ""
|
|
90
101
|
}
|
|
91
102
|
|
|
92
|
-
return
|
|
103
|
+
return (
|
|
104
|
+
new URL(window.location.href).searchParams.get(getSearchUrlParam(tableId)) ??
|
|
105
|
+
""
|
|
106
|
+
)
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
export function readDatoolColumnVisibility(
|
|
@@ -102,15 +116,21 @@ export function readDatoolColumnVisibility(
|
|
|
102
116
|
)
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
export function readDatoolGrouping(tableId: string, columnIds: string[]) {
|
|
120
|
+
return sanitizeGroupBy(readPersistedTableState(tableId)?.groupBy, columnIds)
|
|
121
|
+
}
|
|
122
|
+
|
|
105
123
|
export function writeDatoolUrlState({
|
|
106
124
|
columnIds,
|
|
107
125
|
columnVisibility,
|
|
126
|
+
groupBy,
|
|
108
127
|
search,
|
|
109
128
|
selectedStreamId,
|
|
110
129
|
tableId,
|
|
111
130
|
}: {
|
|
112
131
|
columnIds: string[]
|
|
113
132
|
columnVisibility: VisibilityState
|
|
133
|
+
groupBy: string[]
|
|
114
134
|
search: string
|
|
115
135
|
selectedStreamId: string | null
|
|
116
136
|
tableId: string
|
|
@@ -125,6 +145,7 @@ export function writeDatoolUrlState({
|
|
|
125
145
|
columnVisibility,
|
|
126
146
|
columnIds
|
|
127
147
|
)
|
|
148
|
+
const nextGroupBy = sanitizeGroupBy(groupBy, columnIds)
|
|
128
149
|
const nextTableState = {
|
|
129
150
|
...readPersistedTableState(tableId),
|
|
130
151
|
} satisfies PersistedTableState
|
|
@@ -149,6 +170,12 @@ export function writeDatoolUrlState({
|
|
|
149
170
|
delete nextTableState.columnVisibility
|
|
150
171
|
}
|
|
151
172
|
|
|
173
|
+
if (nextGroupBy.length > 0) {
|
|
174
|
+
nextTableState.groupBy = nextGroupBy
|
|
175
|
+
} else {
|
|
176
|
+
delete nextTableState.groupBy
|
|
177
|
+
}
|
|
178
|
+
|
|
152
179
|
if (Object.keys(nextTableState).length > 0) {
|
|
153
180
|
url.searchParams.set(getTableUrlParam(tableId), JSON.stringify(nextTableState))
|
|
154
181
|
} else {
|
package/src/node/config.ts
CHANGED
|
@@ -124,6 +124,25 @@ function assertActionShape(
|
|
|
124
124
|
assertActionButtonShape(streamId, actionId, value.button)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function assertDateFormatShape(value: unknown) {
|
|
128
|
+
if (value === undefined) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!isRecord(value) || Array.isArray(value)) {
|
|
133
|
+
throw new Error("datool.config.ts dateFormat must be an object.")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
new Intl.DateTimeFormat(
|
|
138
|
+
undefined,
|
|
139
|
+
value as Intl.DateTimeFormatOptions
|
|
140
|
+
)
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error("datool.config.ts defines an invalid dateFormat.")
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
function assertStreamShape(streamId: string, value: unknown) {
|
|
128
147
|
if (!isRecord(value)) {
|
|
129
148
|
throw new Error(`Stream "${streamId}" must be an object.`)
|
|
@@ -198,6 +217,8 @@ export function validateDatoolConfig(
|
|
|
198
217
|
throw new Error("datool.config.ts must export an object.")
|
|
199
218
|
}
|
|
200
219
|
|
|
220
|
+
assertDateFormatShape(config.dateFormat)
|
|
221
|
+
|
|
201
222
|
if (!isRecord(config.streams) || Object.keys(config.streams).length === 0) {
|
|
202
223
|
throw new Error("datool.config.ts must define at least one stream.")
|
|
203
224
|
}
|
|
@@ -209,6 +230,7 @@ export function validateDatoolConfig(
|
|
|
209
230
|
|
|
210
231
|
export function toClientConfig(config: DatoolConfig): DatoolClientConfig {
|
|
211
232
|
return {
|
|
233
|
+
dateFormat: config.dateFormat,
|
|
212
234
|
streams: Object.entries(config.streams).map(([id, stream]) => ({
|
|
213
235
|
actions: Object.entries(stream.actions ?? {}).map(([actionId, action]) => ({
|
|
214
236
|
button: action.button,
|
package/src/shared/types.ts
CHANGED
|
@@ -89,6 +89,8 @@ export type DatoolActionButtonConfig =
|
|
|
89
89
|
variant?: DatoolActionButtonVariant
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export type DatoolDateFormat = Intl.DateTimeFormatOptions
|
|
93
|
+
|
|
92
94
|
export type DatoolColumn = {
|
|
93
95
|
accessorKey: string
|
|
94
96
|
align?: "left" | "center" | "right"
|
|
@@ -168,6 +170,7 @@ export type DatoolStream<Row extends Record<string, unknown>> = {
|
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
export type DatoolConfig = {
|
|
173
|
+
dateFormat?: DatoolDateFormat
|
|
171
174
|
server?: {
|
|
172
175
|
host?: string
|
|
173
176
|
port?: number
|
|
@@ -195,6 +198,7 @@ export type DatoolClientAction = {
|
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
export type DatoolClientConfig = {
|
|
201
|
+
dateFormat?: DatoolDateFormat
|
|
198
202
|
streams: DatoolClientStream[]
|
|
199
203
|
}
|
|
200
204
|
|