Skip to content

Commit 0f40e2c

Browse files
committed
Status filter
1 parent d548dd0 commit 0f40e2c

3 files changed

Lines changed: 152 additions & 1 deletion

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import { StatusFilterSelect } from "./StatusFilterSelect";
5+
6+
describe("StatusFilterSelect", () => {
7+
describe("rendering", () => {
8+
it("should render with default placeholder when no value", () => {
9+
render(<StatusFilterSelect value={undefined} onChange={vi.fn()} />);
10+
11+
expect(screen.getByRole("combobox")).toBeInTheDocument();
12+
expect(screen.getByText("All statuses")).toBeInTheDocument();
13+
});
14+
15+
it("should render with Running status", () => {
16+
render(<StatusFilterSelect value="RUNNING" onChange={vi.fn()} />);
17+
18+
expect(screen.getByText("Running")).toBeInTheDocument();
19+
});
20+
21+
it("should render with Succeeded status", () => {
22+
render(<StatusFilterSelect value="SUCCEEDED" onChange={vi.fn()} />);
23+
24+
expect(screen.getByText("Succeeded")).toBeInTheDocument();
25+
});
26+
27+
it("should render with Failed status", () => {
28+
render(<StatusFilterSelect value="FAILED" onChange={vi.fn()} />);
29+
30+
expect(screen.getByText("Failed")).toBeInTheDocument();
31+
});
32+
33+
it("should render with Pending status", () => {
34+
render(<StatusFilterSelect value="PENDING" onChange={vi.fn()} />);
35+
36+
expect(screen.getByText("Pending")).toBeInTheDocument();
37+
});
38+
39+
it("should render with Cancelled status", () => {
40+
render(<StatusFilterSelect value="CANCELLED" onChange={vi.fn()} />);
41+
42+
expect(screen.getByText("Cancelled")).toBeInTheDocument();
43+
});
44+
45+
it("should render with System error status", () => {
46+
render(<StatusFilterSelect value="SYSTEM_ERROR" onChange={vi.fn()} />);
47+
48+
expect(screen.getByText("System error")).toBeInTheDocument();
49+
});
50+
});
51+
52+
describe("visual indicator", () => {
53+
it("should show visual indicator when filter is active", () => {
54+
const { container } = render(
55+
<StatusFilterSelect value="FAILED" onChange={vi.fn()} />,
56+
);
57+
58+
const trigger = container.querySelector("button");
59+
expect(trigger).toHaveClass("ring-2");
60+
});
61+
62+
it("should not show visual indicator when no filter", () => {
63+
const { container } = render(
64+
<StatusFilterSelect value={undefined} onChange={vi.fn()} />,
65+
);
66+
67+
const trigger = container.querySelector("button");
68+
expect(trigger).not.toHaveClass("ring-2");
69+
});
70+
});
71+
72+
describe("custom className", () => {
73+
it("should apply custom className to trigger", () => {
74+
const { container } = render(
75+
<StatusFilterSelect
76+
value={undefined}
77+
onChange={vi.fn()}
78+
className="custom-class"
79+
/>,
80+
);
81+
82+
const trigger = container.querySelector("button");
83+
expect(trigger).toHaveClass("custom-class");
84+
});
85+
});
86+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { ContainerExecutionStatus } from "@/api/types.gen";
2+
import {
3+
Select,
4+
SelectContent,
5+
SelectItem,
6+
SelectTrigger,
7+
SelectValue,
8+
} from "@/components/ui/select";
9+
import { cn } from "@/lib/utils";
10+
import {
11+
EXECUTION_STATUS_LABELS,
12+
getExecutionStatusLabel,
13+
} from "@/utils/executionStatus";
14+
15+
function isValidStatus(value: string): value is ContainerExecutionStatus {
16+
return value in EXECUTION_STATUS_LABELS;
17+
}
18+
19+
const STATUS_OPTIONS = Object.keys(EXECUTION_STATUS_LABELS).filter(
20+
isValidStatus,
21+
);
22+
23+
interface StatusFilterSelectProps {
24+
value: ContainerExecutionStatus | undefined;
25+
onChange: (value: ContainerExecutionStatus | undefined) => void;
26+
className?: string;
27+
}
28+
29+
export function StatusFilterSelect({
30+
value,
31+
onChange,
32+
className,
33+
}: StatusFilterSelectProps) {
34+
const handleValueChange = (newValue: string) => {
35+
if (newValue === "all") {
36+
onChange(undefined);
37+
} else if (isValidStatus(newValue)) {
38+
onChange(newValue);
39+
}
40+
};
41+
42+
const hasActiveFilter = value !== undefined;
43+
44+
return (
45+
<Select value={value ?? "all"} onValueChange={handleValueChange}>
46+
<SelectTrigger
47+
className={cn(
48+
"w-40",
49+
hasActiveFilter && "ring-2 ring-primary/20",
50+
className,
51+
)}
52+
>
53+
<SelectValue placeholder="Status" />
54+
</SelectTrigger>
55+
<SelectContent>
56+
<SelectItem value="all">All statuses</SelectItem>
57+
{STATUS_OPTIONS.map((status) => (
58+
<SelectItem key={status} value={status}>
59+
{getExecutionStatusLabel(status)}
60+
</SelectItem>
61+
))}
62+
</SelectContent>
63+
</Select>
64+
);
65+
}

src/utils/executionStatus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
* Note: The mapping is intentionally aligned to the status table from:
1010
* https://github.com/TangleML/tangle-ui/issues/1540
1111
*/
12-
const EXECUTION_STATUS_LABELS: Record<string, string> = {
12+
export const EXECUTION_STATUS_LABELS: Record<string, string> = {
1313
CANCELLED: "Cancelled",
1414
CANCELLING: "Cancelling",
1515
FAILED: "Failed",

0 commit comments

Comments
 (0)