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

@@ -351,8 +351,15 @@ class GraphElement {
.appendTo(this.elem);
}
if (this.ctrl.dataWarning) {
$(`<div class="datapoints-warning flot-temp-elem">${this.ctrl.dataWarning.title}</div>`).appendTo(this.elem);
const { dataWarning } = this.ctrl;
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);

View File

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

View File

@@ -20,6 +20,11 @@ import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/l
import { auto } from 'angular';
import { AnnotationsSrv } from 'app/features/annotations/all';
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 {
static template = template;
@@ -33,7 +38,7 @@ class GraphCtrl extends MetricsPanelCtrl {
alertState: any;
annotationsPromise: any;
dataWarning: any;
dataWarning?: DataWarning;
colors: any = [];
subTabIndex: number;
processor: DataProcessor;
@@ -218,27 +223,7 @@ class GraphCtrl extends MetricsPanelCtrl {
this.linkVariableSuggestions = getDataLinksVariableSuggestions(data);
this.dataWarning = null;
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.dataWarning = this.getDataWarning();
this.annotationsPromise.then(
(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() {
if (!this.seriesList) {
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;
}