|
1 | 1 | <script lang="ts">
|
2 |
| - import { writable } from "svelte/store"; |
3 |
| - import { |
4 |
| - createSvelteTable, |
5 |
| - flexRender, |
6 |
| - getCoreRowModel, |
7 |
| - getSortedRowModel, |
8 |
| - } from "@tanstack/svelte-table"; |
9 |
| - import type { |
10 |
| - ColumnDef, |
11 |
| - OnChangeFn, |
12 |
| - SortingState, |
13 |
| - TableOptions, |
14 |
| - } from "@tanstack/svelte-table"; |
| 2 | + import type { ColumnDef } from "@tanstack/svelte-table"; |
| 3 | + import { flexRender } from "@tanstack/svelte-table"; |
15 | 4 | import PublicURLsActionsRow from "./PublicURLsActionsRow.svelte";
|
16 | 5 | import DashboardLink from "./DashboardLink.svelte";
|
17 |
| - import ArrowDown from "@rilldata/web-common/components/icons/ArrowDown.svelte"; |
18 |
| - import type { V1MagicAuthToken } from "@rilldata/web-admin/client"; |
19 |
| - import { createVirtualizer } from "@tanstack/svelte-virtual"; |
| 6 | + import type { |
| 7 | + V1MagicAuthToken, |
| 8 | + RpcStatus, |
| 9 | + V1ListMagicAuthTokensResponse, |
| 10 | + } from "@rilldata/web-admin/client"; |
| 11 | + import InfiniteScrollTable from "@rilldata/web-common/components/table/InfiniteScrollTable.svelte"; |
| 12 | + import type { |
| 13 | + InfiniteData, |
| 14 | + InfiniteQueryObserverResult, |
| 15 | + } from "@tanstack/svelte-query"; |
20 | 16 |
|
21 | 17 | interface MagicAuthTokenProps extends V1MagicAuthToken {
|
22 | 18 | dashboardTitle: string;
|
23 | 19 | }
|
24 | 20 |
|
25 |
| - const ROW_HEIGHT = 40; |
26 |
| - const OVERSCAN = 5; |
27 |
| -
|
28 | 21 | export let data: MagicAuthTokenProps[];
|
29 |
| - export let query: any; |
| 22 | + export let query: InfiniteQueryObserverResult< |
| 23 | + InfiniteData<V1ListMagicAuthTokensResponse, unknown>, |
| 24 | + RpcStatus |
| 25 | + >; |
30 | 26 | export let onDelete: (deletedTokenId: string) => void;
|
31 | 27 |
|
32 |
| - let virtualListEl: HTMLDivElement; |
33 |
| - let sorting: SortingState = []; |
34 |
| -
|
35 |
| - function formatDate(value: string) { |
36 |
| - return new Date(value).toLocaleDateString(undefined, { |
37 |
| - year: "numeric", |
38 |
| - month: "short", |
39 |
| - day: "numeric", |
40 |
| - hour: "numeric", |
41 |
| - minute: "numeric", |
42 |
| - }); |
43 |
| - } |
44 |
| -
|
45 |
| - // Re-render table when data changes |
46 | 28 | $: safeData = Array.isArray(data) ? data : [];
|
47 |
| - $: { |
48 |
| - if (safeData) { |
49 |
| - options.update((old) => ({ |
50 |
| - ...old, |
51 |
| - data: safeData, |
52 |
| - })); |
53 |
| - } |
54 |
| - } |
| 29 | +
|
| 30 | + $: dynamicTableMaxHeight = |
| 31 | + safeData.length > 12 ? `calc(100dvh - 300px)` : "auto"; |
55 | 32 |
|
56 | 33 | const columns: ColumnDef<MagicAuthTokenProps, any>[] = [
|
57 | 34 | {
|
|
83 | 60 | {
|
84 | 61 | accessorKey: "usedOn",
|
85 | 62 | header: "Last acccesed",
|
| 63 | + sortDescFirst: true, |
86 | 64 | cell: (info) => {
|
87 | 65 | if (!info.getValue()) return "-";
|
88 | 66 | const date = formatDate(info.getValue() as string);
|
|
102 | 80 | },
|
103 | 81 | ];
|
104 | 82 |
|
105 |
| - const setSorting: OnChangeFn<SortingState> = (updater) => { |
106 |
| - if (updater instanceof Function) { |
107 |
| - sorting = updater(sorting); |
108 |
| - } else { |
109 |
| - sorting = updater; |
110 |
| - } |
111 |
| -
|
112 |
| - options.update((old) => ({ |
113 |
| - ...old, |
114 |
| - state: { |
115 |
| - ...old.state, |
116 |
| - sorting, |
117 |
| - }, |
118 |
| - })); |
119 |
| - }; |
120 |
| -
|
121 |
| - const options = writable<TableOptions<MagicAuthTokenProps>>({ |
122 |
| - data: safeData, |
123 |
| - columns, |
124 |
| - state: { |
125 |
| - sorting, |
126 |
| - }, |
127 |
| - onSortingChange: setSorting, |
128 |
| - getCoreRowModel: getCoreRowModel(), |
129 |
| - getSortedRowModel: getSortedRowModel(), |
130 |
| - }); |
131 |
| -
|
132 |
| - const table = createSvelteTable(options); |
133 |
| -
|
134 |
| - $: rows = $table.getRowModel().rows; |
135 |
| -
|
136 |
| - $: virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({ |
137 |
| - count: 0, |
138 |
| - getScrollElement: () => virtualListEl, |
139 |
| - estimateSize: () => ROW_HEIGHT, |
140 |
| - overscan: OVERSCAN, |
141 |
| - }); |
142 |
| -
|
143 |
| - $: { |
144 |
| - $virtualizer.setOptions({ |
145 |
| - count: query.hasNextPage ? safeData.length + 1 : safeData.length, |
| 83 | + function formatDate(value: string) { |
| 84 | + return new Date(value).toLocaleDateString(undefined, { |
| 85 | + year: "numeric", |
| 86 | + month: "short", |
| 87 | + day: "numeric", |
| 88 | + hour: "numeric", |
| 89 | + minute: "numeric", |
146 | 90 | });
|
147 |
| -
|
148 |
| - const [lastItem] = [...$virtualizer.getVirtualItems()].reverse(); |
149 |
| -
|
150 |
| - if ( |
151 |
| - lastItem && |
152 |
| - lastItem.index > safeData.length - 1 && |
153 |
| - query.hasNextPage && |
154 |
| - !query.isFetchingNextPage |
155 |
| - ) { |
156 |
| - query.fetchNextPage(); |
157 |
| - } |
158 | 91 | }
|
159 | 92 | </script>
|
160 | 93 |
|
161 |
| -<div class="list scroll-container" bind:this={virtualListEl}> |
162 |
| - <!-- FIXME: hide the bleeding corner in the sticky header --> |
163 |
| - <div |
164 |
| - class="table-wrapper" |
165 |
| - style="position: relative; height: {$virtualizer.getTotalSize()}px;" |
166 |
| - > |
167 |
| - <table> |
168 |
| - <thead> |
169 |
| - {#each $table.getHeaderGroups() as headerGroup} |
170 |
| - <tr class="h-10"> |
171 |
| - {#each headerGroup.headers as header} |
172 |
| - <th |
173 |
| - colSpan={header.colSpan} |
174 |
| - class="px-4 py-2 text-left" |
175 |
| - on:click={header.column.getToggleSortingHandler()} |
176 |
| - > |
177 |
| - {#if !header.isPlaceholder} |
178 |
| - <div |
179 |
| - class:cursor-pointer={header.column.getCanSort()} |
180 |
| - class:select-none={header.column.getCanSort()} |
181 |
| - class="font-semibold text-gray-500 flex flex-row items-center gap-x-1 text-sm" |
182 |
| - > |
183 |
| - <svelte:component |
184 |
| - this={flexRender( |
185 |
| - header.column.columnDef.header, |
186 |
| - header.getContext(), |
187 |
| - )} |
188 |
| - /> |
189 |
| - {#if header.column.getIsSorted().toString() === "asc"} |
190 |
| - <span> |
191 |
| - <ArrowDown flip size="12px" /> |
192 |
| - </span> |
193 |
| - {:else if header.column.getIsSorted().toString() === "desc"} |
194 |
| - <span> |
195 |
| - <ArrowDown size="12px" /> |
196 |
| - </span> |
197 |
| - {/if} |
198 |
| - </div> |
199 |
| - {/if} |
200 |
| - </th> |
201 |
| - {/each} |
202 |
| - </tr> |
203 |
| - {/each} |
204 |
| - </thead> |
205 |
| - <tbody> |
206 |
| - {#each $virtualizer.getVirtualItems() as virtualRow, idx (virtualRow.index)} |
207 |
| - <tr |
208 |
| - style="height: {virtualRow.size}px; transform: translateY({virtualRow.start - |
209 |
| - idx * virtualRow.size}px);" |
210 |
| - > |
211 |
| - {#each rows[virtualRow.index]?.getVisibleCells() ?? [] as cell (cell.id)} |
212 |
| - <td |
213 |
| - class={`px-4 py-2 max-w-[200px] truncate ${cell.column.id === "actions" ? "w-1" : ""}`} |
214 |
| - data-label={cell.column.columnDef.header} |
215 |
| - > |
216 |
| - <svelte:component |
217 |
| - this={flexRender( |
218 |
| - cell.column.columnDef.cell, |
219 |
| - cell.getContext(), |
220 |
| - )} |
221 |
| - /> |
222 |
| - </td> |
223 |
| - {/each} |
224 |
| - </tr> |
225 |
| - {/each} |
226 |
| - </tbody> |
227 |
| - </table> |
228 |
| - </div> |
229 |
| -</div> |
230 |
| - |
231 |
| -<style lang="postcss"> |
232 |
| - table { |
233 |
| - @apply border-separate border-spacing-0 w-full; |
234 |
| - } |
235 |
| - table th, |
236 |
| - table td { |
237 |
| - @apply border-b border-gray-200; |
238 |
| - } |
239 |
| - thead { |
240 |
| - @apply sticky top-0 z-30 bg-white; |
241 |
| - } |
242 |
| - thead tr th { |
243 |
| - @apply border-t border-gray-200; |
244 |
| - } |
245 |
| - thead tr th:first-child { |
246 |
| - @apply border-l; |
247 |
| - @apply rounded-tl-sm; |
248 |
| - } |
249 |
| - thead tr th:last-child { |
250 |
| - @apply border-r; |
251 |
| - @apply rounded-tr-sm; |
252 |
| - } |
253 |
| - thead tr:last-child th { |
254 |
| - @apply border-b; |
255 |
| - } |
256 |
| - tbody tr:first-child { |
257 |
| - @apply border-t-0; |
258 |
| - } |
259 |
| - tbody td:first-child { |
260 |
| - @apply border-l; |
261 |
| - } |
262 |
| - tbody td:last-child { |
263 |
| - @apply border-r; |
264 |
| - } |
265 |
| - tbody tr:last-child td:first-child { |
266 |
| - @apply rounded-bl-sm; |
267 |
| - } |
268 |
| - tbody tr:last-child td:last-child { |
269 |
| - @apply rounded-br-sm; |
270 |
| - } |
271 |
| - .scroll-container { |
272 |
| - height: 680px; |
273 |
| - width: 100%; |
274 |
| - overflow-y: auto; |
275 |
| - } |
276 |
| -</style> |
| 94 | +<InfiniteScrollTable |
| 95 | + data={safeData} |
| 96 | + {columns} |
| 97 | + hasNextPage={query.hasNextPage} |
| 98 | + isFetchingNextPage={query.isFetchingNextPage} |
| 99 | + onLoadMore={() => query.fetchNextPage()} |
| 100 | + maxHeight={dynamicTableMaxHeight} |
| 101 | + rowHeight={40} |
| 102 | +/> |
0 commit comments