ev.stopPropagation()} // prevent click from bubbling to the global click listener for un-pinning
+ data-testid={selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper}
+ />
+ )}
+
+ {/* TODO: figure out an accessible way to trigger the tooltip. */}
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
+ setPinned((prev) => !prev)}
+ onMouseLeave={onMouseLeave}
+ onMouseEnter={onMouseEnter}
+ onBlur={onMouseLeave}
+ onFocus={onMouseEnter}
+ />
+
+ {children}
+ >
+ );
+}
diff --git a/packages/grafana-ui/src/components/Table/TableNG/styles.ts b/packages/grafana-ui/src/components/Table/TableNG/styles.ts
index 983372aeb0b..76de0e3e1bb 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/styles.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/styles.ts
@@ -5,7 +5,7 @@ import { GrafanaTheme2, colorManipulator } from '@grafana/data';
import { COLUMN, TABLE } from './constants';
import { TableCellStyles } from './types';
-import { getJustifyContent } from './utils';
+import { getJustifyContent, TextAlign } from './utils';
export const getGridStyles = (
theme: GrafanaTheme2,
@@ -25,6 +25,8 @@ export const getGridStyles = (
'--rdg-summary-border-color': borderColor,
'--rdg-summary-border-width': '1px',
+ '--rdg-selection-color': theme.colors.info.transparent,
+
// note: this cannot have any transparency since default cells that
// overlay/overflow on hover inherit this background and need to occlude cells below
'--rdg-row-background-color': bgColor,
@@ -174,3 +176,34 @@ export const getLinkStyles = (theme: GrafanaTheme2, canBeColorized: boolean) =>
}),
},
});
+
+const caretTriangle = (direction: 'left' | 'right', bgColor: string) =>
+ `linear-gradient(to top ${direction}, transparent 62.5%, ${bgColor} 50%)`;
+
+export const getTooltipStyles = (theme: GrafanaTheme2, textAlign: TextAlign) => ({
+ tooltipContent: css({
+ height: '100%',
+ width: '100%',
+ }),
+ tooltipWrapper: css({
+ background: theme.colors.background.primary,
+ border: `1px solid ${theme.colors.border.weak}`,
+ borderRadius: theme.shape.radius.default,
+ boxShadow: theme.shadows.z3,
+ overflow: 'hidden',
+ padding: theme.spacing(1),
+ width: 'inherit',
+ }),
+ tooltipCaret: css({
+ cursor: 'pointer',
+ position: 'absolute',
+ top: theme.spacing(0.25),
+ [textAlign === 'right' ? 'right' : 'left']: theme.spacing(0.25),
+ width: theme.spacing(1.75),
+ height: theme.spacing(1.75),
+ background: caretTriangle(textAlign === 'right' ? 'right' : 'left', theme.colors.border.medium),
+ '&:hover, &[aria-pressed=true]': {
+ background: caretTriangle(textAlign === 'right' ? 'right' : 'left', theme.colors.border.strong),
+ },
+ }),
+});
diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts
index 8a2b25bc986..7d61ac6200a 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/types.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts
@@ -1,4 +1,4 @@
-import { SyntheticEvent } from 'react';
+import { ReactNode, SyntheticEvent } from 'react';
import { Column } from 'react-data-grid';
import {
@@ -150,6 +150,8 @@ export interface BaseTableProps {
/* ---------------------------- Table cell props ---------------------------- */
export interface TableNGProps extends BaseTableProps {}
+export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
+
export interface TableCellRendererProps {
rowIdx: number;
frame: DataFrame;
@@ -229,12 +231,6 @@ export interface GeoCellProps {
height: number;
}
-export interface CellColors {
- textColor?: string;
- bgColor?: string;
- bgHoverColor?: string;
-}
-
export interface AutoCellProps {
field: Field;
value: TableCellValue;
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 e159c9933cb..5ebd14336b9 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
@@ -23,7 +23,7 @@ import {
extractPixelValue,
frameToRecords,
getAlignmentFactor,
- getCellColors,
+ getCellColorInlineStyles,
getCellLinks,
getCellOptions,
getComparator,
@@ -44,6 +44,8 @@ import {
getDataLinksCounter,
getPillLineCounter,
getDefaultRowHeight,
+ getDisplayName,
+ predicateByName,
} from './utils';
describe('TableNG utils', () => {
@@ -110,9 +112,9 @@ describe('TableNG utils', () => {
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
- const colors = getCellColors(theme, field, displayValue);
- expect(colors.bgColor).toBe('rgb(255, 0, 0)');
- expect(colors.textColor).toBe('rgb(247, 248, 250)');
+ const colors = getCellColorInlineStyles(theme, field, displayValue);
+ expect(colors.background).toBe('rgb(255, 0, 0)');
+ expect(colors.color).toBe('rgb(247, 248, 250)');
});
it('should handle color background gradient mode', () => {
@@ -123,9 +125,9 @@ describe('TableNG utils', () => {
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
- 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)');
+ const colors = getCellColorInlineStyles(theme, field, displayValue);
+ expect(colors.background).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
+ expect(colors.color).toBe('rgb(247, 248, 250)');
});
});
@@ -1268,4 +1270,54 @@ describe('TableNG utils', () => {
]);
});
});
+
+ describe('getDisplayName', () => {
+ it('should return the display name if set', () => {
+ const field: Field = {
+ name: 'test',
+ type: FieldType.string,
+ config: {},
+ state: { displayName: 'Test Display Name' },
+ values: [],
+ };
+ expect(getDisplayName(field)).toBe('Test Display Name');
+ });
+
+ it('should return the field name if no display name is set', () => {
+ const field: Field = {
+ name: 'test',
+ type: FieldType.string,
+ state: {},
+ config: {},
+ values: [],
+ };
+ expect(getDisplayName(field)).toBe('test');
+ });
+ });
+
+ describe('predicateByName', () => {
+ it('should return true for matching field names', () => {
+ const field: Field = { name: 'test', type: FieldType.string, config: {}, values: [] };
+ const predicate = predicateByName('test');
+ expect(predicate(field)).toBe(true);
+ });
+
+ it('should return true for a matching display name', () => {
+ const field: Field = {
+ name: 'test',
+ type: FieldType.string,
+ config: {},
+ state: { displayName: 'Test Display Name' },
+ values: [],
+ };
+ const predicate = predicateByName('Test Display Name');
+ expect(predicate(field)).toBe(true);
+ });
+
+ it('should return false for non-matching field names', () => {
+ const field: Field = { name: 'test', type: FieldType.string, config: {}, values: [] };
+ const predicate = predicateByName('other');
+ expect(predicate(field)).toBe(false);
+ });
+ });
});
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
index 9ba4a5eddca..3c36e150a3c 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
@@ -29,7 +29,6 @@ import { TableCellOptions } from '../types';
import { inferPills } from './Cells/PillCell';
import { COLUMN, TABLE } from './constants';
import {
- CellColors,
TableRow,
ColumnTypes,
FrameToRowsConverter,
@@ -489,18 +488,17 @@ 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 getCellColors(
+export function getCellColorInlineStyles(
theme: GrafanaTheme2,
cellOptions: TableCellOptions,
displayValue: DisplayValue
-): CellColors {
+): 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;
- // let bgHoverColor: string | undefined = undefined;
if (cellOptions.type === TableCellDisplayMode.ColorText) {
textColor = displayValue.color;
@@ -510,23 +508,16 @@ 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();
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
- // 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})`;
}
}
- return { textColor, bgColor };
+ return { color: textColor, background: bgColor };
}
/**
@@ -814,6 +805,11 @@ export const getDisplayName = (field: Field): string => {
return field.state?.displayName ?? field.name;
};
+/**
+ * @internal given a field name or display name, returns a predicate function that checks if a field matches that name.
+ */
+export const predicateByName = (name: string) => (f: Field) => f.name === name || getDisplayName(f) === name;
+
/**
* @internal
* returns only fields that are not nested tables and not explicitly hidden
@@ -874,7 +870,7 @@ 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) => CellColors) | void {
+export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowIndex: number) => CSSProperties) | void {
for (const field of fields) {
const cellOptions = getCellOptions(field);
const fieldDisplay = field.display;
@@ -883,7 +879,7 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
cellOptions.type === TableCellDisplayMode.ColorBackground &&
cellOptions.applyToRow === true
) {
- return (rowIndex: number) => getCellColors(theme, cellOptions, fieldDisplay(field.values[rowIndex]));
+ return (rowIndex: number) => getCellColorInlineStyles(theme, cellOptions, fieldDisplay(field.values[rowIndex]));
}
}
}
@@ -897,6 +893,18 @@ export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDis
);
}
+/** @internal */
+export function canFieldBeColorized(
+ cellType: TableCellDisplayMode,
+ applyToRowBgFn?: (rowIndex: number) => CSSProperties
+) {
+ return (
+ cellType === TableCellDisplayMode.ColorBackground ||
+ cellType === TableCellDisplayMode.ColorText ||
+ Boolean(applyToRowBgFn)
+ );
+}
+
export const displayJsonValue: DisplayProcessor = (value: unknown): DisplayValue => {
let displayValue: string;
diff --git a/packages/grafana-ui/src/components/Tooltip/Popover.tsx b/packages/grafana-ui/src/components/Tooltip/Popover.tsx
index cb4b5468707..369f3b46698 100644
--- a/packages/grafana-ui/src/components/Tooltip/Popover.tsx
+++ b/packages/grafana-ui/src/components/Tooltip/Popover.tsx
@@ -25,6 +25,7 @@ interface Props extends Omit, 'content'> {
wrapperClassName?: string;
renderArrow?: boolean;
hidePopper?: () => void;
+ style?: React.CSSProperties;
}
export function Popover({
@@ -36,6 +37,7 @@ export function Popover({
referenceElement,
renderArrow,
hidePopper,
+ style: styleOverrides,
...rest
}: Props) {
const theme = useTheme2();
@@ -89,6 +91,7 @@ export function Popover({
style={{
...floatingStyles,
...placementStyles,
+ ...styleOverrides,
}}
className={wrapperClassName}
{...rest}
diff --git a/public/app/plugins/panel/table/table-new/module.tsx b/public/app/plugins/panel/table/table-new/module.tsx
index bde9b5c7c40..0e17f6c1072 100644
--- a/public/app/plugins/panel/table/table-new/module.tsx
+++ b/public/app/plugins/panel/table/table-new/module.tsx
@@ -9,7 +9,13 @@ import {
FieldConfigProperty,
} from '@grafana/data';
import { t } from '@grafana/i18n';
-import { TableCellOptions, TableCellDisplayMode, defaultTableFieldOptions, TableCellHeight } from '@grafana/schema';
+import {
+ TableCellOptions,
+ TableCellDisplayMode,
+ defaultTableFieldOptions,
+ TableCellHeight,
+ TableCellTooltipPlacement,
+} from '@grafana/schema';
import { PaginationEditor } from './PaginationEditor';
import { TableCellOptionEditor } from './TableCellOptionEditor';
@@ -115,6 +121,46 @@ export const plugin = new PanelPlugin(TablePanel)
category,
defaultValue: undefined,
hideFromDefaults: true,
+ })
+ .addFieldNamePicker({
+ path: 'tooltip.field',
+ name: t('table-new.name-tooltip-from-field', 'Tooltip from field'),
+ description: t(
+ 'table-new.description-tooltip-from-field',
+ 'Render a cell from a field (hidden or visible) in a tooltip'
+ ),
+ category: cellCategory,
+ })
+ .addSelect({
+ path: 'tooltip.placement',
+ name: t('table-new.name-tooltip-placement', 'Tooltip placement'),
+ category: cellCategory,
+ settings: {
+ options: [
+ {
+ label: t('table-new.tooltip-placement-options.label-auto', 'Auto'),
+ value: TableCellTooltipPlacement.Auto,
+ },
+ {
+ label: t('table-new.tooltip-placement-options.label-top', 'Top'),
+ value: TableCellTooltipPlacement.Top,
+ },
+ {
+ label: t('table-new.tooltip-placement-options.label-right', 'Right'),
+ value: TableCellTooltipPlacement.Right,
+ },
+ {
+ label: t('table-new.tooltip-placement-options.label-bottom', 'Bottom'),
+ value: TableCellTooltipPlacement.Bottom,
+ },
+ {
+ label: t('table-new.tooltip-placement-options.label-left', 'Left'),
+ value: TableCellTooltipPlacement.Left,
+ },
+ ],
+ },
+ defaultValue: 'auto',
+ showIf: (cfg) => cfg.tooltip?.field !== undefined,
});
},
})
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index bdc8663bbd9..b0299f1ee32 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -12790,6 +12790,7 @@
"description-fields": "Select the fields that should be calculated",
"description-frozen-columns": "Columns are frozen from the left side of the table",
"description-min-column-width": "The minimum width for column auto resizing",
+ "description-tooltip-from-field": "Render a cell from a field (hidden or visible) in a tooltip",
"name-calculation": "Calculation",
"name-cell-height": "Cell height",
"name-cell-type": "Cell type",
@@ -12805,8 +12806,17 @@
"name-min-column-width": "Minimum column width",
"name-show-table-footer": "Show table footer",
"name-show-table-header": "Show table header",
+ "name-tooltip-from-field": "Tooltip from field",
+ "name-tooltip-placement": "Tooltip placement",
"placeholder-column-width": "auto",
- "placeholder-fields": "All Numeric Fields"
+ "placeholder-fields": "All Numeric Fields",
+ "tooltip-placement-options": {
+ "label-auto": "Auto",
+ "label-bottom": "Bottom",
+ "label-left": "Left",
+ "label-right": "Right",
+ "label-top": "Top"
+ }
},
"tag-filter": {
"clear-button": "Clear tags",