TableNG: Extract links and actions tooltip into cell root renderer (#107667)
* TableNG: Extract links and actions tooltip into cell root renderer * make TS happy * fixes & tweaks * lint * skip datalinks and actions tooltip on those cell types * add todo * withTooltip lookup * fix * optional getActions * fix * kill cursor: 'context-menu' * stop event propagation from TableCellActions * update tests to move tooltip tests up to TableNG * remove safety assertion * add value back to DataLinksActionsTooltip --------- Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Paul Marbach
parent
412415ab39
commit
68ee251c5c
@@ -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',
|
||||
|
||||
@@ -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 && (
|
||||
<Portal>
|
||||
<div
|
||||
ref={refCallback}
|
||||
{...getReferenceProps()}
|
||||
{...getFloatingProps()}
|
||||
style={floatingStyles}
|
||||
className={styles.tooltipWrapper}
|
||||
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
|
||||
>
|
||||
<VizTooltipWrapper>
|
||||
<VizTooltipFooter dataLinks={links} actions={actions} />
|
||||
</VizTooltipWrapper>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
<Portal>
|
||||
<div
|
||||
ref={refCallback}
|
||||
{...getReferenceProps()}
|
||||
{...getFloatingProps()}
|
||||
style={floatingStyles}
|
||||
className={styles.tooltipWrapper}
|
||||
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
|
||||
>
|
||||
<VizTooltipWrapper>
|
||||
<VizTooltipFooter dataLinks={links} actions={actions} />
|
||||
</VizTooltipWrapper>
|
||||
</div>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.buttonsGap}>
|
||||
{actions && actions.map((action, i) => <ActionButton key={i} action={action} variant="secondary" />)}
|
||||
{actions.map((action, i) => (
|
||||
<ActionButton key={i} action={action} variant="secondary" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
<AutoCell
|
||||
value="test"
|
||||
field={field}
|
||||
justifyContent="normal"
|
||||
rowIdx={0}
|
||||
cellOptions={{ type: TableCellDisplayMode.Auto }}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<AutoCell
|
||||
value="test"
|
||||
field={field}
|
||||
justifyContent="normal"
|
||||
rowIdx={0}
|
||||
cellOptions={{ type: TableCellDisplayMode.Auto }}
|
||||
/>
|
||||
);
|
||||
|
||||
const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
|
||||
await userEvent.click(cell);
|
||||
|
||||
expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<DataLinksActionsTooltipCoords>();
|
||||
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
|
||||
<div
|
||||
className={styles.cell}
|
||||
onClick={tooltipOnClickHandler(setTooltipCoords)}
|
||||
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
data-testid={selectors.components.TablePanel.autoCell}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], formattedValue, getLinkStyle(styles, cellOptions))
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
actions={actions}
|
||||
value={formattedValue}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
formattedValue
|
||||
)}
|
||||
<div className={styles.cell}>
|
||||
{link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
const renderComponent = () => {
|
||||
return (
|
||||
<BarGauge
|
||||
width={width}
|
||||
height={height - heightOffset}
|
||||
field={config}
|
||||
display={field.display}
|
||||
text={{ valueSize: 14 }}
|
||||
value={displayValue}
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={theme}
|
||||
alignmentFactors={alignmentFactors}
|
||||
itemSpacing={1}
|
||||
lcdCellWidth={8}
|
||||
displayMode={barGaugeMode}
|
||||
valueDisplayMode={valueDisplayMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
onClick={tooltipOnClickHandler(setTooltipCoords)}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], renderComponent())
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
actions={actions}
|
||||
value={renderComponent()}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
renderComponent()
|
||||
)}
|
||||
</div>
|
||||
const barGaugeComponent = (
|
||||
<BarGauge
|
||||
width={width}
|
||||
height={height - heightOffset}
|
||||
field={config}
|
||||
display={field.display}
|
||||
text={{ valueSize: 14 }}
|
||||
value={displayValue}
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={theme}
|
||||
alignmentFactors={alignmentFactors}
|
||||
itemSpacing={1}
|
||||
lcdCellWidth={8}
|
||||
displayMode={barGaugeMode}
|
||||
valueDisplayMode={valueDisplayMode}
|
||||
/>
|
||||
);
|
||||
|
||||
const link = useSingleLink(field, rowIdx);
|
||||
|
||||
return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent);
|
||||
};
|
||||
|
||||
@@ -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<DataLinksActionsTooltipCoords>();
|
||||
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 = <img alt={alt} src={text} className={styles.image} title={title} />;
|
||||
const link = useSingleLink(field, rowIdx);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
onClick={tooltipOnClickHandler(setTooltipCoords)}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], img)
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
actions={actions}
|
||||
value={img}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
img
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.imageContainer}>{link == null ? img : renderSingleLink(link, img)}</div>;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({
|
||||
|
||||
@@ -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<DataLinksActionsTooltipCoords>();
|
||||
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
|
||||
<div
|
||||
className={styles.jsonText}
|
||||
onClick={tooltipOnClickHandler(setTooltipCoords)}
|
||||
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], displayValue)
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
actions={actions}
|
||||
value={displayValue}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
displayValue
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.jsonText}>{link == null ? displayValue : renderSingleLink(link, displayValue)}</div>;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
|
||||
|
||||
@@ -24,7 +24,10 @@ export function TableCellActions(props: TableCellActionsProps) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
// 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
|
||||
<div className={className} onClick={(ev) => ev.stopPropagation()}>
|
||||
{cellInspect && (
|
||||
<IconButton
|
||||
name="eye"
|
||||
|
||||
@@ -25,7 +25,6 @@ const GAUGE_RENDERER: TableCellRenderer = (props) => (
|
||||
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) => (
|
||||
<JSONCell
|
||||
justifyContent={props.justifyContent}
|
||||
value={props.value}
|
||||
field={props.field}
|
||||
rowIdx={props.rowIdx}
|
||||
actions={props.actions}
|
||||
/>
|
||||
<JSONCell justifyContent={props.justifyContent} value={props.value} field={props.field} rowIdx={props.rowIdx} />
|
||||
);
|
||||
|
||||
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) => <DataLinksCell field={props.field} rowIdx={props.rowIdx} />;
|
||||
|
||||
const ACTIONS_RENDERER: TableCellRenderer = (props) => <ActionsCell actions={props.actions} />;
|
||||
const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => (
|
||||
<ActionsCell field={field} rowIdx={rowIdx} getActions={getActions} />
|
||||
);
|
||||
|
||||
const PILL_RENDERER: TableCellRenderer = (props) => <PillCell {...props} />;
|
||||
|
||||
|
||||
@@ -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(<TableNG enableVirtualization={false} data={dataFrame} width={800} height={600} />);
|
||||
|
||||
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(<TableNG enableVirtualization={false} data={dataFrame} width={800} height={600} />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TableRow, TableSummaryRow>) => 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<string, CellRootRenderer>;
|
||||
colsWithTooltip: Record<string, boolean>;
|
||||
}
|
||||
|
||||
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<TableRow, TableSummaryRow>): 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 (
|
||||
<Cell
|
||||
@@ -335,14 +347,11 @@ export function TableNG(props: TableNGProps) {
|
||||
const renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): 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 && (
|
||||
<TableCellActions
|
||||
@@ -504,12 +514,10 @@ export function TableNG(props: TableNGProps) {
|
||||
expandedRows,
|
||||
filter,
|
||||
footerCalcs,
|
||||
getActions,
|
||||
hasNestedFrames,
|
||||
isCountRowsSet,
|
||||
onCellFilterAdded,
|
||||
panelContext,
|
||||
replaceVariables,
|
||||
rowHeight,
|
||||
rows,
|
||||
setFilter,
|
||||
@@ -519,6 +527,7 @@ export function TableNG(props: TableNGProps) {
|
||||
theme,
|
||||
visibleFields,
|
||||
widths,
|
||||
getCellActions,
|
||||
]);
|
||||
|
||||
// invalidate columns on every structureRev change. this supports width editing in the fieldConfig.
|
||||
@@ -534,6 +543,8 @@ export function TableNG(props: TableNGProps) {
|
||||
return cellRootRenderers[props.column.key](key, props);
|
||||
};
|
||||
|
||||
const [tooltipState, setTooltipState] = useState<DataLinksActionsTooltipState>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGrid<TableRow, TableSummaryRow>
|
||||
@@ -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) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tooltipState && (
|
||||
<DataLinksActionsTooltip
|
||||
links={tooltipState.links ?? []}
|
||||
actions={tooltipState.actions}
|
||||
coords={tooltipState.coords}
|
||||
onTooltipClose={() => setTooltipState(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isContextMenuOpen && (
|
||||
<ContextMenu
|
||||
x={contextMenuProps?.left || 0}
|
||||
@@ -770,31 +808,35 @@ const getCellStyles = (
|
||||
rowHeight: number,
|
||||
shouldWrap: boolean,
|
||||
shouldOverflow: boolean,
|
||||
hasTooltip: boolean,
|
||||
colors: CellColors
|
||||
) => ({
|
||||
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',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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<KeyValue<string>> | 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<TableFieldOptions, 'cellOptions'> & {
|
||||
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;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface TableCellProps extends CellProps<any> {
|
||||
onCellFilterAdded?: TableFilterActionCallback;
|
||||
innerWidth: number;
|
||||
frame: DataFrame;
|
||||
actions?: ActionModel[];
|
||||
actions?: ActionModel[]; // unused in NG
|
||||
setInspectCell?: TableInspectCellCallback;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = (
|
||||
<TableNG
|
||||
height={tableHeight}
|
||||
@@ -75,8 +80,7 @@ export function TablePanel(props: Props) {
|
||||
timeRange={timeRange}
|
||||
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
|
||||
fieldConfig={fieldConfig}
|
||||
getActions={getCellActions}
|
||||
replaceVariables={replaceVariables}
|
||||
getActions={_getActions}
|
||||
structureRev={data.structureRev}
|
||||
/>
|
||||
);
|
||||
@@ -159,28 +163,39 @@ const getCellActions = (
|
||||
field: Field,
|
||||
rowIndex: number,
|
||||
replaceVariables: InterpolateFunction | undefined
|
||||
) => {
|
||||
const actions: Array<ActionModel<Field>> = [];
|
||||
const actionLookup = new Set<string>();
|
||||
): Array<ActionModel<Field>> => {
|
||||
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<ActionModel<Field>> = [];
|
||||
const actionLookup = new Set<string>();
|
||||
|
||||
actions.forEach((action) => {
|
||||
const key = action.title;
|
||||
|
||||
if (!actionLookup.has(key)) {
|
||||
actionsOut.push(action);
|
||||
actionLookup.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
return actionsOut;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
return [];
|
||||
};
|
||||
|
||||
const tableStyles = {
|
||||
|
||||
Reference in New Issue
Block a user