From 2c15dc4794edff4bc64ee8fd34c4d420c70c0d1f Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Wed, 22 Jul 2026 20:11:16 +0530 Subject: [PATCH 1/5] reafc: move to unified filter component --- .../filters/manager/DimensionFilter.svelte | 641 ++++++++++++++++++ .../filters/manager/ExpressionFilters.svelte | 102 +++ .../dimension-filter-manager.svelte.ts | 156 +++++ .../expression-filter-manager.svelte.ts | 136 ++++ .../dashboards/filters/manager/types.ts | 6 + .../state-managers/actions/filters.ts | 10 + .../loaders/DashboardStateSync.ts | 13 + .../dashboards/workspace/Dashboard.svelte | 4 + 8 files changed, 1068 insertions(+) create mode 100644 web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte create mode 100644 web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte create mode 100644 web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts create mode 100644 web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts create mode 100644 web-common/src/features/dashboards/filters/manager/types.ts diff --git a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte new file mode 100644 index 000000000000..0058b62fb65e --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte @@ -0,0 +1,641 @@ + + + { + if (e.key === "Enter") { + await onApply(); + } + }} +/> + + + + {#snippet child({ props })} + + filterData.clear()} + removable={!readOnly && !curPinned && !filterData.pinned} + {readOnly} + removeTooltipText="remove {filterData.selectedValues + .length} value{filterData.selectedValues.length !== 1 ? 's' : ''}" + > + + +
+ + + {filterData.name} + {filterData.pinned + ? "required dimension" + : "dimension"} + + {#if missingRequired} + This filter is required. Select a value to load the dashboard. + {:else} + Click to edit the filters in this dimension + {/if} + +
+
+ {/snippet} +
+ + + +
+ {#if filterData.editing} +
+ {filterData.label} + +
+ { + curRequired = !curRequired; + }} + /> + { + curPinned = !curPinned; + }} + /> +
+
+ {/if} +
+ + +
+ {#if showExtraInfo} +
+ {#if curMode !== DimensionFilterMode.Select} + + {searchResultCountText} + + {:else} +
+ {/if} +
+ {/if} +
+ + {#if showExtraInfo} + + {/if} + +
+ {#if isFetching} +
+ +
+ {:else if error} +
error
+ {:else if inListTooLong} +
+ List is too long. Please remove some values. +
+ {:else if correctedSearchResults} + + + {#if curMode === DimensionFilterMode.Select && !curSearchText} + {#each checkedItems as name (name)} + {@const selected = effectiveSelectedValues.includes(name)} + {@const label = name ?? "null"} + + handleItemClick(name)} + > + + {#if label.length > 240} + {label.slice(0, 240)}... + {:else} + {label} + {/if} + + + {/each} + {/if} + + + {#if curMode === DimensionFilterMode.Select && !curSearchText && checkedItems.length > 0 && uncheckedItems.length > 0} + + {/if} + + + {#each uncheckedItems as name (name)} + {@const selected = effectiveSelectedValues.includes(name)} + {@const label = name ?? "null"} + + handleItemClick(name)} + > + + {#if label.length > 240} + {label.slice(0, 240)}... + {:else} + {label} + {/if} + + + {/each} + + + {#if uncheckedItems.length === 0 && (curMode !== DimensionFilterMode.Select || checkedItems.length === 0)} +
+ no results +
+ {/if} +
+ {/if} +
+ + +
+
diff --git a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte new file mode 100644 index 000000000000..3189b47ae253 --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte @@ -0,0 +1,102 @@ + + +
+
+ {#each expressionFilterManager.dimensionFilterManagers as dimensionFilterManager (dimensionFilterManager.name)} + + {/each} + + + expressionFilterManager.addTemporaryDimensionFilter( + name, + dimensionIdMap, + metricsViewName, + )} + /> + + {#if hasFilters} + + {/if} +
+
diff --git a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts new file mode 100644 index 000000000000..aefaa5a25e8d --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts @@ -0,0 +1,156 @@ +import { page } from "$app/state"; +import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; +import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants.ts"; +import type { + MetricsViewSpecDimension, + V1Expression, +} from "@rilldata/web-common/runtime-client"; +import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts"; +import { + createInExpression, + createLikeExpression, +} from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; + +export type DimensionFilterManagerInit = { + mode: DimensionFilterMode; + selectedValues?: string[]; + inputText?: string; + exclude?: boolean; + pinned?: boolean; + required?: boolean; +}; + +export class DimensionFilterManager { + public expr: V1Expression | undefined = $state(undefined); + + public mode: DimensionFilterMode; + public selectedValues: string[]; + public inputText: string; + public exclude: boolean; + public pinned: boolean; + public required: boolean; + + public constructor( + public readonly name: string, + public readonly label: string, + public readonly dimensions: Map, + public readonly editing: boolean, // TODO: maybe separate editing, pinned & required? + init: DimensionFilterManagerInit, + ) { + this.mode = $state(init.mode); + this.selectedValues = $state(init.selectedValues ?? []); + this.inputText = $state(init.inputText ?? ""); + this.exclude = $state(init.exclude ?? false); + this.pinned = $state(init.pinned ?? false); + this.required = $state(init.required ?? false); + this.buildExpression(); + } + + public toggleMultipleValues( + dimensionValues: string[], + isExclusiveFilter?: boolean, + exclude?: boolean, + ) { + if (exclude !== undefined) this.exclude = exclude; + + if (this.mode !== DimensionFilterMode.Select) { + this.inputText = ""; + eventBus.emit("notification", { + message: "Converted filter type to Select", + link: { + text: m.common_undo(), + href: page.url.href, + }, + }); + this.mode = DimensionFilterMode.Select; + this.selectedValues = dimensionValues; + return this.buildExpression(); + } + + if (isExclusiveFilter) { + this.selectedValues = dimensionValues; + return this.buildExpression(); + } + + dimensionValues.forEach((v) => { + const removedIndex = this.toggleValue(v, false); + if (removedIndex === -1) return; + + // TODO: decrement pinIndex if the removed value was before the pinned value + }); + + this.buildExpression(); + } + + public toggleValue(dimensionValue: string, isExclusiveFilter: boolean) { + const inIdx = this.selectedValues.findIndex((v) => v === dimensionValue); + let retIdx = inIdx; + + if (inIdx === -1) { + if (isExclusiveFilter) { + this.selectedValues = [dimensionValue]; + retIdx = -1; + } else { + this.selectedValues = [...this.selectedValues, dimensionValue]; + } + } else { + this.selectedValues = this.selectedValues.splice(inIdx, 1); + } + + return retIdx; + } + + public setInList(values: string[], exclude?: boolean) { + if (exclude !== undefined) this.exclude = exclude; + + this.mode = DimensionFilterMode.InList; + this.selectedValues = values; + this.inputText = ""; + this.buildExpression(); + } + + public setContainsText(searchText: string, exclude?: boolean) { + if (exclude !== undefined) this.exclude = exclude; + + this.mode = DimensionFilterMode.Contains; + this.selectedValues = []; + this.inputText = searchText; + this.buildExpression(); + } + + public toggleExclude() { + this.exclude = !this.exclude; + } + + public togglePinned() { + this.pinned = !this.pinned; + } + + public toggleRequired() { + this.required = !this.required; + } + + public clear() { + this.selectedValues = []; + this.inputText = ""; + this.expr = undefined; + this.buildExpression(); + } + + private buildExpression() { + switch (this.mode) { + case DimensionFilterMode.Select: + case DimensionFilterMode.InList: + this.expr = this.selectedValues.length + ? createInExpression(this.name, this.selectedValues, this.exclude) + : undefined; + break; + + case DimensionFilterMode.Contains: + this.expr = this.inputText + ? createLikeExpression(this.name, `%${this.inputText}%`, this.exclude) + : undefined; + break; + } + } +} diff --git a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts new file mode 100644 index 000000000000..06f716ceba27 --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts @@ -0,0 +1,136 @@ +import { + DimensionFilterManager, + type DimensionFilterManagerInit, +} from "@rilldata/web-common/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts"; +import type { V1Expression } from "@rilldata/web-admin/client"; +import { + convertExpressionToFilterParam, + convertFilterParamToExpression, +} from "@rilldata/web-common/features/dashboards/url-state/filters/converters.ts"; +import { + createAndExpression, + forEachIdentifier, + getValuesInExpression, +} from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; +import { + type MetricsViewSpecDimension, + V1Operation, +} from "@rilldata/web-common/runtime-client"; +import { getDimensionDisplayName } from "@rilldata/web-common/features/dashboards/filters/getDisplayName.ts"; +import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants.ts"; + +export class FilterManagerManager { + public dimensionFilterManagers: DimensionFilterManager[] = $state([]); + public expr: V1Expression; + public exprParam: string = ""; + + private temporaryFilter: DimensionFilterManager | undefined = + $state(undefined); + + public constructor() { + this.expr = $derived.by(() => { + const exprs = this.dimensionFilterManagers + .filter((dfm) => !!dfm.expr) + .map((d) => d.expr as V1Expression); + return createAndExpression(exprs); + }); + } + + public setExprParam( + exprParam: string, + dimensionIdMap: Map, + metricsViewName: string, + ) { + if (exprParam === this.exprParam) return; + this.exprParam = exprParam; + + const { expr, dimensionsWithInlistFilter } = + convertFilterParamToExpression(exprParam); + // TODO: check complex filter + if (!expr) { + this.dimensionFilterManagers = []; + return; + } + + const addedDimension = new Set(); + const newDimensionFilterManagers: DimensionFilterManager[] = []; + forEachIdentifier(expr, (e, ident) => { + const dim = dimensionIdMap.get(ident); + if (!dim) return; + addedDimension.add(ident); + + const op = e.cond?.op; + let init: DimensionFilterManagerInit | undefined = undefined; + if (op === V1Operation.OPERATION_IN || op === V1Operation.OPERATION_NIN) { + const isInListMode = dimensionsWithInlistFilter.includes(ident); + init = { + mode: isInListMode + ? DimensionFilterMode.InList + : DimensionFilterMode.Select, + selectedValues: getValuesInExpression(e), + exclude: op === V1Operation.OPERATION_NIN, + inputText: undefined, + }; + } else if ( + op === V1Operation.OPERATION_LIKE || + op === V1Operation.OPERATION_NLIKE + ) { + init = { + mode: DimensionFilterMode.Contains, + selectedValues: [], + inputText: e.cond?.exprs?.[1]?.val?.toString?.() ?? "", + exclude: op === V1Operation.OPERATION_NLIKE, + }; + } + + if (!init) return; + + newDimensionFilterManagers.push( + new DimensionFilterManager( + ident, + getDimensionDisplayName(dim), + new Map([[metricsViewName, dim]]), + false, + init, + ), + ); + }); + + if (this.temporaryFilter) { + if (!addedDimension.has(this.temporaryFilter.name)) { + newDimensionFilterManagers.push(this.temporaryFilter); + } else { + this.temporaryFilter = undefined; + } + } + + this.dimensionFilterManagers = newDimensionFilterManagers; + } + + public addTemporaryDimensionFilter( + name: string, + dimensionIdMap: Map, + metricsViewName: string, + ) { + const dim = dimensionIdMap.get(name); + if (!dim) return; + + this.temporaryFilter = new DimensionFilterManager( + name, + getDimensionDisplayName(dim), + new Map([[metricsViewName, dim]]), + false, + { mode: DimensionFilterMode.Select }, + ); + this.dimensionFilterManagers = [ + ...this.dimensionFilterManagers, + this.temporaryFilter, + ]; + } + + public clear() { + this.dimensionFilterManagers = []; + this.temporaryFilter = undefined; + console.log("clear", [...this.dimensionFilterManagers]); + } +} diff --git a/web-common/src/features/dashboards/filters/manager/types.ts b/web-common/src/features/dashboards/filters/manager/types.ts new file mode 100644 index 000000000000..44398b69b135 --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/types.ts @@ -0,0 +1,6 @@ +import type { V1Expression } from "@rilldata/web-common/runtime-client"; + +export type RawParsedFilter = { + expr: V1Expression | undefined; + dimensionsWithInlistFilter: string[]; +}; diff --git a/web-common/src/features/dashboards/state-managers/actions/filters.ts b/web-common/src/features/dashboards/state-managers/actions/filters.ts index 32749c48449f..760b14553c51 100644 --- a/web-common/src/features/dashboards/state-managers/actions/filters.ts +++ b/web-common/src/features/dashboards/state-managers/actions/filters.ts @@ -1,5 +1,6 @@ import type { DashboardMutables } from "@rilldata/web-common/features/dashboards/state-managers/actions/types"; import { createAndExpression } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; +import type { V1Expression } from "@rilldata/web-common/runtime-client"; export function clearAllFilters({ dashboard }: DashboardMutables) { const hasFilters = @@ -23,6 +24,13 @@ export function setTemporaryFilterName( dashboard.temporaryFilterName = name; } +export function setFilter( + { dashboard }: DashboardMutables, + expr: V1Expression, +) { + dashboard.whereFilter = expr; +} + export const filterActions = { /** * Clears all filters and resets related fields @@ -30,4 +38,6 @@ export const filterActions = { clearAllFilters, setTemporaryFilterName, + + setFilter, }; diff --git a/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts b/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts index 7b1c3e3e1240..7033c60e9d11 100644 --- a/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts +++ b/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts @@ -20,6 +20,7 @@ import type { AfterNavigate } from "@sveltejs/kit"; import { getContext, setContext } from "svelte"; import { derived, get, type Readable } from "svelte/store"; import type { CompoundQueryResult } from "@rilldata/web-common/features/compound-query-result"; +import { ExploreStateURLParams } from "@rilldata/web-common/features/dashboards/url-state/url-params.ts"; export const DASHBOARD_STATE_SYNC_KEY = Symbol("state-sync"); @@ -185,6 +186,7 @@ export class DashboardStateSync { ); } + // log("INIT", redirectUrl); // If the current url same as the new url then there is no need to do anything if (redirectUrl.search === pageState.url.search) { this.initialized = true; @@ -294,6 +296,7 @@ export class DashboardStateSync { this.updating = false; } + // log("URL", redirectUrl); // If the url doesn't need to be changed further then we can skip the goto if (redirectUrl.search === pageState.url.search) { return; @@ -349,6 +352,7 @@ export class DashboardStateSync { ); } + // log("GOTO", newUrl); // If the state didnt result in a new url then skip goto. // This avoids adding redundant urls to the history. if (newUrl.search === pageState.url.search) { @@ -362,3 +366,12 @@ export class DashboardStateSync { } } } + +function log(label: string, toUrl: URL) { + const fromUrlSearch = get(page).url.search; + const toUrlSearch = toUrl.search; + const equal = fromUrlSearch === toUrlSearch; + console.log( + `[${label}] ${fromUrlSearch} =${equal ? "X" : "="}> ${toUrlSearch}`, + ); +} diff --git a/web-common/src/features/dashboards/workspace/Dashboard.svelte b/web-common/src/features/dashboards/workspace/Dashboard.svelte index cbac3e2f4682..9c48ab4c6094 100644 --- a/web-common/src/features/dashboards/workspace/Dashboard.svelte +++ b/web-common/src/features/dashboards/workspace/Dashboard.svelte @@ -22,6 +22,7 @@ import MeasuresContainer from "../big-number/MeasuresContainer.svelte"; import DimensionDisplay from "../dimension-table/DimensionDisplay.svelte"; import Filters from "../filters/Filters.svelte"; + import ExpressionFilters from "../filters/manager/ExpressionFilters.svelte"; import { selectedMockUserStore } from "../granular-access-policies/stores"; import LeaderboardDisplay from "../leaderboard/LeaderboardDisplay.svelte"; import RowsViewerAccordion from "../rows-viewer/RowsViewerAccordion.svelte"; @@ -165,6 +166,9 @@
{:else} {#key exploreName} +
+ +
From 102e28b82e6571733af56356c81654e7563d3892 Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Thu, 23 Jul 2026 12:18:16 +0530 Subject: [PATCH 2/5] Simplify proxy logic --- .../DimensionFilterFooter.svelte | 2 +- .../filters/manager/DimensionFilter.svelte | 430 ++++++------------ .../filters/manager/ExpressionFilters.svelte | 26 +- .../dimension-filter-manager.svelte.ts | 140 +++--- .../expression-filter-manager.svelte.ts | 47 +- .../filters/manager/queries.svelte.ts | 172 +++++++ 6 files changed, 416 insertions(+), 401 deletions(-) create mode 100644 web-common/src/features/dashboards/filters/manager/queries.svelte.ts diff --git a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterFooter.svelte b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterFooter.svelte index bd6e59d9ffd7..1b82318efb09 100644 --- a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterFooter.svelte +++ b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilterFooter.svelte @@ -9,7 +9,7 @@ export let excludeMode: boolean; export let allSelected: boolean; export let disableApplyButton: boolean; - export let onToggleExcludeMode: (checked: boolean) => void; + export let onToggleExcludeMode: () => void; export let onToggleSelectAll: () => void; export let onApply: () => void; diff --git a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte index 0058b62fb65e..60824566723c 100644 --- a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte +++ b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte @@ -21,18 +21,18 @@ import DimensionFilterChipBody from "@rilldata/web-common/features/dashboards/filters/dimension-filters/DimensionFilterChipBody.svelte"; import DimensionFilterFooter from "@rilldata/web-common/features/dashboards/filters/dimension-filters/DimensionFilterFooter.svelte"; import DimensionFilterModeSelector from "@rilldata/web-common/features/dashboards/filters/dimension-filters/DimensionFilterModeSelector.svelte"; - import type { V1Expression } from "@rilldata/web-common/runtime-client"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; - import { - useAllSearchResultsCount, - useDimensionSearch, - } from "web-common/src/features/dashboards/filters/dimension-filters/dimension-filter-values"; import PinButton from "../PinButton.svelte"; import RequiredButton from "../RequiredButton.svelte"; import { DimensionFilterManager } from "./dimension-filter-manager.svelte.ts"; + import { + getAllSearchResultsCount, + getDimensionSearchQuery, + } from "@rilldata/web-common/features/dashboards/filters/manager/queries.svelte.ts"; + import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts"; - export let filterData: DimensionFilterManager; - export let expressionMap: Map; + export let manager: ExpressionFilterManager; + export let dimensionManager: DimensionFilterManager; export let openOnMount: boolean = true; export let readOnly: boolean = false; export let timeStart: string | undefined; @@ -45,51 +45,22 @@ values: string[], ) => boolean = () => false; + let proxyDimensionManager = $derived(dimensionManager.clone()); + let open = - openOnMount && !filterData.selectedValues?.length && !filterData.inputText; - let curMode = filterData.mode; - let curSearchText = filterData.inputText ?? ""; - let curExcludeMode = filterData.exclude === true; + openOnMount && + !dimensionManager.selectedValues?.length && + !dimensionManager.inputText; + let curSearchText = dimensionManager.inputText ?? ""; let inListTooLong = false; - let selectedValuesProxy: string[] = filterData.selectedValues ?? []; - let searchedBulkValues: string[] = - filterData.mode === DimensionFilterMode.InList - ? (filterData.selectedValues ?? []) - : []; - let curPinned = filterData.pinned; - let curRequired = filterData.required; - let excludeModeDirty = false; const client = useRuntimeClient(); - $: missingRequired = Boolean(filterData.pinned && !filterData.expr); - - $: if ( - !open && - excludeModeDirty && - (filterData.exclude === true) === curExcludeMode - ) { - excludeModeDirty = false; - } - - $: if ( - !open && - (filterData.mode !== curMode || - (!excludeModeDirty && (filterData.exclude === true) !== curExcludeMode)) - ) { - resyncFilterData(); - } - - // Sync proxy when selectedValues changes (for Select mode) - $: if (!open && filterData.mode === DimensionFilterMode.Select) { - selectedValuesProxy = [...filterData.selectedValues]; - } - - $: metricsViewNames = Array.from(filterData.dimensions.keys()); - - $: excludeMode = filterData.exclude === true; + $: missingRequired = Boolean( + dimensionManager.pinned && !dimensionManager.expr, + ); - $: sanitisedSearchText = filterData.inputText + $: sanitisedSearchText = dimensionManager.inputText ?.replace(/^%/, "") .replace(/%$/, ""); @@ -97,27 +68,24 @@ $: enableSearchQuery = Boolean(timeControlsReady && open) && - (curMode === DimensionFilterMode.Select || - (curMode === DimensionFilterMode.Contains && curSearchText.length > 0) || - (curMode === DimensionFilterMode.InList && - searchedBulkValues.length > 0)); + (proxyDimensionManager.mode === DimensionFilterMode.Select || + (proxyDimensionManager.mode === DimensionFilterMode.Contains && + curSearchText.length > 0) || + (proxyDimensionManager.mode === DimensionFilterMode.InList && + proxyDimensionManager.selectedValues.length > 0)); - $: searchResultsQuery = useDimensionSearch( + $: searchResultsQuery = getDimensionSearchQuery( client, - metricsViewNames, - filterData.name, + manager, + dimensionManager, { - mode: curMode, - values: - curMode === DimensionFilterMode.Select - ? filterData.selectedValues - : searchedBulkValues, + mode: proxyDimensionManager.mode, + values: proxyDimensionManager.selectedValues, searchText: curSearchText, timeStart, timeEnd, timeDimension, enabled: enableSearchQuery, - metricsViewWheres: expressionMap, }, ); $: ({ @@ -129,24 +97,23 @@ $: enableSearchCountQuery = Boolean(timeControlsReady) && - ((curMode === DimensionFilterMode.Contains && curSearchText.length > 0) || - (curMode === DimensionFilterMode.InList && - searchedBulkValues.length > 0)); + ((proxyDimensionManager.mode === DimensionFilterMode.Contains && + curSearchText.length > 0) || + (proxyDimensionManager.mode === DimensionFilterMode.InList && + proxyDimensionManager.selectedValues.length > 0)); - $: allSearchResultsCountQuery = useAllSearchResultsCount( + $: allSearchResultsCountQuery = getAllSearchResultsCount( client, - metricsViewNames, - filterData.name, + manager, + dimensionManager, { - mode: curMode, - values: searchedBulkValues, + mode: proxyDimensionManager.mode, + values: proxyDimensionManager.selectedValues, searchText: curSearchText, timeStart, timeEnd, timeDimension, enabled: enableSearchCountQuery, - - metricsViewWheres: expressionMap, }, ); $: ({ @@ -155,32 +122,32 @@ isFetching: isFetchingFromAllSearchResultsCount, } = $allSearchResultsCountQuery); $: searchResultCountText = enableSearchCountQuery - ? curMode === DimensionFilterMode.Contains + ? proxyDimensionManager.mode === DimensionFilterMode.Contains ? `${allSearchResultsCount} results` - : `${allSearchResultsCount} of ${searchedBulkValues.length} matched` + : `${allSearchResultsCount} of ${proxyDimensionManager.selectedValues.length} matched` : "0 results"; - $: searchPlaceholder = getSearchPlaceholder(curMode); + $: searchPlaceholder = getSearchPlaceholder(proxyDimensionManager.mode); $: error = errorFromSearchResults ?? errorFromAllSearchResultsCount; $: isFetching = isFetchingFromSearchResults ?? isFetchingFromAllSearchResultsCount; - $: showExtraInfo = curMode !== DimensionFilterMode.Select; // || curSearchText.length > 0; (Add once we have docs) + $: showExtraInfo = proxyDimensionManager.mode !== DimensionFilterMode.Select; // || curSearchText.length > 0; (Add once we have docs) $: allSelected = Boolean( effectiveSelectedValues.length && correctedSearchResults?.length === effectiveSelectedValues.length, ); $: effectiveSelectedValues = getEffectiveSelectedValues( - curMode, - selectedValuesProxy, + proxyDimensionManager.mode, + proxyDimensionManager.selectedValues, correctedSearchResults ?? [], - filterData.selectedValues, + dimensionManager.selectedValues, ); $: disableApplyButton = shouldDisableApplyButton( - curMode, + proxyDimensionManager.mode, enableSearchCountQuery, inListTooLong, ); @@ -188,225 +155,107 @@ // Split results into checked and unchecked for better UX (like SelectionDropdown) // Use actual selectedValues (not proxy) so items only sort after dropdown closes $: ({ checkedItems, uncheckedItems } = getItemLists( - curMode, + proxyDimensionManager.mode, correctedSearchResults ?? [], - filterData.selectedValues, + dimensionManager.selectedValues, curSearchText, )); - /** - * Reset filter settings based on params to the component. - */ - function resetFilterSettings( - mode: DimensionFilterMode, - sanitisedSearchText: string | undefined, - ) { - curExcludeMode = excludeMode; - switch (mode) { - case DimensionFilterMode.Select: - curMode = DimensionFilterMode.Select; - curSearchText = ""; - selectedValuesProxy = [...filterData.selectedValues]; - break; - - case DimensionFilterMode.InList: - curMode = DimensionFilterMode.InList; - curSearchText = mergeDimensionSearchValues(filterData.selectedValues); - searchedBulkValues = filterData.selectedValues; // Ensure searchedBulkValues includes existing selections - break; - - case DimensionFilterMode.Contains: - curMode = DimensionFilterMode.Contains; - curSearchText = sanitisedSearchText ?? ""; - break; - } - } - function checkSearchText(inputText: string) { inListTooLong = false; // Only InList mode parses bulk values. Other modes treat input as search text. - if (curMode !== DimensionFilterMode.InList) return; + if (proxyDimensionManager.mode !== DimensionFilterMode.InList) return; const values = splitDimensionSearchText(inputText); if (values.length <= 1) { - searchedBulkValues = inputText === "" ? [] : values; + proxyDimensionManager.selectedValues = inputText === "" ? [] : values; return; } // Include both existing selected values and new search values so the // below-fold query can find existing selected values that might not be // in the top 250. - searchedBulkValues = [ - ...new Set([...filterData.selectedValues, ...values]), + proxyDimensionManager.selectedValues = [ + ...new Set([...proxyDimensionManager.selectedValues, ...values]), ]; - inListTooLong = isUrlTooLongAfterInListFilter(searchedBulkValues); + inListTooLong = isUrlTooLongAfterInListFilter( + proxyDimensionManager.selectedValues, + ); } function handleModeChange(newMode: DimensionFilterMode) { curSearchText = ""; - if (newMode !== DimensionFilterMode.InList) { - searchedBulkValues = []; - // Reset proxy when switching to/from Select mode - if (newMode === DimensionFilterMode.Select) { - curExcludeMode = excludeMode; - selectedValuesProxy = [...filterData.selectedValues]; - } - } else { - checkSearchText(curSearchText); + switch (newMode) { + case DimensionFilterMode.Select: + proxyDimensionManager.setSelectedValues( + [...proxyDimensionManager.selectedValues], + proxyDimensionManager.exclude, + ); + break; + + case DimensionFilterMode.InList: + proxyDimensionManager.setInList( + [...proxyDimensionManager.selectedValues], + proxyDimensionManager.exclude, + ); + break; + + case DimensionFilterMode.Contains: + proxyDimensionManager.setContainsText( + curSearchText, + proxyDimensionManager.exclude, + ); + break; } + checkSearchText(curSearchText); } - async function handleOpenChange(open: boolean) { + function handleOpenChange(open: boolean) { if (open) { curSearchText = - filterData.mode === DimensionFilterMode.InList - ? mergeDimensionSearchValues(filterData.selectedValues) + dimensionManager.mode === DimensionFilterMode.InList + ? mergeDimensionSearchValues(dimensionManager.selectedValues) : (sanitisedSearchText ?? ""); } else { - if (filterData.pinned !== curPinned) { - filterData.togglePinned(); - } - if (filterData.pinned !== curRequired) { - filterData.toggleRequired(); - } - // Apply proxy changes for Select mode when dropdown closes - if (curMode === DimensionFilterMode.Select) { - await applySelectModeChanges(); - // Don't reset immediately for Select mode - let props update first - return; - } - - if (filterData.selectedValues.length === 0 && !filterData.inputText) { - // filter was cleared. so remove the filter - filterData.clear(); // TODO: await + if (proxyDimensionManager.mode === DimensionFilterMode.Select) { + dimensionManager.expr = proxyDimensionManager.expr; } else { - // reset the settings on unmount (but not for Select mode) - resetFilterSettings(filterData.mode, sanitisedSearchText); + proxyDimensionManager = dimensionManager.clone(); } } } - function handleToggleExcludeMode(checked: boolean) { - curExcludeMode = checked; - excludeModeDirty = true; + function handleToggleExcludeMode() { + proxyDimensionManager.toggleExclude(); } function onToggleSelectAll() { - if (curMode === DimensionFilterMode.Select) { - // Update proxy for select all/deselect all - if (allSelected) { - selectedValuesProxy = selectedValuesProxy.filter( - (v) => !correctedSearchResults?.includes(v), - ); - } else { - const newValues = - correctedSearchResults?.filter( - (v) => !selectedValuesProxy.includes(v), - ) ?? []; - selectedValuesProxy = [...selectedValuesProxy, ...newValues]; - } - } else { - correctedSearchResults?.forEach((dimensionValue) => { - if (!allSelected && effectiveSelectedValues.includes(dimensionValue)) - return; - - filterData.toggleMultipleValues([dimensionValue]); - }); - } + proxyDimensionManager.setSelectedValues( + [ + ...proxyDimensionManager.selectedValues, + ...(correctedSearchResults ?? []), + ], + proxyDimensionManager.exclude, + ); } - async function onApply(close = true) { + function onApply(close = true) { if (disableApplyButton) return; - const newExclude = - curExcludeMode !== excludeMode ? curExcludeMode : undefined; - switch (curMode) { - case DimensionFilterMode.Select: - // Apply proxy changes for Select mode - await applySelectModeChanges(); - if (close) open = false; - break; - case DimensionFilterMode.InList: - if (searchedBulkValues.length === 0) return; - filterData.setInList(searchedBulkValues, newExclude); // TODO: await - if (close) open = false; - break; - case DimensionFilterMode.Contains: - if (curSearchText.length === 0) return; - filterData.setContainsText(curSearchText, newExclude); // TODO: await - if (close) open = false; - break; - } + dimensionManager.expr = proxyDimensionManager.expr; + if (close) open = false; } - async function applySelectModeChanges() { - // Find values that were added or removed - const currentValues = new Set(filterData.selectedValues); - const proxyValues = new Set(selectedValuesProxy); - const changedValues = [...currentValues, ...proxyValues].filter((value) => { - const wasSelected = currentValues.has(value); - const isSelected = proxyValues.has(value); - - return wasSelected !== isSelected; - }); - const shouldToggleExcludeMode = curExcludeMode !== excludeMode; - const shouldCommitSelectMode = - filterData.mode !== DimensionFilterMode.Select && currentValues.size > 0; - - if (!currentValues.size && !proxyValues.size) { - console.log("no change"); - // No changes - return; - } - - // Existing filters still need to be negated. New filters get the operator - // from curExcludeMode when values are committed below. - if (shouldToggleExcludeMode && currentValues.size > 0) { - console.log("toggleExclude"); - filterData.toggleExclude(); // TODO: await - } - - if (changedValues.length || shouldCommitSelectMode) { - console.log("toggleMultipleValues"); - filterData.toggleMultipleValues(changedValues, undefined, curExcludeMode); // TODO: await - } - } - - async function handleItemClick(value: string) { - if (curMode === DimensionFilterMode.Select) { - // Update proxy instead of calling onSelect immediately - if (selectedValuesProxy.includes(value)) { - selectedValuesProxy = selectedValuesProxy.filter((v) => v !== value); - } else { - selectedValuesProxy = [...selectedValuesProxy, value]; - } - } else { - filterData.toggleMultipleValues([value]); // TODO: await - } - } - - function resyncFilterData() { - curMode = filterData.mode; - curSearchText = filterData.inputText ?? ""; - curExcludeMode = filterData.exclude === true; - excludeModeDirty = false; - selectedValuesProxy = filterData.selectedValues ?? []; - searchedBulkValues = - filterData.mode === DimensionFilterMode.InList - ? filterData.selectedValues - : []; - curPinned = filterData.pinned; - curRequired = filterData.required; + function handleItemClick(value: string) { + proxyDimensionManager.toggleValue(value, false); } { - if (e.key === "Enter") { - await onApply(); - } + if (e.key === "Enter") onApply(); }} /> @@ -423,31 +272,36 @@ filterData.clear()} - removable={!readOnly && !curPinned && !filterData.pinned} + onRemove={() => dimensionManager.clear()} + removable={!readOnly && + !proxyDimensionManager.pinned && + !dimensionManager.pinned} {readOnly} - removeTooltipText="remove {filterData.selectedValues - .length} value{filterData.selectedValues.length !== 1 ? 's' : ''}" + removeTooltipText="remove {dimensionManager.selectedValues + .length} value{dimensionManager.selectedValues.length !== 1 + ? 's' + : ''}" > @@ -455,9 +309,11 @@
- {filterData.name} + {dimensionManager.name} {filterData.pinned + >{dimensionManager.pinned ? "required dimension" : "dimension"} @@ -481,37 +337,33 @@ class="flex flex-col max-h-96 w-[400px] overflow-hidden p-0" >
- {#if filterData.editing} + {#if dimensionManager.editing}
- {filterData.label} + {dimensionManager.label}
{ - curRequired = !curRequired; - }} + required={proxyDimensionManager.required} + onToggleRequired={() => proxyDimensionManager.toggleRequired()} /> { - curPinned = !curPinned; - }} + pinned={proxyDimensionManager.pinned} + onTogglePin={() => proxyDimensionManager.togglePinned()} />
{/if}
{#if showExtraInfo}
- {#if curMode !== DimensionFilterMode.Select} + {#if proxyDimensionManager.mode !== DimensionFilterMode.Select} {searchResultCountText} @@ -557,10 +409,10 @@ {:else if correctedSearchResults} - {#if curMode === DimensionFilterMode.Select && !curSearchText} + {#if proxyDimensionManager.mode === DimensionFilterMode.Select && !curSearchText} {#each checkedItems as name (name)} {@const selected = effectiveSelectedValues.includes(name)} {@const label = name ?? "null"} @@ -570,7 +422,7 @@ closeOnSelect={false} class="text-xs cursor-pointer" checked={selected} - showXForSelected={curExcludeMode} + showXForSelected={proxyDimensionManager.exclude} onclick={() => handleItemClick(name)} > @@ -585,7 +437,7 @@ {/if} - {#if curMode === DimensionFilterMode.Select && !curSearchText && checkedItems.length > 0 && uncheckedItems.length > 0} + {#if proxyDimensionManager.mode === DimensionFilterMode.Select && !curSearchText && checkedItems.length > 0 && uncheckedItems.length > 0} {/if} @@ -595,17 +447,19 @@ {@const label = name ?? "null"} handleItemClick(name)} > @@ -619,7 +473,7 @@ {/each} - {#if uncheckedItems.length === 0 && (curMode !== DimensionFilterMode.Select || checkedItems.length === 0)} + {#if uncheckedItems.length === 0 && (proxyDimensionManager.mode !== DimensionFilterMode.Select || checkedItems.length === 0)}
no results
@@ -629,8 +483,8 @@
- expressionFilterManager.setExprParam( + manager.setExprParam( filterUrlStore.value ?? "", dimensionIdMap, metricsViewName, ), ); - let hasFilters = $derived( - expressionFilterManager.dimensionFilterManagers.length > 0, - ); - - $effect(() => setFilter(expressionFilterManager.expr)); + let hasFilters = $derived(manager.dimensionFilterManagers.length > 0); - let expressionMap = $derived( - new Map([[metricsViewName, expressionFilterManager.expr]]), - ); + $effect(() => setFilter(manager.expr)); let { selectedTimeDimension } = $derived($dashboardStore); const timeControlsStore = useTimeControlStore(StateManagers); @@ -68,10 +62,10 @@
- {#each expressionFilterManager.dimensionFilterManagers as dimensionFilterManager (dimensionFilterManager.name)} + {#each manager.dimensionFilterManagers as dimensionManager (dimensionManager.name)} - expressionFilterManager.addTemporaryDimensionFilter( + manager.addTemporaryDimensionFilter( name, dimensionIdMap, metricsViewName, @@ -94,7 +88,7 @@ {#if hasFilters} - {/if} diff --git a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts index aefaa5a25e8d..c16dfb2501f0 100644 --- a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts @@ -1,23 +1,24 @@ import { page } from "$app/state"; import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants.ts"; -import type { - MetricsViewSpecDimension, - V1Expression, +import { + type MetricsViewSpecDimension, + type V1Expression, + V1Operation, } from "@rilldata/web-common/runtime-client"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts"; import { + copyFilterExpression, createInExpression, createLikeExpression, + getValuesInExpression, } from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; export type DimensionFilterManagerInit = { mode: DimensionFilterMode; - selectedValues?: string[]; - inputText?: string; - exclude?: boolean; - pinned?: boolean; - required?: boolean; + selectedValues: string[]; + inputText: string; + exclude: boolean; }; export class DimensionFilterManager { @@ -30,96 +31,100 @@ export class DimensionFilterManager { public pinned: boolean; public required: boolean; + private oldMode: DimensionFilterMode; + public constructor( public readonly name: string, public readonly label: string, public readonly dimensions: Map, public readonly editing: boolean, // TODO: maybe separate editing, pinned & required? - init: DimensionFilterManagerInit, + initExpr: V1Expression = createInExpression(name, []), + isInList: boolean = false, ) { - this.mode = $state(init.mode); - this.selectedValues = $state(init.selectedValues ?? []); - this.inputText = $state(init.inputText ?? ""); - this.exclude = $state(init.exclude ?? false); - this.pinned = $state(init.pinned ?? false); - this.required = $state(init.required ?? false); - this.buildExpression(); - } - - public toggleMultipleValues( - dimensionValues: string[], - isExclusiveFilter?: boolean, - exclude?: boolean, - ) { - if (exclude !== undefined) this.exclude = exclude; - - if (this.mode !== DimensionFilterMode.Select) { - this.inputText = ""; - eventBus.emit("notification", { - message: "Converted filter type to Select", - link: { - text: m.common_undo(), - href: page.url.href, - }, - }); - this.mode = DimensionFilterMode.Select; - this.selectedValues = dimensionValues; - return this.buildExpression(); - } - - if (isExclusiveFilter) { - this.selectedValues = dimensionValues; - return this.buildExpression(); + let initMode: DimensionFilterMode = DimensionFilterMode.Select; + let initSelectedValues: string[] = []; + let initInputText: string = ""; + let initExclude: boolean = false; + + const op = initExpr.cond?.op; + if (op === V1Operation.OPERATION_IN || op === V1Operation.OPERATION_NIN) { + initMode = isInList + ? DimensionFilterMode.InList + : DimensionFilterMode.Select; + initSelectedValues = getValuesInExpression(initExpr); + initExclude = op === V1Operation.OPERATION_NIN; + } else if ( + op === V1Operation.OPERATION_LIKE || + op === V1Operation.OPERATION_NLIKE + ) { + initMode = DimensionFilterMode.Contains; + initInputText = initExpr.cond?.exprs?.[1]?.val?.toString?.() ?? ""; + initExclude = op === V1Operation.OPERATION_NLIKE; } - dimensionValues.forEach((v) => { - const removedIndex = this.toggleValue(v, false); - if (removedIndex === -1) return; + this.mode = $state(initMode); + this.oldMode = initMode; + this.selectedValues = $state(initSelectedValues); + this.inputText = $state(initInputText); + this.exclude = $state(initExclude); + this.pinned = $state(false); + this.required = $state(false); + this.commit(); + } - // TODO: decrement pinIndex if the removed value was before the pinned value - }); + public clone() { + return new DimensionFilterManager( + this.name, + this.label, + this.dimensions, + this.editing, + this.expr, + this.mode === DimensionFilterMode.InList, + ); + } - this.buildExpression(); + public setSelectedValues(dimensionValues: string[], exclude: boolean) { + this.mode = DimensionFilterMode.Select; + this.selectedValues = dimensionValues; + this.inputText = ""; + this.exclude = exclude; + this.commit(); } public toggleValue(dimensionValue: string, isExclusiveFilter: boolean) { const inIdx = this.selectedValues.findIndex((v) => v === dimensionValue); - let retIdx = inIdx; if (inIdx === -1) { if (isExclusiveFilter) { this.selectedValues = [dimensionValue]; - retIdx = -1; } else { this.selectedValues = [...this.selectedValues, dimensionValue]; } } else { this.selectedValues = this.selectedValues.splice(inIdx, 1); } - - return retIdx; + this.commit(); } - public setInList(values: string[], exclude?: boolean) { - if (exclude !== undefined) this.exclude = exclude; - + public setInList(values: string[], exclude: boolean) { this.mode = DimensionFilterMode.InList; this.selectedValues = values; this.inputText = ""; - this.buildExpression(); + this.exclude = exclude; + this.commit(); } - public setContainsText(searchText: string, exclude?: boolean) { - if (exclude !== undefined) this.exclude = exclude; - + public setContainsText(searchText: string, exclude: boolean) { this.mode = DimensionFilterMode.Contains; this.selectedValues = []; this.inputText = searchText; - this.buildExpression(); + this.exclude = exclude; + this.commit(); } public toggleExclude() { this.exclude = !this.exclude; + this.commit(); } public togglePinned() { @@ -134,12 +139,22 @@ export class DimensionFilterManager { this.selectedValues = []; this.inputText = ""; this.expr = undefined; - this.buildExpression(); + this.commit(); } - private buildExpression() { + public commit() { switch (this.mode) { case DimensionFilterMode.Select: + if (this.oldMode !== DimensionFilterMode.Select) { + eventBus.emit("notification", { + message: "Converted filter type to Select", + link: { + text: m.common_undo(), + href: page.url.href, + }, + }); + } + // eslint-disable-next-line no-fallthrough case DimensionFilterMode.InList: this.expr = this.selectedValues.length ? createInExpression(this.name, this.selectedValues, this.exclude) @@ -152,5 +167,6 @@ export class DimensionFilterManager { : undefined; break; } + this.oldMode = this.mode; } } diff --git a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts index 06f716ceba27..d0b4fc020e70 100644 --- a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts @@ -3,12 +3,10 @@ import { type DimensionFilterManagerInit, } from "@rilldata/web-common/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts"; import type { V1Expression } from "@rilldata/web-admin/client"; -import { - convertExpressionToFilterParam, - convertFilterParamToExpression, -} from "@rilldata/web-common/features/dashboards/url-state/filters/converters.ts"; +import { convertFilterParamToExpression } from "@rilldata/web-common/features/dashboards/url-state/filters/converters.ts"; import { createAndExpression, + createInExpression, forEachIdentifier, getValuesInExpression, } from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; @@ -19,7 +17,7 @@ import { import { getDimensionDisplayName } from "@rilldata/web-common/features/dashboards/filters/getDisplayName.ts"; import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants.ts"; -export class FilterManagerManager { +export class ExpressionFilterManager { public dimensionFilterManagers: DimensionFilterManager[] = $state([]); public expr: V1Expression; public exprParam: string = ""; @@ -59,39 +57,15 @@ export class FilterManagerManager { if (!dim) return; addedDimension.add(ident); - const op = e.cond?.op; - let init: DimensionFilterManagerInit | undefined = undefined; - if (op === V1Operation.OPERATION_IN || op === V1Operation.OPERATION_NIN) { - const isInListMode = dimensionsWithInlistFilter.includes(ident); - init = { - mode: isInListMode - ? DimensionFilterMode.InList - : DimensionFilterMode.Select, - selectedValues: getValuesInExpression(e), - exclude: op === V1Operation.OPERATION_NIN, - inputText: undefined, - }; - } else if ( - op === V1Operation.OPERATION_LIKE || - op === V1Operation.OPERATION_NLIKE - ) { - init = { - mode: DimensionFilterMode.Contains, - selectedValues: [], - inputText: e.cond?.exprs?.[1]?.val?.toString?.() ?? "", - exclude: op === V1Operation.OPERATION_NLIKE, - }; - } - - if (!init) return; - + const isInListMode = dimensionsWithInlistFilter.includes(ident); newDimensionFilterManagers.push( new DimensionFilterManager( ident, getDimensionDisplayName(dim), new Map([[metricsViewName, dim]]), false, - init, + e, + isInListMode, ), ); }); @@ -120,7 +94,6 @@ export class FilterManagerManager { getDimensionDisplayName(dim), new Map([[metricsViewName, dim]]), false, - { mode: DimensionFilterMode.Select }, ); this.dimensionFilterManagers = [ ...this.dimensionFilterManagers, @@ -131,6 +104,12 @@ export class FilterManagerManager { public clear() { this.dimensionFilterManagers = []; this.temporaryFilter = undefined; - console.log("clear", [...this.dimensionFilterManagers]); + } + + public getOtherDimensionsFilter(name: string) { + const exprs = this.dimensionFilterManagers + .filter((dfm) => !!dfm.expr && dfm.name !== name) + .map((d) => d.expr as V1Expression); + return exprs.length === 0 ? undefined : createAndExpression(exprs); } } diff --git a/web-common/src/features/dashboards/filters/manager/queries.svelte.ts b/web-common/src/features/dashboards/filters/manager/queries.svelte.ts new file mode 100644 index 000000000000..9c4762f4e68a --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/queries.svelte.ts @@ -0,0 +1,172 @@ +import { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; +import { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts"; +import type { DimensionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts"; +import { createQuery } from "@tanstack/svelte-query"; +import { + getQueryServiceMetricsViewAggregationQueryOptions, + V1BuiltinMeasure, + type V1Expression, +} from "@rilldata/web-common/runtime-client"; +import { + createAndExpression, + createInExpression, + createLikeExpression, +} from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; +import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants.ts"; + +type DimensionSearchArgs = { + mode: DimensionFilterMode; + searchText: string; + values: string[]; + timeStart?: string; + timeEnd?: string; + timeDimension?: string; + enabled?: boolean; +}; + +export function getDimensionSearchQuery( + client: RuntimeClient, + manager: ExpressionFilterManager, + dimensionManager: DimensionFilterManager, + { + mode, + searchText, + values, + timeStart, + timeEnd, + timeDimension, + enabled, + }: DimensionSearchArgs, +) { + const dimensionName = dimensionManager.name; + const mvName = [...dimensionManager.dimensions.keys()][0] ?? ""; + + const otherDimsFilter = $derived.by(() => { + manager.expr; // Force rederivation when expr changes. + return manager.getOtherDimensionsFilter(dimensionManager.name); + }); + const where = getFilterForSearchArgs(dimensionManager.name, { + mode, + searchText, + values, + additionalFilter: otherDimsFilter, + }); + + const optionsStore = $derived( + getQueryServiceMetricsViewAggregationQueryOptions( + client, + { + metricsView: mvName, + dimensions: [{ name: dimensionName }], + timeRange: { start: timeStart, end: timeEnd, timeDimension }, + limit: "250", + offset: "0", + sort: [{ name: dimensionName }], + where, + }, + { + query: { + select: (resp) => + resp.data?.map((d) => d[dimensionName] as string) ?? [], + enabled, + }, + }, + ), + ); + return createQuery(optionsStore); +} + +export function getAllSearchResultsCount( + client: RuntimeClient, + manager: ExpressionFilterManager, + dimensionManager: DimensionFilterManager, + { + mode, + searchText, + values, + timeStart, + timeEnd, + timeDimension, + enabled, + }: DimensionSearchArgs, +) { + const dimensionName = dimensionManager.name; + const mvName = [...dimensionManager.dimensions.keys()][0] ?? ""; + const countMeasureName = dimensionName + "__distinct_count"; + + const otherDimsFilter = $derived.by(() => { + manager.expr; // Force rederivation when expr changes. + return manager.getOtherDimensionsFilter(dimensionManager.name); + }); + const where = getFilterForSearchArgs(dimensionManager.name, { + mode, + searchText, + values, + additionalFilter: otherDimsFilter, + }); + const optionsStore = $derived( + getQueryServiceMetricsViewAggregationQueryOptions( + client, + { + metricsView: mvName, + measures: [ + { + name: countMeasureName, + builtinMeasure: V1BuiltinMeasure.BUILTIN_MEASURE_COUNT_DISTINCT, + builtinMeasureArgs: [dimensionName], + }, + ], + timeRange: { start: timeStart, end: timeEnd, timeDimension }, + where, + }, + { + query: { + enabled, + select: (resp) => { + if (!resp.data?.length) return 0; + return resp.data[0][countMeasureName] as number; + }, + }, + }, + ), + ); + return createQuery(optionsStore); +} + +/** + * Builds the filter for dimension search results or dimension search results count. + * Note the difference, this is for the search results from the search input. + * + * 1. For Select mode, while the final query is an `in` filter, the search results from the search input is a `like` filter. + * 2. For InList mode it is an `in` filter with all the selected values. + * 3. For Contains mode it is a `like` filter. + */ +function getFilterForSearchArgs( + dimensionName: string, + { + mode, + searchText, + values, + additionalFilter, + }: { + mode: DimensionFilterMode; + searchText: string; + values: string[]; + additionalFilter?: V1Expression; + }, +) { + let filter: V1Expression; + if (mode === DimensionFilterMode.InList) { + filter = createInExpression(dimensionName, values); + } else { + const addNull = searchText.length !== 0 && "null".includes(searchText); + filter = addNull + ? createInExpression(dimensionName, [null]) + : createLikeExpression(dimensionName, `%${searchText}%`); + } + + if (additionalFilter) { + return createAndExpression([filter, additionalFilter]); + } + return filter; +} From 8307b26b02f608f606440500bdbb11135d63ff4f Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Thu, 23 Jul 2026 19:01:36 +0530 Subject: [PATCH 3/5] Add measure filters --- .../filters/manager/DimensionFilter.svelte | 204 ++++++++++-------- .../filters/manager/ExpressionFilters.svelte | 17 +- .../filters/manager/MeasureFilter.svelte | 147 +++++++++++++ .../dimension-filter-manager.svelte.ts | 9 +- .../expression-filter-manager.svelte.ts | 120 ++++++++--- .../manager/measure-filter-manager.svelte.ts | 90 ++++++++ .../store-utils/url-params-state.svelte.ts | 5 +- 7 files changed, 471 insertions(+), 121 deletions(-) create mode 100644 web-common/src/features/dashboards/filters/manager/MeasureFilter.svelte create mode 100644 web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts diff --git a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte index 60824566723c..be51f15bc230 100644 --- a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte +++ b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte @@ -31,54 +31,65 @@ } from "@rilldata/web-common/features/dashboards/filters/manager/queries.svelte.ts"; import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts"; - export let manager: ExpressionFilterManager; - export let dimensionManager: DimensionFilterManager; - export let openOnMount: boolean = true; - export let readOnly: boolean = false; - export let timeStart: string | undefined; - export let timeEnd: string | undefined; - export let timeDimension: string | undefined = undefined; - export let timeControlsReady: boolean | undefined; - export let smallChip = false; - export let side: "top" | "right" | "bottom" | "left" = "bottom"; - export let isUrlTooLongAfterInListFilter: ( - values: string[], - ) => boolean = () => false; + let { + manager, + dimensionManager, + openOnMount = true, + readOnly = false, + timeStart, + timeEnd, + timeDimension = undefined, + timeControlsReady, + smallChip = false, + side = "bottom", + isUrlTooLongAfterInListFilter = () => false, + }: { + manager: ExpressionFilterManager; + dimensionManager: DimensionFilterManager; + openOnMount?: boolean; + readOnly?: boolean; + timeStart: string | undefined; + timeEnd: string | undefined; + timeDimension: string | undefined; + timeControlsReady: boolean | undefined; + smallChip?: boolean; + side?: "top" | "right" | "bottom" | "left"; + isUrlTooLongAfterInListFilter?: (values: string[]) => boolean; + } = $props(); let proxyDimensionManager = $derived(dimensionManager.clone()); - let open = + let open = $state( + // eslint-disable-next-line svelte/valid-compile openOnMount && - !dimensionManager.selectedValues?.length && - !dimensionManager.inputText; - let curSearchText = dimensionManager.inputText ?? ""; - let inListTooLong = false; + // eslint-disable-next-line svelte/valid-compile + !dimensionManager.selectedValues?.length && + // eslint-disable-next-line svelte/valid-compile + !dimensionManager.inputText, + ); + // eslint-disable-next-line svelte/valid-compile + let curSearchText = $state(dimensionManager.inputText ?? ""); + let inListTooLong = $state(false); const client = useRuntimeClient(); - $: missingRequired = Boolean( - dimensionManager.pinned && !dimensionManager.expr, + let missingRequired = $derived( + Boolean(dimensionManager.pinned && !dimensionManager.expr), ); - $: sanitisedSearchText = dimensionManager.inputText - ?.replace(/^%/, "") - .replace(/%$/, ""); + $effect(() => checkSearchText(curSearchText)); - $: checkSearchText(curSearchText); - - $: enableSearchQuery = + let enableSearchQuery = $derived( Boolean(timeControlsReady && open) && - (proxyDimensionManager.mode === DimensionFilterMode.Select || - (proxyDimensionManager.mode === DimensionFilterMode.Contains && - curSearchText.length > 0) || - (proxyDimensionManager.mode === DimensionFilterMode.InList && - proxyDimensionManager.selectedValues.length > 0)); + (proxyDimensionManager.mode === DimensionFilterMode.Select || + (proxyDimensionManager.mode === DimensionFilterMode.Contains && + curSearchText.length > 0) || + (proxyDimensionManager.mode === DimensionFilterMode.InList && + proxyDimensionManager.selectedValues.length > 0)), + ); - $: searchResultsQuery = getDimensionSearchQuery( - client, - manager, - dimensionManager, - { + let searchResultsQuery = $derived( + getDimensionSearchQuery(client, manager, dimensionManager, { mode: proxyDimensionManager.mode, values: proxyDimensionManager.selectedValues, searchText: curSearchText, @@ -86,27 +97,25 @@ timeEnd, timeDimension, enabled: enableSearchQuery, - }, + }), ); - $: ({ + let { data: searchResults, error: errorFromSearchResults, isFetching: isFetchingFromSearchResults, - } = $searchResultsQuery); - $: correctedSearchResults = enableSearchQuery ? searchResults : []; + } = $derived($searchResultsQuery); + let correctedSearchResults = $derived(enableSearchQuery ? searchResults : []); - $: enableSearchCountQuery = + let enableSearchCountQuery = $derived( Boolean(timeControlsReady) && - ((proxyDimensionManager.mode === DimensionFilterMode.Contains && - curSearchText.length > 0) || - (proxyDimensionManager.mode === DimensionFilterMode.InList && - proxyDimensionManager.selectedValues.length > 0)); + ((proxyDimensionManager.mode === DimensionFilterMode.Contains && + curSearchText.length > 0) || + (proxyDimensionManager.mode === DimensionFilterMode.InList && + proxyDimensionManager.selectedValues.length > 0)), + ); - $: allSearchResultsCountQuery = getAllSearchResultsCount( - client, - manager, - dimensionManager, - { + let allSearchResultsCountQuery = $derived( + getAllSearchResultsCount(client, manager, dimensionManager, { mode: proxyDimensionManager.mode, values: proxyDimensionManager.selectedValues, searchText: curSearchText, @@ -114,52 +123,69 @@ timeEnd, timeDimension, enabled: enableSearchCountQuery, - }, + }), ); - $: ({ + let { data: allSearchResultsCount, error: errorFromAllSearchResultsCount, isFetching: isFetchingFromAllSearchResultsCount, - } = $allSearchResultsCountQuery); - $: searchResultCountText = enableSearchCountQuery - ? proxyDimensionManager.mode === DimensionFilterMode.Contains - ? `${allSearchResultsCount} results` - : `${allSearchResultsCount} of ${proxyDimensionManager.selectedValues.length} matched` - : "0 results"; + } = $derived($allSearchResultsCountQuery); + let searchResultCountText = $derived( + enableSearchCountQuery + ? proxyDimensionManager.mode === DimensionFilterMode.Contains + ? `${allSearchResultsCount} results` + : `${allSearchResultsCount} of ${proxyDimensionManager.selectedValues.length} matched` + : "0 results", + ); - $: searchPlaceholder = getSearchPlaceholder(proxyDimensionManager.mode); + let searchPlaceholder = $derived( + getSearchPlaceholder(proxyDimensionManager.mode), + ); - $: error = errorFromSearchResults ?? errorFromAllSearchResultsCount; - $: isFetching = - isFetchingFromSearchResults ?? isFetchingFromAllSearchResultsCount; + let error = $derived( + errorFromSearchResults ?? errorFromAllSearchResultsCount, + ); + let isFetching = $derived( + isFetchingFromSearchResults ?? isFetchingFromAllSearchResultsCount, + ); - $: showExtraInfo = proxyDimensionManager.mode !== DimensionFilterMode.Select; // || curSearchText.length > 0; (Add once we have docs) + let showExtraInfo = $derived( + proxyDimensionManager.mode !== DimensionFilterMode.Select, + ); // || curSearchText.length > 0; (Add once we have docs) - $: allSelected = Boolean( - effectiveSelectedValues.length && - correctedSearchResults?.length === effectiveSelectedValues.length, + let effectiveSelectedValues = $derived( + getEffectiveSelectedValues( + proxyDimensionManager.mode, + proxyDimensionManager.selectedValues, + correctedSearchResults ?? [], + dimensionManager.selectedValues, + ), ); - $: effectiveSelectedValues = getEffectiveSelectedValues( - proxyDimensionManager.mode, - proxyDimensionManager.selectedValues, - correctedSearchResults ?? [], - dimensionManager.selectedValues, + let allSelected = $derived( + Boolean( + effectiveSelectedValues.length && + correctedSearchResults?.length === effectiveSelectedValues.length, + ), ); - $: disableApplyButton = shouldDisableApplyButton( - proxyDimensionManager.mode, - enableSearchCountQuery, - inListTooLong, + let disableApplyButton = $derived( + shouldDisableApplyButton( + proxyDimensionManager.mode, + enableSearchCountQuery, + inListTooLong, + ), ); // Split results into checked and unchecked for better UX (like SelectionDropdown) // Use actual selectedValues (not proxy) so items only sort after dropdown closes - $: ({ checkedItems, uncheckedItems } = getItemLists( - proxyDimensionManager.mode, - correctedSearchResults ?? [], - dimensionManager.selectedValues, - curSearchText, - )); + let { checkedItems, uncheckedItems } = $derived( + getItemLists( + proxyDimensionManager.mode, + correctedSearchResults ?? [], + dimensionManager.selectedValues, + curSearchText, + ), + ); function checkSearchText(inputText: string) { inListTooLong = false; @@ -217,7 +243,7 @@ curSearchText = dimensionManager.mode === DimensionFilterMode.InList ? mergeDimensionSearchValues(dimensionManager.selectedValues) - : (sanitisedSearchText ?? ""); + : dimensionManager.inputText; } else { // Apply proxy changes for Select mode when dropdown closes if (proxyDimensionManager.mode === DimensionFilterMode.Select) { @@ -417,8 +443,7 @@ {@const selected = effectiveSelectedValues.includes(name)} {@const label = name ?? "null"} - - + {/each} {/if} @@ -445,11 +470,12 @@ {#each uncheckedItems as name (name)} {@const selected = effectiveSelectedValues.includes(name)} {@const label = name ?? "null"} - - diff --git a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte index d1622c79f2bd..788b4ffdf8ae 100644 --- a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte +++ b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte @@ -9,6 +9,7 @@ import { useTimeControlStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store.ts"; import FilterButton from "@rilldata/web-common/features/dashboards/filters/FilterButton.svelte"; import { Button } from "@rilldata/web-common/components/button"; + import MeasureFilter from "@rilldata/web-common/features/dashboards/filters/manager/MeasureFilter.svelte"; const StateManagers = getStateManagers(); const { @@ -18,7 +19,7 @@ selectors: { dimensions: { allDimensions }, dimensionFilters: { dimensionHasFilter }, - measures: { filteredSimpleMeasures }, + measures: { allMeasures, filteredSimpleMeasures }, measureFilters: { measureHasFilter }, }, validSpecStore, @@ -28,6 +29,10 @@ let validExplore = $derived($validSpecStore.data?.explore ?? {}); let metricsViewName = $derived(validExplore.metricsView ?? ""); + let measureIdMap = $derived( + getMapFromArray($allMeasures, (measure) => measure.name as string), + ); + let dimensionIdMap = $derived( getMapFromArray( $allDimensions, @@ -43,6 +48,7 @@ $effect(() => manager.setExprParam( filterUrlStore.value ?? "", + measureIdMap, dimensionIdMap, metricsViewName, ), @@ -70,6 +76,15 @@ {timeEnd} {timeControlsReady} timeDimension={selectedTimeDimension} + openOnMount={manager.temporaryFilter === dimensionManager} + /> + {/each} + + {#each manager.measureFilterManagers as measureManager (measureManager.name)} + {/each} diff --git a/web-common/src/features/dashboards/filters/manager/MeasureFilter.svelte b/web-common/src/features/dashboards/filters/manager/MeasureFilter.svelte new file mode 100644 index 000000000000..2ba1aafdf36d --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/MeasureFilter.svelte @@ -0,0 +1,147 @@ + + + { + if (!open) return; + if (measureManager.pinned !== curPinned) { + measureManager.togglePinned(); + } + if (measureManager.required !== curRequired) { + measureManager.toggleRequired(); + } + }} +> + + {#snippet child({ props })} + + measureManager.clear()} + removable={!curPinned && !measureManager.required} + removeTooltipText={m.dashboard_remove_label({ + label: measureManager.label, + })} + > + { + return d.name === measureManager.dimension; + })?.displayName ?? ""} + {filter} + label={measureManager.label} + slot="body" + /> + +
+ + + {measureManager.name} + {measureManager.required + ? m.dashboard_required_measure() + : measureManager.label || ""} + + + {#if missingRequired} + {m.dashboard_filter_required_set_value()} + {:else} + + {m.dashboard_click_to_edit_values()} + + {/if} + +
+
+ {/snippet} +
+ + {#if open} + onApply(dimension, filter)} + bind:pinned={curPinned} + bind:required={curRequired} + showPinControl={measureManager.editing} + showRequiredControl={measureManager.editing} + {side} + /> + {/if} +
diff --git a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts index c16dfb2501f0..937ced2e78f8 100644 --- a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts @@ -8,7 +8,6 @@ import { } from "@rilldata/web-common/runtime-client"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts"; import { - copyFilterExpression, createInExpression, createLikeExpression, getValuesInExpression, @@ -58,7 +57,9 @@ export class DimensionFilterManager { op === V1Operation.OPERATION_NLIKE ) { initMode = DimensionFilterMode.Contains; - initInputText = initExpr.cond?.exprs?.[1]?.val?.toString?.() ?? ""; + initInputText = sanitizeSearchText( + initExpr.cond?.exprs?.[1]?.val?.toString?.() ?? "", + ); initExclude = op === V1Operation.OPERATION_NLIKE; } @@ -170,3 +171,7 @@ export class DimensionFilterManager { this.oldMode = this.mode; } } + +export function sanitizeSearchText(searchText: string) { + return searchText.replace(/^%/, "").replace(/%$/, ""); +} diff --git a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts index d0b4fc020e70..a4952f1fa523 100644 --- a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts @@ -1,41 +1,46 @@ -import { - DimensionFilterManager, - type DimensionFilterManagerInit, -} from "@rilldata/web-common/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts"; +import { DimensionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts"; import type { V1Expression } from "@rilldata/web-admin/client"; import { convertFilterParamToExpression } from "@rilldata/web-common/features/dashboards/url-state/filters/converters.ts"; import { createAndExpression, - createInExpression, forEachIdentifier, - getValuesInExpression, } from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; import { type MetricsViewSpecDimension, - V1Operation, + type MetricsViewSpecMeasure, } from "@rilldata/web-common/runtime-client"; -import { getDimensionDisplayName } from "@rilldata/web-common/features/dashboards/filters/getDisplayName.ts"; -import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants.ts"; +import { + getDimensionDisplayName, + getMeasureDisplayName, +} from "@rilldata/web-common/features/dashboards/filters/getDisplayName.ts"; +import { MeasureFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/measure-filter-manager.svelte.ts"; export class ExpressionFilterManager { + public measureFilterManagers: MeasureFilterManager[] = $state([]); public dimensionFilterManagers: DimensionFilterManager[] = $state([]); + public temporaryFilter: + | MeasureFilterManager + | DimensionFilterManager + | undefined = $state(undefined); + public expr: V1Expression; public exprParam: string = ""; - private temporaryFilter: DimensionFilterManager | undefined = - $state(undefined); - public constructor() { this.expr = $derived.by(() => { - const exprs = this.dimensionFilterManagers + const dimExprs = this.dimensionFilterManagers .filter((dfm) => !!dfm.expr) .map((d) => d.expr as V1Expression); - return createAndExpression(exprs); + const mesExprs = this.measureFilterManagers + .filter((mfm) => !!mfm.expr) + .map((m) => m.expr as V1Expression); + return createAndExpression(dimExprs.concat(mesExprs)); }); } public setExprParam( exprParam: string, + measureIdMap: Map, dimensionIdMap: Map, metricsViewName: string, ) { @@ -47,30 +52,67 @@ export class ExpressionFilterManager { // TODO: check complex filter if (!expr) { this.dimensionFilterManagers = []; + this.measureFilterManagers = []; return; } + const addedMeasure = new Set(); + const newMeasureFilterManagers: MeasureFilterManager[] = []; + const addedDimension = new Set(); const newDimensionFilterManagers: DimensionFilterManager[] = []; + forEachIdentifier(expr, (e, ident) => { const dim = dimensionIdMap.get(ident); if (!dim) return; - addedDimension.add(ident); - - const isInListMode = dimensionsWithInlistFilter.includes(ident); - newDimensionFilterManagers.push( - new DimensionFilterManager( - ident, - getDimensionDisplayName(dim), - new Map([[metricsViewName, dim]]), - false, - e, - isInListMode, - ), - ); + + const firstValueExpr = e?.cond?.exprs?.[1]; + + if (firstValueExpr?.subquery) { + const measureName = firstValueExpr.subquery.measures?.[0]; + if (!measureName) return; + + const measure = measureIdMap.get(measureName); + if (!measure) return; + + addedMeasure.add(measureName); + + newMeasureFilterManagers.push( + new MeasureFilterManager( + measureName, + getMeasureDisplayName(measure), + new Map([ + [metricsViewName, measure], + ]), + false, + ident, + firstValueExpr, + ), + ); + } else { + addedDimension.add(ident); + + const isInListMode = dimensionsWithInlistFilter.includes(ident); + newDimensionFilterManagers.push( + new DimensionFilterManager( + ident, + getDimensionDisplayName(dim), + new Map([[metricsViewName, dim]]), + false, + e, + isInListMode, + ), + ); + } }); - if (this.temporaryFilter) { + if (this.temporaryFilter instanceof MeasureFilterManager) { + if (!addedMeasure.has(this.temporaryFilter.name)) { + newMeasureFilterManagers.push(this.temporaryFilter); + } else { + this.temporaryFilter = undefined; + } + } else if (this.temporaryFilter instanceof DimensionFilterManager) { if (!addedDimension.has(this.temporaryFilter.name)) { newDimensionFilterManagers.push(this.temporaryFilter); } else { @@ -78,9 +120,30 @@ export class ExpressionFilterManager { } } + this.measureFilterManagers = newMeasureFilterManagers; this.dimensionFilterManagers = newDimensionFilterManagers; } + public addTemporaryMeasureFilter( + name: string, + measureIdMap: Map, + metricsViewName: string, + ) { + const measure = measureIdMap.get(name); + if (!measure) return; + + this.temporaryFilter = new MeasureFilterManager( + name, + getMeasureDisplayName(measure), + new Map([[metricsViewName, measure]]), + false, + ); + this.measureFilterManagers = [ + ...this.measureFilterManagers, + this.temporaryFilter, + ]; + } + public addTemporaryDimensionFilter( name: string, dimensionIdMap: Map, @@ -102,6 +165,7 @@ export class ExpressionFilterManager { } public clear() { + this.measureFilterManagers = []; this.dimensionFilterManagers = []; this.temporaryFilter = undefined; } diff --git a/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts new file mode 100644 index 000000000000..9adb9a28f75b --- /dev/null +++ b/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts @@ -0,0 +1,90 @@ +import type { + MetricsViewSpecMeasure, + V1Expression, +} from "@rilldata/web-common/runtime-client"; +import { + mapExprToMeasureFilter, + mapMeasureFilterToExpr, + type MeasureFilterEntry, +} from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-entry.ts"; +import { + MeasureFilterOperation, + MeasureFilterType, +} from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-options.ts"; +import { + createSubQueryExpression, + removeWrapperAndOrExpression, +} from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; + +export class MeasureFilterManager { + public expr: V1Expression | undefined = $state(undefined); + + public dimension: string; + public operation: MeasureFilterOperation; + public type: MeasureFilterType; + public value1: string; + public value2: string; + public pinned: boolean; + public required: boolean; + + public constructor( + public readonly name: string, + public readonly label: string, + public readonly measures: Map, + public readonly editing: boolean, // TODO: maybe separate editing, pinned & required? + initDimension: string = "", + initExpr: V1Expression | undefined = undefined, + ) { + const unwrappedHavingFilter = removeWrapperAndOrExpression( + initExpr?.subquery?.having, + ); + const mappedMeasureFilter = mapExprToMeasureFilter(unwrappedHavingFilter); + + this.dimension = $state(initDimension); + this.operation = $state( + mappedMeasureFilter?.operation ?? MeasureFilterOperation.LessThan, + ); + this.type = $state(mappedMeasureFilter?.type ?? MeasureFilterType.Value); + this.value1 = $state(mappedMeasureFilter?.value1 ?? ""); + this.value2 = $state(mappedMeasureFilter?.value2 ?? ""); + this.pinned = $state(false); + this.required = $state(false); + this.commit(); + } + + public apply(dimension: string, newFilter: MeasureFilterEntry) { + this.dimension = dimension; + this.operation = $state(newFilter.operation); + this.type = $state(newFilter.type); + this.value1 = $state(newFilter.value1); + this.value2 = $state(newFilter.value2); + this.commit(); + } + + public togglePinned() { + this.pinned = !this.pinned; + } + + public toggleRequired() { + this.required = !this.required; + } + + public clear() { + this.expr = undefined; + this.commit(); + } + + public commit() { + const measureFilterExpr = mapMeasureFilterToExpr({ + measure: this.name, + operation: this.operation, + type: this.type, + value1: this.value1, + value2: this.value2, + }); + const hasFilter = Boolean(this.dimension && measureFilterExpr); + this.expr = hasFilter + ? createSubQueryExpression(this.dimension, [this.name], measureFilterExpr) + : undefined; + } +} 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 b1b1cfa32dad..7c29a88096ab 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 @@ -76,6 +76,8 @@ function flushParams() { if (newParams.length === 0) return; const newUrl = new SvelteURL(page.url); + const curUrlSearch = page.url.search; + newParams.forEach(([key, value]) => { if (value === null) { newUrl.searchParams.delete(key); @@ -83,7 +85,8 @@ function flushParams() { newUrl.searchParams.set(key, value); } }); - newParams = []; + + if (curUrlSearch === newUrl.search) return; void goto(newUrl, { noScroll: true, keepFocus: true }); } From bc00a722661f1e580cbcbec94e84850cdc17280a Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Fri, 24 Jul 2026 16:42:14 +0530 Subject: [PATCH 4/5] Use the new manager across the explore --- .../GlobalDimensionSearchResults.svelte | 9 +- .../dimension-table/DimensionDisplay.svelte | 24 ++- .../dimension-table/DimensionHeader.svelte | 41 ++-- .../dimension-table/DimensionTable.svelte | 181 ++++++++++-------- .../dashboards/filters/Filters.svelte | 178 +---------------- .../dimension-filters/DimensionFilter.svelte | 4 +- .../filters/manager/DimensionFilter.svelte | 23 +-- .../filters/manager/ExpressionFilters.svelte | 101 ++++++---- .../dimension-filter-manager.svelte.ts | 25 ++- .../expression-filter-manager.svelte.ts | 75 ++++++-- .../manager/measure-filter-manager.svelte.ts | 8 +- .../leaderboard/LeaderboardDisplay.svelte | 14 +- .../actions/dimension-filters.ts | 112 +---------- .../loaders/DashboardStateManager.svelte | 5 + .../loaders/DashboardStateSync.ts | 17 +- .../selectors/dimension-filters.ts | 114 ----------- .../state-managers/state-managers.ts | 3 + .../time-dimension-details/TDDHeader.svelte | 99 ++++++---- .../TimeDimensionDisplay.svelte | 144 ++++++++------ .../MetricsTimeSeriesCharts.svelte | 9 +- .../dashboards/workspace/Dashboard.svelte | 4 - .../store-utils/url-params-state.svelte.ts | 4 - 22 files changed, 489 insertions(+), 705 deletions(-) diff --git a/web-common/src/features/dashboards/dimension-search/GlobalDimensionSearchResults.svelte b/web-common/src/features/dashboards/dimension-search/GlobalDimensionSearchResults.svelte index ed56ae48532e..35c9e5de1daa 100644 --- a/web-common/src/features/dashboards/dimension-search/GlobalDimensionSearchResults.svelte +++ b/web-common/src/features/dashboards/dimension-search/GlobalDimensionSearchResults.svelte @@ -19,12 +19,10 @@ export let open: boolean; const { - actions: { - dimensionsFilter: { toggleDimensionValueSelection }, - }, timeRangeSummaryStore, metricsViewName, validSpecStore, + expressionFilterManager, } = getStateManagers(); const client = useRuntimeClient(); @@ -49,7 +47,10 @@ function onItemSelect(dimension: string, value: any) { onSelect(); - toggleDimensionValueSelection(dimension, value, false, true); + expressionFilterManager.dimensionFilterAction( + dimension, + (dimensionManager) => dimensionManager.toggleValue(value, false), + ); } diff --git a/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte b/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte index a780b6bb6b61..6d8bc32e24d9 100644 --- a/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte +++ b/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte @@ -47,7 +47,6 @@ const { selectors: { - dimensionFilters: { unselectedDimensionValues }, dimensionTable: { virtualizedTableColumns, prepareDimTableRows }, sorting: { sortedAscending, sortType }, leaderboard: { @@ -55,15 +54,9 @@ leaderboardSortByMeasureName, }, }, - actions: { - dimensionsFilter: { - toggleDimensionValueSelection, - selectItemsInFilter, - deselectItemsInFilter, - }, - }, dashboardStore, validSpecStore, + expressionFilterManager, } = getStateManagers(); $: metricsViewSpec = $validSpecStore.data?.metricsView ?? {}; @@ -200,25 +193,30 @@ function onSelectItem(data: { index: number; meta: boolean }) { const label = tableRows[data.index][dimensionName] as string; - toggleDimensionValueSelection(dimensionName, label, false, data.meta); + expressionFilterManager.dimensionFilterAction( + dimensionName, + (dimensionManager) => dimensionManager.toggleValue(label, false), + ); } function toggleAllSearchItems() { const labels = tableRows.map((row) => row[dimensionName] as string); if (areAllTableRowsSelected) { - deselectItemsInFilter(dimensionName, labels); + expressionFilterManager.dimensionFilterAction( + dimensionName, + (dimensionManager) => dimensionManager.removeSelectedValues(labels), + ); eventBus.emit("notification", { message: `Removed ${labels.length} items from filter`, }); return; } else { - const newValuesSelected = $unselectedDimensionValues( + const newValuesSelected = expressionFilterManager.dimensionFilterAction( dimensionName, - labels, + (dimensionManager) => dimensionManager.appendSelectedValues(labels), ); - selectItemsInFilter(dimensionName, labels); eventBus.emit("notification", { message: `Added ${newValuesSelected.length} items to filter`, }); diff --git a/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte b/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte index 5efabf64269d..d9d5266ac715 100644 --- a/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte +++ b/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte @@ -18,39 +18,51 @@ import StartPivotButton from "../toolbars/StartPivotButton.svelte"; import { getDimensionTableExportQuery } from "./dimension-table-export"; - export let dimensionName: string; - export let areAllTableRowsSelected = false; - export let isRowsEmpty = true; - export let searchText: string; - export let hideStartPivotButton = false; - export let onToggleSearchItems: () => void; + let { + dimensionName, + areAllTableRowsSelected, + isRowsEmpty, + searchText = $bindable(""), + hideStartPivotButton, + onToggleSearchItems, + }: { + dimensionName: string; + areAllTableRowsSelected: boolean; + isRowsEmpty: boolean; + searchText: string; + hideStartPivotButton: boolean; + onToggleSearchItems: () => void; + } = $props(); const stateManagers = getStateManagers(); const { selectors: { sorting: { sortedByDimensionValue }, dimensions: { getDimensionDisplayName }, - dimensionFilters: { isFilterExcludeMode }, measures: { visibleMeasures }, }, actions: { sorting: { toggleSort }, dimensions: { setPrimaryDimension }, - dimensionsFilter: { toggleDimensionFilterMode }, leaderboard: { toggleLeaderboardShowContextForAllMeasures }, }, timeRangeSummaryStore, dashboardStore, exploreName, + expressionFilterManager, } = stateManagers; const { adminServer, exports } = featureFlags; - $: exploreHasTimeDimension = !!$timeRangeSummaryStore.data; + let exploreHasTimeDimension = $derived(!!$timeRangeSummaryStore.data); - $: excludeMode = $isFilterExcludeMode(dimensionName); + let excludeMode = $derived( + expressionFilterManager.dimensionFilterManagers.find( + (dfm) => dfm.name === dimensionName, + )?.exclude ?? false, + ); - let isLeaderboardActionsOpen = false; + let isLeaderboardActionsOpen = $state(false); function resetSearchText() { searchText = ""; @@ -72,10 +84,13 @@ setPrimaryDimension(""); }; function toggleFilterMode() { - toggleDimensionFilterMode(dimensionName); + expressionFilterManager.dimensionFilterAction( + dimensionName, + (dimensionManager) => dimensionManager.toggleExclude(), + ); } - let showReplacePivotModal = false; + let showReplacePivotModal = $state(false); function startPivotForDimensionTable() { const pivot = $dashboardStore?.pivot; diff --git a/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte b/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte index ef722326dafb..6e76a01f64a3 100644 --- a/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte +++ b/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte @@ -1,4 +1,4 @@ - - {#if hasFilters} - - {/if} - {/if} -
-
+
diff --git a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte index c26e3a789511..87fe704e2545 100644 --- a/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte +++ b/web-common/src/features/dashboards/filters/dimension-filters/DimensionFilter.svelte @@ -303,8 +303,8 @@ } } - function handleToggleExcludeMode(checked: boolean) { - curExcludeMode = checked; + function handleToggleExcludeMode() { + curExcludeMode = !curExcludeMode; excludeModeDirty = true; } diff --git a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte index be51f15bc230..19a6719fa696 100644 --- a/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte +++ b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte @@ -30,6 +30,7 @@ getDimensionSearchQuery, } from "@rilldata/web-common/features/dashboards/filters/manager/queries.svelte.ts"; import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts"; + import { onMount } from "svelte"; let { manager, @@ -59,16 +60,8 @@ let proxyDimensionManager = $derived(dimensionManager.clone()); - let open = $state( - // eslint-disable-next-line svelte/valid-compile - openOnMount && - // eslint-disable-next-line svelte/valid-compile - !dimensionManager.selectedValues?.length && - // eslint-disable-next-line svelte/valid-compile - !dimensionManager.inputText, - ); - // eslint-disable-next-line svelte/valid-compile - let curSearchText = $state(dimensionManager.inputText ?? ""); + let open = $state(false); + let curSearchText = $state(""); let inListTooLong = $state(false); const client = useRuntimeClient(); @@ -277,10 +270,18 @@ function handleItemClick(value: string) { proxyDimensionManager.toggleValue(value, false); } + + onMount(() => { + open = + openOnMount && + !dimensionManager.selectedValues?.length && + !dimensionManager.inputText; + curSearchText = dimensionManager.inputText; + }); { + onkeydown={(e) => { if (e.key === "Enter") onApply(); }} /> diff --git a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte index 788b4ffdf8ae..c33ab230dfd7 100644 --- a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte +++ b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte @@ -1,15 +1,22 @@
- {#each manager.dimensionFilterManagers as dimensionManager (dimensionManager.name)} - - {/each} + {#if !hasFilters} +
+ {m.dashboard_no_filters_selected()} +
+ {:else} + {#each expressionFilterManager.dimensionFilterManagers as dimensionManager (dimensionManager.name)} + + isUrlTooLongAfterInListFilter(dimensionManager.name, values) + : undefined} + /> + {/each} - {#each manager.measureFilterManagers as measureManager (measureManager.name)} - - {/each} + {#each expressionFilterManager.measureFilterManagers as measureManager (measureManager.name)} + + {/each} + {/if} - manager.addTemporaryDimensionFilter( - name, - dimensionIdMap, - metricsViewName, - )} + setTemporaryFilterName={handleAddNew} /> {#if hasFilters} - {/if} diff --git a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts index 937ced2e78f8..21b187cd163c 100644 --- a/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts @@ -13,13 +13,6 @@ import { getValuesInExpression, } from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts"; -export type DimensionFilterManagerInit = { - mode: DimensionFilterMode; - selectedValues: string[]; - inputText: string; - exclude: boolean; -}; - export class DimensionFilterManager { public expr: V1Expression | undefined = $state(undefined); @@ -102,11 +95,27 @@ export class DimensionFilterManager { this.selectedValues = [...this.selectedValues, dimensionValue]; } } else { - this.selectedValues = this.selectedValues.splice(inIdx, 1); + this.selectedValues = this.selectedValues.toSpliced(inIdx, 1); } this.commit(); } + public appendSelectedValues(dimensionValues: string[]) { + const newValues = dimensionValues.filter( + (v) => !this.selectedValues.includes(v), + ); + this.selectedValues = [...this.selectedValues, ...newValues]; + this.commit(); + return newValues; + } + + public removeSelectedValues(dimensionValues: string[]) { + this.selectedValues = this.selectedValues.filter( + (v) => !dimensionValues.includes(v), + ); + this.commit(); + } + public setInList(values: string[], exclude: boolean) { this.mode = DimensionFilterMode.InList; this.selectedValues = values; diff --git a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts index a4952f1fa523..dcc162fb7c54 100644 --- a/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts @@ -26,6 +26,10 @@ export class ExpressionFilterManager { public expr: V1Expression; public exprParam: string = ""; + private metricsViewName: string = ""; + private measureIdMap: Map = new Map(); + private dimensionIdMap: Map = new Map(); + public constructor() { this.expr = $derived.by(() => { const dimExprs = this.dimensionFilterManagers @@ -38,12 +42,17 @@ export class ExpressionFilterManager { }); } - public setExprParam( - exprParam: string, + public syncSpec( + metricsViewName: string, measureIdMap: Map, dimensionIdMap: Map, - metricsViewName: string, ) { + this.metricsViewName = metricsViewName; + this.measureIdMap = measureIdMap; + this.dimensionIdMap = dimensionIdMap; + } + + public setExprParam(exprParam: string) { if (exprParam === this.exprParam) return; this.exprParam = exprParam; @@ -63,7 +72,7 @@ export class ExpressionFilterManager { const newDimensionFilterManagers: DimensionFilterManager[] = []; forEachIdentifier(expr, (e, ident) => { - const dim = dimensionIdMap.get(ident); + const dim = this.dimensionIdMap.get(ident); if (!dim) return; const firstValueExpr = e?.cond?.exprs?.[1]; @@ -72,7 +81,7 @@ export class ExpressionFilterManager { const measureName = firstValueExpr.subquery.measures?.[0]; if (!measureName) return; - const measure = measureIdMap.get(measureName); + const measure = this.measureIdMap.get(measureName); if (!measure) return; addedMeasure.add(measureName); @@ -82,7 +91,7 @@ export class ExpressionFilterManager { measureName, getMeasureDisplayName(measure), new Map([ - [metricsViewName, measure], + [this.metricsViewName, measure], ]), false, ident, @@ -97,7 +106,9 @@ export class ExpressionFilterManager { new DimensionFilterManager( ident, getDimensionDisplayName(dim), - new Map([[metricsViewName, dim]]), + new Map([ + [this.metricsViewName, dim], + ]), false, e, isInListMode, @@ -124,18 +135,16 @@ export class ExpressionFilterManager { this.dimensionFilterManagers = newDimensionFilterManagers; } - public addTemporaryMeasureFilter( - name: string, - measureIdMap: Map, - metricsViewName: string, - ) { - const measure = measureIdMap.get(name); + public addTemporaryMeasureFilter(name: string) { + const measure = this.measureIdMap.get(name); if (!measure) return; this.temporaryFilter = new MeasureFilterManager( name, getMeasureDisplayName(measure), - new Map([[metricsViewName, measure]]), + new Map([ + [this.metricsViewName, measure], + ]), false, ); this.measureFilterManagers = [ @@ -144,18 +153,14 @@ export class ExpressionFilterManager { ]; } - public addTemporaryDimensionFilter( - name: string, - dimensionIdMap: Map, - metricsViewName: string, - ) { - const dim = dimensionIdMap.get(name); + public addTemporaryDimensionFilter(name: string) { + const dim = this.dimensionIdMap.get(name); if (!dim) return; this.temporaryFilter = new DimensionFilterManager( name, getDimensionDisplayName(dim), - new Map([[metricsViewName, dim]]), + new Map([[this.metricsViewName, dim]]), false, ); this.dimensionFilterManagers = [ @@ -164,6 +169,34 @@ export class ExpressionFilterManager { ]; } + public dimensionFilterAction( + name: string, + callback: (dimensionFilterManager: DimensionFilterManager) => any, + ) { + const dim = this.dimensionIdMap.get(name); + if (!dim) return; + + const existingManager = this.dimensionFilterManagers.find( + (dfm) => dfm.name === name, + ); + const dimensionFilterManager = + existingManager ?? + new DimensionFilterManager( + name, + getDimensionDisplayName(dim), + new Map([ + [this.metricsViewName, dim], + ]), + false, + ); + + const ret = callback(dimensionFilterManager); + if (!existingManager && dimensionFilterManager.expr) { + this.dimensionFilterManagers.push(dimensionFilterManager); + } + return ret; + } + public clear() { this.measureFilterManagers = []; this.dimensionFilterManagers = []; diff --git a/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts index 9adb9a28f75b..a213531b1795 100644 --- a/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts +++ b/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts @@ -54,10 +54,10 @@ export class MeasureFilterManager { public apply(dimension: string, newFilter: MeasureFilterEntry) { this.dimension = dimension; - this.operation = $state(newFilter.operation); - this.type = $state(newFilter.type); - this.value1 = $state(newFilter.value1); - this.value2 = $state(newFilter.value2); + this.operation = newFilter.operation; + this.type = newFilter.type; + this.value1 = newFilter.value1; + this.value2 = newFilter.value2; this.commit(); } diff --git a/web-common/src/features/dashboards/leaderboard/LeaderboardDisplay.svelte b/web-common/src/features/dashboards/leaderboard/LeaderboardDisplay.svelte index d8b0af4a7f86..d73fbd66c99a 100644 --- a/web-common/src/features/dashboards/leaderboard/LeaderboardDisplay.svelte +++ b/web-common/src/features/dashboards/leaderboard/LeaderboardDisplay.svelte @@ -35,7 +35,6 @@ measureTooltipFormatters, activeMeasureTooltipFormatter, }, - dimensionFilters: { isFilterExcludeMode }, dimensions: { visibleDimensions }, comparison: { isBeingCompared: isBeingComparedReadable }, sorting: { sortedAscending, sortType }, @@ -49,11 +48,11 @@ actions: { dimensions: { setPrimaryDimension }, sorting: { toggleSort }, - dimensionsFilter: { toggleDimensionValueSelection }, comparison: { toggleComparisonDimension }, }, exploreName, dashboardStore, + expressionFilterManager, } = StateManagers; const client = useRuntimeClient(); @@ -111,7 +110,9 @@ {dimensionColumnWidth} sortedAscending={$sortedAscending} sortType={$sortType} - filterExcludeMode={$isFilterExcludeMode(dimension.name)} + filterExcludeMode={expressionFilterManager.dimensionFilterManagers.find( + (dfm) => dfm.name === dimension.name, + )?.exclude ?? false} {comparisonTimeRange} {dimension} {parentElement} @@ -136,7 +137,12 @@ }} {setPrimaryDimension} {toggleSort} - {toggleDimensionValueSelection} + toggleDimensionValueSelection={(_1, value, _2, exclusive) => + expressionFilterManager.dimensionFilterAction( + dimension.name!, + (dimensionManager) => + dimensionManager.toggleValue(value, exclusive ?? false), + )} {toggleComparisonDimension} measureLabel={$measureLabel} onDimensionColumnResize={dimensionColumn.set} diff --git a/web-common/src/features/dashboards/state-managers/actions/dimension-filters.ts b/web-common/src/features/dashboards/state-managers/actions/dimension-filters.ts index e7a0c1a067f6..03201faec7bb 100644 --- a/web-common/src/features/dashboards/state-managers/actions/dimension-filters.ts +++ b/web-common/src/features/dashboards/state-managers/actions/dimension-filters.ts @@ -1,11 +1,9 @@ import { page } from "$app/stores"; import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; -import { splitWhereFilter } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-utils"; import { createInExpression, createLikeExpression, getValuesInExpression, - negateExpression, } from "@rilldata/web-common/features/dashboards/stores/filter-utils"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; import { @@ -171,28 +169,6 @@ export function applyDimensionContainsMode( } } -export function toggleDimensionFilterMode( - { dashboard }: DashboardMutables, - dimensionName: string, -) { - const exclude = dashboard.dimensionFilterExcludeMode.get(dimensionName); - dashboard.dimensionFilterExcludeMode.set(dimensionName, !exclude); - - if (!dashboard.whereFilter?.cond?.exprs) { - return; - } - - const exprIdx = dashboard.whereFilter.cond.exprs.findIndex( - (e) => e.cond?.exprs?.[0].ident === dimensionName, - ); - if (exprIdx === -1) { - return; - } - dashboard.whereFilter.cond.exprs[exprIdx] = negateExpression( - dashboard.whereFilter.cond.exprs[exprIdx], - ); -} - export function removeDimensionFilter( { dashboard }: DashboardMutables, dimensionName: string, @@ -207,74 +183,6 @@ export function removeDimensionFilter( dashboard.whereFilter?.cond?.exprs?.splice(exprIdx, 1); } -export function selectItemsInFilter( - { dashboard }: DashboardMutables, - dimensionName: string, - values: (string | null)[], -) { - const isExclude = !!dashboard.dimensionFilterExcludeMode.get(dimensionName); - const exprIdx = getWhereFilterExpressionIndex({ dashboard })(dimensionName); - if (exprIdx === undefined || exprIdx === -1) { - dashboard.whereFilter.cond?.exprs?.push( - createInExpression(dimensionName, values, isExclude), - ); - return; - } - - const expr = dashboard.whereFilter.cond?.exprs?.[exprIdx]; - if (!expr?.cond?.exprs) { - // should never happen since getWhereFilterExpressionIndex runs a find - return; - } - - // preserve old selections and add only new ones - const oldValues = getValuesInExpression(expr); - const newValues = values.filter((v) => !oldValues.includes(v)); - // newValuesSelected = newValues.length; // TODO - expr.cond.exprs.push(...newValues.map((v): V1Expression => ({ val: v }))); -} - -export function deselectItemsInFilter( - { dashboard }: DashboardMutables, - dimensionName: string, - values: (string | null)[], -) { - const exprIdx = getWhereFilterExpressionIndex({ dashboard })(dimensionName); - if (exprIdx === undefined || exprIdx === -1) { - return; - } - - const expr = dashboard.whereFilter.cond?.exprs?.[exprIdx]; - if (!expr?.cond?.exprs) { - // should never happen since getWhereFilterExpressionIndex runs a find - return; - } - - // remove only deselected values - const oldValues = getValuesInExpression(expr); - const newValues = oldValues.filter((v) => !values.includes(v)); - - if (newValues.length) { - expr.cond.exprs.splice( - 1, - expr.cond.exprs.length - 1, - ...newValues.map((v): V1Expression => ({ val: v })), - ); - } else { - dashboard.whereFilter.cond?.exprs?.splice(exprIdx, 1); - } -} - -export function setFilters( - { dashboard }: DashboardMutables, - filter: V1Expression, -) { - const { dimensionFilters, dimensionThresholdFilters } = - splitWhereFilter(filter); - dashboard.whereFilter = dimensionFilters; - dashboard.dimensionThresholdFilters = dimensionThresholdFilters; -} - export function toggleDimensionFilterValue( expr: V1Expression, dimensionValue: string | null, @@ -303,22 +211,4 @@ export function toggleDimensionFilterValue( return inIdx; } -export const dimensionFilterActions = { - /** - * Toggles whether the given dimension value is selected in the - * dimension filter for the given dimension. - * - * Note that this is different than the include/exclude mode for - * dimension filters. This is a toggle for a specific value, whereas - * the include/exclude mode is a toggle for the entire dimension. - */ - toggleDimensionValueSelection, - toggleMultipleDimensionValueSelections, - applyDimensionInListMode, - applyDimensionContainsMode, - toggleDimensionFilterMode, - removeDimensionFilter, - selectItemsInFilter, - deselectItemsInFilter, - setFilters, -}; +export const dimensionFilterActions = {}; diff --git a/web-common/src/features/dashboards/state-managers/loaders/DashboardStateManager.svelte b/web-common/src/features/dashboards/state-managers/loaders/DashboardStateManager.svelte index 14c17a68a992..e8cc303f4e28 100644 --- a/web-common/src/features/dashboards/state-managers/loaders/DashboardStateManager.svelte +++ b/web-common/src/features/dashboards/state-managers/loaders/DashboardStateManager.svelte @@ -22,6 +22,7 @@ import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { onDestroy } from "svelte"; import { clearExploreSessionStore } from "@rilldata/web-common/features/dashboards/state-managers/loaders/explore-web-view-store.ts"; + import { getStateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers.ts"; export let exploreName: string; export let storageNamespacePrefix: string | undefined = undefined; @@ -33,6 +34,9 @@ const client = useRuntimeClient(); + const StateManagers = getStateManagers(); + const { expressionFilterManager } = StateManagers; + $: exploreSpecQuery = useExploreValidSpec(client, exploreName); $: exploreSpec = $exploreSpecQuery.data?.explore ?? {}; $: metricsViewName = exploreSpec?.metricsView ?? ""; @@ -56,6 +60,7 @@ exploreName, storageNamespacePrefix, dataLoader, + expressionFilterManager, ); } diff --git a/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts b/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts index 7033c60e9d11..2d3569e039d4 100644 --- a/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts +++ b/web-common/src/features/dashboards/state-managers/loaders/DashboardStateSync.ts @@ -21,6 +21,7 @@ import { getContext, setContext } from "svelte"; import { derived, get, type Readable } from "svelte/store"; import type { CompoundQueryResult } from "@rilldata/web-common/features/compound-query-result"; import { ExploreStateURLParams } from "@rilldata/web-common/features/dashboards/url-state/url-params.ts"; +import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts"; export const DASHBOARD_STATE_SYNC_KEY = Symbol("state-sync"); @@ -55,6 +56,7 @@ export class DashboardStateSync { private readonly exploreName: string, private readonly extraPrefix: string | undefined, private readonly dataLoader: DashboardStateDataLoader, + private readonly expressionFilterManager: ExpressionFilterManager, ) { this.exploreStore = useExploreState(exploreName); this.timeControlStore = createTimeControlStoreFromName( @@ -186,7 +188,10 @@ export class DashboardStateSync { ); } - // log("INIT", redirectUrl); + log("INIT", redirectUrl); + this.expressionFilterManager.setExprParam( + redirectUrl.searchParams.get(ExploreStateURLParams.Filters) ?? "", + ); // If the current url same as the new url then there is no need to do anything if (redirectUrl.search === pageState.url.search) { this.initialized = true; @@ -296,7 +301,10 @@ export class DashboardStateSync { this.updating = false; } - // log("URL", redirectUrl); + log("URL", redirectUrl); + this.expressionFilterManager.setExprParam( + redirectUrl.searchParams.get(ExploreStateURLParams.Filters) ?? "", + ); // If the url doesn't need to be changed further then we can skip the goto if (redirectUrl.search === pageState.url.search) { return; @@ -352,7 +360,10 @@ export class DashboardStateSync { ); } - // log("GOTO", newUrl); + log("GOTO", newUrl); + this.expressionFilterManager.setExprParam( + newUrl.searchParams.get(ExploreStateURLParams.Filters) ?? "", + ); // If the state didnt result in a new url then skip goto. // This avoids adding redundant urls to the history. if (newUrl.search === pageState.url.search) { diff --git a/web-common/src/features/dashboards/state-managers/selectors/dimension-filters.ts b/web-common/src/features/dashboards/state-managers/selectors/dimension-filters.ts index 94fd870222cd..20eef1c8fa73 100644 --- a/web-common/src/features/dashboards/state-managers/selectors/dimension-filters.ts +++ b/web-common/src/features/dashboards/state-managers/selectors/dimension-filters.ts @@ -1,7 +1,6 @@ import { DimensionFilterMode } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/constants"; import { useDimensionSearch } from "@rilldata/web-common/features/dashboards/filters/dimension-filters/dimension-filter-values"; import { getDimensionDisplayName } from "@rilldata/web-common/features/dashboards/filters/getDisplayName"; -import { filterItemsSortFunction } from "@rilldata/web-common/features/dashboards/state-managers/selectors/filters"; import type { StateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers"; import { forEachIdentifier, @@ -97,13 +96,6 @@ export const useSelectedValuesForCompareDimension = (ctx: StateManagers) => { ) as ReturnType; }; -export const isFilterExcludeMode = ( - dashData: AtLeast, -): ((dimName: string) => boolean) => { - return (dimName: string) => - dashData.dashboard.dimensionFilterExcludeMode.get(dimName) ?? false; -}; - export const dimensionHasFilter = ( dashData: AtLeast, ) => { @@ -144,19 +136,6 @@ export type DimensionFilterItem = { missingRequired?: boolean; }; -export function getDimensionFilterItems( - dashData: AtLeast, -) { - return (dimensionIdMap: Map) => { - return getDimensionFilters( - dimensionIdMap, - dashData.dashboard.whereFilter, - dashData.dashboard.dimensionsWithInlistFilter, - dashData.validExplore?.metricsView, - ); - }; -} - export function getDimensionFiltersMap( dimensionIdMap: Map, filter: V1Expression | undefined, @@ -230,102 +209,9 @@ export function getDimensionFilters( ); } -export const getAllDimensionFilterItems = ( - dashData: AtLeast, -) => { - return ( - dimensionFilterItem: DimensionFilterItem[], - dimensionIdMap: Map, - ) => { - const allDimensionFilterItem = [...dimensionFilterItem]; - - // if the temporary filter is a dimension filter add it - if ( - dashData.dashboard.temporaryFilterName && - dimensionIdMap.has(dashData.dashboard.temporaryFilterName) && - dashData.validExplore?.metricsView - ) { - allDimensionFilterItem.push({ - name: dashData.dashboard.temporaryFilterName, - label: getDimensionDisplayName( - dimensionIdMap.get(dashData.dashboard.temporaryFilterName), - ), - mode: DimensionFilterMode.Select, - selectedValues: [], - isInclude: true, - dimensions: new Map([ - [ - dashData.validExplore?.metricsView, - dimensionIdMap.get(dashData.dashboard.temporaryFilterName)!, - ], - ]), - pinned: false, - }); - } - - // sort based on name to make sure toggling include/exclude is not jarring - return allDimensionFilterItem.sort(filterItemsSortFunction); - }; -}; - -export const unselectedDimensionValues = ( - dashData: AtLeast, -) => { - return (dimensionName: string, values: unknown[]): unknown[] => { - const expr = getWhereFilterExpression(dashData)(dimensionName); - if (expr === undefined) { - return values; - } - - return values.filter( - (v) => expr.cond?.exprs?.findIndex((e) => e.val === v) === -1, - ); - }; -}; - -export const includedDimensionValues = ( - dashData: AtLeast, -) => { - return (dimensionName: string): unknown[] => { - const expr = getWhereFilterExpression(dashData)(dimensionName); - if (expr === undefined || expr.cond?.op !== V1Operation.OPERATION_IN) { - return []; - } - - return getValuesInExpression(expr); - }; -}; - -export const hasAtLeastOneDimensionFilter = ( - dashData: AtLeast, -) => { - const whereFilter = dashData.dashboard.whereFilter; - return whereFilter.cond?.exprs?.length && whereFilter.cond.exprs.length > 0; -}; - export const dimensionFilterSelectors = { - /** - * Returns a function that can be used to get whether the specified - * dimension is in exclude mode. - */ - isFilterExcludeMode, - /** * Check if a dimension has any filter */ dimensionHasFilter, - - /** - * Get filter items based on currently selected values for a dimension - */ - getDimensionFilterItems, - - /** - * Get filter items on dimension along with an empty entry for temporary filter if it is a dimension - */ - getAllDimensionFilterItems, - - unselectedDimensionValues, - includedDimensionValues, - hasAtLeastOneDimensionFilter, }; diff --git a/web-common/src/features/dashboards/state-managers/state-managers.ts b/web-common/src/features/dashboards/state-managers/state-managers.ts index f9f9036cd15b..487d75c88924 100644 --- a/web-common/src/features/dashboards/state-managers/state-managers.ts +++ b/web-common/src/features/dashboards/state-managers/state-managers.ts @@ -33,6 +33,7 @@ import { contextColWidthDefaults, type ContextColWidths, } from "../leaderboard-context-column"; +import { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts"; export type StateManagers = { runtimeClient: RuntimeClient; @@ -65,6 +66,7 @@ export type StateManagers = { */ contextColumnWidths: Writable; defaultExploreState: Readable; + expressionFilterManager: ExpressionFilterManager; }; export const DEFAULT_STORE_KEY = Symbol("state-managers"); @@ -191,5 +193,6 @@ export function createStateManagers({ }), contextColumnWidths, defaultExploreState, + expressionFilterManager: new ExpressionFilterManager(), }; } diff --git a/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte b/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte index 9b47263de2d1..676c72594dbc 100644 --- a/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte +++ b/web-common/src/features/dashboards/time-dimension-details/TDDHeader.svelte @@ -30,15 +30,29 @@ import type { TDDComparison } from "./types"; import { V1TimeGrainToDateTimeUnit } from "@rilldata/web-common/lib/time/new-grains"; - export let exploreName: string; - export let dimensionName: string; - export let isFetching = false; - export let comparing: TDDComparison | undefined; - export let areAllTableRowsSelected = false; - export let isRowsEmpty = false; - export let expandedMeasureName: string; - export let onToggleSearchItems: () => void; - export let hideStartPivotButton = false; + interface Props { + exploreName: string; + dimensionName: string; + isFetching?: boolean; + comparing: TDDComparison | undefined; + areAllTableRowsSelected?: boolean; + isRowsEmpty?: boolean; + expandedMeasureName: string; + onToggleSearchItems: () => void; + hideStartPivotButton?: boolean; + } + + let { + exploreName, + dimensionName, + isFetching = false, + comparing, + areAllTableRowsSelected = false, + isRowsEmpty = false, + expandedMeasureName, + onToggleSearchItems, + hideStartPivotButton = false, + }: Props = $props(); const { adminServer, exports } = featureFlags; const stateManagers = getStateManagers(); @@ -48,30 +62,34 @@ measures: { measureLabel, allMeasures }, dimensions: { getDimensionDisplayName }, }, - actions: { - dimensionsFilter: { toggleDimensionFilterMode }, - }, dashboardStore, validSpecStore, + expressionFilterManager, } = stateManagers; - $: selectableMeasures = $allMeasures - .filter((m) => m.name !== undefined || m.displayName !== undefined) - .map((m) => - // Note: undefined values are filtered out above, so the - // empty string fallback is unreachable. - ({ - name: m.name || "", - label: m.displayName || "", - }), - ); + const selectableMeasures = $derived( + $allMeasures + .filter((m) => m.name !== undefined || m.displayName !== undefined) + .map((m) => + // Note: undefined values are filtered out above, so the + // empty string fallback is unreachable. + ({ + name: m.name || "", + label: m.displayName || "", + }), + ), + ); - $: selectedMeasureLabel = + const selectedMeasureLabel = $derived( $allMeasures.find((m) => m.name === expandedMeasureName)?.displayName || - expandedMeasureName; + expandedMeasureName, + ); - $: excludeMode = - $dashboardStore?.dimensionFilterExcludeMode.get(dimensionName) ?? false; + const excludeMode = $derived( + expressionFilterManager.dimensionFilterManagers.find( + (dfm) => dfm.name === dimensionName, + )?.exclude ?? false, + ); function closeSearchBar() { dimensionSearchText.set(""); @@ -85,14 +103,17 @@ } function toggleFilterMode() { - toggleDimensionFilterMode(dimensionName); + expressionFilterManager.dimensionFilterAction( + dimensionName, + (dimensionManager) => dimensionManager.toggleExclude(), + ); } function switchMeasure(measureName: string) { metricsExplorerStore.setExpandedMeasureName(exploreName, measureName); } - let showReplacePivotModal = false; + let showReplacePivotModal = $state(false); function startPivotForTDD() { const pivot = $dashboardStore?.pivot; @@ -139,17 +160,21 @@ const timeControlsStore = useTimeControlStore(stateManagers); - $: ({ minTimeGrain, timeStart, timeEnd, selectedTimeRange } = - $timeControlsStore); + const minTimeGrain = $derived($timeControlsStore.minTimeGrain); + const timeStart = $derived($timeControlsStore.timeStart); + const timeEnd = $derived($timeControlsStore.timeEnd); + const selectedTimeRange = $derived($timeControlsStore.selectedTimeRange); - $: activeTimeGrain = selectedTimeRange?.interval; + const activeTimeGrain = $derived(selectedTimeRange?.interval); - $: baseTimeRange = selectedTimeRange?.start && - selectedTimeRange?.end && { - name: selectedTimeRange?.name, - start: selectedTimeRange.start, - end: selectedTimeRange.end, - }; + const baseTimeRange = $derived( + selectedTimeRange?.start && + selectedTimeRange?.end && { + name: selectedTimeRange?.name, + start: selectedTimeRange.start, + end: selectedTimeRange.end, + }, + ); function onTimeGrainSelect(timeGrain: V1TimeGrain) { if (baseTimeRange) { diff --git a/web-common/src/features/dashboards/time-dimension-details/TimeDimensionDisplay.svelte b/web-common/src/features/dashboards/time-dimension-details/TimeDimensionDisplay.svelte index 81d0b160b726..4edf67613f33 100644 --- a/web-common/src/features/dashboards/time-dimension-details/TimeDimensionDisplay.svelte +++ b/web-common/src/features/dashboards/time-dimension-details/TimeDimensionDisplay.svelte @@ -23,84 +23,105 @@ import type { TDDComparison, TableData } from "./types"; import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; - export let exploreName: string; - export let expandedMeasureName: string; - export let hideStartPivotButton = false; + interface Props { + exploreName: string; + expandedMeasureName: string; + hideStartPivotButton?: boolean; + } + + let { + exploreName, + expandedMeasureName, + hideStartPivotButton = false, + }: Props = $props(); - $: isEmbedded = EmbedStore.isEmbedded(); + const isEmbedded = $derived(EmbedStore.isEmbedded()); const stateManagers = getStateManagers(); const { dashboardStore, selectors: { dimensions: { allDimensions }, - dimensionFilters: { unselectedDimensionValues }, measures: { allMeasures }, }, actions: { - dimensionsFilter: { - toggleDimensionValueSelection, - selectItemsInFilter, - deselectItemsInFilter, - }, sorting: { toggleSort }, }, + expressionFilterManager, } = getStateManagers(); const timeDimensionDataStore = useTimeDimensionDataStore(stateManagers); const timeControlStore = useTimeControlStore(stateManagers); - $: dimensionName = $dashboardStore?.selectedComparisonDimension ?? ""; - $: comparing = $timeDimensionDataStore?.comparing; + const dimensionName = $derived( + $dashboardStore?.selectedComparisonDimension ?? "", + ); + const comparing = $derived($timeDimensionDataStore?.comparing); - $: pinIndex = $dashboardStore?.tdd.pinIndex; + const pinIndex = $derived($dashboardStore?.tdd.pinIndex); - $: timeGrain = $timeControlStore.selectedTimeRange?.interval; + const timeGrain = $derived($timeControlStore.selectedTimeRange?.interval); - $: measure = $allMeasures.find((m) => m.name === expandedMeasureName); + const measure = $derived( + $allMeasures.find((m) => m.name === expandedMeasureName), + ); - $: measureLabel = measure?.displayName ?? ""; + const measureLabel = $derived(measure?.displayName ?? ""); - let dimensionLabel = ""; - $: if (comparing === "dimension") { - dimensionLabel = - $allDimensions.find((d) => d.name === dimensionName)?.displayName ?? ""; - } else if (comparing === "time") { - dimensionLabel = m.dashboard_tdd_time(); - } else if (comparing === "none") { - dimensionLabel = m.dashboard_tdd_no_comparison(); - } + const dimensionLabel = $derived.by(() => { + if (comparing === "dimension") { + return ( + $allDimensions.find((d) => d.name === dimensionName)?.displayName ?? "" + ); + } else if (comparing === "time") { + return m.dashboard_tdd_time(); + } else if (comparing === "none") { + return m.dashboard_tdd_no_comparison(); + } + return ""; + }); // Create a copy of the data to avoid flashing of table in transient states - let timeDimensionDataCopy: TableData; - let comparisonCopy: TDDComparison | undefined; - $: if ( - $timeDimensionDataStore?.data && - $timeDimensionDataStore?.data?.columnHeaderData - ) { - comparisonCopy = comparing; - timeDimensionDataCopy = $timeDimensionDataStore.data; - } - $: formattedData = timeDimensionDataCopy; - $: excludeMode = - $dashboardStore?.dimensionFilterExcludeMode.get(dimensionName) ?? false; + let timeDimensionDataCopy = $state(); + let comparisonCopy = $state(); + $effect(() => { + if ( + $timeDimensionDataStore?.data && + $timeDimensionDataStore?.data?.columnHeaderData + ) { + comparisonCopy = comparing; + timeDimensionDataCopy = $timeDimensionDataStore.data; + } + }); + const formattedData = $derived(timeDimensionDataCopy); + const excludeMode = $derived( + expressionFilterManager.dimensionFilterManagers.find( + (dfm) => dfm.name === dimensionName, + )?.exclude ?? false, + ); - $: rowHeaderLabels = - formattedData?.rowHeaderData?.slice(1)?.map((row) => row[0]?.value) ?? []; + const rowHeaderLabels = $derived( + formattedData?.rowHeaderData?.slice(1)?.map((row) => row[0]?.value) ?? [], + ); - $: areAllTableRowsSelected = rowHeaderLabels?.every( - (val) => val !== undefined && formattedData?.selectedValues?.includes(val), + const areAllTableRowsSelected = $derived( + rowHeaderLabels?.every( + (val) => + val !== undefined && formattedData?.selectedValues?.includes(val), + ), ); - $: columnHeaders = formattedData?.columnHeaderData?.flat(); + const columnHeaders = $derived(formattedData?.columnHeaderData?.flat()); - $: highlightedColStart = $hoverIndex?.start; - $: highlightedColEnd = $hoverIndex?.end; + const highlightedColStart = $derived($hoverIndex?.start); + const highlightedColEnd = $derived($hoverIndex?.end); // Create a time formatter for the column headers - $: timeFormatter = timeFormat( - timeGrain ? TIME_GRAIN[timeGrain].d3format : "%H:%M", - ) as (d: Date) => string; + const timeFormatter = $derived( + timeFormat( + timeGrain ? TIME_GRAIN[timeGrain].d3format : "%H:%M", + ) as (d: Date) => string, + ); function highlightCell(x: number | undefined, y: number | undefined) { if (x === undefined || y === undefined) { @@ -127,8 +148,11 @@ const debounceHighlightCell = debounce(highlightCell, 50); - function toggleFilter(label: string | null) { - toggleDimensionValueSelection(dimensionName, label); + function toggleFilter(label: string) { + expressionFilterManager.dimensionFilterAction( + dimensionName, + (dimensionManager) => dimensionManager.toggleValue(label, false), + ); } function toggleAllSearchItems() { @@ -139,9 +163,10 @@ if (headerHasUndefined) return; if (areAllTableRowsSelected) { - deselectItemsInFilter( + expressionFilterManager.dimensionFilterAction( dimensionName, - rowHeaderLabels as (string | null)[], + (dimensionManager) => + dimensionManager.removeSelectedValues(rowHeaderLabels as string[]), ); eventBus.emit("notification", { @@ -151,11 +176,11 @@ }); return; } else { - const newValuesSelected = $unselectedDimensionValues( + const newValuesSelected = expressionFilterManager.dimensionFilterAction( dimensionName, - rowHeaderLabels, + (dimensionManager) => + dimensionManager.appendSelectedValues(rowHeaderLabels as string[]), ); - selectItemsInFilter(dimensionName, rowHeaderLabels as (string | null)[]); eventBus.emit("notification", { message: m.dashboard_added_items_filter({ count: newValuesSelected.length.toString(), @@ -166,23 +191,24 @@ function togglePin() { let newPinIndex = -1; + const selectedCount = formattedData?.selectedValues?.length ?? 0; // Pin if some selected items are not pinned yet - if (pinIndex > -1 && pinIndex < formattedData?.selectedValues?.length - 1) { - newPinIndex = formattedData?.selectedValues?.length - 1; + if (pinIndex > -1 && pinIndex < selectedCount - 1) { + newPinIndex = selectedCount - 1; } // Pin if no items are pinned yet else if (pinIndex === -1) { - newPinIndex = formattedData?.selectedValues?.length - 1; + newPinIndex = selectedCount - 1; } metricsExplorerStore.setPinIndex(exploreName, newPinIndex); } - function handleKeyDown(e) { + function handleKeyDown(e: KeyboardEvent) { if (comparisonCopy !== "dimension") return; // Select all items on Meta+A if ((e.ctrlKey || e.metaKey) && e.key === "a") { - if (e.target.tagName === "INPUT") return; + if ((e.target as HTMLElement)?.tagName === "INPUT") return; e.preventDefault(); if (areAllTableRowsSelected) return; toggleAllSearchItems(); diff --git a/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte b/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte index 1a4832521b3b..324782dcd285 100644 --- a/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte +++ b/web-common/src/features/dashboards/time-series/MetricsTimeSeriesCharts.svelte @@ -66,13 +66,13 @@ dashboardStore, selectors: { measures: { allMeasures, visibleMeasures, getMeasureByName }, - dimensionFilters: { includedDimensionValues }, charts: { canPanLeft, canPanRight, getNewPanRange }, tags: { measureTagIndex }, }, actions: { measures: { setMeasureVisibility }, }, + expressionFilterManager, } = StateManagers; const timeControlsStore = useTimeControlStore(StateManagers); @@ -148,9 +148,10 @@ const [start, end] = a <= b ? [a, b] : [b, a]; return Interval.fromDateTimes(start, end) as Interval; })(); - $: includedValuesForDimension = $includedDimensionValues( - comparisonDimension as string, - ); + $: includedValuesForDimension = + expressionFilterManager.dimensionFilterManagers.find( + (dfm) => dfm.name === comparisonDimension, + )?.selectedValues ?? []; $: chartDimensionValues = includedValuesForDimension.slice( 0, showTimeDimensionDetail ? 11 : 7, diff --git a/web-common/src/features/dashboards/workspace/Dashboard.svelte b/web-common/src/features/dashboards/workspace/Dashboard.svelte index 9c48ab4c6094..cbac3e2f4682 100644 --- a/web-common/src/features/dashboards/workspace/Dashboard.svelte +++ b/web-common/src/features/dashboards/workspace/Dashboard.svelte @@ -22,7 +22,6 @@ import MeasuresContainer from "../big-number/MeasuresContainer.svelte"; import DimensionDisplay from "../dimension-table/DimensionDisplay.svelte"; import Filters from "../filters/Filters.svelte"; - import ExpressionFilters from "../filters/manager/ExpressionFilters.svelte"; import { selectedMockUserStore } from "../granular-access-policies/stores"; import LeaderboardDisplay from "../leaderboard/LeaderboardDisplay.svelte"; import RowsViewerAccordion from "../rows-viewer/RowsViewerAccordion.svelte"; @@ -166,9 +165,6 @@
{:else} {#key exploreName} -
- -
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 7c29a88096ab..16697ae04fa1 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 @@ -76,8 +76,6 @@ function flushParams() { if (newParams.length === 0) return; const newUrl = new SvelteURL(page.url); - const curUrlSearch = page.url.search; - newParams.forEach(([key, value]) => { if (value === null) { newUrl.searchParams.delete(key); @@ -86,7 +84,5 @@ function flushParams() { } }); newParams = []; - - if (curUrlSearch === newUrl.search) return; void goto(newUrl, { noScroll: true, keepFocus: true }); } From 7918110f3bf2575853e5dc368eef33ab3cfdf3e0 Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Fri, 24 Jul 2026 19:33:13 +0530 Subject: [PATCH 5/5] Move alerts/reports to the new filter class --- .../src/features/alerts/AlertForm.svelte | 20 +- .../src/features/alerts/alert-preview-data.ts | 12 +- .../src/features/alerts/create-alert-utils.ts | 19 +- .../AlertDialogCriteriaTab.svelte | 4 +- .../alerts/criteria-tab/AlertPreview.svelte | 41 ++-- .../alerts/data-tab/AlertDialogDataTab.svelte | 4 +- .../alerts/data-tab/DataPreview.svelte | 42 ++-- web-common/src/features/alerts/form-utils.ts | 11 +- .../dashboards/filters/Filters.svelte | 182 ++++++++++++------ .../filters/manager/ExpressionFilters.svelte | 98 +++++----- .../expression-filter-manager.svelte.ts | 15 +- .../state-managers/actions/filters.ts | 2 + .../state-managers/selectors/measures.ts | 36 ++-- .../BaseScheduledReportForm.svelte | 4 +- .../scheduled-reports/FiltersForm.svelte | 160 ++------------- .../ScheduledReportDialog.svelte | 8 +- .../src/features/scheduled-reports/utils.ts | 22 +-- .../store-utils/url-params-state.svelte.ts | 1 + 18 files changed, 323 insertions(+), 358 deletions(-) diff --git a/web-common/src/features/alerts/AlertForm.svelte b/web-common/src/features/alerts/AlertForm.svelte index 76f962c202d0..2ff658deca6e 100644 --- a/web-common/src/features/alerts/AlertForm.svelte +++ b/web-common/src/features/alerts/AlertForm.svelte @@ -185,7 +185,7 @@ queryArgsJson: JSON.stringify( getAlertQueryArgsFromFormValues( values, - filters.toState(), + filters.expr, timeControls.toState(), exploreSpec, ), @@ -324,18 +324,18 @@
{#if currentTabIndex === 0} - + {:else} - + {/if} {#if currentTabIndex !== 2} - + {:else}
{/if} - +
diff --git a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte index c33ab230dfd7..66befd97775b 100644 --- a/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte +++ b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte @@ -1,59 +1,54 @@