TableNG: Use same hover behavior for one and multiple AutoCell links (#108194)

pull/107803/head^2
Leon Sorokin 2 days ago committed by GitHub
parent 08afd73c0c
commit 4b1558b3ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx
  2. 11
      packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
  3. 41
      packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx
  4. 14
      packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx
  5. 13
      packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx
  6. 32
      packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx
  7. 13
      packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
  8. 10
      packages/grafana-ui/src/components/Table/TableNG/hooks.ts

@ -1,7 +1,6 @@
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 { useMemo, ReactNode } from 'react';
import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -16,7 +15,7 @@ import { DataLinksActionsTooltipCoords } from './utils';
interface Props {
links: LinkModel[];
actions?: ActionModel[];
value?: string | ReactElement;
value?: ReactNode;
coords: DataLinksActionsTooltipCoords;
onTooltipClose?: () => void;
}
@ -102,11 +101,7 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt
);
};
export const renderSingleLink = (
link: LinkModel,
children: string | React.JSX.Element,
className?: string
): React.JSX.Element => {
export const renderSingleLink = (link: LinkModel, children: ReactNode, className?: string): ReactNode => {
return (
<a
href={link.href}

@ -1,13 +1,14 @@
import { formattedValueToString } from '@grafana/data';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { useSingleLink } from '../hooks';
import { MaybeWrapWithLink } from '../MaybeWrapWithLink';
import { AutoCellProps } from '../types';
export default function AutoCell({ value, field, rowIdx }: AutoCellProps) {
const displayValue = field.display!(value);
const formattedValue = formattedValueToString(displayValue);
const link = useSingleLink(field, rowIdx);
return link != null ? renderSingleLink(link, formattedValue) : formattedValue;
return (
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
{formattedValue}
</MaybeWrapWithLink>
);
}

@ -2,8 +2,7 @@ import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMin
import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema';
import { BarGauge } from '../../../BarGauge/BarGauge';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { useSingleLink } from '../hooks';
import { MaybeWrapWithLink } from '../MaybeWrapWithLink';
import { BarGaugeCellProps } from '../types';
import { extractPixelValue, getCellOptions, getAlignmentFactor } from '../utils';
@ -47,25 +46,23 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar
const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!);
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}
/>
return (
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
<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}
/>
</MaybeWrapWithLink>
);
const link = useSingleLink(field, rowIdx);
return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent);
};

@ -4,9 +4,8 @@ import { Property } from 'csstype';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellDisplayMode } from '../../types';
import { useSingleLink } from '../hooks';
import { MaybeWrapWithLink } from '../MaybeWrapWithLink';
import { ImageCellProps } from '../types';
const DATALINKS_HEIGHT_OFFSET = 10;
@ -19,10 +18,13 @@ export const ImageCell = ({ cellOptions, field, height, justifyContent, value, r
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 <div className={styles.imageContainer}>{link == null ? img : renderSingleLink(link, img)}</div>;
return (
<div className={styles.imageContainer}>
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
<img alt={alt} src={text} className={styles.image} title={title} />
</MaybeWrapWithLink>
</div>
);
};
const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({

@ -4,8 +4,7 @@ import { Property } from 'csstype';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { useSingleLink } from '../hooks';
import { MaybeWrapWithLink } from '../MaybeWrapWithLink';
import { JSONCellProps } from '../types';
export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => {
@ -31,9 +30,13 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps
}
}
const link = useSingleLink(field, rowIdx);
return <div className={styles.jsonText}>{link == null ? displayValue : renderSingleLink(link, displayValue)}</div>;
return (
<div className={styles.jsonText}>
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
{displayValue}
</MaybeWrapWithLink>
</div>
);
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({

@ -0,0 +1,32 @@
import { memo, ReactNode } from 'react';
import { Field } from '@grafana/data';
import { renderSingleLink } from '../DataLinksActionsTooltip';
import { getCellLinks } from './utils';
interface MaybeWrapWithLinkProps {
field: Field;
rowIdx: number;
children: ReactNode;
}
export const MaybeWrapWithLink = memo(({ field, rowIdx, children }: MaybeWrapWithLinkProps): ReactNode => {
const linksCount = field.config.links?.length ?? 0;
const actionsCount = field.config.actions?.length ?? 0;
// as real, single link
if (linksCount === 1 && actionsCount === 0) {
let link = (getCellLinks(field, rowIdx) ?? [])[0];
return renderSingleLink(link, children);
}
// as faux link that acts as hit-area for tooltip activation
else if (linksCount + actionsCount > 0) {
// eslint-disable-next-line jsx-a11y/anchor-is-valid
return <a aria-haspopup="menu">{children}</a>;
}
// raw value
return children;
});

@ -621,11 +621,17 @@ export function TableNG(props: TableNGProps) {
rows={paginatedRows}
headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: noHeader })}
headerRowHeight={headerHeight}
onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => {
onCellClick={({ column, row }, { clientX, clientY, preventGridDefault, target }) => {
// Note: could be column.field; JS says yes, but TS says no!
const field = columns[column.idx].field;
if (colsWithTooltip[getDisplayName(field)]) {
if (
colsWithTooltip[getDisplayName(field)] &&
target instanceof HTMLElement &&
// this walks up the tree to find either a faux link wrapper or the cell root
// it then only proceeds if we matched the faux link wrapper
target.closest('a[aria-haspopup], .rdg-cell')?.matches('a')
) {
const rowIdx = row.__index;
setTooltipState({
coords: {
@ -895,7 +901,6 @@ const getCellStyles = (
minHeight: '100%',
backgroundClip: 'padding-box !important', // helps when cells have a bg color
...(shouldWrap && { whiteSpace: 'pre-line' }),
...(hasTooltip && { cursor: 'pointer' }),
'&:last-child': {
borderInlineEnd: 'none',
@ -914,7 +919,7 @@ const getCellStyles = (
}),
},
[hasTooltip ? '&' : 'a']: {
a: {
cursor: 'pointer',
...(isColorized
? {

@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, Ref
import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid';
import { varPreLine } from 'uwrap';
import { Field, fieldReducers, FieldType, formattedValueToString, LinkModel, reduceField } from '@grafana/data';
import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data';
import { useTheme2 } from '../../../themes/ThemeContext';
import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types';
@ -17,7 +17,6 @@ import {
getColumnTypes,
GetMaxWrapCellOptions,
getMaxWrapCell,
getCellLinks,
} from './utils';
// Helper function to get displayed value
@ -602,13 +601,6 @@ 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]);
}
export function useScrollbarWidth(ref: RefObject<DataGridHandle>, height: number, renderedRows: TableRow[]) {
const [scrollbarWidth, setScrollbarWidth] = useState(0);

Loading…
Cancel
Save