diff --git a/src/components/shared/PipelineRunFiltersBar/PipelineRunFiltersBar.tsx b/src/components/shared/PipelineRunFiltersBar/PipelineRunFiltersBar.tsx index 726813ea6..b221a6d59 100644 --- a/src/components/shared/PipelineRunFiltersBar/PipelineRunFiltersBar.tsx +++ b/src/components/shared/PipelineRunFiltersBar/PipelineRunFiltersBar.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import type { DateRange } from "react-day-picker"; import { AnnotationFilterInput } from "@/components/shared/AnnotationFilterInput/AnnotationFilterInput"; +import { StatusFilterSelect } from "@/components/shared/StatusFilterSelect/StatusFilterSelect"; import { Button } from "@/components/ui/button"; import { DatePickerWithRange } from "@/components/ui/date-picker"; import { Icon } from "@/components/ui/icon"; @@ -72,6 +73,13 @@ export function PipelineRunFiltersBar() { )} +
+ setFilter("status", value)} + /> +
+
{ + describe("rendering", () => { + it("should render with default placeholder when no value", () => { + render(); + + expect(screen.getByRole("combobox")).toBeInTheDocument(); + expect(screen.getByText("All statuses")).toBeInTheDocument(); + }); + + it("should render with Running status", () => { + render(); + + expect(screen.getByText("Running")).toBeInTheDocument(); + }); + + it("should render with Succeeded status", () => { + render(); + + expect(screen.getByText("Succeeded")).toBeInTheDocument(); + }); + + it("should render with Failed status", () => { + render(); + + expect(screen.getByText("Failed")).toBeInTheDocument(); + }); + + it("should render with Pending status", () => { + render(); + + expect(screen.getByText("Pending")).toBeInTheDocument(); + }); + + it("should render with Cancelled status", () => { + render(); + + expect(screen.getByText("Cancelled")).toBeInTheDocument(); + }); + + it("should render with System error status", () => { + render(); + + expect(screen.getByText("System error")).toBeInTheDocument(); + }); + }); + + describe("visual indicator", () => { + it("should show visual indicator when filter is active", () => { + const { container } = render( + , + ); + + const trigger = container.querySelector("button"); + expect(trigger).toHaveClass("ring-2"); + }); + + it("should not show visual indicator when no filter", () => { + const { container } = render( + , + ); + + const trigger = container.querySelector("button"); + expect(trigger).not.toHaveClass("ring-2"); + }); + }); + + describe("custom className", () => { + it("should apply custom className to trigger", () => { + const { container } = render( + , + ); + + const trigger = container.querySelector("button"); + expect(trigger).toHaveClass("custom-class"); + }); + }); +}); diff --git a/src/components/shared/StatusFilterSelect/StatusFilterSelect.tsx b/src/components/shared/StatusFilterSelect/StatusFilterSelect.tsx new file mode 100644 index 000000000..d038eb5d1 --- /dev/null +++ b/src/components/shared/StatusFilterSelect/StatusFilterSelect.tsx @@ -0,0 +1,65 @@ +import type { ContainerExecutionStatus } from "@/api/types.gen"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { + EXECUTION_STATUS_LABELS, + getExecutionStatusLabel, +} from "@/utils/executionStatus"; + +function isValidStatus(value: string): value is ContainerExecutionStatus { + return value in EXECUTION_STATUS_LABELS; +} + +const STATUS_OPTIONS = Object.keys(EXECUTION_STATUS_LABELS).filter( + isValidStatus, +); + +interface StatusFilterSelectProps { + value: ContainerExecutionStatus | undefined; + onChange: (value: ContainerExecutionStatus | undefined) => void; + className?: string; +} + +export function StatusFilterSelect({ + value, + onChange, + className, +}: StatusFilterSelectProps) { + const handleValueChange = (newValue: string) => { + if (newValue === "all") { + onChange(undefined); + } else if (isValidStatus(newValue)) { + onChange(newValue); + } + }; + + const hasActiveFilter = value !== undefined; + + return ( + + ); +}