Table Panel: Add ability to set background colors for entire rows (#83939)
* 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 <torkel@grafana.com> Co-authored-by: jev forsberg <jev.forsberg@grafana.com>
This commit is contained in:
co-authored by
Torkel Ödegaard
jev forsberg
parent
5b1b280798
commit
2a6a1fb3b3
@@ -792,6 +792,7 @@ export interface TableSparklineCellOptions extends GraphFieldConfig {
|
||||
* Colored background cell options
|
||||
*/
|
||||
export interface TableColoredBackgroundCellOptions {
|
||||
applyToRow?: boolean;
|
||||
mode?: TableCellBackgroundDisplayMode;
|
||||
type: TableCellDisplayMode.ColorBackground;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ TableSparklineCellOptions: {
|
||||
TableColoredBackgroundCellOptions: {
|
||||
type: TableCellDisplayMode & "color-background"
|
||||
mode?: TableCellBackgroundDisplayMode
|
||||
applyToRow?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Height of a table cell
|
||||
|
||||
@@ -48,7 +48,7 @@ export function CellActions({ field, cell, previewMode, showFilters, onCellFilte
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`cellActions ${isRightAligned ? 'cellActionsLeft' : ''}`}>
|
||||
<div className={`cellActions${isRightAligned ? ' cellActionsLeft' : ''}`}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
{inspectEnabled && (
|
||||
<IconButton
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { DisplayValue, formattedValueToString } from '@grafana/data';
|
||||
import { TableCellBackgroundDisplayMode, TableCellDisplayMode } from '@grafana/schema';
|
||||
import { TableCellDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { getCellLinks, getTextColorForAlphaBackground } from '../../utils';
|
||||
import { getCellLinks } from '../../utils';
|
||||
import { clearLinkButtonStyles } from '../Button';
|
||||
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
|
||||
|
||||
import { CellActions } from './CellActions';
|
||||
import { TableStyles } from './styles';
|
||||
import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './types';
|
||||
import { getCellOptions } from './utils';
|
||||
import { getCellColors, getCellOptions } from './utils';
|
||||
|
||||
export const DefaultCell = (props: TableCellProps) => {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<HTMLDivElement> = {};
|
||||
|
||||
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 (
|
||||
<div
|
||||
{...row.getRowProps({ style, ...additionalProps })}
|
||||
@@ -244,6 +269,7 @@ export const RowsList = (props: RowsListProps) => {
|
||||
columnCount={row.cells.length}
|
||||
timeRange={timeRange}
|
||||
frame={data}
|
||||
rowStyled={rowBg !== undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -264,6 +290,7 @@ export const RowsList = (props: RowsListProps) => {
|
||||
theme.components.table.rowHoverBackground,
|
||||
timeRange,
|
||||
width,
|
||||
rowBg,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -140,3 +140,10 @@ export type TableFieldOptions = Omit<schema.TableFieldOptions, 'cellOptions'> &
|
||||
cellOptions: TableCellOptions;
|
||||
headerComponent?: React.ComponentType<CustomHeaderRendererProps>;
|
||||
};
|
||||
|
||||
// Cell background and text colors
|
||||
// Can also be used for table rows
|
||||
export interface CellColors {
|
||||
textColor?: string;
|
||||
bgColor?: string;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Field label="Background display mode">
|
||||
<RadioButtonGroup
|
||||
value={cellOptions?.mode ?? TableCellBackgroundDisplayMode.Gradient}
|
||||
onChange={onCellOptionsChange}
|
||||
options={colorBackgroundOpts}
|
||||
/>
|
||||
</Field>
|
||||
<>
|
||||
<Field label="Background display mode">
|
||||
<RadioButtonGroup
|
||||
value={cellOptions?.mode ?? TableCellBackgroundDisplayMode.Gradient}
|
||||
onChange={onCellOptionsChange}
|
||||
options={colorBackgroundOpts}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Apply to entire row"
|
||||
description="If selected the entire row will be colored as this cell would be."
|
||||
>
|
||||
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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őđęľ"
|
||||
|
||||
Reference in New Issue
Block a user