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 54a72fe commit 48fef2b
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 53 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
54 changes: 47 additions & 7 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,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,
Expand All @@ -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)
Expand Down Expand Up @@ -87,7 +123,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
140 changes: 95 additions & 45 deletions dashboard/src/pages/hardwareDetails/HardwareDetailsHeaderTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type {
ColumnDef,
PaginationState,
RowSelectionState,
SortingState,
VisibilityState,
} from '@tanstack/react-table';
import {
flexRender,
Expand All @@ -12,7 +14,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 +25,43 @@ 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';
import { useFeatureFlag } from '@/hooks/useFeatureFlag';

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 +108,86 @@ 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 { showDev } = useFeatureFlag();
const [columnVisibility] = useState<VisibilityState>({
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,
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,
columnVisibility,
},
});

Expand All @@ -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 => {
Expand All @@ -176,14 +226,14 @@ 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">
<BaseTable headerComponents={tableHeaders}>
<TableBody>{tableRows}</TableBody>
</BaseTable>
<PaginationInfo table={table} data={data} intlLabel="global.trees" />
<PaginationInfo table={table} data={treeItems} intlLabel="global.trees" />
</div>
);
}
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
Loading

0 comments on commit 48fef2b

Please sign in to comment.