mirror of https://github.com/grafana/grafana.git
Table: Tooltip from Field (#109428)
* Table: Tooltip by Field * add colorization support * more progress on customizing the tooltip based on cell customization * configurable as an option * tooltip triggered from a chip in the corner, pinning tooltip * i18n * use enum where appropriate * correctly orient the tooltip to the cell - but at what cost * clean up some console.logs * e2e test for the tooltip * fix global click stuff * remove console.log * refine the style of the caret * caret placement on same-side as alignment to avoid collision with inspect hover * some updates from self-review * increase hit target of tooltip caret * fix width and height auto-sizing especially for dynamic height * reorganize to pre-calc the per-field stuff * use linear gradient for triangle * update e2e to reflect current behavior, that clicking caret multiple times doesn't toggle pinning * clean up event handlers a bit * restore test for toggle click * alright, re-remove the toggle case * cursor pointer * remove optional root from Popover for now * remove this ridiculous autogenerated file * update some of the text * kill the cellRefMatrix * remove unused import * extract a util for the predicateByName part * skip the intermediary step for getCellColors
This commit is contained in:
parent
4e5a51968f
commit
66eee1cb08
|
@ -299,6 +299,14 @@
|
||||||
{
|
{
|
||||||
"id": "custom.width",
|
"id": "custom.width",
|
||||||
"value": 255
|
"value": 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.tooltip.field",
|
||||||
|
"value": "State"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.tooltip.placement",
|
||||||
|
"value": "left"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -353,6 +353,69 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||||
// TODO -- saving for another day.
|
// TODO -- saving for another day.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Tests tooltip interactions', async ({ gotoDashboardPage, selectors }) => {
|
||||||
|
const dashboardPage = await gotoDashboardPage({
|
||||||
|
uid: DASHBOARD_UID,
|
||||||
|
queryParams: new URLSearchParams({ editPanel: '1' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const firstCaret = dashboardPage
|
||||||
|
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Caret)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
// test hovering over and blurring the caret, and whether the tooltip appears and disappears as expected.
|
||||||
|
await firstCaret.hover();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')).hover();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
// when a pinned tooltip is open, clicking outside of it should close it.
|
||||||
|
await firstCaret.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
// when a pinned tooltip is open, clicking inside of it should NOT close it.
|
||||||
|
await firstCaret.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const tooltip = dashboardPage.getByGrafanaSelector(
|
||||||
|
selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper
|
||||||
|
);
|
||||||
|
await tooltip.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper)
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test('Empty Table panel', async ({ gotoDashboardPage, selectors }) => {
|
test('Empty Table panel', async ({ gotoDashboardPage, selectors }) => {
|
||||||
const dashboardPage = await gotoDashboardPage({
|
const dashboardPage = await gotoDashboardPage({
|
||||||
uid: DASHBOARD_UID,
|
uid: DASHBOARD_UID,
|
||||||
|
|
|
@ -493,6 +493,14 @@ export const versionedComponents = {
|
||||||
'12.1.0': 'data-testid tableng filter select-all',
|
'12.1.0': 'data-testid tableng filter select-all',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tooltip: {
|
||||||
|
Wrapper: {
|
||||||
|
'12.2.0': 'data-testid tableng tooltip wrapper',
|
||||||
|
},
|
||||||
|
Caret: {
|
||||||
|
'12.2.0': 'data-testid tableng tooltip caret',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -976,6 +976,14 @@ export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions
|
||||||
type: TableCellDisplayMode.Geo
|
type: TableCellDisplayMode.Geo
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export enum TableCellTooltipPlacement {
|
||||||
|
Auto = 'auto',
|
||||||
|
Bottom = 'bottom',
|
||||||
|
Left = 'left',
|
||||||
|
Right = 'right',
|
||||||
|
Top = 'top',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||||
* Generally defines alignment, filtering capabilties, display options, etc.
|
* Generally defines alignment, filtering capabilties, display options, etc.
|
||||||
|
@ -995,6 +1003,19 @@ export interface TableFieldOptions {
|
||||||
hideHeader?: boolean;
|
hideHeader?: boolean;
|
||||||
inspect: boolean;
|
inspect: boolean;
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
|
/**
|
||||||
|
* Selecting or hovering this field will show a tooltip containing the content within the target field
|
||||||
|
*/
|
||||||
|
tooltip?: {
|
||||||
|
/**
|
||||||
|
* The name of the field to get the tooltip content from
|
||||||
|
*/
|
||||||
|
field: string;
|
||||||
|
/**
|
||||||
|
* placement of the tooltip
|
||||||
|
*/
|
||||||
|
placement?: TableCellTooltipPlacement;
|
||||||
|
};
|
||||||
width?: number;
|
width?: number;
|
||||||
/**
|
/**
|
||||||
* Enables text wrapping for column headers
|
* Enables text wrapping for column headers
|
||||||
|
|
|
@ -107,10 +107,21 @@ TableGeoCellOptions: {
|
||||||
// Height of a table cell
|
// Height of a table cell
|
||||||
TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum")
|
TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Table cell options. Each cell has a display mode
|
// Table cell options. Each cell has a display mode
|
||||||
// and other potential options for that display.
|
// and other potential options for that display.
|
||||||
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions | TableGeoCellOptions @cuetsy(kind="type")
|
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions | TableGeoCellOptions @cuetsy(kind="type")
|
||||||
|
|
||||||
|
TableCellTooltipPlacement: "top" | "bottom" | "left" | "right" | "auto" @cuetsy(kind="enum")
|
||||||
|
|
||||||
|
TableCellTooltipOptions: {
|
||||||
|
// The name of the field to get the tooltip content from
|
||||||
|
field: string
|
||||||
|
// placement of the tooltip
|
||||||
|
placement?: TableCellTooltipPlacement & (*"auto" | _)
|
||||||
|
}
|
||||||
|
|
||||||
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||||
// Generally defines alignment, filtering capabilties, display options, etc.
|
// Generally defines alignment, filtering capabilties, display options, etc.
|
||||||
TableFieldOptions: {
|
TableFieldOptions: {
|
||||||
|
@ -127,4 +138,6 @@ TableFieldOptions: {
|
||||||
hideHeader?: bool
|
hideHeader?: bool
|
||||||
// Enables text wrapping for column headers
|
// Enables text wrapping for column headers
|
||||||
wrapHeaderText?: bool
|
wrapHeaderText?: bool
|
||||||
|
// Selecting or hovering this field will show a tooltip containing the content within the target field
|
||||||
|
tooltip?: TableCellTooltipOptions
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
|
@ -6,12 +6,19 @@ import { MaybeWrapWithLink } from '../MaybeWrapWithLink';
|
||||||
import { MarkdownCellProps, TableCellStyles } from '../types';
|
import { MarkdownCellProps, TableCellStyles } from '../types';
|
||||||
|
|
||||||
export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) {
|
export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) {
|
||||||
|
const rawValue = field.values[rowIdx];
|
||||||
|
if (rawValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderValue = field.display!(rawValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
|
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
|
||||||
<div
|
<div
|
||||||
className="markdown-container"
|
className="markdown-container"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: renderMarkdown(field.values[rowIdx], { noSanitize: disableSanitizeHtml }).trim(),
|
__html: renderMarkdown(renderValue.text, { noSanitize: disableSanitizeHtml }).trim(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</MaybeWrapWithLink>
|
</MaybeWrapWithLink>
|
||||||
|
@ -26,20 +33,17 @@ export const getStyles: TableCellStyles = (theme) =>
|
||||||
|
|
||||||
'.markdown-container': {
|
'.markdown-container': {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
|
||||||
|
|
||||||
'& ol, & ul': {
|
|
||||||
paddingLeft: theme.spacing(1.5),
|
|
||||||
},
|
|
||||||
'& p': {
|
|
||||||
whiteSpace: 'pre-line',
|
|
||||||
},
|
|
||||||
'& a': {
|
|
||||||
color: theme.colors.primary.text,
|
|
||||||
},
|
|
||||||
// for elements like `p`, `h*`, etc. which have an inherent margin,
|
// for elements like `p`, `h*`, etc. which have an inherent margin,
|
||||||
// we want to remove the bottom margin for the last one in the container.
|
// we want to remove the bottom margin for the last one in the container.
|
||||||
'& > .markdown-container > *:last-child': {
|
'> *:last-child': {
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'ol, ul': {
|
||||||
|
paddingLeft: theme.spacing(2.5),
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { Field, FieldType, GrafanaTheme2, isDataFrame, isTimeSeriesFrame } from '@grafana/data';
|
import { Field, FieldType, GrafanaTheme2, isDataFrame, isTimeSeriesFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { TableCellDisplayMode, TableCellOptions, TableCustomCellOptions } from '../../types';
|
import { TableCellDisplayMode, TableCellOptions, TableCustomCellOptions } from '../../types';
|
||||||
import { TableCellRendererProps, TableCellStyleOptions, TableCellStyles } from '../types';
|
import { TableCellRenderer, TableCellStyleOptions, TableCellStyles } from '../types';
|
||||||
|
import { getCellOptions } from '../utils';
|
||||||
|
|
||||||
import { ActionsCell, getStyles as getActionsCellStyles } from './ActionsCell';
|
import { ActionsCell, getStyles as getActionsCellStyles } from './ActionsCell';
|
||||||
import { AutoCell, getStyles as getAutoCellStyles, getJsonCellStyles } from './AutoCell';
|
import { AutoCell, getStyles as getAutoCellStyles, getJsonCellStyles } from './AutoCell';
|
||||||
|
@ -16,8 +16,6 @@ import { MarkdownCell, getStyles as getMarkdownCellStyles } from './MarkdownCell
|
||||||
import { PillCell, getStyles as getPillStyles } from './PillCell';
|
import { PillCell, getStyles as getPillStyles } from './PillCell';
|
||||||
import { SparklineCell, getStyles as getSparklineCellStyles } from './SparklineCell';
|
import { SparklineCell, getStyles as getSparklineCellStyles } from './SparklineCell';
|
||||||
|
|
||||||
export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
|
|
||||||
|
|
||||||
const GAUGE_RENDERER: TableCellRenderer = (props) => (
|
const GAUGE_RENDERER: TableCellRenderer = (props) => (
|
||||||
<BarGaugeCell
|
<BarGaugeCell
|
||||||
field={props.field}
|
field={props.field}
|
||||||
|
@ -141,7 +139,10 @@ const STRING_ONLY_RENDERERS = new Set<TableCellOptions['type']>([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function getCellRenderer(field: Field, cellOptions: TableCellOptions): TableCellRenderer {
|
export function getCellRenderer(
|
||||||
|
field: Field,
|
||||||
|
cellOptions: TableCellOptions = getCellOptions(field)
|
||||||
|
): TableCellRenderer {
|
||||||
const cellType = cellOptions?.type ?? TableCellDisplayMode.Auto;
|
const cellType = cellOptions?.type ?? TableCellDisplayMode.Auto;
|
||||||
if (cellType === TableCellDisplayMode.Auto) {
|
if (cellType === TableCellDisplayMode.Auto) {
|
||||||
return CELL_RENDERERS[getAutoRendererDisplayMode(field)].renderer;
|
return CELL_RENDERERS[getAutoRendererDisplayMode(field)].renderer;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {
|
||||||
ReducerID,
|
ReducerID,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { t, Trans } from '@grafana/i18n';
|
import { t, Trans } from '@grafana/i18n';
|
||||||
import { FieldColorModeId } from '@grafana/schema';
|
import { FieldColorModeId, TableCellTooltipPlacement } from '@grafana/schema';
|
||||||
|
|
||||||
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
|
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
|
||||||
import { ContextMenu } from '../../ContextMenu/ContextMenu';
|
import { ContextMenu } from '../../ContextMenu/ContextMenu';
|
||||||
|
@ -40,6 +40,7 @@ import { getCellRenderer, getCellSpecificStyles } from './Cells/renderers';
|
||||||
import { HeaderCell } from './components/HeaderCell';
|
import { HeaderCell } from './components/HeaderCell';
|
||||||
import { RowExpander } from './components/RowExpander';
|
import { RowExpander } from './components/RowExpander';
|
||||||
import { TableCellActions } from './components/TableCellActions';
|
import { TableCellActions } from './components/TableCellActions';
|
||||||
|
import { TableCellTooltip } from './components/TableCellTooltip';
|
||||||
import { COLUMN, TABLE } from './constants';
|
import { COLUMN, TABLE } from './constants';
|
||||||
import {
|
import {
|
||||||
useColumnResize,
|
useColumnResize,
|
||||||
|
@ -51,10 +52,18 @@ import {
|
||||||
useScrollbarWidth,
|
useScrollbarWidth,
|
||||||
useSortedRows,
|
useSortedRows,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { getDefaultCellStyles, getFooterStyles, getGridStyles, getHeaderCellStyles, getLinkStyles } from './styles';
|
import {
|
||||||
|
getDefaultCellStyles,
|
||||||
|
getFooterStyles,
|
||||||
|
getGridStyles,
|
||||||
|
getHeaderCellStyles,
|
||||||
|
getLinkStyles,
|
||||||
|
getTooltipStyles,
|
||||||
|
} from './styles';
|
||||||
import { TableNGProps, TableRow, TableSummaryRow, TableColumn, ContextMenuProps, TableCellStyleOptions } from './types';
|
import { TableNGProps, TableRow, TableSummaryRow, TableColumn, ContextMenuProps, TableCellStyleOptions } from './types';
|
||||||
import {
|
import {
|
||||||
applySort,
|
applySort,
|
||||||
|
canFieldBeColorized,
|
||||||
computeColWidths,
|
computeColWidths,
|
||||||
createTypographyContext,
|
createTypographyContext,
|
||||||
displayJsonValue,
|
displayJsonValue,
|
||||||
|
@ -62,7 +71,7 @@ import {
|
||||||
frameToRecords,
|
frameToRecords,
|
||||||
getAlignment,
|
getAlignment,
|
||||||
getApplyToRowBgFn,
|
getApplyToRowBgFn,
|
||||||
getCellColors,
|
getCellColorInlineStyles,
|
||||||
getCellLinks,
|
getCellLinks,
|
||||||
getCellOptions,
|
getCellOptions,
|
||||||
getDefaultRowHeight,
|
getDefaultRowHeight,
|
||||||
|
@ -71,6 +80,7 @@ import {
|
||||||
getJustifyContent,
|
getJustifyContent,
|
||||||
getVisibleFields,
|
getVisibleFields,
|
||||||
isCellInspectEnabled,
|
isCellInspectEnabled,
|
||||||
|
predicateByName,
|
||||||
shouldTextOverflow,
|
shouldTextOverflow,
|
||||||
shouldTextWrap,
|
shouldTextWrap,
|
||||||
withDataLinksActionsTooltip,
|
withDataLinksActionsTooltip,
|
||||||
|
@ -379,10 +389,7 @@ export function TableNG(props: TableNGProps) {
|
||||||
const shouldOverflow = rowHeight !== 'auto' && shouldTextOverflow(field);
|
const shouldOverflow = rowHeight !== 'auto' && shouldTextOverflow(field);
|
||||||
const textWrap = rowHeight === 'auto' || shouldTextWrap(field);
|
const textWrap = rowHeight === 'auto' || shouldTextWrap(field);
|
||||||
const withTooltip = withDataLinksActionsTooltip(field, cellType);
|
const withTooltip = withDataLinksActionsTooltip(field, cellType);
|
||||||
const canBeColorized =
|
const canBeColorized = canFieldBeColorized(cellType, applyToRowBgFn);
|
||||||
cellType === TableCellDisplayMode.ColorBackground ||
|
|
||||||
cellType === TableCellDisplayMode.ColorText ||
|
|
||||||
Boolean(applyToRowBgFn);
|
|
||||||
const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow };
|
const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow };
|
||||||
|
|
||||||
result.colsWithTooltip[displayName] = withTooltip;
|
result.colsWithTooltip[displayName] = withTooltip;
|
||||||
|
@ -406,9 +413,7 @@ export function TableNG(props: TableNGProps) {
|
||||||
|
|
||||||
// generate shared styles for whole row
|
// generate shared styles for whole row
|
||||||
if (applyToRowBgFn != null) {
|
if (applyToRowBgFn != null) {
|
||||||
let { textColor, bgColor } = applyToRowBgFn(rowIdx);
|
rowCellStyle = { ...rowCellStyle, ...applyToRowBgFn(rowIdx) };
|
||||||
rowCellStyle.color = textColor;
|
|
||||||
rowCellStyle.background = bgColor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,16 +421,10 @@ export function TableNG(props: TableNGProps) {
|
||||||
|
|
||||||
if (rowCellStyle.color != null || rowCellStyle.background != null) {
|
if (rowCellStyle.color != null || rowCellStyle.background != null) {
|
||||||
style = rowCellStyle;
|
style = rowCellStyle;
|
||||||
}
|
} else if (canBeColorized) {
|
||||||
// apply background for cell types which can have a background and have proper
|
|
||||||
else if (canBeColorized) {
|
|
||||||
const value = props.row[props.column.key];
|
const value = props.row[props.column.key];
|
||||||
const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value?
|
const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value?
|
||||||
let { textColor, bgColor } = getCellColors(theme, cellOptions, displayValue);
|
style = getCellColorInlineStyles(theme, cellOptions, displayValue);
|
||||||
style = {
|
|
||||||
color: textColor,
|
|
||||||
background: bgColor,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -440,8 +439,7 @@ export function TableNG(props: TableNGProps) {
|
||||||
|
|
||||||
result.cellRootRenderers[displayName] = renderCellRoot;
|
result.cellRootRenderers[displayName] = renderCellRoot;
|
||||||
|
|
||||||
// this fires second
|
const renderBasicCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
|
||||||
const renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
|
|
||||||
const rowIdx = props.row.__index;
|
const rowIdx = props.row.__index;
|
||||||
const value = props.row[props.column.key];
|
const value = props.row[props.column.key];
|
||||||
// TODO: it would be nice to get rid of passing height down as a prop. but this value
|
// TODO: it would be nice to get rid of passing height down as a prop. but this value
|
||||||
|
@ -486,6 +484,77 @@ export function TableNG(props: TableNGProps) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// renderCellContent fires second.
|
||||||
|
let renderCellContent = renderBasicCellContent;
|
||||||
|
|
||||||
|
const tooltipFieldName = field.config.custom?.tooltip?.field;
|
||||||
|
if (tooltipFieldName) {
|
||||||
|
const tooltipField = data.fields.find(predicateByName(tooltipFieldName));
|
||||||
|
if (tooltipField) {
|
||||||
|
const tooltipDisplayName = getDisplayName(tooltipField);
|
||||||
|
const tooltipCellOptions = getCellOptions(tooltipField);
|
||||||
|
const tooltipFieldRenderer = getCellRenderer(tooltipField, tooltipCellOptions);
|
||||||
|
const tooltipCellStyleOptions = {
|
||||||
|
textAlign: getAlignment(tooltipField),
|
||||||
|
textWrap: shouldTextWrap(tooltipField),
|
||||||
|
shouldOverflow: false,
|
||||||
|
} satisfies TableCellStyleOptions;
|
||||||
|
const tooltipCanBeColorized = canFieldBeColorized(tooltipCellOptions.type, applyToRowBgFn);
|
||||||
|
const tooltipDefaultStyles = getDefaultCellStyles(theme, tooltipCellStyleOptions);
|
||||||
|
const tooltipSpecificStyles = getCellSpecificStyles(
|
||||||
|
tooltipCellOptions.type,
|
||||||
|
tooltipField,
|
||||||
|
theme,
|
||||||
|
tooltipCellStyleOptions
|
||||||
|
);
|
||||||
|
const tooltipLinkStyles = getLinkStyles(theme, tooltipCanBeColorized);
|
||||||
|
const tooltipClasses = getTooltipStyles(theme, textAlign);
|
||||||
|
|
||||||
|
const placement = field.config.custom?.tooltip?.placement ?? TableCellTooltipPlacement.Auto;
|
||||||
|
const tooltipWidth =
|
||||||
|
placement === TableCellTooltipPlacement.Left || placement === TableCellTooltipPlacement.Right
|
||||||
|
? tooltipField.config.custom?.width
|
||||||
|
: width;
|
||||||
|
|
||||||
|
const tooltipProps = {
|
||||||
|
cellOptions: tooltipCellOptions,
|
||||||
|
classes: tooltipClasses,
|
||||||
|
className: clsx(
|
||||||
|
tooltipClasses.tooltipContent,
|
||||||
|
tooltipDefaultStyles,
|
||||||
|
tooltipSpecificStyles,
|
||||||
|
tooltipLinkStyles
|
||||||
|
),
|
||||||
|
data,
|
||||||
|
disableSanitizeHtml,
|
||||||
|
field: tooltipField,
|
||||||
|
getActions: getCellActions,
|
||||||
|
gridRef,
|
||||||
|
placement,
|
||||||
|
renderer: tooltipFieldRenderer,
|
||||||
|
tooltipField,
|
||||||
|
theme,
|
||||||
|
width: tooltipWidth,
|
||||||
|
} satisfies Partial<React.ComponentProps<typeof TableCellTooltip>>;
|
||||||
|
|
||||||
|
renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
|
||||||
|
// cached so we don't care about multiple calls.
|
||||||
|
const height = rowHeightFn(props.row);
|
||||||
|
let tooltipStyle: CSSProperties | undefined;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCellTooltip {...tooltipProps} height={height} rowIdx={props.rowIdx} style={tooltipStyle}>
|
||||||
|
{renderBasicCellContent(props)}
|
||||||
|
</TableCellTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const column: TableColumn = {
|
const column: TableColumn = {
|
||||||
field,
|
field,
|
||||||
key: displayName,
|
key: displayName,
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { CSSProperties, ReactElement, useMemo, useState, useRef, useEffect, RefObject } from 'react';
|
||||||
|
import { DataGridHandle } from 'react-data-grid';
|
||||||
|
|
||||||
|
import { ActionModel, DataFrame, Field, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { TableCellTooltipPlacement } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { Popover } from '../../../Tooltip/Popover';
|
||||||
|
import { TableCellOptions } from '../../types';
|
||||||
|
import { getTooltipStyles } from '../styles';
|
||||||
|
import { TableCellRenderer, TableCellRendererProps } from '../types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
cellOptions: TableCellOptions;
|
||||||
|
children: ReactElement;
|
||||||
|
classes: ReturnType<typeof getTooltipStyles>;
|
||||||
|
className?: string;
|
||||||
|
data: DataFrame;
|
||||||
|
disableSanitizeHtml?: boolean;
|
||||||
|
field: Field;
|
||||||
|
getActions: (field: Field, rowIdx: number) => ActionModel[];
|
||||||
|
gridRef: RefObject<DataGridHandle>;
|
||||||
|
height: number;
|
||||||
|
placement?: TableCellTooltipPlacement;
|
||||||
|
renderer: TableCellRenderer;
|
||||||
|
rowIdx: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
tooltipField: Field;
|
||||||
|
theme: GrafanaTheme2;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableCellTooltip({
|
||||||
|
cellOptions,
|
||||||
|
children,
|
||||||
|
classes,
|
||||||
|
className,
|
||||||
|
data,
|
||||||
|
disableSanitizeHtml,
|
||||||
|
field,
|
||||||
|
getActions,
|
||||||
|
gridRef,
|
||||||
|
height,
|
||||||
|
placement,
|
||||||
|
renderer,
|
||||||
|
rowIdx,
|
||||||
|
style,
|
||||||
|
theme,
|
||||||
|
tooltipField,
|
||||||
|
width = 300,
|
||||||
|
}: Props) {
|
||||||
|
const rawValue = field.values[rowIdx];
|
||||||
|
const tooltipCaretRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
const [pinned, setPinned] = useState(false);
|
||||||
|
|
||||||
|
const show = hovered || pinned;
|
||||||
|
const dynamicHeight = tooltipField.config.custom?.cellOptions?.dynamicHeight;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pinned) {
|
||||||
|
const gridRoot = gridRef.current?.element;
|
||||||
|
|
||||||
|
const windowListener = (ev: Event) => {
|
||||||
|
if (ev.target === tooltipCaretRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPinned(false);
|
||||||
|
window.removeEventListener('click', windowListener);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', windowListener);
|
||||||
|
|
||||||
|
// right now, we kill the pinned tooltip on any form of scrolling to avoid awkward rendering
|
||||||
|
// where the tooltip bumps up against the edge of the scrollable container. we could try to
|
||||||
|
// kill the tooltip when it hits these boundaries rather than when scrolling starts.
|
||||||
|
const scrollListener = () => {
|
||||||
|
setPinned(false);
|
||||||
|
};
|
||||||
|
gridRoot?.addEventListener('scroll', scrollListener, { once: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', windowListener);
|
||||||
|
gridRoot?.removeEventListener('scroll', scrollListener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, [pinned, gridRef]);
|
||||||
|
|
||||||
|
const rendererProps = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
cellInspect: false,
|
||||||
|
cellOptions,
|
||||||
|
disableSanitizeHtml,
|
||||||
|
field,
|
||||||
|
frame: data,
|
||||||
|
getActions,
|
||||||
|
height,
|
||||||
|
rowIdx,
|
||||||
|
showFilters: false,
|
||||||
|
theme,
|
||||||
|
value: rawValue,
|
||||||
|
width,
|
||||||
|
}) satisfies TableCellRendererProps,
|
||||||
|
[cellOptions, data, disableSanitizeHtml, field, getActions, height, rawValue, rowIdx, theme, width]
|
||||||
|
);
|
||||||
|
|
||||||
|
const cellElement = tooltipCaretRef.current?.closest<HTMLElement>('.rdg-cell');
|
||||||
|
|
||||||
|
if (rawValue === null || rawValue === undefined) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = <>{renderer(rendererProps)}</>;
|
||||||
|
|
||||||
|
// TODO: perist the hover if you mouse out of the trigger and into the popover
|
||||||
|
const onMouseLeave = () => setHovered(false);
|
||||||
|
const onMouseEnter = () => setHovered(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cellElement && (
|
||||||
|
<Popover
|
||||||
|
content={body}
|
||||||
|
show={show}
|
||||||
|
placement={placement}
|
||||||
|
wrapperClassName={classes.tooltipWrapper}
|
||||||
|
className={className}
|
||||||
|
style={{ ...style, minWidth: width, ...(!dynamicHeight && { height }) }}
|
||||||
|
referenceElement={cellElement}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onClick={(ev) => 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 */}
|
||||||
|
<div
|
||||||
|
className={classes.tooltipCaret}
|
||||||
|
ref={tooltipCaretRef}
|
||||||
|
data-testid={selectors.components.Panels.Visualization.TableNG.Tooltip.Caret}
|
||||||
|
aria-pressed={pinned}
|
||||||
|
onClick={() => setPinned((prev) => !prev)}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onBlur={onMouseLeave}
|
||||||
|
onFocus={onMouseEnter}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
||||||
|
|
||||||
import { COLUMN, TABLE } from './constants';
|
import { COLUMN, TABLE } from './constants';
|
||||||
import { TableCellStyles } from './types';
|
import { TableCellStyles } from './types';
|
||||||
import { getJustifyContent } from './utils';
|
import { getJustifyContent, TextAlign } from './utils';
|
||||||
|
|
||||||
export const getGridStyles = (
|
export const getGridStyles = (
|
||||||
theme: GrafanaTheme2,
|
theme: GrafanaTheme2,
|
||||||
|
@ -25,6 +25,8 @@ export const getGridStyles = (
|
||||||
'--rdg-summary-border-color': borderColor,
|
'--rdg-summary-border-color': borderColor,
|
||||||
'--rdg-summary-border-width': '1px',
|
'--rdg-summary-border-width': '1px',
|
||||||
|
|
||||||
|
'--rdg-selection-color': theme.colors.info.transparent,
|
||||||
|
|
||||||
// note: this cannot have any transparency since default cells that
|
// note: this cannot have any transparency since default cells that
|
||||||
// overlay/overflow on hover inherit this background and need to occlude cells below
|
// overlay/overflow on hover inherit this background and need to occlude cells below
|
||||||
'--rdg-row-background-color': bgColor,
|
'--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),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SyntheticEvent } from 'react';
|
import { ReactNode, SyntheticEvent } from 'react';
|
||||||
import { Column } from 'react-data-grid';
|
import { Column } from 'react-data-grid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -150,6 +150,8 @@ export interface BaseTableProps {
|
||||||
/* ---------------------------- Table cell props ---------------------------- */
|
/* ---------------------------- Table cell props ---------------------------- */
|
||||||
export interface TableNGProps extends BaseTableProps {}
|
export interface TableNGProps extends BaseTableProps {}
|
||||||
|
|
||||||
|
export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
|
||||||
|
|
||||||
export interface TableCellRendererProps {
|
export interface TableCellRendererProps {
|
||||||
rowIdx: number;
|
rowIdx: number;
|
||||||
frame: DataFrame;
|
frame: DataFrame;
|
||||||
|
@ -229,12 +231,6 @@ export interface GeoCellProps {
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CellColors {
|
|
||||||
textColor?: string;
|
|
||||||
bgColor?: string;
|
|
||||||
bgHoverColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutoCellProps {
|
export interface AutoCellProps {
|
||||||
field: Field;
|
field: Field;
|
||||||
value: TableCellValue;
|
value: TableCellValue;
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
extractPixelValue,
|
extractPixelValue,
|
||||||
frameToRecords,
|
frameToRecords,
|
||||||
getAlignmentFactor,
|
getAlignmentFactor,
|
||||||
getCellColors,
|
getCellColorInlineStyles,
|
||||||
getCellLinks,
|
getCellLinks,
|
||||||
getCellOptions,
|
getCellOptions,
|
||||||
getComparator,
|
getComparator,
|
||||||
|
@ -44,6 +44,8 @@ import {
|
||||||
getDataLinksCounter,
|
getDataLinksCounter,
|
||||||
getPillLineCounter,
|
getPillLineCounter,
|
||||||
getDefaultRowHeight,
|
getDefaultRowHeight,
|
||||||
|
getDisplayName,
|
||||||
|
predicateByName,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
describe('TableNG utils', () => {
|
describe('TableNG utils', () => {
|
||||||
|
@ -110,9 +112,9 @@ describe('TableNG utils', () => {
|
||||||
|
|
||||||
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
||||||
|
|
||||||
const colors = getCellColors(theme, field, displayValue);
|
const colors = getCellColorInlineStyles(theme, field, displayValue);
|
||||||
expect(colors.bgColor).toBe('rgb(255, 0, 0)');
|
expect(colors.background).toBe('rgb(255, 0, 0)');
|
||||||
expect(colors.textColor).toBe('rgb(247, 248, 250)');
|
expect(colors.color).toBe('rgb(247, 248, 250)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle color background gradient mode', () => {
|
it('should handle color background gradient mode', () => {
|
||||||
|
@ -123,9 +125,9 @@ describe('TableNG utils', () => {
|
||||||
|
|
||||||
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
const displayValue = { text: '100', numeric: 100, color: '#ff0000' };
|
||||||
|
|
||||||
const colors = getCellColors(theme, field, displayValue);
|
const colors = getCellColorInlineStyles(theme, field, displayValue);
|
||||||
expect(colors.bgColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
|
expect(colors.background).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
|
||||||
expect(colors.textColor).toBe('rgb(247, 248, 250)');
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,7 +29,6 @@ import { TableCellOptions } from '../types';
|
||||||
import { inferPills } from './Cells/PillCell';
|
import { inferPills } from './Cells/PillCell';
|
||||||
import { COLUMN, TABLE } from './constants';
|
import { COLUMN, TABLE } from './constants';
|
||||||
import {
|
import {
|
||||||
CellColors,
|
|
||||||
TableRow,
|
TableRow,
|
||||||
ColumnTypes,
|
ColumnTypes,
|
||||||
FrameToRowsConverter,
|
FrameToRowsConverter,
|
||||||
|
@ -489,18 +488,17 @@ const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5;
|
||||||
* @internal
|
* @internal
|
||||||
* Returns the text and background colors for a table cell based on its options and display value.
|
* 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,
|
theme: GrafanaTheme2,
|
||||||
cellOptions: TableCellOptions,
|
cellOptions: TableCellOptions,
|
||||||
displayValue: DisplayValue
|
displayValue: DisplayValue
|
||||||
): CellColors {
|
): CSSProperties {
|
||||||
// How much to darken elements depends upon if we're in dark mode
|
// How much to darken elements depends upon if we're in dark mode
|
||||||
const darkeningFactor = theme.isDark ? 1 : -0.7;
|
const darkeningFactor = theme.isDark ? 1 : -0.7;
|
||||||
|
|
||||||
// Setup color variables
|
// Setup color variables
|
||||||
let textColor: string | undefined = undefined;
|
let textColor: string | undefined = undefined;
|
||||||
let bgColor: string | undefined = undefined;
|
let bgColor: string | undefined = undefined;
|
||||||
// let bgHoverColor: string | undefined = undefined;
|
|
||||||
|
|
||||||
if (cellOptions.type === TableCellDisplayMode.ColorText) {
|
if (cellOptions.type === TableCellDisplayMode.ColorText) {
|
||||||
textColor = displayValue.color;
|
textColor = displayValue.color;
|
||||||
|
@ -510,23 +508,16 @@ export function getCellColors(
|
||||||
if (mode === TableCellBackgroundDisplayMode.Basic) {
|
if (mode === TableCellBackgroundDisplayMode.Basic) {
|
||||||
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
|
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
|
||||||
bgColor = tinycolor(displayValue.color).toRgbString();
|
bgColor = tinycolor(displayValue.color).toRgbString();
|
||||||
// bgHoverColor = tinycolor(displayValue.color)
|
|
||||||
// .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
|
|
||||||
// .toRgbString();
|
|
||||||
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
|
} else if (mode === TableCellBackgroundDisplayMode.Gradient) {
|
||||||
// const hoverColor = tinycolor(displayValue.color)
|
|
||||||
// .darken(CELL_GRADIENT_DARKENING_MULTIPLIER * darkeningFactor)
|
|
||||||
// .toRgbString();
|
|
||||||
const bgColor2 = tinycolor(displayValue.color)
|
const bgColor2 = tinycolor(displayValue.color)
|
||||||
.darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
|
.darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor)
|
||||||
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES);
|
.spin(CELL_GRADIENT_HUE_ROTATION_DEGREES);
|
||||||
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
|
textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark);
|
||||||
bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`;
|
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;
|
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
|
* @internal
|
||||||
* returns only fields that are not nested tables and not explicitly hidden
|
* returns only fields that are not nested tables and not explicitly hidden
|
||||||
|
@ -874,7 +870,7 @@ export function computeColWidths(fields: Field[], availWidth: number) {
|
||||||
* @internal
|
* @internal
|
||||||
* if applyToRow is true in any field, return a function that gets the row background color
|
* 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) {
|
for (const field of fields) {
|
||||||
const cellOptions = getCellOptions(field);
|
const cellOptions = getCellOptions(field);
|
||||||
const fieldDisplay = field.display;
|
const fieldDisplay = field.display;
|
||||||
|
@ -883,7 +879,7 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
|
||||||
cellOptions.type === TableCellDisplayMode.ColorBackground &&
|
cellOptions.type === TableCellDisplayMode.ColorBackground &&
|
||||||
cellOptions.applyToRow === true
|
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 => {
|
export const displayJsonValue: DisplayProcessor = (value: unknown): DisplayValue => {
|
||||||
let displayValue: string;
|
let displayValue: string;
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
renderArrow?: boolean;
|
renderArrow?: boolean;
|
||||||
hidePopper?: () => void;
|
hidePopper?: () => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Popover({
|
export function Popover({
|
||||||
|
@ -36,6 +37,7 @@ export function Popover({
|
||||||
referenceElement,
|
referenceElement,
|
||||||
renderArrow,
|
renderArrow,
|
||||||
hidePopper,
|
hidePopper,
|
||||||
|
style: styleOverrides,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
|
@ -89,6 +91,7 @@ export function Popover({
|
||||||
style={{
|
style={{
|
||||||
...floatingStyles,
|
...floatingStyles,
|
||||||
...placementStyles,
|
...placementStyles,
|
||||||
|
...styleOverrides,
|
||||||
}}
|
}}
|
||||||
className={wrapperClassName}
|
className={wrapperClassName}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|
|
@ -9,7 +9,13 @@ import {
|
||||||
FieldConfigProperty,
|
FieldConfigProperty,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
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 { PaginationEditor } from './PaginationEditor';
|
||||||
import { TableCellOptionEditor } from './TableCellOptionEditor';
|
import { TableCellOptionEditor } from './TableCellOptionEditor';
|
||||||
|
@ -115,6 +121,46 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
|
||||||
category,
|
category,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
hideFromDefaults: true,
|
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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -12790,6 +12790,7 @@
|
||||||
"description-fields": "Select the fields that should be calculated",
|
"description-fields": "Select the fields that should be calculated",
|
||||||
"description-frozen-columns": "Columns are frozen from the left side of the table",
|
"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-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-calculation": "Calculation",
|
||||||
"name-cell-height": "Cell height",
|
"name-cell-height": "Cell height",
|
||||||
"name-cell-type": "Cell type",
|
"name-cell-type": "Cell type",
|
||||||
|
@ -12805,8 +12806,17 @@
|
||||||
"name-min-column-width": "Minimum column width",
|
"name-min-column-width": "Minimum column width",
|
||||||
"name-show-table-footer": "Show table footer",
|
"name-show-table-footer": "Show table footer",
|
||||||
"name-show-table-header": "Show table header",
|
"name-show-table-header": "Show table header",
|
||||||
|
"name-tooltip-from-field": "Tooltip from field",
|
||||||
|
"name-tooltip-placement": "Tooltip placement",
|
||||||
"placeholder-column-width": "auto",
|
"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": {
|
"tag-filter": {
|
||||||
"clear-button": "Clear tags",
|
"clear-button": "Clear tags",
|
||||||
|
|
Loading…
Reference in New Issue