TableNG: Follow-up style fixes (#107274)

* open cell inspect in code mode for JSON panels

* increase opacity of TableCellActions background for legibility in overlap cases

* fix nested grid width calculation

* fix 'jumping' on hover overflow

* route transparency through (needs a scenes update)

* base row hover color on transparency

* update the colors for table

* remove console.log

* reinstate header toggle for nested row transformation

* fix #59474 - graceful handling when subtable has no rows

* fix i18n

* use TABLE.LINE_HEIGHT

* change nestedData back to const
This commit is contained in:
Paul Marbach
2025-07-09 14:54:48 -04:00
committed by GitHub
parent ebe494c7e6
commit a63a9357bb
7 changed files with 55 additions and 28 deletions

View File

@ -42,7 +42,8 @@ export function TableCellActions(props: TableCellActionsProps) {
dataProjection: 'EPSG:4326', dataProjection: 'EPSG:4326',
}); });
mode = TableCellInspectorMode.code; mode = TableCellInspectorMode.code;
} else if ('cellType' in cellOptions && cellOptions.cellType === TableCellDisplayMode.JSONView) { }
if (cellOptions.type === TableCellDisplayMode.JSONView) {
mode = TableCellInspectorMode.code; mode = TableCellInspectorMode.code;
} }

View File

@ -81,13 +81,14 @@ export function TableNG(props: TableNGProps) {
onSortByChange, onSortByChange,
showTypeIcons, showTypeIcons,
structureRev, structureRev,
transparent,
width, width,
} = props; } = props;
const theme = useTheme2(); const theme = useTheme2();
const styles = useStyles2(getGridStyles, { const styles = useStyles2(getGridStyles, {
enablePagination, enablePagination,
noHeader, transparent,
}); });
const panelContext = usePanelContext(); const panelContext = usePanelContext();
@ -244,21 +245,9 @@ export function TableNG(props: TableNGProps) {
}, },
sortColumns, sortColumns,
rowHeight, rowHeight,
headerRowClass: styles.headerRow,
headerRowHeight: headerHeight,
bottomSummaryRows: hasFooter ? [{}] : undefined, bottomSummaryRows: hasFooter ? [{}] : undefined,
}) satisfies Partial<DataGridProps<TableRow, TableSummaryRow>>, }) satisfies Partial<DataGridProps<TableRow, TableSummaryRow>>,
[ [enableVirtualization, resizeHandler, sortColumns, rowHeight, hasFooter, setSortColumns, onSortByChange]
enableVirtualization,
resizeHandler,
sortColumns,
headerHeight,
styles.headerRow,
rowHeight,
hasFooter,
setSortColumns,
onSortByChange,
]
); );
interface Schema { interface Schema {
@ -437,6 +426,7 @@ export function TableNG(props: TableNGProps) {
return result; return result;
} }
const hasNestedHeaders = firstNestedData.meta?.custom?.noHeader !== true;
const renderRow = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair); const renderRow = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair);
const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields( const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields(
firstNestedData.fields, firstNestedData.fields,
@ -486,11 +476,20 @@ export function TableNG(props: TableNGProps) {
} }
const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns); const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns);
if (!expandedRecords.length) {
return (
<div className={styles.noDataNested}>
<Trans i18nKey="grafana-ui.table.nested-table.no-data">No data</Trans>
</div>
);
}
return ( return (
<DataGrid<TableRow, TableSummaryRow> <DataGrid<TableRow, TableSummaryRow>
{...commonDataGridProps} {...commonDataGridProps}
className={cx(styles.grid, styles.gridNested)} className={cx(styles.grid, styles.gridNested)}
headerRowClass={cx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })}
headerRowHeight={hasNestedHeaders ? defaultHeaderHeight : 0}
columns={nestedColumns} columns={nestedColumns}
rows={expandedRecords} rows={expandedRecords}
renderers={{ renderRow, renderCell: renderCellRoot }} renderers={{ renderRow, renderCell: renderCellRoot }}
@ -509,6 +508,7 @@ export function TableNG(props: TableNGProps) {
crossFilterOrder, crossFilterOrder,
crossFilterRows, crossFilterRows,
data, data,
defaultHeaderHeight,
defaultRowHeight, defaultRowHeight,
enableSharedCrosshair, enableSharedCrosshair,
expandedRows, expandedRows,
@ -552,6 +552,8 @@ export function TableNG(props: TableNGProps) {
className={styles.grid} className={styles.grid}
columns={structureRevColumns} columns={structureRevColumns}
rows={paginatedRows} rows={paginatedRows}
headerRowClass={cx(styles.headerRow, { [styles.displayNone]: noHeader })}
headerRowHeight={headerHeight}
onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => { onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => {
// Note: could be column.field; JS says yes, but TS says no! // Note: could be column.field; JS says yes, but TS says no!
const field = columns[column.idx].field; const field = columns[column.idx].field;
@ -693,17 +695,20 @@ const renderRowFactory =
const getGridStyles = ( const getGridStyles = (
theme: GrafanaTheme2, theme: GrafanaTheme2,
{ enablePagination, noHeader }: { enablePagination?: boolean; noHeader?: boolean } { enablePagination, transparent }: { enablePagination?: boolean; transparent?: boolean }
) => ({ ) => ({
grid: css({ grid: css({
'--rdg-background-color': theme.colors.background.primary, '--rdg-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary,
'--rdg-header-background-color': theme.colors.background.primary, '--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary,
'--rdg-border-color': theme.isDark ? '#282b30' : '#ebebec', '--rdg-border-color': theme.colors.border.weak,
'--rdg-color': theme.colors.text.primary, '--rdg-color': theme.colors.text.primary,
// note: this cannot have any transparency since default cells that // note: this cannot have any transparency since default cells that
// overlay/overflow on hover inherit this background and need to occlude cells below // overlay/overflow on hover inherit this background and need to occlude cells below
'--rdg-row-hover-background-color': theme.isDark ? '#212428' : '#f4f5f5', '--rdg-row-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary,
'--rdg-row-hover-background-color': transparent
? theme.colors.background.primary
: theme.colors.background.secondary,
// TODO: magic 32px number is unfortunate. it would be better to have the content // TODO: magic 32px number is unfortunate. it would be better to have the content
// flow using flexbox rather than hard-coding this size via a calc // flow using flexbox rather than hard-coding this size via a calc
@ -723,15 +728,24 @@ const getGridStyles = (
}), }),
gridNested: css({ gridNested: css({
height: '100%', height: '100%',
width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - 1}px)`, width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`,
overflow: 'visible', overflow: 'visible',
marginLeft: COLUMN.EXPANDER_WIDTH - 1, marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1,
marginBlock: TABLE.CELL_PADDING,
}), }),
cellNested: css({ cellNested: css({
'&[aria-selected=true]': { '&[aria-selected=true]': {
outline: 'none', outline: 'none',
}, },
}), }),
noDataNested: css({
height: TABLE.NESTED_NO_DATA_HEIGHT,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.colors.text.secondary,
fontSize: theme.typography.h4.fontSize,
}),
cellActions: css({ cellActions: css({
display: 'none', display: 'none',
position: 'absolute', position: 'absolute',
@ -739,7 +753,7 @@ const getGridStyles = (
margin: 'auto', margin: 'auto',
height: '100%', height: '100%',
color: theme.colors.text.primary, color: theme.colors.text.primary,
background: theme.isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.7)', background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)',
padding: theme.spacing.x0_5, padding: theme.spacing.x0_5,
paddingInlineStart: theme.spacing.x1, paddingInlineStart: theme.spacing.x1,
}), }),
@ -752,12 +766,14 @@ const getGridStyles = (
headerRow: css({ headerRow: css({
paddingBlockStart: 0, paddingBlockStart: 0,
fontWeight: 'normal', fontWeight: 'normal',
...(noHeader ? { display: 'none' } : {}),
'& .rdg-cell': { '& .rdg-cell': {
height: '100%', height: '100%',
alignItems: 'flex-end', alignItems: 'flex-end',
}, },
}), }),
displayNone: css({
display: 'none',
}),
paginationContainer: css({ paginationContainer: css({
alignItems: 'center', alignItems: 'center',
display: 'flex', display: 'flex',
@ -835,6 +851,7 @@ const getCellStyles = (
whiteSpace: 'pre-line', whiteSpace: 'pre-line',
height: 'fit-content', height: 'fit-content',
minWidth: 'fit-content', minWidth: 'fit-content',
paddingBlock: (rowHeight - TABLE.LINE_HEIGHT) / 2 - 1,
}), }),
}, },
}), }),

View File

@ -14,5 +14,6 @@ export const TABLE = {
SCROLL_BAR_WIDTH: 8, SCROLL_BAR_WIDTH: 8,
SCROLL_BAR_MARGIN: 2, SCROLL_BAR_MARGIN: 2,
LINE_HEIGHT: 22, LINE_HEIGHT: 22,
NESTED_NO_DATA_HEIGHT: 60,
BORDER_RIGHT: 0.666667, BORDER_RIGHT: 0.666667,
}; };

View File

@ -503,9 +503,13 @@ export function useRowHeight({
return 0; return 0;
} }
// Ensure we have a minimum height (defaultHeight) for the nested table even if data is empty
const rowCount = row.data?.length ?? 0; const rowCount = row.data?.length ?? 0;
return Math.max(defaultHeight, defaultHeight * rowCount + headerHeight); if (rowCount === 0) {
return TABLE.NESTED_NO_DATA_HEIGHT + TABLE.CELL_PADDING * 2;
}
const nestedHeaderHeight = row.data?.meta?.custom?.noHeader ? 0 : defaultHeight;
return Math.max(defaultHeight, defaultHeight * rowCount + nestedHeaderHeight + TABLE.CELL_PADDING * 2);
} }
// regular rows // regular rows
@ -519,7 +523,6 @@ export function useRowHeight({
fields, fields,
hasNestedFrames, hasNestedFrames,
hasWrappedCols, hasWrappedCols,
headerHeight,
maxWrapCellOptions, maxWrapCellOptions,
colWidths, colWidths,
]); ]);

View File

@ -131,6 +131,7 @@ export interface BaseTableProps {
enablePagination?: boolean; enablePagination?: boolean;
cellHeight?: TableCellHeight; cellHeight?: TableCellHeight;
structureRev?: number; structureRev?: number;
transparent?: boolean;
/** @alpha Used by SparklineCell when provided */ /** @alpha Used by SparklineCell when provided */
timeRange?: TimeRange; timeRange?: TimeRange;
enableSharedCrosshair?: boolean; enableSharedCrosshair?: boolean;

View File

@ -26,7 +26,7 @@ import { Options } from './panelcfg.gen';
interface Props extends PanelProps<Options> {} interface Props extends PanelProps<Options> {}
export function TablePanel(props: Props) { export function TablePanel(props: Props) {
const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables } = props; const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables, transparent } = props;
useMemo(() => { useMemo(() => {
cacheFieldDisplayNames(data.series); cacheFieldDisplayNames(data.series);
@ -82,6 +82,7 @@ export function TablePanel(props: Props) {
fieldConfig={fieldConfig} fieldConfig={fieldConfig}
getActions={_getActions} getActions={_getActions}
structureRev={data.structureRev} structureRev={data.structureRev}
transparent={transparent}
/> />
); );

View File

@ -8011,6 +8011,9 @@
"filter-popup-match-case": "Match case", "filter-popup-match-case": "Match case",
"inspect-drawer-title": "Inspect value", "inspect-drawer-title": "Inspect value",
"inspect-menu-label": "Inspect value", "inspect-menu-label": "Inspect value",
"nested-table": {
"no-data": "No data"
},
"no-values-label": "No values", "no-values-label": "No values",
"pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows" "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows"
}, },