Table: Support DataLinks and Actions in SparklineCell (#112244)

* Table: Support DataLinks in SparklineCell

* add data links to sparkline gdev

* fix migrator test

* Clean up single action use case
This commit is contained in:
Paul Marbach
2025-10-30 10:04:08 -04:00
committed by GitHub
parent 05dc9b2be1
commit c487952279
7 changed files with 61 additions and 27 deletions
@@ -208,7 +208,27 @@
"value": 80
}
]
}
},
"actions": [
{
"fetch": {
"body": "{}",
"headers": [["Content-Type", "application/json"]],
"method": "GET",
"queryParams": [],
"url": "/api/health"
},
"title": "Get instance health",
"type": "fetch"
}
],
"links": [
{
"targetBlank": true,
"title": "Google Grafana",
"url": "https://google.com/search?q=grafana"
}
]
},
"overrides": []
},
@@ -592,4 +612,4 @@
"title": "Panel Tests - Table - Sparklines",
"uid": "d6373b49-1957-4f00-9218-ee2120d3ecd9",
"weekStart": ""
}
}
@@ -204,7 +204,27 @@
"value": 80
}
]
}
},
"actions": [
{
"fetch": {
"body": "{}",
"headers": [["Content-Type", "application/json"]],
"method": "GET",
"queryParams": [],
"url": "/api/health"
},
"title": "Get instance health",
"type": "fetch"
}
],
"links": [
{
"targetBlank": true,
"title": "Google Grafana",
"url": "https://google.com/search?q=grafana"
}
]
},
"overrides": []
},
@@ -17,6 +17,7 @@ import {
import { measureText } from '../../../../utils/measureText';
import { FormattedValueDisplay } from '../../../FormattedValueDisplay/FormattedValueDisplay';
import { Sparkline } from '../../../Sparkline/Sparkline';
import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink';
import { SparklineCellProps, TableCellStyles } from '../types';
import { getAlignmentFactor, getCellOptions, prepareSparklineValue } from '../utils';
@@ -38,7 +39,11 @@ export const SparklineCell = (props: SparklineCellProps) => {
const sparkline = prepareSparklineValue(value, field);
if (!sparkline) {
return <>{field.config.noValue || t('grafana-ui.table.sparkline.no-data', 'no data')}</>;
return (
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
{field.config.noValue || t('grafana-ui.table.sparkline.no-data', 'no data')}
</MaybeWrapWithLink>
);
}
// Get the step from the first two values to null-fill the x-axis based on timerange
@@ -87,10 +92,10 @@ export const SparklineCell = (props: SparklineCellProps) => {
}
return (
<>
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
{valueElement}
<Sparkline width={width - valueWidth} height={25} sparkline={sparkline} config={config} theme={theme} />
</>
</MaybeWrapWithLink>
);
};
@@ -107,8 +112,12 @@ function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions {
export const getStyles: TableCellStyles = (theme, { textAlign }) =>
css({
width: '100%',
gap: theme.spacing(1),
justifyContent: 'space-between',
...(textAlign === 'right' && { flexDirection: 'row-reverse' }),
'&, & > a': {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: theme.spacing(1),
...(textAlign === 'right' && { flexDirection: 'row-reverse' }),
},
});
@@ -94,7 +94,6 @@ import {
predicateByName,
shouldTextOverflow,
shouldTextWrap,
withDataLinksActionsTooltip,
getSummaryCellTextAlign,
parseStyleJson,
IS_SAFARI_26,
@@ -406,7 +405,6 @@ export function TableNG(props: TableNGProps) {
const result: FromFieldsResult = {
columns: [],
cellRootRenderers: {},
colsWithTooltip: {},
};
let lastRowIdx = -1;
@@ -464,7 +462,6 @@ export function TableNG(props: TableNGProps) {
const shouldOverflow =
!IS_SAFARI_26 && rowHeight !== 'auto' && (shouldTextOverflow(field) || Boolean(maxRowHeight));
const textWrap = rowHeight === 'auto' || shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType);
const canBeColorized = canFieldBeColorized(cellType, applyToRowBgFn);
const cellStyleOptions: TableCellStyleOptions = {
textAlign,
@@ -473,8 +470,6 @@ export function TableNG(props: TableNGProps) {
maxHeight: maxRowHeight,
};
result.colsWithTooltip[displayName] = withTooltip;
const defaultCellStyles = getDefaultCellStyles(theme, cellStyleOptions);
const cellSpecificStyles = getCellSpecificStyles(cellType, field, theme, cellStyleOptions);
const linkStyles = getLinkStyles(theme, canBeColorized);
@@ -737,7 +732,7 @@ export function TableNG(props: TableNGProps) {
);
const [nestedFieldWidths] = useColWidths(firstRowNestedData?.fields ?? [], availableWidth);
const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => {
const { columns, cellRootRenderers } = useMemo(() => {
const result = fromFields(visibleFields, widths);
// if nested frames are present, augment the columns to include the nested table expander column.
@@ -805,7 +800,6 @@ export function TableNG(props: TableNGProps) {
const field = columns[column.idx].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
@@ -34,4 +34,5 @@ export const MaybeWrapWithLink = memo(({ field, rowIdx, children }: MaybeWrapWit
// raw value
return children;
});
MaybeWrapWithLink.displayName = 'MaybeWrapWithLink';
@@ -308,7 +308,6 @@ export type CellRootRenderer = (key: React.Key, props: CellRendererProps<TableRo
export interface FromFieldsResult {
columns: TableColumn[];
cellRootRenderers: Record<string, CellRootRenderer>;
colsWithTooltip: Record<string, boolean>;
}
export interface FooterFieldState extends FieldState {
@@ -955,15 +955,6 @@ export function getApplyToRowBgFn(
}
}
/** @internal */
export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDisplayMode) {
return (
cellType !== TableCellDisplayMode.DataLinks &&
cellType !== TableCellDisplayMode.Actions &&
(field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1
);
}
/** @internal */
export function canFieldBeColorized(
cellType: TableCellDisplayMode,