diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index e7eecdd6f4c..a8fd784f58c 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -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", diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx index 71293bbc06e..45427f703e8 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -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 ( -
- {link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))} -
- ); + return link != null ? renderSingleLink(link, formattedValue) : formattedValue; } - -const getLinkStyle = (styles: ReturnType, 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, - }, - }), -}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx index d1612de97c8..a83a623ee88 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx @@ -16,7 +16,7 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => { {links && links.map((link, idx) => { return !link.href && link.onClick == null ? ( - + {link.title} ) : ( @@ -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, diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx index d1bdcdad4a2..fe4bc7e3f0c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx @@ -29,13 +29,7 @@ const GAUGE_RENDERER: TableCellRenderer = (props) => ( ); const AUTO_RENDERER: TableCellRenderer = (props) => ( - + ); const SPARKLINE_RENDERER: TableCellRenderer = (props) => ( diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 5590dad8e3e..ea128eddafb 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -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 = { + 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): 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 ( - - ); + return ; // TODO: remove expensive concat }; result.cellRootRenderers[displayName] = renderCellRoot; @@ -518,8 +546,8 @@ export function TableNG(props: TableNGProps) { return ( {...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', + }, + }), + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 65d40d3c4d1..742d495b806 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -246,9 +246,7 @@ export interface CellColors { export interface AutoCellProps { field: Field; value: TableCellValue; - justifyContent: Property.JustifyContent; rowIdx: number; - cellOptions: TableCellOptions; } export interface ActionCellProps { diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 302ef067224..59e58541744 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -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))'); }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index a99ec3edab7..1cd6d279856 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -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 }; } /** diff --git a/yarn.lock b/yarn.lock index 3b7ead9baf2..cdd4a49083c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3747,6 +3747,7 @@ __metadata: calculate-size: "npm:1.1.1" chance: "npm:^1.1.13" classnames: "npm:2.5.1" + clsx: "npm:^2.1.1" common-tags: "npm:1.8.2" core-js: "npm:3.40.0" css-loader: "npm:7.1.2"