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,9 +3,11 @@
|
||||
import { sessions, loadSessions } from '$lib/stores/chat'
|
||||
import { openTaskWindow, openNewTaskWindow } from '$lib/stores/windows'
|
||||
import { liveEvents, subscribeEvents } from '$lib/stores/events'
|
||||
import { bucket, statusStyle, FILTERS, TASK_EVENTS, heading, type Bucket } from '$lib/tasks'
|
||||
import { relativeTime } from '$lib/utils'
|
||||
import { bucket, FILTERS, TASK_EVENTS, heading, type Bucket } from '$lib/tasks'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import DataTable from '$lib/components/data-table/DataTable.svelte'
|
||||
import type { DataTableColumn } from '$lib/components/data-table/DataTable.svelte.ts'
|
||||
import StatusDotRenderer from '$lib/components/data-table/renderers/StatusDotRenderer.svelte'
|
||||
import PlusIcon from '@lucide/svelte/icons/plus'
|
||||
import type { Session } from '$lib/api'
|
||||
|
||||
@@ -17,6 +19,7 @@
|
||||
for (const s of $sessions) c[bucket(s)]++
|
||||
return c
|
||||
})
|
||||
|
||||
const visible = $derived(
|
||||
filter === 'all' ? $sessions : $sessions.filter((s) => bucket(s) === filter)
|
||||
)
|
||||
@@ -29,9 +32,6 @@
|
||||
loadSessions()
|
||||
const unsubStream = subscribeEvents()
|
||||
|
||||
// Refetch the board when a task's lifecycle changes anywhere. Scan all
|
||||
// events newer than the last seen (entity.touched fires constantly and
|
||||
// buries task.status); debounce a burst into one refetch.
|
||||
let lastSeenId = 0
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const unsub = liveEvents.subscribe((evs) => {
|
||||
@@ -51,6 +51,19 @@
|
||||
unsubStream()
|
||||
}
|
||||
})
|
||||
|
||||
const columns: DataTableColumn<Session>[] = [
|
||||
{ key: '_status', header: 'Status', render: StatusDotRenderer, width: '140px' },
|
||||
{ key: '_heading', header: 'Task', sortable: true, accessor: heading, truncate: true },
|
||||
{ key: 'summary', header: 'Summary', accessor: (s) => s.summary || '—', truncate: true, headerClass: 'hidden md:table-cell', class: 'hidden md:table-cell' },
|
||||
{ key: 'last_active_at', header: 'Last active', render: 'relative-time', sortable: true, width: '112px', align: 'right' },
|
||||
]
|
||||
|
||||
const emptyMessage = $derived(
|
||||
filter === 'all'
|
||||
? 'No tasks yet. Start one and Nomos will plan it, execute it, and report the outcome.'
|
||||
: `No ${FILTERS.find((f) => f.id === filter)?.label.toLowerCase()} tasks.`
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="relative flex h-full flex-col gap-3 overflow-hidden p-4">
|
||||
@@ -75,51 +88,13 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 min-h-0 flex-1 overflow-auto rounded-xl border bg-card/70 backdrop-blur">
|
||||
{#if visible.length === 0}
|
||||
<div class="flex flex-col items-center gap-2 px-4 py-16 text-center">
|
||||
<p class="max-w-sm text-sm text-muted-foreground">
|
||||
{filter === 'all'
|
||||
? 'No tasks yet. Start one and Nomos will plan it, execute it, and report the outcome.'
|
||||
: `No ${FILTERS.find((f) => f.id === filter)?.label.toLowerCase()} tasks.`}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b text-left text-xs text-muted-foreground [&>th]:sticky [&>th]:top-0 [&>th]:z-10 [&>th]:bg-card/95 [&>th]:backdrop-blur">
|
||||
<th class="w-36 px-4 py-2 font-medium">Status</th>
|
||||
<th class="px-4 py-2 font-medium">Task</th>
|
||||
<th class="hidden px-4 py-2 font-medium md:table-cell">Summary</th>
|
||||
<th class="w-28 px-4 py-2 text-right font-medium">Last active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each visible as s (s.id)}
|
||||
{@const st = statusStyle(s)}
|
||||
<tr
|
||||
class="cursor-pointer border-b last:border-0 transition-colors hover:bg-muted/40"
|
||||
onclick={() => openTask(s)}
|
||||
>
|
||||
<td class="px-4 py-2.5">
|
||||
<span class="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
<span class="size-2 rounded-full {st.dot} {st.pulse ? 'animate-pulse' : ''}"></span>
|
||||
{st.label}
|
||||
</span>
|
||||
</td>
|
||||
<td class="max-w-0 px-4 py-2.5">
|
||||
<span class="line-clamp-1 font-medium">{heading(s)}</span>
|
||||
</td>
|
||||
<td class="hidden max-w-0 px-4 py-2.5 md:table-cell">
|
||||
<span class="line-clamp-1 text-xs text-muted-foreground">{s.summary || '—'}</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-2.5 text-right text-[11px] text-muted-foreground">
|
||||
{relativeTime(s.last_active_at)}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
<div class="relative z-10 min-h-0 flex-1 overflow-hidden rounded-xl border bg-card/70 backdrop-blur">
|
||||
<DataTable
|
||||
{columns}
|
||||
data={visible}
|
||||
{emptyMessage}
|
||||
bordered={false}
|
||||
onRowClick={openTask}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user