Table Panel: Enable text wrapping (#86895)

* Calculate row height

* Move things around

* Update getItemSize to use text bounding box

* Update types

* Cleanups and reminders

* Calculate line height

* Update line height calculation

* Remove debugging

* Add cell option editing

* Prettier

* Use field configured for text wrapping

* Add TODO

* Make sure column configuration is correct

* Update height heuristic and hover behavior

* Disable overflow on hover with text wrapping

* Update heuristic

* Clean things up

* Color background cell options

* Fix tests

* Prettier

* React deps

* Remove old hook dep

* Fix type errors

* Update label and description for editor

* Fix non-wrapped case

* Make sure color background works

* Prettier

* Address review comments

* fix prettier

* Add heuristic for field sizing

* Fix up logic

* Prettier

* Fix test

* Oh prettier 🙈

* Don't wrap text on non-string fields

* Add wrapping to color text cell

* Prettier

* Fix option not showing for auto cell type

* Move longest field guessing into function

* Clean things up

* Add tests

* Make sure text won't flake

* Prettier

* Remove spurious import

* Ignore any in this case

* Add alpha label

* Prettier

* Fix typecheck

* Fix crash when sampling when there are undefined records

* Update heuristic to take into account long strings

* Prettier

* Update scale factors

* Update field index selection

* Prettier

---------

Co-authored-by: jev forsberg <jev.forsberg@grafana.com>
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
Kyle Cunningham 2024-06-07 11:15:33 -05:00 committed by GitHub
parent 694499ae6d
commit 8aa1bbe27c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 413 additions and 15 deletions

View File

@ -565,8 +565,6 @@ github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240430073540-ce4d126ae8b8
github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240430073540-ce4d126ae8b8/go.mod h1:u4K9vVN6eU86loO68977eTXGypC4brUCnk4sfDzutZU=
github.com/grafana/grafana-plugin-sdk-go v0.228.0/go.mod h1:u4K9vVN6eU86loO68977eTXGypC4brUCnk4sfDzutZU=
github.com/grafana/grafana-plugin-sdk-go v0.229.0/go.mod h1:6V6ikT4ryva8MrAp7Bdz5fTJx3/ztzKvpMJFfpzr4CI=
github.com/grafana/grafana-plugin-sdk-go v0.230.0 h1:Y4IL+eT1jXqTCctlNzdCvxAozpBZ8xEsRGWjGAVRXxo=
github.com/grafana/grafana-plugin-sdk-go v0.230.0/go.mod h1:6V6ikT4ryva8MrAp7Bdz5fTJx3/ztzKvpMJFfpzr4CI=
github.com/grafana/grafana-plugin-sdk-go v0.231.1-0.20240523124942-62dae9836284/go.mod h1:bNgmNmub1I7Mc8dzIncgNqHC5jTgSZPPHlZ3aG8HKJQ=
github.com/grafana/grafana/pkg/promlib v0.0.3/go.mod h1:3El4NlsfALz8QQCbEGHGFvJUG+538QLMuALRhZ3pcoo=
github.com/grafana/grafana/pkg/promlib v0.0.6/go.mod h1:shFkrG1fQ/PPNRGhxAPNMLp0SAeG/jhqaLoG6n2191M=
@ -983,6 +981,7 @@ golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGb
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=

View File

@ -749,6 +749,7 @@ export const defaultTableFooterOptions: Partial<TableFooterOptions> = {
*/
export interface TableAutoCellOptions {
type: TableCellDisplayMode.Auto;
wrapText?: boolean;
}
/**
@ -756,6 +757,7 @@ export interface TableAutoCellOptions {
*/
export interface TableColorTextCellOptions {
type: TableCellDisplayMode.ColorText;
wrapText?: boolean;
}
/**
@ -803,12 +805,14 @@ export interface TableColoredBackgroundCellOptions {
applyToRow?: boolean;
mode?: TableCellBackgroundDisplayMode;
type: TableCellDisplayMode.ColorBackground;
wrapText?: boolean;
}
/**
* Height of a table cell
*/
export enum TableCellHeight {
Auto = 'auto',
Lg = 'lg',
Md = 'md',
Sm = 'sm',

View File

@ -31,11 +31,13 @@ TableFooterOptions: {
// Auto mode table cell options
TableAutoCellOptions: {
type: TableCellDisplayMode & "auto"
wrapText?: bool
} @cuetsy(kind="interface")
// Colored text cell options
TableColorTextCellOptions: {
type: TableCellDisplayMode & "color-text"
wrapText?: bool
} @cuetsy(kind="interface")
// Json view cell options
@ -72,10 +74,11 @@ TableColoredBackgroundCellOptions: {
type: TableCellDisplayMode & "color-background"
mode?: TableCellBackgroundDisplayMode
applyToRow?: bool
wrapText?: bool
} @cuetsy(kind="interface")
// Height of a table cell
TableCellHeight: "sm" | "md" | "lg" @cuetsy(kind="enum")
TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum")
// Table cell options. Each cell has a display mode
// and other potential options for that display.

View File

@ -110,6 +110,7 @@
},
"devDependencies": {
"@babel/core": "7.24.7",
"@faker-js/faker": "^8.4.1",
"@grafana/tsconfig": "^1.3.0-rc1",
"@rollup/plugin-node-resolve": "15.2.3",
"@storybook/addon-a11y": "^8.1.6",

View File

@ -15,7 +15,7 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ
import { getCellColors, getCellOptions } from './utils';
export const DefaultCell = (props: TableCellProps) => {
const { field, cell, tableStyles, row, cellProps, frame, rowStyled } = props;
const { field, cell, tableStyles, row, cellProps, frame, rowStyled, textWrapped, height } = props;
const inspectEnabled = Boolean(field.config.custom?.inspect);
const displayValue = field.display!(cell.value);
@ -59,6 +59,7 @@ export const DefaultCell = (props: TableCellProps) => {
inspectEnabled,
isStringValue,
textShouldWrap,
textWrapped,
rowStyled
);
@ -72,6 +73,14 @@ export const DefaultCell = (props: TableCellProps) => {
}
}
if (height) {
cellProps.style = { ...cellProps.style, height };
}
if (textWrapped) {
cellProps.style = { ...cellProps.style, textWrap: 'wrap' };
}
return (
<div
{...cellProps}
@ -112,6 +121,7 @@ function getCellStyle(
disableOverflowOnHover = false,
isStringValue = false,
shouldWrapText = false,
textWrapped = false,
rowStyled = false
) {
// Setup color variables
@ -134,6 +144,7 @@ function getCellStyle(
!disableOverflowOnHover,
isStringValue,
shouldWrapText,
textWrapped,
rowStyled
);
}

View File

@ -1,6 +1,6 @@
import { css, cx } from '@emotion/css';
import React, { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
import { Cell, Row, TableState } from 'react-table';
import { Cell, Row, TableState, HeaderGroup } from 'react-table';
import { VariableSizeList } from 'react-window';
import { Subscription, debounceTime } from 'rxjs';
@ -23,7 +23,12 @@ import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow';
import { TableCell } from './TableCell';
import { TableStyles } from './styles';
import { CellColors, TableFieldOptions, TableFilterActionCallback } from './types';
import { calculateAroundPointThreshold, getCellColors, isPointTimeValAroundTableTimeVal } from './utils';
import {
calculateAroundPointThreshold,
getCellColors,
isPointTimeValAroundTableTimeVal,
guessTextBoundingBox,
} from './utils';
interface RowsListProps {
data: DataFrame;
@ -45,6 +50,8 @@ interface RowsListProps {
timeRange?: TimeRange;
footerPaginationEnabled: boolean;
initialRowIndex?: number;
headerGroups: HeaderGroup[];
longestField?: Field;
}
export const RowsList = (props: RowsListProps) => {
@ -68,6 +75,8 @@ export const RowsList = (props: RowsListProps) => {
listRef,
enableSharedCrosshair = false,
initialRowIndex = undefined,
headerGroups,
longestField,
} = props;
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
@ -75,6 +84,23 @@ export const RowsList = (props: RowsListProps) => {
const theme = useTheme2();
const panelContext = usePanelContext();
// Create off-screen canvas for measuring rows for virtualized rendering
// This line is like this because Jest doesn't have OffscreenCanvas mocked
// nor is it a part of the jest-canvas-mock package
let osContext = null;
if (window.OffscreenCanvas !== undefined) {
// The canvas size is defined arbitrarily
// As we never actually visualize rendered content
// from the offscreen canvas, only perform text measurements
osContext = new OffscreenCanvas(256, 1024).getContext('2d');
}
// Set font property using theme info
// This will make text measurement accurate
if (osContext !== undefined && osContext !== null) {
osContext.font = `${theme.typography.fontSize}px ${theme.typography.body.fontFamily}`;
}
const threshold = useMemo(() => {
const timeField = data.fields.find((f) => f.type === FieldType.time);
@ -202,13 +228,14 @@ export const RowsList = (props: RowsListProps) => {
);
let rowBg: Function | undefined = undefined;
let textWrapField: Field | undefined = undefined;
for (const field of data.fields) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const fieldOptions = field.config.custom as TableFieldOptions;
const cellOptionsExist = fieldOptions !== undefined && fieldOptions.cellOptions !== undefined;
if (
fieldOptions !== undefined &&
fieldOptions.cellOptions !== undefined &&
cellOptionsExist &&
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground &&
fieldOptions.cellOptions.applyToRow
) {
@ -218,6 +245,18 @@ export const RowsList = (props: RowsListProps) => {
return colors;
};
}
if (
cellOptionsExist &&
(fieldOptions.cellOptions.type === TableCellDisplayMode.Auto ||
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground ||
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorText) &&
fieldOptions.cellOptions.wrapText
) {
textWrapField = field;
} else if (longestField !== undefined) {
textWrapField = longestField;
}
}
const RenderRow = useCallback(
@ -236,12 +275,27 @@ export const RowsList = (props: RowsListProps) => {
};
}
// Color rows if enabled
if (rowBg) {
const { bgColor, textColor } = rowBg(row.index);
style.background = bgColor;
style.color = textColor;
}
// If there's a text wrapping field we set the height of it here
if (textWrapField) {
const seriesIndex = data.fields.findIndex((field) => field.name === textWrapField.name);
const pxLineHeight = theme.typography.body.lineHeight * theme.typography.fontSize;
const bbox = guessTextBoundingBox(
textWrapField.values[index],
headerGroups[0].headers[seriesIndex],
osContext,
pxLineHeight,
tableStyles.rowHeight
);
style.height = bbox.height;
}
return (
<div
{...row.getRowProps({ style, ...additionalProps })}
@ -272,6 +326,8 @@ export const RowsList = (props: RowsListProps) => {
timeRange={timeRange}
frame={data}
rowStyled={rowBg !== undefined}
textWrapped={textWrapField !== undefined}
height={Number(style.height)}
/>
))}
</div>
@ -289,20 +345,38 @@ export const RowsList = (props: RowsListProps) => {
rows,
tableState.expanded,
tableStyles,
textWrapField,
theme.components.table.rowHoverBackground,
theme.typography.fontSize,
theme.typography.body.lineHeight,
timeRange,
width,
rowBg,
headerGroups,
osContext,
]
);
const getItemSize = (index: number): number => {
const indexForPagination = rowIndexForPagination(index);
const row = rows[indexForPagination];
if (tableState.expanded[row.id] && nestedDataField) {
return getExpandedRowHeight(nestedDataField, row.index, tableStyles);
}
if (textWrapField) {
const seriesIndex = data.fields.findIndex((field) => field.name === textWrapField.name);
const pxLineHeight = theme.typography.fontSize * theme.typography.body.lineHeight;
return guessTextBoundingBox(
textWrapField.values[index],
headerGroups[0].headers[seriesIndex],
osContext,
pxLineHeight,
tableStyles.rowHeight
).height;
}
return tableStyles.rowHeight;
};

View File

@ -25,7 +25,14 @@ import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks
import { getInitialState, useTableStateReducer } from './reducer';
import { useTableStyles } from './styles';
import { FooterItem, GrafanaTableState, Props } from './types';
import { getColumns, sortCaseInsensitive, sortNumber, getFooterItems, createFooterCalculationValues } from './utils';
import {
getColumns,
sortCaseInsensitive,
sortNumber,
getFooterItems,
createFooterCalculationValues,
guessLongestField,
} from './utils';
const COLUMN_MIN_WIDTH = 150;
const FOOTER_ROW_HEIGHT = 36;
@ -287,6 +294,9 @@ export const Table = memo((props: Props) => {
);
}
// Try to determine the longet field
const longestField = guessLongestField(fieldConfig, data);
return (
<div
{...getTableProps()}
@ -304,6 +314,7 @@ export const Table = memo((props: Props) => {
{itemCount > 0 ? (
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
<RowsList
headerGroups={headerGroups}
data={data}
rows={rows}
width={width}
@ -323,6 +334,7 @@ export const Table = memo((props: Props) => {
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
/>
</div>
) : (

View File

@ -16,9 +16,21 @@ export interface Props {
userProps?: object;
frame: DataFrame;
rowStyled?: boolean;
textWrapped?: boolean;
height?: number;
}
export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, userProps, frame, rowStyled }: Props) => {
export const TableCell = ({
cell,
tableStyles,
onCellFilterAdded,
timeRange,
userProps,
frame,
rowStyled,
textWrapped,
height,
}: Props) => {
const cellProps = cell.getCellProps();
const field = (cell.column as unknown as GrafanaTableColumn).field;
@ -45,6 +57,8 @@ export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, use
userProps,
frame,
rowStyled,
textWrapped,
height,
})}
</>
);

View File

@ -18,6 +18,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
overflowOnHover?: boolean,
asCellText?: boolean,
textShouldWrap?: boolean,
textWrapped?: boolean,
rowStyled?: boolean
) => {
return css({
@ -26,6 +27,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
width: '100%',
// Cell height need to account for row border
height: `${rowHeight - 1}px`,
wordBreak: textWrapped ? 'break-all' : 'inherit',
display: 'flex',
@ -50,9 +52,9 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
},
'&:hover': {
overflow: overflowOnHover ? 'visible' : undefined,
overflow: overflowOnHover && !textWrapped ? 'visible' : undefined,
width: textShouldWrap || !overflowOnHover ? 'auto' : 'auto !important',
height: textShouldWrap || overflowOnHover ? 'auto !important' : `${rowHeight - 1}px`,
height: (textShouldWrap || overflowOnHover) && !textWrapped ? 'auto !important' : `${rowHeight - 1}px`,
minHeight: `${rowHeight - 1}px`,
wordBreak: textShouldWrap ? 'break-word' : undefined,
whiteSpace: textShouldWrap && overflowOnHover ? 'normal' : 'nowrap',

View File

@ -1,3 +1,4 @@
import { faker } from '@faker-js/faker';
import { Row } from 'react-table';
import { Field, FieldType, MutableDataFrame, SelectableValue } from '@grafana/data';
@ -12,6 +13,7 @@ import {
sortNumber,
sortOptions,
valuesToOptions,
guessLongestField,
} from './utils';
function getData() {
@ -43,6 +45,45 @@ function getData() {
return data;
}
function getWrappableData(numRecords: number) {
const data = new MutableDataFrame({
fields: [
{ name: 'Time', type: FieldType.time, values: [] },
{
name: 'Lorem 5',
type: FieldType.string,
values: [],
config: {
custom: {
align: 'center',
},
},
},
{
name: 'Lorem 10',
type: FieldType.string,
values: [],
config: {
custom: {
align: 'center',
},
},
},
],
});
// Set values for the dataframe
// We're not concerned about time in
// this case so we simply leave it as zero
for (let i = 0; i < numRecords; i++) {
data.fields[0].values[i] = 0;
data.fields[1].values[i] = faker.lorem.paragraphs(9);
data.fields[2].values[i] = faker.lorem.paragraphs(11);
}
return data;
}
describe('Table utils', () => {
describe('getColumns', () => {
it('Should build columns from DataFrame', () => {
@ -502,4 +543,54 @@ describe('Table utils', () => {
expect(diff).toBeLessThanOrEqual(20);
});
});
describe('guessLongestField', () => {
it('should guess the longest field correct if there are few records', () => {
const data = getWrappableData(10);
const config = {
defaults: {
custom: {
cellOptions: {
wrapText: true,
},
},
},
};
const longestField = guessLongestField(config, data);
expect(longestField?.name).toBe('Lorem 10');
});
it('should guess the longest field correctly if there are many records', () => {
const data = getWrappableData(1000);
const config = {
defaults: {
custom: {
cellOptions: {
wrapText: true,
},
},
},
};
const longestField = guessLongestField(config, data);
expect(longestField?.name).toBe('Lorem 10');
});
it('should return undefined if there is no data', () => {
const data = getData();
const config = {
defaults: {
custom: {
cellOptions: {
wrapText: true,
},
},
},
};
const longestField = guessLongestField(config, data);
expect(longestField).toBe(undefined);
});
});
});

View File

@ -1,7 +1,7 @@
import { Property } from 'csstype';
import { clone } from 'lodash';
import { clone, sampleSize } from 'lodash';
import memoize from 'micro-memoize';
import { Row } from 'react-table';
import { Row, HeaderGroup } from 'react-table';
import tinycolor from 'tinycolor2';
import {
@ -633,3 +633,130 @@ export function getCellColors(
return { textColor, bgColor, bgHoverColor };
}
/**
* Calculate an estimated bounding box for a block
* of text using an offscreen canvas.
*/
export function guessTextBoundingBox(
text: string,
headerGroup: HeaderGroup,
osContext: OffscreenCanvasRenderingContext2D | null,
lineHeight: number,
defaultRowHeight: number
) {
const width = Number(headerGroup.width ?? 300);
const LINE_SCALE_FACTOR = 1.17;
const LOW_LINE_PAD = 42;
if (osContext !== null && typeof text === 'string') {
const words = text.split(/\s/);
const lines = [];
let currentLine = '';
let wordCount = 0;
let extraLines = 0;
// Let's just wrap the lines and see how well the measurement works
for (let i = 0; i < words.length; i++) {
const currentWord = words[i];
let lineWidth = osContext.measureText(currentLine + ' ' + currentWord).width;
if (lineWidth < width) {
currentLine += ' ' + currentWord;
wordCount++;
} else {
lines.push({
width: lineWidth,
line: currentLine,
});
currentLine = currentWord;
wordCount = 0;
}
}
// We can have extra long strings, for these
// we estimate if it overshoots the line by
// at least one other line
for (let i = 0; i < lines.length; i++) {
if (lines[i].width > width) {
let extra = Math.floor(lines[i].width / width) - 1;
extraLines += extra;
}
}
// Estimated height would be lines multiplied
// by the line height
let lineNumber = lines.length + extraLines;
let height = 38;
if (lineNumber > 5) {
height = lineNumber * lineHeight * LINE_SCALE_FACTOR;
} else {
height = lineNumber * lineHeight + LOW_LINE_PAD;
}
return { width, height };
}
return { width, height: defaultRowHeight };
}
/**
* A function to guess at which field has the longest text.
* To do this we either select a single record if there aren't many records
* or we select records at random and sample their size.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function guessLongestField(fieldConfig: any, data: DataFrame) {
let longestField = undefined;
const SAMPLE_SIZE = 3;
// If the default field option is set to allow text wrapping
// we determine the field to wrap text with here and then
// pass it to the RowsList
if (
fieldConfig !== undefined &&
fieldConfig.defaults.custom !== undefined &&
fieldConfig.defaults.custom.cellOptions.wrapText
) {
const stringFields = data.fields.filter((field: Field) => field.type === FieldType.string);
if (stringFields.length >= 1 && stringFields[0].values.length > 0) {
const numValues = stringFields[0].values.length;
let longestLength = 0;
// If we have less than 30 values we assume
// that the first record is representative
// of the overall data
if (numValues <= 30) {
for (const field of stringFields) {
const fieldLength = field.values[0].length;
if (fieldLength > longestLength) {
longestLength = fieldLength;
longestField = field;
}
}
}
// Otherwise we randomly sample SAMPLE_SIZE values and take
// the mean length
else {
for (const field of stringFields) {
// This could result in duplicate values but
// that should be fairly unlikely. This could potentially
// be improved using a Set datastructure but
// going to leave that one as an exercise for
// the reader to contemplate and possibly code
const vals = sampleSize(field.values, SAMPLE_SIZE);
const meanLength = (vals[0]?.length + vals[1]?.length + vals[2]?.length) / 3;
if (meanLength > longestLength) {
longestLength = meanLength;
longestField = field;
}
}
}
}
}
return longestField;
}

View File

@ -6,6 +6,7 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { TableCellOptions } from '@grafana/schema';
import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui';
import { AutoCellOptionsEditor } from './cells/AutoCellOptionsEditor';
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
@ -60,6 +61,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
<Field>
<Select options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
</Field>
{(cellType === TableCellDisplayMode.Auto || cellType === TableCellDisplayMode.ColorText) && (
<AutoCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
)}
{cellType === TableCellDisplayMode.Gauge && (
<BarGaugeCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
)}

View File

@ -0,0 +1,32 @@
import React from 'react';
import { TableAutoCellOptions, TableColorTextCellOptions } from '@grafana/schema';
import { Field, Switch, Badge, Label } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
export const AutoCellOptionsEditor = ({
cellOptions,
onChange,
}: TableCellEditorProps<TableAutoCellOptions | TableColorTextCellOptions>) => {
// Handle row coloring changes
const onWrapTextChange = () => {
cellOptions.wrapText = !cellOptions.wrapText;
onChange(cellOptions);
};
const label = (
<Label description="If selected text will be wrapped to the width of text in the configured column">
{'Wrap text '}
<Badge text="Alpha" color="blue" style={{ fontSize: '11px', marginLeft: '5px', lineHeight: '1.2' }} />
</Label>
);
return (
<>
<Field label={label}>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>
</>
);
};

View File

@ -2,7 +2,7 @@ import React from 'react';
import { SelectableValue } from '@grafana/data';
import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema';
import { Field, RadioButtonGroup, Switch } from '@grafana/ui';
import { Field, RadioButtonGroup, Switch, Label, Badge } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
@ -27,6 +27,19 @@ export const ColorBackgroundCellOptionsEditor = ({
onChange(cellOptions);
};
// Handle row coloring changes
const onWrapTextChange = () => {
cellOptions.wrapText = !cellOptions.wrapText;
onChange(cellOptions);
};
const label = (
<Label description="If selected text will be wrapped to the width of text in the configured column">
{'Wrap text '}
<Badge text="Alpha" color="blue" style={{ fontSize: '11px', marginLeft: '5px', lineHeight: '1.2' }} />
</Label>
);
return (
<>
<Field label="Background display mode">
@ -42,6 +55,9 @@ export const ColorBackgroundCellOptionsEditor = ({
>
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
</Field>
<Field label={label}>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>
</>
);
};

View File

@ -2418,6 +2418,13 @@ __metadata:
languageName: node
linkType: hard
"@faker-js/faker@npm:^8.4.1":
version: 8.4.1
resolution: "@faker-js/faker@npm:8.4.1"
checksum: 10/5983c2ea64f26055ad6648de748878e11ebe2fb751e3c7435ae141cdffabc2dccfe4c4f49da69a3d2add71e21b415c683ac5fba196fab0d5ed6779fbec436c80
languageName: node
linkType: hard
"@fal-works/esbuild-plugin-global-externals@npm:^2.1.2":
version: 2.1.2
resolution: "@fal-works/esbuild-plugin-global-externals@npm:2.1.2"
@ -3605,6 +3612,7 @@ __metadata:
"@babel/core": "npm:7.24.7"
"@emotion/css": "npm:11.11.2"
"@emotion/react": "npm:11.11.4"
"@faker-js/faker": "npm:^8.4.1"
"@floating-ui/react": "npm:0.26.16"
"@grafana/data": "npm:11.0.0"
"@grafana/e2e-selectors": "npm:11.0.0"