change to Max row height instead of Max cell height

This commit is contained in:
Paul Marbach
2025-08-28 10:47:54 -04:00
parent 717aba33bd
commit ed80de02c8
17 changed files with 111 additions and 80 deletions
@@ -295,10 +295,6 @@
"wrapText": true
}
},
{
"id": "custom.maxHeight",
"value": null
},
{
"id": "custom.width",
"value": 255
@@ -2,6 +2,8 @@ import { Page, Locator } from '@playwright/test';
import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e';
import { getCell, getCellHeight } from './table-utils';
const DASHBOARD_UID = 'dcb9f5e9-8066-4397-889e-864b99555dbb';
test.use({ viewport: { width: 2000, height: 1080 } });
@@ -11,18 +13,6 @@ const waitForTableLoad = async (loc: Page | Locator) => {
await expect(loc.locator('.rdg')).toBeVisible();
};
const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) =>
loc
.getByRole('row')
.nth(rowIdx)
.getByRole(rowIdx === 0 ? 'columnheader' : 'gridcell')
.nth(colIdx);
const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx: number) => {
const cell = await getCell(loc, rowIdx, colIdx);
return (await cell.boundingBox())?.height ?? 0;
};
const getColumnIdx = async (loc: Page | Locator, columnName: string) => {
// find the index of the column "Long text." The kitchen sink table will change over time, but
// we can just find the column programatically and use it throughout the test.
@@ -77,22 +67,19 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
// text wrapping is enabled by default on this panel.
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100);
// toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks.
const longTextFieldOverrides = dashboardPage.getByGrafanaSelector(
selectors.components.OptionsGroup.group('panel-options-override-12')
);
// TODO: we have added a null value for max height to the field overrides in the JSON,
// because there's no good way to add a field override in an e2e at this point.
const maxCellHeightInput = longTextFieldOverrides.getByLabel('Max cell height');
await maxCellHeightInput.fill('80');
// set a max row height, watch the height decrease, then clear it to continue.
const maxRowHeightInput = page.getByLabel('Max row height').last();
await maxRowHeightInput.fill('80');
await expect(async () => {
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
}).toPass();
await maxCellHeightInput.clear();
await maxRowHeightInput.clear();
await longTextFieldOverrides.getByLabel('Wrap text').click({ force: true });
// toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks.
await dashboardPage
.getByGrafanaSelector(selectors.components.OptionsGroup.group('panel-options-override-12'))
.getByLabel('Wrap text')
.click({ force: true });
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// test that hover overflow works.
@@ -1,9 +1,13 @@
import { test, expect } from '@grafana/plugin-e2e';
import { getCellHeight } from './table-utils';
test.use({
viewport: { width: 1280, height: 1080 },
});
const MARKDOWN_DASHBOARD_UID = '2769f5d8-0094-4ac4-a4f0-f68f620339cc';
test.describe(
'Panels test: Table - Markdown',
{
@@ -12,11 +16,28 @@ test.describe(
() => {
test('Tests Markdown tables are successfully rendered', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({
uid: '2769f5d8-0094-4ac4-a4f0-f68f620339cc',
uid: MARKDOWN_DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
await expect(page.getByRole('grid')).toBeVisible();
});
test('Tests dynamic height and max row height', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({
uid: MARKDOWN_DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
});
// confirm that the second row of the table is tall due to the content in it
await expect(getCellHeight(page, 2, 1)).resolves.toBeGreaterThan(100);
// set the max row height to 80, watch the row shrink
const maxRowHeightInput = page.getByLabel('Max row height').last();
await maxRowHeightInput.fill('80');
await expect(async () => {
await expect(getCellHeight(page, 2, 1)).resolves.toBeLessThan(100);
}).toPass();
});
}
);
@@ -0,0 +1,13 @@
import { Page, Locator } from '@playwright/test';
export const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) =>
loc
.getByRole('row')
.nth(rowIdx)
.getByRole(rowIdx === 0 ? 'columnheader' : 'gridcell')
.nth(colIdx);
export const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx: number) => {
const cell = await getCell(loc, rowIdx, colIdx);
return (await cell.boundingBox())?.height ?? 0;
};
@@ -1009,10 +1009,6 @@ export interface TableFieldOptions extends HideableFieldConfig {
*/
hideHeader?: boolean;
inspect: boolean;
/**
* if set, limit the height in pixels that the wrapped text can flow to
*/
maxHeight?: number;
minWidth?: number;
/**
* Selecting or hovering this field will show a tooltip containing the content within the target field
@@ -131,8 +131,6 @@ TableFieldOptions: {
hideHeader?: bool
// if true, wrap the text content of the cell
wrapText?: bool
// if set, limit the height in pixels that the wrapped text can flow to
maxHeight?: number
// Enables text wrapping for column headers
wrapHeaderText?: bool
// Selecting or hovering this field will show a tooltip containing the content within the target field
@@ -31,6 +31,10 @@ export interface Options {
frozenColumns?: {
left?: number;
};
/**
* limits the maximum height of a row, if text wrapping or dynamic height is enabled
*/
maxRowHeight?: number;
/**
* Controls whether the panel should show the header
*/
@@ -24,14 +24,15 @@ export const getStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow, m
whiteSpace: 'pre-line',
},
}),
...(typeof maxHeight === 'number' && {
height: 'auto',
minHeight: 'none',
overflowY: 'hidden',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: Math.floor(maxHeight / TABLE.LINE_HEIGHT),
}),
...(maxHeight != null &&
textWrap && {
height: 'auto',
minHeight: 'none',
overflowY: 'hidden',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: Math.floor(maxHeight / TABLE.LINE_HEIGHT),
}),
});
export const getJsonCellStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow }) =>
@@ -89,7 +89,6 @@ import {
getDisplayName,
getIsNestedTable,
getJustifyContent,
getMaxHeight,
getVisibleFields,
isCellInspectEnabled,
predicateByName,
@@ -113,6 +112,7 @@ export function TableNG(props: TableNGProps) {
getActions = () => [],
height,
initialSortBy,
maxRowHeight: _maxRowHeight,
noHeader,
onCellFilterAdded,
onColumnResize,
@@ -203,6 +203,8 @@ export function TableNG(props: TableNGProps) {
showTypeIcons: showTypeIcons ?? false,
typographyCtx,
});
// the minimum max row height we should honor is a single line of text.
const maxRowHeight = _maxRowHeight != null ? Math.max(TABLE.LINE_HEIGHT, _maxRowHeight) : undefined;
const rowHeight = useRowHeight({
columnWidths: widths,
fields: visibleFields,
@@ -210,6 +212,7 @@ export function TableNG(props: TableNGProps) {
defaultHeight: defaultRowHeight,
expandedRows,
typographyCtx,
maxHeight: maxRowHeight,
});
const {
@@ -419,8 +422,12 @@ export function TableNG(props: TableNGProps) {
const textWrap = rowHeight === 'auto' || shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType);
const canBeColorized = canFieldBeColorized(cellType, applyToRowBgFn);
const maxHeight = getMaxHeight(field);
const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow, maxHeight };
const cellStyleOptions: TableCellStyleOptions = {
textAlign,
textWrap,
shouldOverflow,
maxHeight: maxRowHeight,
};
result.colsWithTooltip[displayName] = withTooltip;
@@ -460,7 +467,7 @@ export function TableNG(props: TableNGProps) {
<Cell
key={key}
{...props}
className={clsx(props.className, { [cellParentStyles]: maxHeight == null })}
className={clsx(props.className, { [cellParentStyles]: maxRowHeight == null })}
style={style}
/>
);
@@ -518,8 +525,8 @@ export function TableNG(props: TableNGProps) {
</>
);
if (maxHeight != null) {
result = clampByMaxHeight(maxHeight, result, cellParentStyles);
if (maxRowHeight != null) {
result = clampByMaxHeight(maxRowHeight, result, cellParentStyles);
}
return result;
@@ -534,13 +541,12 @@ export function TableNG(props: TableNGProps) {
if (tooltipField) {
const tooltipDisplayName = getDisplayName(tooltipField);
const tooltipCellOptions = getCellOptions(tooltipField);
const tooltipMaxHeight = getMaxHeight(tooltipField);
const tooltipCellStyleOptions = {
textAlign: getAlignment(tooltipField),
textWrap: shouldTextWrap(tooltipField),
shouldOverflow: false,
maxHeight: tooltipMaxHeight,
maxHeight: maxRowHeight,
} satisfies TableCellStyleOptions;
const tooltipCanBeColorized = canFieldBeColorized(tooltipCellOptions.type, applyToRowBgFn);
const tooltipDefaultStyles = getDefaultCellStyles(theme, tooltipCellStyleOptions);
@@ -555,10 +561,10 @@ export function TableNG(props: TableNGProps) {
const tooltipBodyClasses = clsx(tooltipDefaultStyles, tooltipSpecificStyles, tooltipLinkStyles);
let tooltipFieldRenderer = getCellRenderer(tooltipField, tooltipCellOptions);
if (tooltipMaxHeight) {
if (maxRowHeight != null) {
const OrigCellRenderer = tooltipFieldRenderer;
tooltipFieldRenderer = (props) =>
clampByMaxHeight(tooltipMaxHeight, <OrigCellRenderer {...props} />, tooltipBodyClasses);
clampByMaxHeight(maxRowHeight, <OrigCellRenderer {...props} />, tooltipBodyClasses);
}
const placement = field.config.custom?.tooltip?.placement ?? TableCellTooltipPlacement.Auto;
@@ -570,7 +576,7 @@ export function TableNG(props: TableNGProps) {
const tooltipProps = {
cellOptions: tooltipCellOptions,
classes: tooltipClasses,
className: clsx(tooltipClasses.tooltipContent, { [tooltipBodyClasses]: tooltipMaxHeight == null }),
className: clsx(tooltipClasses.tooltipContent, { [tooltipBodyClasses]: maxRowHeight == null }),
data,
disableSanitizeHtml,
field: tooltipField,
@@ -664,6 +670,7 @@ export function TableNG(props: TableNGProps) {
getCellColorInlineStyles,
getTextColorForBackground,
isCountRowsSet,
maxRowHeight,
numFrozenColsFullyInView,
onCellFilterAdded,
rowHeight,
@@ -403,6 +403,7 @@ interface UseRowHeightOptions {
defaultHeight: NonNullable<CSSProperties['height']>;
expandedRows: Set<number>;
typographyCtx: TypographyCtx;
maxHeight?: number;
}
export function useRowHeight({
@@ -412,8 +413,12 @@ export function useRowHeight({
defaultHeight,
expandedRows,
typographyCtx,
maxHeight,
}: UseRowHeightOptions): NonNullable<CSSProperties['height']> | ((row: TableRow) => number) {
const measurers = useMemo(() => buildCellHeightMeasurers(fields, typographyCtx), [fields, typographyCtx]);
const measurers = useMemo(
() => buildCellHeightMeasurers(fields, typographyCtx, maxHeight),
[fields, typographyCtx, maxHeight]
);
const hasWrappedCols = useMemo(() => measurers?.length ?? 0 > 0, [measurers]);
const colWidths = useMemo(() => {
@@ -132,6 +132,7 @@ export interface BaseTableProps {
frozenColumns?: number;
enablePagination?: boolean;
cellHeight?: TableCellHeight;
maxRowHeight?: number;
structureRev?: number;
transparent?: boolean;
/** @alpha Used by SparklineCell when provided */
@@ -84,18 +84,13 @@ export function shouldTextWrap(field: Field): boolean {
return Boolean(field.config.custom?.wrapText);
}
export function getMaxHeight(field: Field): number | undefined {
return field.config?.custom?.maxHeight;
}
/**
* @internal wrap a cell height measurer to clamp its output to the maxHeight defined in the field, if any.
*/
function clampByMaxHeight(measurer: MeasureCellHeight): MeasureCellHeight {
function clampByMaxHeight(measurer: MeasureCellHeight, maxHeight?: number): MeasureCellHeight {
return (value, width, field, rowIdx, lineHeight) => {
const rawHeight = measurer(value, width, field, rowIdx, lineHeight);
const maxHeight = getMaxHeight(field);
if (typeof maxHeight !== 'number') {
if (maxHeight == null) {
return rawHeight;
}
@@ -268,7 +263,8 @@ const spaceRegex = /[\s-]/;
*/
export function buildCellHeightMeasurers(
fields: Field[],
typographyCtx: TypographyCtx
typographyCtx: TypographyCtx,
maxHeight?: number
): MeasureCellHeightEntry[] | undefined {
const result: Record<string, MeasureCellHeightEntry> = {};
let wrappedFields = 0;
@@ -298,8 +294,8 @@ export function buildCellHeightMeasurers(
if (!result[measurerFactoryKey]) {
const [measure, estimate] = measurerFactory[measurerFactoryKey]();
result[measurerFactoryKey] = {
measure: clampByMaxHeight(measure),
estimate: estimate != null ? clampByMaxHeight(estimate) : undefined,
measure: clampByMaxHeight(measure, maxHeight),
estimate: estimate != null ? clampByMaxHeight(estimate, maxHeight) : undefined,
fieldIdxs: [],
};
}
@@ -80,6 +80,7 @@ export function TablePanel(props: Props) {
frozenColumns={options.frozenColumns?.left}
enablePagination={options.footer?.enablePagination}
cellHeight={options.cellHeight}
maxRowHeight={options.maxRowHeight}
timeRange={timeRange}
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
fieldConfig={fieldConfig}
+9 -10
View File
@@ -113,16 +113,6 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
name: t('table.name-wrap-text', 'Wrap text'),
category,
})
.addNumberInput({
path: 'maxHeight',
name: t('table.text-wrap-options.label-max-height', 'Max cell height'),
category,
settings: {
placeholder: t('table.text-wrap-options.placeholder-max-height', 'none'),
min: 0,
},
showIf: (cfg) => cfg.wrapText,
})
.addBooleanSwitch({
path: 'wrapHeaderText',
name: t('table.name-wrap-header-text', 'Wrap header text'),
@@ -208,6 +198,15 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
],
},
})
.addNumberInput({
path: 'maxRowHeight',
name: t('table.text-wrap-options.label-max-height', 'Max row height'),
category,
settings: {
placeholder: t('table.text-wrap-options.placeholder-max-height', 'none'),
min: 0,
},
})
.addBooleanSwitch({
path: 'footer.show',
category: footerCategory,
+6 -4
View File
@@ -44,10 +44,12 @@ composableKinds: PanelCfg: {
}
// Controls the height of the rows
cellHeight?: ui.TableCellHeight & (*"sm" | _)
// Defines the number of columns to freeze on the left side of the table
frozenColumns?: {
left?: number | *0
}
// limits the maximum height of a row, if text wrapping or dynamic height is enabled
maxRowHeight?: number
// Defines the number of columns to freeze on the left side of the table
frozenColumns?: {
left?: number | *0
}
} @cuetsy(kind="interface")
FieldConfig: {
ui.TableFieldOptions
@@ -29,6 +29,10 @@ export interface Options {
frozenColumns?: {
left?: number;
};
/**
* limits the maximum height of a row, if text wrapping or dynamic height is enabled
*/
maxRowHeight?: number;
/**
* Controls whether the panel should show the header
*/
+1 -1
View File
@@ -12809,7 +12809,7 @@
"placeholder-column-width": "auto",
"placeholder-fields": "All Numeric Fields",
"text-wrap-options": {
"label-max-height": "Max cell height",
"label-max-height": "Max row height",
"placeholder-max-height": "none"
},
"tooltip-placement-options": {