diff --git a/packages/grafana-data/src/dataframe/index.ts b/packages/grafana-data/src/dataframe/index.ts index 95b7cf951c4..a16bc33e3bc 100644 --- a/packages/grafana-data/src/dataframe/index.ts +++ b/packages/grafana-data/src/dataframe/index.ts @@ -8,3 +8,4 @@ export * from './ArrayDataFrame'; export * from './DataFrameJSON'; export * from './StreamingDataFrame'; export * from './frameComparisons'; +export { anySeriesWithTimeField } from './utils'; diff --git a/packages/grafana-data/src/dataframe/utils.test.ts b/packages/grafana-data/src/dataframe/utils.test.ts new file mode 100644 index 00000000000..943a1d900bf --- /dev/null +++ b/packages/grafana-data/src/dataframe/utils.test.ts @@ -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(); + }); + }); +}); diff --git a/packages/grafana-data/src/dataframe/utils.ts b/packages/grafana-data/src/dataframe/utils.ts index 5e9f1300791..4044393d839 100644 --- a/packages/grafana-data/src/dataframe/utils.ts +++ b/packages/grafana-data/src/dataframe/utils.ts @@ -1,12 +1,27 @@ 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) { 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)); -}; +} + +/** + * 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; +} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index c59b50c546f..c66a37bbbc4 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -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 { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import React from 'react'; @@ -19,12 +19,12 @@ export const TimeSeriesPanel: React.FC = ({ onChangeTimeRange, replaceVariables, }) => { + const { sync } = usePanelContext(); + const getFieldLinks = (field: Field, rowIndex: number) => { return getFieldLinksForExplore({ field, rowIndex, range: timeRange }); }; - const { sync } = usePanelContext(); - if (!data || !data.series?.length) { return (
@@ -33,6 +33,14 @@ export const TimeSeriesPanel: React.FC = ({ ); } + if (!anySeriesWithTimeField(data.series)) { + return ( +
+

Missing time field in the data

+
+ ); + } + return (