From 3c1bc9aca8f4a2d6f5e29ebdd7aaee55b049e3c3 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:00:22 -0400 Subject: [PATCH 01/32] feat: add shared `TableToolbar` component for table view pages Replaces inconsistent toolbar UIs across alerts, reports, public URLs, and environment variables pages with a single reusable component. The toolbar provides: filter dropdown (left), search + sort + optional grid/list toggle (right). Filter options are page-specific via props. Co-Authored-By: Claude Opus 4.6 --- .../alerts/listing/AlertsTable.svelte | 19 ++-- .../PublicURLsResourceTable.svelte | 32 ++++-- .../listing/ReportsTable.svelte | 23 ++-- .../environment-variables/+page.svelte | 106 ++++++------------ .../table-toolbar/TableToolbar.svelte | 59 ++++++++++ .../TableToolbarFilterDropdown.svelte | 51 +++++++++ .../TableToolbarSortDropdown.svelte | 51 +++++++++ .../TableToolbarViewToggle.svelte | 37 ++++++ .../src/components/table-toolbar/index.ts | 10 ++ .../src/components/table-toolbar/types.ts | 19 ++++ .../resources/ResourceTableToolbar.svelte | 66 +++++++++++ 11 files changed, 373 insertions(+), 100 deletions(-) create mode 100644 web-common/src/components/table-toolbar/TableToolbar.svelte create mode 100644 web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte create mode 100644 web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte create mode 100644 web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte create mode 100644 web-common/src/components/table-toolbar/index.ts create mode 100644 web-common/src/components/table-toolbar/types.ts create mode 100644 web-common/src/features/resources/ResourceTableToolbar.svelte diff --git a/web-admin/src/features/alerts/listing/AlertsTable.svelte b/web-admin/src/features/alerts/listing/AlertsTable.svelte index 034a1e66ca48..e806512e9a1a 100644 --- a/web-admin/src/features/alerts/listing/AlertsTable.svelte +++ b/web-admin/src/features/alerts/listing/AlertsTable.svelte @@ -1,6 +1,7 @@ - + + + + import { Chip } from "@rilldata/web-common/components/chip"; - import { Search } from "@rilldata/web-common/components/search"; + import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; + import type { SortDirection } from "@rilldata/web-common/components/table-toolbar/types"; import { ExternalLinkIcon } from "lucide-svelte"; import type { V1MagicAuthToken } from "@rilldata/web-admin/client"; import type { V1Expression } from "@rilldata/web-admin/client"; @@ -15,6 +16,7 @@ export let onDelete: (deletedTokenId: string) => void; let searchText = ""; + let sortDirection: SortDirection = "newest"; $: filteredData = data.filter((row) => { if (!searchText) return true; @@ -25,6 +27,16 @@ return label.includes(q) || dashboard.includes(q) || creator.includes(q); }); + $: sortedData = [...filteredData].sort((a, b) => { + const aTime = new Date(a.createdOn ?? 0).getTime(); + const bTime = new Date(b.createdOn ?? 0).getTime(); + return sortDirection === "newest" ? bTime - aTime : aTime - bTime; + }); + + function handleSortChange(direction: SortDirection) { + sortDirection = direction; + } + function formatDate(value: string | undefined) { if (!value) return "—"; return new Date(value).toLocaleDateString(undefined, { @@ -69,16 +81,14 @@
- - {#if filteredData.length === 0 && data.length === 0} + {#if sortedData.length === 0 && data.length === 0}
- {:else if filteredData.length === 0} + {:else if sortedData.length === 0}
No public URLs match your search @@ -112,7 +122,7 @@ - {#each filteredData as row (row.id)} + {#each sortedData as row (row.id)} {@const filters = getFilters(row.metricsViewFilters)} diff --git a/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte b/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte index b36870730788..dd27338012e7 100644 --- a/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte +++ b/web-admin/src/features/scheduled-reports/listing/ReportsTable.svelte @@ -1,6 +1,7 @@ - + + + + { - // Show all variables if (filterByEnvironment === EnvironmentType.UNDEFINED) { return true; } - // Includes development if (filterByEnvironment === EnvironmentType.DEVELOPMENT) { return ( variable.environment === EnvironmentType.DEVELOPMENT || variable.environment === EnvironmentType.UNDEFINED ); } - // Includes production if (filterByEnvironment === EnvironmentType.PRODUCTION) { return ( variable.environment === EnvironmentType.PRODUCTION || variable.environment === EnvironmentType.UNDEFINED ); } - // No match return false; }); - $: sortedVariables = filteredVariables.sort((a, b) => { - return new Date(b.updatedOn).getTime() - new Date(a.updatedOn).getTime(); + $: sortedVariables = [...filteredVariables].sort((a, b) => { + const aTime = new Date(a.updatedOn).getTime(); + const bTime = new Date(b.updatedOn).getTime(); + return sortDirection === "newest" ? bTime - aTime : aTime - bTime; }); - function handleFilterByEnvironment(environment: EnvironmentTypes) { - filterByEnvironment = environment; + function handleFilterChange(_key: string, value: string) { + filterByEnvironment = value as EnvironmentTypes; + } + + function handleSortChange(direction: SortDirection) { + sortDirection = direction; } $: environmentLabel = @@ -88,6 +88,19 @@ filterByEnvironment === EnvironmentType.UNDEFINED ? "No environment variables" : `No environment variables for ${environmentLabel}`; + + $: filterGroups = [ + { + label: "Filter by environment", + key: "environment", + options: [ + { value: EnvironmentType.UNDEFINED, label: "All environments" }, + { value: EnvironmentType.PRODUCTION, label: "Production" }, + { value: EnvironmentType.DEVELOPMENT, label: "Development" }, + ], + selected: filterByEnvironment, + }, + ];
@@ -112,64 +125,19 @@

-
- - - - {environmentLabel} - {#if isDropdownOpen} - - {:else} - - {/if} - - - Filter by environment - - handleFilterByEnvironment(EnvironmentType.UNDEFINED)} - > - All environments - - - handleFilterByEnvironment(EnvironmentType.PRODUCTION)} - > - Production - - - handleFilterByEnvironment(EnvironmentType.DEVELOPMENT)} - > - Development - - - +
+
+ +
+ import { Search } from "@rilldata/web-common/components/search"; + import TableToolbarFilterDropdown from "./TableToolbarFilterDropdown.svelte"; + import TableToolbarSortDropdown from "./TableToolbarSortDropdown.svelte"; + import TableToolbarViewToggle from "./TableToolbarViewToggle.svelte"; + import type { FilterGroup, SortDirection, ViewMode } from "./types"; + + let { + searchText = $bindable(""), + searchPlaceholder = "Search", + searchDisabled = false, + filterGroups = [], + onFilterChange, + sortDirection = "newest", + onSortChange, + showSort = true, + showViewToggle = false, + viewMode = $bindable("list"), + onViewModeChange, + }: { + searchText?: string; + searchPlaceholder?: string; + searchDisabled?: boolean; + filterGroups?: FilterGroup[]; + onFilterChange?: (key: string, value: string) => void; + sortDirection?: SortDirection; + onSortChange?: (direction: SortDirection) => void; + showSort?: boolean; + showViewToggle?: boolean; + viewMode?: ViewMode; + onViewModeChange?: (mode: ViewMode) => void; + } = $props(); + + +
+
+ +
+ +
+
+ +
+ + {#if showSort} + + {/if} + + {#if showViewToggle} + + {/if} +
+
diff --git a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte new file mode 100644 index 000000000000..ac3ad9afb6c0 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte @@ -0,0 +1,51 @@ + + +{#if filterGroups.length > 0} + + + + Filter + {#if isOpen} + + {:else} + + {/if} + + + {#each filterGroups as group} + {group.label} + {#each group.options as option} + onFilterChange?.(group.key, option.value)} + > + {option.label} + + {/each} + {#if filterGroups.indexOf(group) < filterGroups.length - 1} + + {/if} + {/each} + + +{/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte new file mode 100644 index 000000000000..47ae9789e64a --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte @@ -0,0 +1,51 @@ + + + + + {sortLabel} + {#if isOpen} + + {:else} + + {/if} + + + + onSortChange?.("newest")} + > + Newest + + onSortChange?.("oldest")} + > + Oldest + + + + diff --git a/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte new file mode 100644 index 000000000000..b8a4f135dd43 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte @@ -0,0 +1,37 @@ + + +
+ + +
diff --git a/web-common/src/components/table-toolbar/index.ts b/web-common/src/components/table-toolbar/index.ts new file mode 100644 index 000000000000..007db95627be --- /dev/null +++ b/web-common/src/components/table-toolbar/index.ts @@ -0,0 +1,10 @@ +export { default as TableToolbar } from "./TableToolbar.svelte"; +export { default as TableToolbarFilterDropdown } from "./TableToolbarFilterDropdown.svelte"; +export { default as TableToolbarSortDropdown } from "./TableToolbarSortDropdown.svelte"; +export { default as TableToolbarViewToggle } from "./TableToolbarViewToggle.svelte"; +export type { + FilterGroup, + FilterOption, + SortDirection, + ViewMode, +} from "./types"; diff --git a/web-common/src/components/table-toolbar/types.ts b/web-common/src/components/table-toolbar/types.ts new file mode 100644 index 000000000000..bfd001428eae --- /dev/null +++ b/web-common/src/components/table-toolbar/types.ts @@ -0,0 +1,19 @@ +export type SortDirection = "newest" | "oldest"; + +export type ViewMode = "list" | "grid"; + +export interface FilterOption { + value: string; + label: string; +} + +export interface FilterGroup { + /** Dropdown section header */ + label: string; + /** Unique key for this filter group */ + key: string; + /** Available options */ + options: FilterOption[]; + /** Currently selected value */ + selected: string; +} diff --git a/web-common/src/features/resources/ResourceTableToolbar.svelte b/web-common/src/features/resources/ResourceTableToolbar.svelte new file mode 100644 index 000000000000..fd12557e32c3 --- /dev/null +++ b/web-common/src/features/resources/ResourceTableToolbar.svelte @@ -0,0 +1,66 @@ + + + From bc8ced57ce1e58b7ed1c14e17b77d5f7a732388d Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:32:05 -0400 Subject: [PATCH 02/32] refactor: redesign `TableToolbar` to match updated specs - "Filter" is now plain clickable text opening a dropdown; applied filters show as removable chips with "Clear all" below the toolbar - "Newest" is a toggle button (not dropdown) that flips sort order - Search is a magnifying glass icon (h-9) that expands to an inline input; search text syncs to URL via ?q= param - Add `TableToolbarAppliedFilters`, `TableToolbarSearch`, `TableToolbarSort` sub-components - Remove old `TableToolbarSortDropdown` Co-Authored-By: Claude Opus 4.6 --- .../alerts/listing/AlertsTable.svelte | 10 --- .../PublicURLsResourceTable.svelte | 6 +- .../environment-variables/+page.svelte | 12 +++- .../table-toolbar/TableToolbar.svelte | 53 +++++++-------- .../TableToolbarAppliedFilters.svelte | 51 +++++++++++++++ .../TableToolbarFilterDropdown.svelte | 21 ++---- .../table-toolbar/TableToolbarSearch.svelte | 65 +++++++++++++++++++ .../table-toolbar/TableToolbarSort.svelte | 24 +++++++ .../TableToolbarSortDropdown.svelte | 51 --------------- .../src/components/table-toolbar/index.ts | 4 +- .../src/components/table-toolbar/types.ts | 2 + .../resources/ResourceTableToolbar.svelte | 50 +++++++++++--- 12 files changed, 230 insertions(+), 119 deletions(-) create mode 100644 web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte create mode 100644 web-common/src/components/table-toolbar/TableToolbarSearch.svelte create mode 100644 web-common/src/components/table-toolbar/TableToolbarSort.svelte delete mode 100644 web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte diff --git a/web-admin/src/features/alerts/listing/AlertsTable.svelte b/web-admin/src/features/alerts/listing/AlertsTable.svelte index e806512e9a1a..baaeea9ba410 100644 --- a/web-admin/src/features/alerts/listing/AlertsTable.svelte +++ b/web-admin/src/features/alerts/listing/AlertsTable.svelte @@ -11,16 +11,6 @@ export let organization: string; export let project: string; - /** - * Table column definitions. - * - "composite": Renders all dashboard data in a single cell. - * - Others: Used for sorting and filtering but not displayed. - * - * Note: TypeScript error prevents using `ColumnDef[]`. - * Relevant issues: - * - https://github.com/TanStack/table/issues/4241 - * - https://github.com/TanStack/table/issues/4302 - */ const columns: ColumnDef[] = [ { id: "composite", diff --git a/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte b/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte index af25f60b9f38..4a79cafe23bc 100644 --- a/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte +++ b/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte @@ -33,8 +33,8 @@ return sortDirection === "newest" ? bTime - aTime : aTime - bTime; }); - function handleSortChange(direction: SortDirection) { - sortDirection = direction; + function handleSortToggle() { + sortDirection = sortDirection === "newest" ? "oldest" : "newest"; } function formatDate(value: string | undefined) { @@ -85,7 +85,7 @@ bind:searchText searchDisabled={data.length === 0} {sortDirection} - onSortChange={handleSortChange} + onSortToggle={handleSortToggle} /> {#if sortedData.length === 0 && data.length === 0} diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte index 5e38ff486d26..2b0e42603a1e 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte @@ -73,8 +73,12 @@ filterByEnvironment = value as EnvironmentTypes; } - function handleSortChange(direction: SortDirection) { - sortDirection = direction; + function handleSortToggle() { + sortDirection = sortDirection === "newest" ? "oldest" : "newest"; + } + + function handleClearAllFilters() { + filterByEnvironment = EnvironmentType.UNDEFINED; } $: environmentLabel = @@ -99,6 +103,7 @@ { value: EnvironmentType.DEVELOPMENT, label: "Development" }, ], selected: filterByEnvironment, + defaultValue: EnvironmentType.UNDEFINED, }, ]; @@ -132,8 +137,9 @@ searchDisabled={projectVariables.length === 0} {filterGroups} onFilterChange={handleFilterChange} + onClearAllFilters={handleClearAllFilters} {sortDirection} - onSortChange={handleSortChange} + onSortToggle={handleSortToggle} />
+
+{/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte index ac3ad9afb6c0..3f37fb3c8ce4 100644 --- a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte @@ -1,8 +1,6 @@ {#if filterGroups.length > 0} - + - Filter - {#if isOpen} - - {:else} - - {/if} + Filter - {#each filterGroups as group} + {#each filterGroups as group, i} {group.label} {#each group.options as option} {/each} - {#if filterGroups.indexOf(group) < filterGroups.length - 1} + {#if i < filterGroups.length - 1} {/if} {/each} diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte new file mode 100644 index 000000000000..a005fd2a0695 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -0,0 +1,65 @@ + + +{#if expanded} +
+ + + +
+{:else} + +{/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarSort.svelte b/web-common/src/components/table-toolbar/TableToolbarSort.svelte new file mode 100644 index 000000000000..cc0912fc048b --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarSort.svelte @@ -0,0 +1,24 @@ + + + diff --git a/web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte deleted file mode 100644 index 47ae9789e64a..000000000000 --- a/web-common/src/components/table-toolbar/TableToolbarSortDropdown.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - - - {sortLabel} - {#if isOpen} - - {:else} - - {/if} - - - - onSortChange?.("newest")} - > - Newest - - onSortChange?.("oldest")} - > - Oldest - - - - diff --git a/web-common/src/components/table-toolbar/index.ts b/web-common/src/components/table-toolbar/index.ts index 007db95627be..e2e4bf1a1d3f 100644 --- a/web-common/src/components/table-toolbar/index.ts +++ b/web-common/src/components/table-toolbar/index.ts @@ -1,6 +1,8 @@ export { default as TableToolbar } from "./TableToolbar.svelte"; +export { default as TableToolbarAppliedFilters } from "./TableToolbarAppliedFilters.svelte"; export { default as TableToolbarFilterDropdown } from "./TableToolbarFilterDropdown.svelte"; -export { default as TableToolbarSortDropdown } from "./TableToolbarSortDropdown.svelte"; +export { default as TableToolbarSearch } from "./TableToolbarSearch.svelte"; +export { default as TableToolbarSort } from "./TableToolbarSort.svelte"; export { default as TableToolbarViewToggle } from "./TableToolbarViewToggle.svelte"; export type { FilterGroup, diff --git a/web-common/src/components/table-toolbar/types.ts b/web-common/src/components/table-toolbar/types.ts index bfd001428eae..2564c6aebd30 100644 --- a/web-common/src/components/table-toolbar/types.ts +++ b/web-common/src/components/table-toolbar/types.ts @@ -16,4 +16,6 @@ export interface FilterGroup { options: FilterOption[]; /** Currently selected value */ selected: string; + /** Default value; when selected === defaultValue, no chip is shown */ + defaultValue: string; } diff --git a/web-common/src/features/resources/ResourceTableToolbar.svelte b/web-common/src/features/resources/ResourceTableToolbar.svelte index fd12557e32c3..e642e19b53e2 100644 --- a/web-common/src/features/resources/ResourceTableToolbar.svelte +++ b/web-common/src/features/resources/ResourceTableToolbar.svelte @@ -1,32 +1,37 @@ {#if expanded} -
+
Date: Thu, 2 Apr 2026 16:57:25 -0400 Subject: [PATCH 05/32] fix: switch search to callback pattern for reliable state propagation Replace $bindable chain (3-level deep binding wasn't propagating in TanStack pages) with onSearchChange callback. All consumers now use the callback to update search state. Co-Authored-By: Claude Opus 4.6 --- .../public-urls/PublicURLsResourceTable.svelte | 3 ++- .../settings/environment-variables/+page.svelte | 3 ++- .../components/table-toolbar/TableToolbar.svelte | 15 +++++++++++++-- .../table-toolbar/TableToolbarSearch.svelte | 14 ++++++++------ .../resources/ResourceTableToolbar.svelte | 16 +++++++++------- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte b/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte index 4a79cafe23bc..c5f8593c101c 100644 --- a/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte +++ b/web-admin/src/features/public-urls/PublicURLsResourceTable.svelte @@ -82,7 +82,8 @@
(searchText = text)} searchDisabled={data.length === 0} {sortDirection} onSortToggle={handleSortToggle} diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte index 2b0e42603a1e..932ee5df6b68 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte @@ -133,7 +133,8 @@
(searchText = text)} searchDisabled={projectVariables.length === 0} {filterGroups} onFilterChange={handleFilterChange} diff --git a/web-common/src/components/table-toolbar/TableToolbar.svelte b/web-common/src/components/table-toolbar/TableToolbar.svelte index 40cca46fceb3..a15645e0bcd2 100644 --- a/web-common/src/components/table-toolbar/TableToolbar.svelte +++ b/web-common/src/components/table-toolbar/TableToolbar.svelte @@ -7,7 +7,8 @@ import type { FilterGroup, SortDirection, ViewMode } from "./types"; let { - searchText = $bindable(""), + searchText = "", + onSearchChange, searchDisabled = false, filterGroups = [], onFilterChange, @@ -20,6 +21,7 @@ onViewModeChange, }: { searchText?: string; + onSearchChange?: (text: string) => void; searchDisabled?: boolean; filterGroups?: FilterGroup[]; onFilterChange?: (key: string, value: string) => void; @@ -31,6 +33,11 @@ viewMode?: ViewMode; onViewModeChange?: (mode: ViewMode) => void; } = $props(); + + function handleSearchChange(text: string) { + searchText = text; + onSearchChange?.(text); + }
@@ -44,7 +51,11 @@ {/if} - + {#if showViewToggle} diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte index 43cb592b3ee7..1c4f7f039847 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -4,10 +4,12 @@ import { tick } from "svelte"; let { - searchText = $bindable(""), + searchText = "", + onSearchChange, disabled = false, }: { searchText?: string; + onSearchChange?: (text: string) => void; disabled?: boolean; } = $props(); @@ -22,19 +24,19 @@ } function close() { - searchText = ""; + onSearchChange?.(""); expanded = false; } + function handleInput(e: Event) { + onSearchChange?.((e.target as HTMLInputElement).value); + } + function handleKeyDown(e: KeyboardEvent) { if (e.key === "Escape") { close(); } } - - function handleInput(e: Event) { - searchText = (e.target as HTMLInputElement).value; - } {#if expanded} diff --git a/web-common/src/features/resources/ResourceTableToolbar.svelte b/web-common/src/features/resources/ResourceTableToolbar.svelte index f6f40a7e2492..fd4589763b8b 100644 --- a/web-common/src/features/resources/ResourceTableToolbar.svelte +++ b/web-common/src/features/resources/ResourceTableToolbar.svelte @@ -53,8 +53,11 @@ mounted = true; }); - // Sync search to TanStack global filter; untrack $table to avoid - // re-triggering when the store updates from setGlobalFilter + function onSearchChange(text: string) { + searchText = text; + } + + // Sync search to TanStack global filter $effect(() => { const text = searchText; untrack(() => { @@ -62,7 +65,7 @@ }); }); - // Sync search to URL; untrack to avoid circular $page dependency + // Sync search to URL $effect(() => { const text = searchText; untrack(() => { @@ -72,9 +75,7 @@ }); }); - // Sync URL back to state on external navigation (back/forward); - // subscribe to $page reactively, but write searchText in untrack - // to avoid re-triggering the URL sync effect + // Sync URL back to state on external navigation (back/forward) $effect(() => { const url = $page.url; if (mounted && filterSync.hasExternalNavigation(url)) { @@ -98,7 +99,8 @@ Date: Thu, 2 Apr 2026 17:44:13 -0400 Subject: [PATCH 06/32] fix: remove prop shadow in `TableToolbar` search handler Remove local `searchText = text` write in `handleSearchChange` which created a Svelte 5 prop shadow, disconnecting the input from parent state. Co-Authored-By: Claude Opus 4.6 --- web-common/src/components/table-toolbar/TableToolbar.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/web-common/src/components/table-toolbar/TableToolbar.svelte b/web-common/src/components/table-toolbar/TableToolbar.svelte index a15645e0bcd2..28f54b9f585c 100644 --- a/web-common/src/components/table-toolbar/TableToolbar.svelte +++ b/web-common/src/components/table-toolbar/TableToolbar.svelte @@ -35,7 +35,6 @@ } = $props(); function handleSearchChange(text: string) { - searchText = text; onSearchChange?.(text); } From b09fcb21fc078be36e0afb80b8234a426dc3ff98 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:50:52 -0400 Subject: [PATCH 07/32] fix: revert alerts/reports, polish `TableToolbar` UI - Revert alerts and reports tables to original `ResourceList` pattern - Replace filter icon with `ListFilter` from lucide-svelte - Filter pills: white bg, h-7, in h-9 container vertically centered - Reorder toolbar right side: Search, Newest, View toggle - Filter and Newest text use `text-fg-primary` - Env vars page: `+` button aligns top with `items-start` Co-Authored-By: Claude Opus 4.6 --- .../alerts/listing/AlertsTable.svelte | 29 ++++++++++++------- .../listing/ReportsTable.svelte | 23 ++++++++------- .../environment-variables/+page.svelte | 2 +- .../table-toolbar/TableToolbar.svelte | 8 ++--- .../TableToolbarAppliedFilters.svelte | 25 ++++++++-------- .../TableToolbarFilterDropdown.svelte | 6 ++-- .../table-toolbar/TableToolbarSort.svelte | 2 +- 7 files changed, 52 insertions(+), 43 deletions(-) diff --git a/web-admin/src/features/alerts/listing/AlertsTable.svelte b/web-admin/src/features/alerts/listing/AlertsTable.svelte index baaeea9ba410..034a1e66ca48 100644 --- a/web-admin/src/features/alerts/listing/AlertsTable.svelte +++ b/web-admin/src/features/alerts/listing/AlertsTable.svelte @@ -1,7 +1,6 @@ - - - - + import ResourceList from "@rilldata/web-common/features/resources/ResourceList.svelte"; import ResourceListEmptyState from "@rilldata/web-common/features/resources/ResourceListEmptyState.svelte"; - import ResourceTableToolbar from "@rilldata/web-common/features/resources/ResourceTableToolbar.svelte"; import ReportIcon from "@rilldata/web-common/components/icons/ReportIcon.svelte"; import type { V1Resource } from "@rilldata/web-common/runtime-client"; import { renderComponent, type ColumnDef } from "tanstack-table-8-svelte-5"; @@ -48,6 +47,17 @@ id: "lastRun", accessorFn: (row) => row.report.state.currentExecution?.reportTime, }, + // { + // id: "nextRun", + // accessorFn: (row) => row.nextRun, + // }, + // { + // id: "actions", + // cell: ({ row }) => + // renderComponent(ReportsTableActionCell, { + // title: row.original.name, + // }), + // }, ]; const columnVisibility = { @@ -56,16 +66,7 @@ }; - - - - +

-
+
- {#if showSort} - - {/if} - + {#if showSort} + + {/if} + {#if showViewToggle} {/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte index 5d5049c7703a..699a8beb84a0 100644 --- a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte @@ -1,5 +1,5 @@ {#if appliedFilters.length > 0} -
+
{#each appliedFilters as filter (filter.key)} - onFilterChange?.(filter.key, filter.defaultValue)} + - {filter.label} - + {filter.label} + + {/each}
-
-
- (searchText = text)} - searchDisabled={projectVariables.length === 0} - {filterGroups} - onFilterChange={handleFilterChange} - onClearAllFilters={handleClearAllFilters} - {sortDirection} - onSortToggle={handleSortToggle} - /> -
+ (searchText = text)} + searchDisabled={projectVariables.length === 0} + {filterGroups} + onFilterChange={handleFilterChange} + onClearAllFilters={handleClearAllFilters} + {sortDirection} + onSortToggle={handleSortToggle} + > -
+ void; @@ -32,6 +34,7 @@ showViewToggle?: boolean; viewMode?: ViewMode; onViewModeChange?: (mode: ViewMode) => void; + children?: Snippet; } = $props(); function handleSearchChange(text: string) { @@ -40,25 +43,27 @@
-
+
+ {#if showSort} + + {/if} + - {#if showSort} - - {/if} - {#if showViewToggle} {/if} + + {@render children?.()}
diff --git a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte index 699a8beb84a0..4d146ad39f05 100644 --- a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte @@ -25,11 +25,13 @@ {#if appliedFilters.length > 0} -
+
{#each appliedFilters as filter (filter.key)} {filter.label}
- {#if showSort} - - {/if} - + {#if showSort} + + {/if} + {#if showViewToggle} {/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte index ff74f524c2d2..d4f48e48ef00 100644 --- a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte @@ -16,6 +16,7 @@ Filter diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte index 1c4f7f039847..7711b21205f5 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -63,11 +63,10 @@
{:else} diff --git a/web-common/src/components/table-toolbar/TableToolbarSort.svelte b/web-common/src/components/table-toolbar/TableToolbarSort.svelte index 96821d3fb60e..14b8fba776ad 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSort.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSort.svelte @@ -1,5 +1,5 @@ diff --git a/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte index b8a4f135dd43..797d3ae63232 100644 --- a/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte @@ -15,7 +15,7 @@
-
+ {#if filterGroups.length > 0} +
- + + {/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte index 7711b21205f5..b00662b53d36 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -63,7 +63,7 @@
{:else} diff --git a/web-common/src/components/table/BasicTable.svelte b/web-common/src/components/table/BasicTable.svelte index da29ca09c885..f747c0921c30 100644 --- a/web-common/src/components/table/BasicTable.svelte +++ b/web-common/src/components/table/BasicTable.svelte @@ -20,7 +20,6 @@ export let emptyText = "No data available"; export let columnLayout = `repeat(${columns.length}, 1fr)`; export let rowPadding = "py-3"; - export let enableSorting = true; let sorting: SortingState = []; @@ -73,7 +72,6 @@ const options = writable>({ data: safeData, columns: columns, - enableSorting, state: { sorting, }, From 7eae9593f6e48e43b05aa34778564e2033942ae3 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:29:37 -0400 Subject: [PATCH 15/32] feat: add `externalSort` prop to BasicTable to hide sort arrows When toolbar controls sort order, BasicTable clears its internal sorting state so column header arrows don't show. Co-Authored-By: Claude Opus 4.6 --- .../EnvironmentVariablesTable.svelte | 1 + web-common/src/components/table/BasicTable.svelte | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte b/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte index 4b30c71e3368..b77bc0098d13 100644 --- a/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte +++ b/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte @@ -64,4 +64,5 @@ emptyIcon={KeyIcon} {emptyText} columnLayout="minmax(170px, 1.75fr) 2fr minmax(84px, 1fr) 56px" + externalSort /> diff --git a/web-common/src/components/table/BasicTable.svelte b/web-common/src/components/table/BasicTable.svelte index f747c0921c30..2fd1a4643ec4 100644 --- a/web-common/src/components/table/BasicTable.svelte +++ b/web-common/src/components/table/BasicTable.svelte @@ -20,6 +20,7 @@ export let emptyText = "No data available"; export let columnLayout = `repeat(${columns.length}, 1fr)`; export let rowPadding = "py-3"; + export let externalSort = false; let sorting: SortingState = []; @@ -53,6 +54,15 @@ } } + // When external sort is active, clear internal sorting (hides arrows) + $: if (externalSort) { + sorting = []; + options.update((old) => ({ + ...old, + state: { ...old.state, sorting: [] }, + })); + } + const setSorting: OnChangeFn = (updater) => { if (updater instanceof Function) { sorting = updater(sorting); From cbd75a08ccb21982f5e191cc5b923d11b4d7d8a6 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:31:25 -0400 Subject: [PATCH 16/32] fix: clear column sort arrows when toolbar sort changes Replace boolean `externalSort` with `externalSortKey` that reacts to value changes. Pass `sortDirection` through to BasicTable so clicking Newest/Oldest clears any active column header arrow. Co-Authored-By: Claude Opus 4.6 --- .../EnvironmentVariablesTable.svelte | 3 ++- .../-/settings/environment-variables/+page.svelte | 1 + web-common/src/components/table/BasicTable.svelte | 8 +++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte b/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte index b77bc0098d13..18869f4c3b48 100644 --- a/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte +++ b/web-admin/src/features/projects/environment-variables/EnvironmentVariablesTable.svelte @@ -13,6 +13,7 @@ export let data: V1ProjectVariable[]; export let emptyText: string = "No environment variables"; export let variableNames: VariableNames = []; + export let sortDirection: string | undefined = undefined; const columns: ColumnDef[] = [ { @@ -64,5 +65,5 @@ emptyIcon={KeyIcon} {emptyText} columnLayout="minmax(170px, 1.75fr) 2fr minmax(84px, 1fr) 56px" - externalSort + externalSortKey={sortDirection} /> diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte index 98645a285bc6..d98bc0ebec76 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte @@ -148,6 +148,7 @@ data={sortedVariables} emptyText={emptyTextWhenNoVariables} {variableNames} + {sortDirection} />
{/if} diff --git a/web-common/src/components/table/BasicTable.svelte b/web-common/src/components/table/BasicTable.svelte index 2fd1a4643ec4..93ef94c51919 100644 --- a/web-common/src/components/table/BasicTable.svelte +++ b/web-common/src/components/table/BasicTable.svelte @@ -20,7 +20,8 @@ export let emptyText = "No data available"; export let columnLayout = `repeat(${columns.length}, 1fr)`; export let rowPadding = "py-3"; - export let externalSort = false; + /** Pass a changing value (e.g. sortDirection) to clear column arrows when external sort changes */ + export let externalSortKey: string | undefined = undefined; let sorting: SortingState = []; @@ -54,8 +55,9 @@ } } - // When external sort is active, clear internal sorting (hides arrows) - $: if (externalSort) { + // When external sort key changes, clear internal sorting (hides column arrows) + $: if (externalSortKey !== undefined) { + void externalSortKey; sorting = []; options.update((old) => ({ ...old, From 798257d2c2ca9b50d0f18c9e43a5c6a47bd53fc2 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:58:43 -0400 Subject: [PATCH 17/32] + New key --- .../[project]/-/settings/environment-variables/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte index d98bc0ebec76..04b6914a706b 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte @@ -141,7 +141,7 @@ onSortToggle={handleSortToggle} > Date: Fri, 3 Apr 2026 12:07:41 -0400 Subject: [PATCH 18/32] prettier --- .../components/table-toolbar/TableToolbarFilterDropdown.svelte | 2 +- .../src/components/table-toolbar/TableToolbarSearch.svelte | 2 +- web-common/src/components/table-toolbar/TableToolbarSort.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte index d4f48e48ef00..93eacf3792fa 100644 --- a/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte @@ -15,7 +15,7 @@ {#if filterGroups.length > 0} diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte index b00662b53d36..34252f9919bc 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -63,7 +63,7 @@
{:else}
{:else} -
- - - - - - - - - - - - - - {#each sortedData as row (row.id)} - {@const filters = getFilters(row.metricsViewFilters)} - - - - - - - - - - {/each} - -
LabelDashboardFiltersExpires onCreated byLast accessed
- - - - {row.displayName || row.dashboardTitle || "Untitled"} - - - - - {row.dashboardTitle || row.resourceName || "—"} - - - {#if filters.length > 0} -
- {#each filters as filter (filter.name)} - - - {filter.isInclude - ? "" - : "Exclude "}{filter.name} - {#if filter.values.length === 1} - {filter.values[0]} - {:else if filter.values.length > 1} - {filter.values[0]} +{filter.values.length - - 1} - {/if} - - - {/each} -
- {:else} - - {/if} -
- {formatDate(row.expiresOn)} - - {row.attributes?.name || "—"} - - {formatDate(row.usedOn)} - - -
-
+ {/if}
- - diff --git a/web-admin/src/features/public-urls/cells/DateCell.svelte b/web-admin/src/features/public-urls/cells/DateCell.svelte new file mode 100644 index 000000000000..9b850c0fbf6a --- /dev/null +++ b/web-admin/src/features/public-urls/cells/DateCell.svelte @@ -0,0 +1,14 @@ + + +{formatDate(value)} diff --git a/web-admin/src/features/public-urls/cells/FiltersCell.svelte b/web-admin/src/features/public-urls/cells/FiltersCell.svelte new file mode 100644 index 000000000000..4ce72d66e942 --- /dev/null +++ b/web-admin/src/features/public-urls/cells/FiltersCell.svelte @@ -0,0 +1,65 @@ + + +{#if filters.length > 0} +
+ {#each filters as filter (filter.name)} + + + + {filter.isInclude ? "" : "Exclude "}{filter.name} + + {#if filter.values.length === 1} + {filter.values[0]} + {:else if filter.values.length > 1} + + {filter.values[0]} +{filter.values.length - 1} + + {/if} + + + {/each} +
+{:else} + +{/if} diff --git a/web-admin/src/features/public-urls/cells/LabelCell.svelte b/web-admin/src/features/public-urls/cells/LabelCell.svelte new file mode 100644 index 000000000000..17310e6392fa --- /dev/null +++ b/web-admin/src/features/public-urls/cells/LabelCell.svelte @@ -0,0 +1,19 @@ + + + + + + {displayName || dashboardTitle || "Untitled"} + + diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte index 04b6914a706b..ea6829e1669c 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte @@ -139,6 +139,7 @@ onClearAllFilters={handleClearAllFilters} {sortDirection} onSortToggle={handleSortToggle} + showSort={false} > - {/if} -
+ { + searchText = text; + }} + {filterGroups} + onFilterChange={(key, value) => { + if (key === "level") toggleLevel(value); + }} + onClearAllFilters={clearFilters} + showSort={false} + />
{#if hasConnectionError} diff --git a/web-admin/src/features/projects/status/resource-table/ProjectResources.svelte b/web-admin/src/features/projects/status/resource-table/ProjectResources.svelte index 2d8c4f7fcb6b..00f40667343a 100644 --- a/web-admin/src/features/projects/status/resource-table/ProjectResources.svelte +++ b/web-admin/src/features/projects/status/resource-table/ProjectResources.svelte @@ -10,10 +10,8 @@ import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { useQueryClient } from "@tanstack/svelte-query"; import Button from "@rilldata/web-common/components/button/Button.svelte"; - import Search from "@rilldata/web-common/components/search/Search.svelte"; - import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu"; - import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; + import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; + import type { FilterGroup } from "@rilldata/web-common/components/table-toolbar/types"; import { ResourceKind, prettyResourceKind, @@ -43,8 +41,6 @@ filterSync.init($page.url); let isConfirmDialogOpen = false; - let filterDropdownOpen = false; - let statusDropdownOpen = false; let searchText = parseStringParam($page.url.searchParams.get("q")); let selectedTypes = parseArrayParam($page.url.searchParams.get("kind")); let selectedStatuses = parseArrayParam($page.url.searchParams.get("status")); @@ -92,6 +88,31 @@ ResourceKind.Connector, ]; + $: filterGroups = [ + { + label: "Type", + key: "kind", + options: filterableTypes.map((t) => ({ + value: t, + label: prettyResourceKind(t), + })), + selected: selectedTypes, + defaultValue: [], + multiSelect: true, + }, + { + label: "Status", + key: "status", + options: statusFilters.map((s) => ({ + value: s.value, + label: s.label, + })), + selected: selectedStatuses, + defaultValue: [], + multiSelect: true, + }, + ] satisfies FilterGroup[]; + $: resources = useResources(runtimeClient); // Parse errors @@ -159,103 +180,19 @@

Resources

- -
-
- -
- - - - - {#if selectedTypes.length === 0} - All types - {:else if selectedTypes.length === 1} - {prettyResourceKind(selectedTypes[0])} - {:else} - {prettyResourceKind(selectedTypes[0])}, +{selectedTypes.length - 1} other{selectedTypes.length > - 2 - ? "s" - : ""} - {/if} - - {#if filterDropdownOpen} - - {:else} - - {/if} - - - {#each filterableTypes as type} - toggleType(type)} - > - {prettyResourceKind(type)} - - {/each} - - - - - - - {#if selectedStatuses.length === 0} - All statuses - {:else if selectedStatuses.length === 1} - {statusFilters.find((s) => s.value === selectedStatuses[0]) - ?.label ?? selectedStatuses[0]} - {:else} - {statusFilters.find((s) => s.value === selectedStatuses[0])?.label}, - +{selectedStatuses.length - 1} other{selectedStatuses.length > 2 - ? "s" - : ""} - {/if} - - {#if statusDropdownOpen} - - {:else} - - {/if} - - - {#each statusFilters as status} - toggleStatus(status.value)} - > - {status.label} - - {/each} - - - - {#if selectedTypes.length > 0 || searchText || selectedStatuses.length > 0} - - {/if} - + { + searchText = text; + }} + {filterGroups} + onFilterChange={(key, value) => { + if (key === "kind") toggleType(value); + if (key === "status") toggleStatus(value); + }} + onClearAllFilters={clearFilters} + showSort={false} + > -
+ {#if $resources.isLoading} diff --git a/web-admin/src/features/projects/status/tables/ProjectTables.svelte b/web-admin/src/features/projects/status/tables/ProjectTables.svelte index d0d85f602ee0..10ec297fd5ea 100644 --- a/web-admin/src/features/projects/status/tables/ProjectTables.svelte +++ b/web-admin/src/features/projects/status/tables/ProjectTables.svelte @@ -1,10 +1,8 @@ -{formatDate(value)} +{formatted} diff --git a/web-admin/src/features/public-urls/cells/FiltersCell.svelte b/web-admin/src/features/public-urls/cells/FiltersCell.svelte index 4ce72d66e942..34a79feac232 100644 --- a/web-admin/src/features/public-urls/cells/FiltersCell.svelte +++ b/web-admin/src/features/public-urls/cells/FiltersCell.svelte @@ -2,7 +2,11 @@ import { Chip } from "@rilldata/web-common/components/chip"; import type { V1Expression } from "@rilldata/web-admin/client"; - export let metricsViewFilters: { [key: string]: V1Expression } | undefined; + let { + metricsViewFilters, + }: { + metricsViewFilters: { [key: string]: V1Expression } | undefined; + } = $props(); interface FilterEntry { name: string; @@ -28,11 +32,13 @@ return [{ name, values, isInclude }]; } - $: filters = metricsViewFilters - ? Object.values(metricsViewFilters).flatMap((expr) => - extractDimensionFilters(expr), - ) - : []; + const filters = $derived( + metricsViewFilters + ? Object.values(metricsViewFilters).flatMap((expr) => + extractDimensionFilters(expr), + ) + : [], + ); {#if filters.length > 0} diff --git a/web-admin/src/features/public-urls/cells/LabelCell.svelte b/web-admin/src/features/public-urls/cells/LabelCell.svelte index 17310e6392fa..0891582cb89e 100644 --- a/web-admin/src/features/public-urls/cells/LabelCell.svelte +++ b/web-admin/src/features/public-urls/cells/LabelCell.svelte @@ -1,9 +1,15 @@
- {#if filterGroups.length > 0} - - {/if} + diff --git a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte index 0ee8af499497..89e5d7812cfa 100644 --- a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte @@ -12,15 +12,34 @@ onClearAllFilters?: () => void; } = $props(); + interface AppliedChip { + key: string; + resetValue: string; + label: string; + } + const appliedFilters = $derived( - filterGroups - .filter((g) => g.selected !== g.defaultValue) - .map((g) => ({ - key: g.key, - defaultValue: g.defaultValue, - label: - g.options.find((o) => o.value === g.selected)?.label ?? g.selected, - })), + filterGroups.flatMap((g): AppliedChip[] => { + if (g.multiSelect && Array.isArray(g.selected)) { + return g.selected.map((val) => ({ + key: g.key, + resetValue: val, + label: g.options.find((o) => o.value === val)?.label ?? val, + })); + } + if (typeof g.selected === "string" && g.selected !== g.defaultValue) { + return [ + { + key: g.key, + resetValue: g.defaultValue as string, + label: + g.options.find((o) => o.value === g.selected)?.label ?? + (g.selected as string), + }, + ]; + } + return []; + }), ); @@ -28,14 +47,14 @@
- {#each appliedFilters as filter (filter.key)} + {#each appliedFilters as filter (`${filter.key}:${filter.resetValue}`)} {filter.label}
+ + {/each} +
+ - - {/each} -
- + Clear all + +
+ {/if}
-{/if} +
+ + diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte index 1fea29cad885..0035a2140deb 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -13,7 +13,7 @@ disabled?: boolean; } = $props(); - let expanded = $state(false); + let expanded = $state(searchText.length > 0); let inputRef: HTMLInputElement | undefined = $state(); async function open() { diff --git a/web-common/src/components/table/BasicTable.svelte b/web-common/src/components/table/BasicTable.svelte index f3fc1502952f..ac5aeedfb5da 100644 --- a/web-common/src/components/table/BasicTable.svelte +++ b/web-common/src/components/table/BasicTable.svelte @@ -19,7 +19,6 @@ export let emptyIcon: any | null = null; export let emptyText = "No data available"; export let columnLayout = `repeat(${columns.length}, 1fr)`; - export let rowPadding = "py-3"; /** Pass a changing value (e.g. sortDirection) to clear column arrows when external sort changes */ export let externalSortKey: string | undefined = undefined; @@ -144,7 +143,7 @@ {/each} {#each rows as row (row.id)} -
+
{#each row.getVisibleCells() as cell (cell.id)}
- {#if emptyIcon} - - {/if} - {emptyText} -
+ {#if $$slots.empty} + + {:else} +
+ {#if emptyIcon} + + {/if} + {emptyText} +
+ {/if} {/each}
From ae6fbae9d983a9a86ec72fe7c2abf872663cc396 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:58:31 -0400 Subject: [PATCH 28/32] code qual Co-Authored-By: Claude Opus 4.6 --- .../src/components/table-toolbar/TableToolbarSearch.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte index 0035a2140deb..a156cdbef94a 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSearch.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -13,19 +13,20 @@ disabled?: boolean; } = $props(); - let expanded = $state(searchText.length > 0); + let manualExpanded = $state(false); + let expanded = $derived(manualExpanded || searchText.length > 0); let inputRef: HTMLInputElement | undefined = $state(); async function open() { if (disabled) return; - expanded = true; + manualExpanded = true; await tick(); inputRef?.focus(); } function close() { onSearchChange?.(""); - expanded = false; + manualExpanded = false; } function handleInput(e: Event) { From 60550bc83faa754c4dba442ba01fe83137f52d18 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:41:39 -0400 Subject: [PATCH 29/32] add search to branches; nit css --- .../features/branches/BranchesSection.svelte | 65 ++++++++++++++++++- .../table-toolbar/TableToolbar.svelte | 2 +- .../TableToolbarAppliedFilters.svelte | 2 +- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte index 192c13442983..753afc05596d 100644 --- a/web-admin/src/features/branches/BranchesSection.svelte +++ b/web-admin/src/features/branches/BranchesSection.svelte @@ -33,6 +33,8 @@ import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu"; import CopyableCodeBlock from "@rilldata/web-common/components/calls-to-action/CopyableCodeBlock.svelte"; import ThreeDot from "@rilldata/web-common/components/icons/ThreeDot.svelte"; + import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; + import type { FilterGroup } from "@rilldata/web-common/components/table-toolbar/types"; import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte"; import { EyeIcon, @@ -100,10 +102,55 @@ : null, ); + // Toolbar state + let searchText = $state(""); + let statusFilter = $state<"all" | "running" | "stopped" | "pending" | "errored">("all"); + + let filterGroups = $derived([ + { + label: "Status", + key: "status", + options: [ + { label: "Ready", value: "running" }, + { label: "Pending", value: "pending" }, + { label: "Error", value: "errored" }, + { label: "Stopped", value: "stopped" }, + ], + selected: statusFilter, + defaultValue: "all", + }, + ] satisfies FilterGroup[]); + + function statusMatches(d: V1Deployment): boolean { + if (statusFilter === "all") return true; + const s = d.status; + switch (statusFilter) { + case "running": + return s === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING; + case "pending": + return ( + s === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING || + s === V1DeploymentStatus.DEPLOYMENT_STATUS_UPDATING + ); + case "errored": + return s === V1DeploymentStatus.DEPLOYMENT_STATUS_ERRORED; + case "stopped": + return ( + s === V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPED || + s === V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPING + ); + default: + return true; + } + } + let visibleDeployments = $derived.by(() => { + const q = searchText.trim().toLowerCase(); const active = ($allDeployments.data?.deployments ?? []).filter( (d: V1Deployment) => - d.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_DELETED, + d.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_DELETED && + statusMatches(d) && + (q === "" || (d.branch ?? "").toLowerCase().includes(q)), ); return [...active].sort((a, b) => { const aIsProd = isProdDeployment(a); @@ -205,6 +252,22 @@

Branches

+ { + searchText = text; + }} + {filterGroups} + onFilterChange={(key, value) => { + if (key === "status") statusFilter = value as typeof statusFilter; + }} + onClearAllFilters={() => { + statusFilter = "all"; + searchText = ""; + }} + showSort={false} + /> + {#if $allDeployments.isLoading}
diff --git a/web-common/src/components/table-toolbar/TableToolbar.svelte b/web-common/src/components/table-toolbar/TableToolbar.svelte index f7ef73b069f7..d64934a75620 100644 --- a/web-common/src/components/table-toolbar/TableToolbar.svelte +++ b/web-common/src/components/table-toolbar/TableToolbar.svelte @@ -38,7 +38,7 @@ } = $props(); -
+
diff --git a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte index bf709c2eab25..93df4a2948c6 100644 --- a/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte @@ -48,7 +48,7 @@
{#if hasFilters} -
+
{#each appliedFilters as filter (`${filter.key}:${filter.resetValue}`)} From 54f7aa3775bceecdfd723d8cb252166989e507c3 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:46:04 -0400 Subject: [PATCH 30/32] prettier --- web-admin/src/features/branches/BranchesSection.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte index 753afc05596d..b587afe33490 100644 --- a/web-admin/src/features/branches/BranchesSection.svelte +++ b/web-admin/src/features/branches/BranchesSection.svelte @@ -104,7 +104,9 @@ // Toolbar state let searchText = $state(""); - let statusFilter = $state<"all" | "running" | "stopped" | "pending" | "errored">("all"); + let statusFilter = $state< + "all" | "running" | "stopped" | "pending" | "errored" + >("all"); let filterGroups = $derived([ { From 3b5dc37c9b7a55cc4f8856098252cfb361cb4e1d Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:56:33 -0400 Subject: [PATCH 31/32] tie filter to URL --- .../features/branches/BranchesSection.svelte | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte index b587afe33490..2c1d711ed5f2 100644 --- a/web-admin/src/features/branches/BranchesSection.svelte +++ b/web-admin/src/features/branches/BranchesSection.svelte @@ -36,6 +36,11 @@ import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; import type { FilterGroup } from "@rilldata/web-common/components/table-toolbar/types"; import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte"; + import { + createUrlFilterSync, + parseEnumParam, + parseStringParam, + } from "@rilldata/web-common/lib/url-filter-sync"; import { EyeIcon, GitBranchIcon, @@ -44,6 +49,7 @@ Trash2Icon, } from "lucide-svelte"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; + import { onMount } from "svelte"; let { organization, project }: { organization: string; project: string } = $props(); @@ -102,11 +108,51 @@ : null, ); - // Toolbar state - let searchText = $state(""); - let statusFilter = $state< - "all" | "running" | "stopped" | "pending" | "errored" - >("all"); + // Toolbar state — synced to URL params `q` and `status` + const statusValues = [ + "all", + "running", + "pending", + "errored", + "stopped", + ] as const; + + const filterSync = createUrlFilterSync([ + { key: "q", type: "string" }, + { key: "status", type: "enum", defaultValue: "all" }, + ]); + + let searchText = $state(parseStringParam(page.url.searchParams.get("q"))); + let statusFilter = $state<(typeof statusValues)[number]>( + parseEnumParam(page.url.searchParams.get("status"), statusValues, "all"), + ); + let mounted = $state(false); + + onMount(() => { + filterSync.init(page.url); + mounted = true; + }); + + // URL → local state on external navigation (back/forward) + $effect(() => { + if (!mounted) return; + const url = page.url; + if (filterSync.hasExternalNavigation(url)) { + filterSync.markSynced(url); + searchText = parseStringParam(url.searchParams.get("q")); + statusFilter = parseEnumParam( + url.searchParams.get("status"), + statusValues, + "all", + ); + } + }); + + // Local state → URL + $effect(() => { + if (!mounted) return; + filterSync.syncToUrl({ q: searchText, status: statusFilter }); + }); let filterGroups = $derived([ { From d8d57ed84df5081e784c66732af72711ad288952 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:15:51 -0400 Subject: [PATCH 32/32] tie to URL; multiselect filters --- .../features/branches/BranchesSection.svelte | 75 +++++++------- .../status/tables/ProjectTables.svelte | 28 +++--- .../projects/status/tables/utils.spec.ts | 18 ++-- .../environment-variables/+page.svelte | 98 ++++++++++++------- .../features/projects/status/tables/utils.ts | 9 +- .../features/tables/LocalProjectTables.svelte | 9 +- 6 files changed, 137 insertions(+), 100 deletions(-) diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte index 2c1d711ed5f2..ddf93fb72007 100644 --- a/web-admin/src/features/branches/BranchesSection.svelte +++ b/web-admin/src/features/branches/BranchesSection.svelte @@ -38,7 +38,7 @@ import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte"; import { createUrlFilterSync, - parseEnumParam, + parseArrayParam, parseStringParam, } from "@rilldata/web-common/lib/url-filter-sync"; import { @@ -108,23 +108,15 @@ : null, ); - // Toolbar state — synced to URL params `q` and `status` - const statusValues = [ - "all", - "running", - "pending", - "errored", - "stopped", - ] as const; - + // Toolbar state — synced to URL params `q` and `status` (multi-select array) const filterSync = createUrlFilterSync([ { key: "q", type: "string" }, - { key: "status", type: "enum", defaultValue: "all" }, + { key: "status", type: "array" }, ]); let searchText = $state(parseStringParam(page.url.searchParams.get("q"))); - let statusFilter = $state<(typeof statusValues)[number]>( - parseEnumParam(page.url.searchParams.get("status"), statusValues, "all"), + let statusFilter = $state( + parseArrayParam(page.url.searchParams.get("status")), ); let mounted = $state(false); @@ -140,11 +132,7 @@ if (filterSync.hasExternalNavigation(url)) { filterSync.markSynced(url); searchText = parseStringParam(url.searchParams.get("q")); - statusFilter = parseEnumParam( - url.searchParams.get("status"), - statusValues, - "all", - ); + statusFilter = parseArrayParam(url.searchParams.get("status")); } }); @@ -165,31 +153,34 @@ { label: "Stopped", value: "stopped" }, ], selected: statusFilter, - defaultValue: "all", + defaultValue: [], + multiSelect: true, }, ] satisfies FilterGroup[]); function statusMatches(d: V1Deployment): boolean { - if (statusFilter === "all") return true; + if (statusFilter.length === 0) return true; const s = d.status; - switch (statusFilter) { - case "running": - return s === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING; - case "pending": - return ( - s === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING || - s === V1DeploymentStatus.DEPLOYMENT_STATUS_UPDATING - ); - case "errored": - return s === V1DeploymentStatus.DEPLOYMENT_STATUS_ERRORED; - case "stopped": - return ( - s === V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPED || - s === V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPING - ); - default: - return true; - } + return statusFilter.some((sel) => { + switch (sel) { + case "running": + return s === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING; + case "pending": + return ( + s === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING || + s === V1DeploymentStatus.DEPLOYMENT_STATUS_UPDATING + ); + case "errored": + return s === V1DeploymentStatus.DEPLOYMENT_STATUS_ERRORED; + case "stopped": + return ( + s === V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPED || + s === V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPING + ); + default: + return false; + } + }); } let visibleDeployments = $derived.by(() => { @@ -307,10 +298,14 @@ }} {filterGroups} onFilterChange={(key, value) => { - if (key === "status") statusFilter = value as typeof statusFilter; + if (key === "status") { + statusFilter = statusFilter.includes(value) + ? statusFilter.filter((v) => v !== value) + : [...statusFilter, value]; + } }} onClearAllFilters={() => { - statusFilter = "all"; + statusFilter = []; searchText = ""; }} showSort={false} diff --git a/web-admin/src/features/projects/status/tables/ProjectTables.svelte b/web-admin/src/features/projects/status/tables/ProjectTables.svelte index 59d801a72e3f..fdee939217d7 100644 --- a/web-admin/src/features/projects/status/tables/ProjectTables.svelte +++ b/web-admin/src/features/projects/status/tables/ProjectTables.svelte @@ -29,7 +29,7 @@ import RefreshResourceConfirmDialog from "@rilldata/web-common/features/projects/status/RefreshResourceConfirmDialog.svelte"; import { createUrlFilterSync, - parseEnumParam, + parseArrayParam, parseStringParam, } from "@rilldata/web-common/lib/url-filter-sync"; import { onMount } from "svelte"; @@ -43,14 +43,13 @@ $: instance = $instanceQuery.data?.instance; $: connectorName = instance?.olapConnector ?? ""; - // Filters — initialized from URL params + // Filters — initialized from URL params (type is multi-select array) const filterSync = createUrlFilterSync([ { key: "q", type: "string" }, - { key: "type", type: "enum", defaultValue: "all" }, + { key: "type", type: "array" }, ]); filterSync.init($page.url); - const typeValues = ["all", "table", "view"] as const; let searchText = parseStringParam($page.url.searchParams.get("q")); // Debounce search for server-side filtering @@ -84,10 +83,8 @@ // createQuery (unlike createInfiniteQuery) handles re-creation in $: blocks safely $: modelResourcesQuery = useModelResources(runtimeClient); $: modelResources = $modelResourcesQuery.data ?? new Map(); - let typeFilter: (typeof typeValues)[number] = parseEnumParam( + let typeFilter: string[] = parseArrayParam( $page.url.searchParams.get("type"), - typeValues, - "all", ); let mounted = false; @@ -95,11 +92,7 @@ $: if (mounted && filterSync.hasExternalNavigation($page.url)) { filterSync.markSynced($page.url); searchText = parseStringParam($page.url.searchParams.get("q")); - typeFilter = parseEnumParam( - $page.url.searchParams.get("type"), - typeValues, - "all", - ); + typeFilter = parseArrayParam($page.url.searchParams.get("type")); } // Sync filter state → URL @@ -120,7 +113,8 @@ { label: "View", value: "view" }, ], selected: typeFilter, - defaultValue: "all", + defaultValue: [], + multiSelect: true, }, ] satisfies FilterGroup[]; @@ -227,10 +221,14 @@ }} {filterGroups} onFilterChange={(key, value) => { - if (key === "type") typeFilter = value as typeof typeFilter; + if (key === "type") { + typeFilter = typeFilter.includes(value) + ? typeFilter.filter((v) => v !== value) + : [...typeFilter, value]; + } }} onClearAllFilters={() => { - typeFilter = "all"; + typeFilter = []; searchText = ""; }} showSort={false} diff --git a/web-admin/src/features/projects/status/tables/utils.spec.ts b/web-admin/src/features/projects/status/tables/utils.spec.ts index c07e2a764585..3ded68bde0fb 100644 --- a/web-admin/src/features/projects/status/tables/utils.spec.ts +++ b/web-admin/src/features/projects/status/tables/utils.spec.ts @@ -331,12 +331,12 @@ describe("tables utils", () => { ]); it("returns all tables when type is 'all'", () => { - const result = applyTableFilters(tables, "all", viewMap); + const result = applyTableFilters(tables, [], viewMap); expect(result).toEqual(tables); }); it("filters by type: table", () => { - const result = applyTableFilters(tables, "table", viewMap); + const result = applyTableFilters(tables, ["table"], viewMap); expect(result).toEqual([ { name: "users", physicalSizeBytes: "1024" }, { name: "orders", physicalSizeBytes: "2048" }, @@ -344,7 +344,7 @@ describe("tables utils", () => { }); it("filters by type: view", () => { - const result = applyTableFilters(tables, "view", viewMap); + const result = applyTableFilters(tables, ["view"], viewMap); expect(result).toEqual([ { name: "analytics_view", physicalSizeBytes: "0" }, ]); @@ -355,12 +355,12 @@ describe("tables utils", () => { { name: "view_a", physicalSizeBytes: "0" }, ]; const allViewMap = new Map([["view_a", true]]); - const result = applyTableFilters(allViews, "table", allViewMap); + const result = applyTableFilters(allViews, ["table"], allViewMap); expect(result).toEqual([]); }); it("handles empty viewMap gracefully (falls back to size heuristic)", () => { - const result = applyTableFilters(tables, "table", new Map()); + const result = applyTableFilters(tables, ["table"], new Map()); // With empty viewMap, isLikelyView falls back to physicalSizeBytes heuristic // analytics_view has physicalSizeBytes "0", so isLikelyView returns true → filtered out expect(result).toEqual([ @@ -379,12 +379,16 @@ describe("tables utils", () => { const tableResult = applyTableFilters( tablesWithUnknown, - "table", + ["table"], emptyMap, ); expect(tableResult).toEqual(tablesWithUnknown); - const viewResult = applyTableFilters(tablesWithUnknown, "view", emptyMap); + const viewResult = applyTableFilters( + tablesWithUnknown, + ["view"], + emptyMap, + ); expect(viewResult).toEqual([ { name: "loading_table", physicalSizeBytes: undefined }, ]); diff --git a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte index a9ee5dcdadd3..74282babee69 100644 --- a/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/settings/environment-variables/+page.svelte @@ -12,11 +12,46 @@ import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; import RadixLarge from "@rilldata/web-common/components/typography/RadixLarge.svelte"; import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte"; + import { + createUrlFilterSync, + parseArrayParam, + parseStringParam, + } from "@rilldata/web-common/lib/url-filter-sync"; import { Plus } from "lucide-svelte"; + import { onMount } from "svelte"; let open = false; - let searchText = ""; - let filterByEnvironment: EnvironmentTypes = EnvironmentType.UNDEFINED; + + // Filters — synced to URL params `q` and `env` (multi-select array) + const filterSync = createUrlFilterSync([ + { key: "q", type: "string" }, + { key: "env", type: "array" }, + ]); + filterSync.init($page.url); + + let searchText = parseStringParam($page.url.searchParams.get("q")); + let envFilter: EnvironmentTypes[] = parseArrayParam( + $page.url.searchParams.get("env"), + ) as EnvironmentTypes[]; + let mounted = false; + + // URL → local state on external navigation (back/forward) + $: if (mounted && filterSync.hasExternalNavigation($page.url)) { + filterSync.markSynced($page.url); + searchText = parseStringParam($page.url.searchParams.get("q")); + envFilter = parseArrayParam( + $page.url.searchParams.get("env"), + ) as EnvironmentTypes[]; + } + + // Local state → URL + $: if (mounted) { + filterSync.syncToUrl({ q: searchText, env: envFilter }); + } + + onMount(() => { + mounted = true; + }); $: organization = $page.params.organization; $: project = $page.params.project; @@ -43,22 +78,22 @@ ); $: filteredVariables = searchedVariables.filter((variable) => { - if (filterByEnvironment === EnvironmentType.UNDEFINED) { - return true; - } - if (filterByEnvironment === EnvironmentType.DEVELOPMENT) { - return ( - variable.environment === EnvironmentType.DEVELOPMENT || - variable.environment === EnvironmentType.UNDEFINED - ); - } - if (filterByEnvironment === EnvironmentType.PRODUCTION) { - return ( - variable.environment === EnvironmentType.PRODUCTION || - variable.environment === EnvironmentType.UNDEFINED - ); - } - return false; + if (envFilter.length === 0) return true; + return envFilter.some((sel) => { + if (sel === EnvironmentType.DEVELOPMENT) { + return ( + variable.environment === EnvironmentType.DEVELOPMENT || + variable.environment === EnvironmentType.UNDEFINED + ); + } + if (sel === EnvironmentType.PRODUCTION) { + return ( + variable.environment === EnvironmentType.PRODUCTION || + variable.environment === EnvironmentType.UNDEFINED + ); + } + return false; + }); }); $: sortedVariables = [...filteredVariables].sort((a, b) => { @@ -66,36 +101,33 @@ }); function handleFilterChange(_key: string, value: string) { - filterByEnvironment = value as EnvironmentTypes; + const v = value as EnvironmentTypes; + envFilter = envFilter.includes(v) + ? envFilter.filter((x) => x !== v) + : [...envFilter, v]; } function handleClearAllFilters() { - filterByEnvironment = EnvironmentType.UNDEFINED; + envFilter = []; + searchText = ""; } - $: environmentLabel = - filterByEnvironment === EnvironmentType.UNDEFINED - ? "All environments" - : filterByEnvironment === EnvironmentType.PRODUCTION - ? "Production" - : "Development"; - $: emptyTextWhenNoVariables = - filterByEnvironment === EnvironmentType.UNDEFINED + envFilter.length === 0 ? "No environment variables" - : `No environment variables for ${environmentLabel}`; + : `No environment variables match the selected filters`; $: filterGroups = [ { - label: "Filter by environment", + label: "Environment", key: "environment", options: [ - { value: EnvironmentType.UNDEFINED, label: "All environments" }, { value: EnvironmentType.PRODUCTION, label: "Production" }, { value: EnvironmentType.DEVELOPMENT, label: "Development" }, ], - selected: filterByEnvironment, - defaultValue: EnvironmentType.UNDEFINED, + selected: envFilter, + defaultValue: [], + multiSelect: true, }, ]; diff --git a/web-common/src/features/projects/status/tables/utils.ts b/web-common/src/features/projects/status/tables/utils.ts index 9d584e40d446..291a67d99ffa 100644 --- a/web-common/src/features/projects/status/tables/utils.ts +++ b/web-common/src/features/projects/status/tables/utils.ts @@ -175,14 +175,17 @@ export function splitTablesByModel( */ export function applyTableFilters( tables: V1OlapTableInfo[], - type: "all" | "table" | "view", + types: string[], viewMap: Map, ): V1OlapTableInfo[] { - if (type === "all") return tables; + if (types.length === 0) return tables; + const wantTable = types.includes("table"); + const wantView = types.includes("view"); + if (wantTable && wantView) return tables; return tables.filter((t) => { const name = t.name ?? ""; const likelyView = isLikelyView(viewMap.get(name), t.physicalSizeBytes); if (likelyView === undefined) return true; - return (type === "view" && likelyView) || (type === "table" && !likelyView); + return (wantView && likelyView) || (wantTable && !likelyView); }); } diff --git a/web-local/src/features/tables/LocalProjectTables.svelte b/web-local/src/features/tables/LocalProjectTables.svelte index 4027fd6081fd..53dd35f48656 100644 --- a/web-local/src/features/tables/LocalProjectTables.svelte +++ b/web-local/src/features/tables/LocalProjectTables.svelte @@ -118,10 +118,15 @@ // Split once on unfiltered tables, then apply type filter per section $: ({ modelTables: allModelTables, externalTables: allExternalTables } = splitTablesByModel(filteredTables, modelResources)); - $: modelTables = applyTableFilters(allModelTables, typeFilter, isViewMap); + $: typeFilterArray = typeFilter === "all" ? [] : [typeFilter]; + $: modelTables = applyTableFilters( + allModelTables, + typeFilterArray, + isViewMap, + ); $: externalTables = applyTableFilters( allExternalTables, - typeFilter, + typeFilterArray, isViewMap, );