feat(web): adopt @vincjo/datatables for all tables, standardize shared components
- Add DataTable.svelte: declarative columns, built-in sorting, sticky headers, text truncation, column alignment, configurable widths, optional pagination/search - 12 built-in renderers: BadgeRenderer, StatusBadgeRenderer (unified risk/severity/ execution/state/type variant mapping), HealthDotRenderer, RelativeTimeRenderer, DateRenderer, DurationRenderer, StatusDotRenderer, SignalActions, ApprovalActions, ActivityAction, ActivityCancel - Migrate Overview (task board), Signals, Ops (3 tables) to DataTable - Refactor EntityTable treegrid to use shared SortHeader, EmptyState, HealthDotRenderer - Create shared components: EmptyState, StatusBadge, FilterTabs - Clean up Knowledge.svelte: replace inline relTime() and typeVariant() with shared utils - Add width, align, truncate column props; table-fixed layout; rounded-xl borders - Bump version to 0.11.0
This commit is contained in:
@@ -3,11 +3,12 @@
|
||||
import { fetchSignals, ackSignal, resolveSignal, muteSignal, type Signal } from '$lib/api'
|
||||
import { liveEvents, subscribeEvents } from '$lib/stores/events'
|
||||
import * as Tabs from '$lib/components/ui/tabs'
|
||||
import * as Table from '$lib/components/ui/table'
|
||||
import { Badge } from '$lib/components/ui/badge'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import * as Select from '$lib/components/ui/select'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import DataTable from '$lib/components/data-table/DataTable.svelte'
|
||||
import type { DataTableColumn } from '$lib/components/data-table/DataTable.svelte.ts'
|
||||
import SignalActions from '$lib/components/data-table/renderers/SignalActions.svelte'
|
||||
|
||||
let signals = $state<Signal[]>([])
|
||||
let severityFilter = $state('all')
|
||||
@@ -66,12 +67,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function severityVariant(sev: string): 'default' | 'secondary' | 'destructive' {
|
||||
if (sev === 'critical') return 'destructive'
|
||||
if (sev === 'warning') return 'secondary'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function bySeverity(list: Signal[]) {
|
||||
return severityFilter === 'all' ? list : list.filter((s) => s.severity === severityFilter)
|
||||
}
|
||||
@@ -79,60 +74,29 @@
|
||||
const open = $derived(bySeverity(signals.filter((s) => ['raised', 'acknowledged', 'acting'].includes(s.state))))
|
||||
const muted = $derived(bySeverity(signals.filter((s) => s.state === 'muted')))
|
||||
const resolved = $derived(bySeverity(signals.filter((s) => ['resolved', 'failed'].includes(s.state))))
|
||||
</script>
|
||||
|
||||
{#snippet signalTable(list: Signal[], showActions: boolean)}
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Target</Table.Head>
|
||||
<Table.Head>Kind</Table.Head>
|
||||
<Table.Head>Severity</Table.Head>
|
||||
<Table.Head>State</Table.Head>
|
||||
<Table.Head>Occurrences</Table.Head>
|
||||
<Table.Head>Last seen</Table.Head>
|
||||
{#if showActions}
|
||||
<Table.Head class="text-right">Actions</Table.Head>
|
||||
{/if}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each list as signal (signal.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-mono text-xs">{signal.target ?? '—'}</Table.Cell>
|
||||
<Table.Cell>{signal.kind}</Table.Cell>
|
||||
<Table.Cell><Badge variant={severityVariant(signal.severity)}>{signal.severity}</Badge></Table.Cell>
|
||||
<Table.Cell><Badge variant="outline">{signal.state}</Badge></Table.Cell>
|
||||
<Table.Cell>{signal.occurrence_count}</Table.Cell>
|
||||
<Table.Cell class="text-xs text-muted-foreground"
|
||||
>{new Date(signal.last_seen_at).toLocaleString()}</Table.Cell
|
||||
>
|
||||
{#if showActions}
|
||||
<Table.Cell class="flex justify-end gap-2">
|
||||
{#if signal.state === 'raised'}
|
||||
<Button size="sm" variant="outline" disabled={acting === signal.id} onclick={() => ack(signal.id)}
|
||||
>Ack</Button
|
||||
>
|
||||
{/if}
|
||||
<Button size="sm" variant="outline" disabled={acting === signal.id} onclick={() => mute(signal.id)}
|
||||
>Mute 1h</Button
|
||||
>
|
||||
<Button size="sm" disabled={acting === signal.id} onclick={() => resolve(signal.id)}>Resolve</Button>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={showActions ? 7 : 6} class="text-center text-muted-foreground"
|
||||
>No signals.</Table.Cell
|
||||
>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
function makeColumns(showActions: boolean, actingVal: string | null): DataTableColumn<Signal>[] {
|
||||
const base: DataTableColumn<Signal>[] = [
|
||||
{ key: 'target', header: 'Target', class: 'font-mono text-xs', width: '180px', accessor: (s) => s.target ?? '—', truncate: true },
|
||||
{ key: 'kind', header: 'Kind', width: '120px' },
|
||||
{ key: 'severity', header: 'Severity', render: 'status-badge', renderProps: { kind: 'severity' }, width: '100px' },
|
||||
{ key: 'state', header: 'State', render: 'status-badge', renderProps: { kind: 'state' }, width: '110px' },
|
||||
{ key: 'occurrence_count', header: 'Occurrences', width: '100px', align: 'right' },
|
||||
{ key: 'last_seen_at', header: 'Last seen', render: 'date', width: '170px' },
|
||||
]
|
||||
if (showActions) {
|
||||
base.push({
|
||||
key: '_actions', header: '', render: SignalActions,
|
||||
renderProps: { acting: actingVal, onAck: ack, onMute: mute, onResolve: resolve },
|
||||
align: 'right', headerClass: 'text-right', width: '220px'
|
||||
})
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
const columnsWithActions = $derived(makeColumns(true, acting))
|
||||
const columnsWithoutActions = makeColumns(false, null)
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -159,13 +123,13 @@
|
||||
<Tabs.Trigger value="resolved">Resolved</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="open" class="flex-1 overflow-auto">
|
||||
{@render signalTable(open, true)}
|
||||
<DataTable columns={columnsWithActions} data={open} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="muted" class="flex-1 overflow-auto">
|
||||
{@render signalTable(muted, true)}
|
||||
<DataTable columns={columnsWithActions} data={muted} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="resolved" class="flex-1 overflow-auto">
|
||||
{@render signalTable(resolved, false)}
|
||||
<DataTable columns={columnsWithoutActions} data={resolved} />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user