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
co-authored by Leon Sorokin Ryan McKinley
parent 69a38def63
commit 32fafffed7
14 changed files with 110 additions and 103 deletions
+1 -2
View File
@@ -5594,8 +5594,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Do not use any type assertions.", "4"]
], ],
"public/app/plugins/panel/heatmap/palettes.ts:5381": [ "public/app/plugins/panel/heatmap/palettes.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
@@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash'; 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 { map, mergeMap } from 'rxjs/operators';
import { import {
@@ -26,6 +26,7 @@ import {
toDataFrame, toDataFrame,
transformDataFrame, transformDataFrame,
preProcessPanelData, preProcessPanelData,
ApplyFieldOverrideOptions,
} from '@grafana/data'; } from '@grafana/data';
import { getTemplateSrv, toDataQueryError } from '@grafana/runtime'; import { getTemplateSrv, toDataQueryError } from '@grafana/runtime';
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
@@ -91,7 +92,9 @@ export class PanelQueryRunner {
getData(options: GetDataOptions): Observable<PanelData> { getData(options: GetDataOptions): Observable<PanelData> {
const { withFieldConfig, withTransforms } = options; const { withFieldConfig, withTransforms } = options;
let structureRev = 1; let structureRev = 1;
let lastData: DataFrame[] = []; let lastFieldConfig: ApplyFieldOverrideOptions | undefined = undefined;
let lastProcessedFrames: DataFrame[] = [];
let lastRawFrames: DataFrame[] = [];
let isFirstPacket = true; let isFirstPacket = true;
let lastConfigRev = -1; let lastConfigRev = -1;
@@ -105,97 +108,103 @@ export class PanelQueryRunner {
} }
return this.subject.pipe( return this.subject.pipe(
this.getTransformationsStream(withTransforms), mergeMap((data: PanelData) => {
map((data: PanelData) => { let fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
let processedData = data;
let streamingPacketWithSameSchema = false;
if (withFieldConfig && data.series?.length) { if (data.series === lastRawFrames && lastFieldConfig?.fieldConfig === fieldConfig?.fieldConfig) {
if (lastConfigRev === this.dataConfigSource.configRev) { return of({ ...data, structureRev, series: lastProcessedFrames });
const streamingDataFrame = data.series.find((data) => isStreamingDataFrame(data)) as }
| StreamingDataFrame
| undefined; 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 ( if (
streamingDataFrame && !streamingPacketWithSameSchema &&
!streamingDataFrame.packetInfo.schemaChanged && !compareArrayValues(lastProcessedFrames, processedData.series, compareDataFrameStructures)
// TODO: remove the condition below after fixing
// https://github.com/grafana/grafana/pull/41492#issuecomment-970281430
lastData[0].fields.length === streamingDataFrame.fields.length
) { ) {
processedData = { structureRev++;
...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;
} }
}
// Apply field defaults and overrides lastProcessedFrames = processedData.series;
let fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
if (fieldConfig != null && (isFirstPacket || !streamingPacketWithSameSchema)) { return { ...processedData, structureRev };
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 };
}) })
); );
} }
private getTransformationsStream = (withTransforms: boolean): MonoTypeOperatorFunction<PanelData> => { private applyTransformations(data: PanelData): Observable<PanelData> {
return (inputStream) => const transformations = this.dataConfigSource.getTransformations();
inputStream.pipe(
mergeMap((data) => {
if (!withTransforms) {
return of(data);
}
const transformations = this.dataConfigSource.getTransformations(); if (!transformations || transformations.length === 0) {
return of(data);
}
if (!transformations || transformations.length === 0) { const ctx: DataTransformContext = {
return of(data); interpolate: (v: string) => getTemplateSrv().replace(v, data?.request?.scopedVars),
} };
const ctx: DataTransformContext = { return transformDataFrame(transformations, data.series, ctx).pipe(map((series) => ({ ...data, series })));
interpolate: (v: string) => getTemplateSrv().replace(v, data?.request?.scopedVars), }
};
return transformDataFrame(transformations, data.series, ctx).pipe(map((series) => ({ ...data, series })));
})
);
};
async run(options: QueryRunnerOptions) { async run(options: QueryRunnerOptions) {
const { const {
@@ -216,7 +225,7 @@ export class PanelQueryRunner {
} = options; } = options;
if (isSharedDashboardQuery(datasource)) { if (isSharedDashboardQuery(datasource)) {
this.pipeToSubject(runSharedRequest(options, queries[0]), panelId); this.pipeToSubject(runSharedRequest(options, queries[0]), panelId, true);
return; 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) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
@@ -299,7 +308,7 @@ export class PanelQueryRunner {
this.subscription = panelData.subscribe({ this.subscription = panelData.subscribe({
next: (data) => { 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 // Store preprocessed query results for applying overrides later on in the pipeline
this.subject.next(this.lastResult); this.subject.next(this.lastResult);
}, },
@@ -98,7 +98,7 @@ export const BarChartPanel = ({ data, options, fieldConfig, width, height, timeZ
const frame0Ref = useRef<DataFrame>(); const frame0Ref = useRef<DataFrame>();
const colorByFieldRef = useRef<Field>(); 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; const chartDisplay = 'viz' in info ? info : null;
colorByFieldRef.current = chartDisplay?.colorByField; colorByFieldRef.current = chartDisplay?.colorByField;
@@ -49,7 +49,7 @@ export const CandlestickPanel = ({
const info = useMemo(() => { const info = useMemo(() => {
return prepareCandlestickFields(data.series, options, theme, timeRange); return prepareCandlestickFields(data.series, options, theme, timeRange);
}, [data, options, theme, timeRange]); }, [data.series, options, theme, timeRange]);
const { renderers, tweakScale, tweakAxis, shouldRenderPrice } = useMemo(() => { const { renderers, tweakScale, tweakAxis, shouldRenderPrice } = useMemo(() => {
let tweakScale = (opts: ScaleProps, forField: Field) => opts; let tweakScale = (opts: ScaleProps, forField: Field) => opts;
@@ -56,11 +56,11 @@ export const HeatmapPanel = ({
const info = useMemo(() => { const info = useMemo(() => {
try { try {
return prepareHeatmapData(data, options, theme, getFieldLinksSupplier); return prepareHeatmapData(data.series, data.annotations, options, theme, getFieldLinksSupplier);
} catch (ex) { } catch (ex) {
return { warning: `${ex}` }; return { warning: `${ex}` };
} }
}, [data, options, theme, getFieldLinksSupplier]); }, [data.series, data.annotations, options, theme, getFieldLinksSupplier]);
const facets = useMemo(() => { const facets = useMemo(() => {
let exemplarsXFacet: number[] = []; // "Time" field let exemplarsXFacet: number[] = []; // "Time" field
+3 -4
View File
@@ -8,7 +8,6 @@ import {
GrafanaTheme2, GrafanaTheme2,
LinkModel, LinkModel,
outerJoinDataFrames, outerJoinDataFrames,
PanelData,
ValueFormatter, ValueFormatter,
ValueLinkConfig, ValueLinkConfig,
} from '@grafana/data'; } from '@grafana/data';
@@ -56,17 +55,17 @@ export interface HeatmapData {
} }
export function prepareHeatmapData( export function prepareHeatmapData(
data: PanelData, frames: DataFrame[],
annotations: DataFrame[] | undefined,
options: PanelOptions, options: PanelOptions,
theme: GrafanaTheme2, theme: GrafanaTheme2,
getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array<LinkModel<Field>> getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array<LinkModel<Field>>
): HeatmapData { ): HeatmapData {
let frames = data.series;
if (!frames?.length) { if (!frames?.length) {
return {}; return {};
} }
const exemplars = data.annotations?.find((f) => f.name === 'exemplar'); const exemplars = annotations?.find((f) => f.name === 'exemplar');
if (getFieldLinks) { if (getFieldLinks) {
exemplars?.fields.forEach((field, index) => { exemplars?.fields.forEach((field, index) => {
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react'; 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 { config } from '@grafana/runtime';
import { import {
AxisPlacement, AxisPlacement,
@@ -48,7 +48,7 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
let isOrdinalY = false; let isOrdinalY = false;
try { 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; isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null;
} catch {} } catch {}
@@ -18,7 +18,7 @@ export class HeatmapSuggestionsSupplier {
return; return;
} }
const info = prepareHeatmapData(builder.data, defaultPanelOptions, config.theme2); const info = prepareHeatmapData(builder.data.series, undefined, defaultPanelOptions, config.theme2);
if (!info || info.warning) { if (!info || info.warning) {
return; return;
} }
@@ -13,7 +13,7 @@ export const HistogramPanel = ({ data, options, width, height }: Props) => {
const theme = useTheme2(); const theme = useTheme2();
const histogram = useMemo(() => { const histogram = useMemo(() => {
if (!data?.series?.length) { if (!data.series.length) {
return undefined; return undefined;
} }
@@ -71,8 +71,8 @@ export const StateTimelinePanel = ({
}; };
const { frames, warn } = useMemo( const { frames, warn } = useMemo(
() => prepareTimelineFields(data?.series, options.mergeValues ?? true, timeRange, theme), () => prepareTimelineFields(data.series, options.mergeValues ?? true, timeRange, theme),
[data, options.mergeValues, timeRange, theme] [data.series, options.mergeValues, timeRange, theme]
); );
const legendItems = useMemo( const legendItems = useMemo(
@@ -68,8 +68,8 @@ export const StatusHistoryPanel = ({
}; };
const { frames, warn } = useMemo( const { frames, warn } = useMemo(
() => prepareTimelineFields(data?.series, false, timeRange, theme), () => prepareTimelineFields(data.series, false, timeRange, theme),
[data, timeRange, theme] [data.series, timeRange, theme]
); );
const legendItems = useMemo( const legendItems = useMemo(
@@ -38,7 +38,7 @@ export const TimeSeriesPanel = ({
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange }); 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 timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
const suggestions = useMemo(() => { const suggestions = useMemo(() => {
if (frames?.length && frames.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) { if (frames?.length && frames.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) {
@@ -66,7 +66,7 @@ export const TrendPanel = ({
} }
return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) }; return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) };
}, [data, options.xField]); }, [data.series, options.xField]);
if (info.warning || !info.frames) { if (info.warning || !info.frames) {
return ( return (
@@ -90,7 +90,7 @@ export const XYChartPanel2 = (props: Props) => {
useEffect(() => { useEffect(() => {
if (oldOptions !== props.options || oldData?.structureRev !== props.data.structureRev) { if (oldOptions !== props.options || oldData?.structureRev !== props.data.structureRev) {
initSeries(); initSeries();
} else if (oldData !== props.data) { } else if (oldData?.series !== props.data.series) {
initFacets(); initFacets();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps