Skip to content

Commit 17364a9

Browse files
Virtualized Table in Project Resources (#7406)
* ENG-68: virtualized table for project resources * dynamic container height when less than 10 items * bump overscan * Update overscan default --------- Co-authored-by: Eric P Green <[email protected]>
1 parent 435f8e1 commit 17364a9

File tree

2 files changed

+212
-5
lines changed

2 files changed

+212
-5
lines changed

web-admin/src/features/projects/status/ProjectResourcesTable.svelte

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { getResourceKindTagColor } from "./display-utils";
1010
import { flexRender } from "@tanstack/svelte-table";
1111
import type { ColumnDef } from "@tanstack/svelte-table";
12-
import BasicTable from "@rilldata/web-common/components/table/BasicTable.svelte";
12+
import VirtualizedTable from "@rilldata/web-common/components/table/VirtualizedTable.svelte";
1313
import RefreshCell from "./RefreshCell.svelte";
1414
import NameCell from "./NameCell.svelte";
1515
import ActionsCell from "./ActionsCell.svelte";
@@ -88,12 +88,14 @@
8888
},
8989
},
9090
];
91-
</script>
9291
93-
<BasicTable
94-
data={data.filter(
92+
$: tableData = data.filter(
9593
(resource) => resource.meta.name.kind !== ResourceKind.Component,
96-
)}
94+
);
95+
</script>
96+
97+
<VirtualizedTable
98+
data={tableData}
9799
{columns}
98100
columnLayout="minmax(95px, 108px) minmax(100px, 3fr) 48px minmax(80px, 2fr) minmax(100px, 2fr) 56px"
99101
/>
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<script lang="ts">
2+
import ArrowDown from "@rilldata/web-common/components/icons/ArrowDown.svelte";
3+
import type {
4+
ColumnDef,
5+
OnChangeFn,
6+
SortingState,
7+
TableOptions,
8+
} from "@tanstack/svelte-table";
9+
import {
10+
createSvelteTable,
11+
flexRender,
12+
getCoreRowModel,
13+
getSortedRowModel,
14+
} from "@tanstack/svelte-table";
15+
import { createVirtualizer } from "@tanstack/svelte-virtual";
16+
import { writable } from "svelte/store";
17+
18+
export let data: any[];
19+
export let columns: ColumnDef<any, any>[];
20+
export let emptyIcon: any | null = null;
21+
export let emptyText = "No data available";
22+
export let columnLayout = `repeat(${columns.length}, 1fr)`;
23+
export let rowPadding = "py-3";
24+
export let rowHeight = 46;
25+
export let containerHeight = 400;
26+
export let overscan = 1;
27+
28+
let containerElement: HTMLDivElement;
29+
let sorting: SortingState = [];
30+
31+
// Initialize sorting for sortDescFirst column
32+
const sortDescFirstColumn = columns.find((col) => col.sortDescFirst);
33+
if (sortDescFirstColumn) {
34+
const columnId =
35+
"id" in sortDescFirstColumn
36+
? sortDescFirstColumn.id
37+
: "accessorKey" in sortDescFirstColumn
38+
? sortDescFirstColumn.accessorKey
39+
: "accessorFn" in sortDescFirstColumn
40+
? (sortDescFirstColumn.header as string)
41+
: Object.keys(sortDescFirstColumn)[0];
42+
43+
sorting = [
44+
{
45+
id: columnId as string,
46+
desc: true,
47+
},
48+
];
49+
}
50+
51+
$: safeData = Array.isArray(data) ? data : [];
52+
$: {
53+
if (safeData) {
54+
options.update((old) => ({
55+
...old,
56+
data: safeData,
57+
}));
58+
}
59+
}
60+
61+
const setSorting: OnChangeFn<SortingState> = (updater) => {
62+
if (updater instanceof Function) {
63+
sorting = updater(sorting);
64+
} else {
65+
sorting = updater;
66+
}
67+
68+
options.update((old) => ({
69+
...old,
70+
state: {
71+
...old.state,
72+
sorting,
73+
},
74+
}));
75+
};
76+
77+
const options = writable<TableOptions<any>>({
78+
data: safeData,
79+
columns: columns,
80+
state: {
81+
sorting,
82+
},
83+
onSortingChange: setSorting,
84+
getCoreRowModel: getCoreRowModel(),
85+
getSortedRowModel: getSortedRowModel(),
86+
enableSortingRemoval: false,
87+
});
88+
89+
const table = createSvelteTable(options);
90+
91+
$: ({ getHeaderGroups } = $table);
92+
$: headers = getHeaderGroups();
93+
94+
$: rows = $table.getRowModel().rows;
95+
$: virtualizer = createVirtualizer({
96+
count: rows.length,
97+
getScrollElement: () => containerElement,
98+
estimateSize: () => rowHeight,
99+
overscan: overscan,
100+
initialOffset: rowScrollOffset,
101+
});
102+
103+
$: virtualRows = $virtualizer.getVirtualItems();
104+
$: rowScrollOffset = $virtualizer?.scrollOffset || 0;
105+
106+
$: dynamicContainerHeight =
107+
rows.length <= 10 ? rowHeight * rows.length : containerHeight;
108+
</script>
109+
110+
<div
111+
class="flex flex-col border rounded-sm overflow-hidden"
112+
style:--grid-template-columns={columnLayout}
113+
>
114+
{#each headers as headerGroup (headerGroup.id)}
115+
<div class="row sticky top-0 z-30 bg-surface">
116+
{#each headerGroup.headers as header (header.id)}
117+
<svelte:element
118+
this={header.column.getCanSort() ? "button" : "div"}
119+
role="columnheader"
120+
tabindex="0"
121+
class="pl-{header.column.columnDef.meta?.marginLeft ||
122+
'4'} py-2 font-semibold text-gray-500 text-left flex flex-row items-center gap-x-1 truncate text-sm"
123+
on:click={header.column.getToggleSortingHandler()}
124+
>
125+
{#if !header.isPlaceholder}
126+
<span class="truncate">
127+
<svelte:component
128+
this={flexRender(
129+
header.column.columnDef.header,
130+
header.getContext(),
131+
)}
132+
/>
133+
</span>
134+
{#if header.column.getIsSorted()}
135+
<span>
136+
<ArrowDown
137+
flip={header.column.getIsSorted().toString() === "asc"}
138+
size="12px"
139+
/>
140+
</span>
141+
{/if}
142+
{/if}
143+
</svelte:element>
144+
{/each}
145+
</div>
146+
{/each}
147+
148+
<div
149+
bind:this={containerElement}
150+
class="relative overflow-auto"
151+
style="height: {dynamicContainerHeight}px;"
152+
>
153+
{#if !rows || rows.length === 0}
154+
<div class="flex flex-col items-center gap-y-1 py-10">
155+
{#if emptyIcon}
156+
<svelte:component this={emptyIcon} size={32} color="#CBD5E1" />
157+
{/if}
158+
<span class="text-gray-600 font-semibold text-sm">{emptyText}</span>
159+
</div>
160+
{:else}
161+
<div
162+
class="relative w-full"
163+
style="height: {$virtualizer.getTotalSize()}px;"
164+
>
165+
{#each virtualRows as virtualRow}
166+
{@const row = rows[virtualRow.index]}
167+
<div
168+
class="row {rowPadding} absolute top-0 left-0 w-full"
169+
style="transform: translateY({virtualRow.start}px);"
170+
>
171+
{#each row.getVisibleCells() as cell (cell.id)}
172+
<div
173+
class="pl-{cell.column.columnDef.meta?.marginLeft ||
174+
'4'} pr-1 flex items-center truncate"
175+
>
176+
<svelte:component
177+
this={flexRender(
178+
cell.column.columnDef.cell,
179+
cell.getContext(),
180+
)}
181+
/>
182+
</div>
183+
{/each}
184+
</div>
185+
{/each}
186+
</div>
187+
{/if}
188+
</div>
189+
</div>
190+
191+
<style lang="postcss">
192+
* {
193+
@apply border-slate-200;
194+
}
195+
196+
.row {
197+
@apply w-fit min-w-full;
198+
display: grid;
199+
grid-template-columns: var(--grid-template-columns);
200+
}
201+
202+
.row:not(:last-of-type) {
203+
@apply border-b;
204+
}
205+
</style>

0 commit comments

Comments
 (0)