TableNG: Memoize some components to improve rendering performance (#109763)
* try React.memo * Table: Rework the renderers file and some types * more test cleanups * remove for gauge for now * memoize all of the renderers * also memoize the Tooltip component * memoize the TableCellActions * memo useStyles2 * update useStyles2 usage in TableNG * remove console.log * add a test for the AutoCell fallback rendering case for testField * style nit - update variable name to be more idiomatic --------- Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Paul Marbach
parent
950423903f
commit
424e336ae1
@@ -73,46 +73,46 @@ describe('TableNG Cells renderers', () => {
|
||||
}
|
||||
|
||||
// Helper function to render a cell and get the test ID
|
||||
const renderCell = (field: Field, cellOptions: TableCellOptions) =>
|
||||
render(
|
||||
getCellRenderer(
|
||||
field,
|
||||
cellOptions
|
||||
)({
|
||||
field,
|
||||
value: 'test-value',
|
||||
rowIdx: 0,
|
||||
frame: createDataFrame({ fields: [field] }),
|
||||
height: 100,
|
||||
width: 100,
|
||||
theme: createTheme(),
|
||||
cellOptions,
|
||||
cellInspect: false,
|
||||
showFilters: false,
|
||||
getActions: jest.fn(() => [
|
||||
const renderCell = (field: Field, cellOptions: TableCellOptions) => {
|
||||
// eslint-disable-next-line testing-library/render-result-naming-convention
|
||||
const CellComponent = getCellRenderer(field, cellOptions);
|
||||
return render(
|
||||
<CellComponent
|
||||
field={field}
|
||||
value="test-value"
|
||||
rowIdx={0}
|
||||
frame={createDataFrame({ fields: [field] })}
|
||||
height={100}
|
||||
width={100}
|
||||
theme={createTheme()}
|
||||
cellOptions={cellOptions}
|
||||
cellInspect={false}
|
||||
showFilters={false}
|
||||
getActions={jest.fn(() => [
|
||||
{ title: 'Action', onClick: jest.fn(() => {}), confirmation: jest.fn(), style: {} },
|
||||
]),
|
||||
})
|
||||
])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Performance test helper
|
||||
const benchmarkCellPerformance = (field: Field, cellOptions: TableCellOptions, iterations = 100) => {
|
||||
// eslint-disable-next-line testing-library/render-result-naming-convention
|
||||
const r = getCellRenderer(field, cellOptions);
|
||||
const CellComponent = getCellRenderer(field, cellOptions);
|
||||
return measurePerformance(() => {
|
||||
render(
|
||||
r({
|
||||
field,
|
||||
value: 'test-value',
|
||||
rowIdx: 0,
|
||||
frame: createDataFrame({ fields: [field] }),
|
||||
height: 100,
|
||||
width: 100,
|
||||
theme: createTheme(),
|
||||
cellOptions,
|
||||
cellInspect: false,
|
||||
showFilters: false,
|
||||
})
|
||||
<CellComponent
|
||||
field={field}
|
||||
value="test-value"
|
||||
rowIdx={0}
|
||||
frame={createDataFrame({ fields: [field] })}
|
||||
height={100}
|
||||
width={100}
|
||||
theme={createTheme()}
|
||||
cellOptions={cellOptions}
|
||||
cellInspect={false}
|
||||
showFilters={false}
|
||||
/>
|
||||
);
|
||||
}, iterations);
|
||||
};
|
||||
@@ -289,6 +289,22 @@ describe('TableNG Cells renderers', () => {
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.childNodes).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should use AutoCell when attempting to render a field with an unsupported type', () => {
|
||||
// confirm that a real pill cell has spans.
|
||||
const stringField = createField(FieldType.string, ['42']);
|
||||
const { container: stringFieldContainer } = renderCell(stringField, { type: TableCellDisplayMode.Pill });
|
||||
expect(stringFieldContainer).toBeInTheDocument();
|
||||
expect(stringFieldContainer.childNodes).toHaveLength(1);
|
||||
expect(stringFieldContainer.querySelector('span')).toBeInTheDocument();
|
||||
|
||||
// confirm that number pill cell doesn't actually render a pill cell.
|
||||
const numberField = createField(FieldType.number, [42]);
|
||||
const { container: numberFieldContainer } = renderCell(numberField, { type: TableCellDisplayMode.Pill });
|
||||
expect(numberFieldContainer).toBeInTheDocument();
|
||||
expect(numberFieldContainer.childNodes).toHaveLength(1);
|
||||
expect(numberFieldContainer.querySelector('span')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('performance benchmarks', () => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { memo, MemoExoticComponent } from 'react';
|
||||
|
||||
import { Field, FieldType, GrafanaTheme2, isDataFrame, isTimeSeriesFrame } from '@grafana/data';
|
||||
|
||||
import { TableCellDisplayMode, TableCellOptions, TableCustomCellOptions } from '../../types';
|
||||
import { TableCellRenderer, TableCellStyleOptions, TableCellStyles } from '../types';
|
||||
import { TableCellRenderer, TableCellRendererProps, TableCellStyleOptions, TableCellStyles } from '../types';
|
||||
import { getCellOptions } from '../utils';
|
||||
|
||||
import { ActionsCell, getStyles as getActionsCellStyles } from './ActionsCell';
|
||||
@@ -16,49 +17,9 @@ import { MarkdownCell, getStyles as getMarkdownCellStyles } from './MarkdownCell
|
||||
import { PillCell, getStyles as getPillStyles } from './PillCell';
|
||||
import { SparklineCell, getStyles as getSparklineCellStyles } from './SparklineCell';
|
||||
|
||||
const GAUGE_RENDERER: TableCellRenderer = (props) => (
|
||||
<BarGaugeCell
|
||||
field={props.field}
|
||||
value={props.value}
|
||||
theme={props.theme}
|
||||
height={props.height}
|
||||
width={props.width}
|
||||
rowIdx={props.rowIdx}
|
||||
/>
|
||||
);
|
||||
|
||||
const AUTO_RENDERER: TableCellRenderer = (props) => (
|
||||
const AutoCellRenderer = memo((props: TableCellRendererProps) => (
|
||||
<AutoCell value={props.value} field={props.field} rowIdx={props.rowIdx} />
|
||||
);
|
||||
|
||||
const SPARKLINE_RENDERER: TableCellRenderer = (props) => (
|
||||
<SparklineCell
|
||||
value={props.value}
|
||||
field={props.field}
|
||||
timeRange={props.timeRange}
|
||||
rowIdx={props.rowIdx}
|
||||
theme={props.theme}
|
||||
width={props.width}
|
||||
/>
|
||||
);
|
||||
|
||||
const GEO_RENDERER: TableCellRenderer = (props) => <GeoCell value={props.value} height={props.height} />;
|
||||
|
||||
const IMAGE_RENDERER: TableCellRenderer = (props) => (
|
||||
<ImageCell cellOptions={props.cellOptions} field={props.field} value={props.value} rowIdx={props.rowIdx} />
|
||||
);
|
||||
|
||||
const DATA_LINKS_RENDERER: TableCellRenderer = (props) => <DataLinksCell field={props.field} rowIdx={props.rowIdx} />;
|
||||
|
||||
const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => (
|
||||
<ActionsCell field={field} rowIdx={rowIdx} getActions={getActions} />
|
||||
);
|
||||
|
||||
const MARKDOWN_RENDERER: TableCellRenderer = (props) => (
|
||||
<MarkdownCell field={props.field} rowIdx={props.rowIdx} disableSanitizeHtml={props.disableSanitizeHtml} />
|
||||
);
|
||||
|
||||
const PILL_RENDERER: TableCellRenderer = (props) => <PillCell {...props} />;
|
||||
));
|
||||
|
||||
function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions {
|
||||
return options.type === TableCellDisplayMode.Custom;
|
||||
@@ -71,73 +32,101 @@ function mixinAutoCellStyles(fn: TableCellStyles): TableCellStyles {
|
||||
};
|
||||
}
|
||||
|
||||
const CUSTOM_RENDERER: TableCellRenderer = (props) => {
|
||||
if (!isCustomCellOptions(props.cellOptions) || !props.cellOptions.cellComponent) {
|
||||
return null; // nonsensical case, but better to typeguard it than throw.
|
||||
}
|
||||
const CustomCellComponent = props.cellOptions.cellComponent;
|
||||
return <CustomCellComponent field={props.field} rowIndex={props.rowIdx} frame={props.frame} value={props.value} />;
|
||||
};
|
||||
interface CellRegistryEntry {
|
||||
renderer: MemoExoticComponent<TableCellRenderer>;
|
||||
getStyles?: TableCellStyles;
|
||||
testField?: (field: Field) => boolean;
|
||||
}
|
||||
|
||||
const CELL_RENDERERS: Record<TableCellOptions['type'], { renderer: TableCellRenderer; getStyles?: TableCellStyles }> = {
|
||||
[TableCellDisplayMode.Actions]: {
|
||||
renderer: ACTIONS_RENDERER,
|
||||
getStyles: getActionsCellStyles,
|
||||
},
|
||||
const CELL_REGISTRY: Record<TableCellOptions['type'], CellRegistryEntry> = {
|
||||
[TableCellDisplayMode.Auto]: {
|
||||
renderer: AUTO_RENDERER,
|
||||
renderer: AutoCellRenderer,
|
||||
getStyles: getAutoCellStyles,
|
||||
},
|
||||
[TableCellDisplayMode.ColorBackground]: {
|
||||
renderer: AUTO_RENDERER,
|
||||
renderer: AutoCellRenderer,
|
||||
getStyles: getAutoCellStyles,
|
||||
},
|
||||
[TableCellDisplayMode.ColorText]: {
|
||||
renderer: AUTO_RENDERER,
|
||||
renderer: AutoCellRenderer,
|
||||
getStyles: getAutoCellStyles,
|
||||
},
|
||||
[TableCellDisplayMode.Custom]: {
|
||||
renderer: CUSTOM_RENDERER,
|
||||
[TableCellDisplayMode.JSONView]: {
|
||||
renderer: AutoCellRenderer,
|
||||
getStyles: mixinAutoCellStyles(getJsonCellStyles),
|
||||
},
|
||||
[TableCellDisplayMode.Actions]: {
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<ActionsCell field={props.field} rowIdx={props.rowIdx} getActions={props.getActions ?? (() => [])} />
|
||||
)),
|
||||
getStyles: getActionsCellStyles,
|
||||
},
|
||||
[TableCellDisplayMode.DataLinks]: {
|
||||
renderer: DATA_LINKS_RENDERER,
|
||||
renderer: memo((props: TableCellRendererProps) => <DataLinksCell field={props.field} rowIdx={props.rowIdx} />),
|
||||
getStyles: getDataLinksStyles,
|
||||
},
|
||||
[TableCellDisplayMode.Gauge]: {
|
||||
renderer: GAUGE_RENDERER,
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<BarGaugeCell
|
||||
field={props.field}
|
||||
value={props.value}
|
||||
theme={props.theme}
|
||||
height={props.height}
|
||||
width={props.width}
|
||||
rowIdx={props.rowIdx}
|
||||
/>
|
||||
)),
|
||||
},
|
||||
[TableCellDisplayMode.Sparkline]: {
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<SparklineCell
|
||||
value={props.value}
|
||||
field={props.field}
|
||||
timeRange={props.timeRange}
|
||||
rowIdx={props.rowIdx}
|
||||
theme={props.theme}
|
||||
width={props.width}
|
||||
/>
|
||||
)),
|
||||
getStyles: getSparklineCellStyles,
|
||||
},
|
||||
[TableCellDisplayMode.Geo]: {
|
||||
renderer: GEO_RENDERER,
|
||||
renderer: memo((props: TableCellRendererProps) => <GeoCell value={props.value} height={props.height} />),
|
||||
getStyles: getGeoCellStyles,
|
||||
},
|
||||
[TableCellDisplayMode.Image]: {
|
||||
renderer: IMAGE_RENDERER,
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<ImageCell cellOptions={props.cellOptions} field={props.field} value={props.value} rowIdx={props.rowIdx} />
|
||||
)),
|
||||
getStyles: getImageStyles,
|
||||
},
|
||||
[TableCellDisplayMode.JSONView]: {
|
||||
renderer: AUTO_RENDERER,
|
||||
getStyles: mixinAutoCellStyles(getJsonCellStyles),
|
||||
},
|
||||
[TableCellDisplayMode.Pill]: {
|
||||
renderer: PILL_RENDERER,
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<PillCell rowIdx={props.rowIdx} field={props.field} theme={props.theme} />
|
||||
)),
|
||||
getStyles: getPillStyles,
|
||||
},
|
||||
[TableCellDisplayMode.Sparkline]: {
|
||||
renderer: SPARKLINE_RENDERER,
|
||||
getStyles: getSparklineCellStyles,
|
||||
testField: (field: Field) => field.type === FieldType.string,
|
||||
},
|
||||
[TableCellDisplayMode.Markdown]: {
|
||||
renderer: MARKDOWN_RENDERER,
|
||||
renderer: memo((props: TableCellRendererProps) => (
|
||||
<MarkdownCell field={props.field} rowIdx={props.rowIdx} disableSanitizeHtml={props.disableSanitizeHtml} />
|
||||
)),
|
||||
getStyles: getMarkdownCellStyles,
|
||||
testField: (field: Field) => field.type === FieldType.string,
|
||||
},
|
||||
[TableCellDisplayMode.Custom]: {
|
||||
renderer: memo((props: TableCellRendererProps) => {
|
||||
if (!isCustomCellOptions(props.cellOptions) || !props.cellOptions.cellComponent) {
|
||||
return null; // nonsensical case, but better to typeguard it than throw.
|
||||
}
|
||||
const CustomCellComponent = props.cellOptions.cellComponent;
|
||||
return (
|
||||
<CustomCellComponent field={props.field} rowIndex={props.rowIdx} frame={props.frame} value={props.value} />
|
||||
);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: come up with a more elegant way to handle this.
|
||||
const STRING_ONLY_RENDERERS = new Set<TableCellOptions['type']>([
|
||||
TableCellDisplayMode.Markdown,
|
||||
TableCellDisplayMode.Pill,
|
||||
]);
|
||||
|
||||
/** @internal */
|
||||
export function getCellRenderer(
|
||||
field: Field,
|
||||
@@ -145,15 +134,16 @@ export function getCellRenderer(
|
||||
): TableCellRenderer {
|
||||
const cellType = cellOptions?.type ?? TableCellDisplayMode.Auto;
|
||||
if (cellType === TableCellDisplayMode.Auto) {
|
||||
return CELL_RENDERERS[getAutoRendererDisplayMode(field)].renderer;
|
||||
return CELL_REGISTRY[getAutoRendererDisplayMode(field)].renderer;
|
||||
}
|
||||
|
||||
if (STRING_ONLY_RENDERERS.has(cellType) && field.type !== FieldType.string) {
|
||||
return AUTO_RENDERER;
|
||||
// if the field fails the test for a specific renderer, fallback to Auto
|
||||
if (CELL_REGISTRY[cellType]?.testField && CELL_REGISTRY[cellType].testField(field) !== true) {
|
||||
return AutoCellRenderer;
|
||||
}
|
||||
|
||||
// cautious fallback to Auto renderer in case some garbage cell type has been provided.
|
||||
return CELL_RENDERERS[cellType]?.renderer ?? AUTO_RENDERER;
|
||||
return CELL_REGISTRY[cellType]?.renderer ?? AutoCellRenderer;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -166,7 +156,7 @@ export function getCellSpecificStyles(
|
||||
if (cellType === TableCellDisplayMode.Auto) {
|
||||
return getAutoRendererStyles(theme, options, field);
|
||||
}
|
||||
return CELL_RENDERERS[cellType]?.getStyles?.(theme, options);
|
||||
return CELL_REGISTRY[cellType]?.getStyles?.(theme, options);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -177,7 +167,7 @@ export function getAutoRendererStyles(
|
||||
): string | undefined {
|
||||
const impliedDisplayMode = getAutoRendererDisplayMode(field);
|
||||
if (impliedDisplayMode !== TableCellDisplayMode.Auto) {
|
||||
return CELL_RENDERERS[impliedDisplayMode]?.getStyles?.(theme, options);
|
||||
return CELL_REGISTRY[impliedDisplayMode]?.getStyles?.(theme, options);
|
||||
}
|
||||
return getAutoCellStyles(theme, options);
|
||||
}
|
||||
|
||||
@@ -111,10 +111,7 @@ export function TableNG(props: TableNGProps) {
|
||||
} = props;
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getGridStyles, {
|
||||
enablePagination,
|
||||
transparent,
|
||||
});
|
||||
const styles = useStyles2(getGridStyles, enablePagination, transparent);
|
||||
const panelContext = usePanelContext();
|
||||
|
||||
const getCellActions = useCallback(
|
||||
@@ -328,7 +325,7 @@ export function TableNG(props: TableNGProps) {
|
||||
const footerStyles = getFooterStyles(justifyContent);
|
||||
const displayName = getDisplayName(field);
|
||||
const headerCellClass = getHeaderCellStyles(theme, justifyContent);
|
||||
const renderFieldCell = getCellRenderer(field, cellOptions);
|
||||
const CellType = getCellRenderer(field, cellOptions);
|
||||
|
||||
const cellInspect = isCellInspectEnabled(field);
|
||||
const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null);
|
||||
@@ -409,21 +406,21 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderFieldCell({
|
||||
cellOptions,
|
||||
frame,
|
||||
field,
|
||||
height,
|
||||
rowIdx,
|
||||
theme,
|
||||
value,
|
||||
width,
|
||||
timeRange,
|
||||
cellInspect,
|
||||
showFilters,
|
||||
getActions: getCellActions,
|
||||
disableSanitizeHtml,
|
||||
})}
|
||||
<CellType
|
||||
cellOptions={cellOptions}
|
||||
frame={frame}
|
||||
field={field}
|
||||
height={height}
|
||||
rowIdx={rowIdx}
|
||||
theme={theme}
|
||||
value={value}
|
||||
width={width}
|
||||
timeRange={timeRange}
|
||||
cellInspect={cellInspect}
|
||||
showFilters={showFilters}
|
||||
getActions={getCellActions}
|
||||
disableSanitizeHtml={disableSanitizeHtml}
|
||||
/>
|
||||
{showActions && (
|
||||
<TableCellActions
|
||||
field={field}
|
||||
@@ -496,7 +493,7 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
|
||||
// cached so we don't care about multiple calls.
|
||||
const height = rowHeightFn(props.row);
|
||||
const tooltipHeight = rowHeightFn(props.row);
|
||||
let tooltipStyle: CSSProperties | undefined;
|
||||
if (tooltipCanBeColorized) {
|
||||
const tooltipDisplayValue = tooltipField.display!(props.row[tooltipDisplayName]); // this is yet another call to field.display() for the tooltip field
|
||||
@@ -504,7 +501,7 @@ export function TableNG(props: TableNGProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCellTooltip {...tooltipProps} height={height} rowIdx={props.rowIdx} style={tooltipStyle}>
|
||||
<TableCellTooltip {...tooltipProps} height={tooltipHeight} rowIdx={props.rowIdx} style={tooltipStyle}>
|
||||
{renderBasicCellContent(props)}
|
||||
</TableCellTooltip>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import WKT from 'ol/format/WKT';
|
||||
import Geometry from 'ol/geom/Geometry';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { FieldType } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -9,8 +10,8 @@ import { TableCellInspectorMode } from '../../TableCellInspector';
|
||||
import { TableCellDisplayMode } from '../../types';
|
||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellActionsProps } from '../types';
|
||||
|
||||
export function TableCellActions(props: TableCellActionsProps) {
|
||||
const {
|
||||
export const TableCellActions = memo(
|
||||
({
|
||||
field,
|
||||
value,
|
||||
cellOptions,
|
||||
@@ -20,9 +21,7 @@ export function TableCellActions(props: TableCellActionsProps) {
|
||||
className,
|
||||
cellInspect,
|
||||
showFilters,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
}: TableCellActionsProps) => (
|
||||
// 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
|
||||
@@ -80,5 +79,5 @@ export function TableCellActions(props: TableCellActionsProps) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
+113
-113
@@ -1,4 +1,4 @@
|
||||
import { CSSProperties, ReactElement, useMemo, useState, useRef, useEffect, RefObject } from 'react';
|
||||
import { CSSProperties, ReactElement, useMemo, useState, useRef, useEffect, memo, RefObject } from 'react';
|
||||
import { DataGridHandle } from 'react-data-grid';
|
||||
|
||||
import { ActionModel, DataFrame, Field, GrafanaTheme2 } from '@grafana/data';
|
||||
@@ -10,7 +10,7 @@ import { TableCellOptions } from '../../types';
|
||||
import { getTooltipStyles } from '../styles';
|
||||
import { TableCellRenderer, TableCellRendererProps } from '../types';
|
||||
|
||||
export interface Props {
|
||||
export interface TableCellTooltipProps {
|
||||
cellOptions: TableCellOptions;
|
||||
children: ReactElement;
|
||||
classes: ReturnType<typeof getTooltipStyles>;
|
||||
@@ -30,130 +30,130 @@ export interface Props {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function TableCellTooltip({
|
||||
cellOptions,
|
||||
children,
|
||||
classes,
|
||||
className,
|
||||
data,
|
||||
disableSanitizeHtml,
|
||||
field,
|
||||
getActions,
|
||||
gridRef,
|
||||
height,
|
||||
placement,
|
||||
renderer,
|
||||
rowIdx,
|
||||
style,
|
||||
theme,
|
||||
tooltipField,
|
||||
width = 300,
|
||||
}: Props) {
|
||||
const rawValue = field.values[rowIdx];
|
||||
const tooltipCaretRef = useRef<HTMLDivElement>(null);
|
||||
export const TableCellTooltip = memo(
|
||||
({
|
||||
cellOptions,
|
||||
children,
|
||||
classes,
|
||||
className,
|
||||
data,
|
||||
disableSanitizeHtml,
|
||||
field,
|
||||
getActions,
|
||||
gridRef,
|
||||
height,
|
||||
placement,
|
||||
renderer: CellRenderer,
|
||||
rowIdx,
|
||||
style,
|
||||
theme,
|
||||
tooltipField,
|
||||
width = 300,
|
||||
}: TableCellTooltipProps) => {
|
||||
const rawValue = field.values[rowIdx];
|
||||
const tooltipCaretRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [pinned, setPinned] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [pinned, setPinned] = useState(false);
|
||||
|
||||
const show = hovered || pinned;
|
||||
const dynamicHeight = tooltipField.config.custom?.cellOptions?.dynamicHeight;
|
||||
const show = hovered || pinned;
|
||||
const dynamicHeight = tooltipField.config.custom?.cellOptions?.dynamicHeight;
|
||||
|
||||
useEffect(() => {
|
||||
if (pinned) {
|
||||
const gridRoot = gridRef.current?.element;
|
||||
useEffect(() => {
|
||||
if (pinned) {
|
||||
const gridRoot = gridRef.current?.element;
|
||||
|
||||
const windowListener = (ev: Event) => {
|
||||
if (ev.target === tooltipCaretRef.current) {
|
||||
return;
|
||||
}
|
||||
const windowListener = (ev: Event) => {
|
||||
if (ev.target === tooltipCaretRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPinned(false);
|
||||
window.removeEventListener('click', windowListener);
|
||||
};
|
||||
setPinned(false);
|
||||
window.removeEventListener('click', windowListener);
|
||||
};
|
||||
|
||||
window.addEventListener('click', windowListener);
|
||||
window.addEventListener('click', windowListener);
|
||||
|
||||
// right now, we kill the pinned tooltip on any form of scrolling to avoid awkward rendering
|
||||
// where the tooltip bumps up against the edge of the scrollable container. we could try to
|
||||
// kill the tooltip when it hits these boundaries rather than when scrolling starts.
|
||||
const scrollListener = () => {
|
||||
setPinned(false);
|
||||
};
|
||||
gridRoot?.addEventListener('scroll', scrollListener, { once: true });
|
||||
// right now, we kill the pinned tooltip on any form of scrolling to avoid awkward rendering
|
||||
// where the tooltip bumps up against the edge of the scrollable container. we could try to
|
||||
// kill the tooltip when it hits these boundaries rather than when scrolling starts.
|
||||
const scrollListener = () => {
|
||||
setPinned(false);
|
||||
};
|
||||
gridRoot?.addEventListener('scroll', scrollListener, { once: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', windowListener);
|
||||
gridRoot?.removeEventListener('scroll', scrollListener);
|
||||
};
|
||||
return () => {
|
||||
window.removeEventListener('click', windowListener);
|
||||
gridRoot?.removeEventListener('scroll', scrollListener);
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}, [pinned, gridRef]);
|
||||
|
||||
const rendererProps = useMemo(
|
||||
() =>
|
||||
({
|
||||
cellInspect: false,
|
||||
cellOptions,
|
||||
disableSanitizeHtml,
|
||||
field,
|
||||
frame: data,
|
||||
getActions,
|
||||
height,
|
||||
rowIdx,
|
||||
showFilters: false,
|
||||
theme,
|
||||
value: rawValue,
|
||||
width,
|
||||
}) satisfies TableCellRendererProps,
|
||||
[cellOptions, data, disableSanitizeHtml, field, getActions, height, rawValue, rowIdx, theme, width]
|
||||
);
|
||||
|
||||
const cellElement = tooltipCaretRef.current?.closest<HTMLElement>('.rdg-cell');
|
||||
|
||||
if (rawValue === null || rawValue === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return;
|
||||
}, [pinned, gridRef]);
|
||||
// TODO: perist the hover if you mouse out of the trigger and into the popover
|
||||
const onMouseLeave = () => setHovered(false);
|
||||
const onMouseEnter = () => setHovered(true);
|
||||
|
||||
const rendererProps = useMemo(
|
||||
() =>
|
||||
({
|
||||
cellInspect: false,
|
||||
cellOptions,
|
||||
disableSanitizeHtml,
|
||||
field,
|
||||
frame: data,
|
||||
getActions,
|
||||
height,
|
||||
rowIdx,
|
||||
showFilters: false,
|
||||
theme,
|
||||
value: rawValue,
|
||||
width,
|
||||
}) satisfies TableCellRendererProps,
|
||||
[cellOptions, data, disableSanitizeHtml, field, getActions, height, rawValue, rowIdx, theme, width]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{cellElement && (
|
||||
<Popover
|
||||
content={<CellRenderer {...rendererProps} />}
|
||||
show={show}
|
||||
placement={placement}
|
||||
wrapperClassName={classes.tooltipWrapper}
|
||||
className={className}
|
||||
style={{ ...style, minWidth: width, ...(!dynamicHeight && { height }) }}
|
||||
referenceElement={cellElement}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={(ev) => ev.stopPropagation()} // prevent click from bubbling to the global click listener for un-pinning
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper}
|
||||
/>
|
||||
)}
|
||||
|
||||
const cellElement = tooltipCaretRef.current?.closest<HTMLElement>('.rdg-cell');
|
||||
|
||||
if (rawValue === null || rawValue === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const body = <>{renderer(rendererProps)}</>;
|
||||
|
||||
// TODO: perist the hover if you mouse out of the trigger and into the popover
|
||||
const onMouseLeave = () => setHovered(false);
|
||||
const onMouseEnter = () => setHovered(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{cellElement && (
|
||||
<Popover
|
||||
content={body}
|
||||
show={show}
|
||||
placement={placement}
|
||||
wrapperClassName={classes.tooltipWrapper}
|
||||
className={className}
|
||||
style={{ ...style, minWidth: width, ...(!dynamicHeight && { height }) }}
|
||||
referenceElement={cellElement}
|
||||
{/* TODO: figure out an accessible way to trigger the tooltip. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classes.tooltipCaret}
|
||||
ref={tooltipCaretRef}
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.Tooltip.Caret}
|
||||
aria-pressed={pinned}
|
||||
onClick={() => setPinned((prev) => !prev)}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={(ev) => ev.stopPropagation()} // prevent click from bubbling to the global click listener for un-pinning
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.Tooltip.Wrapper}
|
||||
onBlur={onMouseLeave}
|
||||
onFocus={onMouseEnter}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TODO: figure out an accessible way to trigger the tooltip. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classes.tooltipCaret}
|
||||
ref={tooltipCaretRef}
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.Tooltip.Caret}
|
||||
aria-pressed={pinned}
|
||||
onClick={() => setPinned((prev) => !prev)}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onBlur={onMouseLeave}
|
||||
onFocus={onMouseEnter}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,10 +7,7 @@ import { COLUMN, TABLE } from './constants';
|
||||
import { TableCellStyles } from './types';
|
||||
import { getJustifyContent, TextAlign } from './utils';
|
||||
|
||||
export const getGridStyles = (
|
||||
theme: GrafanaTheme2,
|
||||
{ enablePagination, transparent }: { enablePagination?: boolean; transparent?: boolean }
|
||||
) => {
|
||||
export const getGridStyles = (theme: GrafanaTheme2, enablePagination?: boolean, transparent?: boolean) => {
|
||||
const bgColor = transparent ? theme.colors.background.canvas : theme.colors.background.primary;
|
||||
// this needs to be pre-calc'd since the theme colors have alpha and the border color becomes
|
||||
// unpredictable for background color cells
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, SyntheticEvent } from 'react';
|
||||
import { FC, SyntheticEvent } from 'react';
|
||||
import { Column } from 'react-data-grid';
|
||||
|
||||
import {
|
||||
@@ -150,7 +150,7 @@ export interface BaseTableProps {
|
||||
/* ---------------------------- Table cell props ---------------------------- */
|
||||
export interface TableNGProps extends BaseTableProps {}
|
||||
|
||||
export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
|
||||
export type TableCellRenderer = FC<TableCellRendererProps>;
|
||||
|
||||
export interface TableCellRendererProps {
|
||||
rowIdx: number;
|
||||
|
||||
Reference in New Issue
Block a user