Graph Panel: show warnings with actions (#23975)

This commit is contained in:
Ryan McKinley
2020-04-29 23:21:28 -07:00
committed by GitHub
parent 5116420e9a
commit efeb4c1341
7 changed files with 129 additions and 27 deletions

View File

@@ -262,7 +262,10 @@ export const isTableData = (data: any): data is DataFrame => data && data.hasOwn
export const isDataFrame = (data: any): data is DataFrame => data && data.hasOwnProperty('fields'); export const isDataFrame = (data: any): data is DataFrame => data && data.hasOwnProperty('fields');
export const toDataFrame = (data: any): DataFrame => { /**
* Inspect any object and return the results as a DataFrame
*/
export function toDataFrame(data: any): DataFrame {
if ('fields' in data) { if ('fields' in data) {
// DataFrameDTO does not have length // DataFrameDTO does not have length
if ('length' in data) { if ('length' in data) {
@@ -292,7 +295,7 @@ export const toDataFrame = (data: any): DataFrame => {
console.warn('Can not convert', data); console.warn('Can not convert', data);
throw new Error('Unsupported data format'); throw new Error('Unsupported data format');
}; }
export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData => { export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData => {
const { fields } = frame; const { fields } = frame;

View File

@@ -351,8 +351,15 @@ class GraphElement {
.appendTo(this.elem); .appendTo(this.elem);
} }
if (this.ctrl.dataWarning) { const { dataWarning } = this.ctrl;
$(`<div class="datapoints-warning flot-temp-elem">${this.ctrl.dataWarning.title}</div>`).appendTo(this.elem); if (dataWarning) {
const msg = $(`<div class="datapoints-warning flot-temp-elem">${dataWarning.title}</div>`);
if (dataWarning.action) {
$(`<button class="btn btn-secondary">${dataWarning.actionText}</button>`)
.click(dataWarning.action)
.appendTo(msg);
}
msg.appendTo(this.elem);
} }
this.thresholdManager.draw(plot); this.thresholdManager.draw(plot);

View File

@@ -150,7 +150,7 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
}; };
elem.mouseleave(() => { elem.mouseleave(() => {
if (panel.tooltip.shared) { if (panel.tooltip?.shared) {
const plot = elem.data().plot; const plot = elem.data().plot;
if (plot) { if (plot) {
$tooltip.detach(); $tooltip.detach();

View File

@@ -20,6 +20,11 @@ import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/l
import { auto } from 'angular'; import { auto } from 'angular';
import { AnnotationsSrv } from 'app/features/annotations/all'; import { AnnotationsSrv } from 'app/features/annotations/all';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { DataWarning } from './types';
import { getLocationSrv } from '@grafana/runtime';
import { getDataTimeRange } from './utils';
import { changePanelPlugin } from 'app/features/dashboard/state/actions';
import { dispatch } from 'app/store/store';
class GraphCtrl extends MetricsPanelCtrl { class GraphCtrl extends MetricsPanelCtrl {
static template = template; static template = template;
@@ -33,7 +38,7 @@ class GraphCtrl extends MetricsPanelCtrl {
alertState: any; alertState: any;
annotationsPromise: any; annotationsPromise: any;
dataWarning: any; dataWarning?: DataWarning;
colors: any = []; colors: any = [];
subTabIndex: number; subTabIndex: number;
processor: DataProcessor; processor: DataProcessor;
@@ -218,27 +223,7 @@ class GraphCtrl extends MetricsPanelCtrl {
this.linkVariableSuggestions = getDataLinksVariableSuggestions(data); this.linkVariableSuggestions = getDataLinksVariableSuggestions(data);
this.dataWarning = null; this.dataWarning = this.getDataWarning();
const datapointsCount = this.seriesList.reduce((prev, series) => {
return prev + series.datapoints.length;
}, 0);
if (datapointsCount === 0) {
this.dataWarning = {
title: 'No data',
tip: 'No data returned from query',
};
} else {
for (const series of this.seriesList) {
if (series.isOutsideRange) {
this.dataWarning = {
title: 'Data outside time range',
tip: 'Can be caused by timezone mismatch or missing time filter in query',
};
break;
}
}
}
this.annotationsPromise.then( this.annotationsPromise.then(
(result: { alertState: any; annotations: any }) => { (result: { alertState: any; annotations: any }) => {
@@ -261,6 +246,66 @@ class GraphCtrl extends MetricsPanelCtrl {
); );
} }
getDataWarning(): DataWarning {
const datapointsCount = this.seriesList.reduce((prev, series) => {
return prev + series.datapoints.length;
}, 0);
if (datapointsCount === 0) {
if (this.dataList) {
for (const frame of this.dataList) {
if (frame.length && frame.fields?.length) {
return {
title: 'Unable to graph data',
tip: 'Data exists, but is not timeseries',
actionText: 'Switch to table view',
action: () => {
console.log('Change from graph to table');
dispatch(changePanelPlugin(this.panel, 'table'));
},
};
}
}
}
return {
title: 'No data',
tip: 'No data returned from query',
};
}
// Look for data points outside time range
for (const series of this.seriesList) {
if (!series.isOutsideRange) {
continue;
}
const dataWarning: DataWarning = {
title: 'Data outside time range',
tip: 'Can be caused by timezone mismatch or missing time filter in query',
};
const range = getDataTimeRange(this.dataList);
if (range) {
dataWarning.actionText = 'Zoom to data';
dataWarning.action = () => {
getLocationSrv().update({
partial: true,
query: {
from: range.from,
to: range.to,
},
});
};
}
return dataWarning;
}
return null;
}
onRender() { onRender() {
if (!this.seriesList) { if (!this.seriesList) {
return; return;

View File

@@ -0,0 +1,6 @@
export interface DataWarning {
title: string;
tip: string;
action?: () => void;
actionText?: string;
}

View File

@@ -0,0 +1,17 @@
import { toDataFrame, FieldType } from '@grafana/data';
import { getDataTimeRange } from './utils';
describe('DataFrame utility functions', () => {
const frame = toDataFrame({
fields: [
{ name: 'fist', type: FieldType.time, values: [2, 3, 5] },
{ name: 'second', type: FieldType.time, values: [7, 8, 9] },
{ name: 'third', type: FieldType.number, values: [2000, 3000, 1000] },
],
});
it('Should find time range', () => {
const range = getDataTimeRange([frame]);
expect(range!.from).toEqual(2);
expect(range!.to).toEqual(9);
});
});

View File

@@ -0,0 +1,24 @@
import { DataFrame, ReducerID, reduceField, AbsoluteTimeRange, FieldType } from '@grafana/data';
/**
* Find the min and max time that covers all data
*/
export function getDataTimeRange(frames: DataFrame[]): AbsoluteTimeRange | undefined {
const range: AbsoluteTimeRange = {
from: Number.MAX_SAFE_INTEGER,
to: Number.MIN_SAFE_INTEGER,
};
let found = false;
const reducers = [ReducerID.min, ReducerID.max];
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type === FieldType.time) {
const calcs = reduceField({ field, reducers });
range.from = Math.min(range.from, calcs[ReducerID.min]);
range.to = Math.max(range.to, calcs[ReducerID.max]);
found = true;
}
}
}
return found ? range : undefined;
}