Skip to content
Open
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
3 changes: 2 additions & 1 deletion apps/portal/src/components/ui/stories.play.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import * as tabs from "./tabs.stories";
import * as textarea from "./textarea.stories";
import * as tooltip from "./tooltip.stories";
import * as sonner from "./sonner.stories";
import * as runsIterationFilter from "../../pages/RunsList.stories";

const modules: Record<string, Record<string, unknown>> = {
alert, alertDialog, checkbox, dialog, dropdownMenu, label, popover, scrollArea,
select, separator, sheet, skeleton, switch: sw, table, tabs, textarea,
tooltip, sonner,
tooltip, sonner, runsIterationFilter,
};

afterEach(() => cleanup());
Expand Down
56 changes: 56 additions & 0 deletions apps/portal/src/pages/RunsList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent } from "storybook/test";
import { NumericComparatorRow } from "./RunsList";

function EmptyIterationFilter() {
const [op, setOp] = useState<"eq" | "gte" | "lte">("gte");
const [value, setValue] = useState("");

return (
<div className="w-72 space-y-2">
<NumericComparatorRow
label="Turns"
ariaLabel="Filter runs by number of turns"
op={op}
value={value}
onChange={(next) => {
setOp(next.op);
setValue(next.value);
}}
/>
<output data-testid="iteration-filter">{`${op}:${value || "any"}`}</output>
</div>
);
}

const meta = {
title: "Pages/Runs/Iteration filter",
component: NumericComparatorRow,
tags: ["ai-generated", "needs-work"],
} satisfies Meta<typeof NumericComparatorRow>;

export default meta;
type Story = StoryObj<typeof meta>;

export const ChooseComparisonBeforeValue: Story = {
args: {
label: "Turns",
ariaLabel: "Filter runs by number of turns",
op: "gte",
value: "",
onChange: () => {},
},
render: () => <EmptyIterationFilter />,
play: async ({ canvas }) => {
await userEvent.click(canvas.getByRole("combobox", { name: "Filter runs by number of turns comparator" }));
await userEvent.click(await screen.findByRole("option", { name: "≤" }));
await expect(canvas.getByTestId("iteration-filter")).toHaveTextContent("lte:any");

await userEvent.type(canvas.getByRole("spinbutton", { name: "Filter runs by number of turns" }), "3");
await expect(canvas.getByTestId("iteration-filter")).toHaveTextContent("lte:3");
},
};
8 changes: 4 additions & 4 deletions apps/portal/src/pages/RunsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ function GroupByToggle({
}

// Sidebar filter widget: pick a comparator (≥ / ≤ / =) and a numeric value.
// Clearing the input clears both the value and the op from the URL. Used for
// The op is retained without a value so users can choose it before typing. Used for
// the Turns and Max iterations filters; mirrors the API contract that accepts
// `{turns, turnsOp}` / `{maxIterations, maxIterationsOp}` with op ∈ eq|gte|lte.
const NUMERIC_OPS: ReadonlyArray<{ value: IterationOp; label: string; aria: string }> = [
Expand All @@ -392,7 +392,7 @@ const NUMERIC_OPS: ReadonlyArray<{ value: IterationOp; label: string; aria: stri
{ value: "eq", label: "=", aria: "equal to" },
];

function NumericComparatorRow({
export function NumericComparatorRow({
label,
ariaLabel,
op,
Expand Down Expand Up @@ -1981,7 +1981,7 @@ export function RunsList() {
onChange={({ op, value }) => {
state.setFilters({
turns: value || null,
turnsOp: value ? op : null,
turnsOp: op,
});
}}
/>
Expand All @@ -1993,7 +1993,7 @@ export function RunsList() {
onChange={({ op, value }) => {
state.setFilters({
maxIter: value || null,
maxIterOp: value ? op : null,
maxIterOp: op,
});
}}
/>
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture/app-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,10 @@ A single value compiles to an equality clause; multiple values compile to `$in`.
| `criteria` | `scenario.criteria` |
| `turns` / `maxIterations` | `run.turns` size / `maxIterations` |

In the Portal's Iterations filter, the comparison operator can be selected
before a number is entered. The operator is kept in URL state while the value is
empty; the API query includes it only when the corresponding number is present.

Additional cross-cutting filters:

- **`(Unknown)` sentinel** — the literal value `__empty__` (exported as `EMPTY_FILTER_VALUE`)
Expand Down