mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataLinks: Handle getLinks() regen during data updates and frame joining (#83654)
This commit is contained in:
parent
c9d8d8713b
commit
42b55aedbc
@ -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,
|
||||
};
|
||||
|
||||
|
@ -93,6 +93,7 @@ export class TimelineChart extends React.Component<TimelineProps> {
|
||||
prepConfig={this.prepConfig}
|
||||
propsToDiff={propsToDiff}
|
||||
renderLegend={this.renderLegend}
|
||||
dataLinkPostProcessor={this.panelContext?.dataLinkPostProcessor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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) => {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) => {
|
||||
|
@ -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} />
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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} />
|
||||
|
@ -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) => [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user