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 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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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