diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte index 07fb0c92cc31..fe52c5cf942e 100644 --- a/web-admin/src/features/branches/BranchesSection.svelte +++ b/web-admin/src/features/branches/BranchesSection.svelte @@ -38,11 +38,6 @@ 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, - parseArrayParam, - parseStringParam, - } from "@rilldata/web-common/lib/url-filter-sync"; import { GitBranchIcon, PlayIcon, @@ -50,8 +45,9 @@ Trash2Icon, } from "lucide-svelte"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; - import { onMount } from "svelte"; import { featureFlags } from "@rilldata/web-common/features/feature-flags"; + import { UrlParamsState } from "@rilldata/web-common/lib/store-utils/url-params-state.svelte.ts"; + import { DebouncedRuneStore } from "@rilldata/web-common/lib/store-utils/types.svelte.ts"; import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; let { organization, project }: { organization: string; project: string } = @@ -121,41 +117,13 @@ : null, ); - // Toolbar state — synced to URL params `q` and `status` (multi-select array) - const filterSync = createUrlFilterSync([ - { key: "q", type: "string" }, - { key: "status", type: "array" }, - ]); - - let searchText = $state(parseStringParam(page.url.searchParams.get("q"))); - let statusFilter = $state( - parseArrayParam(page.url.searchParams.get("status")), + const searchTextStore = new DebouncedRuneStore( + UrlParamsState.createStringParam("q"), + 500, ); - 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 = parseArrayParam(url.searchParams.get("status")); - } - }); + const statusFilterStore = UrlParamsState.createStringArrayParam("status"); - // Local state → URL - $effect(() => { - if (!mounted) return; - filterSync.syncToUrl({ q: searchText, status: statusFilter }); - }); - - let filterGroups = $derived([ + let filterGroups = $derived([ { label: m.common_status(), key: "status", @@ -165,16 +133,21 @@ { label: m.branch_status_error(), value: "errored" }, { label: m.branch_status_stopped(), value: "stopped" }, ], - selected: statusFilter, + selectedStore: statusFilterStore, defaultValue: [], multiSelect: true, }, - ] satisfies FilterGroup[]); + ]); + + function onClearAllFilters() { + searchTextStore.setter(""); + statusFilterStore.setter([]); + } function statusMatches(d: V1Deployment): boolean { - if (statusFilter.length === 0) return true; + if (statusFilterStore.value.length === 0) return true; const s = d.status; - return statusFilter.some((sel) => { + return statusFilterStore.value.some((sel) => { switch (sel) { case "running": return s === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING; @@ -197,7 +170,7 @@ } let visibleDeployments = $derived.by(() => { - const q = searchText.trim().toLowerCase(); + const q = searchTextStore.value.trim().toLowerCase(); const active = ($allDeployments.data?.deployments ?? []).filter( (d: V1Deployment) => d.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_DELETED && @@ -246,10 +219,6 @@ editable: boolean; } | null>(null); - function onFilterChange(key: string, selected: string[]) { - if (key === "status") statusFilter = selected; - } - async function mutateDeployment( deploymentId: string, branch: string | undefined, @@ -316,16 +285,7 @@

{m.branch_branches()}

- { - statusFilter = []; - searchText = ""; - }} - showSort={false} - /> + {#if $allDeployments.isLoading}
diff --git a/web-admin/src/features/dashboards/listing/DashboardsTable.svelte b/web-admin/src/features/dashboards/listing/DashboardsTable.svelte index 1a57c9d860d8..2b486833a596 100644 --- a/web-admin/src/features/dashboards/listing/DashboardsTable.svelte +++ b/web-admin/src/features/dashboards/listing/DashboardsTable.svelte @@ -16,7 +16,7 @@ import ResizableSidebar from "@rilldata/web-common/layout/ResizableSidebar.svelte"; import DashboardsTagSidebar from "@rilldata/web-admin/features/dashboards/listing/DashboardsTagSidebar.svelte"; import { filterResources } from "@rilldata/web-common/features/resources/resource-filter-utils.ts"; - import { Throttler } from "@rilldata/web-common/lib/throttler.ts"; + import { DebouncedRuneStore } from "@rilldata/web-common/lib/store-utils/types.svelte.ts"; import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; import { escapeHtml } from "@rilldata/web-common/lib/i18n"; @@ -32,11 +32,10 @@ const selectedTagsState = UrlParamsState.createStringArrayParam("tags"); - const searchTextState = UrlParamsState.createStringParam("search"); - const throttler = new Throttler(500, 500); - const throttledSearchSetter = (newValue: string) => { - throttler.throttle(() => searchTextState.setter(newValue)); - }; + const searchTextStore = new DebouncedRuneStore( + UrlParamsState.createStringParam("q"), + 500, + ); const runtimeClient = useRuntimeClient(); let { organization, project } = $derived(page.params); @@ -61,7 +60,7 @@ filterResources( allDashboards, [], - searchTextState.value, + searchTextStore.value, [], selectedTagsState.value, ), @@ -161,9 +160,10 @@
{/if} @@ -180,7 +180,7 @@ > {/if} 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 6bbb5a4b7f06..9ed67e25d12c 100644 --- a/web-admin/src/features/projects/status/resource-table/ProjectResources.svelte +++ b/web-admin/src/features/projects/status/resource-table/ProjectResources.svelte @@ -1,5 +1,4 @@
- + - import { page } from "$app/stores"; + import { page } from "$app/state"; import { createAdminServiceGetProjectVariables } from "@rilldata/web-admin/client"; import AddDialog from "@rilldata/web-admin/features/projects/environment-variables/AddDialog.svelte"; import EnvironmentVariablesTable from "@rilldata/web-admin/features/projects/environment-variables/EnvironmentVariablesTable.svelte"; - import { - EnvironmentType, - type EnvironmentTypes, - } from "@rilldata/web-admin/features/projects/environment-variables/types"; + import { EnvironmentType } from "@rilldata/web-admin/features/projects/environment-variables/types"; import { getEnvironmentType } from "@rilldata/web-admin/features/projects/environment-variables/utils"; import Button from "@rilldata/web-common/components/button/Button.svelte"; - import { TableToolbar } from "@rilldata/web-common/components/table-toolbar"; + import { + type FilterGroup, + 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 { @@ -19,101 +19,81 @@ } from "@rilldata/web-common/lib/url-filter-sync"; import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; import { Plus } from "lucide-svelte"; - import { onMount } from "svelte"; - - let open = false; - - // 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); + import { UrlParamsState } from "@rilldata/web-common/lib/store-utils/url-params-state.svelte.ts"; + import { DebouncedRuneStore } from "@rilldata/web-common/lib/store-utils/types.svelte.ts"; - let searchText = parseStringParam($page.url.searchParams.get("q")); - let envFilter: EnvironmentTypes[] = parseArrayParam( - $page.url.searchParams.get("env"), - ) as EnvironmentTypes[]; - let mounted = false; + let open = $state(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; - }); + const searchTextStore = new DebouncedRuneStore( + UrlParamsState.createStringParam("q"), + 500, + ); + const envFilterStore = UrlParamsState.createStringArrayParam("env"); - $: organization = $page.params.organization; - $: project = $page.params.project; + let { organization, project } = $derived(page.params); - $: getProjectVariables = createAdminServiceGetProjectVariables( - organization, - project, - { + let getProjectVariables = $derived( + createAdminServiceGetProjectVariables(organization, project, { forAllEnvironments: true, - }, + }), ); - $: projectVariables = $getProjectVariables.data?.variables || []; - - $: variableNames = projectVariables.map((variable) => { - return { - environment: getEnvironmentType(variable.environment), - name: variable.name, - }; - }); + let projectVariables = $derived($getProjectVariables.data?.variables || []); - $: searchedVariables = projectVariables.filter((variable) => - variable.name.toLowerCase().includes(searchText.toLowerCase()), + let variableNames = $derived( + projectVariables.map((variable) => { + return { + environment: getEnvironmentType(variable.environment), + name: variable.name, + }; + }), ); - $: filteredVariables = searchedVariables.filter((variable) => { - 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; - }); - }); + let searchedVariables = $derived( + projectVariables.filter((variable) => + variable.name.toLowerCase().includes(searchTextStore.value.toLowerCase()), + ), + ); - $: sortedVariables = [...filteredVariables].sort((a, b) => { - return new Date(b.updatedOn).getTime() - new Date(a.updatedOn).getTime(); - }); + let filteredVariables = $derived( + searchedVariables.filter((variable) => { + if (envFilterStore.value.length === 0) return true; + return envFilterStore.value.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; + }); + }), + ); - function handleFilterChange(_key: string, selected: string | string[]) { - envFilter = selected as EnvironmentTypes[]; - } + let sortedVariables = $derived( + [...filteredVariables].sort((a, b) => { + return new Date(b.updatedOn).getTime() - new Date(a.updatedOn).getTime(); + }), + ); function handleClearAllFilters() { - envFilter = []; - searchText = ""; + searchTextStore.setter(""); + envFilterStore.setter([]); } - $: emptyTextWhenNoVariables = - envFilter.length === 0 ? m.env_no_variables() : m.env_no_match_filters(); + let emptyTextWhenNoVariables = $derived( + envFilterStore.value.length === 0 + ? m.env_no_variables() + : m.env_no_match_filters(), + ); - $: filterGroups = [ + let filterGroups = $derived([ { label: m.env_environment_label(), key: "environment", @@ -124,11 +104,11 @@ label: m.env_development_label(), }, ], - selected: envFilter, + selectedStore: envFilterStore, defaultValue: [], multiSelect: true, }, - ]; + ]);
@@ -156,12 +136,9 @@

-{:else} - {/if} diff --git a/web-common/src/components/table-toolbar/TableToolbarSort.svelte b/web-common/src/components/table-toolbar/TableToolbarSort.svelte index d02d1401663b..69867598f84d 100644 --- a/web-common/src/components/table-toolbar/TableToolbarSort.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarSort.svelte @@ -1,26 +1,39 @@ - + + + + Sort by {sortLabel} + + + {#each sortOptions as option (option.value)} + sortStore.setter(option.value)} + > + {option.label} + + {/each} + + diff --git a/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte index fab12f3696f0..a0877b594562 100644 --- a/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte +++ b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte @@ -3,12 +3,14 @@ import List from "@rilldata/web-common/components/icons/List.svelte"; import { LayoutGrid } from "lucide-svelte"; import type { ViewMode } from "./types"; + import type { RuneStore } from "@rilldata/web-common/lib/store-utils/types.svelte.ts"; let { - viewMode = $bindable("list"), + viewModeStore, }: { - viewMode: ViewMode; + viewModeStore: RuneStore; } = $props(); + let viewMode = $derived(viewModeStore.value);
@@ -17,7 +19,7 @@ class="flex items-center justify-center w-8 h-8 {viewMode === 'grid' ? 'bg-surface-hover' : 'bg-input hover:bg-surface-hover'}" - onclick={() => (viewMode = "grid")} + onclick={() => viewModeStore.setter("grid")} aria-label="Grid view" aria-pressed={viewMode === "grid"} > @@ -29,7 +31,7 @@ 'list' ? 'bg-surface-hover' : 'bg-input hover:bg-surface-hover'}" - onclick={() => (viewMode = "list")} + onclick={() => viewModeStore.setter("list")} aria-label="List view" aria-pressed={viewMode === "list"} > diff --git a/web-common/src/components/table-toolbar/types.ts b/web-common/src/components/table-toolbar/types.ts index 5178712ff915..69f74c150070 100644 --- a/web-common/src/components/table-toolbar/types.ts +++ b/web-common/src/components/table-toolbar/types.ts @@ -1,4 +1,8 @@ -export type SortDirection = "newest" | "oldest"; +import type { + ArrayRuneStore, + RuneStore, +} from "@rilldata/web-common/lib/store-utils/types.svelte.ts"; +import type { ColumnSort } from "tanstack-table-8-svelte-5"; export type ViewMode = "list" | "grid"; @@ -7,17 +11,25 @@ export interface FilterOption { label: string; } -export interface FilterGroup { - /** Dropdown section header */ +export type FilterGroup = { label: string; - /** Unique key for this filter group */ key: string; - /** Available options */ options: FilterOption[]; - /** Currently selected value(s). String for single-select, string[] for multi-select. */ - selected: string | string[]; - /** Default value; when selected matches defaultValue, no chip is shown */ - defaultValue: string | string[]; - /** Allow multiple selections. Default: false (single-select radio behavior). */ - multiSelect?: boolean; -} +} & ( + | { + selectedStore: RuneStore; + defaultValue: string; + multiSelect?: false; + } + | { + selectedStore: ArrayRuneStore; + defaultValue: string[]; + multiSelect: true; + } +); + +export type SortOption = { + value: string; + label: string; + sort: ColumnSort; +}; diff --git a/web-common/src/lib/store-utils/types.svelte.ts b/web-common/src/lib/store-utils/types.svelte.ts index ec9f12382bdf..145672f382ca 100644 --- a/web-common/src/lib/store-utils/types.svelte.ts +++ b/web-common/src/lib/store-utils/types.svelte.ts @@ -1,3 +1,5 @@ +import { debounce } from "@rilldata/web-common/lib/create-debouncer.ts"; + export interface RuneStore { value: Val | DefaultVal; getter: () => Val | DefaultVal; @@ -44,4 +46,23 @@ export class ArrayRuneStore implements RuneStore { : [...this.value, value]; this.setter(newTags); }; + + public delete = (value: Val) => { + const newTags = this.value.filter((v) => v !== value); + this.setter(newTags); + }; +} + +export class DebouncedRuneStore + implements RuneStore +{ + public value: Val; + public getter: () => Val; + public setter: (newValue: Val) => void; + + public constructor(store: RuneStore, timeout: number) { + this.value = $derived(store.value); + this.getter = store.getter; + this.setter = debounce(store.setter, timeout); + } } diff --git a/web-common/src/lib/store-utils/url-params-state.svelte.ts b/web-common/src/lib/store-utils/url-params-state.svelte.ts index 7106245d9664..efba886a0ca8 100644 --- a/web-common/src/lib/store-utils/url-params-state.svelte.ts +++ b/web-common/src/lib/store-utils/url-params-state.svelte.ts @@ -5,6 +5,9 @@ import { ArrayRuneStore, type RuneStore, } from "@rilldata/web-common/lib/store-utils/types.svelte.ts"; +import { tick } from "svelte"; + +let newParams: [key: string, value: string | null][] = []; export class UrlParamsState implements RuneStore @@ -56,15 +59,24 @@ export class UrlParamsState public setter = (newValue: Val) => { const newParamValue = this.serializer(newValue); - const newUrl = new SvelteURL(window.location.href); - if (newParamValue === null) { - newUrl.searchParams.delete(this.param); + const hasParam = newParams.length > 0; + newParams.push([this.param, newParamValue]); + if (!hasParam) void tick().then(flushParams); + }; +} + +function flushParams() { + if (newParams.length === 0) return; + + const newUrl = new SvelteURL(window.location.href); + newParams.forEach(([key, value]) => { + if (value === null) { + newUrl.searchParams.delete(key); } else { - newUrl.searchParams.set(this.param, newParamValue); + newUrl.searchParams.set(key, value); } + }); - this.paramValue = newParamValue; - this.value = newValue; - void goto(newUrl, { noScroll: true, keepFocus: true }); - }; + newParams = []; + void goto(newUrl, { noScroll: true, keepFocus: true }); }