mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
694499ae6d
commit
8aa1bbe27c
@ -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.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.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.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-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.3/go.mod h1:3El4NlsfALz8QQCbEGHGFvJUG+538QLMuALRhZ3pcoo=
|
||||||
github.com/grafana/grafana/pkg/promlib v0.0.6/go.mod h1:shFkrG1fQ/PPNRGhxAPNMLp0SAeG/jhqaLoG6n2191M=
|
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/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/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 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-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 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
|
||||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||||
|
@ -749,6 +749,7 @@ export const defaultTableFooterOptions: Partial<TableFooterOptions> = {
|
|||||||
*/
|
*/
|
||||||
export interface TableAutoCellOptions {
|
export interface TableAutoCellOptions {
|
||||||
type: TableCellDisplayMode.Auto;
|
type: TableCellDisplayMode.Auto;
|
||||||
|
wrapText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -756,6 +757,7 @@ export interface TableAutoCellOptions {
|
|||||||
*/
|
*/
|
||||||
export interface TableColorTextCellOptions {
|
export interface TableColorTextCellOptions {
|
||||||
type: TableCellDisplayMode.ColorText;
|
type: TableCellDisplayMode.ColorText;
|
||||||
|
wrapText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -803,12 +805,14 @@ export interface TableColoredBackgroundCellOptions {
|
|||||||
applyToRow?: boolean;
|
applyToRow?: boolean;
|
||||||
mode?: TableCellBackgroundDisplayMode;
|
mode?: TableCellBackgroundDisplayMode;
|
||||||
type: TableCellDisplayMode.ColorBackground;
|
type: TableCellDisplayMode.ColorBackground;
|
||||||
|
wrapText?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Height of a table cell
|
* Height of a table cell
|
||||||
*/
|
*/
|
||||||
export enum TableCellHeight {
|
export enum TableCellHeight {
|
||||||
|
Auto = 'auto',
|
||||||
Lg = 'lg',
|
Lg = 'lg',
|
||||||
Md = 'md',
|
Md = 'md',
|
||||||
Sm = 'sm',
|
Sm = 'sm',
|
||||||
|
@ -31,11 +31,13 @@ TableFooterOptions: {
|
|||||||
// Auto mode table cell options
|
// Auto mode table cell options
|
||||||
TableAutoCellOptions: {
|
TableAutoCellOptions: {
|
||||||
type: TableCellDisplayMode & "auto"
|
type: TableCellDisplayMode & "auto"
|
||||||
|
wrapText?: bool
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
// Colored text cell options
|
// Colored text cell options
|
||||||
TableColorTextCellOptions: {
|
TableColorTextCellOptions: {
|
||||||
type: TableCellDisplayMode & "color-text"
|
type: TableCellDisplayMode & "color-text"
|
||||||
|
wrapText?: bool
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
// Json view cell options
|
// Json view cell options
|
||||||
@ -72,10 +74,11 @@ TableColoredBackgroundCellOptions: {
|
|||||||
type: TableCellDisplayMode & "color-background"
|
type: TableCellDisplayMode & "color-background"
|
||||||
mode?: TableCellBackgroundDisplayMode
|
mode?: TableCellBackgroundDisplayMode
|
||||||
applyToRow?: bool
|
applyToRow?: bool
|
||||||
|
wrapText?: bool
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
// Height of a table cell
|
// 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
|
// Table cell options. Each cell has a display mode
|
||||||
// and other potential options for that display.
|
// and other potential options for that display.
|
||||||
|
@ -110,6 +110,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.24.7",
|
"@babel/core": "7.24.7",
|
||||||
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@grafana/tsconfig": "^1.3.0-rc1",
|
"@grafana/tsconfig": "^1.3.0-rc1",
|
||||||
"@rollup/plugin-node-resolve": "15.2.3",
|
"@rollup/plugin-node-resolve": "15.2.3",
|
||||||
"@storybook/addon-a11y": "^8.1.6",
|
"@storybook/addon-a11y": "^8.1.6",
|
||||||
|
@ -15,7 +15,7 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ
|
|||||||
import { getCellColors, getCellOptions } from './utils';
|
import { getCellColors, getCellOptions } from './utils';
|
||||||
|
|
||||||
export const DefaultCell = (props: TableCellProps) => {
|
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 inspectEnabled = Boolean(field.config.custom?.inspect);
|
||||||
const displayValue = field.display!(cell.value);
|
const displayValue = field.display!(cell.value);
|
||||||
@ -59,6 +59,7 @@ export const DefaultCell = (props: TableCellProps) => {
|
|||||||
inspectEnabled,
|
inspectEnabled,
|
||||||
isStringValue,
|
isStringValue,
|
||||||
textShouldWrap,
|
textShouldWrap,
|
||||||
|
textWrapped,
|
||||||
rowStyled
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
{...cellProps}
|
{...cellProps}
|
||||||
@ -112,6 +121,7 @@ function getCellStyle(
|
|||||||
disableOverflowOnHover = false,
|
disableOverflowOnHover = false,
|
||||||
isStringValue = false,
|
isStringValue = false,
|
||||||
shouldWrapText = false,
|
shouldWrapText = false,
|
||||||
|
textWrapped = false,
|
||||||
rowStyled = false
|
rowStyled = false
|
||||||
) {
|
) {
|
||||||
// Setup color variables
|
// Setup color variables
|
||||||
@ -134,6 +144,7 @@ function getCellStyle(
|
|||||||
!disableOverflowOnHover,
|
!disableOverflowOnHover,
|
||||||
isStringValue,
|
isStringValue,
|
||||||
shouldWrapText,
|
shouldWrapText,
|
||||||
|
textWrapped,
|
||||||
rowStyled
|
rowStyled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
|
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 { VariableSizeList } from 'react-window';
|
||||||
import { Subscription, debounceTime } from 'rxjs';
|
import { Subscription, debounceTime } from 'rxjs';
|
||||||
|
|
||||||
@ -23,7 +23,12 @@ import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow';
|
|||||||
import { TableCell } from './TableCell';
|
import { TableCell } from './TableCell';
|
||||||
import { TableStyles } from './styles';
|
import { TableStyles } from './styles';
|
||||||
import { CellColors, TableFieldOptions, TableFilterActionCallback } from './types';
|
import { CellColors, TableFieldOptions, TableFilterActionCallback } from './types';
|
||||||
import { calculateAroundPointThreshold, getCellColors, isPointTimeValAroundTableTimeVal } from './utils';
|
import {
|
||||||
|
calculateAroundPointThreshold,
|
||||||
|
getCellColors,
|
||||||
|
isPointTimeValAroundTableTimeVal,
|
||||||
|
guessTextBoundingBox,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
interface RowsListProps {
|
interface RowsListProps {
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
@ -45,6 +50,8 @@ interface RowsListProps {
|
|||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
footerPaginationEnabled: boolean;
|
footerPaginationEnabled: boolean;
|
||||||
initialRowIndex?: number;
|
initialRowIndex?: number;
|
||||||
|
headerGroups: HeaderGroup[];
|
||||||
|
longestField?: Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RowsList = (props: RowsListProps) => {
|
export const RowsList = (props: RowsListProps) => {
|
||||||
@ -68,6 +75,8 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
listRef,
|
listRef,
|
||||||
enableSharedCrosshair = false,
|
enableSharedCrosshair = false,
|
||||||
initialRowIndex = undefined,
|
initialRowIndex = undefined,
|
||||||
|
headerGroups,
|
||||||
|
longestField,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
|
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
|
||||||
@ -75,6 +84,23 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const panelContext = usePanelContext();
|
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 threshold = useMemo(() => {
|
||||||
const timeField = data.fields.find((f) => f.type === FieldType.time);
|
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 rowBg: Function | undefined = undefined;
|
||||||
|
let textWrapField: Field | undefined = undefined;
|
||||||
for (const field of data.fields) {
|
for (const field of data.fields) {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
const fieldOptions = field.config.custom as TableFieldOptions;
|
const fieldOptions = field.config.custom as TableFieldOptions;
|
||||||
|
const cellOptionsExist = fieldOptions !== undefined && fieldOptions.cellOptions !== undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
fieldOptions !== undefined &&
|
cellOptionsExist &&
|
||||||
fieldOptions.cellOptions !== undefined &&
|
|
||||||
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground &&
|
fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground &&
|
||||||
fieldOptions.cellOptions.applyToRow
|
fieldOptions.cellOptions.applyToRow
|
||||||
) {
|
) {
|
||||||
@ -218,6 +245,18 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
return colors;
|
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(
|
const RenderRow = useCallback(
|
||||||
@ -236,12 +275,27 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color rows if enabled
|
||||||
if (rowBg) {
|
if (rowBg) {
|
||||||
const { bgColor, textColor } = rowBg(row.index);
|
const { bgColor, textColor } = rowBg(row.index);
|
||||||
style.background = bgColor;
|
style.background = bgColor;
|
||||||
style.color = textColor;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
{...row.getRowProps({ style, ...additionalProps })}
|
{...row.getRowProps({ style, ...additionalProps })}
|
||||||
@ -272,6 +326,8 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
frame={data}
|
frame={data}
|
||||||
rowStyled={rowBg !== undefined}
|
rowStyled={rowBg !== undefined}
|
||||||
|
textWrapped={textWrapField !== undefined}
|
||||||
|
height={Number(style.height)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -289,20 +345,38 @@ export const RowsList = (props: RowsListProps) => {
|
|||||||
rows,
|
rows,
|
||||||
tableState.expanded,
|
tableState.expanded,
|
||||||
tableStyles,
|
tableStyles,
|
||||||
|
textWrapField,
|
||||||
theme.components.table.rowHoverBackground,
|
theme.components.table.rowHoverBackground,
|
||||||
|
theme.typography.fontSize,
|
||||||
|
theme.typography.body.lineHeight,
|
||||||
timeRange,
|
timeRange,
|
||||||
width,
|
width,
|
||||||
rowBg,
|
rowBg,
|
||||||
|
headerGroups,
|
||||||
|
osContext,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getItemSize = (index: number): number => {
|
const getItemSize = (index: number): number => {
|
||||||
const indexForPagination = rowIndexForPagination(index);
|
const indexForPagination = rowIndexForPagination(index);
|
||||||
const row = rows[indexForPagination];
|
const row = rows[indexForPagination];
|
||||||
|
|
||||||
if (tableState.expanded[row.id] && nestedDataField) {
|
if (tableState.expanded[row.id] && nestedDataField) {
|
||||||
return getExpandedRowHeight(nestedDataField, row.index, tableStyles);
|
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;
|
return tableStyles.rowHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,7 +25,14 @@ import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks
|
|||||||
import { getInitialState, useTableStateReducer } from './reducer';
|
import { getInitialState, useTableStateReducer } from './reducer';
|
||||||
import { useTableStyles } from './styles';
|
import { useTableStyles } from './styles';
|
||||||
import { FooterItem, GrafanaTableState, Props } from './types';
|
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 COLUMN_MIN_WIDTH = 150;
|
||||||
const FOOTER_ROW_HEIGHT = 36;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
{...getTableProps()}
|
{...getTableProps()}
|
||||||
@ -304,6 +314,7 @@ export const Table = memo((props: Props) => {
|
|||||||
{itemCount > 0 ? (
|
{itemCount > 0 ? (
|
||||||
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
|
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
|
||||||
<RowsList
|
<RowsList
|
||||||
|
headerGroups={headerGroups}
|
||||||
data={data}
|
data={data}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
width={width}
|
width={width}
|
||||||
@ -323,6 +334,7 @@ export const Table = memo((props: Props) => {
|
|||||||
footerPaginationEnabled={Boolean(enablePagination)}
|
footerPaginationEnabled={Boolean(enablePagination)}
|
||||||
enableSharedCrosshair={enableSharedCrosshair}
|
enableSharedCrosshair={enableSharedCrosshair}
|
||||||
initialRowIndex={initialRowIndex}
|
initialRowIndex={initialRowIndex}
|
||||||
|
longestField={longestField}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -16,9 +16,21 @@ export interface Props {
|
|||||||
userProps?: object;
|
userProps?: object;
|
||||||
frame: DataFrame;
|
frame: DataFrame;
|
||||||
rowStyled?: boolean;
|
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 cellProps = cell.getCellProps();
|
||||||
const field = (cell.column as unknown as GrafanaTableColumn).field;
|
const field = (cell.column as unknown as GrafanaTableColumn).field;
|
||||||
|
|
||||||
@ -45,6 +57,8 @@ export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, use
|
|||||||
userProps,
|
userProps,
|
||||||
frame,
|
frame,
|
||||||
rowStyled,
|
rowStyled,
|
||||||
|
textWrapped,
|
||||||
|
height,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -18,6 +18,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
|||||||
overflowOnHover?: boolean,
|
overflowOnHover?: boolean,
|
||||||
asCellText?: boolean,
|
asCellText?: boolean,
|
||||||
textShouldWrap?: boolean,
|
textShouldWrap?: boolean,
|
||||||
|
textWrapped?: boolean,
|
||||||
rowStyled?: boolean
|
rowStyled?: boolean
|
||||||
) => {
|
) => {
|
||||||
return css({
|
return css({
|
||||||
@ -26,6 +27,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
// Cell height need to account for row border
|
// Cell height need to account for row border
|
||||||
height: `${rowHeight - 1}px`,
|
height: `${rowHeight - 1}px`,
|
||||||
|
wordBreak: textWrapped ? 'break-all' : 'inherit',
|
||||||
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
||||||
@ -50,9 +52,9 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
|||||||
},
|
},
|
||||||
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
overflow: overflowOnHover ? 'visible' : undefined,
|
overflow: overflowOnHover && !textWrapped ? 'visible' : undefined,
|
||||||
width: textShouldWrap || !overflowOnHover ? 'auto' : 'auto !important',
|
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`,
|
minHeight: `${rowHeight - 1}px`,
|
||||||
wordBreak: textShouldWrap ? 'break-word' : undefined,
|
wordBreak: textShouldWrap ? 'break-word' : undefined,
|
||||||
whiteSpace: textShouldWrap && overflowOnHover ? 'normal' : 'nowrap',
|
whiteSpace: textShouldWrap && overflowOnHover ? 'normal' : 'nowrap',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
import { Row } from 'react-table';
|
import { Row } from 'react-table';
|
||||||
|
|
||||||
import { Field, FieldType, MutableDataFrame, SelectableValue } from '@grafana/data';
|
import { Field, FieldType, MutableDataFrame, SelectableValue } from '@grafana/data';
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
sortNumber,
|
sortNumber,
|
||||||
sortOptions,
|
sortOptions,
|
||||||
valuesToOptions,
|
valuesToOptions,
|
||||||
|
guessLongestField,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function getData() {
|
function getData() {
|
||||||
@ -43,6 +45,45 @@ function getData() {
|
|||||||
return data;
|
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('Table utils', () => {
|
||||||
describe('getColumns', () => {
|
describe('getColumns', () => {
|
||||||
it('Should build columns from DataFrame', () => {
|
it('Should build columns from DataFrame', () => {
|
||||||
@ -502,4 +543,54 @@ describe('Table utils', () => {
|
|||||||
expect(diff).toBeLessThanOrEqual(20);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Property } from 'csstype';
|
import { Property } from 'csstype';
|
||||||
import { clone } from 'lodash';
|
import { clone, sampleSize } from 'lodash';
|
||||||
import memoize from 'micro-memoize';
|
import memoize from 'micro-memoize';
|
||||||
import { Row } from 'react-table';
|
import { Row, HeaderGroup } from 'react-table';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -633,3 +633,130 @@ export function getCellColors(
|
|||||||
|
|
||||||
return { textColor, bgColor, bgHoverColor };
|
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;
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
|||||||
import { TableCellOptions } from '@grafana/schema';
|
import { TableCellOptions } from '@grafana/schema';
|
||||||
import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui';
|
import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { AutoCellOptionsEditor } from './cells/AutoCellOptionsEditor';
|
||||||
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
|
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
|
||||||
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
|
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
|
||||||
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
|
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
|
||||||
@ -60,6 +61,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
|||||||
<Field>
|
<Field>
|
||||||
<Select options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
|
<Select options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
|
||||||
</Field>
|
</Field>
|
||||||
|
{(cellType === TableCellDisplayMode.Auto || cellType === TableCellDisplayMode.ColorText) && (
|
||||||
|
<AutoCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||||
|
)}
|
||||||
{cellType === TableCellDisplayMode.Gauge && (
|
{cellType === TableCellDisplayMode.Gauge && (
|
||||||
<BarGaugeCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
<BarGaugeCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema';
|
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';
|
import { TableCellEditorProps } from '../TableCellOptionEditor';
|
||||||
|
|
||||||
@ -27,6 +27,19 @@ export const ColorBackgroundCellOptionsEditor = ({
|
|||||||
onChange(cellOptions);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label="Background display mode">
|
<Field label="Background display mode">
|
||||||
@ -42,6 +55,9 @@ export const ColorBackgroundCellOptionsEditor = ({
|
|||||||
>
|
>
|
||||||
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
|
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label={label}>
|
||||||
|
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
|
||||||
|
</Field>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2418,6 +2418,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@fal-works/esbuild-plugin-global-externals@npm:^2.1.2":
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
resolution: "@fal-works/esbuild-plugin-global-externals@npm: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"
|
"@babel/core": "npm:7.24.7"
|
||||||
"@emotion/css": "npm:11.11.2"
|
"@emotion/css": "npm:11.11.2"
|
||||||
"@emotion/react": "npm:11.11.4"
|
"@emotion/react": "npm:11.11.4"
|
||||||
|
"@faker-js/faker": "npm:^8.4.1"
|
||||||
"@floating-ui/react": "npm:0.26.16"
|
"@floating-ui/react": "npm:0.26.16"
|
||||||
"@grafana/data": "npm:11.0.0"
|
"@grafana/data": "npm:11.0.0"
|
||||||
"@grafana/e2e-selectors": "npm:11.0.0"
|
"@grafana/e2e-selectors": "npm:11.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user