mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Graph Panel: show warnings with actions (#23975)
This commit is contained in:
@@ -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 toDataFrame = (data: any): DataFrame => {
|
||||
/**
|
||||
* Inspect any object and return the results as a DataFrame
|
||||
*/
|
||||
export function toDataFrame(data: any): DataFrame {
|
||||
if ('fields' in data) {
|
||||
// DataFrameDTO does not have length
|
||||
if ('length' in data) {
|
||||
@@ -292,7 +295,7 @@ export const toDataFrame = (data: any): DataFrame => {
|
||||
|
||||
console.warn('Can not convert', data);
|
||||
throw new Error('Unsupported data format');
|
||||
};
|
||||
}
|
||||
|
||||
export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData => {
|
||||
const { fields } = frame;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
public/app/plugins/panel/graph/types.ts
Normal file
6
public/app/plugins/panel/graph/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface DataWarning {
|
||||
title: string;
|
||||
tip: string;
|
||||
action?: () => void;
|
||||
actionText?: string;
|
||||
}
|
||||
17
public/app/plugins/panel/graph/utils.test.ts
Normal file
17
public/app/plugins/panel/graph/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
24
public/app/plugins/panel/graph/utils.ts
Normal file
24
public/app/plugins/panel/graph/utils.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user