TableNG: Avoid render-time emotion calls for AutoCell (and colored cells) (#108048)
Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Paul Marbach
parent
cd1f086d33
commit
e42c45c30f
@@ -84,6 +84,7 @@
|
||||
"@types/react-table": "7.7.20",
|
||||
"calculate-size": "1.1.1",
|
||||
"classnames": "2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3": "7.9.0",
|
||||
"date-fns": "4.1.0",
|
||||
"downshift": "^9.0.6",
|
||||
|
||||
@@ -1,65 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Property } from 'csstype';
|
||||
import { formattedValueToString } from '@grafana/data';
|
||||
|
||||
import { GrafanaTheme2, formattedValueToString } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../../../themes/ThemeContext';
|
||||
import { renderSingleLink } from '../../DataLinksActionsTooltip';
|
||||
import { TableCellOptions, TableCellDisplayMode } from '../../types';
|
||||
import { useSingleLink } from '../hooks';
|
||||
import { AutoCellProps } from '../types';
|
||||
|
||||
export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions }: AutoCellProps) {
|
||||
const styles = useStyles2(getStyles, justifyContent);
|
||||
|
||||
export default function AutoCell({ value, field, rowIdx }: AutoCellProps) {
|
||||
const displayValue = field.display!(value);
|
||||
const formattedValue = formattedValueToString(displayValue);
|
||||
const link = useSingleLink(field, rowIdx);
|
||||
|
||||
return (
|
||||
<div className={styles.cell}>
|
||||
{link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))}
|
||||
</div>
|
||||
);
|
||||
return link != null ? renderSingleLink(link, formattedValue) : formattedValue;
|
||||
}
|
||||
|
||||
const getLinkStyle = (styles: ReturnType<typeof getStyles>, cellOptions: TableCellOptions) => {
|
||||
if (cellOptions.type === TableCellDisplayMode.Auto) {
|
||||
return styles.linkCell;
|
||||
}
|
||||
|
||||
return styles.cellLinkForColoredCell;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({
|
||||
cell: css({
|
||||
display: 'flex',
|
||||
justifyContent: justifyContent,
|
||||
a: {
|
||||
color: 'inherit',
|
||||
},
|
||||
}),
|
||||
cellLinkForColoredCell: css({
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
textDecoration: 'underline',
|
||||
}),
|
||||
linkCell: css({
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
color: `${theme.colors.text.link} !important`,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
paddingRight: theme.spacing(1.5),
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => {
|
||||
{links &&
|
||||
links.map((link, idx) => {
|
||||
return !link.href && link.onClick == null ? (
|
||||
<span key={idx} className={styles.cellLinkEmpty}>
|
||||
<span key={idx} className={styles.linkCell}>
|
||||
{link.title}
|
||||
</span>
|
||||
) : (
|
||||
@@ -34,25 +34,6 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => {
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
linkCell: css({
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors.text.link,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
paddingRight: theme.spacing(1.5),
|
||||
a: {
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
}),
|
||||
cellLinkEmpty: css({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
|
||||
@@ -29,13 +29,7 @@ const GAUGE_RENDERER: TableCellRenderer = (props) => (
|
||||
);
|
||||
|
||||
const AUTO_RENDERER: TableCellRenderer = (props) => (
|
||||
<AutoCell
|
||||
value={props.value}
|
||||
field={props.field}
|
||||
justifyContent={props.justifyContent}
|
||||
rowIdx={props.rowIdx}
|
||||
cellOptions={props.cellOptions}
|
||||
/>
|
||||
<AutoCell value={props.value} field={props.field} rowIdx={props.rowIdx} />
|
||||
);
|
||||
|
||||
const SPARKLINE_RENDERER: TableCellRenderer = (props) => (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'react-data-grid/lib/styles.css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { clsx } from 'clsx';
|
||||
import { Property } from 'csstype';
|
||||
import { Key, ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CSSProperties, Key, ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Cell,
|
||||
CellRendererProps,
|
||||
@@ -34,7 +35,7 @@ import { Pagination } from '../../Pagination/Pagination';
|
||||
import { PanelContext, usePanelContext } from '../../PanelChrome';
|
||||
import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip';
|
||||
import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector';
|
||||
import { CellColors, TableCellDisplayMode } from '../types';
|
||||
import { TableCellDisplayMode } from '../types';
|
||||
import { DataLinksActionsTooltipState } from '../utils';
|
||||
|
||||
import { HeaderCell } from './Cells/HeaderCell';
|
||||
@@ -279,6 +280,11 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
let lastRowIdx = -1;
|
||||
let _rowHeight = 0;
|
||||
// shared when whole row will be styled by a single cell's color
|
||||
let rowCellStyle: Partial<CSSProperties> = {
|
||||
color: undefined,
|
||||
background: undefined,
|
||||
};
|
||||
|
||||
f.forEach((field, i) => {
|
||||
const cellOptions = getCellOptions(field);
|
||||
@@ -304,7 +310,7 @@ export function TableNG(props: TableNGProps) {
|
||||
const justifyContent = getTextAlign(field);
|
||||
const footerStyles = getFooterStyles(justifyContent);
|
||||
const displayName = getDisplayName(field);
|
||||
const headerCellClass = getHeaderCellStyles(theme, justifyContent).headerCell;
|
||||
const headerCellClass = getHeaderCellStyles(theme, justifyContent);
|
||||
const renderFieldCell = getCellRenderer(field, cellOptions);
|
||||
|
||||
const cellInspect = isCellInspectEnabled(field);
|
||||
@@ -314,7 +320,7 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
// helps us avoid string cx and emotion per-cell
|
||||
const cellActionClassName = showActions
|
||||
? cx(
|
||||
? clsx(
|
||||
'table-cell-actions',
|
||||
styles.cellActions,
|
||||
justifyContent === 'flex-end' ? styles.cellActionsEnd : styles.cellActionsStart
|
||||
@@ -324,41 +330,63 @@ export function TableNG(props: TableNGProps) {
|
||||
const shouldOverflow = shouldTextOverflow(field);
|
||||
const shouldWrap = shouldTextWrap(field);
|
||||
const withTooltip = withDataLinksActionsTooltip(field, cellType);
|
||||
const canBeColorized =
|
||||
cellType === TableCellDisplayMode.ColorBackground || cellType === TableCellDisplayMode.ColorText;
|
||||
|
||||
result.colsWithTooltip[displayName] = withTooltip;
|
||||
|
||||
// get static cell class based on col props
|
||||
|
||||
let cellClass = '';
|
||||
|
||||
switch (cellType) {
|
||||
case TableCellDisplayMode.Auto:
|
||||
case TableCellDisplayMode.ColorBackground:
|
||||
case TableCellDisplayMode.ColorText:
|
||||
case TableCellDisplayMode.DataLinks:
|
||||
cellClass = getCellStyles(theme, justifyContent, shouldWrap, shouldOverflow, withTooltip, canBeColorized);
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: in future extend this to ensure a non-classic color scheme is set with AutoCell
|
||||
|
||||
// this fires first
|
||||
const renderCellRoot = (key: Key, props: CellRendererProps<TableRow, TableSummaryRow>): ReactNode => {
|
||||
const rowIdx = props.row.__index;
|
||||
const value = props.row[props.column.key];
|
||||
|
||||
// meh, this should be cached by the renderRow() call?
|
||||
if (rowIdx !== lastRowIdx) {
|
||||
_rowHeight = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight;
|
||||
lastRowIdx = rowIdx;
|
||||
|
||||
rowCellStyle.color = undefined;
|
||||
rowCellStyle.background = undefined;
|
||||
|
||||
// generate shared styles for whole row
|
||||
if (applyToRowBgFn != null) {
|
||||
let { textColor, bgColor } = applyToRowBgFn(rowIdx);
|
||||
rowCellStyle.color = textColor;
|
||||
rowCellStyle.background = bgColor;
|
||||
}
|
||||
}
|
||||
|
||||
let colors: CellColors;
|
||||
let style: CSSProperties | undefined;
|
||||
|
||||
if (applyToRowBgFn != null) {
|
||||
colors = applyToRowBgFn(props.rowIdx);
|
||||
} else if (cellType !== TableCellDisplayMode.Auto) {
|
||||
if (rowCellStyle.color != null || rowCellStyle.background != null) {
|
||||
style = rowCellStyle;
|
||||
}
|
||||
// apply background for cell types which can have a background and have proper
|
||||
else 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?
|
||||
colors = getCellColors(theme, cellOptions, displayValue);
|
||||
} else {
|
||||
colors = {};
|
||||
let { textColor, bgColor } = getCellColors(theme, cellOptions, displayValue);
|
||||
style = {
|
||||
color: textColor,
|
||||
background: bgColor,
|
||||
};
|
||||
}
|
||||
|
||||
const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, withTooltip, colors);
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={key}
|
||||
{...props}
|
||||
className={cx(props.className, cellStyle.cell)}
|
||||
style={{ color: colors.textColor ?? 'inherit' }}
|
||||
/>
|
||||
);
|
||||
return <Cell key={key} {...props} className={clsx(props.className, cellClass)} style={style} />; // TODO: remove expensive concat
|
||||
};
|
||||
|
||||
result.cellRootRenderers[displayName] = renderCellRoot;
|
||||
@@ -518,8 +546,8 @@ export function TableNG(props: TableNGProps) {
|
||||
return (
|
||||
<DataGrid<TableRow, TableSummaryRow>
|
||||
{...commonDataGridProps}
|
||||
className={cx(styles.grid, styles.gridNested)}
|
||||
headerRowClass={cx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })}
|
||||
className={clsx(styles.grid, styles.gridNested)}
|
||||
headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })}
|
||||
headerRowHeight={hasNestedHeaders ? defaultHeaderHeight : 0}
|
||||
columns={nestedColumns}
|
||||
rows={expandedRecords}
|
||||
@@ -584,7 +612,7 @@ export function TableNG(props: TableNGProps) {
|
||||
className={styles.grid}
|
||||
columns={structureRevColumns}
|
||||
rows={paginatedRows}
|
||||
headerRowClass={cx(styles.headerRow, { [styles.displayNone]: noHeader })}
|
||||
headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: noHeader })}
|
||||
headerRowHeight={headerHeight}
|
||||
onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => {
|
||||
// Note: could be column.field; JS says yes, but TS says no!
|
||||
@@ -836,8 +864,8 @@ const getFooterStyles = (justifyContent: Property.JustifyContent) => ({
|
||||
}),
|
||||
});
|
||||
|
||||
const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
|
||||
headerCell: css({
|
||||
const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) =>
|
||||
css({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(0.5),
|
||||
zIndex: theme.zIndex.tooltip - 1,
|
||||
@@ -847,45 +875,56 @@ const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.Just
|
||||
'&:last-child': {
|
||||
borderInlineEnd: 'none',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const getCellStyles = (
|
||||
theme: GrafanaTheme2,
|
||||
field: Field,
|
||||
rowHeight: number,
|
||||
justifyContent: Property.JustifyContent,
|
||||
shouldWrap: boolean,
|
||||
shouldOverflow: boolean,
|
||||
hasTooltip: boolean,
|
||||
colors: CellColors
|
||||
) => {
|
||||
return {
|
||||
cell: css({
|
||||
textOverflow: 'initial',
|
||||
background: colors.bgColor ?? 'inherit',
|
||||
alignContent: 'center',
|
||||
justifyContent: getTextAlign(field),
|
||||
paddingInline: TABLE.CELL_PADDING,
|
||||
height: '100%',
|
||||
minHeight: rowHeight, // min height interacts with the fit-content property on the overflow container
|
||||
...(shouldWrap && { whiteSpace: 'pre-line' }),
|
||||
...(hasTooltip && { cursor: 'pointer' }),
|
||||
'&:last-child': {
|
||||
borderInlineEnd: 'none',
|
||||
isColorized: boolean
|
||||
) =>
|
||||
css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent,
|
||||
paddingInline: TABLE.CELL_PADDING,
|
||||
minHeight: '100%',
|
||||
backgroundClip: 'padding-box !important', // helps when cells have a bg color
|
||||
...(shouldWrap && { whiteSpace: 'pre-line' }),
|
||||
...(hasTooltip && { cursor: 'pointer' }),
|
||||
|
||||
'&:last-child': {
|
||||
borderInlineEnd: 'none',
|
||||
},
|
||||
|
||||
// should omit if no cell actions, and no shouldOverflow
|
||||
'&:hover': {
|
||||
'.table-cell-actions': {
|
||||
display: 'flex',
|
||||
},
|
||||
'&:hover': {
|
||||
background: colors.bgHoverColor,
|
||||
'.table-cell-actions': {
|
||||
display: 'flex',
|
||||
},
|
||||
...(shouldOverflow && {
|
||||
zIndex: theme.zIndex.tooltip - 2,
|
||||
whiteSpace: 'pre-line',
|
||||
height: 'fit-content',
|
||||
minWidth: 'fit-content',
|
||||
paddingBlock: (rowHeight - TABLE.LINE_HEIGHT) / 2 - 1,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
...(shouldOverflow && {
|
||||
zIndex: theme.zIndex.tooltip - 2,
|
||||
whiteSpace: 'pre-line',
|
||||
height: 'fit-content',
|
||||
minWidth: 'fit-content',
|
||||
}),
|
||||
},
|
||||
|
||||
[hasTooltip ? '&' : 'a']: {
|
||||
cursor: 'pointer',
|
||||
...(isColorized
|
||||
? {
|
||||
color: 'inherit',
|
||||
textDecoration: 'underline',
|
||||
}
|
||||
: {
|
||||
color: theme.colors.text.link,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -246,9 +246,7 @@ export interface CellColors {
|
||||
export interface AutoCellProps {
|
||||
field: Field;
|
||||
value: TableCellValue;
|
||||
justifyContent: Property.JustifyContent;
|
||||
rowIdx: number;
|
||||
cellOptions: TableCellOptions;
|
||||
}
|
||||
|
||||
export interface ActionCellProps {
|
||||
|
||||
@@ -188,7 +188,6 @@ describe('TableNG utils', () => {
|
||||
const colors = getCellColors(theme, field, displayValue);
|
||||
expect(colors.bgColor).toBe('rgb(255, 0, 0)');
|
||||
expect(colors.textColor).toBe('rgb(247, 248, 250)');
|
||||
expect(colors.bgHoverColor).toBe('rgb(255, 36, 36)');
|
||||
});
|
||||
|
||||
it('should handle color background gradient mode', () => {
|
||||
@@ -206,7 +205,6 @@ describe('TableNG utils', () => {
|
||||
const colors = getCellColors(theme, field, displayValue);
|
||||
expect(colors.bgColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
|
||||
expect(colors.textColor).toBe('rgb(247, 248, 250)');
|
||||
expect(colors.bgHoverColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), rgb(255, 54, 54))');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -230,7 +230,6 @@ export function getAlignmentFactor(
|
||||
|
||||
/* ------------------------- Cell color calculation ------------------------- */
|
||||
const CELL_COLOR_DARKENING_MULTIPLIER = 10;
|
||||
const CELL_GRADIENT_DARKENING_MULTIPLIER = 15;
|
||||
const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5;
|
||||
|
||||
/**
|
||||
@@ -248,7 +247,7 @@ export function getCellColors(
|
||||
// Setup color variables
|
||||
let textColor: string | undefined = undefined;
|
||||
let bgColor: string | undefined = undefined;
|
||||
let bgHoverColor: string | undefined = undefined;
|
||||
// let bgHoverColor: string | undefined = undefined;
|
||||
|
||||
if (cellOptions.type === TableCellDisplayMode.ColorText) {
|
||||
textColor = displayValue.color;
|
||||
@@ -258,23 +257,23 @@ export function getCellColors(
|
||||
if (mode === TableCellBackgroundDisplayMode.Basic) {
|
||||
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
|
||||
bgColor = tinycolor(displayValue.color).toRgbString();
|
||||
bgHoverColor = tinycolor(displayValue.color)
|
||||
.darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
|
||||
.toRgbString();
|
||||
// bgHoverColor = tinycolor(displayValue.color)
|
||||
// .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
|
||||
// .toRgbString();
|
||||
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
|
||||
const hoverColor = tinycolor(displayValue.color)
|
||||
.darken(CELL_GRADIENT_DARKENING_MULTIPLIER * darkeningFactor)
|
||||
.toRgbString();
|
||||
// const hoverColor = tinycolor(displayValue.color)
|
||||
// .darken(CELL_GRADIENT_DARKENING_MULTIPLIER * darkeningFactor)
|
||||
// .toRgbString();
|
||||
const bgColor2 = tinycolor(displayValue.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})`;
|
||||
bgHoverColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${hoverColor})`;
|
||||
// bgHoverColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${hoverColor})`;
|
||||
}
|
||||
}
|
||||
|
||||
return { textColor, bgColor, bgHoverColor };
|
||||
return { textColor, bgColor };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user