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: measure filter ux #6247

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 3 additions & 2 deletions web-common/src/features/dashboards/filters/Filters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,13 @@
{#each allMeasureFilters as { name, label, dimensionName, filter } (name)}
<div animate:flip={{ duration: 200 }}>
<MeasureFilter
allDimensions={dimensions}
{name}
{label}
{dimensionName}
{filter}
on:remove={() => removeMeasureFilter(dimensionName, name)}
on:apply={({ detail: { dimension, oldDimension, filter } }) =>
onRemove={() => removeMeasureFilter(dimensionName, name)}
onApply={({ dimension, oldDimension, filter }) =>
handleMeasureFilterApply(dimension, name, oldDimension, filter)}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,148 @@
<script lang="ts">
import { Chip } from "@rilldata/web-common/components/chip";
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu/";
import * as Popover from "@rilldata/web-common/components/popover/";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
import TooltipTitle from "@rilldata/web-common/components/tooltip/TooltipTitle.svelte";
import type { MeasureFilterEntry } from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-entry";
import MeasureFilterBody from "@rilldata/web-common/features/dashboards/filters/measure-filters/MeasureFilterBody.svelte";
import MeasureFilterMenu from "@rilldata/web-common/features/dashboards/filters/measure-filters/MeasureFilterMenu.svelte";
import { createEventDispatcher } from "svelte";
import type { MetricsViewSpecDimensionV2 } from "@rilldata/web-common/runtime-client";
import { fly } from "svelte/transition";
import Input from "@rilldata/web-common/components/forms/Input.svelte";
import Select from "@rilldata/web-common/components/forms/Select.svelte";
import { getDimensionDisplayName } from "@rilldata/web-common/features/dashboards/filters/getDisplayName";
import {
MeasureFilterOperation,
MeasureFilterOperationOptions,
MeasureFilterType,
} from "@rilldata/web-common/features/dashboards/filters/measure-filters/measure-filter-options";
import { defaults, superForm } from "sveltekit-superforms";
import { yup } from "sveltekit-superforms/adapters";
import { string, object, mixed } from "yup";
import Button from "@rilldata/web-common/components/button/Button.svelte";

export let dimensionName: string;
export let name: string;
export let label: string | undefined = undefined;
export let filter: MeasureFilterEntry | undefined = undefined;

const dispatch = createEventDispatcher();
export let onRemove: () => void;
export let onApply: (params: {
dimension: string;
oldDimension: string;
filter: MeasureFilterEntry;
}) => void;
export let allDimensions: MetricsViewSpecDimensionV2[];

let active = !filter;

const initialValues = {
dimension: dimensionName,
operation: filter?.operation ?? MeasureFilterOperationOptions[0].value,
value1: filter?.value1 ?? "",
value2: filter?.value2 ?? "",
};

const validationSchema = object().shape({
dimension: string().required("Required"),
operation: mixed<MeasureFilterOperation>()
.oneOf(Object.values(MeasureFilterOperation))
.required("Required"),
value1: string()
.required("Required")
.test("is-numeric", "Value must be a valid number", (value) => {
return !isNaN(Number(value)) && value.trim() !== "";
}),
value2: string().when("operation", {
is: (val: MeasureFilterOperation) => expressionIsBetween(val),
then: (schema) =>
schema
.required("Required")
.test("is-numeric", "Value must be a valid number", (value) => {
return !isNaN(Number(value)) && value.trim() !== "";
}),
otherwise: (schema) => schema.optional(),
}),
});

const { form, errors, submit, enhance, reset } = superForm(
defaults(initialValues, yup(validationSchema)),
{
SPA: true,
validators: yup(validationSchema),
onUpdate({ form }) {
if (!form.valid) return;
const values = form.data;

onApply({
dimension: values.dimension,
oldDimension: dimensionName,
filter: {
measure: name,
operation: values.operation,
type: MeasureFilterType.Value,
value1: values.value1,
value2: values.value2 ?? "",
},
});

active = false;
},
invalidateAll: false,
resetForm: false,
},
);

$: ({ operation } = $form);

$: isBetweenExpression = expressionIsBetween(operation);

$: dimensionOptions =
allDimensions.map((d) => ({
value: d.name as string,
label: getDimensionDisplayName(d),
})) ?? [];

function handleDismiss() {
if (!filter) {
dispatch("remove");
onRemove();
} else {
active = false;
reset({
data: {
dimension: dimensionName,
operation:
filter?.operation ?? MeasureFilterOperationOptions[0].value,
value1: filter?.value1 ?? "",
value2: filter?.value2 ?? "",
},
});
}
}

function expressionIsBetween(op: MeasureFilterOperation | string) {
return (
isMFO(op) &&
briangregoryholmes marked this conversation as resolved.
Show resolved Hide resolved
(op === MeasureFilterOperation.Between ||
op === MeasureFilterOperation.NotBetween)
);
}

function isMFO(value: string): value is MeasureFilterOperation {
return value in MeasureFilterOperation;
}
</script>

<DropdownMenu.Root
<svelte:window
on:keydown={(e) => {
if (e.key === "Escape") {
handleDismiss();
} else if (e.key === "Enter") {
submit();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this on top of onEnter for inputs? Would this interfere with other inputs outside of this form?

}
}}
/>

<Popover.Root
bind:open={active}
onOpenChange={(open) => {
if (!open) {
Expand All @@ -41,7 +155,7 @@
}}
preventScroll
>
<DropdownMenu.Trigger asChild let:builder>
<Popover.Trigger asChild let:builder>
<Tooltip
activeDelay={60}
alignment="start"
Expand All @@ -54,11 +168,18 @@
{active}
builders={[builder]}
{label}
on:remove={() => dispatch("remove")}
on:remove={onRemove}
removable
removeTooltipText="Remove {label}"
>
<MeasureFilterBody {dimensionName} {filter} {label} slot="body" />
<MeasureFilterBody
dimensionName={allDimensions.find((d) => {
return d.name === dimensionName;
})?.displayName ?? ""}
{filter}
{label}
slot="body"
/>
</Chip>
<div slot="tooltip-content" transition:fly={{ duration: 100, y: 4 }}>
<TooltipContent maxWidth="400px">
Expand All @@ -71,18 +192,60 @@
</TooltipContent>
</div>
</Tooltip>
</DropdownMenu.Trigger>

<MeasureFilterMenu
{dimensionName}
{filter}
{name}
on:apply={({ detail: { dimension, oldDimension, filter } }) =>
dispatch("apply", {
dimension,
oldDimension,
filter,
})}
open={active}
/>
</DropdownMenu.Root>
</Popover.Trigger>

<Popover.Content align="start" class="p-2 px-3 w-[250px]">
<form
use:enhance
autocomplete="off"
class="flex flex-col gap-y-3"
id="measure"
>
<Select
bind:value={$form["dimension"]}
id="dimension"
label="By Dimension"
options={dimensionOptions}
placeholder="Select dimension to split by"
/>
<Select
bind:value={$form["operation"]}
onChange={(newValue) => {
if (!expressionIsBetween(newValue)) {
form.update(
($form) => {
$form.value2 = "";
return $form;
},
{
taint: false,
},
);
}
}}
id="operation"
label="Threshold"
options={MeasureFilterOperationOptions}
/>
<Input
bind:value={$form["value1"]}
errors={$errors["value1"]}
id="value1"
onEnter={submit}
placeholder={isBetweenExpression ? "Lower Value" : "Enter a Number"}
/>

{#if isBetweenExpression}
<Input
bind:value={$form["value2"]}
errors={$errors["value2"]}
id="value2"
placeholder="Higher Value"
onEnter={submit}
/>
{/if}

<Button submitForm type="primary" form="measure">Apply</Button>
</form>
</Popover.Content>
</Popover.Root>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
export let dimensionName: string;
export let label: string | undefined;
export let filter: MeasureFilterEntry | undefined;
export let labelMaxWidth = "160px";
export let labelMaxWidth = "320px";
export let comparisonLabel = "";

let typeLabel: string | undefined;
Expand Down
Loading
Loading