diff --git a/web-admin/src/features/branches/BranchesSection.svelte b/web-admin/src/features/branches/BranchesSection.svelte index 192c1344298..ddf93fb7200 100644 --- a/web-admin/src/features/branches/BranchesSection.svelte +++ b/web-admin/src/features/branches/BranchesSection.svelte @@ -33,7 +33,14 @@ 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 { + createUrlFilterSync, + parseArrayParam, + parseStringParam, + } from "@rilldata/web-common/lib/url-filter-sync"; import { EyeIcon, GitBranchIcon, @@ -42,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(); @@ -100,10 +108,88 @@ : 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")), + ); + 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")); + } + }); + + // Local state → URL + $effect(() => { + if (!mounted) return; + filterSync.syncToUrl({ q: searchText, status: statusFilter }); + }); + + 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: [], + multiSelect: true, + }, + ] satisfies FilterGroup[]); + + function statusMatches(d: V1Deployment): boolean { + if (statusFilter.length === 0) return true; + const s = d.status; + 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(() => { + 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 +291,26 @@

Branches

+ { + searchText = text; + }} + {filterGroups} + onFilterChange={(key, value) => { + if (key === "status") { + statusFilter = statusFilter.includes(value) + ? statusFilter.filter((v) => v !== value) + : [...statusFilter, value]; + } + }} + onClearAllFilters={() => { + statusFilter = []; + searchText = ""; + }} + showSort={false} + /> + {#if $allDeployments.isLoading}
diff --git a/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte b/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte index 6d79a1b2d10..864800e7a7f 100644 --- a/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte +++ b/web-admin/src/features/projects/status/logs/ProjectLogsPage.svelte @@ -7,10 +7,8 @@ } from "@rilldata/web-common/runtime-client/sse-connection-manager"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { V1LogLevel, type V1Log } from "@rilldata/web-common/runtime-client"; - 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 { createUrlFilterSync, parseArrayParam, @@ -34,7 +32,6 @@ let logs: LogEntry[] = []; let logsContainer: HTMLDivElement; let connectionError: string | null = null; - let filterDropdownOpen = false; let searchText = parseStringParam($page.url.searchParams.get("q")); let selectedLevels = parseArrayParam($page.url.searchParams.get("level")); let mounted = false; @@ -81,19 +78,19 @@ return matchesLevel && matchesSearch; }); - $: selectedLevelLabel = (() => { - if (selectedLevels.length === 0) return "All levels"; - if (selectedLevels.length === 1) { - return ( - filterableLevels.find((l) => l.value === selectedLevels[0])?.label ?? - "1 level" - ); - } - const first = filterableLevels.find( - (l) => l.value === selectedLevels[0], - )?.label; - return `${first}, +${selectedLevels.length - 1} other${selectedLevels.length > 2 ? "s" : ""}`; - })(); + $: filterGroups = [ + { + label: "Level", + key: "level", + options: filterableLevels.map((l) => ({ + value: l.value, + label: l.label, + })), + selected: selectedLevels, + defaultValue: [], + multiSelect: true, + }, + ] satisfies FilterGroup[]; let unsubs: (() => void)[] = []; @@ -244,55 +241,18 @@
-
-
- -
- - - - - {selectedLevelLabel} - - {#if filterDropdownOpen} - - {:else} - - {/if} - - - {#each filterableLevels as level} - toggleLevel(level.value)} - > - {level.label} - - {/each} - - - - {#if selectedLevels.length > 0 || searchText} - - {/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 2d8c4f7fcb6..00f40667343 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 d0d85f602ee..fdee939217d 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 @@
- (searchText = text)} + searchDisabled={data.length === 0} + showSort={false} /> - {#if filteredData.length === 0 && data.length === 0} -
-
+ +
+ {#if data.length === 0} -
-
- {:else if filteredData.length === 0} -
-
- No public URLs match your search -
+ {:else} + + No public URLs match your search + + {/if}
- {:else} -
- - - - - - - - - - - - - - {#each filteredData 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, i (filter.name + i)} - - - {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 00000000000..82d4d801ac2 --- /dev/null +++ b/web-admin/src/features/public-urls/cells/DateCell.svelte @@ -0,0 +1,14 @@ + + +{formatted} 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 00000000000..34a79feac23 --- /dev/null +++ b/web-admin/src/features/public-urls/cells/FiltersCell.svelte @@ -0,0 +1,71 @@ + + +{#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 00000000000..0891582cb89 --- /dev/null +++ b/web-admin/src/features/public-urls/cells/LabelCell.svelte @@ -0,0 +1,25 @@ + + + + + + {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 27b80ffcd8a..74282babee6 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 @@ -9,18 +9,49 @@ } 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 * 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 { Search } from "@rilldata/web-common/components/search"; + 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 isDropdownOpen = false; - 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; @@ -47,47 +78,58 @@ ); $: filteredVariables = searchedVariables.filter((variable) => { - // 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; + 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) => { + $: sortedVariables = [...filteredVariables].sort((a, b) => { return new Date(b.updatedOn).getTime() - new Date(a.updatedOn).getTime(); }); - function handleFilterByEnvironment(environment: EnvironmentTypes) { - filterByEnvironment = environment; + function handleFilterChange(_key: string, value: string) { + const v = value as EnvironmentTypes; + envFilter = envFilter.includes(v) + ? envFilter.filter((x) => x !== v) + : [...envFilter, v]; } - $: environmentLabel = - filterByEnvironment === EnvironmentType.UNDEFINED - ? "All environments" - : filterByEnvironment === EnvironmentType.PRODUCTION - ? "Production" - : "Development"; + function handleClearAllFilters() { + envFilter = []; + searchText = ""; + } $: emptyTextWhenNoVariables = - filterByEnvironment === EnvironmentType.UNDEFINED + envFilter.length === 0 ? "No environment variables" - : `No environment variables for ${environmentLabel}`; + : `No environment variables match the selected filters`; + + $: filterGroups = [ + { + label: "Environment", + key: "environment", + options: [ + { value: EnvironmentType.PRODUCTION, label: "Production" }, + { value: EnvironmentType.DEVELOPMENT, label: "Development" }, + ], + selected: envFilter, + defaultValue: [], + multiSelect: true, + }, + ];
@@ -99,7 +141,7 @@ Error loading environment variables: {$getProjectVariables.error}
{:else if $getProjectVariables.isSuccess} -
+
Environment variables

@@ -112,66 +154,19 @@

-
- - - - {environmentLabel} - {#if isDropdownOpen} - - {:else} - - {/if} - - - Filter by environment - - handleFilterByEnvironment(EnvironmentType.UNDEFINED)} - > - All environments - - - handleFilterByEnvironment(EnvironmentType.PRODUCTION)} - > - Production - - - handleFilterByEnvironment(EnvironmentType.DEVELOPMENT)} - > - Development - - - + (searchText = text)} + searchDisabled={projectVariables.length === 0} + {filterGroups} + onFilterChange={handleFilterChange} + onClearAllFilters={handleClearAllFilters} + showSort={false} + > -
+ Error loading public URLs

{:else} - +
+
+ Public URLs +

+ Manage shared public URLs for your dashboards. + Learn more -> + +

+
+ +
{/if}
diff --git a/web-common/src/components/icons/FilterOutlined.svelte b/web-common/src/components/icons/FilterOutlined.svelte new file mode 100644 index 00000000000..62545d3475c --- /dev/null +++ b/web-common/src/components/icons/FilterOutlined.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/web-common/src/components/table-toolbar/TableToolbar.svelte b/web-common/src/components/table-toolbar/TableToolbar.svelte new file mode 100644 index 00000000000..d64934a7562 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbar.svelte @@ -0,0 +1,71 @@ + + +
+
+
+ +
+ +
+ + + {#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 new file mode 100644 index 00000000000..93df4a2948c --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarAppliedFilters.svelte @@ -0,0 +1,96 @@ + + +
+
+ {#if hasFilters} +
+
+
+ {#each appliedFilters as filter (`${filter.key}:${filter.resetValue}`)} + + {filter.label} + + + {/each} +
+ +
+ {/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 00000000000..05808f526ec --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarFilterDropdown.svelte @@ -0,0 +1,49 @@ + + +{#if filterGroups.length > 0} + + + + Filter + + + {#each filterGroups as group, i} + + {group.label} + {#each group.options as option} + onFilterChange?.(group.key, option.value)} + > + {option.label} + + {/each} + + {#if i < filterGroups.length - 1} + + {/if} + {/each} + + +{/if} 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 00000000000..a156cdbef94 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarSearch.svelte @@ -0,0 +1,76 @@ + + +{#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 00000000000..987e428f1d4 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarSort.svelte @@ -0,0 +1,24 @@ + + + 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 00000000000..ab3121316b9 --- /dev/null +++ b/web-common/src/components/table-toolbar/TableToolbarViewToggle.svelte @@ -0,0 +1,40 @@ + + + +
+ + +
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 00000000000..cef6b1def0c --- /dev/null +++ b/web-common/src/components/table-toolbar/index.ts @@ -0,0 +1,7 @@ +export { default as TableToolbar } from "./TableToolbar.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 00000000000..5178712ff91 --- /dev/null +++ b/web-common/src/components/table-toolbar/types.ts @@ -0,0 +1,23 @@ +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(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; +} diff --git a/web-common/src/components/table/BasicTable.svelte b/web-common/src/components/table/BasicTable.svelte index f747c0921c3..ac5aeedfb5d 100644 --- a/web-common/src/components/table/BasicTable.svelte +++ b/web-common/src/components/table/BasicTable.svelte @@ -19,7 +19,8 @@ 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; let sorting: SortingState = []; @@ -53,6 +54,20 @@ } } + // When external sort key changes, clear internal column sorting (hides arrows) + let prevExternalSortKey: string | undefined = externalSortKey; + $: if ( + externalSortKey !== undefined && + externalSortKey !== prevExternalSortKey + ) { + prevExternalSortKey = externalSortKey; + sorting = []; + options.update((old) => ({ + ...old, + state: { ...old.state, sorting: [] }, + })); + } + const setSorting: OnChangeFn = (updater) => { if (updater instanceof Function) { sorting = updater(sorting); @@ -128,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}
diff --git a/web-common/src/features/projects/status/tables/utils.ts b/web-common/src/features/projects/status/tables/utils.ts index 9d584e40d44..291a67d99ff 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 4027fd6081f..53dd35f4865 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, );