Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: human readable dimension filters in URL #5340

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,53 @@
import DashboardStateProvider from "@rilldata/web-common/features/dashboards/stores/DashboardStateProvider.svelte";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { errorStore } from "../../../../features/errors/error-store";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { asyncWait } from "@rilldata/web-common/lib/waitUtils";
import { parseFilterString } from "@rilldata/web-common/lib/url/parsing";

const user = createAdminServiceGetCurrentUser();

const PollIntervalWhenDashboardFirstReconciling = 1000;
const PollIntervalWhenDashboardErrored = 5000;
// const PollIntervalWhenDashboardOk = 60000; // This triggers a layout shift, so removing for now

$: instanceId = $runtime?.instanceId;
$: ({ instanceId } = $runtime);

$: ({
organization: orgName,
project: projectName,
dashboard: dashboardName,
} = $page.params);

$: ({ searchParams } = $page.url);

$: filter = searchParams.get("filter") ?? "";

$: dimensionNames =
$dashboard?.data?.metricsView?.state?.validSpec?.dimensions.map(
(d) => d.name,
) ?? [];

$: ({ initDimensions, errorMessage } = parseFilterString(
filter,
dimensionNames,
));

$: if (errorMessage) {
asyncWait(300)
.then(() => {
eventBus.emit("notification", {
message: errorMessage,
detail: `Filter: ${filter ?? ""}`,
type: "error",
options: { persisted: true },
});
})
.catch((e) => {
console.error(e);
});
}

$: dashboard = useDashboard(instanceId, dashboardName, {
refetchInterval: () => {
if (isDashboardReconcilingForFirstTime) {
Expand All @@ -40,7 +72,6 @@
}
},
});

$: isDashboardNotFound =
!$dashboard.data &&
$dashboard.isError &&
Expand Down Expand Up @@ -89,15 +120,15 @@
<DashboardBookmarksStateProvider {metricViewName}>
<DashboardURLStateProvider {metricViewName}>
<DashboardThemeProvider>
<Dashboard {metricViewName} />
<Dashboard {metricViewName} {initDimensions} />
</DashboardThemeProvider>
</DashboardURLStateProvider>
</DashboardBookmarksStateProvider>
{:else}
<DashboardStateProvider {metricViewName}>
<DashboardURLStateProvider {metricViewName}>
<DashboardThemeProvider>
<Dashboard {metricViewName} />
<Dashboard {metricViewName} {initDimensions} />
</DashboardThemeProvider>
</DashboardURLStateProvider>
</DashboardStateProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<TooltipTitle>
<svelte:fragment slot="name">
{displayName}
<span class="text-gray-400 font-normal">({dimensionName})</span>
</svelte:fragment>
<svelte:fragment slot="description" />
</TooltipTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import type { StateManagers } from "@rilldata/web-common/features/dashboards/state-managers/state-managers";
import { getDefaultMetricsExplorerEntity } from "@rilldata/web-common/features/dashboards/stores/dashboard-store-defaults";
import { metricsExplorerStore } from "@rilldata/web-common/features/dashboards/stores/dashboard-stores";
import { getUrlForPath } from "@rilldata/web-common/lib/url-utils";
import { getUrlForPath } from "@rilldata/web-common/lib/url/url-utils";
import type { V1StructType } from "@rilldata/web-common/runtime-client";
import { Readable, derived, get } from "svelte/store";

Expand Down
23 changes: 23 additions & 0 deletions web-common/src/features/dashboards/workspace/Dashboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,34 @@
import RowsViewerAccordion from "../rows-viewer/RowsViewerAccordion.svelte";
import TimeDimensionDisplay from "../time-dimension-details/TimeDimensionDisplay.svelte";
import MetricsTimeSeriesCharts from "../time-series/MetricsTimeSeriesCharts.svelte";
import { getStateManagers } from "../state-managers/state-managers";

export let metricViewName: string;
export let initDimensions: Map<
string,
{ values: string[]; exclude: boolean }
> = new Map();

const { cloudDataViewer, readOnly } = featureFlags;

const {
actions: {
dimensionsFilter: {
toggleDimensionValueSelection,
toggleDimensionFilterMode,
},
},
} = getStateManagers();

initDimensions.forEach(({ values, exclude }, dimensionName) => {
values.forEach((value) => {
toggleDimensionValueSelection(dimensionName, value, false);
});
if (exclude) {
toggleDimensionFilterMode(dimensionName);
}
});

let exploreContainerWidth: number;

$: extraLeftPadding = !$navigationOpen;
Expand Down
80 changes: 80 additions & 0 deletions web-common/src/lib/url/parsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const conditionRegex = /(\w+)\s+(eq|ne)\s+(.+)/;
const dimensionValueRegex = /[‘'’]([^‘'’]*)[‘'’]/g;

export function parseFilterString(filterString: string, dimensions: string[]) {
const initDimensions = new Map<
string,
{ exclude: boolean; values: string[] }
>();

let errorMessage: string | null = null;

if (!filterString || !dimensions.length) {
return {
initDimensions,
errorMessage,
};
}

if (
(filterString.startsWith(`"`) && filterString.endsWith(`"`)) ||
(filterString.startsWith(`'`) && filterString.endsWith(`'`)) ||
(filterString.startsWith(`“`) && filterString.endsWith(`”`))
) {
filterString = filterString.slice(1, -1);
}

const conditions = filterString.split(" and ");

console.log({ conditions });

conditions.forEach((condition) => {
console.log({ condition });
const match = condition.match(conditionRegex);
if (match) {
const [, dimension, operator, valueString] = match;

const values: string[] = [];

if (valueString.startsWith("(") && valueString.endsWith(")")) {
const rawValues = valueString.slice(1, -1).split(",");

rawValues.forEach((value) => {
if (!value.match(dimensionValueRegex)) {
errorMessage = `Value missing quotes: ${value}`;
return;
} else {
values.push(value.slice(1, -1));
}
});
} else if (valueString.match(dimensionValueRegex)) {
values.push(valueString.slice(1, -1));
} else {
errorMessage = `Value missing quotes: ${valueString}`;
return;
}

if (!dimensions.includes(dimension) && !errorMessage) {
errorMessage = `Invalid dimension: ${dimension}`;
return;
} else if (values.length === 0 && !errorMessage) {
errorMessage = `Invalid values: ${valueString}`;
return;
}

const exclude = operator === "ne" || operator === "nin";

initDimensions.set(dimension, {
exclude,
values,
});
} else {
errorMessage = `Invalid condition. Expected format: <dimension> <eq|ne> ('<value>', '<value>')`;
}
});

return {
initDimensions,
errorMessage,
};
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
getFullUrlForPath,
getUrlForPath,
} from "@rilldata/web-common/lib/url-utils";
} from "@rilldata/web-common/lib/url/url-utils";
import type { Page } from "@sveltejs/kit";
import { Readable, writable } from "svelte/store";
import { beforeAll, describe, it, SpyInstance, vi, expect } from "vitest";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export function getUrlForPath(path: string, retainParams = ["features"]): URL {
const newUrl = new URL(`${url.protocol}//${url.host}${path}`);

for (const param of retainParams) {
if (!url.searchParams.has(param)) continue;
newUrl.searchParams.set(param, url.searchParams.get(param));
const value = url.searchParams.get(param);
if (!value) continue;
newUrl.searchParams.set(param, value);
}
return newUrl;
}
Expand Down
18 changes: 17 additions & 1 deletion web-local/src/routes/(viz)/dashboard/[name]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import { useQueryClient } from "@tanstack/svelte-query";
import type { PageData } from "./$types";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { onMount } from "svelte";
import { asyncWait } from "@rilldata/web-common/lib/waitUtils";
import { useDashboard } from "@rilldata/web-common/features/dashboards/selectors";
import { selectedMockUserStore } from "@rilldata/web-common/features/dashboards/granular-access-policies/stores";

Expand All @@ -21,6 +24,7 @@

$: metricsViewName = data.metricsView.meta?.name?.name as string;

$: ({ initDimensions, errorMessage, filter } = data);
$: ({ instanceId } = $runtime);

$: filePaths = data.metricsView.meta?.filePaths as string[];
Expand All @@ -30,6 +34,18 @@
(error) => filePaths.includes(error.filePath as string),
);

onMount(async () => {
if (errorMessage) {
await asyncWait(300);
eventBus.emit("notification", {
message: errorMessage,
detail: `Filter: ${filter ?? ""}`,
type: "error",
options: { persisted: true },
});
}
});

$: dashboard = useDashboard(instanceId, metricsViewName);
$: mockUserHasNoAccess =
$selectedMockUserStore && $dashboard.error?.response?.status === 404;
Expand Down Expand Up @@ -57,7 +73,7 @@
<DashboardStateProvider metricViewName={metricsViewName}>
<DashboardURLStateProvider metricViewName={metricsViewName}>
<DashboardThemeProvider>
<Dashboard metricViewName={metricsViewName} />
<Dashboard metricViewName={metricsViewName} {initDimensions} />
</DashboardThemeProvider>
</DashboardURLStateProvider>
</DashboardStateProvider>
Expand Down
41 changes: 40 additions & 1 deletion web-local/src/routes/(viz)/dashboard/[name]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { error } from "@sveltejs/kit";
import type { QueryFunction } from "@tanstack/svelte-query";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { get } from "svelte/store";
import { parseFilterString } from "@rilldata/web-common/lib/url/parsing";

export const load = async ({ params, depends }) => {
let ran = false;

export const load = async ({ params, depends, url }) => {
const { instanceId } = get(runtime);

const dashboardName = params.name;
Expand Down Expand Up @@ -43,11 +46,47 @@ export const load = async ({ params, depends }) => {
throw error(404, "Dashboard not found");
}

const spec = metricsViewResource?.metricsView?.spec;
const state = metricsViewResource?.metricsView?.state;

if (!spec || !state || !dashboardName) {
throw error(404, "Metrics view not found");
}

if (ran) {
return {
metricsView: metricsViewResource,
initDimensions: new Map(),
};
}

ran = true;

const searchParams = new URLSearchParams(url.searchParams);

const filter = searchParams.get("filter") ?? "";

const dimensions = spec.dimensions ?? [];

const dimensionNames = dimensions.map(({ name }) => name).filter(isDefined);

const { initDimensions, errorMessage } = parseFilterString(
filter,
dimensionNames,
);

return {
metricsView: metricsViewResource,
initDimensions,
errorMessage,
filter,
};
} catch (e) {
console.error(e);
throw error(404, "Dashboard not found");
}
};

function isDefined(value: string | undefined): value is string {
return value !== undefined;
}
Loading