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

Alerts UI: handle incomplete Criteria #4217

Merged
merged 5 commits into from
Mar 3, 2024
Merged
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 @@ -23,7 +23,11 @@
</span>
</div>
<div class="px-4 py-1.5 border-l border-gray-600 text-sm">
<a href={link.href} on:click={() => dispatch("clear")}>{link.text}</a>
<a
class="text-primary-300 hover:text-primary-200"
href={link.href}
on:click={() => dispatch("clear")}>{link.text}</a
>
</div>
<div class="px-2.5 py-1.5 border-l border-gray-600">
<IconButton on:click={() => dispatch("clear")} bgDark>
Expand Down
16 changes: 16 additions & 0 deletions web-common/src/features/alerts/PreviewEmpty.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import TableIcon from "../../components/icons/TableIcon.svelte";

export let topLine: string;
export let bottomLine: string;
</script>

<div class="pt-5 pb-10 flex flex-col justify-center items-center gap-1">
<TableIcon size="32px" className="text-slate-300" />
<div class="flex flex-col justify-center items-center text-sm">
<div class="text-gray-600 font-semibold">{topLine}</div>
<div class="text-gray-500 font-normal">
{bottomLine}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import FormSection from "@rilldata/web-common/components/forms/FormSection.svelte";
import { translateFilter } from "@rilldata/web-common/features/alerts/alert-filter-utils";
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 AlertDataPreview from "@rilldata/web-common/features/alerts/AlertDataPreview.svelte";

export let formState: any; // svelte-forms-lib's FormState

Expand All @@ -17,7 +17,7 @@
<CriteriaGroup {formState} />
</FormSection>
<FormSection title="Alert Preview">
<AlertDataPreview
<AlertPreview
criteria={translateFilter($form["criteria"], $form["criteriaOperation"])}
measure={$form["measure"]}
metricsViewName={$form["metricsViewName"]}
Expand Down
55 changes: 55 additions & 0 deletions web-common/src/features/alerts/criteria-tab/AlertPreview.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script lang="ts">
import { getAlertPreviewData } from "@rilldata/web-common/features/alerts/alert-preview-data";
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
import { useQueryClient } from "@tanstack/svelte-query";
import PreviewTable from "../../../components/preview-table/PreviewTable.svelte";
import type { V1Expression, V1TimeRange } from "../../../runtime-client";
import { isExpressionIncomplete } from "../../dashboards/stores/filter-utils";
import PreviewEmpty from "../PreviewEmpty.svelte";

export let metricsViewName: string;
export let measure: string;
export let splitByDimension: string;
export let splitByTimeGrain: string;
export let whereFilter: V1Expression;
export let criteria: V1Expression;
export let timeRange: V1TimeRange;

const queryClient = useQueryClient();

$: alertPreviewQuery = getAlertPreviewData(queryClient, {
metricsViewName,
measure,
splitByDimension,
splitByTimeGrain,
whereFilter,
criteria,
timeRange,
});

$: isCriteriaEmpty = isExpressionIncomplete(criteria);
</script>

{#if $alertPreviewQuery.isFetching}
<div class="p-2 flex flex-col justify-center">
<Spinner status={EntityStatus.Running} />
</div>
{:else if isCriteriaEmpty || !$alertPreviewQuery.data}
<PreviewEmpty
topLine="No criteria selected"
bottomLine="Select criteria to see a preview"
/>
{:else if $alertPreviewQuery.data.rows.length > 0}
<div class="max-h-64 overflow-auto">
<PreviewTable
rows={$alertPreviewQuery.data.rows}
columnNames={$alertPreviewQuery.data.schema}
/>
</div>
{:else}
<div>
Given the above criteria, this alert will not trigger for the current time
range.
</div>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import Select from "@rilldata/web-common/components/forms/Select.svelte";
import { CriteriaOperationOptions } from "@rilldata/web-common/features/alerts/criteria-tab/operations";
import { parseCriteriaError } from "@rilldata/web-common/features/alerts/criteria-tab/parseCriteriaError";
import { runtime } from "../../../runtime-client/runtime-store";
import { useMetricsView } from "@rilldata/web-common/features/dashboards/selectors";
import { debounce } from "@rilldata/web-common/lib/create-debouncer";
import { slide } from "svelte/transition";
import { runtime } from "../../../runtime-client/runtime-store";

export let formState: any; // svelte-forms-lib's FormState
export let index: number;
Expand Down Expand Up @@ -56,9 +56,9 @@
<Select
id="compareWith"
label=""
options={[{ value: "value" }]}
options={[{ value: "Value" }]}
placeholder="compare with"
value={"value"}
value={"Value"}
/>
<InputV2
alwaysShowError
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import { translateFilter } from "@rilldata/web-common/features/alerts/alert-filter-utils";
import DataPreview from "@rilldata/web-common/features/alerts/data-tab/DataPreview.svelte";
import { AlertIntervalOptions } from "@rilldata/web-common/features/alerts/data-tab/intervals";
import AlertDataPreview from "web-common/src/features/alerts/AlertDataPreview.svelte";
import FormSection from "../../../components/forms/FormSection.svelte";
import InputV2 from "../../../components/forms/InputV2.svelte";
import Select from "../../../components/forms/Select.svelte";
Expand Down Expand Up @@ -92,8 +91,7 @@
/>
</FormSection>
<FormSection title="Data preview">
<AlertDataPreview
criteria={translateFilter($form["criteria"], $form["criteriaOperation"])}
<DataPreview
measure={$form["measure"]}
metricsViewName={$form["metricsViewName"]}
splitByDimension={$form["splitByDimension"]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import Spinner from "@rilldata/web-common/features/entity-management/Spinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
import { useQueryClient } from "@tanstack/svelte-query";
import TableIcon from "../../components/icons/TableIcon.svelte";
import PreviewTable from "../../components/preview-table/PreviewTable.svelte";
import type { V1Expression, V1TimeRange } from "../../runtime-client";

import PreviewTable from "../../../components/preview-table/PreviewTable.svelte";
import type { V1Expression, V1TimeRange } from "../../../runtime-client";
import PreviewEmpty from "../PreviewEmpty.svelte";

export let metricsViewName: string;
export let measure: string;
export let splitByDimension: string;
export let splitByTimeGrain: string;
export let whereFilter: V1Expression;
export let criteria: V1Expression | undefined = undefined;
export let timeRange: V1TimeRange;

const queryClient = useQueryClient();
Expand All @@ -23,7 +23,7 @@
splitByDimension,
splitByTimeGrain,
whereFilter,
criteria,
criteria: undefined,
timeRange,
});
</script>
Expand All @@ -33,15 +33,10 @@
<Spinner status={EntityStatus.Running} />
</div>
{:else if !$alertPreviewQuery.data}
<div class="pt-5 pb-10 flex flex-col justify-center items-center gap-1">
<TableIcon size="32px" className="text-slate-300" />
<div class="flex flex-col justify-center items-center text-sm">
<div class="text-gray-600 font-semibold">No data to preview</div>
<div class="text-gray-500 font-normal">
To see preview, select measures above.
</div>
</div>
</div>
<PreviewEmpty
topLine="No data to preview"
bottomLine="To see a preview, select measures above."
/>
{:else}
<div class="max-h-64 overflow-auto">
<PreviewTable
Expand Down
85 changes: 85 additions & 0 deletions web-common/src/features/dashboards/stores/filter-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import { V1Operation } from "../../../runtime-client";
import { isExpressionIncomplete } from "./filter-utils";

// Mock data for test cases
const testCases = [
{
description: "Simple complete expression with a single condition",
criteria: {
cond: {
op: V1Operation.OPERATION_AND,
exprs: [{ val: "1", ident: "measure_0" }],
},
},
incomplete: false,
},
{
description: "Simple incomplete expression with a missing val",
criteria: {
cond: { op: V1Operation.OPERATION_AND, exprs: [{ ident: "measure_0" }] },
},
incomplete: true,
},
{
description: "Incomplete expression with an unspecified operation",
criteria: { cond: { exprs: [{ val: "1", ident: "measure_0" }] } },
incomplete: true,
},
{
description: "Incomplete expression with an empty string val",
criteria: {
cond: {
op: V1Operation.OPERATION_AND,
exprs: [{ val: "", ident: "measure_0" }],
},
},
incomplete: true,
},
{
description:
"Nested complete expression with one incomplete nested condition",
criteria: {
cond: {
op: V1Operation.OPERATION_AND,
exprs: [
{
cond: {
op: V1Operation.OPERATION_OR,
exprs: [{ val: "1", ident: "measure_0" }, { ident: "measure_1" }],
},
},
],
},
},
incomplete: false,
},
{
description:
"Nested incomplete expression with unspecified operation in nested condition",
criteria: {
cond: {
op: V1Operation.OPERATION_AND,
exprs: [
{
cond: {
exprs: [{ val: "1", ident: "measure_0" }, { ident: "measure_1" }],
},
},
],
},
},
incomplete: true,
},
// Add more test cases as needed
];

// Test suite
describe("isExpressionIncomplete", () => {
testCases.forEach((testCase, index) => {
it(`Test case ${index + 1}: ${testCase.description}`, () => {
const result = isExpressionIncomplete(testCase.criteria);
expect(result).toBe(testCase.incomplete);
});
});
});
44 changes: 43 additions & 1 deletion web-common/src/features/dashboards/stores/filter-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type V1Expression,
V1Condition,
V1Operation,
type V1Expression,
} from "@rilldata/web-common/runtime-client";

export function createLikeExpression(
Expand Down Expand Up @@ -253,3 +254,44 @@ export const sanitiseExpression = (
if (!where?.cond?.exprs?.length) return undefined;
return where;
};

// Check if the operation is unspecified at any level of the condition.
function isOperationUnspecified(cond: V1Condition): boolean {
if (cond.op === V1Operation.OPERATION_UNSPECIFIED || cond.op === undefined) {
return true;
}
// Check nested conditions
return (
cond.exprs?.some(
(expr) => expr.cond && isOperationUnspecified(expr.cond),
) ?? false
);
}

// Check if the val is defined and non-empty at any level of the nested expressions.
function isValDefinedAndNonEmpty(expr: V1Expression): boolean {
if (expr.val !== undefined && expr.val !== "") {
return true; // val is defined and non-empty
}
// If there is a nested condition, check if any nested expression has a defined and non-empty val
return (
expr.cond?.exprs?.some((nestedExpr) =>
isValDefinedAndNonEmpty(nestedExpr),
) ?? false
);
}

export function isExpressionIncomplete(expression: V1Expression): boolean {
// Check the top-level expression's operation
if (expression.cond && isOperationUnspecified(expression.cond)) {
return true; // The top-level operation is unspecified, thus incomplete
}

// If there's no val at the top level, check nested expressions
if (!isValDefinedAndNonEmpty(expression)) {
return true; // No defined and non-empty val found in any expressions, thus incomplete
}

// If the operation is specified and a defined, non-empty val is found, the expression is complete
return false;
}
Loading