diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts
index a14085aa753..6dddba81820 100644
--- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts
+++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts
@@ -343,6 +343,33 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
// TODO -- saving for another day.
});
+ test('Tests nested table expansion', async ({ gotoDashboardPage, selectors, page }) => {
+ const dashboardPage = await gotoDashboardPage({
+ uid: DASHBOARD_UID,
+ queryParams: new URLSearchParams({ editPanel: '4' }),
+ });
+
+ await expect(
+ dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Nested tables'))
+ ).toBeVisible();
+
+ await waitForTableLoad(page);
+
+ await expect(page.locator('[role="row"]')).toHaveCount(3); // header + 2 rows
+
+ const firstRowExpander = dashboardPage
+ .getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.RowExpander)
+ .first();
+
+ await firstRowExpander.click();
+ await expect(page.locator('[role="row"]')).not.toHaveCount(3); // more rows are present now, it is dynamic tho.
+
+ // TODO: test sorting
+
+ await firstRowExpander.click();
+ await expect(page.locator('[role="row"]')).toHaveCount(3); // back to original state
+ });
+
test('Tests tooltip interactions', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 94a0b4c6b3c..2fcb6da5ee3 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -804,11 +804,6 @@
"count": 2
}
},
- "packages/grafana-ui/src/components/Table/TableNG/utils.ts": {
- "@typescript-eslint/consistent-type-assertions": {
- "count": 1
- }
- },
"packages/grafana-ui/src/components/Table/TableRT/Filter.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts
index f1f2ce08642..0755477f93b 100644
--- a/packages/grafana-e2e-selectors/src/selectors/components.ts
+++ b/packages/grafana-e2e-selectors/src/selectors/components.ts
@@ -499,6 +499,9 @@ export const versionedComponents = {
},
},
TableNG: {
+ RowExpander: {
+ '12.4.0': 'data-testid tableng row expander',
+ },
Filters: {
HeaderButton: {
'12.1.0': 'data-testid tableng header filter',
diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
index 0a7c69edbf3..f17a62b92cf 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
@@ -154,8 +154,18 @@ export function TableNG(props: TableNGProps) {
const resizeHandler = useColumnResize(onColumnResize);
- const rows = useMemo(() => frameToRecords(data), [data]);
const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]);
+ const nestedFramesFieldName = useMemo(() => {
+ if (!hasNestedFrames) {
+ return;
+ }
+ const firstNestedField = data.fields.find((f) => f.type === FieldType.nestedFrames);
+ if (!firstNestedField) {
+ return;
+ }
+ return getDisplayName(firstNestedField);
+ }, [data, hasNestedFrames]);
+ const rows = useMemo(() => frameToRecords(data, nestedFramesFieldName), [data, nestedFramesFieldName]);
const getTextColorForBackground = useMemo(() => memoize(_getTextColorForBackground, { maxSize: 1000 }), []);
const {
@@ -374,7 +384,11 @@ export function TableNG(props: TableNGProps) {
return null;
}
- const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns);
+ const expandedRecords = applySort(
+ frameToRecords(nestedData, nestedFramesFieldName),
+ nestedData.fields,
+ sortColumns
+ );
if (!expandedRecords.length) {
return (
@@ -398,7 +412,7 @@ export function TableNG(props: TableNGProps) {
width: COLUMN.EXPANDER_WIDTH,
minWidth: COLUMN.EXPANDER_WIDTH,
}),
- [commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles]
+ [commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles, nestedFramesFieldName]
);
const fromFields = useCallback(
diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/RowExpander.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/RowExpander.tsx
index d1f64824ef3..ab2ee41538d 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/components/RowExpander.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/components/RowExpander.tsx
@@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { useStyles2 } from '../../../../themes/ThemeContext';
@@ -16,13 +17,21 @@ export function RowExpander({ onCellExpand, isExpanded }: RowExpanderNGProps) {
}
}
return (
-
+
diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts
index 3a641de5ac6..ddfaf189f34 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/types.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts
@@ -79,7 +79,6 @@ export interface TableRow {
// Nested table properties
data?: DataFrame;
- __nestedFrames?: DataFrame[];
__expanded?: boolean; // For row expansion state
// Generic typing for column values
@@ -262,7 +261,7 @@ export type TableCellStyles = (theme: GrafanaTheme2, options: TableCellStyleOpti
export type Comparator = (a: TableCellValue, b: TableCellValue) => number;
// Type for converting a DataFrame into an array of TableRows
-export type FrameToRowsConverter = (frame: DataFrame) => TableRow[];
+export type FrameToRowsConverter = (frame: DataFrame, nestedFramesFieldName?: string) => TableRow[];
// Type for mapping column names to their field types
export type ColumnTypes = Record;
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
index 0226f8b6463..b960d8c08c5 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
@@ -675,10 +675,12 @@ export function applySort(
/**
* @internal
*/
-export const frameToRecords = (frame: DataFrame): TableRow[] => {
+export const frameToRecords = (frame: DataFrame, nestedFramesFieldName?: string): TableRow[] => {
const fnBody = `
const rows = Array(frame.length);
const values = frame.fields.map(f => f.values);
+ const hasNestedFrames = '${nestedFramesFieldName ?? ''}'.length > 0;
+
let rowCount = 0;
for (let i = 0; i < frame.length; i++) {
rows[rowCount] = {
@@ -686,11 +688,14 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
__index: i,
${frame.fields.map((field, fieldIdx) => `${JSON.stringify(getDisplayName(field))}: values[${fieldIdx}][i]`).join(',')}
};
- rowCount += 1;
- if (rows[rowCount-1]['__nestedFrames']){
- const childFrame = rows[rowCount-1]['__nestedFrames'];
- rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]}
- rowCount += 1;
+ rowCount++;
+
+ if (hasNestedFrames) {
+ const childFrame = rows[rowCount-1][${JSON.stringify(nestedFramesFieldName)}];
+ if (childFrame){
+ rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]}
+ rowCount++;
+ }
}
}
return rows;
@@ -698,8 +703,9 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
// Creates a function that converts a DataFrame into an array of TableRows
// Uses new Function() for performance as it's faster than creating rows using loops
- const convert = new Function('frame', fnBody) as FrameToRowsConverter;
- return convert(frame);
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const convert = new Function('frame', 'nestedFramesFieldName', fnBody) as FrameToRowsConverter;
+ return convert(frame, nestedFramesFieldName);
};
/* ----------------------------- Data grid comparator ---------------------------- */
diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts
index 01934cc97bf..87000026769 100644
--- a/public/app/plugins/datasource/tempo/resultTransformer.ts
+++ b/public/app/plugins/datasource/tempo/resultTransformer.ts
@@ -911,7 +911,7 @@ const traceSubFrame = (
subFrame.add(transformSpanToTraceData(span, spanSet, trace));
});
- return subFrame;
+ return toDataFrame(subFrame);
};
interface TraceTableData {