From 48fef2bc4e85e7c287781033f5b072d41e648a56 Mon Sep 17 00:00:00 2001 From: Marcelo Robert Santos Date: Tue, 19 Nov 2024 09:14:02 -0300 Subject: [PATCH] feat: frontend header filtering Closes #500 --- dashboard/src/api/hardwareDetails.ts | 20 ++- .../pages/hardwareDetails/HardwareDetails.tsx | 54 ++++++- .../HardwareDetailsHeaderTable.tsx | 140 ++++++++++++------ .../src/routes/hardware/$hardwareId/route.tsx | 1 + .../src/types/hardware/hardwareDetails.ts | 47 ++++++ 5 files changed, 209 insertions(+), 53 deletions(-) diff --git a/dashboard/src/api/hardwareDetails.ts b/dashboard/src/api/hardwareDetails.ts index bef03a2a..234028c7 100644 --- a/dashboard/src/api/hardwareDetails.ts +++ b/dashboard/src/api/hardwareDetails.ts @@ -10,6 +10,7 @@ type fetchHardwareDetailsBody = { startTimestampInSeconds: number; endTimestampInSeconds: number; origin: TOrigins; + selectedTrees: Record; }; const fetchHardwareDetails = async ( @@ -24,13 +25,30 @@ const fetchHardwareDetails = async ( return res.data; }; +const mapIndexesToSelectedTrees = ( + selectedIndexes: number[], +): Record => { + return Object.fromEntries( + Array.from(selectedIndexes, index => [index.toString(), 'selected']), + ); +}; + export const useHardwareDetails = ( hardwareId: string, startTimestampInSeconds: number, endTimestampInSeconds: number, origin: TOrigins, + selectedIndexes: number[], ): UseQueryResult => { - const body = { origin, startTimestampInSeconds, endTimestampInSeconds }; + const selectedTrees = mapIndexesToSelectedTrees(selectedIndexes); + + const body: fetchHardwareDetailsBody = { + origin, + startTimestampInSeconds, + endTimestampInSeconds, + selectedTrees, + }; + return useQuery({ queryKey: ['HardwareDetails', hardwareId, body], queryFn: () => fetchHardwareDetails(hardwareId, body), diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx index 322ccd6d..9e458a30 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, useMemo } from 'react'; + import { Breadcrumb, BreadcrumbItem, @@ -14,10 +16,12 @@ import { import { Skeleton } from '@/components/Skeleton'; import { useHardwareDetails } from '@/api/hardwareDetails'; +import type { Trees } from '@/types/hardware/hardwareDetails'; + import { HardwareHeader } from './HardwareDetailsHeaderTable'; import HardwareDetailsTabs from './Tabs/HardwareDetailsTabs'; -const MILISECONDS_IN_ONE_SECOND = 1000; +const MILLISECONDS_IN_ONE_SECOND = 1000; const get_Timestamps = ( intervalInDays: number, @@ -28,29 +32,61 @@ const get_Timestamps = ( const currentDate = new Date(); currentDate.setDate(currentDate.getDate() - intervalInDays); const startTimestampInSeconds = Math.floor( - currentDate.getTime() / MILISECONDS_IN_ONE_SECOND, + currentDate.getTime() / MILLISECONDS_IN_ONE_SECOND, ); const endTimestampInSeconds = Math.floor( - Date.now() / MILISECONDS_IN_ONE_SECOND, + Date.now() / MILLISECONDS_IN_ONE_SECOND, ); return { startTimestampInSeconds, endTimestampInSeconds }; }; +const sanitizeTreeItems = (treeItems: Trees[]): Trees[] => + treeItems.map(tree => ({ + treeName: tree['treeName'] ?? '-', + gitRepositoryBranch: tree['gitRepositoryBranch'] ?? '-', + headGitCommitName: tree['headGitCommitName'] ?? '-', + headGitCommitHash: tree['headGitCommitHash'] ?? '-', + gitRepositoryUrl: tree['gitRepositoryUrl'] ?? '-', + index: tree['index'], + })); + function HardwareDetails(): JSX.Element { - const searchParams = useSearch({ from: '/hardware/$hardwareId' }); - const { hardwareId } = useParams({ from: '/hardware/$hardwareId' }); const { intervalInDays, origin } = useSearch({ from: '/hardware' }); // TODO: actually we will get this from URL soon const { startTimestampInSeconds, endTimestampInSeconds } = get_Timestamps(intervalInDays); + const searchParams = useSearch({ from: '/hardware/$hardwareId' }); + const { treeFilter: treeIndexes } = searchParams; + + const { hardwareId } = useParams({ from: '/hardware/$hardwareId' }); + + const navigate = useNavigate({ from: '/hardware/$hardwareId' }); + + const updateTreeFilters = useCallback( + (selectedIndexes: number[]) => { + navigate({ + search: previousSearch => ({ + ...previousSearch, + treeFilter: selectedIndexes, + }), + }); + }, + [navigate], + ); const { data, isLoading } = useHardwareDetails( hardwareId, startTimestampInSeconds, endTimestampInSeconds, origin, + treeIndexes ?? [], + ); + + const treeData = useMemo( + () => sanitizeTreeItems(data?.trees || []), + [data?.trees], ); if (isLoading || !data) @@ -87,7 +123,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 => @@ -98,18 +108,26 @@ const columns: ColumnDef[] = [ }, ]; -const sanitizeTreeItems = (treeItems: Trees[]): Trees[] => { - return treeItems.map(tree => ({ - treeName: tree['treeName'] ?? '-', - gitRepositoryBranch: tree['gitRepositoryBranch'] ?? '-', - headGitCommitName: tree['headGitCommitName'] ?? '-', - headGitCommitHash: tree['headGitCommitHash'], - gitRepositoryUrl: tree['gitRepositoryUrl'] ?? '-', - index: tree['index'], - })); +const indexesFromRowSelection = ( + rowSelection: RowSelectionState, + maxTreeItems: number, +): number[] => { + const rowSelectionValues = Object.values(rowSelection); + if ( + rowSelectionValues.length === maxTreeItems || + rowSelectionValues.length === 0 + ) { + return []; + } else { + return Object.keys(rowSelection).map(v => parseInt(v)); + } }; -export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { +export function HardwareHeader({ + treeItems, + selectedIndexes = [], + updateTreeFilters, +}: IHardwareHeader): JSX.Element { const [sorting, setSorting] = useState([ { id: 'treeName', desc: false }, ]); @@ -117,14 +135,44 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { pageIndex: 0, pageSize: 5, }); - // const [rowSelection, setRowSelection] = useState({}); - const data = useMemo(() => { - return sanitizeTreeItems(treeItems); - }, [treeItems]); + const { showDev } = useFeatureFlag(); + const [columnVisibility] = useState({ + select: showDev, + }); + + 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 table = useReactTable({ - data, + data: treeItems, columns, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, @@ -132,12 +180,14 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { getPaginationRowModel: getPaginationRowModel(), onPaginationChange: setPagination, getFilteredRowModel: getFilteredRowModel(), - // enableRowSelection: false, - // onRowSelectionChange: setRowSelection, + getRowId: originalRow => originalRow.index, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, state: { sorting, pagination, - // rowSelection, + rowSelection, + columnVisibility, }, }); @@ -154,7 +204,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 => { @@ -176,14 +226,14 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element { ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [modelRows /*, rowSelection*/]); + }, [modelRows, rowSelection]); return (
{tableRows} - +
); } 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 e4f248c1..d5fe60ad 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({});