diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts
index 0ff43b6807e..088caf6dfa1 100644
--- a/packages/grafana-e2e-selectors/src/selectors/components.ts
+++ b/packages/grafana-e2e-selectors/src/selectors/components.ts
@@ -1179,11 +1179,6 @@ export const versionedComponents = {
'12.1.0': 'data-testid Data links actions tooltip wrapper',
},
},
- TablePanel: {
- autoCell: {
- '12.1.0': 'data-testid Table panel auto cell',
- },
- },
CodeEditor: {
container: {
'10.2.3': 'data-testid Code editor container',
diff --git a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx
index 1bd8774479b..5b2e28c646f 100644
--- a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx
+++ b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx
@@ -74,8 +74,6 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt
const dismiss = useDismiss(context);
- const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length);
-
const { getFloatingProps, getReferenceProps } = useInteractions([dismiss]);
if (links.length === 0 && !Boolean(actions?.length)) {
@@ -84,23 +82,22 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt
return (
<>
+ {/* TODO: we can remove `value` from this component when tableNextGen is fully rolled out */}
{value}
- {hasMultipleLinksOrActions && (
-
-
-
-
-
-
-
- )}
+
+
+
+
+
+
+
>
);
};
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx
index 9616baa5a76..087efa47ecc 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx
@@ -1,4 +1,5 @@
import { css } from '@emotion/css';
+import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@@ -6,12 +7,16 @@ import { useStyles2 } from '../../../../themes/ThemeContext';
import { ActionButton } from '../../../Actions/ActionButton';
import { ActionCellProps } from '../types';
-export const ActionsCell = ({ actions }: ActionCellProps) => {
+export const ActionsCell = ({ field, rowIdx, getActions }: ActionCellProps) => {
const styles = useStyles2(getStyles);
+ const actions = useMemo(() => getActions(field, rowIdx), [getActions, field, rowIdx]);
+
return (
- {actions && actions.map((action, i) =>
)}
+ {actions.map((action, i) => (
+
+ ))}
);
};
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx
deleted file mode 100644
index b65fa0e0297..00000000000
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-import { Field, FieldType, LinkModel } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
-
-import { TableCellDisplayMode } from '../../types';
-
-import AutoCell from './AutoCell';
-
-describe('AutoCell', () => {
- describe('Displays data Links', () => {
- const getFieldWithLinks = (links: LinkModel[]): Field => {
- return {
- name: 'Category',
- type: FieldType.string,
- values: ['A', 'B', 'A', 'B', 'A'],
- config: {
- custom: {
- cellOptions: {
- type: TableCellDisplayMode.Auto,
- wrapText: false,
- },
- },
- },
- display: (value: unknown) => ({
- text: String(value),
- numeric: 0,
- color: undefined,
- prefix: undefined,
- suffix: undefined,
- }),
- state: {},
- getLinks: () => links,
- };
- };
-
- it('shows multiple datalinks in the tooltip', async () => {
- const linksForField = [
- { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
- { href: 'http://asdasd2.com', title: 'Test Title2' } as LinkModel,
- ];
-
- jest.mock('../utils', () => ({
- getCellLinks: () => linksForField,
- }));
-
- const field = getFieldWithLinks(linksForField);
-
- render(
-
- );
-
- const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
- await userEvent.click(cell);
-
- const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper);
- expect(tooltip).toBeInTheDocument();
- expect(screen.getByText('Test Title')).toBeInTheDocument();
- expect(screen.getByText('Test Title2')).toBeInTheDocument();
- });
-
- it('does not show tooltip for multiple links if one is invalid', async () => {
- const linksForField = [
- { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
- { title: 'Test Title2' } as LinkModel,
- ];
-
- jest.mock('../utils', () => ({
- getCellLinks: () => linksForField,
- }));
-
- const field = getFieldWithLinks(linksForField);
-
- render(
-
- );
-
- const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
- await userEvent.click(cell);
-
- expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
- });
- });
-});
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 3fdcfc85491..71293bbc06e 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
@@ -1,49 +1,24 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
-import { useState } from 'react';
import { GrafanaTheme2, formattedValueToString } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../../../themes/ThemeContext';
-import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
+import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellOptions, TableCellDisplayMode } from '../../types';
-import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils';
+import { useSingleLink } from '../hooks';
import { AutoCellProps } from '../types';
-import { getCellLinks } from '../utils';
-export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions, actions }: AutoCellProps) {
+export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions }: AutoCellProps) {
const styles = useStyles2(getStyles, justifyContent);
const displayValue = field.display!(value);
const formattedValue = formattedValueToString(displayValue);
- const links = getCellLinks(field, rowIdx) || [];
-
- const [tooltipCoords, setTooltipCoords] = useState();
- const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
- const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
+ const link = useSingleLink(field, rowIdx);
return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-
- {shouldShowLink ? (
- renderSingleLink(links[0], formattedValue, getLinkStyle(styles, cellOptions))
- ) : shouldShowTooltip ? (
-
setTooltipCoords(undefined)}
- />
- ) : (
- formattedValue
- )}
+
+ {link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))}
);
}
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx
index 710a8c65726..69ebfac671c 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx
@@ -1,13 +1,11 @@
-import { useState } from 'react';
-
import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data';
import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema';
import { BarGauge } from '../../../BarGauge/BarGauge';
-import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
-import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
+import { renderSingleLink } from '../../DataLinksActionsTooltip';
+import { useSingleLink } from '../hooks';
import { BarGaugeCellProps } from '../types';
-import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils';
+import { extractPixelValue, getCellOptions, getAlignmentFactor } from '../utils';
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
@@ -23,7 +21,7 @@ const defaultScale: ThresholdsConfig = {
],
};
-export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actions }: BarGaugeCellProps) => {
+export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => {
const displayValue = field.display!(value);
const cellOptions = getCellOptions(field);
const heightOffset = extractPixelValue(theme.spacing(1));
@@ -48,51 +46,26 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actio
}
const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!);
- const links = getCellLinks(field, rowIdx) || [];
- const [tooltipCoords, setTooltipCoords] = useState();
- const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
- const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
-
- const renderComponent = () => {
- return (
-
- );
- };
-
- return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-
- {shouldShowLink ? (
- renderSingleLink(links[0], renderComponent())
- ) : shouldShowTooltip ? (
- setTooltipCoords(undefined)}
- />
- ) : (
- renderComponent()
- )}
-
+ const barGaugeComponent = (
+
);
+
+ const link = useSingleLink(field, rowIdx);
+
+ return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent);
};
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx
index bc62e266b12..e0c1050396a 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx
@@ -1,55 +1,28 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
-import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
-import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
+import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellDisplayMode } from '../../types';
-import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils';
+import { useSingleLink } from '../hooks';
import { ImageCellProps } from '../types';
-import { getCellLinks } from '../utils';
const DATALINKS_HEIGHT_OFFSET = 10;
-export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx, actions }: ImageCellProps) => {
+export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => {
const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET;
const styles = useStyles2(getStyles, calculatedHeight, justifyContent);
- const links = getCellLinks(field, rowIdx) || [];
-
- const [tooltipCoords, setTooltipCoords] = useState();
- const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
- const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
const { text } = field.display!(value);
const { alt, title } =
cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { alt: undefined, title: undefined };
const img =
;
+ const link = useSingleLink(field, rowIdx);
- return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-
- {shouldShowLink ? (
- renderSingleLink(links[0], img)
- ) : shouldShowTooltip ? (
- setTooltipCoords(undefined)}
- />
- ) : (
- img
- )}
-
- );
+ return {link == null ? img : renderSingleLink(link, img)}
;
};
const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx
index 2506255cb03..dc195cbcd67 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx
@@ -1,16 +1,14 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
-import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
-import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
-import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
+import { renderSingleLink } from '../../DataLinksActionsTooltip';
+import { useSingleLink } from '../hooks';
import { JSONCellProps } from '../types';
-import { getCellLinks } from '../utils';
-export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSONCellProps) => {
+export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => {
const styles = useStyles2(getStyles, justifyContent);
let displayValue = value;
@@ -33,34 +31,9 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSON
}
}
- const links = getCellLinks(field, rowIdx) || [];
+ const link = useSingleLink(field, rowIdx);
- const [tooltipCoords, setTooltipCoords] = useState();
- const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
- const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
-
- return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-
- {shouldShowLink ? (
- renderSingleLink(links[0], displayValue)
- ) : shouldShowTooltip ? (
- setTooltipCoords(undefined)}
- />
- ) : (
- displayValue
- )}
-
- );
+ return {link == null ? displayValue : renderSingleLink(link, displayValue)}
;
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx
index eb67a369cb2..558d1c7978d 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx
@@ -24,7 +24,10 @@ export function TableCellActions(props: TableCellActionsProps) {
} = props;
return (
-
+ // stopping propagation to prevent clicks within the actions menu from triggering the cell click events
+ // for things like the data links tooltip.
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+
ev.stopPropagation()}>
{cellInspect && (
(
height={props.height}
width={props.width}
rowIdx={props.rowIdx}
- actions={props.actions}
/>
);
@@ -36,7 +35,6 @@ const AUTO_RENDERER: TableCellRenderer = (props) => (
justifyContent={props.justifyContent}
rowIdx={props.rowIdx}
cellOptions={props.cellOptions}
- actions={props.actions}
/>
);
@@ -53,13 +51,7 @@ const SPARKLINE_RENDERER: TableCellRenderer = (props) => (
);
const JSON_RENDERER: TableCellRenderer = (props) => (
-
+
);
const GEO_RENDERER: TableCellRenderer = (props) => (
@@ -74,13 +66,14 @@ const IMAGE_RENDERER: TableCellRenderer = (props) => (
justifyContent={props.justifyContent}
value={props.value}
rowIdx={props.rowIdx}
- actions={props.actions}
/>
);
const DATA_LINKS_RENDERER: TableCellRenderer = (props) => ;
-const ACTIONS_RENDERER: TableCellRenderer = (props) => ;
+const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => (
+
+);
const PILL_RENDERER: TableCellRenderer = (props) => ;
diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx
index dd1d4364128..9dfe1a2719b 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx
@@ -1,7 +1,17 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { applyFieldOverrides, createTheme, DataFrame, EventBus, FieldType, toDataFrame } from '@grafana/data';
+import {
+ applyFieldOverrides,
+ createTheme,
+ DataFrame,
+ DataLink,
+ EventBus,
+ FieldType,
+ LinkModel,
+ toDataFrame,
+} from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
import { TableCellBackgroundDisplayMode } from '@grafana/schema';
import { PanelContext, PanelContextProvider } from '../../../components/PanelChrome';
@@ -1682,4 +1692,55 @@ describe('TableNG', () => {
expect(mockEventBus.publish).not.toHaveBeenCalled();
});
});
+
+ describe('Displays data Links', () => {
+ function toLinkModel(link: DataLink): LinkModel {
+ return {
+ href: link.url,
+ title: link.title,
+ target: link.targetBlank ? '_blank' : '_self',
+ origin: link.origin || 'panel',
+ };
+ }
+
+ it('shows multiple datalinks in the tooltip', async () => {
+ const dataFrame = createBasicDataFrame();
+ const links: DataLink[] = [
+ { url: 'http://asdasd.com', title: 'Test Title' },
+ { url: 'http://asdasd2.com', title: 'Test Title2' },
+ ];
+
+ dataFrame.fields[0].config.links = links;
+ dataFrame.fields[0].getLinks = () => links.map(toLinkModel);
+
+ render();
+
+ const cell = screen.getByText('A1');
+ await userEvent.click(cell);
+
+ const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper);
+ expect(tooltip).toBeInTheDocument();
+
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
+ expect(screen.getByText('Test Title2')).toBeInTheDocument();
+ });
+
+ it('does not show tooltip for a single link', async () => {
+ const dataFrame = createBasicDataFrame();
+
+ const links: DataLink[] = [{ url: 'http://asdasd.com', title: 'Test Title' }];
+
+ dataFrame.fields[0].config.links = links;
+ dataFrame.fields[0].getLinks = () => links.map(toLinkModel);
+
+ render();
+
+ const cell = screen.getByText('A1');
+
+ // we need to click the parent since the cell itself is a link.
+ await userEvent.click(cell.parentElement!);
+
+ expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
index 5a0a2fdc036..aa35b99ae9c 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
@@ -1,7 +1,7 @@
import 'react-data-grid/lib/styles.css';
import { css, cx } from '@emotion/css';
import { Property } from 'csstype';
-import { Key, ReactNode, useLayoutEffect, useMemo, useState } from 'react';
+import { Key, ReactNode, useCallback, useLayoutEffect, useMemo, useState } from 'react';
import {
Cell,
CellRendererProps,
@@ -22,8 +22,10 @@ import { ContextMenu } from '../../ContextMenu/ContextMenu';
import { MenuItem } from '../../Menu/MenuItem';
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 { DataLinksActionsTooltipState } from '../utils';
import { HeaderCell } from './Cells/HeaderCell';
import { RowExpander } from './Cells/RowExpander';
@@ -56,6 +58,8 @@ import {
getCellOptions,
shouldTextWrap,
isCellInspectEnabled,
+ getCellLinks,
+ withDataLinksActionsTooltip,
} from './utils';
type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode;
@@ -68,14 +72,13 @@ export function TableNG(props: TableNGProps) {
enableSharedCrosshair = false,
enableVirtualization,
footerOptions,
- getActions,
+ getActions = () => [],
height,
initialSortBy,
noHeader,
onCellFilterAdded,
onColumnResize,
onSortByChange,
- replaceVariables,
showTypeIcons,
structureRev,
width,
@@ -88,6 +91,11 @@ export function TableNG(props: TableNGProps) {
});
const panelContext = usePanelContext();
+ const getCellActions = useCallback(
+ (field: Field, rowIdx: number) => getActions(data, field, rowIdx),
+ [getActions, data]
+ );
+
const hasHeader = !noHeader;
const hasFooter = Boolean(footerOptions?.show && footerOptions.reducer?.length);
const isCountRowsSet = Boolean(
@@ -256,13 +264,15 @@ export function TableNG(props: TableNGProps) {
interface Schema {
columns: TableColumn[];
cellRootRenderers: Record;
+ colsWithTooltip: Record;
}
- const { columns, cellRootRenderers } = useMemo(() => {
+ const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => {
const fromFields = (f: Field[], widths: number[]) => {
const result: Schema = {
columns: [],
cellRootRenderers: {},
+ colsWithTooltip: {},
};
let lastRowIdx = -1;
@@ -280,7 +290,6 @@ export function TableNG(props: TableNGProps) {
const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null);
const showActions = cellInspect || showFilters;
const width = widths[i];
- const frame = data;
// helps us avoid string cx and emotion per-cell
const cellActionClassName = showActions
@@ -294,6 +303,9 @@ export function TableNG(props: TableNGProps) {
const cellType = cellOptions.type;
const shouldOverflow = shouldTextOverflow(field);
const shouldWrap = shouldTextWrap(field);
+ const withTooltip = withDataLinksActionsTooltip(field, cellType);
+
+ result.colsWithTooltip[displayName] = withTooltip;
// this fires first
const renderCellRoot = (key: Key, props: CellRendererProps): ReactNode => {
@@ -317,7 +329,7 @@ export function TableNG(props: TableNGProps) {
colors = {};
}
- const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, colors);
+ const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, withTooltip, colors);
return (
): JSX.Element => {
const rowIdx = props.row.__index;
const value = props.row[props.column.key];
-
- // TODO: defer until click?
- const actions = getActions?.(frame, field, props.row.__index, replaceVariables);
+ const frame = data;
return (
<>
{renderFieldCell({
- actions,
cellOptions,
frame,
field,
@@ -354,6 +363,7 @@ export function TableNG(props: TableNGProps) {
width,
cellInspect,
showFilters,
+ getActions: getCellActions,
})}
{showActions && (
();
+
return (
<>
@@ -541,6 +552,24 @@ export function TableNG(props: TableNGProps) {
className={styles.grid}
columns={structureRevColumns}
rows={paginatedRows}
+ onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => {
+ // Note: could be column.field; JS says yes, but TS says no!
+ const field = columns[column.idx].field;
+
+ if (colsWithTooltip[getDisplayName(field)]) {
+ const rowIdx = row.__index;
+ setTooltipState({
+ coords: {
+ clientX,
+ clientY,
+ },
+ links: getCellLinks(field, rowIdx),
+ actions: getCellActions(field, rowIdx),
+ });
+
+ preventGridDefault();
+ }
+ }}
onCellKeyDown={
hasNestedFrames
? (_, event) => {
@@ -577,6 +606,15 @@ export function TableNG(props: TableNGProps) {
|
)}
+ {tooltipState && (
+
setTooltipState(undefined)}
+ />
+ )}
+
{isContextMenuOpen && (
({
- 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' }),
- '&:last-child': {
- borderInlineEnd: 'none',
- },
- '&:hover': {
- background: colors.bgHoverColor,
- '.table-cell-actions': {
- display: 'flex',
+) => {
+ 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',
},
- ...(shouldOverflow && {
- zIndex: theme.zIndex.tooltip - 2,
- whiteSpace: 'pre-line',
- height: 'fit-content',
- minWidth: 'fit-content',
- }),
- },
- }),
-});
+ '&:hover': {
+ background: colors.bgHoverColor,
+ '.table-cell-actions': {
+ display: 'flex',
+ },
+ ...(shouldOverflow && {
+ zIndex: theme.zIndex.tooltip - 2,
+ whiteSpace: 'pre-line',
+ height: 'fit-content',
+ minWidth: 'fit-content',
+ }),
+ },
+ }),
+ };
+};
diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts
index 1540d1e0383..33c89162939 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts
@@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect } fr
import { Column, DataGridProps, SortColumn } from 'react-data-grid';
import { varPreLine } from 'uwrap';
-import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data';
+import { Field, fieldReducers, FieldType, formattedValueToString, LinkModel, reduceField } from '@grafana/data';
import { useTheme2 } from '../../../themes/ThemeContext';
import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types';
@@ -17,6 +17,7 @@ import {
getColumnTypes,
GetMaxWrapCellOptions,
getMaxWrapCell,
+ getCellLinks,
} from './utils';
// Helper function to get displayed value
@@ -597,3 +598,10 @@ export function useColumnResize(
return dataGridResizeHandler;
}
+
+export function useSingleLink(field: Field, rowIdx: number): LinkModel | undefined {
+ const linksCount = field.config.links?.length ?? 0;
+ const actionsCount = field.config.actions?.length ?? 0;
+ const shouldShowLink = linksCount === 1 && actionsCount === 0;
+ return useMemo(() => (shouldShowLink ? (getCellLinks(field, rowIdx) ?? []) : [])[0], [field, shouldShowLink, rowIdx]);
+}
diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts
index 2946923db8f..04caa62c85c 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/types.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts
@@ -10,7 +10,6 @@ import {
TimeRange,
FieldConfigSource,
ActionModel,
- InterpolateFunction,
FieldType,
DataFrameWithValue,
SelectableValue,
@@ -30,12 +29,9 @@ export type TableColumnResizeActionCallback = (fieldDisplayName: string, width:
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
export type FooterItem = Array> | string | undefined;
-export type GetActionsFunction = (
- frame: DataFrame,
- field: Field,
- rowIndex: number,
- replaceVariables?: InterpolateFunction
-) => ActionModel[];
+export type GetActionsFunction = (frame: DataFrame, field: Field, rowIndex: number) => ActionModel[];
+
+export type GetActionsFunctionLocal = (field: Field, rowIndex: number) => ActionModel[];
export type TableFieldOptionsType = Omit & {
cellOptions: TableCellOptions;
@@ -142,7 +138,6 @@ export interface BaseTableProps {
initialRowIndex?: number;
fieldConfig?: FieldConfigSource;
getActions?: GetActionsFunction;
- replaceVariables?: InterpolateFunction;
// Used solely for testing as RTL can't correctly render the table otherwise
enableVirtualization?: boolean;
}
@@ -151,7 +146,6 @@ export interface BaseTableProps {
export interface TableNGProps extends BaseTableProps {}
export interface TableCellRendererProps {
- actions?: ActionModel[];
rowIdx: number;
frame: DataFrame;
timeRange?: TimeRange;
@@ -165,6 +159,7 @@ export interface TableCellRendererProps {
cellInspect: boolean;
showFilters: boolean;
justifyContent: Property.JustifyContent;
+ getActions?: GetActionsFunctionLocal;
}
export type ContextMenuProps = {
@@ -205,7 +200,7 @@ export interface SparklineCellProps {
width: number;
}
-export interface BarGaugeCellProps extends ActionCellProps {
+export interface BarGaugeCellProps {
field: Field;
height: number;
rowIdx: number;
@@ -214,7 +209,7 @@ export interface BarGaugeCellProps extends ActionCellProps {
width: number;
}
-export interface ImageCellProps extends ActionCellProps {
+export interface ImageCellProps {
cellOptions: TableCellOptions;
field: Field;
height: number;
@@ -223,7 +218,7 @@ export interface ImageCellProps extends ActionCellProps {
rowIdx: number;
}
-export interface JSONCellProps extends ActionCellProps {
+export interface JSONCellProps {
justifyContent: Property.JustifyContent;
value: TableCellValue;
field: Field;
@@ -241,24 +236,26 @@ export interface GeoCellProps {
height: number;
}
-export interface ActionCellProps {
- actions?: ActionModel[];
-}
-
export interface CellColors {
textColor?: string;
bgColor?: string;
bgHoverColor?: string;
}
-export interface AutoCellProps extends ActionCellProps {
- value: TableCellValue;
+export interface AutoCellProps {
field: Field;
+ value: TableCellValue;
justifyContent: Property.JustifyContent;
rowIdx: number;
cellOptions: TableCellOptions;
}
+export interface ActionCellProps {
+ field: Field;
+ rowIdx: number;
+ getActions: GetActionsFunctionLocal;
+}
+
// Comparator for sorting table values
export type Comparator = (a: TableCellValue, b: TableCellValue) => number;
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
index 78f5442f234..90004837874 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
@@ -172,6 +172,7 @@ const DEFAULT_CELL_OPTIONS = { type: TableCellDisplayMode.Auto } as const;
/**
* @internal
* Returns the cell options for a field, migrating from legacy displayMode if necessary.
+ * TODO: remove live migration in favor of doing it in dashboard or panel migrator
*/
export function getCellOptions(field: Field): TableCellOptions {
if (field.config.custom?.displayMode) {
@@ -613,3 +614,12 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
}
}
}
+
+/** @internal */
+export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDisplayMode) {
+ return (
+ cellType !== TableCellDisplayMode.DataLinks &&
+ cellType !== TableCellDisplayMode.Actions &&
+ (field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1
+ );
+}
diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts
index e2c5b5b8099..1d029ae7099 100644
--- a/packages/grafana-ui/src/components/Table/types.ts
+++ b/packages/grafana-ui/src/components/Table/types.ts
@@ -57,7 +57,7 @@ export interface TableCellProps extends CellProps {
onCellFilterAdded?: TableFilterActionCallback;
innerWidth: number;
frame: DataFrame;
- actions?: ActionModel[];
+ actions?: ActionModel[]; // unused in NG
setInspectCell?: TableInspectCellCallback;
}
diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts
index 16f5ccac9d2..77dc0cec078 100644
--- a/packages/grafana-ui/src/components/Table/utils.ts
+++ b/packages/grafana-ui/src/components/Table/utils.ts
@@ -766,10 +766,16 @@ export function guessLongestField(fieldConfig: FieldConfigSource, data: DataFram
return longestField;
}
-export type DataLinksActionsTooltipCoords = {
+export interface DataLinksActionsTooltipState {
+ coords: DataLinksActionsTooltipCoords;
+ links?: LinkModel[];
+ actions?: ActionModel[];
+}
+
+export interface DataLinksActionsTooltipCoords {
clientX: number;
clientY: number;
-};
+}
export const getDataLinksActionsTooltipUtils = (links: LinkModel[], actions?: ActionModel[]) => {
const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length);
diff --git a/public/app/plugins/panel/table/table-new/TablePanel.tsx b/public/app/plugins/panel/table/table-new/TablePanel.tsx
index 903a74f65c3..d5bf34dd869 100644
--- a/public/app/plugins/panel/table/table-new/TablePanel.tsx
+++ b/public/app/plugins/panel/table/table-new/TablePanel.tsx
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
-import { useMemo } from 'react';
+import { useCallback, useMemo } from 'react';
import {
ActionModel,
@@ -57,6 +57,11 @@ export function TablePanel(props: Props) {
const enableSharedCrosshair = panelContext.sync && panelContext.sync() !== DashboardCursorSync.Off;
+ const _getActions = useCallback(
+ (frame: DataFrame, field: Field, rowIndex: number) => getCellActions(frame, field, rowIndex, replaceVariables),
+ [replaceVariables]
+ );
+
const tableElement = (
);
@@ -159,28 +163,39 @@ const getCellActions = (
field: Field,
rowIndex: number,
replaceVariables: InterpolateFunction | undefined
-) => {
- const actions: Array> = [];
- const actionLookup = new Set();
+): Array> => {
+ const numActions = field.config.actions?.length ?? 0;
- const actionsModel = getActions(
- dataFrame,
- field,
- field.state!.scopedVars!,
- replaceVariables ?? replaceVars,
- field.config.actions ?? [],
- { valueRowIndex: rowIndex }
- );
+ if (numActions > 0) {
+ const actions = getActions(
+ dataFrame,
+ field,
+ field.state!.scopedVars!,
+ replaceVariables ?? replaceVars,
+ field.config.actions ?? [],
+ { valueRowIndex: rowIndex }
+ );
- actionsModel.forEach((action) => {
- const key = `${action.title}`;
- if (!actionLookup.has(key)) {
- actions.push(action);
- actionLookup.add(key);
+ if (actions.length === 1) {
+ return actions;
+ } else {
+ const actionsOut: Array> = [];
+ const actionLookup = new Set();
+
+ actions.forEach((action) => {
+ const key = action.title;
+
+ if (!actionLookup.has(key)) {
+ actionsOut.push(action);
+ actionLookup.add(key);
+ }
+ });
+
+ return actionsOut;
}
- });
+ }
- return actions;
+ return [];
};
const tableStyles = {