PanelQueryRunner: Return previous processed (transform+field config) series for loading state (#67768)

* Return same series for loading state

* Fix shared query issue

* include structureRev

* heatmap should depend on the series, not the wrapper

* fix more panels

* keep config for comparison

* fieldConfig.fieldConfig!

* cleanup

* cmon

---------

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Torkel Ödegaard 2023-05-07 15:21:40 +02:00 committed by GitHub
parent 69a38def63
commit 32fafffed7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 110 additions and 103 deletions

View File

@ -5594,8 +5594,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"]
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/panel/heatmap/palettes.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash';
import { MonoTypeOperatorFunction, Observable, of, ReplaySubject, Unsubscribable } from 'rxjs';
import { Observable, of, ReplaySubject, Unsubscribable } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import {
@ -26,6 +26,7 @@ import {
toDataFrame,
transformDataFrame,
preProcessPanelData,
ApplyFieldOverrideOptions,
} from '@grafana/data';
import { getTemplateSrv, toDataQueryError } from '@grafana/runtime';
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
@ -91,7 +92,9 @@ export class PanelQueryRunner {
getData(options: GetDataOptions): Observable<PanelData> {
const { withFieldConfig, withTransforms } = options;
let structureRev = 1;
let lastData: DataFrame[] = [];
let lastFieldConfig: ApplyFieldOverrideOptions | undefined = undefined;
let lastProcessedFrames: DataFrame[] = [];
let lastRawFrames: DataFrame[] = [];
let isFirstPacket = true;
let lastConfigRev = -1;
@ -105,97 +108,103 @@ export class PanelQueryRunner {
}
return this.subject.pipe(
this.getTransformationsStream(withTransforms),
map((data: PanelData) => {
let processedData = data;
let streamingPacketWithSameSchema = false;
mergeMap((data: PanelData) => {
let fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
if (withFieldConfig && data.series?.length) {
if (lastConfigRev === this.dataConfigSource.configRev) {
const streamingDataFrame = data.series.find((data) => isStreamingDataFrame(data)) as
| StreamingDataFrame
| undefined;
if (data.series === lastRawFrames && lastFieldConfig?.fieldConfig === fieldConfig?.fieldConfig) {
return of({ ...data, structureRev, series: lastProcessedFrames });
}
lastFieldConfig = fieldConfig;
lastRawFrames = data.series;
let dataWithTransforms = of(data);
if (withTransforms) {
dataWithTransforms = this.applyTransformations(data);
}
return dataWithTransforms.pipe(
map((data: PanelData) => {
let processedData = data;
let streamingPacketWithSameSchema = false;
if (withFieldConfig && data.series?.length) {
if (lastConfigRev === this.dataConfigSource.configRev) {
const streamingDataFrame = data.series.find((data) => isStreamingDataFrame(data)) as
| StreamingDataFrame
| undefined;
if (
streamingDataFrame &&
!streamingDataFrame.packetInfo.schemaChanged &&
// TODO: remove the condition below after fixing
// https://github.com/grafana/grafana/pull/41492#issuecomment-970281430
lastProcessedFrames[0].fields.length === streamingDataFrame.fields.length
) {
processedData = {
...processedData,
series: lastProcessedFrames.map((frame, frameIndex) => ({
...frame,
length: data.series[frameIndex].length,
fields: frame.fields.map((field, fieldIndex) => ({
...field,
values: data.series[frameIndex].fields[fieldIndex].values,
state: {
...field.state,
calcs: undefined,
range: undefined,
},
})),
})),
};
streamingPacketWithSameSchema = true;
}
}
if (fieldConfig != null && (isFirstPacket || !streamingPacketWithSameSchema)) {
lastConfigRev = this.dataConfigSource.configRev!;
processedData = {
...processedData,
series: applyFieldOverrides({
timeZone: data.request?.timezone ?? 'browser',
data: processedData.series,
...fieldConfig!,
}),
};
isFirstPacket = false;
}
}
if (
streamingDataFrame &&
!streamingDataFrame.packetInfo.schemaChanged &&
// TODO: remove the condition below after fixing
// https://github.com/grafana/grafana/pull/41492#issuecomment-970281430
lastData[0].fields.length === streamingDataFrame.fields.length
!streamingPacketWithSameSchema &&
!compareArrayValues(lastProcessedFrames, processedData.series, compareDataFrameStructures)
) {
processedData = {
...processedData,
series: lastData.map((frame, frameIndex) => ({
...frame,
length: data.series[frameIndex].length,
fields: frame.fields.map((field, fieldIndex) => ({
...field,
values: data.series[frameIndex].fields[fieldIndex].values,
state: {
...field.state,
calcs: undefined,
range: undefined,
},
})),
})),
};
streamingPacketWithSameSchema = true;
structureRev++;
}
}
// Apply field defaults and overrides
let fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
lastProcessedFrames = processedData.series;
if (fieldConfig != null && (isFirstPacket || !streamingPacketWithSameSchema)) {
lastConfigRev = this.dataConfigSource.configRev!;
processedData = {
...processedData,
series: applyFieldOverrides({
timeZone: data.request?.timezone ?? 'browser',
data: processedData.series,
...fieldConfig!,
}),
};
isFirstPacket = false;
}
}
if (
!streamingPacketWithSameSchema &&
!compareArrayValues(lastData, processedData.series, compareDataFrameStructures)
) {
structureRev++;
}
lastData = processedData.series;
return { ...processedData, structureRev };
return { ...processedData, structureRev };
})
);
})
);
}
private getTransformationsStream = (withTransforms: boolean): MonoTypeOperatorFunction<PanelData> => {
return (inputStream) =>
inputStream.pipe(
mergeMap((data) => {
if (!withTransforms) {
return of(data);
}
private applyTransformations(data: PanelData): Observable<PanelData> {
const transformations = this.dataConfigSource.getTransformations();
const transformations = this.dataConfigSource.getTransformations();
if (!transformations || transformations.length === 0) {
return of(data);
}
if (!transformations || transformations.length === 0) {
return of(data);
}
const ctx: DataTransformContext = {
interpolate: (v: string) => getTemplateSrv().replace(v, data?.request?.scopedVars),
};
const ctx: DataTransformContext = {
interpolate: (v: string) => getTemplateSrv().replace(v, data?.request?.scopedVars),
};
return transformDataFrame(transformations, data.series, ctx).pipe(map((series) => ({ ...data, series })));
})
);
};
return transformDataFrame(transformations, data.series, ctx).pipe(map((series) => ({ ...data, series })));
}
async run(options: QueryRunnerOptions) {
const {
@ -216,7 +225,7 @@ export class PanelQueryRunner {
} = options;
if (isSharedDashboardQuery(datasource)) {
this.pipeToSubject(runSharedRequest(options, queries[0]), panelId);
this.pipeToSubject(runSharedRequest(options, queries[0]), panelId, true);
return;
}
@ -284,7 +293,7 @@ export class PanelQueryRunner {
}
}
private pipeToSubject(observable: Observable<PanelData>, panelId?: number) {
private pipeToSubject(observable: Observable<PanelData>, panelId?: number, skipPreProcess = false) {
if (this.subscription) {
this.subscription.unsubscribe();
}
@ -299,7 +308,7 @@ export class PanelQueryRunner {
this.subscription = panelData.subscribe({
next: (data) => {
this.lastResult = preProcessPanelData(data, this.lastResult);
this.lastResult = skipPreProcess ? data : preProcessPanelData(data, this.lastResult);
// Store preprocessed query results for applying overrides later on in the pipeline
this.subject.next(this.lastResult);
},

View File

@ -98,7 +98,7 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
const frame0Ref = useRef<DataFrame>();
const colorByFieldRef = useRef<Field>();
const info = useMemo(() => prepareBarChartDisplayValues(data?.series, theme, options), [data, theme, options]);
const info = useMemo(() => prepareBarChartDisplayValues(data.series, theme, options), [data.series, theme, options]);
const chartDisplay = 'viz' in info ? info : null;
colorByFieldRef.current = chartDisplay?.colorByField;

View File

@ -49,7 +49,7 @@ export const CandlestickPanel = ({
const info = useMemo(() => {
return prepareCandlestickFields(data.series, options, theme, timeRange);
}, [data, options, theme, timeRange]);
}, [data.series, options, theme, timeRange]);
const { renderers, tweakScale, tweakAxis, shouldRenderPrice } = useMemo(() => {
let tweakScale = (opts: ScaleProps, forField: Field) => opts;

View File

@ -56,11 +56,11 @@ export const HeatmapPanel = ({
const info = useMemo(() => {
try {
return prepareHeatmapData(data, options, theme, getFieldLinksSupplier);
return prepareHeatmapData(data.series, data.annotations, options, theme, getFieldLinksSupplier);
} catch (ex) {
return { warning: `${ex}` };
}
}, [data, options, theme, getFieldLinksSupplier]);
}, [data.series, data.annotations, options, theme, getFieldLinksSupplier]);
const facets = useMemo(() => {
let exemplarsXFacet: number[] = []; // "Time" field

View File

@ -8,7 +8,6 @@ import {
GrafanaTheme2,
LinkModel,
outerJoinDataFrames,
PanelData,
ValueFormatter,
ValueLinkConfig,
} from '@grafana/data';
@ -56,17 +55,17 @@ export interface HeatmapData {
}
export function prepareHeatmapData(
data: PanelData,
frames: DataFrame[],
annotations: DataFrame[] | undefined,
options: PanelOptions,
theme: GrafanaTheme2,
getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array<LinkModel<Field>>
): HeatmapData {
let frames = data.series;
if (!frames?.length) {
return {};
}
const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
const exemplars = annotations?.find((f) => f.name === 'exemplar');
if (getFieldLinks) {
exemplars?.fields.forEach((field, index) => {

View File

@ -1,6 +1,6 @@
import React from 'react';
import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelData, PanelPlugin } from '@grafana/data';
import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data';
import { config } from '@grafana/runtime';
import {
AxisPlacement,
@ -48,7 +48,7 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
let isOrdinalY = false;
try {
const v = prepareHeatmapData({ series: context.data } as PanelData, opts, config.theme2);
const v = prepareHeatmapData(context.data, undefined, opts, config.theme2);
isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null;
} catch {}

View File

@ -18,7 +18,7 @@ export class HeatmapSuggestionsSupplier {
return;
}
const info = prepareHeatmapData(builder.data, defaultPanelOptions, config.theme2);
const info = prepareHeatmapData(builder.data.series, undefined, defaultPanelOptions, config.theme2);
if (!info || info.warning) {
return;
}

View File

@ -13,7 +13,7 @@ export const HistogramPanel = ({ data, options, width, height }: Props) => {
const theme = useTheme2();
const histogram = useMemo(() => {
if (!data?.series?.length) {
if (!data.series.length) {
return undefined;
}

View File

@ -71,8 +71,8 @@ export const StateTimelinePanel = ({
};
const { frames, warn } = useMemo(
() => prepareTimelineFields(data?.series, options.mergeValues ?? true, timeRange, theme),
[data, options.mergeValues, timeRange, theme]
() => prepareTimelineFields(data.series, options.mergeValues ?? true, timeRange, theme),
[data.series, options.mergeValues, timeRange, theme]
);
const legendItems = useMemo(

View File

@ -68,8 +68,8 @@ export const StatusHistoryPanel = ({
};
const { frames, warn } = useMemo(
() => prepareTimelineFields(data?.series, false, timeRange, theme),
[data, timeRange, theme]
() => prepareTimelineFields(data.series, false, timeRange, theme),
[data.series, timeRange, theme]
);
const legendItems = useMemo(

View File

@ -38,7 +38,7 @@ export const TimeSeriesPanel = ({
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange });
};
const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data, timeRange]);
const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data.series, timeRange]);
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
const suggestions = useMemo(() => {
if (frames?.length && frames.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) {

View File

@ -66,7 +66,7 @@ export const TrendPanel = ({
}
return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) };
}, [data, options.xField]);
}, [data.series, options.xField]);
if (info.warning || !info.frames) {
return (

View File

@ -90,7 +90,7 @@ export const XYChartPanel2 = (props: Props) => {
useEffect(() => {
if (oldOptions !== props.options || oldData?.structureRev !== props.data.structureRev) {
initSeries();
} else if (oldData !== props.data) {
} else if (oldData?.series !== props.data.series) {
initFacets();
}
// eslint-disable-next-line react-hooks/exhaustive-deps