change to Max row height instead of Max cell height
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user