TableNG: Data links and actions tooltip (#101015)
This commit is contained in:
@@ -1141,6 +1141,16 @@ export const versionedComponents = {
|
||||
[MIN_GRAFANA_VERSION]: 'Data link',
|
||||
},
|
||||
},
|
||||
DataLinksActionsTooltip: {
|
||||
tooltipWrapper: {
|
||||
'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',
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { isFunction } from 'lodash';
|
||||
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 { DataLinksContextMenu, DataLinksContextMenuApi } from '../../DataLinks/DataLinksContextMenu';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../DataLinksActionsTooltip';
|
||||
import { TableCellProps } from '../types';
|
||||
import { getAlignmentFactor, getCellOptions } from '../utils';
|
||||
import {
|
||||
DataLinksActionsTooltipCoords,
|
||||
getAlignmentFactor,
|
||||
getCellOptions,
|
||||
getDataLinksActionsTooltipUtils,
|
||||
} from '../utils';
|
||||
|
||||
const defaultScale: ThresholdsConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
@@ -54,12 +60,9 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
return field.getLinks({ valueRowIndex: row.index });
|
||||
};
|
||||
|
||||
const hasLinks = Boolean(getLinks().length);
|
||||
const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index);
|
||||
|
||||
const renderComponent = (menuProps: DataLinksContextMenuApi) => {
|
||||
const { openMenu, targetClassName } = menuProps;
|
||||
|
||||
const renderComponent = () => {
|
||||
return (
|
||||
<BarGauge
|
||||
width={innerWidth}
|
||||
@@ -71,8 +74,6 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={tableStyles.theme}
|
||||
alignmentFactors={alignmentFactors}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
itemSpacing={1}
|
||||
lcdCellWidth={8}
|
||||
displayMode={barGaugeMode}
|
||||
@@ -81,14 +82,33 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(getLinks());
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
const links = getLinks();
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={getLinks} style={{ display: 'flex', width: '100%' }}>
|
||||
{(api) => renderComponent(api)}
|
||||
</DataLinksContextMenu>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
{...cellProps}
|
||||
className={tableStyles.cellContainer}
|
||||
style={{ ...cellProps.style, cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
onClick={({ clientX, clientY }) => {
|
||||
setTooltipCoords({ clientX, clientY });
|
||||
}}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], renderComponent())
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
value={renderComponent()}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
renderComponent({})
|
||||
renderComponent()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactElement, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DisplayValue, formattedValueToString } from '@grafana/data';
|
||||
import { TableCellDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { getCellLinks } from '../../../utils';
|
||||
import { clearLinkButtonStyles } from '../../Button';
|
||||
import { DataLinksContextMenu } from '../../DataLinks/DataLinksContextMenu';
|
||||
import { CellActions } from '../CellActions';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../DataLinksActionsTooltip';
|
||||
import { TableCellInspectorMode } from '../TableCellInspector';
|
||||
import { TableStyles } from '../TableRT/styles';
|
||||
import { TableCellProps, CustomCellRendererProps, TableCellOptions } from '../types';
|
||||
import { getCellColors, getCellOptions } from '../utils';
|
||||
import {
|
||||
DataLinksActionsTooltipCoords,
|
||||
getCellColors,
|
||||
getCellOptions,
|
||||
getDataLinksActionsTooltipUtils,
|
||||
} from '../utils';
|
||||
|
||||
export const DefaultCell = (props: TableCellProps) => {
|
||||
const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props;
|
||||
@@ -23,9 +25,6 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
const showFilters = props.onCellFilterAdded && field.config.filterable;
|
||||
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
|
||||
const cellOptions = getCellOptions(field);
|
||||
const cellLinks = getCellLinks(field, row);
|
||||
const hasLinks = cellLinks?.some((link) => link.href || link.onClick != null);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
let value: string | ReactElement;
|
||||
|
||||
const OG_TWEET_LENGTH = 140; // 🙏
|
||||
@@ -76,28 +75,32 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
}
|
||||
|
||||
const { key, ...rest } = cellProps;
|
||||
const links = getCellLinks(field, row) || [];
|
||||
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
return (
|
||||
<div key={key} {...rest} className={cellStyle}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu
|
||||
links={() => getCellLinks(field, row)?.filter((link) => link.href || link.onClick != null) || []}
|
||||
>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
<button
|
||||
className={cx(clearButtonStyle, getLinkStyle(tableStyles, cellOptions, api.targetClassName))}
|
||||
onClick={api.openMenu}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return <div className={getLinkStyle(tableStyles, cellOptions, api.targetClassName)}>{value}</div>;
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
key={key}
|
||||
{...rest}
|
||||
className={cellStyle}
|
||||
style={{ ...cellProps.style, cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
onClick={({ clientX, clientY }) => {
|
||||
setTooltipCoords({ clientX, clientY });
|
||||
}}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], value, getLinkStyle(tableStyles, cellOptions))
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
value={value}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : isStringValue ? (
|
||||
`${value}`
|
||||
) : (
|
||||
@@ -109,6 +112,14 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getLinkStyle = (tableStyles: TableStyles, cellOptions: TableCellOptions) => {
|
||||
if (cellOptions.type === TableCellDisplayMode.Auto) {
|
||||
return tableStyles.cellLink;
|
||||
}
|
||||
|
||||
return tableStyles.cellLinkForColoredCell;
|
||||
};
|
||||
|
||||
function getCellStyle(
|
||||
tableStyles: TableStyles,
|
||||
cellOptions: TableCellOptions,
|
||||
@@ -131,7 +142,7 @@ function getCellStyle(
|
||||
bgColor = colors.bgColor;
|
||||
bgHoverColor = colors.bgHoverColor;
|
||||
|
||||
// If we have definied colors return those styles
|
||||
// If we have defined colors return those styles
|
||||
// Otherwise we return default styles
|
||||
return tableStyles.buildCellContainerStyle(
|
||||
textColor,
|
||||
@@ -145,11 +156,3 @@ function getCellStyle(
|
||||
rowExpanded
|
||||
);
|
||||
}
|
||||
|
||||
function getLinkStyle(tableStyles: TableStyles, cellOptions: TableCellOptions, targetClassName: string | undefined) {
|
||||
if (cellOptions.type === TableCellDisplayMode.Auto) {
|
||||
return cx(tableStyles.cellLink, targetClassName);
|
||||
}
|
||||
|
||||
return cx(tableStyles.cellLinkForColoredCell, targetClassName);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { getCellLinks } from '../../../utils';
|
||||
import { DataLinksContextMenu } from '../../DataLinks/DataLinksContextMenu';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../DataLinksActionsTooltip';
|
||||
import { TableCellDisplayMode, TableCellProps } from '../types';
|
||||
import { getCellOptions } from '../utils';
|
||||
import { DataLinksActionsTooltipCoords, getCellOptions, getDataLinksActionsTooltipUtils } from '../utils';
|
||||
|
||||
const DATALINKS_HEIGHT_OFFSET = 10;
|
||||
|
||||
@@ -13,7 +13,12 @@ export const ImageCell = (props: TableCellProps) => {
|
||||
const { title, alt } =
|
||||
cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined };
|
||||
const displayValue = field.display!(cell.value);
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
|
||||
const links = getCellLinks(field, row) || [];
|
||||
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
// The image element
|
||||
const img = (
|
||||
@@ -27,36 +32,26 @@ export const ImageCell = (props: TableCellProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
{...cellProps}
|
||||
className={tableStyles.cellContainer}
|
||||
style={{ ...cellProps.style, cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
|
||||
onClick={({ clientX, clientY }) => {
|
||||
setTooltipCoords({ clientX, clientY });
|
||||
}}
|
||||
>
|
||||
{/* If there are data links/actions, we render them with image */}
|
||||
{/* Otherwise we simply render the image */}
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu
|
||||
style={{ height: tableStyles.cellHeight - DATALINKS_HEIGHT_OFFSET, width: 'auto' }}
|
||||
links={() => getCellLinks(field, row) || []}
|
||||
>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
<div
|
||||
onClick={api.openMenu}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && api.openMenu) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
||||
api.openMenu(e as any);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{img}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return img;
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], img)
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
value={img}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
img
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { isString } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { getCellLinks } from '../../../utils';
|
||||
import { Button, clearLinkButtonStyles } from '../../Button';
|
||||
import { DataLinksContextMenu } from '../../DataLinks/DataLinksContextMenu';
|
||||
import { CellActions } from '../CellActions';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../DataLinksActionsTooltip';
|
||||
import { TableCellInspectorMode } from '../TableCellInspector';
|
||||
import { TableCellProps } from '../types';
|
||||
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../utils';
|
||||
|
||||
export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
const { cell, tableStyles, cellProps, field, row } = props;
|
||||
@@ -28,26 +28,28 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
displayValue = JSON.stringify(value, null, ' ');
|
||||
}
|
||||
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
const links = getCellLinks(field, row) || [];
|
||||
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={inspectEnabled ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer}>
|
||||
<div className={cx(tableStyles.cellText, txt)}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
<Button className={cx(clearButtonStyle)} onClick={api.openMenu}>
|
||||
{displayValue}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return <>{displayValue}</>;
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={cx(tableStyles.cellText, txt)}
|
||||
onClick={({ clientX, clientY }) => setTooltipCoords({ clientX, clientY })}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], displayValue)
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
value={displayValue}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<div className={tableStyles.cellText}>{displayValue}</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { LinkModel, ActionModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { DataLinksActionsTooltip } from './DataLinksActionsTooltip';
|
||||
|
||||
describe('DataLinksActionsTooltip', () => {
|
||||
const mockCoords = { clientX: 100, clientY: 100 };
|
||||
const mockLink: LinkModel = {
|
||||
href: 'http://link1.com',
|
||||
title: 'Data Link1',
|
||||
target: '_blank',
|
||||
onClick: jest.fn(),
|
||||
origin: { ref: { uid: 'test' } },
|
||||
};
|
||||
|
||||
const mockAction: ActionModel = {
|
||||
title: 'Action1',
|
||||
onClick: jest.fn(),
|
||||
style: { backgroundColor: '#ff0000' },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not render when there is only one link', () => {
|
||||
const { container } = render(<DataLinksActionsTooltip links={[mockLink]} actions={[]} coords={mockCoords} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render tooltip with multiple links', async () => {
|
||||
const multipleLinks = [mockLink, { ...mockLink, title: 'Data Link2', href: 'http://link2.com' }];
|
||||
render(<DataLinksActionsTooltip links={multipleLinks} coords={mockCoords} />);
|
||||
|
||||
expect(screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).toBeInTheDocument();
|
||||
expect(await screen.findByText('Data Link1')).toBeInTheDocument();
|
||||
|
||||
const link = screen.getByText('Data Link1');
|
||||
await userEvent.click(link);
|
||||
expect(mockLink.onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle links click events', async () => {
|
||||
const mockLinks = [mockLink, { ...mockLink, title: 'Data Link2', href: 'http://link2.com' }];
|
||||
|
||||
render(<DataLinksActionsTooltip links={mockLinks} coords={mockCoords} />);
|
||||
|
||||
const link = screen.getByText('Data Link1');
|
||||
await userEvent.click(link);
|
||||
expect(mockLink.onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render when there is only one action', () => {
|
||||
const { container } = render(<DataLinksActionsTooltip links={[]} actions={[mockAction]} coords={mockCoords} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render tooltip with actions', () => {
|
||||
const mockActions = [mockAction, { ...mockAction, title: 'Action2' }];
|
||||
|
||||
render(<DataLinksActionsTooltip links={[]} actions={mockActions} coords={mockCoords} />);
|
||||
|
||||
expect(screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).toBeInTheDocument();
|
||||
|
||||
// Action button should be rendered
|
||||
const actionButton = screen.getByText('Action1');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onTooltipClose when tooltip is dismissed', async () => {
|
||||
const onTooltipClose = jest.fn();
|
||||
render(
|
||||
<DataLinksActionsTooltip
|
||||
links={[mockLink, { ...mockLink, title: 'Data Link2', href: 'http://link2.com' }]}
|
||||
coords={mockCoords}
|
||||
onTooltipClose={onTooltipClose}
|
||||
/>
|
||||
);
|
||||
|
||||
// click outside the tooltip
|
||||
await userEvent.click(document.body);
|
||||
|
||||
expect(onTooltipClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render custom value', () => {
|
||||
const customValue = <div data-testid="custom-value">Custom Value</div>;
|
||||
render(<DataLinksActionsTooltip links={[mockLink]} coords={mockCoords} value={customValue} />);
|
||||
|
||||
expect(screen.getByTestId('custom-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { flip, shift, useDismiss, useFloating, useInteractions } from '@floating-ui/react';
|
||||
import { ReactElement, useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
import { VizTooltipFooter } from '../VizTooltip/VizTooltipFooter';
|
||||
import { VizTooltipWrapper } from '../VizTooltip/VizTooltipWrapper';
|
||||
|
||||
import { DataLinksActionsTooltipCoords } from './utils';
|
||||
|
||||
interface Props {
|
||||
links: LinkModel[];
|
||||
actions?: ActionModel[];
|
||||
value?: string | ReactElement;
|
||||
coords: DataLinksActionsTooltipCoords;
|
||||
onTooltipClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const DataLinksActionsTooltip = ({ links, actions, value, coords, onTooltipClose }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
flip({
|
||||
fallbackAxisSideDirection: 'end',
|
||||
// see https://floating-ui.com/docs/flip#combining-with-shift
|
||||
crossAxis: false,
|
||||
boundary: document.body,
|
||||
}),
|
||||
shift(),
|
||||
];
|
||||
|
||||
const virtual = useMemo(() => {
|
||||
const { clientX, clientY } = coords;
|
||||
|
||||
// https://floating-ui.com/docs/virtual-elements
|
||||
return {
|
||||
getBoundingClientRect() {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
right: clientX,
|
||||
bottom: clientY,
|
||||
};
|
||||
},
|
||||
};
|
||||
}, [coords]);
|
||||
|
||||
const refCallback = (el: HTMLDivElement) => {
|
||||
refs.setFloating(el);
|
||||
refs.setReference(virtual);
|
||||
};
|
||||
|
||||
const { context, refs, floatingStyles } = useFloating({
|
||||
open: true,
|
||||
placement: 'right-start',
|
||||
onOpenChange: onTooltipClose,
|
||||
middleware,
|
||||
// whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
const dismiss = useDismiss(context);
|
||||
|
||||
const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length);
|
||||
|
||||
const { getFloatingProps, getReferenceProps } = useInteractions([dismiss]);
|
||||
|
||||
if (links.length === 0 && !Boolean(actions?.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{value}
|
||||
{hasMultipleLinksOrActions && (
|
||||
<Portal>
|
||||
<div
|
||||
ref={refCallback}
|
||||
{...getReferenceProps({ onClick: (e) => e.stopPropagation() })}
|
||||
{...getFloatingProps()}
|
||||
style={floatingStyles}
|
||||
className={styles.tooltipWrapper}
|
||||
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
|
||||
>
|
||||
<VizTooltipWrapper>
|
||||
<VizTooltipFooter dataLinks={links} actions={actions} />
|
||||
</VizTooltipWrapper>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderSingleLink = (
|
||||
link: LinkModel,
|
||||
children: string | React.JSX.Element,
|
||||
className?: string
|
||||
): React.JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
target={link.target}
|
||||
title={link.title}
|
||||
data-testid={selectors.components.DataLinksContextMenu.singleLink}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
tooltipWrapper: css({
|
||||
zIndex: theme.zIndex.portal,
|
||||
whiteSpace: 'pre',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
background: theme.colors.background.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
boxShadow: theme.shadows.z3,
|
||||
userSelect: 'text',
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
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 '@grafana/schema';
|
||||
|
||||
import AutoCell from './AutoCell';
|
||||
@@ -32,7 +34,7 @@ describe('AutoCell', () => {
|
||||
};
|
||||
};
|
||||
|
||||
it('shows multiple datalinks in a context menu behind a button', () => {
|
||||
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,
|
||||
@@ -53,11 +55,17 @@ describe('AutoCell', () => {
|
||||
cellOptions={{ type: TableCellDisplayMode.Auto }}
|
||||
/>
|
||||
);
|
||||
const submitButton = screen.getByRole('button');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
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 button for menu for multiple links if one is invalid', () => {
|
||||
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,
|
||||
@@ -78,8 +86,11 @@ describe('AutoCell', () => {
|
||||
cellOptions={{ type: TableCellDisplayMode.Auto }}
|
||||
/>
|
||||
);
|
||||
const submitButton = screen.queryByRole('button');
|
||||
expect(submitButton).not.toBeInTheDocument();
|
||||
|
||||
const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
|
||||
await userEvent.click(cell);
|
||||
|
||||
expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
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 { TableCellDisplayMode, TableCellOptions } from '@grafana/schema';
|
||||
|
||||
import { useStyles2 } from '../../../../themes';
|
||||
import { clearLinkButtonStyles } from '../../../Button';
|
||||
import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
|
||||
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
|
||||
import { AutoCellProps } from '../types';
|
||||
import { getCellLinks } from '../utils';
|
||||
|
||||
export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions }: AutoCellProps) {
|
||||
export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions, actions }: AutoCellProps) {
|
||||
const styles = useStyles2(getStyles, justifyContent);
|
||||
|
||||
const displayValue = field.display!(value);
|
||||
const formattedValue = formattedValueToString(displayValue);
|
||||
const cellLinks = getCellLinks(field, rowIdx);
|
||||
const hasLinks = cellLinks?.some((link) => link.href || link.onClick != null);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
const links = getCellLinks(field, rowIdx) || [];
|
||||
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.cell}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu
|
||||
links={() => getCellLinks(field, rowIdx)?.filter((link) => link.href || link.onClick != null) || []}
|
||||
>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
<button
|
||||
className={cx(clearButtonStyle, getLinkStyle(styles, cellOptions, api.targetClassName))}
|
||||
onClick={api.openMenu}
|
||||
>
|
||||
{formattedValue}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return <div className={getLinkStyle(styles, cellOptions, api.targetClassName)}>{formattedValue}</div>;
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.cell}
|
||||
onClick={({ clientX, clientY }) => setTooltipCoords({ clientX, clientY })}
|
||||
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
|
||||
)}
|
||||
@@ -47,16 +48,12 @@ export default function AutoCell({ value, field, justifyContent, rowIdx, cellOpt
|
||||
);
|
||||
}
|
||||
|
||||
const getLinkStyle = (
|
||||
styles: ReturnType<typeof getStyles>,
|
||||
cellOptions: TableCellOptions,
|
||||
targetClassName: string | undefined
|
||||
) => {
|
||||
const getLinkStyle = (styles: ReturnType<typeof getStyles>, cellOptions: TableCellOptions) => {
|
||||
if (cellOptions.type === TableCellDisplayMode.Auto) {
|
||||
return cx(styles.linkCell, targetClassName);
|
||||
return styles.linkCell;
|
||||
}
|
||||
|
||||
return cx(styles.cellLinkForColoredCell, targetClassName);
|
||||
return styles.cellLinkForColoredCell;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({
|
||||
@@ -83,12 +80,9 @@ const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors.text.link,
|
||||
color: `${theme.colors.text.link} !important`,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
paddingRight: theme.spacing(1.5),
|
||||
a: {
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
color: theme.colors.text.link,
|
||||
|
||||
@@ -1,8 +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 { DataLinksContextMenu, DataLinksContextMenuApi } from '../../../DataLinks/DataLinksContextMenu';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
|
||||
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
|
||||
import { BarGaugeCellProps } from '../types';
|
||||
import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils';
|
||||
|
||||
@@ -20,7 +23,7 @@ const defaultScale: ThresholdsConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => {
|
||||
export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actions }: BarGaugeCellProps) => {
|
||||
const displayValue = field.display!(value);
|
||||
const cellOptions = getCellOptions(field);
|
||||
const heightOffset = extractPixelValue(theme.spacing(1));
|
||||
@@ -44,13 +47,14 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar
|
||||
cellOptions.valueDisplayMode !== undefined ? cellOptions.valueDisplayMode : BarGaugeValueMode.Text;
|
||||
}
|
||||
|
||||
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
|
||||
|
||||
const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!);
|
||||
const links = getCellLinks(field, rowIdx) || [];
|
||||
|
||||
const renderComponent = (menuProps: DataLinksContextMenuApi) => {
|
||||
const { openMenu } = menuProps;
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
const renderComponent = () => {
|
||||
return (
|
||||
<BarGauge
|
||||
width={width}
|
||||
@@ -62,7 +66,6 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={theme}
|
||||
alignmentFactors={alignmentFactors}
|
||||
onClick={openMenu}
|
||||
itemSpacing={1}
|
||||
lcdCellWidth={8}
|
||||
displayMode={barGaugeMode}
|
||||
@@ -71,19 +74,25 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar
|
||||
);
|
||||
};
|
||||
|
||||
// @TODO: Actions
|
||||
return (
|
||||
<>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu
|
||||
links={() => getCellLinks(field, rowIdx) || []}
|
||||
style={{ display: 'flex', width: '100%' }}
|
||||
>
|
||||
{(api) => renderComponent(api)}
|
||||
</DataLinksContextMenu>
|
||||
// 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={({ clientX, clientY }) => setTooltipCoords({ clientX, clientY })}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], renderComponent())
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
actions={actions}
|
||||
value={renderComponent()}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
renderComponent({})
|
||||
renderComponent()
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('DataLinksCell', () => {
|
||||
expect(screen.getByRole('link', { name: link.title })).toHaveAttribute('href', link.href);
|
||||
} else {
|
||||
expect(screen.queryByRole('link', { name: link.title })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(link.title)).toBeInTheDocument();
|
||||
expect(screen.queryByText(link.title)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Property } from 'csstype';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { TableCellDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { useStyles2 } from '../../../../themes';
|
||||
import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
|
||||
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
|
||||
import { ImageCellProps } from '../types';
|
||||
import { getCellLinks } from '../utils';
|
||||
|
||||
const DATALINKS_HEIGHT_OFFSET = 10;
|
||||
|
||||
export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => {
|
||||
export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx, actions }: ImageCellProps) => {
|
||||
const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET;
|
||||
const styles = useStyles2(getStyles, calculatedHeight, justifyContent);
|
||||
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
|
||||
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 } =
|
||||
@@ -23,33 +28,25 @@ export const ImageCell = ({ cellOptions, field, height, justifyContent, value, r
|
||||
|
||||
const img = <img alt={alt} src={text} className={styles.image} title={title} />;
|
||||
|
||||
// TODO: Implement actions
|
||||
return (
|
||||
<div className={styles.imageContainer}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, rowIdx) || []}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
<div
|
||||
onClick={api.openMenu}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && api.openMenu) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
||||
api.openMenu(e as any);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{img}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return img;
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
// 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={({ clientX, clientY }) => {
|
||||
setTooltipCoords({ clientX, clientY });
|
||||
}}
|
||||
>
|
||||
{shouldShowLink ? (
|
||||
renderSingleLink(links[0], img)
|
||||
) : shouldShowTooltip ? (
|
||||
<DataLinksActionsTooltip
|
||||
links={links}
|
||||
actions={actions}
|
||||
value={img}
|
||||
coords={tooltipCoords}
|
||||
onTooltipClose={() => setTooltipCoords(undefined)}
|
||||
/>
|
||||
) : (
|
||||
img
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { Property } from 'csstype';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../../../themes';
|
||||
import { Button, clearLinkButtonStyles } from '../../../Button';
|
||||
import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu';
|
||||
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
|
||||
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
|
||||
import { JSONCellProps } from '../types';
|
||||
import { getCellLinks } from '../utils';
|
||||
|
||||
export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => {
|
||||
export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSONCellProps) => {
|
||||
const styles = useStyles2(getStyles, justifyContent);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
|
||||
let displayValue = value;
|
||||
|
||||
@@ -33,25 +33,29 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps
|
||||
}
|
||||
}
|
||||
|
||||
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
|
||||
const links = getCellLinks(field, rowIdx) || [];
|
||||
|
||||
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
|
||||
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
|
||||
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
|
||||
|
||||
// TODO: Implement actions
|
||||
return (
|
||||
<div className={styles.jsonText}>
|
||||
{hasLinks ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, rowIdx) || []}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
<Button className={cx(clearButtonStyle)} onClick={api.openMenu}>
|
||||
{displayValue}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return <>{displayValue}</>;
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.jsonText}
|
||||
onClick={({ clientX, clientY }) => setTooltipCoords({ clientX, clientY })}
|
||||
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
|
||||
)}
|
||||
|
||||
@@ -100,6 +100,7 @@ export const SparklineCell = (props: SparklineCellProps) => {
|
||||
width: `${valueWidth - theme.spacing.gridSize}px`,
|
||||
textAlign: 'right',
|
||||
marginRight: theme.spacing(1),
|
||||
marginLeft: theme.spacing(1),
|
||||
}}
|
||||
className={styles.valueContainer}
|
||||
value={displayValue}
|
||||
|
||||
@@ -109,13 +109,22 @@ export function TableCellNG(props: TableCellNGProps) {
|
||||
case TableCellDisplayMode.BasicGauge:
|
||||
case TableCellDisplayMode.GradientGauge:
|
||||
case TableCellDisplayMode.LcdGauge:
|
||||
cell = <BarGaugeCell {...commonProps} theme={theme} timeRange={timeRange} height={height} width={divWidth} />;
|
||||
cell = (
|
||||
<BarGaugeCell
|
||||
{...commonProps}
|
||||
theme={theme}
|
||||
timeRange={timeRange}
|
||||
height={height}
|
||||
width={divWidth}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case TableCellDisplayMode.Image:
|
||||
cell = <ImageCell {...commonProps} cellOptions={cellOptions} height={height} />;
|
||||
cell = <ImageCell {...commonProps} cellOptions={cellOptions} height={height} actions={actions} />;
|
||||
break;
|
||||
case TableCellDisplayMode.JSONView:
|
||||
cell = <JSONCell {...commonProps} />;
|
||||
cell = <JSONCell {...commonProps} actions={actions} />;
|
||||
break;
|
||||
case TableCellDisplayMode.DataLinks:
|
||||
cell = <DataLinksCell field={field} rowIdx={rowIdx} />;
|
||||
@@ -137,12 +146,12 @@ export function TableCellNG(props: TableCellNGProps) {
|
||||
if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) {
|
||||
cell = <SparklineCell {...commonProps} theme={theme} timeRange={timeRange} width={divWidth} />;
|
||||
} else {
|
||||
cell = <JSONCell {...commonProps} />;
|
||||
cell = <JSONCell {...commonProps} actions={actions} />;
|
||||
}
|
||||
} else if (field.type === FieldType.other) {
|
||||
cell = <JSONCell {...commonProps} />;
|
||||
cell = <JSONCell {...commonProps} actions={actions} />;
|
||||
} else {
|
||||
cell = <AutoCell {...commonProps} cellOptions={cellOptions} />;
|
||||
cell = <AutoCell {...commonProps} cellOptions={cellOptions} actions={actions} />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export interface SparklineCellProps {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface BarGaugeCellProps {
|
||||
export interface BarGaugeCellProps extends ActionCellProps {
|
||||
field: Field;
|
||||
height: number;
|
||||
rowIdx: number;
|
||||
@@ -189,7 +189,7 @@ export interface BarGaugeCellProps {
|
||||
timeRange: TimeRange;
|
||||
}
|
||||
|
||||
export interface ImageCellProps {
|
||||
export interface ImageCellProps extends ActionCellProps {
|
||||
cellOptions: TableCellOptions;
|
||||
field: Field;
|
||||
height: number;
|
||||
@@ -198,7 +198,7 @@ export interface ImageCellProps {
|
||||
rowIdx: number;
|
||||
}
|
||||
|
||||
export interface JSONCellProps {
|
||||
export interface JSONCellProps extends ActionCellProps {
|
||||
justifyContent: Property.JustifyContent;
|
||||
value: TableCellValue;
|
||||
field: Field;
|
||||
@@ -226,7 +226,7 @@ export interface CellColors {
|
||||
bgHoverColor?: string;
|
||||
}
|
||||
|
||||
export interface AutoCellProps {
|
||||
export interface AutoCellProps extends ActionCellProps {
|
||||
value: TableCellValue;
|
||||
field: Field;
|
||||
justifyContent: Property.JustifyContent;
|
||||
|
||||
@@ -423,7 +423,7 @@ export const getCellLinks = (field: Field, rowIdx: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
return links.filter((link) => link.href || link.onClick != null);
|
||||
};
|
||||
|
||||
/* ----------------------------- Data grid sorting ---------------------------- */
|
||||
|
||||
@@ -184,7 +184,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors.text.link,
|
||||
color: `${theme.colors.text.link} !important`,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
paddingRight: theme.spacing(1.5),
|
||||
'&:hover': {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HeaderGroup, Row } from 'react-table';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import {
|
||||
ActionModel,
|
||||
DataFrame,
|
||||
DisplayValue,
|
||||
DisplayValueAlignmentFactors,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
isDataFrame,
|
||||
isDataFrameWithValue,
|
||||
isTimeSeriesFrame,
|
||||
LinkModel,
|
||||
reduceField,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
@@ -762,3 +764,15 @@ export function guessLongestField(fieldConfig: FieldConfigSource, data: DataFram
|
||||
|
||||
return longestField;
|
||||
}
|
||||
|
||||
export type DataLinksActionsTooltipCoords = {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
};
|
||||
|
||||
export const getDataLinksActionsTooltipUtils = (links: LinkModel[], actions?: ActionModel[]) => {
|
||||
const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length);
|
||||
const shouldShowLink = links.length === 1 && !Boolean(actions?.length);
|
||||
|
||||
return { shouldShowLink, hasMultipleLinksOrActions };
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
textDecoration: 'underline',
|
||||
background: 'none',
|
||||
},
|
||||
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
'& span': {
|
||||
whiteSpace: 'normal',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Field, LinkModel } from '@grafana/data';
|
||||
* @internal
|
||||
*/
|
||||
export const getCellLinks = (field: Field, row: Row) => {
|
||||
let links: Array<LinkModel<unknown>> | undefined;
|
||||
let links: Array<LinkModel<Field>> | undefined;
|
||||
if (field.getLinks) {
|
||||
links = field.getLinks({
|
||||
valueRowIndex: row.index,
|
||||
@@ -34,5 +34,5 @@ export const getCellLinks = (field: Field, row: Row) => {
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
return links.filter((link) => link.href || link.onClick != null);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user