Skip to content

Commit

Permalink
[UI v2] feat: Adds search, sort, filtering, and pagination functional…
Browse files Browse the repository at this point in the history
…ity to data table
  • Loading branch information
devinvillarosa committed Feb 19, 2025
1 parent 5ceb3ad commit cd7bbee
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 52 deletions.
2 changes: 1 addition & 1 deletion ui-v2/src/api/task-runs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const buildListTaskRunsQuery = (
* const { data } = useSuspenseQuery(buildListTaskRunsQuery(["id-0", "id-1"]));
* ```
*/
export const buildGetFlowRusTaskRunsCountQuery = (
export const buildGetFlowRunsTaskRunsCountQuery = (
flow_run_ids: Array<string>,
) => {
return queryOptions({
Expand Down
6 changes: 3 additions & 3 deletions ui-v2/src/api/task-runs/task-runs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest";
import {
TaskRun,
TaskRunsFilter,
buildGetFlowRusTaskRunsCountQuery,
buildGetFlowRunsTaskRunsCountQuery,
buildListTaskRunsQuery,
} from ".";

Expand Down Expand Up @@ -75,7 +75,7 @@ describe("task runs api", () => {
});
});

describe("buildGetFlowRusTaskRunsCountQuery", () => {
describe("buildGetFlowRunsTaskRunsCountQuery", () => {
const mockGetFlowRunsTaskRunsCountAPI = (
response: Record<string, number>,
) => {
Expand All @@ -93,7 +93,7 @@ describe("task runs api", () => {

const queryClient = new QueryClient();
const { result } = renderHook(
() => useSuspenseQuery(buildGetFlowRusTaskRunsCountQuery(mockIds)),
() => useSuspenseQuery(buildGetFlowRunsTaskRunsCountQuery(mockIds)),
{ wrapper: createWrapper({ queryClient }) },
);

Expand Down
70 changes: 64 additions & 6 deletions ui-v2/src/components/flow-runs/data-table/data-table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,34 @@ import {
toastDecorator,
} from "@/storybook/utils";
import { faker } from "@faker-js/faker";
import { fn } from "@storybook/test";
import { buildApiUrl } from "@tests/utils/handlers";
import { http, HttpResponse } from "msw";
import { useMemo, useState } from "react";
import { FlowRunsDataTable } from "./data-table";
import { FlowRunState } from "./state-filter";

const MOCK_DATA = [
createFakeFlowRunWithDeploymentAndFlow({
id: "0",
state: { type: "SCHEDULED", id: "0" },
state: { type: "SCHEDULED", name: "Late", id: "0" },
}),
createFakeFlowRunWithDeploymentAndFlow({
id: "1",
state: { type: "COMPLETED", name: "Cached", id: "0" },
}),
createFakeFlowRunWithDeploymentAndFlow({
id: "2",
state: { type: "SCHEDULED", name: "Scheduled", id: "0" },
}),
createFakeFlowRunWithDeploymentAndFlow({
id: "3",
state: { type: "COMPLETED", name: "Completed", id: "0" },
}),
createFakeFlowRunWithDeploymentAndFlow({
id: "4",
state: { type: "FAILED", name: "Failed", id: "0" },
}),
createFakeFlowRunWithDeploymentAndFlow({ id: "1" }),
createFakeFlowRunWithDeploymentAndFlow({ id: "2" }),
createFakeFlowRunWithDeploymentAndFlow({ id: "3" }),
createFakeFlowRunWithDeploymentAndFlow({ id: "4" }),
];

const MOCK_FLOW_RUNS_TASK_COUNT = {
Expand All @@ -34,7 +49,7 @@ const meta: Meta<typeof FlowRunsDataTable> = {
title: "Components/FlowRuns/DataTable/FlowRunsDataTable",
decorators: [routerDecorator, reactQueryDecorator, toastDecorator],
args: { flowRuns: MOCK_DATA, flowRunsCount: MOCK_DATA.length },
component: FlowRunsDataTable,
render: () => <FlowRunDataTableStory />,
parameters: {
msw: {
handlers: [
Expand All @@ -48,3 +63,46 @@ const meta: Meta<typeof FlowRunsDataTable> = {
export default meta;

export const story: StoryObj = { name: "FlowRunsDataTable" };

const FlowRunDataTableStory = () => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);

const [search, setSearch] = useState("");
const [filters, setFilters] = useState<Set<FlowRunState>>(new Set());

const flowRuns = useMemo(() => {
return MOCK_DATA.filter((flowRun) =>
flowRun.name?.toLocaleLowerCase().includes(search.toLowerCase()),
).filter((flowRun) =>
filters.size === 0
? flowRun
: filters.has(flowRun.state?.name as FlowRunState),
);
}, [filters, search]);

return (
<FlowRunsDataTable
flowRuns={flowRuns}
flowRunsCount={flowRuns.length}
pagination={{ pageIndex, pageSize }}
pageCount={Math.ceil(flowRuns.length / pageSize)}
onPaginationChange={(pagination) => {
setPageIndex(pagination.pageIndex);
setPageSize(pagination.pageSize);
}}
filter={{
value: filters,
onSelect: setFilters,
}}
search={{
value: search,
onChange: setSearch,
}}
sort={{
value: "NAME_ASC",
onSelect: fn(),
}}
/>
);
};
14 changes: 13 additions & 1 deletion ui-v2/src/components/flow-runs/data-table/data-table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createWrapper } from "@tests/utils";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { FlowRunsDataTable, type FlowRunsDataTableProps } from "./data-table";

// Wraps component in test with a Tanstack router provider
Expand Down Expand Up @@ -46,6 +46,12 @@ describe("Flow Runs DataTable", () => {
<FlowRunsDataTableRouter
flowRuns={MOCK_DATA}
flowRunsCount={MOCK_DATA.length}
pageCount={5}
pagination={{
pageSize: 10,
pageIndex: 2,
}}
onPaginationChange={vi.fn()}
/>
</>,
{ wrapper: createWrapper() },
Expand Down Expand Up @@ -75,6 +81,12 @@ describe("Flow Runs DataTable", () => {
<FlowRunsDataTableRouter
flowRuns={MOCK_DATA}
flowRunsCount={MOCK_DATA.length}
pageCount={5}
pagination={{
pageSize: 10,
pageIndex: 2,
}}
onPaginationChange={vi.fn()}
/>
</>,
{ wrapper: createWrapper() },
Expand Down
116 changes: 82 additions & 34 deletions ui-v2/src/components/flow-runs/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,6 @@ import { DataTable } from "@/components/ui/data-table";
import { StateBadge } from "@/components/ui/state-badge";
import { TagBadgeGroup } from "@/components/ui/tag-badge-group";

import {
RowSelectionState,
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Suspense, useMemo, useState } from "react";

import { Flow } from "@/api/flows";
import { Checkbox } from "@/components/ui/checkbox";
import { DeleteConfirmationDialog } from "@/components/ui/delete-confirmation-dialog";
Expand All @@ -26,14 +17,24 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Typography } from "@/components/ui/typography";
import { pluralize } from "@/utils";
import { CheckedState } from "@radix-ui/react-checkbox";
import {
OnChangeFn,
PaginationState,
RowSelectionState,
createColumnHelper,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Suspense, useCallback, useMemo, useState } from "react";

import { DeploymentCell } from "./deployment-cell";
import { DurationCell } from "./duration-cell";
import { NameCell } from "./name-cell";
import { ParametersCell } from "./parameters-cell";
import { RunNameSearch } from "./run-name-search";
import { SortFilter } from "./sort-filter";
import { SortFilter, SortFilters } from "./sort-filter";
import { StartTimeCell } from "./start-time-cell";
import { StateFilter } from "./state-filter";
import { FlowRunState, StateFilter } from "./state-filter";
import { TasksCell } from "./tasks-cell";
import { useDeleteFlowRunsDialog } from "./use-delete-flow-runs-dialog";

Expand Down Expand Up @@ -163,11 +164,40 @@ const createColumns = ({
return ret;
};

type PaginationProps = {
pageCount: number;
pagination: PaginationState;
onPaginationChange: (pagination: PaginationState) => void;
};
type SearchProps = {
onChange: (value: string) => void;
value: string;
};
type FilterProps = {
defaultValue?: Set<FlowRunState>;
value: Set<FlowRunState>;
onSelect: (filters: Set<FlowRunState>) => void;
};
type SortProps = {
defaultValue?: SortFilters;
value: SortFilters;
onSelect: (sort: SortFilters) => void;
};

export type FlowRunsDataTableProps = {
search?: SearchProps;
filter?: FilterProps;
sort?: SortProps;
flowRunsCount: number;
flowRuns: Array<FlowRunWithDeploymentAndFlow | FlowRunWithFlow>;
};
} & PaginationProps;
export const FlowRunsDataTable = ({
pageCount,
pagination,
onPaginationChange,
search,
sort,
filter,
flowRunsCount,
flowRuns,
}: FlowRunsDataTableProps) => {
Expand All @@ -181,16 +211,32 @@ export const FlowRunsDataTable = ({
[flowRuns],
);

const handlePaginationChange: OnChangeFn<PaginationState> = useCallback(
(updater) => {
let newPagination = pagination;
if (typeof updater === "function") {
newPagination = updater(pagination);
} else {
newPagination = updater;
}
onPaginationChange(newPagination);
},
[pagination, onPaginationChange],
);

const table = useReactTable({
getRowId: (row) => row.id,
onRowSelectionChange: setRowSelection,
state: { rowSelection },
state: { pagination, rowSelection },
data: flowRuns,
columns: createColumns({
showDeployment,
}),
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), // TODO: use server-side pagination
pageCount,
manualPagination: true,
defaultColumn: { maxSize: 300 },
onPaginationChange: handlePaginationChange,
});

const selectedRows = Object.keys(rowSelection);
Expand Down Expand Up @@ -223,26 +269,28 @@ export const FlowRunsDataTable = ({
</Typography>
)}
</div>
<div className="sm:col-span-2 md:col-span-2 lg:col-span-3">
<RunNameSearch
// TODO
placeholder="Search by run name"
/>
</div>
<div className="xs:col-span-1 md:col-span-2 lg:col-span-3">
<StateFilter
// TODO
selectedFilters={new Set([])}
onSelectFilter={() => {}}
/>
</div>
<div className="xs:col-span-1 md:col-span-2 lg:col-span-2">
<SortFilter
// TODO
value={undefined}
onSelect={() => {}}
/>
</div>
{search && (
<div className="sm:col-span-2 md:col-span-2 lg:col-span-3">
<RunNameSearch
value={search.value}
onChange={(e) => search.onChange(e.target.value)}
placeholder="Search by run name"
/>
</div>
)}
{filter && (
<div className="xs:col-span-1 md:col-span-2 lg:col-span-3">
<StateFilter
selectedFilters={filter.value}
onSelectFilter={filter.onSelect}
/>
</div>
)}
{sort && (
<div className="xs:col-span-1 md:col-span-2 lg:col-span-2">
<SortFilter value={sort.value} onSelect={sort.onSelect} />
</div>
)}
</div>

<DataTable table={table} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { Input, type InputProps } from "@/components/ui/input";
export const RunNameSearch = (props: InputProps) => {
return (
<div className="relative">
<Input placeholder="Search by run name" className="pl-10" {...props} />
<Input
aria-label="search by run name"
placeholder="Search by run name"
className="pl-10"
{...props}
/>
<Icon
id="Search"
className="absolute left-3 top-2.5 text-muted-foreground"
Expand Down
11 changes: 8 additions & 3 deletions ui-v2/src/components/flow-runs/data-table/sort-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ import {
SelectValue,
} from "@/components/ui/select";

type SortFilters =
export type SortFilters =
| "START_TIME_ASC"
| "START_TIME_DESC"
| "NAME_ASC"
| "NAME_DESC";

type SortFilterProps = {
defaultValue?: SortFilters;
onSelect: (filter: SortFilters) => void;
value: undefined | SortFilters;
};

export const SortFilter = ({ value, onSelect }: SortFilterProps) => {
export const SortFilter = ({
defaultValue,
value,
onSelect,
}: SortFilterProps) => {
return (
<Select value={value} onValueChange={onSelect}>
<Select defaultValue={defaultValue} value={value} onValueChange={onSelect}>
<SelectTrigger aria-label="Flow run sort order">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
Expand Down
4 changes: 3 additions & 1 deletion ui-v2/src/components/flow-runs/data-table/state-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ const FLOW_RUN_STATES_MAP = {
const MAX_FILTERS_DISPLAYED = 4;

type StateFilterProps = {
defaultValue?: Set<FlowRunState>;
selectedFilters: Set<FlowRunState> | undefined;
onSelectFilter: (filters: Set<FlowRunState>) => void;
};

export const StateFilter = ({
selectedFilters = new Set(),
defaultValue,
selectedFilters = defaultValue || new Set(),
onSelectFilter,
}: StateFilterProps) => {
const [open, setOpen] = useState(false);
Expand Down
Loading

0 comments on commit cd7bbee

Please sign in to comment.