mirror of https://github.com/grafana/grafana.git
TableNG: Filter and sort sub tables (#104327)
* feat: filter and sort sub tables * chore: extract row processing into it's own function for filtering and sorting --------- Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
parent
bf918976b2
commit
2c1851e8c8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<number, TableRow> = 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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue