From 2a6a1fb3b3336394d138532f2a692d02f844ac29 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Tue, 19 Mar 2024 11:23:12 -0500 Subject: [PATCH] Table Panel: Add ability to set background colors for entire rows (#83939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Table: hacky poc for colored rows * update * Update options editor * Refactor out styling * Make sure option applies * Start unifying coloring logic * Support various options * Cleanups * Fix up hover behavior * Update label * Fix bug with sorting and make cleanups * Ensure text color also applies to whole row * New treatment for cell inspect * Update cell inspect spacing * codeincarnate/table-row-background-selected-riffs/ run linter * Fix hover colors * Fix coloring * The other part of color application * Another color fix * codeincarnate/table-row-background-selected-riffs/ lint * Update cell value inspect * Prettier * Update cell value inspect coloring * Fix text coloring * Update inspect style * Linting and betterer * Fix tests * i18n --------- Co-authored-by: Torkel Ödegaard Co-authored-by: jev forsberg --- .../grafana-schema/src/common/common.gen.ts | 1 + packages/grafana-schema/src/common/table.cue | 1 + .../src/components/Table/CellActions.tsx | 2 +- .../src/components/Table/DefaultCell.tsx | 48 ++++++++----------- .../src/components/Table/RowsList.tsx | 35 ++++++++++++-- .../src/components/Table/TableCell.tsx | 4 +- .../grafana-ui/src/components/Table/styles.ts | 17 ++++--- .../grafana-ui/src/components/Table/types.ts | 7 +++ .../grafana-ui/src/components/Table/utils.ts | 48 +++++++++++++++++++ .../ColorBackgroundCellOptionsEditor.tsx | 30 ++++++++---- public/locales/en-US/grafana.json | 1 - public/locales/pseudo-LOCALE/grafana.json | 1 - 12 files changed, 145 insertions(+), 50 deletions(-) diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index e95004ac43a..9dcdcdcb5d2 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -792,6 +792,7 @@ export interface TableSparklineCellOptions extends GraphFieldConfig { * Colored background cell options */ export interface TableColoredBackgroundCellOptions { + applyToRow?: boolean; mode?: TableCellBackgroundDisplayMode; type: TableCellDisplayMode.ColorBackground; } diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index c6aa05df53e..5c6dce2a5de 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -71,6 +71,7 @@ TableSparklineCellOptions: { TableColoredBackgroundCellOptions: { type: TableCellDisplayMode & "color-background" mode?: TableCellBackgroundDisplayMode + applyToRow?: bool } @cuetsy(kind="interface") // Height of a table cell diff --git a/packages/grafana-ui/src/components/Table/CellActions.tsx b/packages/grafana-ui/src/components/Table/CellActions.tsx index bb8f4be9010..b8a2dc62d63 100644 --- a/packages/grafana-ui/src/components/Table/CellActions.tsx +++ b/packages/grafana-ui/src/components/Table/CellActions.tsx @@ -48,7 +48,7 @@ export function CellActions({ field, cell, previewMode, showFilters, onCellFilte return ( <> -
+
{inspectEnabled && ( { - const { field, cell, tableStyles, row, cellProps, frame } = props; + const { field, cell, tableStyles, row, cellProps, frame, rowStyled } = props; const inspectEnabled = Boolean(field.config.custom?.inspect); const displayValue = field.display!(cell.value); @@ -53,7 +52,15 @@ export const DefaultCell = (props: TableCellProps) => { // Text should wrap when the content length is less than or equal to the length of an OG tweet and it contains whitespace const textShouldWrap = displayValue.text.length <= OG_TWEET_LENGTH && /\s/.test(displayValue.text); - const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled, isStringValue, textShouldWrap); + const cellStyle = getCellStyle( + tableStyles, + cellOptions, + displayValue, + inspectEnabled, + isStringValue, + textShouldWrap, + rowStyled + ); if (isStringValue) { let justifyContent = cellProps.style?.justifyContent; @@ -104,31 +111,17 @@ function getCellStyle( displayValue: DisplayValue, disableOverflowOnHover = false, isStringValue = false, - shouldWrapText = false + shouldWrapText = false, + rowStyled = false ) { - // How much to darken elements depends upon if we're in dark mode - const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; - // Setup color variables let textColor: string | undefined = undefined; let bgColor: string | undefined = undefined; - if (cellOptions.type === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (cellOptions.type === TableCellDisplayMode.ColorBackground) { - const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient; - - if (mode === TableCellBackgroundDisplayMode.Basic) { - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = tinycolor(displayValue.color).toRgbString(); - } else if (mode === TableCellBackgroundDisplayMode.Gradient) { - const bgColor2 = tinycolor(displayValue.color) - .darken(10 * darkeningFactor) - .spin(5); - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; - } - } + // Get colors + const colors = getCellColors(tableStyles, cellOptions, displayValue); + textColor = colors.textColor; + bgColor = colors.bgColor; // If we have definied colors return those styles // Otherwise we return default styles @@ -137,7 +130,8 @@ function getCellStyle( bgColor, !disableOverflowOnHover, isStringValue, - shouldWrapText + shouldWrapText, + rowStyled ); } diff --git a/packages/grafana-ui/src/components/Table/RowsList.tsx b/packages/grafana-ui/src/components/Table/RowsList.tsx index 6b16f4430a2..5194d9d693d 100644 --- a/packages/grafana-ui/src/components/Table/RowsList.tsx +++ b/packages/grafana-ui/src/components/Table/RowsList.tsx @@ -13,7 +13,7 @@ import { TimeRange, hasTimeField, } from '@grafana/data'; -import { TableCellHeight } from '@grafana/schema'; +import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema'; import { useTheme2 } from '../../themes'; import CustomScrollbar from '../CustomScrollbar/CustomScrollbar'; @@ -22,8 +22,8 @@ import { usePanelContext } from '../PanelChrome'; import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow'; import { TableCell } from './TableCell'; import { TableStyles } from './styles'; -import { TableFilterActionCallback } from './types'; -import { calculateAroundPointThreshold, isPointTimeValAroundTableTimeVal } from './utils'; +import { CellColors, TableFieldOptions, TableFilterActionCallback } from './types'; +import { calculateAroundPointThreshold, getCellColors, isPointTimeValAroundTableTimeVal } from './utils'; interface RowsListProps { data: DataFrame; @@ -201,12 +201,30 @@ export const RowsList = (props: RowsListProps) => { [tableState.pageIndex, tableState.pageSize] ); + let rowBg: Function | undefined = undefined; + for (const field of data.fields) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const fieldOptions = field.config.custom as TableFieldOptions; + + if ( + fieldOptions !== undefined && + fieldOptions.cellOptions !== undefined && + fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground && + fieldOptions.cellOptions.applyToRow + ) { + rowBg = (rowIndex: number): CellColors => { + const display = field.display!(field.values.get(rowIndex)); + const colors = getCellColors(tableStyles, fieldOptions.cellOptions, display); + return colors; + }; + } + } + const RenderRow = useCallback( ({ index, style, rowHighlightIndex }: { index: number; style: CSSProperties; rowHighlightIndex?: number }) => { const indexForPagination = rowIndexForPagination(index); const row = rows[indexForPagination]; let additionalProps: React.HTMLAttributes = {}; - prepareRow(row); const expandedRowStyle = tableState.expanded[row.id] ? css({ '&:hover': { background: 'inherit' } }) : {}; @@ -217,6 +235,13 @@ export const RowsList = (props: RowsListProps) => { 'aria-selected': 'true', }; } + + if (rowBg) { + const { bgColor, textColor } = rowBg(row.index); + style.background = bgColor; + style.color = textColor; + } + return (
{ columnCount={row.cells.length} timeRange={timeRange} frame={data} + rowStyled={rowBg !== undefined} /> ))}
@@ -264,6 +290,7 @@ export const RowsList = (props: RowsListProps) => { theme.components.table.rowHoverBackground, timeRange, width, + rowBg, ] ); diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index 8b0452a8550..4419560aa93 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -15,9 +15,10 @@ export interface Props { timeRange?: TimeRange; userProps?: object; frame: DataFrame; + rowStyled?: boolean; } -export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, userProps, frame }: Props) => { +export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, userProps, frame, rowStyled }: Props) => { const cellProps = cell.getCellProps(); const field = (cell.column as unknown as GrafanaTableColumn).field; @@ -43,6 +44,7 @@ export const TableCell = ({ cell, tableStyles, onCellFilterAdded, timeRange, use timeRange, userProps, frame, + rowStyled, })} ); diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/styles.ts index 787e2ee5b51..0385f11d9d3 100644 --- a/packages/grafana-ui/src/components/Table/styles.ts +++ b/packages/grafana-ui/src/components/Table/styles.ts @@ -16,7 +16,8 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell background?: string, overflowOnHover?: boolean, asCellText?: boolean, - textShouldWrap?: boolean + textShouldWrap?: boolean, + rowStyled?: boolean ) => { return css({ label: overflowOnHover ? 'cellContainerOverflow' : 'cellContainerNoOverflow', @@ -39,8 +40,8 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell alignItems: 'center', borderRight: `1px solid ${borderColor}`, - color: color ?? undefined, - background: background ?? undefined, + color: rowStyled ? 'inherit' : color ?? undefined, + background: rowStyled ? undefined : background ?? undefined, backgroundClip: 'padding-box', '&:last-child:not(:only-child)': { @@ -55,12 +56,14 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell wordBreak: textShouldWrap ? 'break-word' : undefined, whiteSpace: textShouldWrap && overflowOnHover ? 'normal' : 'nowrap', boxShadow: overflowOnHover ? `0 0 2px ${theme.colors.primary.main}` : undefined, - background: overflowOnHover ? background ?? theme.components.table.rowHoverBackground : undefined, + background: rowStyled ? 'inherit' : background ?? undefined, zIndex: 1, '.cellActions': { + color: '#FFF', visibility: 'visible', opacity: 1, width: 'auto', + background: 'rgba(0, 0, 0, 0.6)', }, }, @@ -71,7 +74,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell '.cellActions': { display: 'flex', position: overflowOnHover ? undefined : 'absolute', - top: overflowOnHover ? undefined : 0, + top: overflowOnHover ? undefined : '1px', right: overflowOnHover ? undefined : 0, margin: overflowOnHover ? theme.spacing(0, -0.5, 0, 0.5) : 'auto', visibility: 'hidden', @@ -79,8 +82,8 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell width: 0, alignItems: 'center', height: '100%', - padding: theme.spacing(1, 0.5, 1, 0.5), - background: background ? 'none' : theme.colors.emphasize(theme.colors.background.primary, 0.03), + padding: theme.spacing(1, 0.5, 1, 1), + background: background ? 'none' : 'rgba(0, 0, 0, 0.5)', svg: { color, diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 563a0d9ee48..160edbc2742 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -140,3 +140,10 @@ export type TableFieldOptions = Omit & cellOptions: TableCellOptions; headerComponent?: React.ComponentType; }; + +// Cell background and text colors +// Can also be used for table rows +export interface CellColors { + textColor?: string; + bgColor?: string; +} diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 34a8efe5aeb..1274dc053d2 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -2,6 +2,7 @@ import { Property } from 'csstype'; import { clone } from 'lodash'; import memoize from 'micro-memoize'; import { Row } from 'react-table'; +import tinycolor from 'tinycolor2'; import { DataFrame, @@ -27,6 +28,8 @@ import { TableCellDisplayMode, } from '@grafana/schema'; +import { getTextColorForAlphaBackground } from '../../utils'; + import { BarGaugeCell } from './BarGaugeCell'; import { DataLinksCell } from './DataLinksCell'; import { DefaultCell } from './DefaultCell'; @@ -36,6 +39,7 @@ import { ImageCell } from './ImageCell'; import { JSONViewCell } from './JSONViewCell'; import { RowExpander } from './RowExpander'; import { SparklineCell } from './SparklineCell'; +import { TableStyles } from './styles'; import { CellComponent, TableCellOptions, @@ -43,6 +47,7 @@ import { FooterItem, GrafanaTableColumn, TableFooterCalc, + CellColors, } from './types'; export const EXPANDER_WIDTH = 50; @@ -581,3 +586,46 @@ export function calculateAroundPointThreshold(timeField: Field): number { return (max - min) / timeField.values.length; } + +/** + * Retrieve colors for a table cell (or table row). + * + * @param tableStyles + * Styles for the table + * @param cellOptions + * Table cell configuration options + * @param displayValue + * The value that will be displayed + * @returns CellColors + */ +export function getCellColors( + tableStyles: TableStyles, + cellOptions: TableCellOptions, + displayValue: DisplayValue +): CellColors { + // How much to darken elements depends upon if we're in dark mode + const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; + + // Setup color variables + let textColor: string | undefined = undefined; + let bgColor: string | undefined = undefined; + + if (cellOptions.type === TableCellDisplayMode.ColorText) { + textColor = displayValue.color; + } else if (cellOptions.type === TableCellDisplayMode.ColorBackground) { + const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient; + + if (mode === TableCellBackgroundDisplayMode.Basic) { + textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); + bgColor = tinycolor(displayValue.color).setAlpha(1).toRgbString(); + } else if (mode === TableCellBackgroundDisplayMode.Gradient) { + const bgColor2 = tinycolor(displayValue.color) + .darken(10 * darkeningFactor) + .spin(5); + textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); + bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; + } + } + + return { textColor, bgColor }; +} diff --git a/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx b/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx index 26bb955edf6..76cdf277e5d 100644 --- a/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx +++ b/public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema'; -import { Field, RadioButtonGroup } from '@grafana/ui'; +import { Field, RadioButtonGroup, Switch } from '@grafana/ui'; import { TableCellEditorProps } from '../TableCellOptionEditor'; @@ -21,13 +21,27 @@ export const ColorBackgroundCellOptionsEditor = ({ onChange(cellOptions); }; + // Handle row coloring changes + const onColorRowChange = () => { + cellOptions.applyToRow = !cellOptions.applyToRow; + onChange(cellOptions); + }; + return ( - - - + <> + + + + + + + ); }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 75b56554755..e3f83ca3384 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -369,7 +369,6 @@ "title-label": "Title" }, "json-editor": { - "apply-button": "Apply changes", "save-button": "Save changes", "subtitle": "The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, panel settings, layout, queries, and so on.", "title": "JSON Model" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 548641ea6fe..688aa9d31b9 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -369,7 +369,6 @@ "title-label": "Ŧįŧľę" }, "json-editor": { - "apply-button": "Åppľy čĥäʼnģęş", "save-button": "Ŝävę čĥäʼnģęş", "subtitle": "Ŧĥę ĴŜØŃ mőđęľ þęľőŵ įş ŧĥę đäŧä şŧřūčŧūřę ŧĥäŧ đęƒįʼnęş ŧĥę đäşĥþőäřđ. Ŧĥįş įʼnčľūđęş đäşĥþőäřđ şęŧŧįʼnģş, päʼnęľ şęŧŧįʼnģş, ľäyőūŧ, qūęřįęş, äʼnđ şő őʼn.", "title": "ĴŜØŃ Mőđęľ"