diff --git a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json
index 2b9ab7817cf..44c37b2ae71 100644
--- a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json
+++ b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json
@@ -295,6 +295,10 @@
"wrapText": true
}
},
+ {
+ "id": "custom.maxHeight",
+ "value": null
+ },
{
"id": "custom.width",
"value": 255
diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts
index c0efd234599..1a66796e84c 100644
--- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts
+++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts
@@ -55,7 +55,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 +77,22 @@ 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);
- await dashboardPage
- .getByGrafanaSelector(selectors.components.OptionsGroup.group('panel-options-override-12'))
- .getByText('Wrap text')
- .click();
+ // 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');
+ await expect(async () => {
+ await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
+ }).toPass();
+ await maxCellHeightInput.clear();
+
+ await longTextFieldOverrides.getByLabel('Wrap text').click({ force: true });
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
// test that hover overflow works.
diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts
index 8d7c5f85bef..e847cd43c4f 100644
--- a/packages/grafana-schema/src/common/common.gen.ts
+++ b/packages/grafana-schema/src/common/common.gen.ts
@@ -1009,6 +1009,10 @@ 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
diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue
index 91def1fa980..8b96bad7aca 100644
--- a/packages/grafana-schema/src/common/table.cue
+++ b/packages/grafana-schema/src/common/table.cue
@@ -131,6 +131,8 @@ 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
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
index a1e7aa55ec0..24774d9df91 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
@@ -3,6 +3,7 @@ import { css } from '@emotion/css';
import { formattedValueToString } from '@grafana/data';
import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink';
+import { TABLE } from '../constants';
import { AutoCellProps, TableCellStyles } from '../types';
export function AutoCell({ value, field, rowIdx }: AutoCellProps) {
@@ -15,7 +16,7 @@ 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 && {
@@ -23,6 +24,14 @@ export const getStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow })
whiteSpace: 'pre-line',
},
}),
+ ...(typeof maxHeight === 'number' && {
+ 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 }) =>
diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
index 39979cabfb3..75724676a4f 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
@@ -89,6 +89,7 @@ import {
getDisplayName,
getIsNestedTable,
getJustifyContent,
+ getMaxHeight,
getVisibleFields,
isCellInspectEnabled,
predicateByName,
@@ -418,13 +419,15 @@ export function TableNG(props: TableNGProps) {
const textWrap = rowHeight === 'auto' || shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType);
const canBeColorized = canFieldBeColorized(cellType, applyToRowBgFn);
- const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow };
+ const maxHeight = getMaxHeight(field);
+ const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow, maxHeight };
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, cellSpecificStyles, linkStyles);
// TODO: in future extend this to ensure a non-classic color scheme is set with AutoCell
@@ -457,7 +460,7 @@ export function TableNG(props: TableNGProps) {
|
);
@@ -465,6 +468,12 @@ export function TableNG(props: TableNGProps) {
result.cellRootRenderers[displayName] = renderCellRoot;
+ const clampByMaxHeight = (maxHeight: number, children: ReactNode, cellStyles: string) => (
+
+ {children}
+
+ );
+
const renderBasicCellContent = (props: RenderCellProps): JSX.Element => {
const rowIdx = props.row.__index;
const value = props.row[props.column.key];
@@ -475,7 +484,7 @@ export function TableNG(props: TableNGProps) {
const height = rowHeightFn(props.row);
const frame = data;
- return (
+ let result = (
<>
);
+
+ if (maxHeight != null) {
+ result = clampByMaxHeight(maxHeight, result, cellParentStyles);
+ }
+
+ return result;
};
// renderCellContent fires second.
@@ -519,11 +534,13 @@ export function TableNG(props: TableNGProps) {
if (tooltipField) {
const tooltipDisplayName = getDisplayName(tooltipField);
const tooltipCellOptions = getCellOptions(tooltipField);
- const tooltipFieldRenderer = getCellRenderer(tooltipField, tooltipCellOptions);
+ const tooltipMaxHeight = getMaxHeight(tooltipField);
+
const tooltipCellStyleOptions = {
textAlign: getAlignment(tooltipField),
textWrap: shouldTextWrap(tooltipField),
shouldOverflow: false,
+ maxHeight: tooltipMaxHeight,
} satisfies TableCellStyleOptions;
const tooltipCanBeColorized = canFieldBeColorized(tooltipCellOptions.type, applyToRowBgFn);
const tooltipDefaultStyles = getDefaultCellStyles(theme, tooltipCellStyleOptions);
@@ -535,6 +552,14 @@ export function TableNG(props: TableNGProps) {
);
const tooltipLinkStyles = getLinkStyles(theme, tooltipCanBeColorized);
const tooltipClasses = getTooltipStyles(theme, textAlign);
+ const tooltipBodyClasses = clsx(tooltipDefaultStyles, tooltipSpecificStyles, tooltipLinkStyles);
+
+ let tooltipFieldRenderer = getCellRenderer(tooltipField, tooltipCellOptions);
+ if (tooltipMaxHeight) {
+ const OrigCellRenderer = tooltipFieldRenderer;
+ tooltipFieldRenderer = (props) =>
+ clampByMaxHeight(tooltipMaxHeight, , tooltipBodyClasses);
+ }
const placement = field.config.custom?.tooltip?.placement ?? TableCellTooltipPlacement.Auto;
const tooltipWidth =
@@ -545,12 +570,7 @@ export function TableNG(props: TableNGProps) {
const tooltipProps = {
cellOptions: tooltipCellOptions,
classes: tooltipClasses,
- className: clsx(
- tooltipClasses.tooltipContent,
- tooltipDefaultStyles,
- tooltipSpecificStyles,
- tooltipLinkStyles
- ),
+ className: clsx(tooltipClasses.tooltipContent, { [tooltipBodyClasses]: tooltipMaxHeight == null }),
data,
disableSanitizeHtml,
field: tooltipField,
@@ -579,7 +599,12 @@ export function TableNG(props: TableNGProps) {
}
return (
-
+
{renderBasicCellContent(props)}
);
@@ -646,6 +671,7 @@ export function TableNG(props: TableNGProps) {
rows,
setFilter,
showTypeIcons,
+ styles.cellClamp,
theme,
timeRange,
]
diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/TableCellTooltip.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/TableCellTooltip.tsx
index 232f3bb01a5..660f85397f6 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/components/TableCellTooltip.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/components/TableCellTooltip.tsx
@@ -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}
diff --git a/packages/grafana-ui/src/components/Table/TableNG/styles.ts b/packages/grafana-ui/src/components/Table/TableNG/styles.ts
index 1094108a587..89c16e266a9 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/styles.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/styles.ts
@@ -80,6 +80,10 @@ export const getGridStyles = (theme: GrafanaTheme2, enablePagination?: boolean,
marginBlock: TABLE.CELL_PADDING,
}),
cellNested: css({ '&[aria-selected=true]': { outline: 'none' } }),
+ cellClamp: css({
+ overflowY: 'hidden',
+ justifyContent: 'flex-start !important',
+ }),
noDataNested: css({
height: TABLE.NESTED_NO_DATA_HEIGHT,
display: 'flex',
@@ -183,6 +187,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,
diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts
index 123a8f39e7d..10e419121b2 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/types.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts
@@ -258,6 +258,7 @@ export interface TableCellStyleOptions {
textWrap: boolean;
textAlign: TextAlign;
shouldOverflow: boolean;
+ maxHeight?: number;
}
export type TableCellStyles = (theme: GrafanaTheme2, options: TableCellStyleOptions) => string;
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
index 0bc491d5733..5946b3865b6 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
@@ -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);
+ expect(measurersWithMax![0].measure!(fields[0].values[2], 20, fields[0], 2, 100)).toBe(50);
+ });
});
describe('getRowHeight', () => {
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
index 0e818b9eac2..e22104dbd6f 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
@@ -84,6 +84,25 @@ 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 {
+ return (value, width, field, rowIdx, lineHeight) => {
+ const rawHeight = measurer(value, width, field, rowIdx, lineHeight);
+ const maxHeight = getMaxHeight(field);
+ if (typeof maxHeight !== 'number') {
+ return rawHeight;
+ }
+
+ 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.
@@ -279,8 +298,8 @@ export function buildCellHeightMeasurers(
if (!result[measurerFactoryKey]) {
const [measure, estimate] = measurerFactory[measurerFactoryKey]();
result[measurerFactoryKey] = {
- measure,
- estimate,
+ measure: clampByMaxHeight(measure),
+ estimate: estimate != null ? clampByMaxHeight(estimate) : undefined,
fieldIdxs: [],
};
}
diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx
index 2dafbfba899..83399bbf018 100644
--- a/public/app/plugins/panel/table/module.tsx
+++ b/public/app/plugins/panel/table/module.tsx
@@ -113,6 +113,16 @@ export const plugin = new PanelPlugin(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'),
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 86c5e18ab7d..14c4abc0315 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -12808,6 +12808,10 @@
"name-wrap-text": "Wrap text",
"placeholder-column-width": "auto",
"placeholder-fields": "All Numeric Fields",
+ "text-wrap-options": {
+ "label-max-height": "Max cell height",
+ "placeholder-max-height": "none"
+ },
"tooltip-placement-options": {
"label-auto": "Auto",
"label-bottom": "Bottom",