add datalinks to AutoCell, ImageCell, JsonCell

This commit is contained in:
Adela Almasan 2025-02-14 15:29:55 -06:00
parent 8f1abd652c
commit a7fb7de084
7 changed files with 201 additions and 26 deletions

View File

@ -1,28 +1,94 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { Property } from 'csstype';
import { GrafanaTheme2, formattedValueToString } from '@grafana/data';
import { TableCellOptions } from '@grafana/schema';
import { TableCellDisplayMode, TableCellOptions } from '@grafana/schema';
import { useStyles2 } from '../../../../themes';
import { clearLinkButtonStyles } from '../../../Button';
import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu';
import { CellNGProps } from '../types';
import { getCellLinks } from '../utils';
interface AutoCellProps extends CellNGProps {
cellOptions: TableCellOptions;
}
export default function AutoCell({ value, field, justifyContent, cellOptions }: AutoCellProps) {
export default function AutoCell({ value, field, justifyContent, cellOptions, rowIdx }: AutoCellProps) {
const styles = useStyles2(getStyles, justifyContent);
const displayValue = field.display!(value);
const formattedValue = formattedValueToString(displayValue);
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
return <div className={styles.cell}>{formattedValue}</div>;
return (
<div className={styles.cell}>
{hasLinks ? (
<DataLinksContextMenu links={() => getCellLinks(field, rowIdx) || []}>
{(api) => {
if (api.openMenu) {
return (
<button
className={cx(clearButtonStyle, getLinkStyle(styles, cellOptions, api.targetClassName))}
onClick={api.openMenu}
>
{value}
</button>
);
} else {
return <button className={getLinkStyle(styles, cellOptions, api.targetClassName)}>{value}</button>;
}
}}
</DataLinksContextMenu>
) : (
formattedValue
)}
</div>
);
}
const getLinkStyle = (
styles: ReturnType<typeof getStyles>,
cellOptions: TableCellOptions,
targetClassName: string | undefined
) => {
if (cellOptions.type === TableCellDisplayMode.Auto) {
return cx(styles.linkCell, targetClassName);
}
return cx(styles.cellLinkForColoredCell, targetClassName);
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({
cell: css({
display: 'flex',
justifyContent: justifyContent,
}),
cellLinkForColoredCell: css({
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
userSelect: 'text',
whiteSpace: 'nowrap',
fontWeight: theme.typography.fontWeightMedium,
textDecoration: 'underline',
}),
linkCell: css({
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
userSelect: 'text',
whiteSpace: 'nowrap',
color: theme.colors.text.link,
fontWeight: theme.typography.fontWeightMedium,
paddingRight: theme.spacing(1.5),
a: {
color: theme.colors.text.link,
},
'&:hover': {
textDecoration: 'underline',
color: theme.colors.text.link,
},
}),
});

View File

@ -1,5 +1,3 @@
import { isFunction } from 'lodash';
import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data';
import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema';
@ -7,6 +5,7 @@ import { BarGauge } from '../../../BarGauge/BarGauge';
import { DataLinksContextMenu, DataLinksContextMenuApi } from '../../../DataLinks/DataLinksContextMenu';
import { getAlignmentFactor, getCellOptions } from '../../utils';
import { BarGaugeCellProps } from '../types';
import { getCellLinks } from '../utils';
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
@ -45,15 +44,7 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar
cellOptions.valueDisplayMode !== undefined ? cellOptions.valueDisplayMode : BarGaugeValueMode.Text;
}
const getLinks = () => {
if (!isFunction(field.getLinks)) {
return [];
}
return field.getLinks({ valueRowIndex: rowIdx });
};
const hasLinks = Boolean(getLinks().length);
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!);
@ -84,7 +75,10 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar
return (
<>
{hasLinks ? (
<DataLinksContextMenu links={getLinks} style={{ display: 'flex', width: '100%' }}>
<DataLinksContextMenu
links={() => getCellLinks(field, rowIdx) || []}
style={{ display: 'flex', width: '100%' }}
>
{(api) => renderComponent(api)}
</DataLinksContextMenu>
) : (

View File

@ -1,17 +1,21 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { TableCellDisplayMode } from '@grafana/schema';
import { useStyles2 } from '../../../../themes';
import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu';
import { ImageCellProps } from '../types';
import { getCellLinks } from '../utils';
const DATALINKS_HEIGHT_OFFSET = 10;
export const ImageCell = ({ cellOptions, field, height, justifyContent, value }: ImageCellProps) => {
export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => {
const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET;
const styles = useStyles2(getStyles, calculatedHeight, justifyContent);
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
const { text } = field.display!(value);
const { alt, title } =
@ -19,8 +23,38 @@ export const ImageCell = ({ cellOptions, field, height, justifyContent, value }:
const img = <img alt={alt} src={text} className={styles.image} title={title} />;
// TODO: Implement DataLinksContextMenu + actions
return <div className={styles.imageContainer}>{img}</div>;
// 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>
) : (
img
)}
</div>
);
};
const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({

View File

@ -1,14 +1,18 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { Property } from 'csstype';
import { isString } from 'lodash';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes';
import { Button, clearLinkButtonStyles } from '../../../Button';
import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu';
import { CellNGProps } from '../types';
import { getCellLinks } from '../utils';
export const JSONCell = ({ value, justifyContent }: Omit<CellNGProps, 'theme' | 'field'>) => {
export const JSONCell = ({ value, justifyContent, field, rowIdx }: Omit<CellNGProps, 'theme'>) => {
const styles = useStyles2(getStyles, justifyContent);
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
let localValue = value;
let displayValue = localValue;
@ -21,8 +25,30 @@ export const JSONCell = ({ value, justifyContent }: Omit<CellNGProps, 'theme' |
displayValue = JSON.stringify(localValue, null, ' ');
}
// TODO: Implement DataLinksContextMenu + actions
return <div className={styles.jsonText}>{displayValue}</div>;
const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
// 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>
) : (
displayValue
)}
</div>
);
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({

View File

@ -92,6 +92,7 @@ export function TableCellNG(props: any) {
timeRange={timeRange}
height={height}
width={divWidth}
rowIdx={rowIdx}
/>
);
break;
@ -103,11 +104,12 @@ export function TableCellNG(props: any) {
height={height}
justifyContent={justifyContent}
value={value}
rowIdx={rowIdx}
/>
);
break;
case TableCellDisplayMode.JSONView:
cell = <JSONCell value={value} justifyContent={justifyContent} />;
cell = <JSONCell value={value} justifyContent={justifyContent} field={field} rowIdx={rowIdx} />;
break;
case TableCellDisplayMode.Auto:
default:
@ -118,6 +120,7 @@ export function TableCellNG(props: any) {
theme={theme}
justifyContent={justifyContent}
cellOptions={fieldConfig.custom.cellOptions}
rowIdx={rowIdx}
/>
);
}

View File

@ -9,7 +9,7 @@ export interface CellNGProps {
theme?: GrafanaTheme2;
height?: number;
justifyContent: Property.JustifyContent;
rowIdx?: number;
rowIdx: number;
}
export interface RowExpanderNGProps {

View File

@ -1,6 +1,14 @@
import tinycolor from 'tinycolor2';
import { FieldType, Field, formattedValueToString, reduceField, GrafanaTheme2, DisplayValue } from '@grafana/data';
import {
FieldType,
Field,
formattedValueToString,
reduceField,
GrafanaTheme2,
DisplayValue,
LinkModel,
} from '@grafana/data';
import { TableCellBackgroundDisplayMode, TableCellDisplayMode, TableCellOptions } from '@grafana/schema';
import { getTextColorForAlphaBackground } from '../../../utils';
@ -207,3 +215,47 @@ export function getCellColors(
return { textColor, bgColor, bgHoverColor };
}
export const getLinks = (field: Field, rowIdx: number) => {
if (field.getLinks) {
return field.getLinks({ valueRowIndex: rowIdx });
}
return [];
};
/**
* @internal
* TODO: unify with the existing getCellLinks
*/
export const getCellLinks = (field: Field, rowIndex: number) => {
let links: Array<LinkModel<unknown>> | undefined;
if (field.getLinks) {
links = field.getLinks({
valueRowIndex: rowIndex,
});
}
if (!links) {
return;
}
for (let i = 0; i < links?.length; i++) {
if (links[i].onClick) {
const origOnClick = links[i].onClick;
links[i].onClick = (event) => {
// Allow opening in new tab
if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
event.preventDefault();
origOnClick!(event, {
field,
rowIndex: rowIndex,
});
}
};
}
}
return links;
};