mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Do not crash timeseries panel if there is no time series data in the response (#33993)
This commit is contained in:
@@ -8,3 +8,4 @@ export * from './ArrayDataFrame';
|
|||||||
export * from './DataFrameJSON';
|
export * from './DataFrameJSON';
|
||||||
export * from './StreamingDataFrame';
|
export * from './StreamingDataFrame';
|
||||||
export * from './frameComparisons';
|
export * from './frameComparisons';
|
||||||
|
export { anySeriesWithTimeField } from './utils';
|
||||||
|
|||||||
78
packages/grafana-data/src/dataframe/utils.test.ts
Normal file
78
packages/grafana-data/src/dataframe/utils.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { toDataFrame } from './processDataFrame';
|
||||||
|
import { FieldType } from '../types';
|
||||||
|
import { anySeriesWithTimeField } from './utils';
|
||||||
|
|
||||||
|
describe('anySeriesWithTimeField', () => {
|
||||||
|
describe('single frame', () => {
|
||||||
|
test('without time field', () => {
|
||||||
|
const frameA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(anySeriesWithTimeField([frameA])).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with time field', () => {
|
||||||
|
const frameA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(anySeriesWithTimeField([frameA])).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple frames', () => {
|
||||||
|
test('without time field', () => {
|
||||||
|
const frameA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const frameB = toDataFrame({
|
||||||
|
fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }],
|
||||||
|
});
|
||||||
|
expect(anySeriesWithTimeField([frameA, frameB])).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with time field in any frame', () => {
|
||||||
|
const frameA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const frameB = toDataFrame({
|
||||||
|
fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }],
|
||||||
|
});
|
||||||
|
const frameC = toDataFrame({
|
||||||
|
fields: [{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(anySeriesWithTimeField([frameA, frameB, frameC])).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with time field in a all frames', () => {
|
||||||
|
const frameA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const frameB = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(anySeriesWithTimeField([frameA, frameB])).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,27 @@
|
|||||||
import { DataFrame, FieldType } from '../types/dataFrame';
|
import { DataFrame, FieldType } from '../types/dataFrame';
|
||||||
|
import { getTimeField } from './processDataFrame';
|
||||||
|
|
||||||
export const isTimeSerie = (frame: DataFrame): boolean => {
|
export function isTimeSerie(frame: DataFrame) {
|
||||||
if (frame.fields.length > 2) {
|
if (frame.fields.length > 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!frame.fields.find((field) => field.type === FieldType.time);
|
return Boolean(frame.fields.find((field) => field.type === FieldType.time));
|
||||||
};
|
}
|
||||||
|
|
||||||
export const isTimeSeries = (data: DataFrame[]): boolean => {
|
export function isTimeSeries(data: DataFrame[]) {
|
||||||
return !data.find((frame) => !isTimeSerie(frame));
|
return !data.find((frame) => !isTimeSerie(frame));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if there is any time field in the array of data frames
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function anySeriesWithTimeField(data: DataFrame[]) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const timeField = getTimeField(data[i]);
|
||||||
|
if (timeField.timeField !== undefined && timeField.timeIndex !== undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DashboardCursorSync, Field, PanelProps } from '@grafana/data';
|
import { anySeriesWithTimeField, DashboardCursorSync, Field, PanelProps } from '@grafana/data';
|
||||||
import { TooltipDisplayMode, usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin } from '@grafana/ui';
|
import { TooltipDisplayMode, usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin } from '@grafana/ui';
|
||||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -19,12 +19,12 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
|||||||
onChangeTimeRange,
|
onChangeTimeRange,
|
||||||
replaceVariables,
|
replaceVariables,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { sync } = usePanelContext();
|
||||||
|
|
||||||
const getFieldLinks = (field: Field, rowIndex: number) => {
|
const getFieldLinks = (field: Field, rowIndex: number) => {
|
||||||
return getFieldLinksForExplore({ field, rowIndex, range: timeRange });
|
return getFieldLinksForExplore({ field, rowIndex, range: timeRange });
|
||||||
};
|
};
|
||||||
|
|
||||||
const { sync } = usePanelContext();
|
|
||||||
|
|
||||||
if (!data || !data.series?.length) {
|
if (!data || !data.series?.length) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-empty">
|
<div className="panel-empty">
|
||||||
@@ -33,6 +33,14 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anySeriesWithTimeField(data.series)) {
|
||||||
|
return (
|
||||||
|
<div className="panel-empty">
|
||||||
|
<p>Missing time field in the data</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeSeries
|
<TimeSeries
|
||||||
frames={data.series}
|
frames={data.series}
|
||||||
|
|||||||
Reference in New Issue
Block a user