diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 26b5229815a..0a89c90f7a1 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -56,6 +56,7 @@ import { getTextAlign, handleSort, MapFrameToGridOptions, + processNestedTableRows, shouldTextOverflow, } from './utils'; @@ -268,14 +269,6 @@ export function TableNG(props: TableNGProps) { [textWraps, columnTypes, getColumnWidths, headersLength, fieldDisplayType] ); - const getDisplayedValue = (row: TableRow, key: string) => { - const field = props.data.fields.find((field) => { - return getDisplayName(field) === key; - })!; - const displayedValue = formattedValueToString(field.display!(row[key])); - return displayedValue; - }; - // Filter rows const filteredRows = useMemo(() => { const filterValues = Object.entries(filter); @@ -285,6 +278,13 @@ export function TableNG(props: TableNGProps) { return rows; } + // Helper function to get displayed value + const getDisplayedValue = (row: TableRow, key: string) => { + const field = props.data.fields.find((field) => field.name === key)!; + const displayedValue = formattedValueToString(field.display!(row[key])); + return displayedValue; + }; + // Update crossFilterOrder const filterKeys = new Set(filterValues.map(([key]) => key)); filterKeys.forEach((key) => { @@ -300,6 +300,28 @@ export function TableNG(props: TableNGProps) { // reset crossFilterRows crossFilterRows.current = {}; + // For nested tables, only filter parent rows and keep their children + if (isNestedTable) { + return processNestedTableRows(rows, (parents) => + parents.filter((row) => { + for (const [key, value] of filterValues) { + const displayedValue = getDisplayedValue(row, key); + if (!value.filteredSet.has(displayedValue)) { + return false; + } + // collect rows for crossFilter + if (!crossFilterRows.current[key]) { + crossFilterRows.current[key] = [row]; + } else { + crossFilterRows.current[key].push(row); + } + } + return true; + }) + ); + } + + // Regular filtering for non-nested tables return rows.filter((row) => { for (const [key, value] of filterValues) { const displayedValue = getDisplayedValue(row, key); @@ -315,35 +337,38 @@ export function TableNG(props: TableNGProps) { } return true; }); - }, [rows, filter, props.data.fields]); // eslint-disable-line react-hooks/exhaustive-deps + }, [rows, filter, isNestedTable, props.data.fields]); // Sort rows const sortedRows = useMemo(() => { - const comparators = sortColumns.map(({ columnKey }) => getComparator(columnTypes[columnKey])); - const sortDirs = sortColumns.map(({ direction }) => (direction === 'ASC' ? 1 : -1)); - if (sortColumns.length === 0) { return filteredRows; } - return filteredRows.slice().sort((a, b) => { + // Common sort comparator function + const compareRows = (a: TableRow, b: TableRow): number => { let result = 0; - let sortIndex = 0; - - for (const { columnKey } of sortColumns) { - const compare = comparators[sortIndex]; - result = sortDirs[sortIndex] * compare(a[columnKey], b[columnKey]); + for (let i = 0; i < sortColumns.length; i++) { + const { columnKey, direction } = sortColumns[i]; + const compare = getComparator(columnTypes[columnKey]); + const sortDir = direction === 'ASC' ? 1 : -1; + result = sortDir * compare(a[columnKey], b[columnKey]); if (result !== 0) { break; } - - sortIndex += 1; } - return result; - }); - }, [filteredRows, sortColumns, columnTypes]); + }; + + // Handle nested tables + if (isNestedTable) { + return processNestedTableRows(filteredRows, (parents) => [...parents].sort(compareRows)); + } + + // Regular sort for tables without nesting + return filteredRows.slice().sort((a, b) => compareRows(a, b)); + }, [filteredRows, sortColumns, columnTypes, isNestedTable]); // Paginated rows // TODO consolidate calculations into pagination wrapper component and only use when needed diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index db3d9b2ef5a..eb2abfc4df6 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -600,6 +600,41 @@ export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDispl export const getIsNestedTable = (dataFrame: DataFrame): boolean => dataFrame.fields.some(({ type }) => type === FieldType.nestedFrames); +/** Processes nested table rows */ +export const processNestedTableRows = ( + rows: TableRow[], + processParents: (parents: TableRow[]) => TableRow[] +): TableRow[] => { + // Separate parent and child rows + // Array for parentRows: enables sorting and maintains order for iteration + // Map for childRows: provides O(1) lookup by parent index when reconstructing the result + const parentRows: TableRow[] = []; + const childRows: Map = new Map(); + + rows.forEach((row) => { + if (Number(row.__depth) === 0) { + parentRows.push(row); + } else { + childRows.set(Number(row.__index), row); + } + }); + + // Process parent rows (filter or sort) + const processedParents = processParents(parentRows); + + // Reconstruct the result + const result: TableRow[] = []; + processedParents.forEach((row) => { + result.push(row); + const childRow = childRows.get(Number(row.__index)); + if (childRow) { + result.push(childRow); + } + }); + + return result; +}; + export const getDisplayName = (field: Field): string => { return field.state?.displayName ?? field.name; };