From 44dd8fe6e0819c192389bcd110e62da3d5cb8e34 Mon Sep 17 00:00:00 2001 From: Marcelo Robert Santos Date: Wed, 13 Nov 2024 15:47:29 -0300 Subject: [PATCH] feat: frontend header filtering Closes #500 --- dashboard/src/api/hardwareDetails.ts | 3 +- .../pages/hardwareDetails/HardwareDetails.tsx | 30 ++++- .../HardwareDetailsHeaderTable.tsx | 126 +++++++++++++----- .../src/routes/hardware/$hardwareId/route.tsx | 1 + .../src/types/hardware/hardwareDetails.ts | 47 +++++++ 5 files changed, 170 insertions(+), 37 deletions(-) diff --git a/dashboard/src/api/hardwareDetails.ts b/dashboard/src/api/hardwareDetails.ts index 707e4916..84bf6174 100644 --- a/dashboard/src/api/hardwareDetails.ts +++ b/dashboard/src/api/hardwareDetails.ts @@ -26,8 +26,9 @@ export const useHardwareDetails = ( hardwareId: string, intervalInDays: number, origin: TOrigins, + selectedIndexes: number[], ): UseQueryResult => { - const params = { intervalInDays, origin }; + const params = { intervalInDays, origin, selectedIndexes }; return useQuery({ queryKey: ['HardwareDetails', hardwareId, params], queryFn: () => fetchHardwareDetails(hardwareId, params), diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx index 18f73527..8dec7a49 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx @@ -1,7 +1,9 @@ -import { useParams, useSearch } from '@tanstack/react-router'; +import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; import { FormattedMessage } from 'react-intl'; +import { useCallback } from 'react'; + import { Breadcrumb, BreadcrumbItem, @@ -18,13 +20,31 @@ import { HardwareHeader } from './HardwareDetailsHeaderTable'; import HardwareDetailsTabs from './Tabs/HardwareDetailsTabs'; function HardwareDetails(): JSX.Element { + const { intervalInDays, origin } = useSearch({ from: '/hardware' }); const searchParams = useSearch({ from: '/hardware/$hardwareId' }); + const { treeFilter: treeIndexes } = searchParams; + const { hardwareId } = useParams({ from: '/hardware/$hardwareId' }); - const { intervalInDays, origin } = useSearch({ from: '/hardware' }); + + const navigate = useNavigate({ from: '/hardware/$hardwareId' }); + + const updateTreeFilters = useCallback( + (selectedIndexes?: number[]) => { + navigate({ + search: previousSearch => ({ + ...previousSearch, + treeFilter: selectedIndexes, + }), + }); + }, + [navigate], + ); + const { data, isLoading } = useHardwareDetails( hardwareId, intervalInDays, origin, + treeIndexes ?? [], ); if (isLoading || !data) @@ -61,7 +81,11 @@ function HardwareDetails(): JSX.Element {
- + void; } const columns: ColumnDef[] = [ - // { - // id: 'select', - // header: ({ table }) => ( - // - // ), - // cell: ({ row }) => ( - // - // ), - // }, + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row, table }) => ( + + ), + }, { accessorKey: 'treeName', header: ({ column }): JSX.Element => @@ -103,12 +111,31 @@ const sanitizeTreeItems = (treeItems: Record[]): Trees[] => { treeName: tree['tree_name'] ?? '-', gitRepositoryBranch: tree['git_repository_branch'] ?? '-', gitCommitName: tree['git_commit_name'] ?? '-', - gitCommitHash: tree['git_commit_hash'], + gitCommitHash: tree['git_commit_hash'] ?? '-', gitRepositoryUrl: tree['git_repository_url'] ?? '-', })); }; -export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { +const indexesFromRowSelection = ( + rowSelection: RowSelectionState, + maxTreeItems: number, +): number[] | undefined => { + const rowSelectionValues = Object.values(rowSelection); + if ( + rowSelectionValues.length === maxTreeItems || + rowSelectionValues.length === 0 + ) { + return undefined; + } else { + return Object.keys(rowSelection).map(v => parseInt(v)); + } +}; + +export function HardwareHeader({ + treeItems, + selectedIndexes = [], + updateTreeFilters, +}: IHardwareHeader): JSX.Element { const [sorting, setSorting] = useState([ { id: 'treeName', desc: false }, ]); @@ -116,10 +143,43 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { pageIndex: 0, pageSize: 5, }); - // const [rowSelection, setRowSelection] = useState({}); + + const initialRowSelection = useMemo(() => { + if (selectedIndexes.length === 0) { + return Object.fromEntries( + Array.from({ length: treeItems.length }, (_, i) => [ + i.toString(), + true, + ]), + ); + } else { + return Object.fromEntries( + Array.from(selectedIndexes, treeIndex => [treeIndex.toString(), true]), + ); + } + }, [selectedIndexes, treeItems]); + + const [rowSelection, setRowSelection] = useState(initialRowSelection); + + useEffect(() => { + const handler = setTimeout(() => { + const updatedSelection = indexesFromRowSelection( + rowSelection, + treeItems.length, + ); + updateTreeFilters(updatedSelection); + }, DEBOUNCE_INTERVAL); + return (): void => { + clearTimeout(handler); + }; + }, [rowSelection, treeItems.length, updateTreeFilters]); const data = useMemo(() => { - return sanitizeTreeItems(treeItems); + return sanitizeTreeItems(treeItems).sort((a, b) => { + const aKey = a.treeName! + a.gitRepositoryBranch! + a.gitRepositoryUrl!; + const bKey = b.treeName! + b.gitRepositoryBranch! + b.gitRepositoryUrl!; + return aKey.localeCompare(bKey); + }); }, [treeItems]); const table = useReactTable({ @@ -131,12 +191,12 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { getPaginationRowModel: getPaginationRowModel(), onPaginationChange: setPagination, getFilteredRowModel: getFilteredRowModel(), - // enableRowSelection: false, - // onRowSelectionChange: setRowSelection, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, state: { sorting, pagination, - // rowSelection, + rowSelection, }, }); @@ -153,7 +213,7 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { }); // TODO: remove exhaustive-deps and change memo (all tables) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [groupHeaders, sorting /*, rowSelection*/]); + }, [groupHeaders, sorting, rowSelection]); const modelRows = table.getRowModel().rows; const tableRows = useMemo((): JSX.Element[] | JSX.Element => { @@ -175,7 +235,7 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [modelRows /*, rowSelection*/]); + }, [modelRows, rowSelection]); return (
diff --git a/dashboard/src/routes/hardware/$hardwareId/route.tsx b/dashboard/src/routes/hardware/$hardwareId/route.tsx index 22062643..aab63b2b 100644 --- a/dashboard/src/routes/hardware/$hardwareId/route.tsx +++ b/dashboard/src/routes/hardware/$hardwareId/route.tsx @@ -6,6 +6,7 @@ import { zPossibleValidator, zTableFilterInfo } from '@/types/tree/TreeDetails'; const hardwareDetailsSearchSchema = z.object({ currentPageTab: zPossibleValidator, + treeFilter: z.array(z.number().int()).optional(), tableFilter: zTableFilterInfo.catch({ bootsTable: 'all', buildsTable: 'all', diff --git a/dashboard/src/types/hardware/hardwareDetails.ts b/dashboard/src/types/hardware/hardwareDetails.ts index 9b556967..85a80375 100644 --- a/dashboard/src/types/hardware/hardwareDetails.ts +++ b/dashboard/src/types/hardware/hardwareDetails.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import type { ArchCompilerStatus, BuildsTabBuild, @@ -51,3 +53,48 @@ export type THardwareDetails = { boots: Tests; trees: Trees[]; }; + +// TODO: move to general types +const zFilterBoolValue = z.record(z.boolean()).optional(); +const zFilterNumberValue = z.number().optional(); + +export const zFilterObjectsKeys = z.enum([ + 'configs', + 'archs', + 'compilers', + 'buildStatus', + 'bootStatus', + 'testStatus', +]); +export const zFilterNumberKeys = z.enum([ + 'buildDurationMin', + 'buildDurationMax', + 'bootDurationMin', + 'bootDurationMax', + 'testDurationMin', + 'testDurationMax', +]); + +export type TFilterKeys = + | z.infer + | z.infer; + +export const zDiffFilter = z + .union([ + z.object({ + configs: zFilterBoolValue, + archs: zFilterBoolValue, + buildStatus: zFilterBoolValue, + compilers: zFilterBoolValue, + bootStatus: zFilterBoolValue, + testStatus: zFilterBoolValue, + buildDurationMax: zFilterNumberValue, + buildDurationMin: zFilterNumberValue, + bootDurationMin: zFilterNumberValue, + bootDurationMax: zFilterNumberValue, + testDurationMin: zFilterNumberValue, + testDurationMax: zFilterNumberValue, + } satisfies Record), + z.record(z.never()), + ]) + .catch({});