DataLinks: Handle getLinks() regen during data updates and frame joining (#83654)

This commit is contained in:
Leon Sorokin 2024-02-29 13:46:53 -06:00 committed by GitHub
parent c9d8d8713b
commit 42b55aedbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 91 additions and 82 deletions

View File

@ -7,10 +7,13 @@ import {
DataFrame,
DataHoverClearEvent,
DataHoverEvent,
DataLinkPostProcessor,
Field,
FieldMatcherID,
fieldMatchers,
FieldType,
getLinksSupplier,
InterpolateFunction,
LegacyGraphHoverEvent,
TimeRange,
TimeZone,
@ -49,6 +52,8 @@ export interface GraphNGProps extends Themeable2 {
propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
replaceVariables: InterpolateFunction;
dataLinkPostProcessor?: DataLinkPostProcessor;
/**
* needed for propsToDiff to re-init the plot & config
@ -105,30 +110,67 @@ export class GraphNG extends Component<GraphNGProps, GraphNGState> {
prepState(props: GraphNGProps, withConfig = true) {
let state: GraphNGState = null as any;
const { frames, fields, preparePlotFrame } = props;
const { frames, fields, preparePlotFrame, replaceVariables, dataLinkPostProcessor } = props;
const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame;
const preparePlotFrameFn = preparePlotFrame ?? defaultPreparePlotFrame;
const matchY = fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum]));
// if there are data links, we have to keep all fields so they're index-matched, then filter out dimFields.y
const withLinks = frames.some((frame) => frame.fields.some((field) => (field.config.links?.length ?? 0) > 0));
const dimFields = fields ?? {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: withLinks ? () => true : matchY,
};
const alignedFrame = preparePlotFrameFn(frames, dimFields, props.timeRange);
const alignedFrame = preparePlotFrameFn(
frames,
fields || {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
},
props.timeRange
);
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
if (alignedFrame) {
let alignedFrameFinal = alignedFrame;
if (withLinks) {
const timeZone = Array.isArray(this.props.timeZone) ? this.props.timeZone[0] : this.props.timeZone;
alignedFrame.fields.forEach((field) => {
field.getLinks = getLinksSupplier(
alignedFrame,
field,
{
...field.state?.scopedVars,
__dataContext: {
value: {
data: [alignedFrame],
field: field,
frame: alignedFrame,
frameIndex: 0,
},
},
},
replaceVariables,
timeZone,
dataLinkPostProcessor
);
});
// filter join field and dimFields.y
alignedFrameFinal = {
...alignedFrame,
fields: alignedFrame.fields.filter((field, i) => i === 0 || matchY(field, alignedFrame, [alignedFrame])),
};
}
let config = this.state?.config;
if (withConfig) {
config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange);
config = props.prepConfig(alignedFrameFinal, this.props.frames, this.getTimeRange);
pluginLog('GraphNG', false, 'config prepared', config);
}
state = {
alignedFrame,
alignedFrame: alignedFrameFinal,
config,
};

View File

@ -93,6 +93,7 @@ export class TimelineChart extends React.Component<TimelineProps> {
prepConfig={this.prepConfig}
propsToDiff={propsToDiff}
renderLegend={this.renderLegend}
dataLinkPostProcessor={this.panelContext?.dataLinkPostProcessor}
/>
);
}

View File

@ -3,7 +3,7 @@ import React, { useEffect, useRef } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { BehaviorSubject } from 'rxjs';
import { DataFrame, TimeRange } from '@grafana/data';
import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data';
import { VisibilityMode } from '@grafana/schema';
import { LegendDisplayMode, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
@ -15,6 +15,9 @@ interface LogTimelineViewerProps {
onPointerMove?: (seriesIdx: number, pointerIdx: number) => void;
}
// noop
const replaceVariables: InterpolateFunction = (v) => v;
export const LogTimelineViewer = React.memo(({ frames, timeRange, onPointerMove = noop }: LogTimelineViewerProps) => {
const theme = useTheme2();
const { setupCursorTracking } = useCursorTimelinePosition(onPointerMove);
@ -45,6 +48,7 @@ export const LogTimelineViewer = React.memo(({ frames, timeRange, onPointerMove
{ label: 'NoData', color: theme.colors.info.main, yAxis: 1 },
{ label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 },
]}
replaceVariables={replaceVariables}
>
{(builder) => {
setupCursorTracking(builder);

View File

@ -69,9 +69,9 @@ const propsToDiff: Array<string | PropDiffFn> = [
interface Props extends PanelProps<Options> {}
export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZone, id }: Props) => {
export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZone, id, replaceVariables }: Props) => {
const theme = useTheme2();
const { eventBus } = usePanelContext();
const { eventBus, dataLinkPostProcessor } = usePanelContext();
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
const isToolTipOpen = useRef<boolean>(false);
@ -326,6 +326,8 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
structureRev={structureRev}
width={width}
height={height}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
>
{(config) => {
if (showNewVizTooltips && options.tooltip.mode !== TooltipDisplayMode.None) {

View File

@ -42,7 +42,8 @@ export const CandlestickPanel = ({
onChangeTimeRange,
replaceVariables,
}: CandlestickPanelProps) => {
const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, showThresholds } = usePanelContext();
const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, showThresholds, dataLinkPostProcessor } =
usePanelContext();
const theme = useTheme2();
@ -256,6 +257,8 @@ export const CandlestickPanel = ({
tweakAxis={tweakAxis}
tweakScale={tweakScale}
options={options}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
>
{(uplotConfig, alignedDataFrame) => {
alignedDataFrame.fields.forEach((field) => {

View File

@ -70,7 +70,7 @@ export const StateTimelinePanel = ({
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { sync, canAddAnnotations } = usePanelContext();
const { sync, canAddAnnotations, dataLinkPostProcessor } = usePanelContext();
const onCloseToolTip = () => {
isToolTipOpen.current = false;
@ -184,6 +184,8 @@ export const StateTimelinePanel = ({
legendItems={legendItems}
{...options}
mode={TimelineMode.Changes}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
>
{(builder, alignedFrame) => {
if (oldConfig.current !== builder && !showNewVizTooltips) {

View File

@ -45,6 +45,7 @@ export const StatusHistoryPanel = ({
options,
width,
height,
replaceVariables,
onChangeTimeRange,
}: TimelinePanelProps) => {
const theme = useTheme2();
@ -67,7 +68,7 @@ export const StatusHistoryPanel = ({
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { sync, canAddAnnotations } = usePanelContext();
const { sync, canAddAnnotations, dataLinkPostProcessor } = usePanelContext();
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
@ -213,6 +214,8 @@ export const StatusHistoryPanel = ({
legendItems={legendItems}
{...options}
mode={TimelineMode.Samples}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
>
{(builder, alignedFrame) => {
if (oldConfig.current !== builder && !showNewVizTooltips) {

View File

@ -4,7 +4,7 @@ export const getDataLinks = (field: Field, rowIdx: number) => {
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
if (field.getLinks) {
if ((field.config.links?.length ?? 0) > 0 && field.getLinks != null) {
const v = field.values[rowIdx];
const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => {

View File

@ -18,7 +18,7 @@ import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin';
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
import { getPrepareTimeseriesSuggestion } from './suggestions';
import { getTimezones, isTooltipScrollable, prepareGraphableFields, regenerateLinksSupplier } from './utils';
import { getTimezones, isTooltipScrollable, prepareGraphableFields } from './utils';
interface TimeSeriesPanelProps extends PanelProps<Options> {}
@ -96,18 +96,10 @@ export const TimeSeriesPanel = ({
height={height}
legend={options.legend}
options={options}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
>
{(uplotConfig, alignedDataFrame) => {
if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
alignedDataFrame = regenerateLinksSupplier(
alignedDataFrame,
frames,
replaceVariables,
timeZone,
dataLinkPostProcessor
);
}
return (
<>
<KeyboardPlugin config={uplotConfig} />

View File

@ -3,10 +3,7 @@ import {
Field,
FieldType,
getDisplayProcessor,
getLinksSupplier,
GrafanaTheme2,
DataLinkPostProcessor,
InterpolateFunction,
isBooleanUnit,
TimeRange,
cacheFieldDisplayNames,
@ -266,43 +263,6 @@ export function getTimezones(timezones: string[] | undefined, defaultTimezone: s
return timezones.map((v) => (v?.length ? v : defaultTimezone));
}
export function regenerateLinksSupplier(
alignedDataFrame: DataFrame,
frames: DataFrame[],
replaceVariables: InterpolateFunction,
timeZone: string,
dataLinkPostProcessor?: DataLinkPostProcessor
): DataFrame {
alignedDataFrame.fields.forEach((field) => {
if (field.state?.origin?.frameIndex === undefined || frames[field.state?.origin?.frameIndex] === undefined) {
return;
}
const tempFields: Field[] = [];
for (const frameField of frames[field.state?.origin?.frameIndex].fields) {
if (frameField.type === FieldType.string) {
tempFields.push(frameField);
}
}
const tempFrame: DataFrame = {
fields: [...alignedDataFrame.fields, ...tempFields],
length: alignedDataFrame.fields.length + tempFields.length,
};
field.getLinks = getLinksSupplier(
tempFrame,
field,
field.state!.scopedVars!,
replaceVariables,
timeZone,
dataLinkPostProcessor
);
});
return alignedDataFrame;
}
export const isTooltipScrollable = (tooltipOptions: VizTooltipOptions) => {
return tooltipOptions.mode === TooltipDisplayMode.Multi && tooltipOptions.maxHeight != null;
};

View File

@ -11,7 +11,7 @@ import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { findFieldIndex } from 'app/features/dimensions';
import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip';
import { isTooltipScrollable, prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils';
import { isTooltipScrollable, prepareGraphableFields } from '../timeseries/utils';
import { Options } from './panelcfg.gen';
@ -109,18 +109,10 @@ export const TrendPanel = ({
legend={options.legend}
options={options}
preparePlotFrame={preparePlotFrameTimeless}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
>
{(uPlotConfig, alignedDataFrame) => {
if (alignedDataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
alignedDataFrame = regenerateLinksSupplier(
alignedDataFrame,
info.frames!,
replaceVariables,
timeZone,
dataLinkPostProcessor
);
}
return (
<>
<KeyboardPlugin config={uPlotConfig} />

View File

@ -154,7 +154,15 @@ function buildData({ dataLinkTitle = 'Grafana', field1Name = 'field_1', field2Na
{
name: field2Name,
type: FieldType.number,
config: {},
config: {
links: [
{
title: dataLinkTitle,
targetBlank: true,
url: 'http://www.someWebsite.com',
},
],
},
values: [500, 300, 150, 250, 600, 500, 700, 400, 540, 630, 460, 250, 500, 400, 800, 930, 360],
getLinks: (_config: ValueLinkConfig) => [
{