diff --git a/dashboard/src/components/Table/BaseTable.tsx b/dashboard/src/components/Table/BaseTable.tsx index 524620fe9..2aea45167 100644 --- a/dashboard/src/components/Table/BaseTable.tsx +++ b/dashboard/src/components/Table/BaseTable.tsx @@ -1,4 +1,10 @@ -import type { ComponentProps, ReactElement, ReactNode, JSX } from 'react'; +import type { + ComponentProps, + CSSProperties, + ReactElement, + ReactNode, + JSX, +} from 'react'; import classNames from 'classnames'; @@ -41,15 +47,18 @@ export const DumbBaseTable = ({ children, className, containerClassName, + style, }: { children: ReactNode; className?: string; containerClassName?: string; + style?: CSSProperties; }): JSX.Element => { return ( {children}
@@ -73,10 +82,12 @@ export const DumbTableHeader = ({ export const TableHead = ({ children, className, + ...props }: ComponentProps): JSX.Element => { return ( {children} diff --git a/dashboard/src/components/TestsTable/DefaultTestsColumns.tsx b/dashboard/src/components/TestsTable/DefaultTestsColumns.tsx index 8b1017669..8c21397aa 100644 --- a/dashboard/src/components/TestsTable/DefaultTestsColumns.tsx +++ b/dashboard/src/components/TestsTable/DefaultTestsColumns.tsx @@ -30,9 +30,37 @@ import { UNKNOWN_STRING } from '@/utils/constants/backend'; import { getDateSortKey } from './groupSummaries'; import type { GroupFieldSummary, UnifiedTestRow } from './types'; -const INDENT_WIDTH = 20; +const INDENT_WIDTH = 12; +export const PATH_TREE_INDENT = INDENT_WIDTH; const MUTED_VALUE_CLASS = 'text-gray-500'; +export const PathWithPrefixEllipsis = ({ + value, +}: { + value: string; +}): JSX.Element => ( +
+ + {value} + +
+); + +export const PathWithTooltip = ({ value }: { value: string }): JSX.Element => ( + + + + + {value} + +); + export type SortDirectionGetter = (columnId: string) => false | 'asc' | 'desc'; const PathCell = ({ @@ -45,32 +73,34 @@ const PathCell = ({ const isLeaf = row.original.kind === 'leaf'; return ( -
+
{isExpandable && ( - + )} - {!isExpandable && row.depth > 0 && } + {!isExpandable && row.depth > 0 && } - - {isLeaf ? ( -
- + + {/* Activation bubbles to the row's onClick, which toggles groups. */} +
+ )} +
{value}
@@ -165,6 +195,7 @@ function renderUniformGroupCell( context: CellContext, value: unknown, ): ReactNode { + const columnId = getColumnId(column); const leafLikeOriginal = { ...context.row.original, kind: 'leaf' as const, @@ -176,6 +207,9 @@ function renderUniformGroupCell( row: { ...context.row, original: leafLikeOriginal, + // Leaf cells read row.getValue(id); the group row has no leaf field values. + getValue: (id: string) => + id === columnId ? value : context.row.getValue(id), } as Row, }); } @@ -402,23 +436,9 @@ export const defaultInnerColumns: ColumnDef[] = [ header: ({ column }): JSX.Element => ( ), - cell: ({ row }): JSX.Element => { - return ( - - -
- - {row.getValue('path')} - -
-
- {row.getValue('path')} -
- ); - }, + cell: ({ row }): JSX.Element => ( + + ), }, { accessorKey: 'status', diff --git a/dashboard/src/components/TestsTable/TestsTable.tsx b/dashboard/src/components/TestsTable/TestsTable.tsx index b61c54fa1..5dd8dd178 100644 --- a/dashboard/src/components/TestsTable/TestsTable.tsx +++ b/dashboard/src/components/TestsTable/TestsTable.tsx @@ -57,6 +57,7 @@ import WrapperTableWithLogSheet from '@/pages/TreeDetails/Tabs/WrapperTableWithL import { adaptColumnsForUnifiedTable, defaultInnerColumns, + PATH_TREE_INDENT, } from './DefaultTestsColumns'; import { buildTestsTree } from './buildTestsTree'; import { @@ -75,6 +76,41 @@ import type { UnifiedTestRow } from './types'; const ESTIMATED_ROW_HEIGHT = 60; const VIRTUALIZER_OVERSCAN = 5; +const PATH_CHEVRON_WIDTH = 24; +const OTHER_COLUMN_MIN_WIDTH = 88; + +const PATH_CHAR_WIDTH = 7.5; +const PATH_CELL_PADDING = 34; +const MIN_PATH_COLUMN_WIDTH = 120; +const MAX_PATH_COLUMN_WIDTH = 480; + +const pathColumnWidthFromStrings = (strings: string[], extra = 0): number => { + const longest = strings.reduce( + (max, value) => Math.max(max, value.length), + 0, + ); + return Math.round( + Math.min( + MAX_PATH_COLUMN_WIDTH, + Math.max( + MIN_PATH_COLUMN_WIDTH, + longest * PATH_CHAR_WIDTH + PATH_CELL_PADDING + extra, + ), + ), + ); +}; + +const maxTreeDepth = (rows: UnifiedTestRow[], depth = 0): number => { + let max = depth; + for (const row of rows) { + if (row.subRows?.length) { + max = Math.max(max, maxTreeDepth(row.subRows, depth + 1)); + } else { + max = Math.max(max, depth); + } + } + return max; +}; export interface ITestsTable { tableKey: TableKeys; @@ -155,6 +191,20 @@ export function TestsTable({ ); const data = groupingMode === 'ungrouped' ? flatData : groupedData; + const pathColumnWidth = useMemo(() => { + const paths = flatData.map(row => row.path); + const maxDepth = + groupingMode === 'ungrouped' ? 0 : maxTreeDepth(groupedData); + const indentExtra = + maxDepth * PATH_TREE_INDENT + (maxDepth > 0 ? PATH_CHEVRON_WIDTH : 0); + return pathColumnWidthFromStrings(paths, indentExtra); + }, [flatData, groupedData, groupingMode]); + + const tableMinWidth = useMemo( + () => pathColumnWidth + (columns.length - 1) * OTHER_COLUMN_MIN_WIDTH, + [columns.length, pathColumnWidth], + ); + useEffect(() => { setExpanded({}); }, [groupingMode, filteredTree]); @@ -270,12 +320,18 @@ export function TestsTable({ sorting, }); return ( - + {headerComponent} ); }); - }, [groupHeaders, sorting]); + }, [groupHeaders, sorting, pathColumnWidth]); const modelRows = table.getRowModel().rows; @@ -492,8 +548,15 @@ export function TestsTable({ currentPathFilter={currentPathFilter} groupingControls={groupingControls} /> -
- +
+ {tableHeaders} diff --git a/dashboard/src/components/TestsTable/collapseTestsTree.test.ts b/dashboard/src/components/TestsTable/collapseTestsTree.test.ts index ff3b414be..bcc909eb6 100644 --- a/dashboard/src/components/TestsTable/collapseTestsTree.test.ts +++ b/dashboard/src/components/TestsTable/collapseTestsTree.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { TestHistory } from '@/types/general'; import type { Status } from '@/types/database'; +import { UNKNOWN_STRING } from '@/utils/constants/backend'; import { buildTestsTree } from './buildTestsTree'; import { collapseSingleChildChains } from './collapseTestsTree'; @@ -102,12 +103,44 @@ describe('buildGroupSummaries', () => { it('marks uniform values and mixed counts', () => { const summaries = buildGroupSummaries([ - leaf({ id: '1', lab: 'lab-a', duration: '1s' }), - leaf({ id: '2', lab: 'lab-a', duration: '2s' }), + leaf({ id: '1', hardware: ['hw-a'], duration: '1s', lab: 'lab-b' }), + leaf({ id: '2', hardware: ['hw-a'], duration: '2s', lab: 'lab-a' }), ]); - expect(summaries.lab).toEqual({ kind: 'uniform', value: 'lab-a' }); + expect(summaries.hardware).toEqual({ kind: 'uniform', value: ['hw-a'] }); expect(summaries.duration).toEqual({ kind: 'mixed', count: 2 }); + expect(summaries.lab).toEqual({ kind: 'mixed', count: 2 }); + }); + + it('counts unique child labs', () => { + const summaries = buildGroupSummaries([ + leaf({ id: '1', lab: 'z-lab' }), + leaf({ id: '2', lab: 'a-lab' }), + leaf({ id: '3', lab: 'z-lab' }), + ]); + + expect(summaries.lab).toEqual({ kind: 'mixed', count: 2 }); + }); + + it('counts Unknown as a distinct lab', () => { + const summaries = buildGroupSummaries([ + leaf({ id: '1', lab: 'lab-a' }), + leaf({ id: '2' }), + ]); + + expect(summaries.lab).toEqual({ kind: 'mixed', count: 2 }); + }); + + it('shows Unknown when all child labs are missing', () => { + const summaries = buildGroupSummaries([ + leaf({ id: '1' }), + leaf({ id: '2' }), + ]); + + expect(summaries.lab).toEqual({ + kind: 'uniform', + value: UNKNOWN_STRING, + }); }); it('builds a date range from distinct start times', () => { diff --git a/dashboard/src/components/TestsTable/groupSummaries.ts b/dashboard/src/components/TestsTable/groupSummaries.ts index 79ffad65a..51d3c96d9 100644 --- a/dashboard/src/components/TestsTable/groupSummaries.ts +++ b/dashboard/src/components/TestsTable/groupSummaries.ts @@ -1,9 +1,10 @@ +import { UNKNOWN_STRING } from '@/utils/constants/backend'; + import type { GroupFieldSummary, UnifiedTestRow } from './types'; const AGGREGATED_FIELDS = [ 'start_time', 'duration', - 'lab', 'hardware', 'treeBranch', ] as const; @@ -78,6 +79,13 @@ function summarizeDates(values: unknown[]): GroupFieldSummary { return { kind: 'dateRange', min: min.value, max: max.value }; } +function normalizeLab(value: unknown): string { + if (value === undefined || value === null || value === '') { + return UNKNOWN_STRING; + } + return String(value); +} + export function buildGroupSummaries( children: UnifiedTestRow[], ): Partial> { @@ -92,6 +100,8 @@ export function buildGroupSummaries( : summarizeGeneric(values); } + summaries.lab = summarizeGeneric(leaves.map(leaf => normalizeLab(leaf.lab))); + return summaries; } diff --git a/dashboard/src/components/ui/table.tsx b/dashboard/src/components/ui/table.tsx index 5204fb664..27799edd1 100644 --- a/dashboard/src/components/ui/table.tsx +++ b/dashboard/src/components/ui/table.tsx @@ -114,7 +114,15 @@ const TableCellWithLink = React.forwardRef< className={cn('align-middle [&:has([role=checkbox])]:pr-0', className)} {...props} > - + {children} diff --git a/dashboard/src/pages/hardwareDetails/Tabs/Tests/HardwareDetailsTestsTable.tsx b/dashboard/src/pages/hardwareDetails/Tabs/Tests/HardwareDetailsTestsTable.tsx index 87b28c61e..106e5ff3a 100644 --- a/dashboard/src/pages/hardwareDetails/Tabs/Tests/HardwareDetailsTestsTable.tsx +++ b/dashboard/src/pages/hardwareDetails/Tabs/Tests/HardwareDetailsTestsTable.tsx @@ -4,8 +4,7 @@ import { useCallback, type JSX } from 'react'; import type { LinkProps } from '@tanstack/react-router'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/Tooltip'; - +import { PathWithTooltip } from '@/components/TestsTable/DefaultTestsColumns'; import type { ITestsTable } from '@/components/TestsTable/TestsTable'; import { TestsTable } from '@/components/TestsTable/TestsTable'; import { TableHeader } from '@/components/Table/TableHeader'; @@ -25,18 +24,9 @@ const innerColumns: ColumnDef[] = [ header: ({ column }): JSX.Element => ( ), - cell: ({ row }): JSX.Element => { - return ( - - -
- {row.getValue('path')} -
-
- {row.getValue('path')} -
- ); - }, + cell: ({ row }): JSX.Element => ( + + ), }, { accessorKey: 'treeBranch',