Skip to content

Commit

Permalink
Add filtering and sorting to deployments table (#16686)
Browse files Browse the repository at this point in the history
  • Loading branch information
desertaxle authored Jan 13, 2025
1 parent 7e04a50 commit 00c8bb2
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 28 deletions.
4 changes: 2 additions & 2 deletions ui-v2/src/api/deployments/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { components } from "@/api/prefect";
import type { components } from "@/api/prefect";
import { getQueryService } from "@/api/service";
import { queryOptions } from "@tanstack/react-query";

Expand Down Expand Up @@ -108,6 +108,6 @@ export const buildCountDeploymentsQuery = (
const res = await getQueryService().POST("/deployments/count", {
body: filter,
});
return res.data ?? [];
return res.data ?? 0;
},
});
2 changes: 1 addition & 1 deletion ui-v2/src/components/deployments/data-table/cells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { FlowRunActivityBarChart } from "@/components/ui/flow-run-acitivity-bar-graph";
import { FlowRunActivityBarChart } from "@/components/ui/flow-run-activity-bar-graph";
import { Icon } from "@/components/ui/icons";
import useDebounce from "@/hooks/use-debounce";
import { useToast } from "@/hooks/use-toast";
Expand Down
94 changes: 94 additions & 0 deletions ui-v2/src/components/deployments/data-table/data-table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createFakeFlowRunWithDeploymentAndFlow } from "@/mocks/create-fake-flow
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { buildApiUrl, createWrapper, server } from "@tests/utils";
import { mockPointerEvents } from "@tests/utils/browser";
import { HttpResponse } from "msw";
import { http } from "msw";
import { beforeEach, describe, expect, it, vi } from "vitest";
Expand Down Expand Up @@ -40,12 +41,17 @@ describe("DeploymentsDataTable", () => {

const defaultProps = {
deployments: [mockDeployment],
currentDeploymentsCount: 1,
pageCount: 5,
pagination: {
pageSize: 10,
pageIndex: 2,
},
sort: "NAME_ASC" as const,
columnFilters: [],
onPaginationChange: vi.fn(),
onSortChange: vi.fn(),
onColumnFiltersChange: vi.fn(),
onQuickRun: vi.fn(),
onCustomRun: vi.fn(),
onEdit: vi.fn(),
Expand Down Expand Up @@ -119,6 +125,7 @@ describe("DeploymentsDataTable", () => {
expect(screen.getByText("Second Deployment")).toBeInTheDocument();
expect(screen.getByText("test-flow")).toBeInTheDocument();
expect(screen.getByText("second-flow")).toBeInTheDocument();
expect(screen.getByText("1 Deployment")).toBeInTheDocument();
});

it("calls onQuickRun when quick run action is clicked", async () => {
Expand Down Expand Up @@ -242,4 +249,91 @@ describe("DeploymentsDataTable", () => {
pageSize: 10,
});
});

it("calls onSortChange when sort is changed", async () => {
const user = userEvent.setup();

mockPointerEvents();
const onSortChange = vi.fn();
render(
<DeploymentsDataTable {...defaultProps} onSortChange={onSortChange} />,
{
wrapper: createWrapper(),
},
);

const select = screen.getByRole("combobox", {
name: "Deployment sort order",
});
await userEvent.click(select);
await userEvent.click(screen.getByText("Created"));

expect(onSortChange).toHaveBeenCalledWith("CREATED_DESC");

await user.click(select);
await user.click(screen.getByText("Updated"));
expect(onSortChange).toHaveBeenCalledWith("UPDATED_DESC");

await user.click(select);
await user.click(screen.getByText("Z to A"));
expect(onSortChange).toHaveBeenCalledWith("NAME_DESC");
});

it("calls onColumnFiltersChange on deployment name search", async () => {
const user = userEvent.setup();

const onColumnFiltersChange = vi.fn();
render(
<DeploymentsDataTable
{...defaultProps}
columnFilters={[{ id: "flowOrDeploymentName", value: "start value" }]}
onColumnFiltersChange={onColumnFiltersChange}
/>,
{
wrapper: createWrapper(),
},
);

// Clear any initial calls from mounting
onColumnFiltersChange.mockClear();

const nameSearchInput = screen.getByPlaceholderText("Search deployments");
expect(nameSearchInput).toHaveValue("start value");

await user.clear(nameSearchInput);
await user.type(nameSearchInput, "my-deployment");

expect(onColumnFiltersChange).toHaveBeenCalledWith([
{ id: "flowOrDeploymentName", value: "my-deployment" },
]);
});

it("calls onColumnFiltersChange on tags search", async () => {
const user = userEvent.setup();

const onColumnFiltersChange = vi.fn();
render(
<DeploymentsDataTable
{...defaultProps}
columnFilters={[{ id: "tags", value: ["tag3"] }]}
onColumnFiltersChange={onColumnFiltersChange}
/>,
{
wrapper: createWrapper(),
},
);

// Clear any initial calls from mounting
onColumnFiltersChange.mockClear();

const tagsSearchInput = screen.getByPlaceholderText("Filter by tags");
expect(await screen.findByText("tag3")).toBeVisible();

await user.type(tagsSearchInput, "tag4");
await user.keyboard("{enter}");

expect(onColumnFiltersChange).toHaveBeenCalledWith([
{ id: "tags", value: ["tag3", "tag4"] },
]);
});
});
102 changes: 97 additions & 5 deletions ui-v2/src/components/deployments/data-table/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import type { DeploymentWithFlow } from "@/api/deployments";
import type { components } from "@/api/prefect";
import { DataTable } from "@/components/ui/data-table";
import { FlowRunActivityBarGraphTooltipProvider } from "@/components/ui/flow-run-acitivity-bar-graph";
import { FlowRunActivityBarGraphTooltipProvider } from "@/components/ui/flow-run-activity-bar-graph";
import { Icon } from "@/components/ui/icons";
import { SearchInput } from "@/components/ui/input";
import { ScheduleBadgeGroup } from "@/components/ui/schedule-badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { StatusBadge } from "@/components/ui/status-badge";
import { TagBadgeGroup } from "@/components/ui/tag-badge-group";
import type { OnChangeFn, PaginationState } from "@tanstack/react-table";
import { TagsInput } from "@/components/ui/tags-input";
import type {
ColumnFiltersState,
OnChangeFn,
PaginationState,
} from "@tanstack/react-table";
import {
createColumnHelper,
getCoreRowModel,
Expand All @@ -16,9 +30,14 @@ import { ActionsCell, ActivityCell } from "./cells";

type DeploymentsDataTableProps = {
deployments: DeploymentWithFlow[];
currentDeploymentsCount: number;
pageCount: number;
pagination: PaginationState;
sort: components["schemas"]["DeploymentSort"];
columnFilters: ColumnFiltersState;
onPaginationChange: (pagination: PaginationState) => void;
onSortChange: (sort: components["schemas"]["DeploymentSort"]) => void;
onColumnFiltersChange: (columnFilters: ColumnFiltersState) => void;
onQuickRun: (deployment: DeploymentWithFlow) => void;
onCustomRun: (deployment: DeploymentWithFlow) => void;
onEdit: (deployment: DeploymentWithFlow) => void;
Expand Down Expand Up @@ -120,15 +139,50 @@ const createColumns = ({

export const DeploymentsDataTable = ({
deployments,
currentDeploymentsCount,
pagination,
pageCount,
sort,
columnFilters,
onPaginationChange,
onSortChange,
onColumnFiltersChange,
onQuickRun,
onCustomRun,
onEdit,
onDelete,
onDuplicate,
}: DeploymentsDataTableProps) => {
const nameSearchValue = (columnFilters.find(
(filter) => filter.id === "flowOrDeploymentName",
)?.value ?? "") as string;
const tagsSearchValue = (columnFilters.find((filter) => filter.id === "tags")
?.value ?? []) as string[];

const handleNameSearchChange = useCallback(
(value?: string) => {
const filters = columnFilters.filter(
(filter) => filter.id !== "flowOrDeploymentName",
);
onColumnFiltersChange(
value ? [...filters, { id: "flowOrDeploymentName", value }] : filters,
);
},
[onColumnFiltersChange, columnFilters],
);

const handleTagsSearchChange: React.ChangeEventHandler<HTMLInputElement> &
((tags: string[]) => void) = useCallback(
(e: string[] | React.ChangeEvent<HTMLInputElement>) => {
const tags = Array.isArray(e) ? e : e.target.value;
const filters = columnFilters.filter((filter) => filter.id !== "tags");
onColumnFiltersChange(
tags.length ? [...filters, { id: "tags", value: tags }] : filters,
);
},
[onColumnFiltersChange, columnFilters],
);

const handlePaginationChange: OnChangeFn<PaginationState> = useCallback(
(updater) => {
let newPagination = pagination;
Expand Down Expand Up @@ -163,8 +217,46 @@ export const DeploymentsDataTable = ({
onPaginationChange: handlePaginationChange,
});
return (
<FlowRunActivityBarGraphTooltipProvider>
<DataTable table={table} />
</FlowRunActivityBarGraphTooltipProvider>
<div>
<div className="grid sm:grid-cols-2 md:grid-cols-6 lg:grid-cols-12 gap-2 pb-4 items-center">
<div className="sm:col-span-2 md:col-span-6 lg:col-span-4 order-last lg:order-first">
<p className="text-sm text-muted-foreground">
{currentDeploymentsCount} Deployment
{currentDeploymentsCount === 1 ? "" : "s"}
</p>
</div>
<div className="sm:col-span-2 md:col-span-2 lg:col-span-3">
<SearchInput
placeholder="Search deployments"
value={nameSearchValue}
onChange={(e) => handleNameSearchChange(e.target.value)}
/>
</div>
<div className="xs:col-span-1 md:col-span-2 lg:col-span-3">
<TagsInput
placeholder="Filter by tags"
onChange={handleTagsSearchChange}
value={tagsSearchValue}
/>
</div>
<div className="xs:col-span-1 md:col-span-2 lg:col-span-2">
<Select value={sort} onValueChange={onSortChange}>
<SelectTrigger aria-label="Deployment sort order">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CREATED_DESC">Created</SelectItem>
<SelectItem value="UPDATED_DESC">Updated</SelectItem>
<SelectItem value="NAME_ASC">A to Z</SelectItem>
<SelectItem value="NAME_DESC">Z to A</SelectItem>
</SelectContent>
</Select>
</div>
</div>

<FlowRunActivityBarGraphTooltipProvider>
<DataTable table={table} />
</FlowRunActivityBarGraphTooltipProvider>
</div>
);
};
Loading

0 comments on commit 00c8bb2

Please sign in to comment.