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}
-
{m.alert_form_cancel()}
+
+ {m.alert_form_cancel()}
+
{:else}
-
{m.alert_form_back()}
+
+ {m.alert_form_back()}
+
{/if}
{#if currentTabIndex !== 2}
-
{m.alert_form_next()}
+
+ {m.alert_form_next()}
+
{:else}
{isCreateForm ? m.alert_form_create() : m.alert_form_update()}
diff --git a/web-common/src/features/alerts/alert-preview-data.ts b/web-common/src/features/alerts/alert-preview-data.ts
index c9477098fc12..d6f0aa79bdfe 100644
--- a/web-common/src/features/alerts/alert-preview-data.ts
+++ b/web-common/src/features/alerts/alert-preview-data.ts
@@ -25,6 +25,7 @@ import {
type StructTypeField,
TypeCode,
type V1ExploreSpec,
+ type V1Expression,
type V1MetricsViewAggregationRequest,
type V1MetricsViewAggregationResponseDataItem,
type V1MetricsViewSpec,
@@ -46,21 +47,20 @@ export function getAlertPreviewData(
client: RuntimeClient,
queryClient: QueryClient,
formValues: AlertFormValues,
- filters: Filters,
+ expr: V1Expression,
timeControls: TimeControls,
): CreateQueryResult {
return derived(
[
useExploreValidSpec(client, formValues.exploreName),
- filters.getStore(),
timeControls.getStore(),
],
- ([validExploreSpec, filtersState, timeControlsState], set) =>
+ ([validExploreSpec, timeControlsState], set) =>
createQueryServiceMetricsViewAggregation(
client,
getAlertPreviewQueryRequest(
formValues,
- filtersState,
+ expr,
timeControlsState,
validExploreSpec.data?.explore ?? {},
),
@@ -77,13 +77,13 @@ export function getAlertPreviewData(
function getAlertPreviewQueryRequest(
formValues: AlertFormValues,
- filtersArgs: FiltersState,
+ expr: V1Expression,
timeControlArgs: TimeControlState,
exploreSpec: V1ExploreSpec,
): V1MetricsViewAggregationRequest {
const req = getAlertQueryArgsFromFormValues(
formValues,
- filtersArgs,
+ expr,
timeControlArgs,
exploreSpec,
);
diff --git a/web-common/src/features/alerts/create-alert-utils.ts b/web-common/src/features/alerts/create-alert-utils.ts
index 73da6f3c9b03..245472b4a45e 100644
--- a/web-common/src/features/alerts/create-alert-utils.ts
+++ b/web-common/src/features/alerts/create-alert-utils.ts
@@ -10,6 +10,9 @@ import { TimeControls } from "@rilldata/web-common/features/dashboards/stores/Ti
import { getInitialScheduleFormValues } from "@rilldata/web-common/features/scheduled-reports/time-utils.ts";
import { V1Operation } from "@rilldata/web-common/runtime-client";
import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2";
+import { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts";
+import { mergeDimensionAndMeasureFilters } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-utils.ts";
+import { convertExpressionToFilterParam } from "@rilldata/web-common/features/dashboards/url-state/filters/converters.ts";
export function getNewAlertInitialFormValues(
metricsViewName: string,
@@ -64,13 +67,15 @@ export function getNewAlertInitialFiltersFormValues(
metricsViewName,
exploreName,
);
- const filters = new Filters(metricsViewMetadata, {
- whereFilter: exploreState.whereFilter ?? createAndExpression([]),
- dimensionsWithInlistFilter: exploreState.dimensionsWithInlistFilter ?? [],
- dimensionThresholdFilters: exploreState.dimensionThresholdFilters ?? [],
- dimensionFilterExcludeMode:
- exploreState.dimensionFilterExcludeMode ?? new Map(),
- });
+
+ const filters = new ExpressionFilterManager();
+ const fullExpr = mergeDimensionAndMeasureFilters(
+ exploreState.whereFilter,
+ exploreState.dimensionThresholdFilters ?? [],
+ );
+ const exprParam = convertExpressionToFilterParam(fullExpr);
+ filters.setExprParam(exprParam);
+
const timeControls = new TimeControls(metricsViewMetadata, {
selectedTimeRange: exploreState.selectedTimeRange,
selectedComparisonTimeRange: exploreState.selectedComparisonTimeRange,
diff --git a/web-common/src/features/alerts/criteria-tab/AlertDialogCriteriaTab.svelte b/web-common/src/features/alerts/criteria-tab/AlertDialogCriteriaTab.svelte
index 24b28ebac273..f4f2a6b7b78a 100644
--- a/web-common/src/features/alerts/criteria-tab/AlertDialogCriteriaTab.svelte
+++ b/web-common/src/features/alerts/criteria-tab/AlertDialogCriteriaTab.svelte
@@ -4,12 +4,12 @@
import AlertPreview from "@rilldata/web-common/features/alerts/criteria-tab/AlertPreview.svelte";
import CriteriaGroup from "@rilldata/web-common/features/alerts/criteria-tab/CriteriaGroup.svelte";
import type { AlertFormValues } from "@rilldata/web-common/features/alerts/form-utils";
- import type { Filters } from "@rilldata/web-common/features/dashboards/stores/Filters.ts";
import type { TimeControls } from "@rilldata/web-common/features/dashboards/stores/TimeControls.ts";
import type { SuperForm } from "sveltekit-superforms/client";
+ import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts";
export let superFormInstance: SuperForm;
- export let filters: Filters;
+ export let filters: ExpressionFilterManager;
export let timeControls: TimeControls;
$: ({ form } = superFormInstance);
diff --git a/web-common/src/features/alerts/criteria-tab/AlertPreview.svelte b/web-common/src/features/alerts/criteria-tab/AlertPreview.svelte
index f2d81f0235e4..2d9f66cc76b1 100644
--- a/web-common/src/features/alerts/criteria-tab/AlertPreview.svelte
+++ b/web-common/src/features/alerts/criteria-tab/AlertPreview.svelte
@@ -6,34 +6,45 @@
import { mapMeasureFilterToExpr } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-entry";
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
- import type { Filters } from "@rilldata/web-common/features/dashboards/stores/Filters.ts";
import type { TimeControls } from "@rilldata/web-common/features/dashboards/stores/TimeControls.ts";
import PreviewEmpty from "../PreviewEmpty.svelte";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
import type { DimensionTableRow } from "../../dashboards/dimension-table/dimension-table-types";
+ import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts";
- export let formValues: AlertFormValues;
- export let filters: Filters;
- export let timeControls: TimeControls;
-
- const runtimeClient = useRuntimeClient();
-
- $: alertPreviewQuery = getAlertPreviewData(
- runtimeClient,
- queryClient,
+ let {
formValues,
filters,
timeControls,
+ }: {
+ formValues: AlertFormValues;
+ filters: ExpressionFilterManager;
+ timeControls: TimeControls;
+ } = $props();
+
+ const runtimeClient = useRuntimeClient();
+
+ let alertPreviewQuery = $derived(
+ getAlertPreviewData(
+ runtimeClient,
+ queryClient,
+ formValues,
+ filters.expr,
+ timeControls,
+ ),
);
- $: isCriteriaEmpty =
- formValues.criteria.map(mapMeasureFilterToExpr).length === 0;
+ let isCriteriaEmpty = $derived(
+ formValues.criteria.map(mapMeasureFilterToExpr).length === 0,
+ );
- $: queryResult = $alertPreviewQuery;
+ let queryResult = $derived($alertPreviewQuery);
- $: rows = (queryResult.data?.rows as DimensionTableRow[] | undefined) ?? [];
- $: columns = queryResult.data?.schema ?? [];
+ let rows = $derived(
+ (queryResult.data?.rows as DimensionTableRow[] | undefined) ?? [],
+ );
+ let columns = $derived(queryResult.data?.schema ?? []);
{#if $alertPreviewQuery.isFetching}
diff --git a/web-common/src/features/alerts/data-tab/AlertDialogDataTab.svelte b/web-common/src/features/alerts/data-tab/AlertDialogDataTab.svelte
index f9bd02bd6782..38f61719be9f 100644
--- a/web-common/src/features/alerts/data-tab/AlertDialogDataTab.svelte
+++ b/web-common/src/features/alerts/data-tab/AlertDialogDataTab.svelte
@@ -2,7 +2,6 @@
import DataPreview from "@rilldata/web-common/features/alerts/data-tab/DataPreview.svelte";
import type { AlertFormValues } from "@rilldata/web-common/features/alerts/form-utils";
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";
- import type { Filters } from "@rilldata/web-common/features/dashboards/stores/Filters.ts";
import FiltersForm from "@rilldata/web-common/features/scheduled-reports/FiltersForm.svelte";
import type { TimeControls } from "@rilldata/web-common/features/dashboards/stores/TimeControls.ts";
import { MetricsViewSpecMeasureType } from "@rilldata/web-common/runtime-client";
@@ -11,9 +10,10 @@
import Select from "../../../components/forms/Select.svelte";
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
import { useMetricsViewValidSpec } from "../../dashboards/selectors";
+ import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts";
export let superFormInstance: SuperForm;
- export let filters: Filters;
+ export let filters: ExpressionFilterManager;
export let timeControls: TimeControls;
const runtimeClient = useRuntimeClient();
diff --git a/web-common/src/features/alerts/data-tab/DataPreview.svelte b/web-common/src/features/alerts/data-tab/DataPreview.svelte
index 23333aa3183c..16b8adcc837f 100644
--- a/web-common/src/features/alerts/data-tab/DataPreview.svelte
+++ b/web-common/src/features/alerts/data-tab/DataPreview.svelte
@@ -5,34 +5,44 @@
import { m } from "@rilldata/web-common/lib/i18n/gen/messages";
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
- import type { Filters } from "@rilldata/web-common/features/dashboards/stores/Filters.ts";
import type { TimeControls } from "@rilldata/web-common/features/dashboards/stores/TimeControls.ts";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
import PreviewEmpty from "../PreviewEmpty.svelte";
import type { DimensionTableRow } from "../../dashboards/dimension-table/dimension-table-types";
+ import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts";
- export let formValues: AlertFormValues;
- export let filters: Filters;
- export let timeControls: TimeControls;
+ let {
+ formValues,
+ filters,
+ timeControls,
+ }: {
+ formValues: AlertFormValues;
+ filters: ExpressionFilterManager;
+ timeControls: TimeControls;
+ } = $props();
const runtimeClient = useRuntimeClient();
- $: alertPreviewQuery = getAlertPreviewData(
- runtimeClient,
- queryClient,
- {
- ...formValues,
- criteria: [],
- },
- filters,
- timeControls,
+ let alertPreviewQuery = $derived(
+ getAlertPreviewData(
+ runtimeClient,
+ queryClient,
+ {
+ ...formValues,
+ criteria: [],
+ },
+ filters.expr,
+ timeControls,
+ ),
);
- $: queryResult = $alertPreviewQuery;
+ let queryResult = $derived($alertPreviewQuery);
- $: rows = (queryResult.data?.rows as DimensionTableRow[] | undefined) ?? [];
- $: columns = queryResult.data?.schema ?? [];
+ let rows = $derived(
+ (queryResult.data?.rows as DimensionTableRow[] | undefined) ?? [],
+ );
+ let columns = $derived(queryResult.data?.schema ?? []);
{#if queryResult.isFetching}
diff --git a/web-common/src/features/alerts/form-utils.ts b/web-common/src/features/alerts/form-utils.ts
index 9efbe1d6f19a..d167e59616ff 100644
--- a/web-common/src/features/alerts/form-utils.ts
+++ b/web-common/src/features/alerts/form-utils.ts
@@ -17,6 +17,7 @@ import type { TimeControlState } from "@rilldata/web-common/features/dashboards/
import { getInitialScheduleFormValues } from "@rilldata/web-common/features/scheduled-reports/time-utils.ts";
import type {
V1ExploreSpec,
+ V1Expression,
V1MetricsViewAggregationRequest,
V1Operation,
} from "@rilldata/web-common/runtime-client";
@@ -46,7 +47,7 @@ export type AlertFormValues = {
export function getAlertQueryArgsFromFormValues(
formValues: AlertFormValues,
- filtersArgs: FiltersState,
+ expr: V1Expression,
timeControlArgs: TimeControlState,
exploreSpec: V1ExploreSpec,
): V1MetricsViewAggregationRequest {
@@ -93,13 +94,7 @@ export function getAlertQueryArgsFromFormValues(
dimensions: formValues.splitByDimension
? [{ name: formValues.splitByDimension }]
: [],
- where: sanitiseExpression(
- mergeDimensionAndMeasureFilters(
- filtersArgs.whereFilter,
- filtersArgs.dimensionThresholdFilters,
- ),
- undefined,
- ),
+ where: sanitiseExpression(expr, undefined),
having: sanitiseExpression(undefined, {
cond: {
op: formValues.criteriaOperation,
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}
- {m.dashboard_clear_filters()}
- {/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/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
new file mode 100644
index 000000000000..19a6719fa696
--- /dev/null
+++ b/web-common/src/features/dashboards/filters/manager/DimensionFilter.svelte
@@ -0,0 +1,522 @@
+
+
+ {
+ if (e.key === "Enter") onApply();
+ }}
+/>
+
+
+
+ {#snippet child({ props })}
+
+ dimensionManager.clear()}
+ removable={!readOnly &&
+ !proxyDimensionManager.pinned &&
+ !dimensionManager.pinned}
+ {readOnly}
+ removeTooltipText="remove {dimensionManager.selectedValues
+ .length} value{dimensionManager.selectedValues.length !== 1
+ ? 's'
+ : ''}"
+ >
+
+
+
+
+
+ {dimensionManager.name}
+ {dimensionManager.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 dimensionManager.editing}
+
+
{dimensionManager.label}
+
+
+
proxyDimensionManager.toggleRequired()}
+ />
+ proxyDimensionManager.togglePinned()}
+ />
+
+
+ {/if}
+
+
+
+
+ {#if showExtraInfo}
+
+ {#if proxyDimensionManager.mode !== 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 proxyDimensionManager.mode === 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 proxyDimensionManager.mode === DimensionFilterMode.Select && !curSearchText && checkedItems.length > 0 && uncheckedItems.length > 0}
+
+ {/if}
+
+
+ {#each uncheckedItems as name (name)}
+ {@const selected = effectiveSelectedValues.includes(name)}
+ {@const label = name ?? "null"}
+ {@const ItemComponent =
+ proxyDimensionManager.mode === DimensionFilterMode.Select
+ ? DropdownMenu.CheckboxItem
+ : DropdownMenu.Item}
+
+ handleItemClick(name)}
+ >
+
+ {#if label.length > 240}
+ {label.slice(0, 240)}...
+ {:else}
+ {label}
+ {/if}
+
+
+ {/each}
+
+
+ {#if uncheckedItems.length === 0 && (proxyDimensionManager.mode !== 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..66befd97775b
--- /dev/null
+++ b/web-common/src/features/dashboards/filters/manager/ExpressionFilters.svelte
@@ -0,0 +1,132 @@
+
+
+
+
+ {#if !hasFilters}
+
+ {m.dashboard_no_filters_selected()}
+
+ {:else}
+ {#each expressionFilterManager.dimensionFilterManagers as dimensionManager (dimensionManager.name)}
+
+ isUrlTooLongAfterInListFilter(dimensionManager.name, values)
+ : undefined}
+ />
+ {/each}
+
+ {#each expressionFilterManager.measureFilterManagers as measureManager (measureManager.name)}
+
+ {/each}
+ {/if}
+
+
+
+ {#if hasFilters}
+ expressionFilterManager.clear()}>
+ {m.dashboard_clear_filters()}
+
+ {/if}
+
+
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
new file mode 100644
index 000000000000..21b187cd163c
--- /dev/null
+++ b/web-common/src/features/dashboards/filters/manager/dimension-filter-manager.svelte.ts
@@ -0,0 +1,186 @@
+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,
+ type V1Expression,
+ V1Operation,
+} from "@rilldata/web-common/runtime-client";
+import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts";
+import {
+ createInExpression,
+ createLikeExpression,
+ getValuesInExpression,
+} from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts";
+
+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;
+
+ 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?
+ initExpr: V1Expression = createInExpression(name, []),
+ isInList: boolean = false,
+ ) {
+ 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 = sanitizeSearchText(
+ initExpr.cond?.exprs?.[1]?.val?.toString?.() ?? "",
+ );
+ initExclude = op === V1Operation.OPERATION_NLIKE;
+ }
+
+ 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();
+ }
+
+ public clone() {
+ return new DimensionFilterManager(
+ this.name,
+ this.label,
+ this.dimensions,
+ this.editing,
+ this.expr,
+ this.mode === DimensionFilterMode.InList,
+ );
+ }
+
+ 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);
+
+ if (inIdx === -1) {
+ if (isExclusiveFilter) {
+ this.selectedValues = [dimensionValue];
+ } else {
+ this.selectedValues = [...this.selectedValues, dimensionValue];
+ }
+ } else {
+ 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;
+ this.inputText = "";
+ this.exclude = exclude;
+ this.commit();
+ }
+
+ public setContainsText(searchText: string, exclude: boolean) {
+ this.mode = DimensionFilterMode.Contains;
+ this.selectedValues = [];
+ this.inputText = searchText;
+ this.exclude = exclude;
+ this.commit();
+ }
+
+ public toggleExclude() {
+ this.exclude = !this.exclude;
+ this.commit();
+ }
+
+ public togglePinned() {
+ this.pinned = !this.pinned;
+ }
+
+ public toggleRequired() {
+ this.required = !this.required;
+ }
+
+ public clear() {
+ this.selectedValues = [];
+ this.inputText = "";
+ this.expr = undefined;
+ this.commit();
+ }
+
+ 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)
+ : undefined;
+ break;
+
+ case DimensionFilterMode.Contains:
+ this.expr = this.inputText
+ ? createLikeExpression(this.name, `%${this.inputText}%`, this.exclude)
+ : undefined;
+ break;
+ }
+ 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
new file mode 100644
index 000000000000..f79444cb117c
--- /dev/null
+++ b/web-common/src/features/dashboards/filters/manager/expression-filter-manager.svelte.ts
@@ -0,0 +1,217 @@
+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,
+ forEachIdentifier,
+ isExpressionUnsupported,
+} from "@rilldata/web-common/features/dashboards/stores/filter-utils.ts";
+import {
+ type MetricsViewSpecDimension,
+ type MetricsViewSpecMeasure,
+} from "@rilldata/web-common/runtime-client";
+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 = "";
+ public isComplexFilter: boolean = $state(false);
+
+ public exploreName: string = "";
+ public metricsViewName: string = "";
+ public measureIdMap: Map = new Map();
+ public dimensionIdMap: Map = new Map();
+
+ public constructor() {
+ this.expr = $derived.by(() => {
+ const dimExprs = this.dimensionFilterManagers
+ .filter((dfm) => !!dfm.expr)
+ .map((d) => d.expr as V1Expression);
+ const mesExprs = this.measureFilterManagers
+ .filter((mfm) => !!mfm.expr)
+ .map((m) => m.expr as V1Expression);
+ return createAndExpression(dimExprs.concat(mesExprs));
+ });
+ }
+
+ public syncSpec(
+ metricsViewName: string,
+ exploreName: string,
+ measureIdMap: Map,
+ dimensionIdMap: Map,
+ ) {
+ this.metricsViewName = metricsViewName;
+ this.exploreName = exploreName;
+ this.measureIdMap = measureIdMap;
+ this.dimensionIdMap = dimensionIdMap;
+ }
+
+ public setExprParam(exprParam: string) {
+ if (exprParam === this.exprParam) return;
+ this.exprParam = exprParam;
+
+ const { expr, dimensionsWithInlistFilter } =
+ convertFilterParamToExpression(exprParam);
+ this.isComplexFilter = expr ? isExpressionUnsupported(expr) : false;
+ if (!expr || this.isComplexFilter) {
+ 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 = this.dimensionIdMap.get(ident);
+ if (!dim) return;
+
+ const firstValueExpr = e?.cond?.exprs?.[1];
+
+ if (firstValueExpr?.subquery) {
+ const measureName = firstValueExpr.subquery.measures?.[0];
+ if (!measureName) return;
+
+ const measure = this.measureIdMap.get(measureName);
+ if (!measure) return;
+
+ addedMeasure.add(measureName);
+
+ newMeasureFilterManagers.push(
+ new MeasureFilterManager(
+ measureName,
+ getMeasureDisplayName(measure),
+ new Map([
+ [this.metricsViewName, measure],
+ ]),
+ false,
+ ident,
+ firstValueExpr,
+ ),
+ );
+ } else {
+ addedDimension.add(ident);
+
+ const isInListMode = dimensionsWithInlistFilter.includes(ident);
+ newDimensionFilterManagers.push(
+ new DimensionFilterManager(
+ ident,
+ getDimensionDisplayName(dim),
+ new Map([
+ [this.metricsViewName, dim],
+ ]),
+ false,
+ e,
+ isInListMode,
+ ),
+ );
+ }
+ });
+
+ 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 {
+ this.temporaryFilter = undefined;
+ }
+ }
+
+ this.measureFilterManagers = newMeasureFilterManagers;
+ this.dimensionFilterManagers = newDimensionFilterManagers;
+ }
+
+ public addTemporaryMeasureFilter(name: string) {
+ const measure = this.measureIdMap.get(name);
+ if (!measure) return;
+
+ this.temporaryFilter = new MeasureFilterManager(
+ name,
+ getMeasureDisplayName(measure),
+ new Map([
+ [this.metricsViewName, measure],
+ ]),
+ false,
+ );
+ this.measureFilterManagers = [
+ ...this.measureFilterManagers,
+ this.temporaryFilter,
+ ];
+ }
+
+ public addTemporaryDimensionFilter(name: string) {
+ const dim = this.dimensionIdMap.get(name);
+ if (!dim) return;
+
+ this.temporaryFilter = new DimensionFilterManager(
+ name,
+ getDimensionDisplayName(dim),
+ new Map([[this.metricsViewName, dim]]),
+ false,
+ );
+ this.dimensionFilterManagers = [
+ ...this.dimensionFilterManagers,
+ this.temporaryFilter,
+ ];
+ }
+
+ 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 = [];
+ this.temporaryFilter = undefined;
+ }
+
+ 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/measure-filter-manager.svelte.ts b/web-common/src/features/dashboards/filters/manager/measure-filter-manager.svelte.ts
new file mode 100644
index 000000000000..a213531b1795
--- /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 = newFilter.operation;
+ this.type = newFilter.type;
+ this.value1 = newFilter.value1;
+ this.value2 = 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/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;
+}
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/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/actions/filters.ts b/web-common/src/features/dashboards/state-managers/actions/filters.ts
index 32749c48449f..fb254ec20efa 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,15 @@ export function setTemporaryFilterName(
dashboard.temporaryFilterName = name;
}
+export function setFilter(
+ { dashboard }: DashboardMutables,
+ expr: V1Expression,
+ inList: string[],
+) {
+ dashboard.whereFilter = expr;
+ dashboard.dimensionThresholdFilters = inList;
+}
+
export const filterActions = {
/**
* Clears all filters and resets related fields
@@ -30,4 +40,6 @@ export const filterActions = {
clearAllFilters,
setTemporaryFilterName,
+
+ setFilter,
};
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 7b1c3e3e1240..2d3569e039d4 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,8 @@ 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";
+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");
@@ -54,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(
@@ -185,6 +188,10 @@ export class DashboardStateSync {
);
}
+ 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;
@@ -294,6 +301,10 @@ export class DashboardStateSync {
this.updating = false;
}
+ 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;
@@ -349,6 +360,10 @@ export class DashboardStateSync {
);
}
+ 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) {
@@ -362,3 +377,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/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/selectors/measures.ts b/web-common/src/features/dashboards/state-managers/selectors/measures.ts
index 031ed78b1039..5be9c1cc60a8 100644
--- a/web-common/src/features/dashboards/state-managers/selectors/measures.ts
+++ b/web-common/src/features/dashboards/state-managers/selectors/measures.ts
@@ -75,24 +75,30 @@ export const filteredSimpleMeasures = ({
validMetricsView,
validExplore,
}: DashboardDataSources) => {
- return () => {
- if (!validMetricsView?.measures || !validExplore?.measures) return [];
-
- return (
- validMetricsView.measures
- .filter(
- (m) => validExplore.measures!.includes(m.name!) && isSimpleMeasure(m),
- )
- // Sort the filtered measures based on their order in validExplore.measures
- .sort(
- (a, b) =>
- validExplore.measures!.indexOf(a.name!) -
- validExplore.measures!.indexOf(b.name!),
- )
+ return () =>
+ getFilteredSimpleMeasures(
+ validMetricsView?.measures ?? [],
+ validExplore?.measures,
);
- };
};
+export function getFilteredSimpleMeasures(
+ allMeasures: MetricsViewSpecMeasure[],
+ exploreMeasures: string[] | undefined,
+) {
+ if (!exploreMeasures) return [];
+
+ return (
+ allMeasures
+ .filter((m) => exploreMeasures.includes(m.name!) && isSimpleMeasure(m))
+ // Sort the filtered measures based on their order in validExplore.measures
+ .sort(
+ (a, b) =>
+ exploreMeasures.indexOf(a.name!) - exploreMeasures.indexOf(b.name!),
+ )
+ );
+}
+
export const isSimpleMeasure = (measure: MetricsViewSpecMeasure) =>
!measure.window &&
measure.type !== MetricsViewSpecMeasureType.MEASURE_TYPE_TIME_COMPARISON;
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/scheduled-reports/BaseScheduledReportForm.svelte b/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte
index 8780279c5271..369b82748441 100644
--- a/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte
+++ b/web-common/src/features/scheduled-reports/BaseScheduledReportForm.svelte
@@ -5,7 +5,6 @@
import MultiInput from "@rilldata/web-common/components/forms/MultiInput.svelte";
import FormSection from "@rilldata/web-common/components/forms/FormSection.svelte";
import { getHasSlackConnection } from "@rilldata/web-common/features/alerts/delivery-tab/notifiers-utils";
- import type { Filters } from "@rilldata/web-common/features/dashboards/stores/Filters.ts";
import type { TimeControls } from "@rilldata/web-common/features/dashboards/stores/TimeControls.ts";
import FiltersForm from "@rilldata/web-common/features/scheduled-reports/FiltersForm.svelte";
import RowsAndColumnsForm from "@rilldata/web-common/features/scheduled-reports/fields/RowsAndColumnsForm.svelte";
@@ -24,6 +23,7 @@
import Select from "../../components/forms/Select.svelte";
import Checkbox from "../../components/forms/Checkbox.svelte";
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
+ import type { ExpressionFilterManager } from "@rilldata/web-common/features/dashboards/filters/manager/expression-filter-manager.svelte.ts";
export let formId: string;
export let data: Readable;
@@ -31,7 +31,7 @@
export let submit: () => void;
export let enhance;
export let exploreName: string;
- export let filters: Filters;
+ export let filters: ExpressionFilterManager;
export let timeControls: TimeControls;
const RUN_AS_OPTIONS = [
diff --git a/web-common/src/features/scheduled-reports/FiltersForm.svelte b/web-common/src/features/scheduled-reports/FiltersForm.svelte
index 3aa46b4e2261..65ecb93e8043 100644
--- a/web-common/src/features/scheduled-reports/FiltersForm.svelte
+++ b/web-common/src/features/scheduled-reports/FiltersForm.svelte
@@ -1,18 +1,9 @@