Table: Color text, color background, and apply to row can co-mingle (#109939)
* Table: Color text, color background, and apply to row can co-mingle * fix test * lean on existing memoization and utils more * just make that method a prop of TableCellRenderer * add prop to tooltip stuff as well * fix another test * update TableNG with apply to row mixed color cell table * simplify color overrides table * special case: apply to row transparent bg * add unit test * delete erroneous import * update for readability
This commit is contained in:
@@ -434,9 +434,6 @@
|
||||
"id": 1,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"frozenColumns": {
|
||||
"left": 1
|
||||
},
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"enablePagination": false,
|
||||
@@ -445,6 +442,9 @@
|
||||
"show": true
|
||||
},
|
||||
"frameIndex": 0,
|
||||
"frozenColumns": {
|
||||
"left": 1
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": [
|
||||
{
|
||||
@@ -797,6 +797,10 @@
|
||||
"value": {
|
||||
"type": "data-links"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 140
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -804,7 +808,7 @@
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 12
|
||||
},
|
||||
@@ -834,6 +838,166 @@
|
||||
"title": "Colors and Links",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "transparent",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"inspect": false,
|
||||
"width": 100,
|
||||
"wrapHeaderText": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "highlight"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"applyToRow": true,
|
||||
"mode": "basic",
|
||||
"type": "color-background"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mappings",
|
||||
"value": [
|
||||
{
|
||||
"options": {
|
||||
"1": {
|
||||
"color": "#fff899",
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": "/^color-*/"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "mappings",
|
||||
"value": [
|
||||
{
|
||||
"options": {
|
||||
"1": {
|
||||
"color": "green",
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "color-bg"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "color-background"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "color-text"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "color-text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "text",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "use-case"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.width"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 12
|
||||
},
|
||||
"id": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "use-case,normal,color-text,color-bg,highlight\n\"color bg and apply to row\",1,0,1,1\n\"color text and apply to row\",1,1,0,1\n\"color text + color bg + apply to row\",1,1,1,1\n\"only apply to row\",1,0,0,1\n\"only color bg\",1,0,1,0\n\"only color text\",1,1,0,0\n\"color text + color bg\",1,1,1,0\n\"no colorization\",1,0,0,0",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Apply to Row - mixed color cell types",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource"
|
||||
@@ -1697,5 +1861,5 @@
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Table - Kitchen Sink",
|
||||
"uid": "dcb9f5e9-8066-4397-889e-864b99555dbb",
|
||||
"version": 9
|
||||
"version": 32
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { render, RenderResult } from '@testing-library/react';
|
||||
|
||||
import { Field, FieldType, MappingType, createTheme } from '@grafana/data';
|
||||
|
||||
import { getTextColorForBackground } from '../../../../utils/colors';
|
||||
|
||||
import { PillCell } from './PillCell';
|
||||
|
||||
describe('PillCell', () => {
|
||||
@@ -26,49 +28,94 @@ describe('PillCell', () => {
|
||||
describe('Color by hash (classic palette)', () => {
|
||||
it('single value', () => {
|
||||
expectHTML(
|
||||
render(<PillCell field={fieldWithValues(['value1'])} rowIdx={0} theme={theme} />),
|
||||
`<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>`
|
||||
render(
|
||||
<PillCell
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
field={fieldWithValues(['value1'])}
|
||||
rowIdx={0}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
`<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>`
|
||||
);
|
||||
});
|
||||
|
||||
it('empty string', () => {
|
||||
expectHTML(render(<PillCell field={fieldWithValues([''])} rowIdx={0} theme={theme} />), '');
|
||||
expectHTML(
|
||||
render(
|
||||
<PillCell
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
field={fieldWithValues([''])}
|
||||
rowIdx={0}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it('null', () => {
|
||||
const { container } = render(<PillCell field={fieldWithValues([])} rowIdx={0} theme={theme} />);
|
||||
const { container } = render(
|
||||
<PillCell
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
field={fieldWithValues([])}
|
||||
rowIdx={0}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('CSV values', () => {
|
||||
expectHTML(
|
||||
render(<PillCell field={fieldWithValues(['value1,value2,value3'])} rowIdx={0} theme={theme} />),
|
||||
render(
|
||||
<PillCell
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
field={fieldWithValues(['value1,value2,value3'])}
|
||||
rowIdx={0}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
`
|
||||
<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
|
||||
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
|
||||
<span style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
|
||||
<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>
|
||||
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">value2</span>
|
||||
<span style="background-color: rgb(81, 149, 206); color: rgb(247, 248, 250);">value3</span>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('JSON array values', () => {
|
||||
expectHTML(
|
||||
render(<PillCell field={fieldWithValues(['["value1","value2","value3"]'])} rowIdx={0} theme={theme} />),
|
||||
render(
|
||||
<PillCell
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
field={fieldWithValues(['["value1","value2","value3"]'])}
|
||||
rowIdx={0}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
`
|
||||
<span style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
|
||||
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
|
||||
<span style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
|
||||
<span style="background-color: rgb(63, 43, 91); color: rgb(247, 248, 250);">value1</span>
|
||||
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">value2</span>
|
||||
<span style="background-color: rgb(81, 149, 206); color: rgb(247, 248, 250);">value3</span>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('non-string values', () => {
|
||||
expectHTML(
|
||||
render(<PillCell field={fieldWithValues(['[100,200,300]'])} rowIdx={0} theme={theme} />),
|
||||
render(
|
||||
<PillCell
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
field={fieldWithValues(['[100,200,300]'])}
|
||||
rowIdx={0}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
`
|
||||
<span style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">100</span>
|
||||
<span style="background-color: rgb(222, 218, 247); color: rgb(0, 0, 0);">200</span>
|
||||
<span style="background-color: rgb(249, 217, 249); color: rgb(0, 0, 0);">300</span>
|
||||
<span style="background-color: rgb(252, 226, 222); color: rgb(32, 34, 38);">100</span>
|
||||
<span style="background-color: rgb(222, 218, 247); color: rgb(32, 34, 38);">200</span>
|
||||
<span style="background-color: rgb(249, 217, 249); color: rgb(32, 34, 38);">300</span>
|
||||
`
|
||||
);
|
||||
});
|
||||
@@ -107,12 +154,14 @@ describe('PillCell', () => {
|
||||
} satisfies Field;
|
||||
|
||||
expectHTML(
|
||||
render(<PillCell field={field} rowIdx={0} theme={theme} />),
|
||||
render(
|
||||
<PillCell getTextColorForBackground={getTextColorForBackground} field={field} rowIdx={0} theme={theme} />
|
||||
),
|
||||
`
|
||||
<span style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 0);">success</span>
|
||||
<span style="background-color: rgb(255, 0, 0); color: rgb(0, 0, 0);">error</span>
|
||||
<span style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">warning</span>
|
||||
<span style="background-color: rgb(255, 120, 10); color: rgb(0, 0, 0);">unknown</span>
|
||||
<span style="background-color: rgb(0, 255, 0); color: rgb(247, 248, 250);">success</span>
|
||||
<span style="background-color: rgb(255, 0, 0); color: rgb(247, 248, 250);">error</span>
|
||||
<span style="background-color: rgb(255, 255, 0); color: rgb(32, 34, 38);">warning</span>
|
||||
<span style="background-color: rgb(255, 120, 10); color: rgb(247, 248, 250);">unknown</span>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useMemo } from 'react';
|
||||
import {
|
||||
GrafanaTheme2,
|
||||
classicColors,
|
||||
colorManipulator,
|
||||
Field,
|
||||
getColorByStringHash,
|
||||
FALLBACK_COLOR,
|
||||
@@ -14,12 +13,23 @@ import { FieldColorModeId } from '@grafana/schema';
|
||||
|
||||
import { PillCellProps, TableCellStyles, TableCellValue } from '../types';
|
||||
|
||||
export function PillCell({ rowIdx, field, theme }: PillCellProps) {
|
||||
export function PillCell({ rowIdx, field, theme, getTextColorForBackground }: PillCellProps) {
|
||||
const value = field.values[rowIdx];
|
||||
const pills: Pill[] = useMemo(() => {
|
||||
const pillValues = inferPills(value);
|
||||
return pillValues.length > 0 ? createPills(pillValues, field, theme) : [];
|
||||
}, [value, field, theme]);
|
||||
return pillValues.length > 0
|
||||
? pillValues.map((pill, index) => {
|
||||
const bgColor = getPillColor(pill, field, theme);
|
||||
const textColor = getTextColorForBackground(bgColor);
|
||||
return {
|
||||
value: String(pill),
|
||||
key: `${pill}-${index}`,
|
||||
bgColor,
|
||||
color: textColor,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
}, [value, field, theme, getTextColorForBackground]);
|
||||
|
||||
if (pills.length === 0) {
|
||||
return null;
|
||||
@@ -49,19 +59,6 @@ interface Pill {
|
||||
const SPLIT_RE = /\s*,\s*/;
|
||||
const TRANSPARENT = 'rgba(0,0,0,0)';
|
||||
|
||||
function createPills(pillValues: unknown[], field: Field, theme: GrafanaTheme2): Pill[] {
|
||||
return pillValues.map((pill, index) => {
|
||||
const bgColor = getPillColor(pill, field, theme);
|
||||
const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000';
|
||||
return {
|
||||
value: String(pill),
|
||||
key: `${pill}-${index}`,
|
||||
bgColor,
|
||||
color: textColor,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function inferPills(rawValue: TableCellValue): unknown[] {
|
||||
if (rawValue === '' || rawValue == null) {
|
||||
return [];
|
||||
|
||||
@@ -91,6 +91,7 @@ describe('TableNG Cells renderers', () => {
|
||||
getActions={jest.fn(() => [
|
||||
{ title: 'Action', onClick: jest.fn(() => {}), confirmation: jest.fn(), style: {} },
|
||||
])}
|
||||
getTextColorForBackground={jest.fn(() => '#000000')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -109,6 +110,7 @@ describe('TableNG Cells renderers', () => {
|
||||
height={100}
|
||||
width={100}
|
||||
theme={createTheme()}
|
||||
getTextColorForBackground={jest.fn(() => '#000000')}
|
||||
cellOptions={cellOptions}
|
||||
cellInspect={false}
|
||||
showFilters={false}
|
||||
|
||||
@@ -110,7 +110,12 @@ const CELL_REGISTRY: Record<TableCellOptions['type'], CellRegistryEntry> = {
|
||||
[TableCellDisplayMode.Pill]: {
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<PillCell rowIdx={props.rowIdx} field={props.field} theme={props.theme} />
|
||||
<PillCell
|
||||
rowIdx={props.rowIdx}
|
||||
field={props.field}
|
||||
theme={props.theme}
|
||||
getTextColorForBackground={props.getTextColorForBackground}
|
||||
/>
|
||||
)),
|
||||
getStyles: getPillStyles,
|
||||
testField: (field: Field) => field.type === FieldType.string,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'react-data-grid/lib/styles.css';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
import memoize from 'micro-memoize';
|
||||
import { CSSProperties, Key, ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Cell,
|
||||
@@ -28,6 +29,7 @@ import { Trans } from '@grafana/i18n';
|
||||
import { FieldColorModeId, TableCellTooltipPlacement } from '@grafana/schema';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
|
||||
import { getTextColorForBackground as _getTextColorForBackground } from '../../../utils/colors';
|
||||
import { Pagination } from '../../Pagination/Pagination';
|
||||
import { PanelContext, usePanelContext } from '../../PanelChrome';
|
||||
import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip';
|
||||
@@ -80,7 +82,7 @@ import {
|
||||
frameToRecords,
|
||||
getAlignment,
|
||||
getApplyToRowBgFn,
|
||||
getCellColorInlineStyles,
|
||||
getCellColorInlineStylesFactory,
|
||||
getCellLinks,
|
||||
getCellOptions,
|
||||
getDefaultRowHeight,
|
||||
@@ -143,6 +145,7 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
const rows = useMemo(() => frameToRecords(data), [data]);
|
||||
const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]);
|
||||
const getTextColorForBackground = useMemo(() => memoize(_getTextColorForBackground, { maxSize: 1000 }), []);
|
||||
|
||||
const {
|
||||
rows: filteredRows,
|
||||
@@ -174,6 +177,11 @@ export function TableNG(props: TableNGProps) {
|
||||
() => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width) - scrollbarWidth,
|
||||
[width, hasNestedFrames, scrollbarWidth]
|
||||
);
|
||||
const getCellColorInlineStyles = useMemo(() => getCellColorInlineStylesFactory(theme), [theme]);
|
||||
const applyToRowBgFn = useMemo(
|
||||
() => getApplyToRowBgFn(data.fields, getCellColorInlineStyles) ?? undefined,
|
||||
[data.fields, getCellColorInlineStyles]
|
||||
);
|
||||
const typographyCtx = useMemo(
|
||||
() =>
|
||||
createTypographyContext(
|
||||
@@ -226,7 +234,6 @@ export function TableNG(props: TableNGProps) {
|
||||
footerOptions,
|
||||
isCountRowsSet,
|
||||
});
|
||||
const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]);
|
||||
|
||||
// normalize the row height into a function which returns a number, so we avoid a bunch of conditionals during rendering.
|
||||
const rowHeightFn = useMemo((): ((row: TableRow) => number) => {
|
||||
@@ -438,14 +445,12 @@ export function TableNG(props: TableNGProps) {
|
||||
}
|
||||
}
|
||||
|
||||
let style: CSSProperties | undefined;
|
||||
|
||||
if (rowCellStyle.color != null || rowCellStyle.background != null) {
|
||||
style = rowCellStyle;
|
||||
} else if (canBeColorized) {
|
||||
let style: CSSProperties = { ...rowCellStyle };
|
||||
if (canBeColorized) {
|
||||
const value = props.row[props.column.key];
|
||||
const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value?
|
||||
style = getCellColorInlineStyles(theme, cellOptions, displayValue);
|
||||
const cellColorStyles = getCellColorInlineStyles(cellOptions, displayValue, applyToRowBgFn != null);
|
||||
Object.assign(style, cellColorStyles);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -486,6 +491,7 @@ export function TableNG(props: TableNGProps) {
|
||||
showFilters={showFilters}
|
||||
getActions={getCellActions}
|
||||
disableSanitizeHtml={disableSanitizeHtml}
|
||||
getTextColorForBackground={getTextColorForBackground}
|
||||
/>
|
||||
{showActions && (
|
||||
<TableCellActions
|
||||
@@ -549,6 +555,7 @@ export function TableNG(props: TableNGProps) {
|
||||
disableSanitizeHtml,
|
||||
field: tooltipField,
|
||||
getActions: getCellActions,
|
||||
getTextColorForBackground,
|
||||
gridRef,
|
||||
placement,
|
||||
renderer: tooltipFieldRenderer,
|
||||
@@ -560,10 +567,15 @@ export function TableNG(props: TableNGProps) {
|
||||
renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
|
||||
// cached so we don't care about multiple calls.
|
||||
const tooltipHeight = rowHeightFn(props.row);
|
||||
let tooltipStyle: CSSProperties | undefined;
|
||||
let tooltipStyle: CSSProperties = { ...rowCellStyle };
|
||||
if (tooltipCanBeColorized) {
|
||||
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]); // this is yet another call to field.display() for the tooltip field
|
||||
tooltipStyle = getCellColorInlineStyles(theme, tooltipCellOptions, tooltipDisplayValue);
|
||||
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]);
|
||||
const tooltipCellColorStyles = getCellColorInlineStyles(
|
||||
tooltipCellOptions,
|
||||
tooltipDisplayValue,
|
||||
applyToRowBgFn != null
|
||||
);
|
||||
Object.assign(tooltipStyle, tooltipCellColorStyles);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -624,6 +636,8 @@ export function TableNG(props: TableNGProps) {
|
||||
footerCalcs,
|
||||
frozenColumns,
|
||||
getCellActions,
|
||||
getCellColorInlineStyles,
|
||||
getTextColorForBackground,
|
||||
isCountRowsSet,
|
||||
numFrozenColsFullyInView,
|
||||
onCellFilterAdded,
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface TableCellTooltipProps {
|
||||
disableSanitizeHtml?: boolean;
|
||||
field: Field;
|
||||
getActions: (field: Field, rowIdx: number) => ActionModel[];
|
||||
getTextColorForBackground: (bgColor: string) => string;
|
||||
gridRef: RefObject<DataGridHandle>;
|
||||
height: number;
|
||||
placement?: TableCellTooltipPlacement;
|
||||
@@ -40,6 +41,7 @@ export const TableCellTooltip = memo(
|
||||
disableSanitizeHtml,
|
||||
field,
|
||||
getActions,
|
||||
getTextColorForBackground,
|
||||
gridRef,
|
||||
height,
|
||||
placement,
|
||||
@@ -100,6 +102,7 @@ export const TableCellTooltip = memo(
|
||||
field,
|
||||
frame: data,
|
||||
getActions,
|
||||
getTextColorForBackground,
|
||||
height,
|
||||
rowIdx,
|
||||
showFilters: false,
|
||||
@@ -107,7 +110,19 @@ export const TableCellTooltip = memo(
|
||||
value: rawValue,
|
||||
width,
|
||||
}) satisfies TableCellRendererProps,
|
||||
[cellOptions, data, disableSanitizeHtml, field, getActions, height, rawValue, rowIdx, theme, width]
|
||||
[
|
||||
cellOptions,
|
||||
data,
|
||||
disableSanitizeHtml,
|
||||
field,
|
||||
getActions,
|
||||
getTextColorForBackground,
|
||||
height,
|
||||
rawValue,
|
||||
rowIdx,
|
||||
theme,
|
||||
width,
|
||||
]
|
||||
);
|
||||
|
||||
const cellElement = tooltipCaretRef.current?.closest<HTMLElement>('.rdg-cell');
|
||||
|
||||
@@ -167,6 +167,7 @@ export interface TableCellRendererProps {
|
||||
showFilters: boolean;
|
||||
getActions?: GetActionsFunctionLocal;
|
||||
disableSanitizeHtml?: boolean;
|
||||
getTextColorForBackground: (color: string) => string;
|
||||
}
|
||||
|
||||
export type InspectCellProps = {
|
||||
@@ -250,6 +251,7 @@ export interface PillCellProps {
|
||||
theme: GrafanaTheme2;
|
||||
field: Field;
|
||||
rowIdx: number;
|
||||
getTextColorForBackground: (color: string) => string;
|
||||
}
|
||||
|
||||
export interface TableCellStyleOptions {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
extractPixelValue,
|
||||
frameToRecords,
|
||||
getAlignmentFactor,
|
||||
getCellColorInlineStyles,
|
||||
getCellColorInlineStylesFactory,
|
||||
getCellLinks,
|
||||
getCellOptions,
|
||||
getComparator,
|
||||
@@ -107,28 +107,104 @@ describe('TableNG utils', () => {
|
||||
},
|
||||
} as unknown as GrafanaTheme2;
|
||||
|
||||
it('should handle color background mode', () => {
|
||||
const field = { type: TableCellDisplayMode.ColorBackground as const, mode: TableCellBackgroundDisplayMode.Basic };
|
||||
it('should handle color text cell type', () => {
|
||||
const cellOptions = {
|
||||
type: TableCellDisplayMode.ColorText as const,
|
||||
};
|
||||
|
||||
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
||||
|
||||
const colors = getCellColorInlineStyles(theme, field, displayValue);
|
||||
expect(colors.background).toBe('rgb(255, 0, 0)');
|
||||
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
|
||||
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
|
||||
expect(colors.color).toBe('#ff0000');
|
||||
expect(colors).not.toHaveProperty('background');
|
||||
});
|
||||
|
||||
it('should pass thru color background cell type in basic mode', () => {
|
||||
const cellOptions = {
|
||||
type: TableCellDisplayMode.ColorBackground as const,
|
||||
mode: TableCellBackgroundDisplayMode.Basic,
|
||||
};
|
||||
|
||||
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
||||
|
||||
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
|
||||
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
|
||||
expect(colors.background).toBe('#ff0000');
|
||||
expect(colors.color).toBe('rgb(247, 248, 250)');
|
||||
});
|
||||
|
||||
it('should handle color background gradient mode', () => {
|
||||
const field = {
|
||||
it('should handle color background cell type in gradient mode', () => {
|
||||
const cellOptions = {
|
||||
type: TableCellDisplayMode.ColorBackground as const,
|
||||
mode: TableCellBackgroundDisplayMode.Gradient,
|
||||
};
|
||||
|
||||
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
||||
|
||||
const colors = getCellColorInlineStyles(theme, field, displayValue);
|
||||
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
|
||||
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
|
||||
expect(colors.background).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
|
||||
expect(colors.color).toBe('rgb(247, 248, 250)');
|
||||
});
|
||||
|
||||
it('does not set CSSProperties for un-mapped cell types', () => {
|
||||
const cellOptions = { type: TableCellDisplayMode.JSONView as const };
|
||||
|
||||
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
||||
|
||||
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
|
||||
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
|
||||
|
||||
expect(colors).toEqual({});
|
||||
});
|
||||
|
||||
describe('applyToRow', () => {
|
||||
it.each([
|
||||
['hex', '#ffffff00'],
|
||||
['rgba', 'rgba(255,255,255,0)'],
|
||||
['hsla', 'hsla(0,100%,100%,0)'],
|
||||
])(
|
||||
'should not apply background color if the display value is transparent (%s) and applyToRow is on',
|
||||
(_format, colorDisplayValue) => {
|
||||
const cellOptions = {
|
||||
type: TableCellDisplayMode.ColorBackground as const,
|
||||
mode: TableCellBackgroundDisplayMode.Basic,
|
||||
};
|
||||
|
||||
const displayValue = { text: '100', numeric: 100, color: colorDisplayValue };
|
||||
|
||||
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
|
||||
const colors = getCellColorInlineStyles(cellOptions, displayValue, true);
|
||||
|
||||
expect(colors).toEqual({});
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['hex', '#ffffff00'],
|
||||
['rgba', 'rgba(255,255,255,0)'],
|
||||
['hsla', 'hsla(0,100%,100%,0)'],
|
||||
])(
|
||||
'should apply background color if the display value is transparent (%s) and applyToRow is off',
|
||||
(_format, colorDisplayValue) => {
|
||||
const cellOptions = {
|
||||
type: TableCellDisplayMode.ColorBackground as const,
|
||||
mode: TableCellBackgroundDisplayMode.Basic,
|
||||
};
|
||||
|
||||
const displayValue = { text: '100', numeric: 100, color: colorDisplayValue };
|
||||
|
||||
const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme);
|
||||
const colors = getCellColorInlineStyles(cellOptions, displayValue, false);
|
||||
|
||||
expect(colors).toEqual({
|
||||
background: colorDisplayValue,
|
||||
color: 'rgb(32, 34, 38)',
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frame to records conversion', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Property } from 'csstype';
|
||||
import memoize from 'micro-memoize';
|
||||
import { CSSProperties } from 'react';
|
||||
import { SortColumn } from 'react-data-grid';
|
||||
import tinycolor from 'tinycolor2';
|
||||
@@ -499,36 +500,58 @@ const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5;
|
||||
* @internal
|
||||
* Returns the text and background colors for a table cell based on its options and display value.
|
||||
*/
|
||||
export function getCellColorInlineStyles(
|
||||
theme: GrafanaTheme2,
|
||||
cellOptions: TableCellOptions,
|
||||
displayValue: DisplayValue
|
||||
): CSSProperties {
|
||||
// How much to darken elements depends upon if we're in dark mode
|
||||
const darkeningFactor = 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!, theme.isDark);
|
||||
bgColor = tinycolor(displayValue.color).toRgbString();
|
||||
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
|
||||
const bgColor2 = tinycolor(displayValue.color)
|
||||
export function getCellColorInlineStylesFactory(theme: GrafanaTheme2) {
|
||||
const bgCellTextColor = memoize((color: string) => getTextColorForAlphaBackground(color, theme.isDark), {
|
||||
maxSize: 1000,
|
||||
});
|
||||
const darkeningFactor = theme.isDark ? 1 : -0.7; // How much to darken elements depends upon if we're in dark mode
|
||||
const gradientBg = memoize(
|
||||
(color: string) =>
|
||||
tinycolor(color)
|
||||
.darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
|
||||
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES);
|
||||
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
|
||||
bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`;
|
||||
}
|
||||
}
|
||||
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES)
|
||||
.toRgbString(),
|
||||
{ maxSize: 1000 }
|
||||
);
|
||||
const isTransparent = memoize(
|
||||
(color: string) => {
|
||||
// if hex, do the simple thing.
|
||||
if (color[0] === '#') {
|
||||
return color.length === 9 && color.endsWith('00');
|
||||
}
|
||||
// if not hex, just use tinycolor to avoid extra logic.
|
||||
return tinycolor(color).getAlpha() === 0;
|
||||
},
|
||||
{ maxSize: 1000 }
|
||||
);
|
||||
|
||||
return { color: textColor, background: bgColor };
|
||||
return (cellOptions: TableCellOptions, displayValue: DisplayValue, hasApplyToRow: boolean): CSSProperties => {
|
||||
const result: CSSProperties = {};
|
||||
const displayValueColor = displayValue.color;
|
||||
|
||||
if (!displayValueColor) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cellOptions.type === TableCellDisplayMode.ColorText) {
|
||||
result.color = displayValueColor;
|
||||
} else if (cellOptions.type === TableCellDisplayMode.ColorBackground) {
|
||||
// return without setting anything if the bg is transparent. this allows
|
||||
// the cell to inherit the row bg color if `applyToRow` is set.
|
||||
if (hasApplyToRow && isTransparent(displayValueColor)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient;
|
||||
result.color = bgCellTextColor(displayValueColor);
|
||||
result.background =
|
||||
mode === TableCellBackgroundDisplayMode.Gradient
|
||||
? `linear-gradient(120deg, ${gradientBg(displayValueColor)}, ${displayValueColor})`
|
||||
: displayValueColor;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -881,7 +904,10 @@ export function computeColWidths(fields: Field[], availWidth: number) {
|
||||
* @internal
|
||||
* if applyToRow is true in any field, return a function that gets the row background color
|
||||
*/
|
||||
export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowIndex: number) => CSSProperties) | void {
|
||||
export function getApplyToRowBgFn(
|
||||
fields: Field[],
|
||||
getCellColorInlineStyles: ReturnType<typeof getCellColorInlineStylesFactory>
|
||||
): ((rowIndex: number) => CSSProperties) | void {
|
||||
for (const field of fields) {
|
||||
const cellOptions = getCellOptions(field);
|
||||
const fieldDisplay = field.display;
|
||||
@@ -890,7 +916,7 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
|
||||
cellOptions.type === TableCellDisplayMode.ColorBackground &&
|
||||
cellOptions.applyToRow === true
|
||||
) {
|
||||
return (rowIndex: number) => getCellColorInlineStyles(theme, cellOptions, fieldDisplay(field.values[rowIndex]));
|
||||
return (rowIndex: number) => getCellColorInlineStyles(cellOptions, fieldDisplay(field.values[rowIndex]), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user