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 (
@@ -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',