Skip to content

Commit 09b244e

Browse files
committed
Invent dark magic
Massive improvement to DX when creating tables.
1 parent 825cc33 commit 09b244e

7 files changed

Lines changed: 194 additions & 410 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { CellContext, ColumnDef, StringOrTemplateHeader } from '@tanstack/table-core';
2+
import DataTableSortButton from '$lib/components/Table/SortButton.svelte';
3+
import { renderComponent, renderSnippet } from '$lib/components/ui/data-table';
4+
import { createRawSnippet } from 'svelte';
5+
import { durationToString, elapsedToString } from '$lib/utils/time';
6+
import type { SemVer } from 'semver';
7+
import type { TwTextColor } from '$lib/types/Tailwind';
8+
import { getReadableUserAgentName } from '$lib/utils/userAgent';
9+
10+
export function CreateSortHeader<TData>(name: string): StringOrTemplateHeader<TData, unknown> {
11+
return ({ column }) =>
12+
renderComponent(DataTableSortButton, {
13+
name,
14+
onclick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
15+
});
16+
}
17+
18+
export function CreateSimpleCellSnippet<TData extends object, K extends keyof TData & string>(
19+
key: K,
20+
renderer: (value: TData[K]) => string
21+
): ColumnDef<TData, unknown>['cell'] {
22+
return ({ row }: CellContext<TData, unknown>) => {
23+
const value = row.getValue(key) as TData[K];
24+
25+
return renderSnippet(
26+
createRawSnippet<[TData[K]]>((getValue) => ({
27+
render: () => renderer(getValue()),
28+
})),
29+
value
30+
);
31+
};
32+
}
33+
34+
type TableCell =
35+
`<div class="px-4 font-medium${'' | ` ${TwTextColor}`}"${'' | ` title="${string}"`}>${string}</div>`;
36+
const CellNA: TableCell = '<div class="px-4 font-medium" title="N/A">N/A</div>';
37+
const CellOrangeNever: TableCell = '<div class="px-4 font-medium text-orange-500">Never</div>';
38+
const CellRedUnknown: TableCell = '<div class="px-4 font-medium text-red-500">Unknown</div>';
39+
const CellRedUnavailable: TableCell =
40+
'<div class="px-4 font-medium text-red-500">Unavailable</div>';
41+
42+
export const LocaleDateRenderer = (date: Date): TableCell =>
43+
`<div class="px-4 font-medium" title="${date}">${date.toLocaleDateString()}</div>`;
44+
45+
export const LocaleDateTimeRenderer = (date: Date): TableCell =>
46+
`<div class="px-4 font-medium" title="${date}">${date.toLocaleString()}</div>`;
47+
48+
export const TimeSinceDurationRenderer = (date: Date): TableCell =>
49+
`<div class="px-4 font-medium" title="${date}">${durationToString(Date.now() - date.getTime())}</div>`;
50+
51+
export const TimeSinceRelativeRenderer = (date: Date): TableCell =>
52+
`<div class="px-4 font-medium" title="${date}">${elapsedToString(date.getTime() - Date.now())}</div>`;
53+
54+
export const TimeSinceRelativeOrNeverRenderer = (date: Date | null | undefined): TableCell => {
55+
if (!date) return CellOrangeNever;
56+
return TimeSinceRelativeRenderer(date);
57+
};
58+
59+
export const NumberRenderer = (number: number | null): TableCell =>
60+
number ? `<div class="px-4 font-medium" title="${number}">${number}</div>` : CellNA;
61+
62+
export const UserAgentRenderer = (userAgent: string | null): TableCell => {
63+
if (!userAgent) return CellRedUnknown;
64+
65+
const readableName = getReadableUserAgentName(userAgent);
66+
if (!readableName)
67+
return `<div class="px-4 font-medium text-orange-500" title="${userAgent}">${userAgent}</div>`;
68+
69+
return `<div class="px-4 font-medium" title="${userAgent}">${readableName}</div>`;
70+
};
71+
72+
export const FirmwareVersionRenderer = (firmwareVersion: SemVer | null): TableCell => {
73+
if (!firmwareVersion) return CellRedUnavailable;
74+
75+
let firmwareVersionString = firmwareVersion.toString();
76+
77+
let color: TwTextColor;
78+
if (firmwareVersionString.length <= 0) {
79+
firmwareVersionString = 'Invalid';
80+
color = 'text-red-500';
81+
} else if (firmwareVersionString === '0.0.0-local') {
82+
color = 'text-orange-500';
83+
} else {
84+
color = 'text-white';
85+
}
86+
87+
return `<div class="px-4 font-medium ${color}" title="${firmwareVersionString}">${firmwareVersionString}</div>`;
88+
};

src/routes/(authenticated)/admin/online-hubs/columns.ts

Lines changed: 19 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import type { ColumnDef, StringOrTemplateHeader } from '@tanstack/table-core';
2-
import { createRawSnippet } from 'svelte';
1+
import type { ColumnDef } from '@tanstack/table-core';
32
import { SemVer } from 'semver';
4-
import { renderComponent, renderSnippet } from '$lib/components/ui/data-table';
5-
import { durationToString } from '$lib/utils/time';
6-
import type { TwColor } from '$lib/types/Tailwind';
3+
import { renderComponent } from '$lib/components/ui/data-table';
74
import DataTableActions from './data-table-actions.svelte';
8-
import { getReadableUserAgentName } from '$lib/utils/userAgent';
9-
import DataTableSortButton from '$lib/components/Table/SortButton.svelte';
5+
import {
6+
CreateSimpleCellSnippet,
7+
CreateSortHeader,
8+
FirmwareVersionRenderer,
9+
NumberRenderer,
10+
TimeSinceDurationRenderer,
11+
UserAgentRenderer,
12+
} from '$lib/components/Table/ColumnUtils';
1013

1114
export type OnlineHubOwner = {
1215
id: string;
@@ -26,13 +29,8 @@ export type OnlineHub = {
2629
rssi: number | null;
2730
};
2831

29-
function CreateSortHeader<TData>(name: string): StringOrTemplateHeader<TData, unknown> {
30-
return ({ column }) =>
31-
renderComponent(DataTableSortButton, {
32-
name,
33-
onclick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
34-
});
35-
}
32+
const OwnerRenderer = (owner: OnlineHubOwner) =>
33+
`<div class="px-4 font-medium" title="${owner.id}">${owner.name}</div>`;
3634

3735
export const columns: ColumnDef<OnlineHub>[] = [
3836
{
@@ -42,17 +40,7 @@ export const columns: ColumnDef<OnlineHub>[] = [
4240
{
4341
accessorKey: 'owner',
4442
header: CreateSortHeader('Owner'),
45-
cell: ({ row }) => {
46-
const ownerCellSnippet = createRawSnippet<[OnlineHubOwner]>((getOwner) => {
47-
const owner = getOwner();
48-
return {
49-
render: () =>
50-
`<div class="text-left font-medium" title="${owner.id}">${owner.name}</div>`,
51-
};
52-
});
53-
54-
return renderSnippet(ownerCellSnippet, row.getValue<OnlineHubOwner>('owner'));
55-
},
43+
cell: CreateSimpleCellSnippet('owner', OwnerRenderer),
5644
sortingFn: (row_a, row_b) => {
5745
const a = row_a.getValue<OnlineHubOwner>('owner');
5846
const b = row_b.getValue<OnlineHubOwner>('owner');
@@ -63,26 +51,7 @@ export const columns: ColumnDef<OnlineHub>[] = [
6351
{
6452
accessorKey: 'firmware_version',
6553
header: CreateSortHeader('Firmware Version'),
66-
cell: ({ row }) => {
67-
const firmwareVersionCellSnippet = createRawSnippet<[string]>((getFirmwareVersion) => {
68-
let firmwareVersion = getFirmwareVersion().toString();
69-
70-
let color: `text-${TwColor}`;
71-
if (firmwareVersion.length <= 0) {
72-
firmwareVersion = 'Invalid';
73-
color = 'text-red-500';
74-
} else if (firmwareVersion === '0.0.0-local') {
75-
color = 'text-orange-500';
76-
}
77-
78-
return {
79-
render: () =>
80-
`<div class="text-left font-medium ${color}" title="${firmwareVersion}">${firmwareVersion}</div>`,
81-
};
82-
});
83-
84-
return renderSnippet(firmwareVersionCellSnippet, row.getValue<string>('firmware_version'));
85-
},
54+
cell: CreateSimpleCellSnippet('firmware_version', FirmwareVersionRenderer),
8655
sortingFn: (row_a, row_b) => {
8756
const a = new SemVer(row_a.getValue<string>('firmware_version'));
8857
const b = new SemVer(row_b.getValue<string>('firmware_version'));
@@ -99,100 +68,27 @@ export const columns: ColumnDef<OnlineHub>[] = [
9968
{
10069
accessorKey: 'connected_at',
10170
header: CreateSortHeader('Online for'),
102-
cell: ({ row }) => {
103-
const connectedAtCellSnippet = createRawSnippet<[Date]>((getConnectedAt) => {
104-
const now = Date.now();
105-
const connectedAt = getConnectedAt();
106-
const formattedDuration = durationToString(now - connectedAt.getTime());
107-
return {
108-
render: () =>
109-
`<div class="text-left font-medium" title="${connectedAt}">${formattedDuration}</div>`,
110-
};
111-
});
112-
113-
return renderSnippet(connectedAtCellSnippet, row.getValue<Date>('connected_at'));
114-
},
71+
cell: CreateSimpleCellSnippet('connected_at', TimeSinceDurationRenderer),
11572
},
11673
{
11774
accessorKey: 'user_agent',
11875
header: 'User Agent',
119-
cell: ({ row }) => {
120-
const userAgentCellSnippet = createRawSnippet<[string | null]>((getUserAgent) => {
121-
const userAgent = getUserAgent();
122-
const readableName = userAgent ? getReadableUserAgentName(userAgent) : 'Unknown';
123-
124-
let color: `` | `text-${TwColor}` = '';
125-
if (!userAgent) {
126-
color = 'text-red-500';
127-
} else if (!readableName) {
128-
color = 'text-orange-500';
129-
}
130-
131-
return {
132-
render: () =>
133-
`<div class="text-left font-medium ${color}" title="${userAgent}">${readableName ?? userAgent}</div>`,
134-
};
135-
});
136-
137-
return renderSnippet(userAgentCellSnippet, row.getValue<string | null>('user_agent'));
138-
},
76+
cell: CreateSimpleCellSnippet('user_agent', UserAgentRenderer),
13977
},
14078
{
14179
accessorKey: 'booted_at',
14280
header: CreateSortHeader('Uptime'),
143-
cell: ({ row }) => {
144-
const bootedAtCellSnippet = createRawSnippet<[Date]>((getBootedAt) => {
145-
const bootedAt = getBootedAt();
146-
const now = Date.now();
147-
const formattedDuration = durationToString(now - bootedAt.getTime());
148-
return {
149-
render: () =>
150-
`<div class="text-left font-medium" title="${bootedAt}">${formattedDuration}</div>`,
151-
};
152-
});
153-
154-
return renderSnippet(bootedAtCellSnippet, row.getValue<Date>('booted_at'));
155-
},
81+
cell: CreateSimpleCellSnippet('booted_at', TimeSinceDurationRenderer),
15682
},
15783
{
15884
accessorKey: 'latency',
15985
header: CreateSortHeader('Latency'),
160-
cell: ({ row }) => {
161-
const latencyCellSnippet = createRawSnippet<[number | null]>((getLatency) => {
162-
const latency = getLatency();
163-
if (!latency) {
164-
return {
165-
render: () => `<div class="text-left font-medium" title="N/A">N/A</div>`,
166-
};
167-
}
168-
169-
return {
170-
render: () => `<div class="text-left font-medium" title="${latency}">${latency}</div>`,
171-
};
172-
});
173-
174-
return renderSnippet(latencyCellSnippet, row.getValue<number | null>('latency'));
175-
},
86+
cell: CreateSimpleCellSnippet('latency', NumberRenderer),
17687
},
17788
{
17889
accessorKey: 'rssi',
17990
header: CreateSortHeader('RSSI'),
180-
cell: ({ row }) => {
181-
const rssiCellSnippet = createRawSnippet<[number | null]>((getRssi) => {
182-
const rssi = getRssi();
183-
if (!rssi) {
184-
return {
185-
render: () => `<div class="text-left font-medium" title="N/A">N/A</div>`,
186-
};
187-
}
188-
189-
return {
190-
render: () => `<div class="text-left font-medium" title="${rssi}">${rssi}</div>`,
191-
};
192-
});
193-
194-
return renderSnippet(rssiCellSnippet, row.getValue<number | null>('rssi'));
195-
},
91+
cell: CreateSimpleCellSnippet('rssi', NumberRenderer),
19692
},
19793
{
19894
id: 'actions',

0 commit comments

Comments
 (0)