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 19, 2024
1 parent 3397541 commit 0107c0f
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 47 deletions.
20 changes: 19 additions & 1 deletion dashboard/src/api/hardwareDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type fetchHardwareDetailsBody = {
startTimestampInSeconds: number;
endTimestampInSeconds: number;
origin: TOrigins;
selectedTrees: Record<string, string>;
};

const fetchHardwareDetails = async (
Expand All @@ -24,13 +25,30 @@ const fetchHardwareDetails = async (
return res.data;
};

const mapIndexesToSelectedTrees = (
selectedIndexes: number[],
): Record<number, string> => {
return Object.fromEntries(
Array.from(selectedIndexes, index => [index.toString(), 'selected']),
);
};

export const useHardwareDetails = (
hardwareId: string,
startTimestampInSeconds: number,
endTimestampInSeconds: number,
origin: TOrigins,
selectedIndexes: number[],
): UseQueryResult<THardwareDetails> => {
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),
Expand Down
43 changes: 41 additions & 2 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, useMemo } from 'react';

import {
Breadcrumb,
BreadcrumbItem,
Expand All @@ -14,19 +16,52 @@ 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 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 { treeFilter: treeIndexes } = searchParams;
const { hardwareId } = useParams({ from: '/hardware/$hardwareId' });
const { 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,
searchParams.startTimestampInSeconds,
searchParams.endTimestampInSeconds,
origin,
treeIndexes ?? [],
);

const treeData = useMemo(
() => sanitizeTreeItems(data?.trees || []),
[data?.trees],
);

if (isLoading || !data)
Expand Down Expand Up @@ -63,7 +98,11 @@ function HardwareDetails(): JSX.Element {
</BreadcrumbList>
</Breadcrumb>
<div className="mt-5">
<HardwareHeader treeItems={data.trees} />
<HardwareHeader
treeItems={treeData}
selectedIndexes={treeIndexes}
updateTreeFilters={updateTreeFilters}
/>
<HardwareDetailsTabs
HardwareDetailsData={data}
hardwareId={hardwareId}
Expand Down
130 changes: 86 additions & 44 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: (selectedIndexes: 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 @@ -98,46 +106,80 @@ const columns: ColumnDef<Trees>[] = [
},
];

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<SortingState>([
{ id: 'treeName', desc: false },
]);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
});
// const [rowSelection, setRowSelection] = useState({});

const data = useMemo(() => {
return sanitizeTreeItems(treeItems);
}, [treeItems]);
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,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
getFilteredRowModel: getFilteredRowModel(),
// enableRowSelection: false,
// onRowSelectionChange: setRowSelection,
getRowId: originalRow => originalRow.index,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
state: {
sorting,
pagination,
// rowSelection,
rowSelection,
},
});

Expand All @@ -154,7 +196,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 @@ -176,7 +218,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 0107c0f

Please sign in to comment.