Skip to content

Commit

Permalink
feat: frontend header filtering
Browse files Browse the repository at this point in the history
Closes #500
  • Loading branch information
MarceloRobert committed Nov 13, 2024
1 parent 38935dd commit 44dd8fe
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 37 deletions.
3 changes: 2 additions & 1 deletion dashboard/src/api/hardwareDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ export const useHardwareDetails = (
hardwareId: string,
intervalInDays: number,
origin: TOrigins,
selectedIndexes: number[],
): UseQueryResult<THardwareDetails> => {
const params = { intervalInDays, origin };
const params = { intervalInDays, origin, selectedIndexes };
return useQuery({
queryKey: ['HardwareDetails', hardwareId, params],
queryFn: () => fetchHardwareDetails(hardwareId, params),
Expand Down
30 changes: 27 additions & 3 deletions dashboard/src/pages/hardwareDetails/HardwareDetails.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -61,7 +81,11 @@ function HardwareDetails(): JSX.Element {
</BreadcrumbList>
</Breadcrumb>
<div className="mt-5">
<HardwareHeader treeItems={data.trees} />
<HardwareHeader
treeItems={data.trees}
selectedIndexes={treeIndexes}
updateTreeFilters={updateTreeFilters}
/>
<HardwareDetailsTabs
HardwareDetailsData={data}
hardwareId={hardwareId}
Expand Down
126 changes: 93 additions & 33 deletions dashboard/src/pages/hardwareDetails/HardwareDetailsHeaderTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
ColumnDef,
PaginationState,
RowSelectionState,
SortingState,
} from '@tanstack/react-table';
import {
Expand All @@ -12,7 +13,7 @@ import {
useReactTable,
} from '@tanstack/react-table';

import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';

import { FormattedMessage } from 'react-intl';

Expand All @@ -23,35 +24,42 @@ import type { Trees } from '@/types/hardware/hardwareDetails';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/Tooltip';
import { sanitizeTableValue } from '@/components/Table/tableUtils';
import { PaginationInfo } from '@/components/Table/PaginationInfo';
// import { IndeterminateCheckbox } from '@/components/Checkbox/IndeterminateCheckbox';
import { IndeterminateCheckbox } from '@/components/Checkbox/IndeterminateCheckbox';

const DEBOUNCE_INTERVAL = 1000;

interface IHardwareHeader {
treeItems: Trees[];
selectedIndexes?: number[];
updateTreeFilters: (selecteds?: number[]) => void;
}

const columns: ColumnDef<Trees>[] = [
// {
// id: 'select',
// header: ({ table }) => (
// <IndeterminateCheckbox
// {...{
// checked: table.getIsAllRowsSelected(),
// indeterminate: table.getIsSomeRowsSelected(),
// onChange: table.getToggleAllRowsSelectedHandler(),
// disabled: true,
// }}
// />
// ),
// cell: ({ row }) => (
// <IndeterminateCheckbox
// {...{
// checked: row.getIsSelected(),
// disabled: !row.getCanSelect(),
// onChange: row.getToggleSelectedHandler(),
// }}
// />
// ),
// },
{
id: 'select',
header: ({ table }) => (
<IndeterminateCheckbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler(),
disabled: table.getIsAllRowsSelected(),
}}
/>
),
cell: ({ row, table }) => (
<IndeterminateCheckbox
{...{
checked: row.getIsSelected(),
disabled:
!row.getCanSelect() ||
(Object.keys(table.getState().rowSelection).length === 1 &&
row.getIsSelected()),
onChange: row.getToggleSelectedHandler(),
}}
/>
),
},
{
accessorKey: 'treeName',
header: ({ column }): JSX.Element =>
Expand Down Expand Up @@ -103,23 +111,75 @@ const sanitizeTreeItems = (treeItems: Record<string, string>[]): 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<SortingState>([
{ id: 'treeName', desc: false },
]);
const [pagination, setPagination] = useState<PaginationState>({
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({
Expand All @@ -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,
},
});

Expand All @@ -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 => {
Expand All @@ -175,7 +235,7 @@ export function HardwareHeader({ treeItems }: IHardwareHeader): JSX.Element {
</TableRow>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modelRows /*, rowSelection*/]);
}, [modelRows, rowSelection]);

return (
<div className="flex flex-col gap-6 pb-4">
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/routes/hardware/$hardwareId/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
47 changes: 47 additions & 0 deletions dashboard/src/types/hardware/hardwareDetails.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { z } from 'zod';

import type {
ArchCompilerStatus,
BuildsTabBuild,
Expand Down Expand Up @@ -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<typeof zFilterObjectsKeys>
| z.infer<typeof zFilterNumberKeys>;

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<TFilterKeys, unknown>),
z.record(z.never()),
])
.catch({});

0 comments on commit 44dd8fe

Please sign in to comment.