Table: Max row height for variable height rows (#109639)

* Table: Max height for wrapped content

* Docs: tableNG max cell height (#110069)

Co-authored-by: Paul Marbach <paul.marbach@grafana.com>

* change to Max row height instead of Max cell height

* fix unit test

* table utils codeowners

* Update packages/grafana-ui/src/components/Table/TableNG/utils.ts

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>

* update docs

* fix docs

* Revert "fix unit test"

This reverts commit c46b0f1bec.

* fix unit test

* trade one important for another

* Tweaked wording

* hover overflow for max row height

* get rid of commented out section

* and we did it without important

* centralize overflow for max height assessment

* some alignment stuff was busted

* didn't end up using the max heigh arg for shouldTextOverflow

* make i18n path more consistent

* put some tooltip things back since they ultimately didnt change

* we can simplify the :not selector

* delete comment

* don't bother with :not

---------

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Paul Marbach 2025-08-29 15:10:17 -04:00 committed by GitHub
parent e47e579bee
commit b22f15ad16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 211 additions and 44 deletions

1
.github/CODEOWNERS vendored
View File

@ -464,6 +464,7 @@
/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/table-markdown.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/table-sparkline.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/table-utils.ts @grafana/dataviz-squad
/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources
/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
/e2e-playwright/smoke-tests-suite/ @grafana/grafana-frontend-platform

View File

@ -204,6 +204,7 @@ This option is only available when you're editing the panel.
| Show table header | Show or hide column names imported from your data source. |
| Frozen columns | Freeze columns starting from the left side of the table. Enter a value to set how many columns are frozen. |
| Cell height | Set the height of the cell. Choose from **Small**, **Medium**, or **Large**. |
| Max row height | Define the maximum height for a row in the table. This can be useful when **Wrap text** is enabled for one or more columns. |
| Enable pagination | Toggle the switch to control how many table rows are visible at once. When switched on, the page size automatically adjusts to the height of the table. This option doesn't affect queries. |
| Minimum column width | Define the lower limit of the column width, in pixels. By default, the minimum width of the table column is 150 pixels. For small-screen devices, such as mobile phones or tablets, reduce the value to `50` to allow table-based panels to render correctly in dashboards. |
| Column width | Define a column width, in pixels, rather than allowing the width to be set automatically. By default, Grafana calculates the column width based on the table size and the minimum column width. |

View File

@ -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.
@ -55,7 +45,11 @@ const disableAllTextWrap = async (loc: Page | Locator, selectors: E2ESelectorGro
};
test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] }, () => {
test('Tests word wrap, hover overflow, and cell inspect', async ({ gotoDashboardPage, selectors, page }) => {
test('Tests word wrap, hover overflow, max cell height, and cell inspect', async ({
gotoDashboardPage,
selectors,
page,
}) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '1' }),
@ -73,10 +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);
// 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 maxRowHeightInput.clear();
// 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'))
.getByText('Wrap text')
.click();
.getByLabel('Wrap text')
.click({ force: true });
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// test that hover overflow works.

View File

@ -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();
});
}
);

View File

@ -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;
};

View File

@ -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
*/

View File

@ -3,6 +3,8 @@ import { css } from '@emotion/css';
import { formattedValueToString } from '@grafana/data';
import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink';
import { TABLE } from '../constants';
import { getActiveCellSelector } from '../styles';
import { AutoCellProps, TableCellStyles } from '../types';
export function AutoCell({ value, field, rowIdx }: AutoCellProps) {
@ -15,22 +17,37 @@ export function AutoCell({ value, field, rowIdx }: AutoCellProps) {
);
}
export const getStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow }) =>
export const getStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow, maxHeight }) =>
css({
...(textWrap && { whiteSpace: 'pre-line' }),
...(shouldOverflow && {
'&:hover, &[aria-selected=true]': {
[getActiveCellSelector(Boolean(maxHeight))]: {
whiteSpace: 'pre-line',
},
}),
...(maxHeight != null &&
textWrap && {
height: 'auto',
overflowY: 'hidden',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: Math.floor(maxHeight / TABLE.LINE_HEIGHT),
[getActiveCellSelector(true)]: {
display: 'flex',
WebkitLineClamp: 'none',
WebkitBoxOrient: 'unset',
overflowY: 'auto',
height: 'fit-content',
},
}),
});
export const getJsonCellStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow }) =>
export const getJsonCellStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow, maxHeight }) =>
css({
fontFamily: 'monospace',
...(textWrap && { whiteSpace: 'pre' }),
...(shouldOverflow && {
'&:hover, &[aria-selected=true]': {
[getActiveCellSelector(Boolean(maxHeight))]: {
whiteSpace: 'pre',
},
}),

View File

@ -22,7 +22,7 @@ export const getStyles: TableCellStyles = (theme, { textWrap, textAlign }) =>
...(textWrap && {
flexDirection: 'column',
justifyContent: 'center',
alignItems: getJustifyContent(textAlign),
alignItems: `${getJustifyContent(textAlign)} !important`, // we can't guarantee order, and alignItems is set on a sibling class.
}),
'> a': {
flexWrap: 'nowrap',

View File

@ -18,7 +18,7 @@ export const ImageCell = ({ cellOptions, field, value, rowIdx }: ImageCellProps)
export const getStyles: TableCellStyles = () =>
css({
'a, img': {
'&, a, img': {
width: '100%',
height: '100%',
},

View File

@ -3,6 +3,7 @@ import { css } from '@emotion/css';
import { renderMarkdown } from '@grafana/data';
import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink';
import { getActiveCellSelector } from '../styles';
import { MarkdownCellProps, TableCellStyles } from '../types';
export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) {
@ -25,9 +26,9 @@ export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCel
);
}
export const getStyles: TableCellStyles = (theme) =>
export const getStyles: TableCellStyles = (theme, { maxHeight }) =>
css({
'&, &:hover, &[aria-selected=true]': {
[`&, ${getActiveCellSelector(Boolean(maxHeight))}`]: {
whiteSpace: 'normal',
},

View File

@ -11,6 +11,7 @@ import {
} from '@grafana/data';
import { FieldColorModeId } from '@grafana/schema';
import { getActiveCellSelector } from '../styles';
import { PillCellProps, TableCellStyles, TableCellValue } from '../types';
export function PillCell({ rowIdx, field, theme, getTextColorForBackground }: PillCellProps) {
@ -101,14 +102,14 @@ function getPillColor(value: unknown, field: Field, theme: GrafanaTheme2): strin
return getColorByStringHash(colors, String(value));
}
export const getStyles: TableCellStyles = (theme, { textWrap, shouldOverflow }) =>
export const getStyles: TableCellStyles = (theme, { textWrap, shouldOverflow, maxHeight }) =>
css({
display: 'inline-flex',
gap: theme.spacing(0.5),
flexWrap: textWrap ? 'wrap' : 'nowrap',
...(shouldOverflow && {
'&:hover, &[aria-selected=true]': {
[getActiveCellSelector(Boolean(maxHeight))]: {
flexWrap: 'wrap',
},
}),

View File

@ -61,6 +61,7 @@ import {
getGridStyles,
getHeaderCellStyles,
getLinkStyles,
getMaxHeightCellStyles,
getTooltipStyles,
} from './styles';
import {
@ -112,6 +113,7 @@ export function TableNG(props: TableNGProps) {
getActions = () => [],
height,
initialSortBy,
maxRowHeight: _maxRowHeight,
noHeader,
onCellFilterAdded,
onColumnResize,
@ -202,6 +204,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,
@ -209,6 +213,7 @@ export function TableNG(props: TableNGProps) {
defaultHeight: defaultRowHeight,
expandedRows,
typographyCtx,
maxHeight: maxRowHeight,
});
const {
@ -414,17 +419,24 @@ export function TableNG(props: TableNGProps) {
? clsx('table-cell-actions', getCellActionStyles(theme, textAlign))
: undefined;
const shouldOverflow = rowHeight !== 'auto' && shouldTextOverflow(field);
const shouldOverflow = rowHeight !== 'auto' && (shouldTextOverflow(field) || Boolean(maxRowHeight));
const textWrap = rowHeight === 'auto' || shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType);
const canBeColorized = canFieldBeColorized(cellType, applyToRowBgFn);
const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow };
const cellStyleOptions: TableCellStyleOptions = {
textAlign,
textWrap,
shouldOverflow,
maxHeight: maxRowHeight,
};
result.colsWithTooltip[displayName] = withTooltip;
const defaultCellStyles = getDefaultCellStyles(theme, cellStyleOptions);
const cellSpecificStyles = getCellSpecificStyles(cellType, field, theme, cellStyleOptions);
const linkStyles = getLinkStyles(theme, canBeColorized);
const cellParentStyles = clsx(defaultCellStyles, linkStyles);
const maxHeightClassName = maxRowHeight ? getMaxHeightCellStyles(theme, cellStyleOptions) : undefined;
// TODO: in future extend this to ensure a non-classic color scheme is set with AutoCell
@ -457,7 +469,11 @@ export function TableNG(props: TableNGProps) {
<Cell
key={key}
{...props}
className={clsx(props.className, defaultCellStyles, cellSpecificStyles, linkStyles)}
className={clsx(
props.className,
cellParentStyles,
cellSpecificStyles != null && { [cellSpecificStyles]: maxRowHeight == null }
)}
style={style}
/>
);
@ -475,7 +491,7 @@ export function TableNG(props: TableNGProps) {
const height = rowHeightFn(props.row);
const frame = data;
return (
let result = (
<>
<CellType
cellOptions={cellOptions}
@ -508,6 +524,12 @@ export function TableNG(props: TableNGProps) {
)}
</>
);
if (maxRowHeight != null) {
result = <div className={clsx(maxHeightClassName, cellSpecificStyles)}>{result}</div>;
}
return result;
};
// renderCellContent fires second.
@ -520,10 +542,12 @@ export function TableNG(props: TableNGProps) {
const tooltipDisplayName = getDisplayName(tooltipField);
const tooltipCellOptions = getCellOptions(tooltipField);
const tooltipFieldRenderer = getCellRenderer(tooltipField, tooltipCellOptions);
const tooltipCellStyleOptions = {
textAlign: getAlignment(tooltipField),
textWrap: shouldTextWrap(tooltipField),
shouldOverflow: false,
maxHeight: maxRowHeight,
} satisfies TableCellStyleOptions;
const tooltipCanBeColorized = canFieldBeColorized(tooltipCellOptions.type, applyToRowBgFn);
const tooltipDefaultStyles = getDefaultCellStyles(theme, tooltipCellStyleOptions);
@ -579,7 +603,12 @@ export function TableNG(props: TableNGProps) {
}
return (
<TableCellTooltip {...tooltipProps} height={tooltipHeight} rowIdx={props.rowIdx} style={tooltipStyle}>
<TableCellTooltip
{...tooltipProps}
height={tooltipHeight}
rowIdx={props.row.__index}
style={tooltipStyle}
>
{renderBasicCellContent(props)}
</TableCellTooltip>
);
@ -639,6 +668,7 @@ export function TableNG(props: TableNGProps) {
getCellColorInlineStyles,
getTextColorForBackground,
isCountRowsSet,
maxRowHeight,
numFrozenColsFullyInView,
onCellFilterAdded,
rowHeight,

View File

@ -144,7 +144,7 @@ export const TableCellTooltip = memo(
placement={placement}
wrapperClassName={classes.tooltipWrapper}
className={className}
style={{ ...style, minWidth: width, ...(!dynamicHeight && { height }) }}
style={{ ...style, width, ...(!dynamicHeight && { height }) }}
referenceElement={cellElement}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}

View File

@ -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(() => {

View File

@ -49,7 +49,7 @@ export const getGridStyles = (theme: GrafanaTheme2, enablePagination?: boolean,
// add a box shadow on hover and selection for all body cells
'& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': {
'&:hover, &[aria-selected=true]': { boxShadow: theme.shadows.z2 },
[getActiveCellSelector()]: { boxShadow: theme.shadows.z2 },
// selected cells should appear below hovered cells.
'&:hover': { zIndex: theme.zIndex.tooltip - 7 },
'&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 6 },
@ -128,14 +128,16 @@ export const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Proper
'&:last-child': { borderInlineEnd: 'none' },
});
export const getDefaultCellStyles: TableCellStyles = (theme, { textAlign, shouldOverflow }) =>
export const getDefaultCellStyles: TableCellStyles = (theme, { textAlign, shouldOverflow, maxHeight }) =>
css({
display: 'flex',
alignItems: 'center',
textAlign,
justifyContent: getJustifyContent(textAlign),
justifyContent: Boolean(maxHeight) ? 'flex-start' : getJustifyContent(textAlign),
...(maxHeight && { overflowY: 'hidden' }),
...(shouldOverflow && { minHeight: '100%' }),
'&:hover, &[aria-selected=true]': {
[getActiveCellSelector()]: {
'.table-cell-actions': { display: 'flex' },
...(shouldOverflow && {
zIndex: theme.zIndex.tooltip - 2,
@ -145,6 +147,21 @@ export const getDefaultCellStyles: TableCellStyles = (theme, { textAlign, should
},
});
export const getMaxHeightCellStyles: TableCellStyles = (_theme, { textAlign, maxHeight }) =>
css({
display: 'flex',
alignItems: 'center',
textAlign,
justifyContent: getJustifyContent(textAlign),
maxHeight,
width: '100%',
overflowY: 'hidden',
[getActiveCellSelector(true)]: {
maxHeight: 'none',
minHeight: '100%',
},
});
export const getCellActionStyles = (theme: GrafanaTheme2, textAlign: TextAlign) =>
css({
display: 'none',
@ -183,6 +200,8 @@ export const getTooltipStyles = (theme: GrafanaTheme2, textAlign: TextAlign) =>
tooltipContent: css({
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
}),
tooltipWrapper: css({
background: theme.colors.background.primary,
@ -206,3 +225,6 @@ export const getTooltipStyles = (theme: GrafanaTheme2, textAlign: TextAlign) =>
},
}),
});
export const getActiveCellSelector = (isNested?: boolean) =>
isNested ? '.rdg-cell:hover &, [aria-selected=true] &' : '&:hover, &[aria-selected=true]';

View File

@ -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 */
@ -258,6 +259,7 @@ export interface TableCellStyleOptions {
textWrap: boolean;
textAlign: TextAlign;
shouldOverflow: boolean;
maxHeight?: number;
}
export type TableCellStyles = (theme: GrafanaTheme2, options: TableCellStyleOptions) => string;

View File

@ -1158,6 +1158,23 @@ describe('TableNG utils', () => {
const measurers = buildCellHeightMeasurers(fields, ctx);
expect(measurers).toBeUndefined();
});
it('clamps by maxHeight if set', () => {
const fields: Field[] = [
{
name: 'Tags',
type: FieldType.string,
values: ['tag1,tag2', 'tag3', '["tag4","tag5","tag6"]'],
config: { custom: { wrapText: true, cellOptions: { type: TableCellDisplayMode.Pill } } },
},
];
const measurers = buildCellHeightMeasurers(fields, ctx);
expect(measurers![0].measure!(fields[0].values[2], 20, fields[0], 2, 100)).toBeGreaterThan(50);
fields[0].config!.custom!.maxHeight = 50;
const measurersWithMax = buildCellHeightMeasurers(fields, ctx, 50);
expect(measurersWithMax![0].measure!(fields[0].values[2], 20, fields[0], 2, 100)).toBe(50);
});
});
describe('getRowHeight', () => {

View File

@ -84,6 +84,16 @@ export function shouldTextWrap(field: Field): boolean {
return Boolean(field.config.custom?.wrapText);
}
/**
* @internal wrap a cell height measurer to clamp its output to the maxHeight defined in the field, if any.
*/
function clampByMaxHeight(measurer: MeasureCellHeight, maxHeight = Infinity): MeasureCellHeight {
return (value, width, field, rowIdx, lineHeight) => {
const rawHeight = measurer(value, width, field, rowIdx, lineHeight);
return Math.min(rawHeight, maxHeight);
};
}
/**
* @internal creates a typography context based on a font size and family. used to measure text
* and estimate size of text in cells.
@ -249,7 +259,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;
@ -279,8 +290,8 @@ export function buildCellHeightMeasurers(
if (!result[measurerFactoryKey]) {
const [measure, estimate] = measurerFactory[measurerFactoryKey]();
result[measurerFactoryKey] = {
measure,
estimate,
measure: clampByMaxHeight(measure, maxHeight),
estimate: estimate != null ? clampByMaxHeight(estimate, maxHeight) : undefined,
fieldIdxs: [],
};
}

View File

@ -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}

View File

@ -198,6 +198,15 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
],
},
})
.addNumberInput({
path: 'maxRowHeight',
name: t('table.name-max-height', 'Max row height'),
category,
settings: {
placeholder: t('table.placeholder-max-height', 'none'),
min: 0,
},
})
.addBooleanSwitch({
path: 'footer.show',
category: footerCategory,

View File

@ -44,6 +44,8 @@ composableKinds: PanelCfg: {
}
// Controls the height of the rows
cellHeight?: ui.TableCellHeight & (*"sm" | _)
// 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

View File

@ -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
*/

View File

@ -12812,6 +12812,7 @@
"name-fields": "Fields",
"name-frozen-columns": "Frozen columns",
"name-hide-in-table": "Hide in table",
"name-max-height": "Max row height",
"name-min-column-width": "Minimum column width",
"name-show-table-footer": "Show table footer",
"name-show-table-header": "Show table header",
@ -12821,6 +12822,7 @@
"name-wrap-text": "Wrap text",
"placeholder-column-width": "auto",
"placeholder-fields": "All Numeric Fields",
"placeholder-max-height": "none",
"tooltip-placement-options": {
"label-auto": "Auto",
"label-bottom": "Bottom",