Problems panel: convert to metrics panel
Problems panel: convert to metrics panel
This commit is contained in:
@@ -29,9 +29,9 @@
|
|||||||
"@babel/preset-env": "^7.7.7",
|
"@babel/preset-env": "^7.7.7",
|
||||||
"@babel/preset-react": "^7.6.3",
|
"@babel/preset-react": "^7.6.3",
|
||||||
"@emotion/core": "^10.0.27",
|
"@emotion/core": "^10.0.27",
|
||||||
"@grafana/data": "^6.7.0",
|
"@grafana/data": "^6.7.3",
|
||||||
"@grafana/runtime": "^6.7.0",
|
"@grafana/runtime": "^6.7.3",
|
||||||
"@grafana/ui": "^6.7.0",
|
"@grafana/ui": "^6.7.3",
|
||||||
"@popperjs/core": "^2.4.0",
|
"@popperjs/core": "^2.4.0",
|
||||||
"@types/classnames": "^2.2.6",
|
"@types/classnames": "^2.2.6",
|
||||||
"@types/grafana": "github:CorpGlory/types-grafana",
|
"@types/grafana": "github:CorpGlory/types-grafana",
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { parseLegacyVariableQuery } from '../utils';
|
import { parseLegacyVariableQuery } from '../utils';
|
||||||
import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui';
|
import { Select, Input } from '@grafana/ui';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
|
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
|
||||||
import { ZabbixInput } from './ZabbixInput';
|
import { ZabbixInput } from './ZabbixInput';
|
||||||
|
|
||||||
|
// FormLabel was renamed to InlineFormLabel in Grafana 7.0
|
||||||
|
import * as grafanaUi from '@grafana/ui';
|
||||||
|
const FormLabel = grafanaUi.FormLabel || (grafanaUi as any).InlineFormLabel;
|
||||||
|
|
||||||
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
||||||
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
|
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
|
||||||
{ value: VariableQueryTypes.Group, label: 'Group'},
|
{ value: VariableQueryTypes.Group, label: 'Group'},
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Plugin IDs
|
||||||
|
export const ZABBIX_PROBLEMS_PANEL_ID = 'alexanderzobnin-zabbix-triggers-panel';
|
||||||
|
export const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
|
||||||
|
|
||||||
// Data point
|
// Data point
|
||||||
export const DATAPOINT_VALUE = 0;
|
export const DATAPOINT_VALUE = 0;
|
||||||
export const DATAPOINT_TS = 1;
|
export const DATAPOINT_TS = 1;
|
||||||
@@ -8,6 +12,7 @@ export const MODE_ITSERVICE = 1;
|
|||||||
export const MODE_TEXT = 2;
|
export const MODE_TEXT = 2;
|
||||||
export const MODE_ITEMID = 3;
|
export const MODE_ITEMID = 3;
|
||||||
export const MODE_TRIGGERS = 4;
|
export const MODE_TRIGGERS = 4;
|
||||||
|
export const MODE_PROBLEMS = 5;
|
||||||
|
|
||||||
// Triggers severity
|
// Triggers severity
|
||||||
export const SEV_NOT_CLASSIFIED = 0;
|
export const SEV_NOT_CLASSIFIED = 0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import config from 'grafana/app/core/config';
|
import config from 'grafana/app/core/config';
|
||||||
|
import { contextSrv } from 'grafana/app/core/core';
|
||||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import * as migrations from './migrations';
|
import * as migrations from './migrations';
|
||||||
@@ -7,16 +8,41 @@ import * as metricFunctions from './metricFunctions';
|
|||||||
import * as c from './constants';
|
import * as c from './constants';
|
||||||
import dataProcessor from './dataProcessor';
|
import dataProcessor from './dataProcessor';
|
||||||
import responseHandler from './responseHandler';
|
import responseHandler from './responseHandler';
|
||||||
|
import problemsHandler from './problemsHandler';
|
||||||
import { Zabbix } from './zabbix/zabbix';
|
import { Zabbix } from './zabbix/zabbix';
|
||||||
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
|
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
|
||||||
import { VariableQueryTypes } from './types';
|
import { VariableQueryTypes, ShowProblemTypes } from './types';
|
||||||
|
|
||||||
const DEFAULT_ZABBIX_VERSION = 3;
|
const DEFAULT_ZABBIX_VERSION = 3;
|
||||||
|
|
||||||
export class ZabbixDatasource {
|
export class ZabbixDatasource {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
basicAuth: any;
|
||||||
|
withCredentials: any;
|
||||||
|
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
trends: boolean;
|
||||||
|
trendsFrom: string;
|
||||||
|
trendsRange: string;
|
||||||
|
cacheTTL: any;
|
||||||
|
alertingEnabled: boolean;
|
||||||
|
addThresholds: boolean;
|
||||||
|
alertingMinSeverity: string;
|
||||||
|
disableReadOnlyUsersAck: boolean;
|
||||||
|
zabbixVersion: string;
|
||||||
|
enableDirectDBConnection: boolean;
|
||||||
|
dbConnectionDatasourceId: number;
|
||||||
|
dbConnectionDatasourceName: string;
|
||||||
|
dbConnectionRetentionPolicy: string;
|
||||||
|
enableDebugLog: boolean;
|
||||||
|
zabbix: any;
|
||||||
|
|
||||||
|
replaceTemplateVars: (target: any, scopedVars?: any) => any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(instanceSettings, templateSrv, zabbixAlertingSrv) {
|
constructor(instanceSettings, private templateSrv, private zabbixAlertingSrv) {
|
||||||
this.templateSrv = templateSrv;
|
this.templateSrv = templateSrv;
|
||||||
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
||||||
|
|
||||||
@@ -43,7 +69,7 @@ export class ZabbixDatasource {
|
|||||||
this.trendsRange = jsonData.trendsRange || '4d';
|
this.trendsRange = jsonData.trendsRange || '4d';
|
||||||
|
|
||||||
// Set cache update interval
|
// Set cache update interval
|
||||||
var ttl = jsonData.cacheTTL || '1h';
|
const ttl = jsonData.cacheTTL || '1h';
|
||||||
this.cacheTTL = utils.parseInterval(ttl);
|
this.cacheTTL = utils.parseInterval(ttl);
|
||||||
|
|
||||||
// Alerting options
|
// Alerting options
|
||||||
@@ -61,7 +87,7 @@ export class ZabbixDatasource {
|
|||||||
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
|
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
|
||||||
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
|
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
|
||||||
|
|
||||||
let zabbixOptions = {
|
const zabbixOptions = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
@@ -103,7 +129,7 @@ export class ZabbixDatasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create request for each target
|
// Create request for each target
|
||||||
let promises = _.map(options.targets, t => {
|
const promises = _.map(options.targets, t => {
|
||||||
// Don't request for hidden targets
|
// Don't request for hidden targets
|
||||||
if (t.hide) {
|
if (t.hide) {
|
||||||
return [];
|
return [];
|
||||||
@@ -123,15 +149,15 @@ export class ZabbixDatasource {
|
|||||||
this.replaceTargetVariables(target, options);
|
this.replaceTargetVariables(target, options);
|
||||||
|
|
||||||
// Apply Time-related functions (timeShift(), etc)
|
// Apply Time-related functions (timeShift(), etc)
|
||||||
let timeFunctions = bindFunctionDefs(target.functions, 'Time');
|
const timeFunctions = bindFunctionDefs(target.functions, 'Time');
|
||||||
if (timeFunctions.length) {
|
if (timeFunctions.length) {
|
||||||
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
|
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
|
||||||
timeFrom = time_from;
|
timeFrom = time_from;
|
||||||
timeTo = time_to;
|
timeTo = time_to;
|
||||||
}
|
}
|
||||||
let timeRange = [timeFrom, timeTo];
|
const timeRange = [timeFrom, timeTo];
|
||||||
|
|
||||||
let useTrends = this.isUseTrends(timeRange);
|
const useTrends = this.isUseTrends(timeRange);
|
||||||
|
|
||||||
// Metrics or Text query
|
// Metrics or Text query
|
||||||
if (!target.queryType || target.queryType === c.MODE_METRICS || target.queryType === c.MODE_TEXT) {
|
if (!target.queryType || target.queryType === c.MODE_METRICS || target.queryType === c.MODE_TEXT) {
|
||||||
@@ -157,6 +183,9 @@ export class ZabbixDatasource {
|
|||||||
} else if (target.queryType === c.MODE_TRIGGERS) {
|
} else if (target.queryType === c.MODE_TRIGGERS) {
|
||||||
// Triggers query
|
// Triggers query
|
||||||
return this.queryTriggersData(target, timeRange);
|
return this.queryTriggersData(target, timeRange);
|
||||||
|
} else if (target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
// Problems query
|
||||||
|
return this.queryProblems(target, timeRange, options);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -175,7 +204,7 @@ export class ZabbixDatasource {
|
|||||||
*/
|
*/
|
||||||
queryNumericData(target, timeRange, useTrends, options) {
|
queryNumericData(target, timeRange, useTrends, options) {
|
||||||
let queryStart, queryEnd;
|
let queryStart, queryEnd;
|
||||||
let getItemOptions = {
|
const getItemOptions = {
|
||||||
itemtype: 'num'
|
itemtype: 'num'
|
||||||
};
|
};
|
||||||
return this.zabbix.getItemsFromTarget(target, getItemOptions)
|
return this.zabbix.getItemsFromTarget(target, getItemOptions)
|
||||||
@@ -185,7 +214,7 @@ export class ZabbixDatasource {
|
|||||||
}).then(result => {
|
}).then(result => {
|
||||||
queryEnd = new Date().getTime();
|
queryEnd = new Date().getTime();
|
||||||
if (this.enableDebugLog) {
|
if (this.enableDebugLog) {
|
||||||
console.debug(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
|
console.log(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@@ -212,18 +241,18 @@ export class ZabbixDatasource {
|
|||||||
|
|
||||||
getTrendValueType(target) {
|
getTrendValueType(target) {
|
||||||
// Find trendValue() function and get specified trend value
|
// Find trendValue() function and get specified trend value
|
||||||
var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
|
const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
|
||||||
var trendValueFunc = _.find(target.functions, func => {
|
const trendValueFunc = _.find(target.functions, func => {
|
||||||
return _.includes(trendFunctions, func.def.name);
|
return _.includes(trendFunctions, func.def.name);
|
||||||
});
|
});
|
||||||
return trendValueFunc ? trendValueFunc.params[0] : "avg";
|
return trendValueFunc ? trendValueFunc.params[0] : "avg";
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDataProcessingFunctions(timeseries_data, target) {
|
applyDataProcessingFunctions(timeseries_data, target) {
|
||||||
let transformFunctions = bindFunctionDefs(target.functions, 'Transform');
|
const transformFunctions = bindFunctionDefs(target.functions, 'Transform');
|
||||||
let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
|
const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
|
||||||
let filterFunctions = bindFunctionDefs(target.functions, 'Filter');
|
const filterFunctions = bindFunctionDefs(target.functions, 'Filter');
|
||||||
let aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
|
const aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
|
||||||
|
|
||||||
// Apply transformation functions
|
// Apply transformation functions
|
||||||
timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => {
|
timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => {
|
||||||
@@ -241,8 +270,8 @@ export class ZabbixDatasource {
|
|||||||
let dp = _.map(timeseries_data, 'datapoints');
|
let dp = _.map(timeseries_data, 'datapoints');
|
||||||
dp = utils.sequence(aggregationFunctions)(dp);
|
dp = utils.sequence(aggregationFunctions)(dp);
|
||||||
|
|
||||||
let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
|
const aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
|
||||||
let lastAgg = _.findLast(target.functions, func => {
|
const lastAgg = _.findLast(target.functions, func => {
|
||||||
return _.includes(aggFuncNames, func.def.name);
|
return _.includes(aggFuncNames, func.def.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -264,11 +293,11 @@ export class ZabbixDatasource {
|
|||||||
|
|
||||||
applyTimeShiftFunction(timeseries_data, target) {
|
applyTimeShiftFunction(timeseries_data, target) {
|
||||||
// Find timeShift() function and get specified interval
|
// Find timeShift() function and get specified interval
|
||||||
let timeShiftFunc = _.find(target.functions, (func) => {
|
const timeShiftFunc = _.find(target.functions, (func) => {
|
||||||
return func.def.name === 'timeShift';
|
return func.def.name === 'timeShift';
|
||||||
});
|
});
|
||||||
if (timeShiftFunc) {
|
if (timeShiftFunc) {
|
||||||
let shift = timeShiftFunc.params[0];
|
const shift = timeShiftFunc.params[0];
|
||||||
_.forEach(timeseries_data, (series) => {
|
_.forEach(timeseries_data, (series) => {
|
||||||
series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints);
|
series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints);
|
||||||
});
|
});
|
||||||
@@ -279,7 +308,7 @@ export class ZabbixDatasource {
|
|||||||
* Query target data for Text
|
* Query target data for Text
|
||||||
*/
|
*/
|
||||||
queryTextData(target, timeRange) {
|
queryTextData(target, timeRange) {
|
||||||
let options = {
|
const options = {
|
||||||
itemtype: 'text'
|
itemtype: 'text'
|
||||||
};
|
};
|
||||||
return this.zabbix.getItemsFromTarget(target, options)
|
return this.zabbix.getItemsFromTarget(target, options)
|
||||||
@@ -332,14 +361,14 @@ export class ZabbixDatasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queryTriggersData(target, timeRange) {
|
queryTriggersData(target, timeRange) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
return this.zabbix.getHostsFromTarget(target)
|
return this.zabbix.getHostsFromTarget(target)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
let [hosts, apps] = results;
|
const [hosts, apps] = results;
|
||||||
if (hosts.length) {
|
if (hosts.length) {
|
||||||
let hostids = _.map(hosts, 'hostid');
|
const hostids = _.map(hosts, 'hostid');
|
||||||
let appids = _.map(apps, 'applicationid');
|
const appids = _.map(apps, 'applicationid');
|
||||||
let options = {
|
const options = {
|
||||||
minSeverity: target.triggers.minSeverity,
|
minSeverity: target.triggers.minSeverity,
|
||||||
acknowledged: target.triggers.acknowledged,
|
acknowledged: target.triggers.acknowledged,
|
||||||
count: target.triggers.count,
|
count: target.triggers.count,
|
||||||
@@ -360,6 +389,74 @@ export class ZabbixDatasource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryProblems(target, timeRange, options) {
|
||||||
|
const [timeFrom, timeTo] = timeRange;
|
||||||
|
const userIsEditor = contextSrv.isEditor || contextSrv.isGrafanaAdmin;
|
||||||
|
|
||||||
|
let proxies;
|
||||||
|
let showAckButton = true;
|
||||||
|
|
||||||
|
const showProblems = target.showProblems || ShowProblemTypes.Problems;
|
||||||
|
const showProxy = target.options.hostProxy;
|
||||||
|
|
||||||
|
const getProxiesPromise = showProxy ? this.zabbix.getProxies() : () => [];
|
||||||
|
showAckButton = !this.disableReadOnlyUsersAck || userIsEditor;
|
||||||
|
|
||||||
|
// Replace template variables
|
||||||
|
const groupFilter = this.replaceTemplateVars(target.group?.filter, options.scopedVars);
|
||||||
|
const hostFilter = this.replaceTemplateVars(target.host?.filter, options.scopedVars);
|
||||||
|
const appFilter = this.replaceTemplateVars(target.application?.filter, options.scopedVars);
|
||||||
|
const proxyFilter = this.replaceTemplateVars(target.proxy?.filter, options.scopedVars);
|
||||||
|
|
||||||
|
const triggerFilter = this.replaceTemplateVars(target.trigger?.filter, options.scopedVars);
|
||||||
|
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, options.scopedVars);
|
||||||
|
|
||||||
|
const replacedTarget = {
|
||||||
|
...target,
|
||||||
|
trigger: { filter: triggerFilter },
|
||||||
|
tags: { filter: tagsFilter },
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggersOptions: any = {
|
||||||
|
showTriggers: showProblems
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showProblems !== ShowProblemTypes.Problems) {
|
||||||
|
triggersOptions.timeFrom = timeFrom;
|
||||||
|
triggersOptions.timeTo = timeTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const problemsPromises = Promise.all([
|
||||||
|
this.zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions, proxyFilter),
|
||||||
|
getProxiesPromise
|
||||||
|
])
|
||||||
|
.then(([triggers, sourceProxies]) => {
|
||||||
|
proxies = _.keyBy(sourceProxies, 'proxyid');
|
||||||
|
const eventids = _.compact(triggers.map(trigger => {
|
||||||
|
return trigger.lastEvent.eventid;
|
||||||
|
}));
|
||||||
|
return Promise.all([
|
||||||
|
this.zabbix.getExtendedEventData(eventids),
|
||||||
|
Promise.resolve(triggers)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(([events, triggers]) => {
|
||||||
|
problemsHandler.addEventTags(events, triggers);
|
||||||
|
problemsHandler.addAcknowledges(events, triggers);
|
||||||
|
return triggers;
|
||||||
|
})
|
||||||
|
.then(triggers => problemsHandler.setMaintenanceStatus(triggers))
|
||||||
|
.then(triggers => problemsHandler.setAckButtonStatus(triggers, showAckButton))
|
||||||
|
.then(triggers => problemsHandler.filterTriggersPre(triggers, replacedTarget))
|
||||||
|
.then(triggers => problemsHandler.addTriggerDataSource(triggers, target))
|
||||||
|
.then(triggers => problemsHandler.addTriggerHostProxy(triggers, proxies));
|
||||||
|
|
||||||
|
return problemsPromises.then(problems => {
|
||||||
|
const problemsDataFrame = problemsHandler.toDataFrame(problems);
|
||||||
|
return problemsDataFrame;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test connection to Zabbix API and external history DB.
|
* Test connection to Zabbix API and external history DB.
|
||||||
*/
|
*/
|
||||||
@@ -480,16 +577,16 @@ export class ZabbixDatasource {
|
|||||||
const timeRange = options.range || options.rangeRaw;
|
const timeRange = options.range || options.rangeRaw;
|
||||||
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
|
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
|
||||||
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
|
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
|
||||||
var annotation = options.annotation;
|
const annotation = options.annotation;
|
||||||
var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS;
|
const showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS;
|
||||||
|
|
||||||
// Show all triggers
|
// Show all triggers
|
||||||
let triggersOptions = {
|
const triggersOptions = {
|
||||||
showTriggers: c.SHOW_ALL_TRIGGERS,
|
showTriggers: c.SHOW_ALL_TRIGGERS,
|
||||||
hideHostsInMaintenance: false
|
hideHostsInMaintenance: false
|
||||||
};
|
};
|
||||||
|
|
||||||
var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}),
|
const getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}),
|
||||||
this.replaceTemplateVars(annotation.host, {}),
|
this.replaceTemplateVars(annotation.host, {}),
|
||||||
this.replaceTemplateVars(annotation.application, {}),
|
this.replaceTemplateVars(annotation.application, {}),
|
||||||
triggersOptions);
|
triggersOptions);
|
||||||
@@ -497,7 +594,7 @@ export class ZabbixDatasource {
|
|||||||
return getTriggers.then(triggers => {
|
return getTriggers.then(triggers => {
|
||||||
|
|
||||||
// Filter triggers by description
|
// Filter triggers by description
|
||||||
let triggerName = this.replaceTemplateVars(annotation.trigger, {});
|
const triggerName = this.replaceTemplateVars(annotation.trigger, {});
|
||||||
if (utils.isRegex(triggerName)) {
|
if (utils.isRegex(triggerName)) {
|
||||||
triggers = _.filter(triggers, trigger => {
|
triggers = _.filter(triggers, trigger => {
|
||||||
return utils.buildRegex(triggerName).test(trigger.description);
|
return utils.buildRegex(triggerName).test(trigger.description);
|
||||||
@@ -513,11 +610,11 @@ export class ZabbixDatasource {
|
|||||||
return Number(trigger.priority) >= Number(annotation.minseverity);
|
return Number(trigger.priority) >= Number(annotation.minseverity);
|
||||||
});
|
});
|
||||||
|
|
||||||
var objectids = _.map(triggers, 'triggerid');
|
const objectids = _.map(triggers, 'triggerid');
|
||||||
return this.zabbix
|
return this.zabbix
|
||||||
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
|
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
|
||||||
.then(events => {
|
.then(events => {
|
||||||
var indexedTriggers = _.keyBy(triggers, 'triggerid');
|
const indexedTriggers = _.keyBy(triggers, 'triggerid');
|
||||||
|
|
||||||
// Hide acknowledged events if option enabled
|
// Hide acknowledged events if option enabled
|
||||||
if (annotation.hideAcknowledged) {
|
if (annotation.hideAcknowledged) {
|
||||||
@@ -533,10 +630,10 @@ export class ZabbixDatasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show event type (OK or Problem)
|
// Show event type (OK or Problem)
|
||||||
let title = Number(event.value) ? 'Problem' : 'OK';
|
const title = Number(event.value) ? 'Problem' : 'OK';
|
||||||
|
|
||||||
let formattedAcknowledges = utils.formatAcknowledges(event.acknowledges);
|
const formattedAcknowledges = utils.formatAcknowledges(event.acknowledges);
|
||||||
let eventName = event.name || indexedTriggers[event.objectid].description;
|
const eventName = event.name || indexedTriggers[event.objectid].description;
|
||||||
return {
|
return {
|
||||||
annotation: annotation,
|
annotation: annotation,
|
||||||
time: event.clock * 1000,
|
time: event.clock * 1000,
|
||||||
@@ -555,8 +652,8 @@ export class ZabbixDatasource {
|
|||||||
* or empty object if no related triggers are finded.
|
* or empty object if no related triggers are finded.
|
||||||
*/
|
*/
|
||||||
alertQuery(options) {
|
alertQuery(options) {
|
||||||
let enabled_targets = filterEnabledTargets(options.targets);
|
const enabled_targets = filterEnabledTargets(options.targets);
|
||||||
let getPanelItems = _.map(enabled_targets, t => {
|
const getPanelItems = _.map(enabled_targets, t => {
|
||||||
let target = _.cloneDeep(t);
|
let target = _.cloneDeep(t);
|
||||||
target = migrations.migrate(target);
|
target = migrations.migrate(target);
|
||||||
this.replaceTargetVariables(target, options);
|
this.replaceTargetVariables(target, options);
|
||||||
@@ -565,8 +662,8 @@ export class ZabbixDatasource {
|
|||||||
|
|
||||||
return Promise.all(getPanelItems)
|
return Promise.all(getPanelItems)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
let items = _.flatten(results);
|
const items = _.flatten(results);
|
||||||
let itemids = _.map(items, 'itemid');
|
const itemids = _.map(items, 'itemid');
|
||||||
|
|
||||||
if (itemids.length === 0) {
|
if (itemids.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -584,12 +681,12 @@ export class ZabbixDatasource {
|
|||||||
|
|
||||||
let state = 'ok';
|
let state = 'ok';
|
||||||
|
|
||||||
let firedTriggers = _.filter(triggers, {value: '1'});
|
const firedTriggers = _.filter(triggers, {value: '1'});
|
||||||
if (firedTriggers.length) {
|
if (firedTriggers.length) {
|
||||||
state = 'alerting';
|
state = 'alerting';
|
||||||
}
|
}
|
||||||
|
|
||||||
let thresholds = _.map(triggers, trigger => {
|
const thresholds = _.map(triggers, trigger => {
|
||||||
return getTriggerThreshold(trigger.expression);
|
return getTriggerThreshold(trigger.expression);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -603,7 +700,7 @@ export class ZabbixDatasource {
|
|||||||
|
|
||||||
// Replace template variables
|
// Replace template variables
|
||||||
replaceTargetVariables(target, options) {
|
replaceTargetVariables(target, options) {
|
||||||
let parts = ['group', 'host', 'application', 'item'];
|
const parts = ['group', 'host', 'application', 'item'];
|
||||||
_.forEach(parts, p => {
|
_.forEach(parts, p => {
|
||||||
if (target[p] && target[p].filter) {
|
if (target[p] && target[p].filter) {
|
||||||
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
|
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
|
||||||
@@ -623,10 +720,10 @@ export class ZabbixDatasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isUseTrends(timeRange) {
|
isUseTrends(timeRange) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
let useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
|
const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
|
||||||
let useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
|
const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
|
||||||
let useTrends = this.trends && (
|
const useTrends = this.trends && (
|
||||||
(timeFrom < useTrendsFrom) ||
|
(timeFrom < useTrendsFrom) ||
|
||||||
(timeTo - timeFrom > useTrendsRange)
|
(timeTo - timeFrom > useTrendsRange)
|
||||||
);
|
);
|
||||||
@@ -635,20 +732,20 @@ export class ZabbixDatasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindFunctionDefs(functionDefs, category) {
|
function bindFunctionDefs(functionDefs, category) {
|
||||||
var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
|
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
|
||||||
var aggFuncDefs = _.filter(functionDefs, function(func) {
|
const aggFuncDefs = _.filter(functionDefs, func => {
|
||||||
return _.includes(aggregationFunctions, func.def.name);
|
return _.includes(aggregationFunctions, func.def.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
return _.map(aggFuncDefs, function(func) {
|
return _.map(aggFuncDefs, func => {
|
||||||
var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
|
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
|
||||||
return funcInstance.bindFunction(dataProcessor.metricFunctions);
|
return funcInstance.bindFunction(dataProcessor.metricFunctions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConsolidateBy(target) {
|
function getConsolidateBy(target) {
|
||||||
let consolidateBy;
|
let consolidateBy;
|
||||||
let funcDef = _.find(target.functions, func => {
|
const funcDef = _.find(target.functions, func => {
|
||||||
return func.def.name === 'consolidateBy';
|
return func.def.name === 'consolidateBy';
|
||||||
});
|
});
|
||||||
if (funcDef && funcDef.params && funcDef.params.length) {
|
if (funcDef && funcDef.params && funcDef.params.length) {
|
||||||
@@ -658,8 +755,8 @@ function getConsolidateBy(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downsampleSeries(timeseries_data, options) {
|
function downsampleSeries(timeseries_data, options) {
|
||||||
let defaultAgg = dataProcessor.aggregationFunctions['avg'];
|
const defaultAgg = dataProcessor.aggregationFunctions['avg'];
|
||||||
let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
const consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
||||||
return _.map(timeseries_data, timeseries => {
|
return _.map(timeseries_data, timeseries => {
|
||||||
if (timeseries.datapoints.length > options.maxDataPoints) {
|
if (timeseries.datapoints.length > options.maxDataPoints) {
|
||||||
timeseries.datapoints = dataProcessor
|
timeseries.datapoints = dataProcessor
|
||||||
@@ -691,7 +788,7 @@ export function zabbixTemplateFormat(value) {
|
|||||||
return utils.escapeRegex(value);
|
return utils.escapeRegex(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
var escapedValues = _.map(value, utils.escapeRegex);
|
const escapedValues = _.map(value, utils.escapeRegex);
|
||||||
return '(' + escapedValues.join('|') + ')';
|
return '(' + escapedValues.join('|') + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +808,7 @@ function zabbixItemIdsTemplateFormat(value) {
|
|||||||
* /$variable/ -> /a|b|c/ -> /a|b|c/
|
* /$variable/ -> /a|b|c/ -> /a|b|c/
|
||||||
*/
|
*/
|
||||||
function replaceTemplateVars(templateSrv, target, scopedVars) {
|
function replaceTemplateVars(templateSrv, target, scopedVars) {
|
||||||
var replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
|
let replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
|
||||||
if (target !== replacedTarget && !utils.isRegex(replacedTarget)) {
|
if (target !== replacedTarget && !utils.isRegex(replacedTarget)) {
|
||||||
replacedTarget = '/^' + replacedTarget + '$/';
|
replacedTarget = '/^' + replacedTarget + '$/';
|
||||||
}
|
}
|
||||||
@@ -725,8 +822,8 @@ function filterEnabledTargets(targets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerThreshold(expression) {
|
function getTriggerThreshold(expression) {
|
||||||
let thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
|
const thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
|
||||||
let finded_thresholds = expression.match(thresholdPattern);
|
const finded_thresholds = expression.match(thresholdPattern);
|
||||||
if (finded_thresholds && finded_thresholds.length >= 2) {
|
if (finded_thresholds && finded_thresholds.length >= 2) {
|
||||||
let threshold = finded_thresholds[1];
|
let threshold = finded_thresholds[1];
|
||||||
threshold = Number(threshold);
|
threshold = Number(threshold);
|
||||||
@@ -735,7 +832,3 @@ function getTriggerThreshold(expression) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for backward compatibility with lodash 2.4
|
|
||||||
if (!_.includes) {_.includes = _.contains;}
|
|
||||||
if (!_.keyBy) {_.keyBy = _.indexBy;}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import { isNumeric } from './utils';
|
||||||
|
|
||||||
var index = [];
|
const index = [];
|
||||||
var categories = {
|
const categories = {
|
||||||
Transform: [],
|
Transform: [],
|
||||||
Aggregate: [],
|
Aggregate: [],
|
||||||
Filter: [],
|
Filter: [],
|
||||||
@@ -298,11 +298,15 @@ addFuncDef({
|
|||||||
defaultParams: ['avg'],
|
defaultParams: ['avg'],
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(categories, function(funcList, catName) {
|
_.each(categories, (funcList, catName) => {
|
||||||
categories[catName] = _.sortBy(funcList, 'name');
|
categories[catName] = _.sortBy(funcList, 'name');
|
||||||
});
|
});
|
||||||
|
|
||||||
class FuncInstance {
|
class FuncInstance {
|
||||||
|
def: any;
|
||||||
|
params: any;
|
||||||
|
text: string;
|
||||||
|
|
||||||
constructor(funcDef, params) {
|
constructor(funcDef, params) {
|
||||||
this.def = funcDef;
|
this.def = funcDef;
|
||||||
|
|
||||||
@@ -318,13 +322,13 @@ class FuncInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bindFunction(metricFunctions) {
|
bindFunction(metricFunctions) {
|
||||||
var func = metricFunctions[this.def.name];
|
const func = metricFunctions[this.def.name];
|
||||||
if (func) {
|
if (func) {
|
||||||
|
|
||||||
// Bind function arguments
|
// Bind function arguments
|
||||||
var bindedFunc = func;
|
let bindedFunc = func;
|
||||||
var param;
|
let param;
|
||||||
for (var i = 0; i < this.params.length; i++) {
|
for (let i = 0; i < this.params.length; i++) {
|
||||||
param = this.params[i];
|
param = this.params[i];
|
||||||
|
|
||||||
// Convert numeric params
|
// Convert numeric params
|
||||||
@@ -341,23 +345,21 @@ class FuncInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(metricExp) {
|
render(metricExp) {
|
||||||
var str = this.def.name + '(';
|
const str = this.def.name + '(';
|
||||||
var parameters = _.map(this.params, function(value, index) {
|
const parameters = _.map(this.params, (value, index) => {
|
||||||
|
const paramType = this.def.params[index].type;
|
||||||
var paramType = this.def.params[index].type;
|
|
||||||
if (paramType === 'int' ||
|
if (paramType === 'int' ||
|
||||||
paramType === 'float' ||
|
paramType === 'float' ||
|
||||||
paramType === 'value_or_series' ||
|
paramType === 'value_or_series' ||
|
||||||
paramType === 'boolean') {
|
paramType === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
}
|
} else if (paramType === 'int_or_interval' && isNumeric(value)) {
|
||||||
else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "'" + value + "'";
|
return "'" + value + "'";
|
||||||
|
|
||||||
}, this);
|
});
|
||||||
|
|
||||||
if (metricExp) {
|
if (metricExp) {
|
||||||
parameters.unshift(metricExp);
|
parameters.unshift(metricExp);
|
||||||
@@ -378,16 +380,15 @@ class FuncInstance {
|
|||||||
// handle optional parameters
|
// handle optional parameters
|
||||||
// if string contains ',' and next param is optional, split and update both
|
// if string contains ',' and next param is optional, split and update both
|
||||||
if (this._hasMultipleParamsInString(strValue, index)) {
|
if (this._hasMultipleParamsInString(strValue, index)) {
|
||||||
_.each(strValue.split(','), function(partVal, idx) {
|
_.each(strValue.split(','), (partVal, idx) => {
|
||||||
this.updateParam(partVal.trim(), idx);
|
this.updateParam(partVal.trim(), idx);
|
||||||
}, this);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strValue === '' && this.def.params[index].optional) {
|
if (strValue === '' && this.def.params[index].optional) {
|
||||||
this.params.splice(index, 1);
|
this.params.splice(index, 1);
|
||||||
}
|
}else {
|
||||||
else {
|
|
||||||
this.params[index] = strValue;
|
this.params[index] = strValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +401,7 @@ class FuncInstance {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = this.def.name + '(';
|
let text = this.def.name + '(';
|
||||||
text += this.params.join(', ');
|
text += this.params.join(', ');
|
||||||
text += ')';
|
text += ')';
|
||||||
this.text = text;
|
this.text = text;
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
|
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
|
||||||
<label class="gf-form-label query-keyword width-8">Format As</label>
|
<label class="gf-form-label query-keyword width-7">Format As</label>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
|
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
<!-- Select Group -->
|
<!-- Select Group -->
|
||||||
<div class="gf-form max-width-20">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-7">Group</label>
|
<label class="gf-form-label query-keyword width-7">Group</label>
|
||||||
@@ -71,8 +71,8 @@
|
|||||||
}"></input>
|
}"></input>
|
||||||
</div>
|
</div>
|
||||||
<!-- Select Host -->
|
<!-- Select Host -->
|
||||||
<div class="gf-form">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-8">Host</label>
|
<label class="gf-form-label query-keyword width-7">Host</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
ng-model="ctrl.target.host.filter"
|
ng-model="ctrl.target.host.filter"
|
||||||
bs-typeahead="ctrl.getHostNames"
|
bs-typeahead="ctrl.getHostNames"
|
||||||
@@ -86,12 +86,27 @@
|
|||||||
}">
|
}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Proxy</label>
|
||||||
|
<input type="text"
|
||||||
|
ng-model="ctrl.target.proxy.filter"
|
||||||
|
bs-typeahead="ctrl.getProxyNames"
|
||||||
|
ng-blur="ctrl.onTargetBlur()"
|
||||||
|
data-min-length=0
|
||||||
|
data-items=100
|
||||||
|
class="gf-form-input width-14"
|
||||||
|
ng-class="{
|
||||||
|
'zbx-variable': ctrl.isVariable(ctrl.target.proxy.filter),
|
||||||
|
'zbx-regex': ctrl.isRegex(ctrl.target.proxy.filter)
|
||||||
|
}">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
<!-- Select Application -->
|
<!-- Select Application -->
|
||||||
<div class="gf-form max-width-20">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-7">Application</label>
|
<label class="gf-form-label query-keyword width-7">Application</label>
|
||||||
@@ -109,8 +124,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Select Item -->
|
<!-- Select Item -->
|
||||||
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT">
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT">
|
||||||
<label class="gf-form-label query-keyword width-8">Item</label>
|
<label class="gf-form-label query-keyword width-7">Item</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
ng-model="ctrl.target.item.filter"
|
ng-model="ctrl.target.item.filter"
|
||||||
bs-typeahead="ctrl.getItemNames"
|
bs-typeahead="ctrl.getItemNames"
|
||||||
@@ -124,9 +139,48 @@
|
|||||||
}">
|
}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form max-width-23" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
<label class="gf-form-label query-keyword width-8">Min Severity</label>
|
<label class="gf-form-label query-keyword width-7">Problem</label>
|
||||||
<div class="gf-form-select-wrapper width-16">
|
<input type="text"
|
||||||
|
ng-model="ctrl.trigger.filter"
|
||||||
|
ng-blur="ctrl.onTargetBlur()"
|
||||||
|
placeholder="Problem name"
|
||||||
|
class="gf-form-input"
|
||||||
|
ng-style="ctrl.target.trigger.style"
|
||||||
|
ng-class="{
|
||||||
|
'zbx-variable': ctrl.isVariable(ctrl.target.trigger.filter),
|
||||||
|
'zbx-regex': ctrl.isRegex(ctrl.target.trigger.filter)
|
||||||
|
}"
|
||||||
|
empty-to-null>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form max-width-20">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Tags</label>
|
||||||
|
<input type="text" class="gf-form-input width-14"
|
||||||
|
ng-model="ctrl.target.tags.filter"
|
||||||
|
ng-blur="ctrl.onTargetBlur()"
|
||||||
|
placeholder="tag1:value1, tag2:value2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Show</label>
|
||||||
|
<div class="gf-form-select-wrapper max-width-20">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.target.showProblems"
|
||||||
|
ng-options="v.value as v.text for v in ctrl.showProblemsOptions"
|
||||||
|
ng-change="ctrl.onTargetBlur()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Min Severity</label>
|
||||||
|
<div class="gf-form-select-wrapper width-14">
|
||||||
<select class="gf-form-input"
|
<select class="gf-form-input"
|
||||||
ng-change="ctrl.onTargetBlur()"
|
ng-change="ctrl.onTargetBlur()"
|
||||||
ng-model="ctrl.target.triggers.minSeverity"
|
ng-model="ctrl.target.triggers.minSeverity"
|
||||||
@@ -134,47 +188,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
|
|
||||||
<label class="gf-form-label query-keyword width-8">Acknowledged</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-change="ctrl.onTargetBlur()"
|
|
||||||
ng-model="ctrl.target.triggers.acknowledged"
|
|
||||||
ng-options="a.value as a.text for a in ctrl.ackFilters">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<gf-form-switch class="gf-form" label="Count" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
|
|
||||||
checked="ctrl.target.triggers.count" on-change="ctrl.onTargetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<label class="gf-form-label gf-form-label--grow">
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
<a ng-click="ctrl.toggleQueryOptions()" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS">
|
|
||||||
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
|
|
||||||
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
|
|
||||||
{{ctrl.queryOptionsText}}
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Query options -->
|
<!-- Text mode options -->
|
||||||
<div class="gf-form-group" ng-if="ctrl.showQueryOptions">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
|
||||||
<div class="gf-form offset-width-7" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS">
|
<!-- Text metric regex -->
|
||||||
<gf-form-switch class="gf-form" label-class="width-10"
|
<div class="gf-form max-width-20">
|
||||||
label="Show disabled items"
|
<label class="gf-form-label query-keyword width-7">Text filter</label>
|
||||||
checked="ctrl.target.options.showDisabledItems"
|
<input type="text"
|
||||||
on-change="ctrl.onQueryOptionChange()">
|
class="gf-form-input"
|
||||||
</gf-form-switch>
|
ng-model="ctrl.target.textFilter"
|
||||||
|
spellcheck='false'
|
||||||
|
placeholder="Text filter (regex)"
|
||||||
|
ng-blur="ctrl.onTargetBlur()">
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form offset-width-7" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
|
|
||||||
<gf-form-switch class="gf-form" label-class="width-10"
|
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
|
||||||
label="Skip empty values"
|
</gf-form-switch>
|
||||||
checked="ctrl.target.options.skipEmptyValues"
|
<div class="gf-form gf-form--grow">
|
||||||
on-change="ctrl.onQueryOptionChange()">
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</gf-form-switch>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -215,23 +251,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text mode options -->
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
|
<label class="gf-form-label gf-form-label--grow">
|
||||||
<!-- Text metric regex -->
|
<a ng-click="ctrl.toggleQueryOptions()">
|
||||||
<div class="gf-form max-width-20">
|
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
|
||||||
<label class="gf-form-label query-keyword width-7">Text filter</label>
|
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
|
||||||
<input type="text"
|
{{ctrl.queryOptionsText}}
|
||||||
class="gf-form-input"
|
</a>
|
||||||
ng-model="ctrl.target.textFilter"
|
</label>
|
||||||
spellcheck='false'
|
</div>
|
||||||
placeholder="Text filter (regex)"
|
|
||||||
ng-blur="ctrl.onTargetBlur()">
|
<!-- Query options -->
|
||||||
|
<div class="gf-form-group offset-width-7" ng-if="ctrl.showQueryOptions">
|
||||||
|
<div class="gf-form" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-10"
|
||||||
|
label="Show disabled items"
|
||||||
|
checked="ctrl.target.options.showDisabledItems"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-10"
|
||||||
|
label="Skip empty values"
|
||||||
|
checked="ctrl.target.options.skipEmptyValues"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
|
<div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
|
||||||
</gf-form-switch>
|
<gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
|
||||||
<div class="gf-form gf-form--grow">
|
label-class="width-9"
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
label="Count"
|
||||||
|
checked="ctrl.target.triggers.count"
|
||||||
|
on-change="ctrl.onTargetBlur()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
|
||||||
|
<label class="gf-form-label width-9">Acknowledged</label>
|
||||||
|
<div class="gf-form-select-wrapper width-12">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-change="ctrl.onTargetBlur()"
|
||||||
|
ng-model="ctrl.target.options.acknowledged"
|
||||||
|
ng-options="a.value as a.text for a in ctrl.ackFilters">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">Sort by</label>
|
||||||
|
<div class="gf-form-select-wrapper width-12">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.target.options.sortProblems"
|
||||||
|
ng-options="f.value as f.text for f in ctrl.sortByFields"
|
||||||
|
ng-change="ctrl.onQueryOptionChange()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<gf-form-switch class="gf-form"
|
||||||
|
label-class="width-9"
|
||||||
|
label="Hosts in maintenance"
|
||||||
|
checked="ctrl.target.options.hostsInMaintenance"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<gf-form-switch class="gf-form"
|
||||||
|
label-class="width-9"
|
||||||
|
label="Host proxy"
|
||||||
|
checked="ctrl.target.options.hostProxy"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">Limit triggers</label>
|
||||||
|
<input class="gf-form-input width-5"
|
||||||
|
type="number" placeholder="100"
|
||||||
|
ng-model="ctrl.target.options.limit"
|
||||||
|
ng-model-onblur ng-change="ctrl.onQueryOptionChange()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</query-editor-row>
|
</query-editor-row>
|
||||||
|
|||||||
142
src/datasource-zabbix/problemsHandler.ts
Normal file
142
src/datasource-zabbix/problemsHandler.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
import TableModel from 'grafana/app/core/table_model';
|
||||||
|
import * as utils from '../datasource-zabbix/utils';
|
||||||
|
import * as c from './constants';
|
||||||
|
import { DataFrame, Field, FieldType, ArrayVector } from '@grafana/data';
|
||||||
|
|
||||||
|
export function addEventTags(events, triggers) {
|
||||||
|
_.each(triggers, trigger => {
|
||||||
|
const event = _.find(events, event => {
|
||||||
|
return event.eventid === trigger.lastEvent.eventid;
|
||||||
|
});
|
||||||
|
if (event && event.tags && event.tags.length) {
|
||||||
|
trigger.tags = event.tags;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addAcknowledges(events, triggers) {
|
||||||
|
// Map events to triggers
|
||||||
|
_.each(triggers, trigger => {
|
||||||
|
const event = _.find(events, event => {
|
||||||
|
return event.eventid === trigger.lastEvent.eventid;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
trigger.acknowledges = event.acknowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trigger.lastEvent.eventid) {
|
||||||
|
trigger.lastEvent = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMaintenanceStatus(triggers) {
|
||||||
|
_.each(triggers, (trigger) => {
|
||||||
|
const maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1');
|
||||||
|
trigger.maintenance = maintenance_status;
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAckButtonStatus(triggers, showAckButton) {
|
||||||
|
_.each(triggers, (trigger) => {
|
||||||
|
trigger.showAckButton = showAckButton;
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTriggerDataSource(triggers, target) {
|
||||||
|
_.each(triggers, (trigger) => {
|
||||||
|
trigger.datasource = target.datasource;
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTriggerHostProxy(triggers, proxies) {
|
||||||
|
triggers.forEach(trigger => {
|
||||||
|
if (trigger.hosts && trigger.hosts.length) {
|
||||||
|
const host = trigger.hosts[0];
|
||||||
|
if (host.proxy_hostid !== '0') {
|
||||||
|
const hostProxy = proxies[host.proxy_hostid];
|
||||||
|
host.proxy = hostProxy ? hostProxy.host : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterTriggersPre(triggerList, replacedTarget) {
|
||||||
|
// Filter triggers by description
|
||||||
|
const triggerFilter = replacedTarget.trigger.filter;
|
||||||
|
if (triggerFilter) {
|
||||||
|
triggerList = filterTriggers(triggerList, triggerFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by tags
|
||||||
|
if (replacedTarget.tags.filter) {
|
||||||
|
let tagsFilter = replacedTarget.tags.filter;
|
||||||
|
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
||||||
|
tagsFilter = tagsFilter.replace('/^', '').replace('$/', '');
|
||||||
|
const tags = utils.parseTags(tagsFilter);
|
||||||
|
triggerList = _.filter(triggerList, trigger => {
|
||||||
|
return _.every(tags, tag => {
|
||||||
|
return _.find(trigger.tags, t => t.tag === tag.tag && (!tag.value || t.value === tag.value));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by maintenance status
|
||||||
|
if (!replacedTarget.options.hostsInMaintenance) {
|
||||||
|
triggerList = _.filter(triggerList, (trigger) => !trigger.maintenance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTriggers(triggers, triggerFilter) {
|
||||||
|
if (utils.isRegex(triggerFilter)) {
|
||||||
|
return _.filter(triggers, trigger => {
|
||||||
|
return utils.buildRegex(triggerFilter).test(trigger.description);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _.filter(triggers, trigger => {
|
||||||
|
return trigger.description === triggerFilter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDataFrame(problems: any[]): DataFrame {
|
||||||
|
const problemsField: Field<any> = {
|
||||||
|
name: 'Problems',
|
||||||
|
type: FieldType.other,
|
||||||
|
values: new ArrayVector(problems),
|
||||||
|
config: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: DataFrame = {
|
||||||
|
name: 'problems',
|
||||||
|
fields: [problemsField],
|
||||||
|
length: problems.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const problemsHandler = {
|
||||||
|
addEventTags,
|
||||||
|
addAcknowledges,
|
||||||
|
addTriggerDataSource,
|
||||||
|
addTriggerHostProxy,
|
||||||
|
setMaintenanceStatus,
|
||||||
|
setAckButtonStatus,
|
||||||
|
filterTriggersPre,
|
||||||
|
toDataFrame,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default problemsHandler;
|
||||||
@@ -4,6 +4,51 @@ import * as c from './constants';
|
|||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import * as metricFunctions from './metricFunctions';
|
import * as metricFunctions from './metricFunctions';
|
||||||
import * as migrations from './migrations';
|
import * as migrations from './migrations';
|
||||||
|
import { ShowProblemTypes } from './types';
|
||||||
|
|
||||||
|
function getTargetDefaults() {
|
||||||
|
return {
|
||||||
|
queryType: c.MODE_METRICS,
|
||||||
|
group: { 'filter': "" },
|
||||||
|
host: { 'filter': "" },
|
||||||
|
application: { 'filter': "" },
|
||||||
|
item: { 'filter': "" },
|
||||||
|
functions: [],
|
||||||
|
triggers: {
|
||||||
|
'count': true,
|
||||||
|
'minSeverity': 3,
|
||||||
|
'acknowledged': 2
|
||||||
|
},
|
||||||
|
trigger: {filter: ""},
|
||||||
|
tags: {filter: ""},
|
||||||
|
proxy: {filter: ""},
|
||||||
|
options: {
|
||||||
|
showDisabledItems: false,
|
||||||
|
skipEmptyValues: false,
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
'skipEmptyValues': false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSLATargetDefaults() {
|
||||||
|
return {
|
||||||
|
slaProperty: { name: "SLA", property: "sla" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProblemsTargetDefaults() {
|
||||||
|
return {
|
||||||
|
showProblems: ShowProblemTypes.Problems,
|
||||||
|
options: {
|
||||||
|
sortTriggersBy: 'default',
|
||||||
|
acknowledged: 2,
|
||||||
|
hostsInMaintenance: false,
|
||||||
|
hostProxy: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class ZabbixQueryController extends QueryCtrl {
|
export class ZabbixQueryController extends QueryCtrl {
|
||||||
|
|
||||||
@@ -21,7 +66,8 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
{value: 'text', text: 'Text', queryType: c.MODE_TEXT},
|
{value: 'text', text: 'Text', queryType: c.MODE_TEXT},
|
||||||
{value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE},
|
{value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE},
|
||||||
{value: 'itemid', text: 'Item ID', queryType: c.MODE_ITEMID},
|
{value: 'itemid', text: 'Item ID', queryType: c.MODE_ITEMID},
|
||||||
{value: 'triggers', text: 'Triggers', queryType: c.MODE_TRIGGERS}
|
{value: 'triggers', text: 'Triggers', queryType: c.MODE_TRIGGERS},
|
||||||
|
{value: 'problems', text: 'Problems', queryType: c.MODE_PROBLEMS},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.$scope.editorMode = {
|
this.$scope.editorMode = {
|
||||||
@@ -29,7 +75,8 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
TEXT: c.MODE_TEXT,
|
TEXT: c.MODE_TEXT,
|
||||||
ITSERVICE: c.MODE_ITSERVICE,
|
ITSERVICE: c.MODE_ITSERVICE,
|
||||||
ITEMID: c.MODE_ITEMID,
|
ITEMID: c.MODE_ITEMID,
|
||||||
TRIGGERS: c.MODE_TRIGGERS
|
TRIGGERS: c.MODE_TRIGGERS,
|
||||||
|
PROBLEMS: c.MODE_PROBLEMS,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.slaPropertyList = [
|
this.slaPropertyList = [
|
||||||
@@ -46,6 +93,30 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
{text: 'acknowledged', value: 1},
|
{text: 'acknowledged', value: 1},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.problemAckFilters = [
|
||||||
|
'all triggers',
|
||||||
|
'unacknowledged',
|
||||||
|
'acknowledged'
|
||||||
|
];
|
||||||
|
|
||||||
|
this.sortByFields = [
|
||||||
|
{ text: 'Default', value: 'default' },
|
||||||
|
{ text: 'Last change', value: 'lastchange' },
|
||||||
|
{ text: 'Severity', value: 'priority' },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.showEventsFields = [
|
||||||
|
{ text: 'All', value: [0,1] },
|
||||||
|
{ text: 'OK', value: [0] },
|
||||||
|
{ text: 'Problems', value: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
this.showProblemsOptions = [
|
||||||
|
{ text: 'Problems', value: 'problems' },
|
||||||
|
{ text: 'Recent problems', value: 'recent' },
|
||||||
|
{ text: 'History', value: 'history' },
|
||||||
|
];
|
||||||
|
|
||||||
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
|
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
|
||||||
|
|
||||||
this.triggerSeverity = c.TRIGGER_SEVERITY;
|
this.triggerSeverity = c.TRIGGER_SEVERITY;
|
||||||
@@ -56,6 +127,7 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
|
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
|
||||||
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
|
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
|
||||||
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
|
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
|
||||||
|
this.getProxyNames = _.bind(this.getMetricNames, this, 'proxyList');
|
||||||
this.getVariables = _.bind(this.getTemplateVariables, this);
|
this.getVariables = _.bind(this.getTemplateVariables, this);
|
||||||
|
|
||||||
// Update metric suggestion when template variable was changed
|
// Update metric suggestion when template variable was changed
|
||||||
@@ -80,40 +152,32 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
_.defaults(this, scopeDefaults);
|
_.defaults(this, scopeDefaults);
|
||||||
|
|
||||||
// Load default values
|
// Load default values
|
||||||
var targetDefaults = {
|
const targetDefaults = getTargetDefaults();
|
||||||
'queryType': c.MODE_METRICS,
|
_.defaultsDeep(target, targetDefaults);
|
||||||
'group': { 'filter': "" },
|
|
||||||
'host': { 'filter': "" },
|
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
|
||||||
'application': { 'filter': "" },
|
target.queryType = c.MODE_PROBLEMS;
|
||||||
'item': { 'filter': "" },
|
}
|
||||||
'functions': [],
|
|
||||||
'triggers': {
|
|
||||||
'count': true,
|
|
||||||
'minSeverity': 3,
|
|
||||||
'acknowledged': 2
|
|
||||||
},
|
|
||||||
'options': {
|
|
||||||
'showDisabledItems': false,
|
|
||||||
'skipEmptyValues': false
|
|
||||||
},
|
|
||||||
'table': {
|
|
||||||
'skipEmptyValues': false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_.defaults(target, targetDefaults);
|
|
||||||
|
|
||||||
// Create function instances from saved JSON
|
// Create function instances from saved JSON
|
||||||
target.functions = _.map(target.functions, function(func) {
|
target.functions = _.map(target.functions, function(func) {
|
||||||
return metricFunctions.createFuncInstance(func.def, func.params);
|
return metricFunctions.createFuncInstance(func.def, func.params);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (target.queryType === c.MODE_ITSERVICE) {
|
||||||
|
_.defaultsDeep(target, getSLATargetDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
_.defaultsDeep(target, getProblemsTargetDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
if (target.queryType === c.MODE_METRICS ||
|
if (target.queryType === c.MODE_METRICS ||
|
||||||
target.queryType === c.MODE_TEXT ||
|
target.queryType === c.MODE_TEXT ||
|
||||||
target.queryType === c.MODE_TRIGGERS) {
|
target.queryType === c.MODE_TRIGGERS ||
|
||||||
|
target.queryType === c.MODE_PROBLEMS) {
|
||||||
this.initFilters();
|
this.initFilters();
|
||||||
}
|
} else if (target.queryType === c.MODE_ITSERVICE) {
|
||||||
else if (target.queryType === c.MODE_ITSERVICE) {
|
|
||||||
_.defaults(target, {slaProperty: {name: "SLA", property: "sla"}});
|
|
||||||
this.suggestITServices();
|
this.suggestITServices();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -125,12 +189,18 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
initFilters() {
|
initFilters() {
|
||||||
let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType});
|
let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType});
|
||||||
itemtype = itemtype ? itemtype.value : null;
|
itemtype = itemtype ? itemtype.value : null;
|
||||||
return Promise.all([
|
const promises = [
|
||||||
this.suggestGroups(),
|
this.suggestGroups(),
|
||||||
this.suggestHosts(),
|
this.suggestHosts(),
|
||||||
this.suggestApps(),
|
this.suggestApps(),
|
||||||
this.suggestItems(itemtype)
|
this.suggestItems(itemtype),
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if (this.target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
promises.push(this.suggestProxies());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get list of metric names for bs-typeahead directive
|
// Get list of metric names for bs-typeahead directive
|
||||||
@@ -207,6 +277,15 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suggestProxies() {
|
||||||
|
return this.zabbix.getProxies()
|
||||||
|
.then(response => {
|
||||||
|
const proxies = _.map(response, 'host');
|
||||||
|
this.metric.proxyList = proxies;
|
||||||
|
return proxies;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isRegex(str) {
|
isRegex(str) {
|
||||||
return utils.isRegex(str);
|
return utils.isRegex(str);
|
||||||
}
|
}
|
||||||
@@ -302,19 +381,42 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderQueryOptionsText() {
|
renderQueryOptionsText() {
|
||||||
var optionsMap = {
|
const metricOptionsMap = {
|
||||||
showDisabledItems: "Show disabled items",
|
showDisabledItems: "Show disabled items",
|
||||||
skipEmptyValues: "Skip empty values"
|
|
||||||
};
|
};
|
||||||
var options = [];
|
|
||||||
|
const problemsOptionsMap = {
|
||||||
|
sortTriggersBy: "Sort problems",
|
||||||
|
acknowledged: "Acknowledged",
|
||||||
|
skipEmptyValues: "Skip empty values",
|
||||||
|
hostsInMaintenance: "Show hosts in maintenance",
|
||||||
|
limit: "Limit problems",
|
||||||
|
hostProxy: "Show proxy",
|
||||||
|
};
|
||||||
|
|
||||||
|
let optionsMap = {};
|
||||||
|
|
||||||
|
if (this.target.queryType === c.MODE_METRICS) {
|
||||||
|
optionsMap = metricOptionsMap;
|
||||||
|
} else if (this.target.queryType === c.MODE_PROBLEMS || this.target.queryType === c.MODE_TRIGGERS) {
|
||||||
|
optionsMap = problemsOptionsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [];
|
||||||
_.forOwn(this.target.options, (value, key) => {
|
_.forOwn(this.target.options, (value, key) => {
|
||||||
if (value) {
|
if (value && optionsMap[key]) {
|
||||||
if (value === true) {
|
if (value === true) {
|
||||||
// Show only option name (if enabled) for boolean options
|
// Show only option name (if enabled) for boolean options
|
||||||
options.push(optionsMap[key]);
|
options.push(optionsMap[key]);
|
||||||
} else {
|
} else {
|
||||||
// Show "option = value" for another options
|
// Show "option = value" for another options
|
||||||
options.push(optionsMap[key] + " = " + value);
|
let optionValue = value;
|
||||||
|
if (value && value.text) {
|
||||||
|
optionValue = value.text;
|
||||||
|
} else if (value && value.value) {
|
||||||
|
optionValue = value.value;
|
||||||
|
}
|
||||||
|
options.push(optionsMap[key] + " = " + optionValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -330,6 +432,7 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
*/
|
*/
|
||||||
switchEditorMode(mode) {
|
switchEditorMode(mode) {
|
||||||
this.target.queryType = mode;
|
this.target.queryType = mode;
|
||||||
|
this.queryOptionsText = this.renderQueryOptionsText();
|
||||||
this.init();
|
this.init();
|
||||||
this.targetChanged();
|
this.targetChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,3 +29,7 @@ This mode is suitable for rendering charts in grafana by passing itemids as url
|
|||||||
|
|
||||||
##### Triggers
|
##### Triggers
|
||||||
Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard.
|
Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard.
|
||||||
|
|
||||||
|
#### Documentation links:
|
||||||
|
|
||||||
|
[Grafana-Zabbix Documentation](https://alexanderzobnin.github.io/grafana-zabbix)
|
||||||
|
|||||||
@@ -258,6 +258,3 @@ export default {
|
|||||||
handleTriggersResponse,
|
handleTriggersResponse,
|
||||||
sortTimeseries
|
sortTimeseries
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fix for backward compatibility with lodash 2.4
|
|
||||||
if (!_.uniqBy) {_.uniqBy = _.uniq;}
|
|
||||||
|
|||||||
@@ -20,35 +20,30 @@ const POINT_TIMESTAMP = 1;
|
|||||||
* Downsample time series by using given function (avg, min, max).
|
* Downsample time series by using given function (avg, min, max).
|
||||||
*/
|
*/
|
||||||
function downsample(datapoints, time_to, ms_interval, func) {
|
function downsample(datapoints, time_to, ms_interval, func) {
|
||||||
var downsampledSeries = [];
|
const downsampledSeries = [];
|
||||||
var timeWindow = {
|
const timeWindow = {
|
||||||
from: time_to * 1000 - ms_interval,
|
from: time_to * 1000 - ms_interval,
|
||||||
to: time_to * 1000
|
to: time_to * 1000
|
||||||
};
|
};
|
||||||
|
|
||||||
var points_sum = 0;
|
let points_sum = 0;
|
||||||
var points_num = 0;
|
let points_num = 0;
|
||||||
var value_avg = 0;
|
let value_avg = 0;
|
||||||
var frame = [];
|
let frame = [];
|
||||||
|
|
||||||
for (var i = datapoints.length - 1; i >= 0; i -= 1) {
|
for (let i = datapoints.length - 1; i >= 0; i -= 1) {
|
||||||
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
|
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
|
||||||
points_sum += datapoints[i][0];
|
points_sum += datapoints[i][0];
|
||||||
points_num++;
|
points_num++;
|
||||||
frame.push(datapoints[i][0]);
|
frame.push(datapoints[i][0]);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
value_avg = points_num ? points_sum / points_num : 0;
|
value_avg = points_num ? points_sum / points_num : 0;
|
||||||
|
|
||||||
if (func === "max") {
|
if (func === "max") {
|
||||||
downsampledSeries.push([_.max(frame), timeWindow.to]);
|
downsampledSeries.push([_.max(frame), timeWindow.to]);
|
||||||
}
|
} else if (func === "min") {
|
||||||
else if (func === "min") {
|
|
||||||
downsampledSeries.push([_.min(frame), timeWindow.to]);
|
downsampledSeries.push([_.min(frame), timeWindow.to]);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// avg by default
|
|
||||||
else {
|
|
||||||
downsampledSeries.push([value_avg, timeWindow.to]);
|
downsampledSeries.push([value_avg, timeWindow.to]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,25 +67,25 @@ function downsample(datapoints, time_to, ms_interval, func) {
|
|||||||
* datapoints: [[<value>, <unixtime>], ...]
|
* datapoints: [[<value>, <unixtime>], ...]
|
||||||
*/
|
*/
|
||||||
function groupBy(datapoints, interval, groupByCallback) {
|
function groupBy(datapoints, interval, groupByCallback) {
|
||||||
var ms_interval = utils.parseInterval(interval);
|
const ms_interval = utils.parseInterval(interval);
|
||||||
|
|
||||||
// Calculate frame timestamps
|
// Calculate frame timestamps
|
||||||
var frames = _.groupBy(datapoints, function (point) {
|
const frames = _.groupBy(datapoints, point => {
|
||||||
// Calculate time for group of points
|
// Calculate time for group of points
|
||||||
return Math.floor(point[1] / ms_interval) * ms_interval;
|
return Math.floor(point[1] / ms_interval) * ms_interval;
|
||||||
});
|
});
|
||||||
|
|
||||||
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
|
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
|
||||||
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
|
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
|
||||||
var grouped = _.mapValues(frames, function (frame) {
|
const grouped = _.mapValues(frames, frame => {
|
||||||
var points = _.map(frame, function (point) {
|
const points = _.map(frame, point => {
|
||||||
return point[0];
|
return point[0];
|
||||||
});
|
});
|
||||||
return groupByCallback(points);
|
return groupByCallback(points);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert points to Grafana format
|
// Convert points to Grafana format
|
||||||
return sortByTime(_.map(grouped, function (value, timestamp) {
|
return sortByTime(_.map(grouped, (value, timestamp) => {
|
||||||
return [Number(value), Number(timestamp)];
|
return [Number(value), Number(timestamp)];
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -104,15 +99,15 @@ export function groupBy_perf(datapoints, interval, groupByCallback) {
|
|||||||
return groupByRange(datapoints, groupByCallback);
|
return groupByRange(datapoints, groupByCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ms_interval = utils.parseInterval(interval);
|
const ms_interval = utils.parseInterval(interval);
|
||||||
let grouped_series = [];
|
const grouped_series = [];
|
||||||
let frame_values = [];
|
let frame_values = [];
|
||||||
let frame_value;
|
let frame_value;
|
||||||
let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
|
let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
|
||||||
let point_frame_ts = frame_ts;
|
let point_frame_ts = frame_ts;
|
||||||
let point;
|
let point;
|
||||||
|
|
||||||
for (let i=0; i < datapoints.length; i++) {
|
for (let i = 0; i < datapoints.length; i++) {
|
||||||
point = datapoints[i];
|
point = datapoints[i];
|
||||||
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
|
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
|
||||||
if (point_frame_ts === frame_ts) {
|
if (point_frame_ts === frame_ts) {
|
||||||
@@ -142,7 +137,7 @@ export function groupByRange(datapoints, groupByCallback) {
|
|||||||
const frame_start = datapoints[0][POINT_TIMESTAMP];
|
const frame_start = datapoints[0][POINT_TIMESTAMP];
|
||||||
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
|
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
|
||||||
let point;
|
let point;
|
||||||
for (let i=0; i < datapoints.length; i++) {
|
for (let i = 0; i < datapoints.length; i++) {
|
||||||
point = datapoints[i];
|
point = datapoints[i];
|
||||||
frame_values.push(point[POINT_VALUE]);
|
frame_values.push(point[POINT_VALUE]);
|
||||||
}
|
}
|
||||||
@@ -157,30 +152,30 @@ export function groupByRange(datapoints, groupByCallback) {
|
|||||||
function sumSeries(timeseries) {
|
function sumSeries(timeseries) {
|
||||||
|
|
||||||
// Calculate new points for interpolation
|
// Calculate new points for interpolation
|
||||||
var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function (point) {
|
let new_timestamps = _.uniq(_.map(_.flatten(timeseries), point => {
|
||||||
return point[1];
|
return point[1];
|
||||||
}));
|
}));
|
||||||
new_timestamps = _.sortBy(new_timestamps);
|
new_timestamps = _.sortBy(new_timestamps);
|
||||||
|
|
||||||
var interpolated_timeseries = _.map(timeseries, function (series) {
|
const interpolated_timeseries = _.map(timeseries, series => {
|
||||||
series = fillZeroes(series, new_timestamps);
|
series = fillZeroes(series, new_timestamps);
|
||||||
var timestamps = _.map(series, function (point) {
|
const timestamps = _.map(series, point => {
|
||||||
return point[1];
|
return point[1];
|
||||||
});
|
});
|
||||||
var new_points = _.map(_.difference(new_timestamps, timestamps), function (timestamp) {
|
const new_points = _.map(_.difference(new_timestamps, timestamps), timestamp => {
|
||||||
return [null, timestamp];
|
return [null, timestamp];
|
||||||
});
|
});
|
||||||
var new_series = series.concat(new_points);
|
const new_series = series.concat(new_points);
|
||||||
return sortByTime(new_series);
|
return sortByTime(new_series);
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(interpolated_timeseries, interpolateSeries);
|
_.each(interpolated_timeseries, interpolateSeries);
|
||||||
|
|
||||||
var new_timeseries = [];
|
const new_timeseries = [];
|
||||||
var sum;
|
let sum;
|
||||||
for (var i = new_timestamps.length - 1; i >= 0; i--) {
|
for (let i = new_timestamps.length - 1; i >= 0; i--) {
|
||||||
sum = 0;
|
sum = 0;
|
||||||
for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
|
for (let j = interpolated_timeseries.length - 1; j >= 0; j--) {
|
||||||
sum += interpolated_timeseries[j][i][0];
|
sum += interpolated_timeseries[j][i][0];
|
||||||
}
|
}
|
||||||
new_timeseries.push([sum, new_timestamps[i]]);
|
new_timeseries.push([sum, new_timestamps[i]]);
|
||||||
@@ -225,9 +220,9 @@ function offset(datapoints, delta) {
|
|||||||
* @param {*} datapoints
|
* @param {*} datapoints
|
||||||
*/
|
*/
|
||||||
function delta(datapoints) {
|
function delta(datapoints) {
|
||||||
let newSeries = [];
|
const newSeries = [];
|
||||||
let deltaValue;
|
let deltaValue;
|
||||||
for (var i = 1; i < datapoints.length; i++) {
|
for (let i = 1; i < datapoints.length; i++) {
|
||||||
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
|
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
|
||||||
newSeries.push([deltaValue, datapoints[i][1]]);
|
newSeries.push([deltaValue, datapoints[i][1]]);
|
||||||
}
|
}
|
||||||
@@ -239,7 +234,7 @@ function delta(datapoints) {
|
|||||||
* @param {*} datapoints
|
* @param {*} datapoints
|
||||||
*/
|
*/
|
||||||
function rate(datapoints) {
|
function rate(datapoints) {
|
||||||
let newSeries = [];
|
const newSeries = [];
|
||||||
let point, point_prev;
|
let point, point_prev;
|
||||||
let valueDelta = 0;
|
let valueDelta = 0;
|
||||||
let timeDelta = 0;
|
let timeDelta = 0;
|
||||||
@@ -261,7 +256,7 @@ function rate(datapoints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function simpleMovingAverage(datapoints, n) {
|
function simpleMovingAverage(datapoints, n) {
|
||||||
let sma = [];
|
const sma = [];
|
||||||
let w_sum;
|
let w_sum;
|
||||||
let w_avg = null;
|
let w_avg = null;
|
||||||
let w_count = 0;
|
let w_count = 0;
|
||||||
@@ -352,7 +347,7 @@ function expMovingAverage(datapoints, n) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PERCENTILE(n, values) {
|
function PERCENTILE(n, values) {
|
||||||
var sorted = _.sortBy(values);
|
const sorted = _.sortBy(values);
|
||||||
return sorted[Math.floor(sorted.length * n / 100)];
|
return sorted[Math.floor(sorted.length * n / 100)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +356,7 @@ function COUNT(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SUM(values) {
|
function SUM(values) {
|
||||||
var sum = null;
|
let sum = null;
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
if (values[i] !== null) {
|
if (values[i] !== null) {
|
||||||
sum += values[i];
|
sum += values[i];
|
||||||
@@ -371,7 +366,7 @@ function SUM(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AVERAGE(values) {
|
function AVERAGE(values) {
|
||||||
let values_non_null = getNonNullValues(values);
|
const values_non_null = getNonNullValues(values);
|
||||||
if (values_non_null.length === 0) {
|
if (values_non_null.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -379,7 +374,7 @@ function AVERAGE(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNonNullValues(values) {
|
function getNonNullValues(values) {
|
||||||
let values_non_null = [];
|
const values_non_null = [];
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
if (values[i] !== null) {
|
if (values[i] !== null) {
|
||||||
values_non_null.push(values[i]);
|
values_non_null.push(values[i]);
|
||||||
@@ -397,7 +392,7 @@ function MAX(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MEDIAN(values) {
|
function MEDIAN(values) {
|
||||||
var sorted = _.sortBy(values);
|
const sorted = _.sortBy(values);
|
||||||
return sorted[Math.floor(sorted.length / 2)];
|
return sorted[Math.floor(sorted.length / 2)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +413,7 @@ function getPointTimeFrame(timestamp, ms_interval) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortByTime(series) {
|
function sortByTime(series) {
|
||||||
return _.sortBy(series, function (point) {
|
return _.sortBy(series, point => {
|
||||||
return point[1];
|
return point[1];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -432,8 +427,8 @@ function sortByTime(series) {
|
|||||||
* @param {*} timestamps
|
* @param {*} timestamps
|
||||||
*/
|
*/
|
||||||
function fillZeroes(series, timestamps) {
|
function fillZeroes(series, timestamps) {
|
||||||
let prepend = [];
|
const prepend = [];
|
||||||
let append = [];
|
const append = [];
|
||||||
let new_point;
|
let new_point;
|
||||||
for (let i = 0; i < timestamps.length; i++) {
|
for (let i = 0; i < timestamps.length; i++) {
|
||||||
if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
|
if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
|
||||||
@@ -451,10 +446,10 @@ function fillZeroes(series, timestamps) {
|
|||||||
* Interpolate series with gaps
|
* Interpolate series with gaps
|
||||||
*/
|
*/
|
||||||
function interpolateSeries(series) {
|
function interpolateSeries(series) {
|
||||||
var left, right;
|
let left, right;
|
||||||
|
|
||||||
// Interpolate series
|
// Interpolate series
|
||||||
for (var i = series.length - 1; i >= 0; i--) {
|
for (let i = series.length - 1; i >= 0; i--) {
|
||||||
if (!series[i][0]) {
|
if (!series[i][0]) {
|
||||||
left = findNearestLeft(series, i);
|
left = findNearestLeft(series, i);
|
||||||
right = findNearestRight(series, i);
|
right = findNearestRight(series, i);
|
||||||
@@ -479,7 +474,7 @@ function linearInterpolation(timestamp, left, right) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNearestRight(series, pointIndex) {
|
function findNearestRight(series, pointIndex) {
|
||||||
for (var i = pointIndex; i < series.length; i++) {
|
for (let i = pointIndex; i < series.length; i++) {
|
||||||
if (series[i][0] !== null) {
|
if (series[i][0] !== null) {
|
||||||
return series[i];
|
return series[i];
|
||||||
}
|
}
|
||||||
@@ -488,7 +483,7 @@ function findNearestRight(series, pointIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNearestLeft(series, pointIndex) {
|
function findNearestLeft(series, pointIndex) {
|
||||||
for (var i = pointIndex; i > 0; i--) {
|
for (let i = pointIndex; i > 0; i--) {
|
||||||
if (series[i][0] !== null) {
|
if (series[i][0] !== null) {
|
||||||
return series[i];
|
return series[i];
|
||||||
}
|
}
|
||||||
@@ -28,3 +28,9 @@ export enum VariableQueryTypes {
|
|||||||
Application = 'application',
|
Application = 'application',
|
||||||
Item = 'item',
|
Item = 'item',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ShowProblemTypes {
|
||||||
|
Problems = 'problems',
|
||||||
|
Recent = 'recent',
|
||||||
|
History = 'history',
|
||||||
|
}
|
||||||
|
|||||||
@@ -222,10 +222,11 @@ export function escapeRegex(value) {
|
|||||||
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
|
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseInterval(interval) {
|
export function parseInterval(interval: string): number {
|
||||||
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
|
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
|
||||||
const momentInterval: any[] = intervalPattern.exec(interval);
|
const momentInterval: any[] = intervalPattern.exec(interval);
|
||||||
return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf();
|
const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]);
|
||||||
|
return (duration.valueOf() as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTimeShiftInterval(interval) {
|
export function parseTimeShiftInterval(interval) {
|
||||||
@@ -344,7 +345,25 @@ export function getArrayDepth(a, level = 0) {
|
|||||||
return level + 1;
|
return level + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for backward compatibility with lodash 2.4
|
/**
|
||||||
if (!_.includes) {
|
* Checks whether its argument represents a numeric value.
|
||||||
_.includes = (_ as any).contains;
|
*/
|
||||||
|
export function isNumeric(n: any): boolean {
|
||||||
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses tags string into array of {tag: value} objects
|
||||||
|
*/
|
||||||
|
export function parseTags(tagStr: string): any[] {
|
||||||
|
if (!tagStr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags: any[] = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||||
|
tags = _.map(tags, (tag) => {
|
||||||
|
const tagParts = tag.split(':');
|
||||||
|
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
||||||
|
});
|
||||||
|
return tags;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import kbn from 'grafana/app/core/utils/kbn';
|
|||||||
import * as utils from '../../../utils';
|
import * as utils from '../../../utils';
|
||||||
import { ZabbixAPICore } from './zabbixAPICore';
|
import { ZabbixAPICore } from './zabbixAPICore';
|
||||||
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
|
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
|
||||||
|
import { ShowProblemTypes } from '../../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zabbix API Wrapper.
|
* Zabbix API Wrapper.
|
||||||
@@ -350,8 +351,10 @@ export class ZabbixAPIConnector {
|
|||||||
selectTags: 'extend'
|
selectTags: 'extend'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showTriggers) {
|
if (showTriggers === ShowProblemTypes.Problems) {
|
||||||
params.filter.value = showTriggers;
|
params.filter.value = 1;
|
||||||
|
} else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) {
|
||||||
|
params.filter.value = [0, 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maintenance) {
|
if (maintenance) {
|
||||||
|
|||||||
@@ -183,14 +183,14 @@ function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSev
|
|||||||
|
|
||||||
let severityDesc: TriggerSeverity;
|
let severityDesc: TriggerSeverity;
|
||||||
severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(problem.priority));
|
severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(problem.priority));
|
||||||
if (problem.lastEvent && problem.lastEvent.severity && problem.value === '1') {
|
if (problem.lastEvent?.severity && problem.value === '1') {
|
||||||
severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(problem.lastEvent.severity));
|
severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(problem.lastEvent.severity));
|
||||||
}
|
}
|
||||||
|
|
||||||
color = severityDesc.color;
|
color = severityDesc.color;
|
||||||
|
|
||||||
// Mark acknowledged triggers with different color
|
// Mark acknowledged triggers with different color
|
||||||
if (markAckEvents && problem.lastEvent.acknowledged === "1") {
|
if (markAckEvents && problem.lastEvent?.acknowledged === "1") {
|
||||||
color = ackEventColor;
|
color = ackEventColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<value-select-dropdown
|
|
||||||
variable="ctrl.dsOptions"
|
|
||||||
on-updated="ctrl.onChange(ctrl.dsOptions)"
|
|
||||||
dashboard="ctrl.dashboard">
|
|
||||||
</value-select-dropdown>
|
|
||||||
`;
|
|
||||||
|
|
||||||
angular
|
|
||||||
.module('grafana.directives')
|
|
||||||
.directive('datasourceSelector', () => {
|
|
||||||
return {
|
|
||||||
scope: {
|
|
||||||
datasources: "=",
|
|
||||||
options: "=",
|
|
||||||
onChange: "&"
|
|
||||||
},
|
|
||||||
controller: DatasourceSelectorCtrl,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
template: template
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
class DatasourceSelectorCtrl {
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope) {
|
|
||||||
this.scope = $scope;
|
|
||||||
let datasources = $scope.datasources;
|
|
||||||
let options = $scope.options;
|
|
||||||
this.dsOptions = {
|
|
||||||
multi: true,
|
|
||||||
current: {value: datasources, text: datasources.join(" + ")},
|
|
||||||
options: _.map(options, (ds) => {
|
|
||||||
return {text: ds, value: ds, selected: _.includes(datasources, ds)};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
// Fix for Grafana 6.0
|
|
||||||
// https://github.com/grafana/grafana/blob/v6.0.0/public/app/core/directives/value_select_dropdown.ts#L291
|
|
||||||
this.dashboard = {
|
|
||||||
on: () => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(updatedOptions) {
|
|
||||||
let newDataSources = updatedOptions.current.value;
|
|
||||||
this.scope.datasources = newDataSources;
|
|
||||||
|
|
||||||
// Run after model was changed
|
|
||||||
this.scope.$$postDigest(() => {
|
|
||||||
this.scope.onChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { getNextRefIdChar } from './utils';
|
import { getNextRefIdChar } from './utils';
|
||||||
import { getDefaultTarget } from './triggers_panel_ctrl';
|
import { ShowProblemTypes } from '../datasource-zabbix/types';
|
||||||
|
|
||||||
// Actual schema version
|
// Actual schema version
|
||||||
export const CURRENT_SCHEMA_VERSION = 7;
|
export const CURRENT_SCHEMA_VERSION = 8;
|
||||||
|
|
||||||
|
export const getDefaultTarget = (targets?) => {
|
||||||
|
return {
|
||||||
|
group: {filter: ""},
|
||||||
|
host: {filter: ""},
|
||||||
|
application: {filter: ""},
|
||||||
|
trigger: {filter: ""},
|
||||||
|
tags: {filter: ""},
|
||||||
|
proxy: {filter: ""},
|
||||||
|
refId: getNextRefIdChar(targets),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDefaultTargetOptions() {
|
||||||
|
return {
|
||||||
|
hostsInMaintenance: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function migratePanelSchema(panel) {
|
export function migratePanelSchema(panel) {
|
||||||
if (isEmptyPanel(panel)) {
|
if (isEmptyPanel(panel)) {
|
||||||
@@ -12,7 +30,7 @@ export function migratePanelSchema(panel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schemaVersion = getSchemaVersion(panel);
|
const schemaVersion = getSchemaVersion(panel);
|
||||||
panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
// panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||||
|
|
||||||
if (schemaVersion < 2) {
|
if (schemaVersion < 2) {
|
||||||
panel.datasources = [panel.datasource];
|
panel.datasources = [panel.datasource];
|
||||||
@@ -66,9 +84,62 @@ export function migratePanelSchema(panel) {
|
|||||||
delete panel.datasources;
|
delete panel.datasources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schemaVersion < 8) {
|
||||||
|
if (panel.targets.length === 1) {
|
||||||
|
if (panel.targets[0].datasource) {
|
||||||
|
panel.datasource = panel.targets[0].datasource;
|
||||||
|
delete panel.targets[0].datasource;
|
||||||
|
}
|
||||||
|
} else if (panel.targets.length > 1) {
|
||||||
|
// Mixed data sources
|
||||||
|
panel.datasource = '-- Mixed --';
|
||||||
|
}
|
||||||
|
for (const target of panel.targets) {
|
||||||
|
// set queryType to PROBLEMS
|
||||||
|
target.queryType = 5;
|
||||||
|
target.showProblems = migrateShowEvents(panel);
|
||||||
|
target.options = migrateOptions(panel);
|
||||||
|
|
||||||
|
_.defaults(target.options, getDefaultTargetOptions());
|
||||||
|
_.defaults(target, { tags: { filter: "" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange';
|
||||||
|
|
||||||
|
delete panel.showEvents;
|
||||||
|
delete panel.showTriggers;
|
||||||
|
delete panel.hostsInMaintenance;
|
||||||
|
delete panel.sortTriggersBy;
|
||||||
|
}
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateOptions(panel) {
|
||||||
|
let acknowledged = 2;
|
||||||
|
if (panel.showTriggers === 'acknowledged') {
|
||||||
|
acknowledged = 1;
|
||||||
|
} else if (panel.showTriggers === 'unacknowledged') {
|
||||||
|
acknowledged = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostsInMaintenance: panel.hostsInMaintenance,
|
||||||
|
sortProblems: panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'default',
|
||||||
|
acknowledged: acknowledged,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateShowEvents(panel) {
|
||||||
|
if (panel.showEvents?.value === 1) {
|
||||||
|
return ShowProblemTypes.Problems;
|
||||||
|
} else if (panel.showEvents?.value === 0 || panel.showEvents?.value?.length > 1) {
|
||||||
|
return ShowProblemTypes.History;
|
||||||
|
} else {
|
||||||
|
return ShowProblemTypes.Problems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSchemaVersion(panel) {
|
function getSchemaVersion(panel) {
|
||||||
return panel.schemaVersion || 1;
|
return panel.schemaVersion || 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
* Licensed under the Apache License, Version 2.0
|
* Licensed under the Apache License, Version 2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TriggerPanelCtrl} from './triggers_panel_ctrl';
|
import { TriggerPanelCtrl } from './triggers_panel_ctrl';
|
||||||
import {loadPluginCss} from 'grafana/app/plugins/sdk';
|
import { loadPluginCss } from 'grafana/app/plugins/sdk';
|
||||||
import './datasource-selector.directive';
|
|
||||||
|
|
||||||
loadPluginCss({
|
loadPluginCss({
|
||||||
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
|
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ class TriggerPanelOptionsCtrl {
|
|||||||
'unacknowledged',
|
'unacknowledged',
|
||||||
'acknowledged'
|
'acknowledged'
|
||||||
];
|
];
|
||||||
this.sortByFields = [
|
|
||||||
{ text: 'last change', value: 'lastchange' },
|
this.sortingOptions = [
|
||||||
{ text: 'severity', value: 'priority' }
|
{ text: 'Default', value: 'default' },
|
||||||
|
{ text: 'Last change', value: 'lastchange' },
|
||||||
|
{ text: 'Severity', value: 'priority' },
|
||||||
];
|
];
|
||||||
|
|
||||||
this.showEventsFields = [
|
this.showEventsFields = [
|
||||||
{ text: 'All', value: [0,1] },
|
{ text: 'All', value: [0,1] },
|
||||||
{ text: 'OK', value: [0] },
|
{ text: 'OK', value: [0] },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="editor-row">
|
<div class="editor-row">
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Show fields</h5>
|
<h5 class="section-heading">Fields</h5>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
label="Host name"
|
label="Host name"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
label="Host proxy"
|
label="Host proxy"
|
||||||
checked="ctrl.panel.hostProxy"
|
checked="ctrl.panel.hostProxy"
|
||||||
on-change="ctrl.refresh()">
|
on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
@@ -72,53 +72,6 @@
|
|||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<h5 class="section-heading">Options</h5>
|
|
||||||
<gf-form-switch class="gf-form"
|
|
||||||
label-class="width-15"
|
|
||||||
label="Show hosts in maintenance"
|
|
||||||
checked="ctrl.panel.hostsInMaintenance"
|
|
||||||
on-change="ctrl.render()">
|
|
||||||
</gf-form-switch>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Acknowledged</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-model="ctrl.panel.showTriggers"
|
|
||||||
ng-options="f for f in editor.ackFilters"
|
|
||||||
ng-change="ctrl.refresh()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Sort by</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-model="ctrl.panel.sortTriggersBy"
|
|
||||||
ng-options="f.text for f in editor.sortByFields track by f.value"
|
|
||||||
ng-change="ctrl.render()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Show events</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-model="ctrl.panel.showEvents"
|
|
||||||
ng-options="f.text for f in editor.showEventsFields track by f.value"
|
|
||||||
ng-change="ctrl.refresh()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Limit triggers</label>
|
|
||||||
<input class="gf-form-input width-5"
|
|
||||||
type="number" placeholder="100"
|
|
||||||
ng-model="ctrl.panel.limit"
|
|
||||||
ng-model-onblur ng-change="ctrl.refresh()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">View options</h5>
|
<h5 class="section-heading">View options</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
@@ -130,6 +83,16 @@
|
|||||||
ng-change="ctrl.render()"></select>
|
ng-change="ctrl.render()"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-10">Sort by</label>
|
||||||
|
<div class="gf-form-select-wrapper max-width-8">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.panel.sortProblems"
|
||||||
|
ng-options="f.value as f.text for f in editor.sortingOptions"
|
||||||
|
ng-change="ctrl.reRenderProblems()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-10">Font size</label>
|
<label class="gf-form-label width-10">Font size</label>
|
||||||
<div class="gf-form-select-wrapper max-width-8">
|
<div class="gf-form-select-wrapper max-width-8">
|
||||||
@@ -202,7 +165,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Triggers severity and colors</h5>
|
<h5 class="section-heading">Problems severity and colors</h5>
|
||||||
<div class="gf-form-inline" ng-repeat="trigger in ctrl.panel.triggerSeverity">
|
<div class="gf-form-inline" ng-repeat="trigger in ctrl.panel.triggerSeverity">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-3">{{ trigger.priority }}</label>
|
<label class="gf-form-label width-3">{{ trigger.priority }}</label>
|
||||||
@@ -224,7 +187,7 @@
|
|||||||
label-class="width-0"
|
label-class="width-0"
|
||||||
label="Show"
|
label="Show"
|
||||||
checked="trigger.show"
|
checked="trigger.show"
|
||||||
on-change="ctrl.refresh()">
|
on-change="ctrl.reRenderProblems()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -246,7 +209,7 @@
|
|||||||
label-class="width-0"
|
label-class="width-0"
|
||||||
label="Show"
|
label="Show"
|
||||||
checked="ctrl.panel.markAckEvents"
|
checked="ctrl.panel.markAckEvents"
|
||||||
on-change="ctrl.refresh()">
|
on-change="ctrl.reRenderProblems()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
<div class="editor-row">
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-9">Data sources</label>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<datasource-selector
|
|
||||||
datasources="editor.selectedDatasources"
|
|
||||||
options="editor.panelCtrl.available_datasources"
|
|
||||||
on-change="editor.datasourcesChanged()">
|
|
||||||
</datasource-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-row" ng-repeat="target in ctrl.panel.targets">
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<h5 class="section-heading">{{ target.datasource }}</h5>
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Group</label>
|
|
||||||
<input type="text"
|
|
||||||
ng-model="target.group.filter"
|
|
||||||
bs-typeahead="editor.getGroupNames[target.datasource]"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
data-min-length=0
|
|
||||||
data-items=100
|
|
||||||
class="gf-form-input width-14"
|
|
||||||
ng-class="{
|
|
||||||
'zbx-variable': editor.isVariable(target.group.filter),
|
|
||||||
'zbx-regex': editor.isRegex(target.group.filter)
|
|
||||||
}">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Host</label>
|
|
||||||
<input type="text"
|
|
||||||
ng-model="target.host.filter"
|
|
||||||
bs-typeahead="editor.getHostNames[target.datasource]"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
data-min-length=0
|
|
||||||
data-items=100
|
|
||||||
class="gf-form-input width-14"
|
|
||||||
ng-class="{
|
|
||||||
'zbx-variable': editor.isVariable(target.host.filter),
|
|
||||||
'zbx-regex': editor.isRegex(target.host.filter)
|
|
||||||
}">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Proxy</label>
|
|
||||||
<input type="text"
|
|
||||||
ng-model="target.proxy.filter"
|
|
||||||
bs-typeahead="editor.getProxyNames[target.datasource]"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
data-min-length=0
|
|
||||||
data-items=100
|
|
||||||
class="gf-form-input width-14"
|
|
||||||
ng-class="{
|
|
||||||
'zbx-variable': editor.isVariable(target.proxy.filter),
|
|
||||||
'zbx-regex': editor.isRegex(target.proxy.filter)
|
|
||||||
}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Application</label>
|
|
||||||
<input type="text"
|
|
||||||
ng-model="target.application.filter"
|
|
||||||
bs-typeahead="editor.getApplicationNames[target.datasource]"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
data-min-length=0
|
|
||||||
data-items=100
|
|
||||||
class="gf-form-input width-14"
|
|
||||||
ng-class="{
|
|
||||||
'zbx-variable': editor.isVariable(target.application.filter),
|
|
||||||
'zbx-regex': editor.isRegex(target.application.filter)
|
|
||||||
}">
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Trigger</label>
|
|
||||||
<input type="text"
|
|
||||||
ng-model="target.trigger.filter"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
placeholder="trigger name"
|
|
||||||
class="gf-form-input width-14"
|
|
||||||
ng-style="target.trigger.style"
|
|
||||||
ng-class="{
|
|
||||||
'zbx-variable': editor.isVariable(target.trigger.filter),
|
|
||||||
'zbx-regex': editor.isRegex(target.trigger.filter)
|
|
||||||
}"
|
|
||||||
empty-to-null>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Tags</label>
|
|
||||||
<input type="text" class="gf-form-input width-14"
|
|
||||||
ng-model="target.tags.filter"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
placeholder="tag1:value1, tag2:value2">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -3,9 +3,6 @@
|
|||||||
"name": "Zabbix Problems",
|
"name": "Zabbix Problems",
|
||||||
"id": "alexanderzobnin-zabbix-triggers-panel",
|
"id": "alexanderzobnin-zabbix-triggers-panel",
|
||||||
|
|
||||||
"dataFormats": [],
|
|
||||||
"skipDataQuery": true,
|
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alexander Zobnin",
|
"name": "Alexander Zobnin",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mocks from '../../test-setup/mocks';
|
import mocks from '../../test-setup/mocks';
|
||||||
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
|
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
|
||||||
import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl';
|
import { DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS } from '../triggers_panel_ctrl';
|
||||||
import {CURRENT_SCHEMA_VERSION} from '../migrations';
|
import { CURRENT_SCHEMA_VERSION } from '../migrations';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => {
|
jest.mock('@grafana/runtime', () => {
|
||||||
return {
|
return {
|
||||||
@@ -34,8 +34,9 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
ageField: true,
|
ageField: true,
|
||||||
infoField: true,
|
infoField: true,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
showTriggers: 'all triggers',
|
showTriggers: 'unacknowledged',
|
||||||
hideHostsInMaintenance: false,
|
hideHostsInMaintenance: false,
|
||||||
|
hostsInMaintenance: false,
|
||||||
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
||||||
showEvents: { text: 'Problems', value: '1' },
|
showEvents: { text: 'Problems', value: '1' },
|
||||||
triggerSeverity: DEFAULT_SEVERITY,
|
triggerSeverity: DEFAULT_SEVERITY,
|
||||||
@@ -48,7 +49,7 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock, {}, {}, {}, mocks.timeSrvMock);
|
updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update old panel schema', () => {
|
it('should update old panel schema', () => {
|
||||||
@@ -56,12 +57,20 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
|
|
||||||
const expected = _.defaultsDeep({
|
const expected = _.defaultsDeep({
|
||||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||||
|
datasource: 'zabbix',
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
...DEFAULT_TARGET,
|
...DEFAULT_TARGET,
|
||||||
datasource: 'zabbix',
|
queryType: 5,
|
||||||
|
showProblems: 'problems',
|
||||||
|
options: {
|
||||||
|
hostsInMaintenance: false,
|
||||||
|
acknowledged: 0,
|
||||||
|
sortProblems: 'default',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
sortProblems: 'lastchange',
|
||||||
ageField: true,
|
ageField: true,
|
||||||
statusField: false,
|
statusField: false,
|
||||||
severityField: false,
|
severityField: false,
|
||||||
@@ -79,27 +88,7 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
|
|
||||||
const expected = _.defaultsDeep({
|
const expected = _.defaultsDeep({
|
||||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||||
targets: [{
|
|
||||||
...DEFAULT_TARGET,
|
|
||||||
datasource: 'zabbix_default'
|
|
||||||
}]
|
|
||||||
}, PANEL_DEFAULTS);
|
}, PANEL_DEFAULTS);
|
||||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
expect(updatedPanelCtrl.panel).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set default targets for new panel with empty targets', () => {
|
|
||||||
ctx.scope.panel = {
|
|
||||||
targets: []
|
|
||||||
};
|
|
||||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
|
||||||
|
|
||||||
const expected = _.defaultsDeep({
|
|
||||||
targets: [{
|
|
||||||
...DEFAULT_TARGET,
|
|
||||||
datasource: 'zabbix_default'
|
|
||||||
}]
|
|
||||||
}, PANEL_DEFAULTS);
|
|
||||||
|
|
||||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mocks from '../../test-setup/mocks';
|
import { TriggerPanelCtrl } from '../triggers_panel_ctrl';
|
||||||
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
|
import { PANEL_DEFAULTS, DEFAULT_TARGET } from '../triggers_panel_ctrl';
|
||||||
import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl';
|
|
||||||
// import { create } from 'domain';
|
|
||||||
|
|
||||||
let datasourceSrvMock, zabbixDSMock;
|
let datasourceSrvMock, zabbixDSMock;
|
||||||
|
|
||||||
@@ -14,85 +12,42 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
|
|
||||||
describe('TriggerPanelCtrl', () => {
|
describe('TriggerPanelCtrl', () => {
|
||||||
let ctx: any = {};
|
let ctx: any = {};
|
||||||
const timeoutMock = () => {};
|
let createPanelCtrl: () => any;
|
||||||
let createPanelCtrl;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx = {scope: {panel: PANEL_DEFAULTS}};
|
ctx = { scope: {
|
||||||
|
panel: {
|
||||||
|
...PANEL_DEFAULTS,
|
||||||
|
sortProblems: 'lastchange',
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
ctx.scope.panel.targets = [{
|
||||||
|
...DEFAULT_TARGET,
|
||||||
|
datasource: 'zabbix_default',
|
||||||
|
}];
|
||||||
|
|
||||||
zabbixDSMock = {
|
zabbixDSMock = {
|
||||||
replaceTemplateVars: () => {},
|
|
||||||
zabbix: {
|
zabbix: {
|
||||||
getTriggers: jest.fn().mockReturnValue([generateTrigger("1"), generateTrigger("1")]),
|
|
||||||
getExtendedEventData: jest.fn().mockResolvedValue([]),
|
getExtendedEventData: jest.fn().mockResolvedValue([]),
|
||||||
getEventAlerts: jest.fn().mockResolvedValue([]),
|
getEventAlerts: jest.fn().mockResolvedValue([]),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
datasourceSrvMock = {
|
datasourceSrvMock = {
|
||||||
getMetricSources: () => {
|
|
||||||
return [
|
|
||||||
{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' },
|
|
||||||
{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix' },
|
|
||||||
{ meta: {id: 'graphite'}, value: {}, name: 'graphite' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
get: () => Promise.resolve(zabbixDSMock)
|
get: () => Promise.resolve(zabbixDSMock)
|
||||||
};
|
};
|
||||||
|
|
||||||
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, {}, {}, {}, mocks.timeSrvMock);
|
const timeoutMock = (fn: () => any) => Promise.resolve(fn());
|
||||||
|
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock);
|
||||||
const getTriggersResp = [
|
|
||||||
[
|
|
||||||
createTrigger({
|
|
||||||
triggerid: "1", lastchange: "1510000010", priority: 5, lastEvent: {eventid: "11"}, hosts: [{maintenance_status: '1'}]
|
|
||||||
}),
|
|
||||||
createTrigger({
|
|
||||||
triggerid: "2", lastchange: "1510000040", priority: 3, lastEvent: {eventid: "12"}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
createTrigger({triggerid: "3", lastchange: "1510000020", priority: 4, lastEvent: {eventid: "13"}}),
|
|
||||||
createTrigger({triggerid: "4", lastchange: "1510000030", priority: 2, lastEvent: {eventid: "14"}}),
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Simulate 2 data sources
|
|
||||||
zabbixDSMock.zabbix.getTriggers = jest.fn()
|
|
||||||
.mockReturnValueOnce(getTriggersResp[0])
|
|
||||||
.mockReturnValueOnce(getTriggersResp[1]);
|
|
||||||
zabbixDSMock.zabbix.getExtendedEventData = jest.fn()
|
|
||||||
.mockReturnValue(Promise.resolve([defaultEvent]));
|
|
||||||
|
|
||||||
ctx.panelCtrl = createPanelCtrl();
|
ctx.panelCtrl = createPanelCtrl();
|
||||||
});
|
|
||||||
|
|
||||||
describe('When adding new panel', () => {
|
ctx.dataFramesReceived = generateDataFramesResponse([
|
||||||
it('should suggest all zabbix data sources', () => {
|
{id: "1", lastchange: "1510000010", priority: 5},
|
||||||
ctx.scope.panel = {};
|
{id: "2", lastchange: "1510000040", priority: 3},
|
||||||
const panelCtrl = createPanelCtrl();
|
{id: "3", lastchange: "1510000020", priority: 4},
|
||||||
expect(panelCtrl.available_datasources).toEqual([
|
{id: "4", lastchange: "1510000030", priority: 2},
|
||||||
'zabbix_default', 'zabbix'
|
]);
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load first zabbix data source as default', () => {
|
|
||||||
ctx.scope.panel = {};
|
|
||||||
const panelCtrl = createPanelCtrl();
|
|
||||||
expect(panelCtrl.panel.targets[0].datasource).toEqual('zabbix_default');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should rewrite default empty target', () => {
|
|
||||||
ctx.scope.panel = {
|
|
||||||
targets: [{
|
|
||||||
"target": "",
|
|
||||||
"refId": "A"
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
const panelCtrl = createPanelCtrl();
|
|
||||||
expect(panelCtrl.available_datasources).toEqual([
|
|
||||||
'zabbix_default', 'zabbix'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When refreshing panel', () => {
|
describe('When refreshing panel', () => {
|
||||||
@@ -112,8 +67,8 @@ describe('TriggerPanelCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should format triggers', (done) => {
|
it('should format triggers', (done) => {
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||||
const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
|
const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, {triggerid: "1"});
|
||||||
expect(formattedTrigger.host).toBe('backend01');
|
expect(formattedTrigger.host).toBe('backend01');
|
||||||
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
|
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
|
||||||
expect(formattedTrigger.datasource).toBe('zabbix_default');
|
expect(formattedTrigger.datasource).toBe('zabbix_default');
|
||||||
@@ -124,8 +79,8 @@ describe('TriggerPanelCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sort triggers by time by default', (done) => {
|
it('should sort triggers by time by default', (done) => {
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||||
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
|
||||||
expect(trigger_ids).toEqual([
|
expect(trigger_ids).toEqual([
|
||||||
'2', '4', '3', '1'
|
'2', '4', '3', '1'
|
||||||
]);
|
]);
|
||||||
@@ -134,175 +89,119 @@ describe('TriggerPanelCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sort triggers by severity', (done) => {
|
it('should sort triggers by severity', (done) => {
|
||||||
ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' };
|
ctx.panelCtrl.panel.sortProblems = 'priority';
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||||
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
|
||||||
expect(trigger_ids).toEqual([
|
expect(trigger_ids).toEqual([
|
||||||
'1', '3', '2', '4'
|
'1', '3', '2', '4'
|
||||||
]);
|
]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add acknowledges to trigger', (done) => {
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
expect(trigger.acknowledges).toHaveLength(1);
|
|
||||||
expect(trigger.acknowledges[0].message).toBe("event ack");
|
|
||||||
|
|
||||||
expect(getTriggerById(2, ctx).acknowledges).toBe(undefined);
|
|
||||||
expect(getTriggerById(3, ctx).acknowledges).toBe(undefined);
|
|
||||||
expect(getTriggerById(4, ctx).acknowledges).toBe(undefined);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When formatting triggers', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx.panelCtrl = createPanelCtrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle new lines in trigger description', () => {
|
|
||||||
ctx.panelCtrl.setTriggerSeverity = jest.fn((trigger) => trigger);
|
|
||||||
const trigger = {comments: "this is\ndescription"};
|
|
||||||
const formattedTrigger = ctx.panelCtrl.formatTrigger(trigger);
|
|
||||||
expect(formattedTrigger.comments).toBe("this is<br>description");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format host name to display (default)', (done) => {
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe('backend01');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format host name to display (tech name)', (done) => {
|
|
||||||
ctx.panelCtrl.panel.hostField = false;
|
|
||||||
ctx.panelCtrl.panel.hostTechNameField = true;
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe('backend01_tech');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format host name to display (both tech and visible)', (done) => {
|
|
||||||
ctx.panelCtrl.panel.hostField = true;
|
|
||||||
ctx.panelCtrl.panel.hostTechNameField = true;
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe('backend01 (backend01_tech)');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should hide hostname if both visible and tech name checkboxes unset', (done) => {
|
|
||||||
ctx.panelCtrl.panel.hostField = false;
|
|
||||||
ctx.panelCtrl.panel.hostTechNameField = false;
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe("");
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When formatting acknowledges', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx.panelCtrl = createPanelCtrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should build proper user name', () => {
|
|
||||||
const ack = {
|
|
||||||
alias: 'alias', name: 'name', surname: 'surname'
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatted = ctx.panelCtrl.formatAcknowledge(ack);
|
|
||||||
expect(formatted.user).toBe('alias (name surname)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty name if it is not defined', () => {
|
|
||||||
const formatted = ctx.panelCtrl.formatAcknowledge({});
|
|
||||||
expect(formatted.user).toBe('');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultTrigger: any = {
|
const defaultProblem: any = {
|
||||||
"triggerid": "13565",
|
"acknowledges": [],
|
||||||
"value": "1",
|
|
||||||
"groups": [{"groupid": "1", "name": "Backend"}] ,
|
|
||||||
"hosts": [{"host": "backend01_tech", "hostid": "10001","maintenance_status": "0", "name": "backend01"}] ,
|
|
||||||
"lastEvent": {
|
|
||||||
"eventid": "11",
|
|
||||||
"clock": "1507229064",
|
|
||||||
"ns": "556202037",
|
|
||||||
"acknowledged": "1",
|
|
||||||
"value": "1",
|
|
||||||
"object": "0",
|
|
||||||
"source": "0",
|
|
||||||
"objectid": "13565",
|
|
||||||
},
|
|
||||||
"tags": [] ,
|
|
||||||
"lastchange": "1440259530",
|
|
||||||
"priority": "2",
|
|
||||||
"description": "Lack of free swap space on server",
|
|
||||||
"comments": "It probably means that the systems requires\nmore physical memory.",
|
"comments": "It probably means that the systems requires\nmore physical memory.",
|
||||||
"url": "https://host.local/path",
|
"correlation_mode": "0",
|
||||||
"templateid": "0", "expression": "{13174}<50", "manual_close": "0", "correlation_mode": "0",
|
"correlation_tag": "",
|
||||||
"correlation_tag": "", "recovery_mode": "0", "recovery_expression": "", "state": "0", "status": "0",
|
"datasource": "zabbix_default",
|
||||||
"flags": "0", "type": "0", "items": [] , "error": ""
|
"description": "Lack of free swap space on server",
|
||||||
};
|
"error": "",
|
||||||
|
"expression": "{13297}>20",
|
||||||
const defaultEvent: any = {
|
"flags": "0",
|
||||||
"eventid": "11",
|
"groups": [
|
||||||
"acknowledges": [
|
|
||||||
{
|
{
|
||||||
"acknowledgeid": "185",
|
"groupid": "2",
|
||||||
"action": "0",
|
"name": "Linux servers"
|
||||||
"alias": "api",
|
},
|
||||||
"clock": "1512382246",
|
{
|
||||||
"eventid": "11",
|
"groupid": "9",
|
||||||
"message": "event ack",
|
"name": "Backend"
|
||||||
"name": "api",
|
|
||||||
"surname": "user",
|
|
||||||
"userid": "3"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"clock": "1507229064",
|
"hosts": [
|
||||||
"ns": "556202037",
|
{
|
||||||
"acknowledged": "1",
|
"host": "backend01_tech",
|
||||||
"value": "1",
|
"hostid": "10111",
|
||||||
"object": "0",
|
"maintenance_status": "1",
|
||||||
"source": "0",
|
"name": "backend01",
|
||||||
"objectid": "1",
|
"proxy_hostid": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemid": "23979",
|
||||||
|
"key_": "system.cpu.util[,iowait]",
|
||||||
|
"lastvalue": "25.2091",
|
||||||
|
"name": "CPU $2 time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastEvent": {
|
||||||
|
"acknowledged": "0",
|
||||||
|
"clock": "1589297010",
|
||||||
|
"eventid": "4399289",
|
||||||
|
"name": "Disk I/O is overloaded on backend01",
|
||||||
|
"ns": "224779201",
|
||||||
|
"object": "0",
|
||||||
|
"objectid": "13682",
|
||||||
|
"severity": "2",
|
||||||
|
"source": "0",
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
"lastchange": "1440259530",
|
||||||
|
"maintenance": true,
|
||||||
|
"manual_close": "0",
|
||||||
|
"priority": "2",
|
||||||
|
"recovery_expression": "",
|
||||||
|
"recovery_mode": "0",
|
||||||
|
"showAckButton": true,
|
||||||
|
"state": "0",
|
||||||
|
"status": "0",
|
||||||
|
"tags": [],
|
||||||
|
"templateid": "13671",
|
||||||
|
"triggerid": "13682",
|
||||||
|
"type": "0",
|
||||||
|
"url": "",
|
||||||
|
"value": "1"
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateTrigger(id, timestamp?, severity?): any {
|
function generateDataFramesResponse(problemDescs: any[] = [{id: 1}]): any {
|
||||||
const trigger = _.cloneDeep(defaultTrigger);
|
const problems = problemDescs.map(problem => generateProblem(problem.id, problem.lastchange, problem.priority));
|
||||||
trigger.triggerid = id.toString();
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"name": "Problems",
|
||||||
|
"state": {
|
||||||
|
"scopedVars": {},
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"type": "other",
|
||||||
|
"values": problems,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"length": 16,
|
||||||
|
"name": "problems"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateProblem(id, timestamp?, severity?): any {
|
||||||
|
const problem = _.cloneDeep(defaultProblem);
|
||||||
|
problem.triggerid = id.toString();
|
||||||
if (severity) {
|
if (severity) {
|
||||||
trigger.priority = severity.toString();
|
problem.priority = severity.toString();
|
||||||
}
|
}
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
trigger.lastchange = timestamp;
|
problem.lastchange = timestamp;
|
||||||
}
|
}
|
||||||
return trigger;
|
return problem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrigger(props): any {
|
function getProblemById(id, ctx): any {
|
||||||
let trigger = _.cloneDeep(defaultTrigger);
|
return _.find(ctx.panelCtrl.renderData, {triggerid: id.toString()});
|
||||||
trigger = _.merge(trigger, props);
|
|
||||||
trigger.lastEvent.objectid = trigger.triggerid;
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTriggerById(id, ctx): any {
|
|
||||||
return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,735 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
|
||||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
|
||||||
import * as utils from '../datasource-zabbix/utils';
|
|
||||||
import { PanelCtrl } from 'grafana/app/plugins/sdk';
|
|
||||||
import { triggerPanelOptionsTab } from './options_tab';
|
|
||||||
import { triggerPanelTriggersTab } from './triggers_tab';
|
|
||||||
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
|
|
||||||
import ProblemList from './components/Problems/Problems';
|
|
||||||
import AlertList from './components/AlertList/AlertList';
|
|
||||||
import { getNextRefIdChar } from './utils';
|
|
||||||
|
|
||||||
const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
|
|
||||||
const PROBLEM_EVENTS_LIMIT = 100;
|
|
||||||
|
|
||||||
export const DEFAULT_TARGET = {
|
|
||||||
group: {filter: ""},
|
|
||||||
host: {filter: ""},
|
|
||||||
application: {filter: ""},
|
|
||||||
trigger: {filter: ""},
|
|
||||||
tags: {filter: ""},
|
|
||||||
proxy: {filter: ""},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDefaultTarget = (targets) => {
|
|
||||||
return {
|
|
||||||
group: {filter: ""},
|
|
||||||
host: {filter: ""},
|
|
||||||
application: {filter: ""},
|
|
||||||
trigger: {filter: ""},
|
|
||||||
tags: {filter: ""},
|
|
||||||
proxy: {filter: ""},
|
|
||||||
refId: getNextRefIdChar(targets),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_SEVERITY = [
|
|
||||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
|
|
||||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true},
|
|
||||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true},
|
|
||||||
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true},
|
|
||||||
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true},
|
|
||||||
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
|
||||||
|
|
||||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
|
||||||
|
|
||||||
export const PANEL_DEFAULTS = {
|
|
||||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
||||||
targets: [getDefaultTarget([])],
|
|
||||||
// Fields
|
|
||||||
hostField: true,
|
|
||||||
hostTechNameField: false,
|
|
||||||
hostGroups: false,
|
|
||||||
hostProxy: false,
|
|
||||||
showTags: true,
|
|
||||||
statusField: true,
|
|
||||||
statusIcon: false,
|
|
||||||
severityField: true,
|
|
||||||
ageField: false,
|
|
||||||
descriptionField: true,
|
|
||||||
descriptionAtNewLine: false,
|
|
||||||
// Options
|
|
||||||
hostsInMaintenance: true,
|
|
||||||
showTriggers: 'all triggers',
|
|
||||||
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
|
||||||
showEvents: { text: 'Problems', value: 1 },
|
|
||||||
limit: 100,
|
|
||||||
// View options
|
|
||||||
layout: 'table',
|
|
||||||
fontSize: '100%',
|
|
||||||
pageSize: 10,
|
|
||||||
problemTimeline: true,
|
|
||||||
highlightBackground: false,
|
|
||||||
highlightNewEvents: false,
|
|
||||||
highlightNewerThan: '1h',
|
|
||||||
customLastChangeFormat: false,
|
|
||||||
lastChangeFormat: "",
|
|
||||||
resizedColumns: [],
|
|
||||||
// Triggers severity and colors
|
|
||||||
triggerSeverity: getDefaultSeverity(),
|
|
||||||
okEventColor: 'rgb(56, 189, 113)',
|
|
||||||
ackEventColor: 'rgb(56, 219, 156)',
|
|
||||||
markAckEvents: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerStatusMap = {
|
|
||||||
'0': 'OK',
|
|
||||||
'1': 'PROBLEM'
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TriggerPanelCtrl extends PanelCtrl {
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope, $injector, $timeout, templateSrv, contextSrv, dashboardSrv, timeSrv) {
|
|
||||||
super($scope, $injector);
|
|
||||||
this.templateSrv = templateSrv;
|
|
||||||
this.contextSrv = contextSrv;
|
|
||||||
this.dashboardSrv = dashboardSrv;
|
|
||||||
this.timeSrv = timeSrv;
|
|
||||||
this.scope = $scope;
|
|
||||||
this.$timeout = $timeout;
|
|
||||||
|
|
||||||
this.editorTabIndex = 1;
|
|
||||||
this.triggerStatusMap = triggerStatusMap;
|
|
||||||
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
|
||||||
this.pageIndex = 0;
|
|
||||||
this.triggerList = [];
|
|
||||||
this.datasources = {};
|
|
||||||
this.range = {};
|
|
||||||
|
|
||||||
this.panel = migratePanelSchema(this.panel);
|
|
||||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
|
||||||
|
|
||||||
this.available_datasources = _.map(this.getZabbixDataSources(), 'name');
|
|
||||||
if (this.panel.targets && !this.panel.targets[0].datasource) {
|
|
||||||
this.panel.targets[0].datasource = this.available_datasources[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initDatasources();
|
|
||||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
|
||||||
this.events.on('refresh', this.onRefresh.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanelError(err, defaultError) {
|
|
||||||
const defaultErrorMessage = defaultError || "Request Error";
|
|
||||||
this.inspector = { error: err };
|
|
||||||
this.error = err.message || defaultErrorMessage;
|
|
||||||
if (err.data) {
|
|
||||||
if (err.data.message) {
|
|
||||||
this.error = err.data.message;
|
|
||||||
}
|
|
||||||
if (err.data.error) {
|
|
||||||
this.error = err.data.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.events.emit('data-error', err);
|
|
||||||
console.log('Panel data error:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
initDatasources() {
|
|
||||||
if (!this.panel.targets) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource));
|
|
||||||
let promises = targetDatasources.map(ds => {
|
|
||||||
// Load datasource
|
|
||||||
return getDataSourceSrv().get(ds)
|
|
||||||
.then(datasource => {
|
|
||||||
this.datasources[ds] = datasource;
|
|
||||||
return datasource;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
getZabbixDataSources() {
|
|
||||||
return _.filter(getDataSourceSrv().getMetricSources(), datasource => {
|
|
||||||
return datasource.meta.id === ZABBIX_DS_ID && datasource.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmptyTargets() {
|
|
||||||
const emptyTargets = _.isEmpty(this.panel.targets);
|
|
||||||
const emptyTarget = (this.panel.targets.length === 1 && (
|
|
||||||
_.isEmpty(this.panel.targets[0]) ||
|
|
||||||
this.panel.targets[0].target === ""
|
|
||||||
));
|
|
||||||
return emptyTargets || emptyTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
onInitEditMode() {
|
|
||||||
this.addEditorTab('Triggers', triggerPanelTriggersTab, 1);
|
|
||||||
this.addEditorTab('Options', triggerPanelOptionsTab, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeQueryStart() {
|
|
||||||
this.timing.queryStart = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeQueryEnd() {
|
|
||||||
this.timing.queryEnd = (new Date()).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRefresh() {
|
|
||||||
// ignore fetching data if another panel is in fullscreen
|
|
||||||
if (this.otherPanelInFullscreenMode()) { return; }
|
|
||||||
|
|
||||||
this.range = this.timeSrv.timeRange();
|
|
||||||
|
|
||||||
// clear loading/error state
|
|
||||||
delete this.error;
|
|
||||||
this.loading = true;
|
|
||||||
this.setTimeQueryStart();
|
|
||||||
this.pageIndex = 0;
|
|
||||||
|
|
||||||
return this.getTriggers()
|
|
||||||
.then(triggers => {
|
|
||||||
// Notify panel that request is finished
|
|
||||||
this.loading = false;
|
|
||||||
this.setTimeQueryEnd();
|
|
||||||
return this.renderTriggers(triggers);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$timeout(() => {
|
|
||||||
this.renderingCompleted();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
if (err.cancelled) {
|
|
||||||
console.log('Panel request cancelled', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setPanelError(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTriggers(zabbixTriggers) {
|
|
||||||
let triggers = _.cloneDeep(zabbixTriggers || this.triggerListUnfiltered);
|
|
||||||
this.triggerListUnfiltered = _.cloneDeep(triggers);
|
|
||||||
|
|
||||||
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
|
||||||
triggers = this.filterTriggersPost(triggers);
|
|
||||||
triggers = this.sortTriggers(triggers);
|
|
||||||
|
|
||||||
// Limit triggers number
|
|
||||||
triggers = triggers.slice(0, this.panel.limit || PANEL_DEFAULTS.limit);
|
|
||||||
|
|
||||||
this.triggerList = triggers;
|
|
||||||
|
|
||||||
return this.$timeout(() => {
|
|
||||||
return super.render(this.triggerList);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getTriggers() {
|
|
||||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
|
||||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
|
||||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
|
||||||
|
|
||||||
let promises = _.map(this.panel.targets, (target) => {
|
|
||||||
const ds = target.datasource;
|
|
||||||
let proxies;
|
|
||||||
let showAckButton = true;
|
|
||||||
return getDataSourceSrv().get(ds)
|
|
||||||
.then(datasource => {
|
|
||||||
const zabbix = datasource.zabbix;
|
|
||||||
const showEvents = this.panel.showEvents.value;
|
|
||||||
const triggerFilter = target;
|
|
||||||
const showProxy = this.panel.hostProxy;
|
|
||||||
const getProxiesPromise = showProxy ? zabbix.getProxies() : () => [];
|
|
||||||
showAckButton = !datasource.disableReadOnlyUsersAck || userIsEditor;
|
|
||||||
|
|
||||||
// Replace template variables
|
|
||||||
const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter, this.panel.scopedVars);
|
|
||||||
const hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter, this.panel.scopedVars);
|
|
||||||
const appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter, this.panel.scopedVars);
|
|
||||||
const proxyFilter = datasource.replaceTemplateVars(triggerFilter.proxy.filter, this.panel.scopedVars);
|
|
||||||
|
|
||||||
let triggersOptions = {
|
|
||||||
showTriggers: showEvents
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showEvents !== 1) {
|
|
||||||
triggersOptions.timeFrom = timeFrom;
|
|
||||||
triggersOptions.timeTo = timeTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions, proxyFilter),
|
|
||||||
getProxiesPromise
|
|
||||||
]);
|
|
||||||
}).then(([triggers, sourceProxies]) => {
|
|
||||||
proxies = _.keyBy(sourceProxies, 'proxyid');
|
|
||||||
const eventids = _.compact(triggers.map(trigger => {
|
|
||||||
return trigger.lastEvent.eventid;
|
|
||||||
}));
|
|
||||||
return Promise.all([
|
|
||||||
this.datasources[ds].zabbix.getExtendedEventData(eventids),
|
|
||||||
Promise.resolve(triggers)
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
.then(([events, triggers]) => {
|
|
||||||
this.addEventTags(events, triggers);
|
|
||||||
this.addAcknowledges(events, triggers);
|
|
||||||
return triggers;
|
|
||||||
})
|
|
||||||
.then(triggers => this.setMaintenanceStatus(triggers))
|
|
||||||
.then(triggers => this.setAckButtonStatus(triggers, showAckButton))
|
|
||||||
.then(triggers => this.filterTriggersPre(triggers, target))
|
|
||||||
.then(triggers => this.addTriggerDataSource(triggers, target))
|
|
||||||
.then(triggers => this.addTriggerHostProxy(triggers, proxies));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(promises)
|
|
||||||
.then(results => _.flatten(results));
|
|
||||||
}
|
|
||||||
|
|
||||||
addAcknowledges(events, triggers) {
|
|
||||||
// Map events to triggers
|
|
||||||
_.each(triggers, trigger => {
|
|
||||||
var event = _.find(events, event => {
|
|
||||||
return event.eventid === trigger.lastEvent.eventid;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
trigger.acknowledges = _.map(event.acknowledges, this.formatAcknowledge.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!trigger.lastEvent.eventid) {
|
|
||||||
trigger.lastEvent = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatAcknowledge(ack) {
|
|
||||||
let timestamp = moment.unix(ack.clock);
|
|
||||||
if (this.panel.customLastChangeFormat) {
|
|
||||||
ack.time = timestamp.format(this.panel.lastChangeFormat);
|
|
||||||
} else {
|
|
||||||
ack.time = timestamp.format(this.defaultTimeFormat);
|
|
||||||
}
|
|
||||||
ack.user = ack.alias || '';
|
|
||||||
if (ack.name || ack.surname) {
|
|
||||||
const fullName = `${ack.name || ''} ${ack.surname || ''}`;
|
|
||||||
ack.user += ` (${fullName})`;
|
|
||||||
}
|
|
||||||
return ack;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventTags(events, triggers) {
|
|
||||||
_.each(triggers, trigger => {
|
|
||||||
var event = _.find(events, event => {
|
|
||||||
return event.eventid === trigger.lastEvent.eventid;
|
|
||||||
});
|
|
||||||
if (event && event.tags && event.tags.length) {
|
|
||||||
trigger.tags = event.tags;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterTriggersPre(triggerList, target) {
|
|
||||||
// Filter triggers by description
|
|
||||||
const ds = target.datasource;
|
|
||||||
let triggerFilter = target.trigger.filter;
|
|
||||||
triggerFilter = this.datasources[ds].replaceTemplateVars(triggerFilter);
|
|
||||||
if (triggerFilter) {
|
|
||||||
triggerList = filterTriggers(triggerList, triggerFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by tags
|
|
||||||
// const target = this.panel.targets[ds];
|
|
||||||
if (target.tags.filter) {
|
|
||||||
let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter);
|
|
||||||
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
|
||||||
tagsFilter = tagsFilter.replace('/^', '').replace('$/', '');
|
|
||||||
const tags = this.parseTags(tagsFilter);
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return _.every(tags, (tag) => {
|
|
||||||
return _.find(trigger.tags, {tag: tag.tag, value: tag.value});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return triggerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterTriggersPost(triggers) {
|
|
||||||
let triggerList = _.cloneDeep(triggers);
|
|
||||||
|
|
||||||
// Filter acknowledged triggers
|
|
||||||
if (this.panel.showTriggers === 'unacknowledged') {
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return !(trigger.acknowledges && trigger.acknowledges.length);
|
|
||||||
});
|
|
||||||
} else if (this.panel.showTriggers === 'acknowledged') {
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return trigger.acknowledges && trigger.acknowledges.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by maintenance status
|
|
||||||
if (!this.panel.hostsInMaintenance) {
|
|
||||||
triggerList = _.filter(triggerList, (trigger) => trigger.maintenance === false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter triggers by severity
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
if (trigger.lastEvent && trigger.lastEvent.severity) {
|
|
||||||
return this.panel.triggerSeverity[trigger.lastEvent.severity].show;
|
|
||||||
} else {
|
|
||||||
return this.panel.triggerSeverity[trigger.priority].show;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return triggerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaintenanceStatus(triggers) {
|
|
||||||
_.each(triggers, (trigger) => {
|
|
||||||
let maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1');
|
|
||||||
trigger.maintenance = maintenance_status;
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAckButtonStatus(triggers, showAckButton) {
|
|
||||||
_.each(triggers, (trigger) => {
|
|
||||||
trigger.showAckButton = showAckButton;
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
addTriggerDataSource(triggers, target) {
|
|
||||||
_.each(triggers, (trigger) => {
|
|
||||||
trigger.datasource = target.datasource;
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
addTriggerHostProxy(triggers, proxies) {
|
|
||||||
triggers.forEach(trigger => {
|
|
||||||
if (trigger.hosts && trigger.hosts.length) {
|
|
||||||
let host = trigger.hosts[0];
|
|
||||||
if (host.proxy_hostid !== '0') {
|
|
||||||
const hostProxy = proxies[host.proxy_hostid];
|
|
||||||
host.proxy = hostProxy ? hostProxy.host : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
sortTriggers(triggerList) {
|
|
||||||
if (this.panel.sortTriggersBy.value === 'priority') {
|
|
||||||
triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']);
|
|
||||||
} else {
|
|
||||||
triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']);
|
|
||||||
}
|
|
||||||
return triggerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTrigger(zabbixTrigger) {
|
|
||||||
let trigger = _.cloneDeep(zabbixTrigger);
|
|
||||||
|
|
||||||
// Set host and proxy that the trigger belongs
|
|
||||||
if (trigger.hosts && trigger.hosts.length) {
|
|
||||||
const host = trigger.hosts[0];
|
|
||||||
trigger.host = host.name;
|
|
||||||
trigger.hostTechName = host.host;
|
|
||||||
if (host.proxy) {
|
|
||||||
trigger.proxy = host.proxy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set tags if present
|
|
||||||
if (trigger.tags && trigger.tags.length === 0) {
|
|
||||||
trigger.tags = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle multi-line description
|
|
||||||
if (trigger.comments) {
|
|
||||||
trigger.comments = trigger.comments.replace('\n', '<br>');
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger.lastchangeUnix = Number(trigger.lastchange);
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseTags(tagStr) {
|
|
||||||
if (!tagStr) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
|
||||||
tags = _.map(tags, (tag) => {
|
|
||||||
const tagParts = tag.split(':');
|
|
||||||
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
|
||||||
});
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
tagsToString(tags) {
|
|
||||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
addTagFilter(tag, datasource) {
|
|
||||||
const target = this.panel.targets.find(t => t.datasource === datasource);
|
|
||||||
console.log(target);
|
|
||||||
let tagFilter = target.tags.filter;
|
|
||||||
let targetTags = this.parseTags(tagFilter);
|
|
||||||
let newTag = {tag: tag.tag, value: tag.value};
|
|
||||||
targetTags.push(newTag);
|
|
||||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
|
||||||
let newFilter = this.tagsToString(targetTags);
|
|
||||||
target.tags.filter = newFilter;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTagFilter(tag, datasource) {
|
|
||||||
const target = this.panel.targets.find(t => t.datasource === datasource);
|
|
||||||
let tagFilter = target.tags.filter;
|
|
||||||
let targetTags = this.parseTags(tagFilter);
|
|
||||||
_.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value);
|
|
||||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
|
||||||
let newFilter = this.tagsToString(targetTags);
|
|
||||||
target.tags.filter = newFilter;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
getProblemEvents(problem) {
|
|
||||||
const triggerids = [problem.triggerid];
|
|
||||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
|
||||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
|
||||||
return getDataSourceSrv().get(problem.datasource)
|
|
||||||
.then(datasource => {
|
|
||||||
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getProblemAlerts(problem) {
|
|
||||||
if (!problem.lastEvent || problem.lastEvent.length === 0) {
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
const eventids = [problem.lastEvent.eventid];
|
|
||||||
return getDataSourceSrv().get(problem.datasource)
|
|
||||||
.then(datasource => {
|
|
||||||
return datasource.zabbix.getEventAlerts(eventids);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatHostName(trigger) {
|
|
||||||
let host = "";
|
|
||||||
if (this.panel.hostField && this.panel.hostTechNameField) {
|
|
||||||
host = `${trigger.host} (${trigger.hostTechName})`;
|
|
||||||
} else if (this.panel.hostField || this.panel.hostTechNameField) {
|
|
||||||
host = this.panel.hostField ? trigger.host : trigger.hostTechName;
|
|
||||||
}
|
|
||||||
if (this.panel.hostProxy && trigger.proxy) {
|
|
||||||
host = `${trigger.proxy}: ${host}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatHostGroups(trigger) {
|
|
||||||
let groupNames = "";
|
|
||||||
if (this.panel.hostGroups) {
|
|
||||||
let groups = _.map(trigger.groups, 'name').join(', ');
|
|
||||||
groupNames += `[ ${groups} ]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNewTrigger(trigger) {
|
|
||||||
try {
|
|
||||||
const highlightIntervalMs = utils.parseInterval(this.panel.highlightNewerThan || PANEL_DEFAULTS.highlightNewerThan);
|
|
||||||
const durationSec = (Date.now() - trigger.lastchangeUnix * 1000);
|
|
||||||
return durationSec < highlightIntervalMs;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAlertIconClass(trigger) {
|
|
||||||
let iconClass = '';
|
|
||||||
if (trigger.value === '1' && trigger.priority >= 2) {
|
|
||||||
iconClass = 'icon-gf-critical';
|
|
||||||
} else {
|
|
||||||
iconClass = 'icon-gf-online';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) {
|
|
||||||
iconClass += ' zabbix-trigger--blinked';
|
|
||||||
}
|
|
||||||
return iconClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAlertIconClassBySeverity(triggerSeverity) {
|
|
||||||
let iconClass = 'icon-gf-online';
|
|
||||||
if (triggerSeverity.priority >= 2) {
|
|
||||||
iconClass = 'icon-gf-critical';
|
|
||||||
}
|
|
||||||
return iconClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAlertStateClass(trigger) {
|
|
||||||
let statusClass = '';
|
|
||||||
|
|
||||||
if (trigger.value === '1') {
|
|
||||||
statusClass = 'alert-state-critical';
|
|
||||||
} else {
|
|
||||||
statusClass = 'alert-state-ok';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) {
|
|
||||||
statusClass += ' zabbix-trigger--blinked';
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetResizedColumns() {
|
|
||||||
this.panel.resizedColumns = [];
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
acknowledgeTrigger(trigger, message) {
|
|
||||||
let eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null;
|
|
||||||
let grafana_user = this.contextSrv.user.name;
|
|
||||||
let ack_message = grafana_user + ' (Grafana): ' + message;
|
|
||||||
return getDataSourceSrv().get(trigger.datasource)
|
|
||||||
.then(datasource => {
|
|
||||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
|
||||||
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
|
||||||
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
|
||||||
}
|
|
||||||
if (eventid) {
|
|
||||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message);
|
|
||||||
} else {
|
|
||||||
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(this.onRefresh.bind(this))
|
|
||||||
.catch((err) => {
|
|
||||||
this.setPanelError(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePageSizeChange(pageSize, pageIndex) {
|
|
||||||
this.panel.pageSize = pageSize;
|
|
||||||
this.pageIndex = pageIndex;
|
|
||||||
this.scope.$apply(() => {
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleColumnResize(newResized) {
|
|
||||||
this.panel.resizedColumns = newResized;
|
|
||||||
this.scope.$apply(() => {
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
link(scope, elem, attrs, ctrl) {
|
|
||||||
let panel = ctrl.panel;
|
|
||||||
let triggerList = ctrl.triggerList;
|
|
||||||
|
|
||||||
scope.$watchGroup(['ctrl.triggerList'], renderPanel);
|
|
||||||
ctrl.events.on('render', (renderData) => {
|
|
||||||
triggerList = renderData || triggerList;
|
|
||||||
renderPanel();
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderPanel() {
|
|
||||||
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
|
||||||
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
|
||||||
|
|
||||||
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1));
|
|
||||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
|
||||||
|
|
||||||
const pageSize = panel.pageSize || 10;
|
|
||||||
const loading = ctrl.loading && (!ctrl.triggerList || !ctrl.triggerList.length);
|
|
||||||
|
|
||||||
let panelOptions = {};
|
|
||||||
for (let prop in PANEL_DEFAULTS) {
|
|
||||||
panelOptions[prop] = ctrl.panel[prop];
|
|
||||||
}
|
|
||||||
const problemsListProps = {
|
|
||||||
problems: ctrl.triggerList,
|
|
||||||
panelOptions,
|
|
||||||
timeRange: { timeFrom, timeTo },
|
|
||||||
loading,
|
|
||||||
pageSize,
|
|
||||||
fontSize: fontSizeProp,
|
|
||||||
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
|
||||||
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
|
||||||
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
|
||||||
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
|
||||||
onProblemAck: (trigger, data) => {
|
|
||||||
const message = data.message;
|
|
||||||
return ctrl.acknowledgeTrigger(trigger, message);
|
|
||||||
},
|
|
||||||
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
|
||||||
if (ctrlKey || shiftKey) {
|
|
||||||
ctrl.removeTagFilter(tag, datasource);
|
|
||||||
} else {
|
|
||||||
ctrl.addTagFilter(tag, datasource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let problemsReactElem;
|
|
||||||
if (panel.layout === 'list') {
|
|
||||||
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
|
||||||
} else {
|
|
||||||
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelContainerElem = elem.find('.panel-content');
|
|
||||||
if (panelContainerElem && panelContainerElem.length) {
|
|
||||||
ReactDOM.render(problemsReactElem, panelContainerElem[0]);
|
|
||||||
} else {
|
|
||||||
ReactDOM.render(problemsReactElem, elem[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
|
||||||
|
|
||||||
function filterTriggers(triggers, triggerFilter) {
|
|
||||||
if (utils.isRegex(triggerFilter)) {
|
|
||||||
return _.filter(triggers, function(trigger) {
|
|
||||||
return utils.buildRegex(triggerFilter).test(trigger.description);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return _.filter(triggers, function(trigger) {
|
|
||||||
return trigger.description === triggerFilter;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
435
src/panel-triggers/triggers_panel_ctrl.ts
Normal file
435
src/panel-triggers/triggers_panel_ctrl.ts
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { PanelEvents } from '@grafana/data';
|
||||||
|
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||||
|
import * as utils from '../datasource-zabbix/utils';
|
||||||
|
import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';
|
||||||
|
import { triggerPanelOptionsTab } from './options_tab';
|
||||||
|
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||||
|
import ProblemList from './components/Problems/Problems';
|
||||||
|
import AlertList from './components/AlertList/AlertList';
|
||||||
|
|
||||||
|
const PROBLEM_EVENTS_LIMIT = 100;
|
||||||
|
|
||||||
|
export const DEFAULT_TARGET = {
|
||||||
|
group: {filter: ""},
|
||||||
|
host: {filter: ""},
|
||||||
|
application: {filter: ""},
|
||||||
|
trigger: {filter: ""},
|
||||||
|
tags: {filter: ""},
|
||||||
|
proxy: {filter: ""},
|
||||||
|
showProblems: 'problems',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_SEVERITY = [
|
||||||
|
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
|
||||||
|
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true},
|
||||||
|
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true},
|
||||||
|
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true},
|
||||||
|
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true},
|
||||||
|
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
||||||
|
|
||||||
|
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||||
|
|
||||||
|
export const PANEL_DEFAULTS = {
|
||||||
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||||
|
// Fields
|
||||||
|
hostField: true,
|
||||||
|
hostTechNameField: false,
|
||||||
|
hostProxy: false,
|
||||||
|
hostGroups: false,
|
||||||
|
showTags: true,
|
||||||
|
statusField: true,
|
||||||
|
statusIcon: false,
|
||||||
|
severityField: true,
|
||||||
|
ageField: false,
|
||||||
|
descriptionField: true,
|
||||||
|
descriptionAtNewLine: false,
|
||||||
|
// Options
|
||||||
|
sortProblems: 'lastchange',
|
||||||
|
limit: null,
|
||||||
|
// View options
|
||||||
|
layout: 'table',
|
||||||
|
fontSize: '100%',
|
||||||
|
pageSize: 10,
|
||||||
|
problemTimeline: true,
|
||||||
|
highlightBackground: false,
|
||||||
|
highlightNewEvents: false,
|
||||||
|
highlightNewerThan: '1h',
|
||||||
|
customLastChangeFormat: false,
|
||||||
|
lastChangeFormat: "",
|
||||||
|
resizedColumns: [],
|
||||||
|
// Triggers severity and colors
|
||||||
|
triggerSeverity: getDefaultSeverity(),
|
||||||
|
okEventColor: 'rgb(56, 189, 113)',
|
||||||
|
ackEventColor: 'rgb(56, 219, 156)',
|
||||||
|
markAckEvents: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerStatusMap = {
|
||||||
|
'0': 'OK',
|
||||||
|
'1': 'PROBLEM'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||||
|
scope: any;
|
||||||
|
useDataFrames: boolean;
|
||||||
|
triggerStatusMap: any;
|
||||||
|
defaultTimeFormat: string;
|
||||||
|
pageIndex: number;
|
||||||
|
renderData: any[];
|
||||||
|
problems: any[];
|
||||||
|
contextSrv: any;
|
||||||
|
static templateUrl: string;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope, $injector, $timeout) {
|
||||||
|
super($scope, $injector);
|
||||||
|
this.scope = $scope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
|
||||||
|
// Tell Grafana do not convert data frames to table or series
|
||||||
|
this.useDataFrames = true;
|
||||||
|
|
||||||
|
this.triggerStatusMap = triggerStatusMap;
|
||||||
|
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
||||||
|
this.pageIndex = 0;
|
||||||
|
this.range = {};
|
||||||
|
this.renderData = [];
|
||||||
|
|
||||||
|
this.panel = migratePanelSchema(this.panel);
|
||||||
|
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||||
|
|
||||||
|
// this.events.on(PanelEvents.render, this.onRender.bind(this));
|
||||||
|
this.events.on('data-frames-received', this.onDataFramesReceived.bind(this));
|
||||||
|
this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this));
|
||||||
|
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitEditMode() {
|
||||||
|
// Update schema version to prevent migration on up-to-date targets
|
||||||
|
this.panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||||
|
this.addEditorTab('Options', triggerPanelOptionsTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataFramesReceived(data: any): Promise<any> {
|
||||||
|
this.range = this.timeSrv.timeRange();
|
||||||
|
let problems = [];
|
||||||
|
|
||||||
|
if (data && data.length) {
|
||||||
|
for (const dataFrame of data) {
|
||||||
|
try {
|
||||||
|
const values = dataFrame.fields[0].values;
|
||||||
|
if (values.toArray) {
|
||||||
|
problems.push(values.toArray());
|
||||||
|
} else if (values.length > 0) {
|
||||||
|
// On snapshot mode values is a plain Array, not ArrayVector
|
||||||
|
problems.push(values);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
problems = _.flatten(problems);
|
||||||
|
this.problems = problems;
|
||||||
|
return this.renderProblems(problems);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataSnapshotLoad(snapshotData) {
|
||||||
|
return this.onDataFramesReceived(snapshotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
reRenderProblems() {
|
||||||
|
if (this.problems) {
|
||||||
|
this.renderProblems(this.problems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanelError(err, defaultError = "Request Error") {
|
||||||
|
this.inspector = { error: err };
|
||||||
|
this.error = err.message || defaultError;
|
||||||
|
if (err.data) {
|
||||||
|
if (err.data.message) {
|
||||||
|
this.error = err.data.message;
|
||||||
|
}
|
||||||
|
if (err.data.error) {
|
||||||
|
this.error = err.data.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.events.emit(PanelEvents.dataError, err);
|
||||||
|
console.log('Panel data error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProblems(problems) {
|
||||||
|
let triggers = _.cloneDeep(problems);
|
||||||
|
|
||||||
|
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
||||||
|
triggers = this.filterTriggersPost(triggers);
|
||||||
|
triggers = this.sortTriggers(triggers);
|
||||||
|
|
||||||
|
// Limit triggers number
|
||||||
|
if (this.panel.limit) {
|
||||||
|
triggers = triggers.slice(0, this.panel.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderData = triggers;
|
||||||
|
|
||||||
|
return this.$timeout(() => {
|
||||||
|
return super.render(triggers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterTriggersPost(triggers) {
|
||||||
|
let triggerList = _.cloneDeep(triggers);
|
||||||
|
|
||||||
|
// Filter acknowledged triggers
|
||||||
|
if (this.panel.showTriggers === 'unacknowledged') {
|
||||||
|
triggerList = _.filter(triggerList, trigger => {
|
||||||
|
return !(trigger.acknowledges && trigger.acknowledges.length);
|
||||||
|
});
|
||||||
|
} else if (this.panel.showTriggers === 'acknowledged') {
|
||||||
|
triggerList = _.filter(triggerList, trigger => {
|
||||||
|
return trigger.acknowledges && trigger.acknowledges.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter triggers by severity
|
||||||
|
triggerList = _.filter(triggerList, trigger => {
|
||||||
|
if (trigger.lastEvent && trigger.lastEvent.severity) {
|
||||||
|
return this.panel.triggerSeverity[trigger.lastEvent.severity].show;
|
||||||
|
} else {
|
||||||
|
return this.panel.triggerSeverity[trigger.priority].show;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return triggerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortTriggers(triggerList) {
|
||||||
|
if (this.panel.sortProblems === 'priority') {
|
||||||
|
triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||||
|
} else if (this.panel.sortProblems === 'lastchange') {
|
||||||
|
triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||||
|
}
|
||||||
|
return triggerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTrigger(zabbixTrigger) {
|
||||||
|
const trigger = _.cloneDeep(zabbixTrigger);
|
||||||
|
|
||||||
|
// Set host and proxy that the trigger belongs
|
||||||
|
if (trigger.hosts && trigger.hosts.length) {
|
||||||
|
const host = trigger.hosts[0];
|
||||||
|
trigger.host = host.name;
|
||||||
|
trigger.hostTechName = host.host;
|
||||||
|
if (host.proxy) {
|
||||||
|
trigger.proxy = host.proxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set tags if present
|
||||||
|
if (trigger.tags && trigger.tags.length === 0) {
|
||||||
|
trigger.tags = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multi-line description
|
||||||
|
if (trigger.comments) {
|
||||||
|
trigger.comments = trigger.comments.replace('\n', '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.lastchangeUnix = Number(trigger.lastchange);
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTags(tagStr) {
|
||||||
|
if (!tagStr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||||
|
tags = _.map(tags, (tag) => {
|
||||||
|
const tagParts = tag.split(':');
|
||||||
|
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
||||||
|
});
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsToString(tags) {
|
||||||
|
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagFilter(tag, datasource) {
|
||||||
|
for (const target of this.panel.targets) {
|
||||||
|
if (target.datasource === datasource || this.panel.datasource === datasource) {
|
||||||
|
const tagFilter = target.tags.filter;
|
||||||
|
let targetTags = this.parseTags(tagFilter);
|
||||||
|
const newTag = {tag: tag.tag, value: tag.value};
|
||||||
|
targetTags.push(newTag);
|
||||||
|
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||||
|
const newFilter = this.tagsToString(targetTags);
|
||||||
|
target.tags.filter = newFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTagFilter(tag, datasource) {
|
||||||
|
const matchTag = t => t.tag === tag.tag && t.value === tag.value;
|
||||||
|
for (const target of this.panel.targets) {
|
||||||
|
if (target.datasource === datasource || this.panel.datasource === datasource) {
|
||||||
|
const tagFilter = target.tags.filter;
|
||||||
|
let targetTags = this.parseTags(tagFilter);
|
||||||
|
_.remove(targetTags, matchTag);
|
||||||
|
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||||
|
const newFilter = this.tagsToString(targetTags);
|
||||||
|
target.tags.filter = newFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
getProblemEvents(problem) {
|
||||||
|
const triggerids = [problem.triggerid];
|
||||||
|
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||||
|
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||||
|
return getDataSourceSrv().get(problem.datasource)
|
||||||
|
.then((datasource: any) => {
|
||||||
|
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProblemAlerts(problem) {
|
||||||
|
if (!problem.lastEvent || problem.lastEvent.length === 0) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
const eventids = [problem.lastEvent.eventid];
|
||||||
|
return getDataSourceSrv().get(problem.datasource)
|
||||||
|
.then((datasource: any) => {
|
||||||
|
return datasource.zabbix.getEventAlerts(eventids);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlertIconClassBySeverity(triggerSeverity) {
|
||||||
|
let iconClass = 'icon-gf-online';
|
||||||
|
if (triggerSeverity.priority >= 2) {
|
||||||
|
iconClass = 'icon-gf-critical';
|
||||||
|
}
|
||||||
|
return iconClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetResizedColumns() {
|
||||||
|
this.panel.resizedColumns = [];
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
acknowledgeTrigger(trigger, message) {
|
||||||
|
const eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null;
|
||||||
|
const grafana_user = this.contextSrv.user.name;
|
||||||
|
const ack_message = grafana_user + ' (Grafana): ' + message;
|
||||||
|
return getDataSourceSrv().get(trigger.datasource)
|
||||||
|
.then((datasource: any) => {
|
||||||
|
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||||
|
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
||||||
|
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
||||||
|
}
|
||||||
|
if (eventid) {
|
||||||
|
return datasource.zabbix.acknowledgeEvent(eventid, ack_message);
|
||||||
|
} else {
|
||||||
|
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(this.refresh.bind(this))
|
||||||
|
.catch((err) => {
|
||||||
|
this.setPanelError(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePageSizeChange(pageSize, pageIndex) {
|
||||||
|
this.panel.pageSize = pageSize;
|
||||||
|
this.pageIndex = pageIndex;
|
||||||
|
this.scope.$apply(() => {
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColumnResize(newResized) {
|
||||||
|
this.panel.resizedColumns = newResized;
|
||||||
|
this.scope.$apply(() => {
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
link(scope, elem, attrs, ctrl) {
|
||||||
|
const panel = ctrl.panel;
|
||||||
|
|
||||||
|
ctrl.events.on(PanelEvents.render, (renderData) => {
|
||||||
|
renderData = renderData || this.renderData;
|
||||||
|
renderPanel(renderData);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderPanel(problems) {
|
||||||
|
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
||||||
|
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
||||||
|
|
||||||
|
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1), 10);
|
||||||
|
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||||
|
|
||||||
|
const pageSize = panel.pageSize || 10;
|
||||||
|
const loading = ctrl.loading && (!problems || !problems.length);
|
||||||
|
|
||||||
|
const panelOptions = {};
|
||||||
|
for (const prop in PANEL_DEFAULTS) {
|
||||||
|
panelOptions[prop] = ctrl.panel[prop];
|
||||||
|
}
|
||||||
|
const problemsListProps = {
|
||||||
|
problems,
|
||||||
|
panelOptions,
|
||||||
|
timeRange: { timeFrom, timeTo },
|
||||||
|
loading,
|
||||||
|
pageSize,
|
||||||
|
fontSize: fontSizeProp,
|
||||||
|
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||||
|
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
||||||
|
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||||
|
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||||
|
onProblemAck: (trigger, data) => {
|
||||||
|
const message = data.message;
|
||||||
|
return ctrl.acknowledgeTrigger(trigger, message);
|
||||||
|
},
|
||||||
|
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
||||||
|
if (ctrlKey || shiftKey) {
|
||||||
|
ctrl.removeTagFilter(tag, datasource);
|
||||||
|
} else {
|
||||||
|
ctrl.addTagFilter(tag, datasource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let problemsReactElem;
|
||||||
|
if (panel.layout === 'list') {
|
||||||
|
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
||||||
|
} else {
|
||||||
|
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelContainerElem = elem.find('.panel-content');
|
||||||
|
if (panelContainerElem && panelContainerElem.length) {
|
||||||
|
ReactDOM.render(problemsReactElem, panelContainerElem[0]);
|
||||||
|
} else {
|
||||||
|
ReactDOM.render(problemsReactElem, elem[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import * as utils from '../datasource-zabbix/utils';
|
|
||||||
import { getDefaultTarget } from './triggers_panel_ctrl';
|
|
||||||
|
|
||||||
class TriggersTabCtrl {
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope, $rootScope, uiSegmentSrv, templateSrv) {
|
|
||||||
$scope.editor = this;
|
|
||||||
this.panelCtrl = $scope.ctrl;
|
|
||||||
this.panel = this.panelCtrl.panel;
|
|
||||||
this.templateSrv = templateSrv;
|
|
||||||
this.datasources = {};
|
|
||||||
|
|
||||||
// Load scope defaults
|
|
||||||
var scopeDefaults = {
|
|
||||||
getGroupNames: {},
|
|
||||||
getHostNames: {},
|
|
||||||
getApplicationNames: {},
|
|
||||||
getProxyNames: {},
|
|
||||||
oldTarget: _.cloneDeep(this.panel.targets)
|
|
||||||
};
|
|
||||||
_.defaultsDeep(this, scopeDefaults);
|
|
||||||
this.selectedDatasources = this.getSelectedDatasources();
|
|
||||||
|
|
||||||
this.initDatasources();
|
|
||||||
this.panelCtrl.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
initDatasources() {
|
|
||||||
return this.panelCtrl.initDatasources()
|
|
||||||
.then((datasources) => {
|
|
||||||
_.each(datasources, (datasource) => {
|
|
||||||
this.datasources[datasource.name] = datasource;
|
|
||||||
this.bindSuggestionFunctions(datasource);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSuggestionFunctions(datasource) {
|
|
||||||
// Map functions for bs-typeahead
|
|
||||||
let ds = datasource.name;
|
|
||||||
this.getGroupNames[ds] = _.bind(this.suggestGroups, this, datasource);
|
|
||||||
this.getHostNames[ds] = _.bind(this.suggestHosts, this, datasource);
|
|
||||||
this.getApplicationNames[ds] = _.bind(this.suggestApps, this, datasource);
|
|
||||||
this.getProxyNames[ds] = _.bind(this.suggestProxies, this, datasource);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedDatasources() {
|
|
||||||
return _.compact(this.panel.targets.map(target => target.datasource));
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestGroups(datasource, query, callback) {
|
|
||||||
return datasource.zabbix.getAllGroups()
|
|
||||||
.then(groups => {
|
|
||||||
return _.map(groups, 'name');
|
|
||||||
})
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestHosts(datasource, query, callback) {
|
|
||||||
const target = this.panel.targets.find(t => t.datasource === datasource.name);
|
|
||||||
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
|
|
||||||
return datasource.zabbix.getAllHosts(groupFilter)
|
|
||||||
.then(hosts => {
|
|
||||||
return _.map(hosts, 'name');
|
|
||||||
})
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestApps(datasource, query, callback) {
|
|
||||||
const target = this.panel.targets.find(t => t.datasource === datasource.name);
|
|
||||||
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
|
|
||||||
let hostFilter = datasource.replaceTemplateVars(target.host.filter);
|
|
||||||
return datasource.zabbix.getAllApps(groupFilter, hostFilter)
|
|
||||||
.then(apps => {
|
|
||||||
return _.map(apps, 'name');
|
|
||||||
})
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestProxies(datasource, query, callback) {
|
|
||||||
return datasource.zabbix.getProxies()
|
|
||||||
.then(proxies => _.map(proxies, 'host'))
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
datasourcesChanged() {
|
|
||||||
const newTargets = [];
|
|
||||||
_.each(this.selectedDatasources, (ds) => {
|
|
||||||
const dsTarget = this.panel.targets.find((target => target.datasource === ds));
|
|
||||||
if (dsTarget) {
|
|
||||||
newTargets.push(dsTarget);
|
|
||||||
} else {
|
|
||||||
const newTarget = getDefaultTarget(this.panel.targets);
|
|
||||||
newTarget.datasource = ds;
|
|
||||||
newTargets.push(newTarget);
|
|
||||||
}
|
|
||||||
this.panel.targets = newTargets;
|
|
||||||
});
|
|
||||||
this.parseTarget();
|
|
||||||
}
|
|
||||||
|
|
||||||
parseTarget() {
|
|
||||||
this.initDatasources()
|
|
||||||
.then(() => {
|
|
||||||
var newTarget = _.cloneDeep(this.panel.targets);
|
|
||||||
if (!_.isEqual(this.oldTarget, newTarget)) {
|
|
||||||
this.oldTarget = newTarget;
|
|
||||||
this.panelCtrl.refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isRegex(str) {
|
|
||||||
return utils.isRegex(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
isVariable(str) {
|
|
||||||
return utils.isTemplateVariable(str, this.templateSrv.variables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function triggerPanelTriggersTab() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: true,
|
|
||||||
templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/triggers_tab.html',
|
|
||||||
controller: TriggersTabCtrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
/* globals global: false */
|
/* globals global: false */
|
||||||
|
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import { PanelCtrl } from './panelStub';
|
import { PanelCtrl, MetricsPanelCtrl } from './panelStub';
|
||||||
|
|
||||||
// Mock Grafana modules that are not available outside of the core project
|
// Mock Grafana modules that are not available outside of the core project
|
||||||
// Required for loading module.js
|
// Required for loading module.js
|
||||||
@@ -24,12 +24,19 @@ jest.mock('grafana/app/core/core_module', () => {
|
|||||||
};
|
};
|
||||||
}, {virtual: true});
|
}, {virtual: true});
|
||||||
|
|
||||||
let mockPanelCtrl = PanelCtrl;
|
jest.mock('grafana/app/core/core', () => ({
|
||||||
|
contextSrv: {},
|
||||||
|
}), {virtual: true});
|
||||||
|
|
||||||
|
const mockPanelCtrl = PanelCtrl;
|
||||||
|
const mockMetricsPanelCtrl = MetricsPanelCtrl;
|
||||||
|
|
||||||
jest.mock('grafana/app/plugins/sdk', () => {
|
jest.mock('grafana/app/plugins/sdk', () => {
|
||||||
return {
|
return {
|
||||||
QueryCtrl: null,
|
QueryCtrl: null,
|
||||||
loadPluginCss: () => {},
|
loadPluginCss: () => {},
|
||||||
PanelCtrl: mockPanelCtrl
|
PanelCtrl: mockPanelCtrl,
|
||||||
|
MetricsPanelCtrl: mockMetricsPanelCtrl,
|
||||||
};
|
};
|
||||||
}, {virtual: true});
|
}, {virtual: true});
|
||||||
|
|
||||||
@@ -85,3 +92,7 @@ let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
|
|||||||
global.window = dom.window;
|
global.window = dom.window;
|
||||||
global.document = global.window.document;
|
global.document = global.window.document;
|
||||||
global.Node = window.Node;
|
global.Node = window.Node;
|
||||||
|
|
||||||
|
// Mock Canvas.getContext(), fixes
|
||||||
|
// Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
|
||||||
|
window.HTMLCanvasElement.prototype.getContext = () => {};
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
// JSHint options
|
|
||||||
/* jshint ignore:start */
|
|
||||||
|
|
||||||
export class PanelCtrl {
|
|
||||||
constructor($scope, $injector) {
|
|
||||||
this.$injector = $injector;
|
|
||||||
this.$scope = $scope;
|
|
||||||
this.panel = $scope.panel;
|
|
||||||
this.timing = {};
|
|
||||||
this.events = {
|
|
||||||
on: () => {},
|
|
||||||
emit: () => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingCompleted() {
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
}
|
|
||||||
|
|
||||||
publishAppEvent(evtName, evt) {
|
|
||||||
}
|
|
||||||
|
|
||||||
changeView(fullscreen, edit) {
|
|
||||||
}
|
|
||||||
|
|
||||||
viewPanel() {
|
|
||||||
this.changeView(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
editPanel() {
|
|
||||||
this.changeView(true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
exitFullscreen() {
|
|
||||||
this.changeView(false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
initEditMode() {
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTab(newIndex) {
|
|
||||||
}
|
|
||||||
|
|
||||||
addEditorTab(title, directiveFn, index) {
|
|
||||||
}
|
|
||||||
|
|
||||||
getMenu() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtendedMenu() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
otherPanelInFullscreenMode() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculatePanelHeight() {
|
|
||||||
}
|
|
||||||
|
|
||||||
render(payload) {
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleEditorHelp(index) {
|
|
||||||
}
|
|
||||||
|
|
||||||
duplicate() {
|
|
||||||
}
|
|
||||||
|
|
||||||
updateColumnSpan(span) {
|
|
||||||
}
|
|
||||||
|
|
||||||
removePanel() {
|
|
||||||
}
|
|
||||||
|
|
||||||
editPanelJson() {
|
|
||||||
}
|
|
||||||
|
|
||||||
replacePanel(newPanel, oldPanel) {
|
|
||||||
}
|
|
||||||
|
|
||||||
sharePanel() {
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfoMode() {
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfoContent(options) {
|
|
||||||
}
|
|
||||||
|
|
||||||
openInspector() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
162
src/test-setup/panelStub.ts
Normal file
162
src/test-setup/panelStub.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { PanelEvents } from '@grafana/data';
|
||||||
|
|
||||||
|
export class PanelCtrl {
|
||||||
|
panel: any;
|
||||||
|
error: any;
|
||||||
|
dashboard: any;
|
||||||
|
pluginName: string;
|
||||||
|
pluginId: string;
|
||||||
|
editorTabs: any;
|
||||||
|
$scope: any;
|
||||||
|
$injector: any;
|
||||||
|
$location: any;
|
||||||
|
$timeout: any;
|
||||||
|
editModeInitiated: boolean;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
containerHeight: any;
|
||||||
|
events: any;
|
||||||
|
loading: boolean;
|
||||||
|
timing: any;
|
||||||
|
|
||||||
|
constructor($scope, $injector) {
|
||||||
|
this.$injector = $injector;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.panel = $scope.panel;
|
||||||
|
this.timing = {};
|
||||||
|
this.events = {
|
||||||
|
on: () => {},
|
||||||
|
emit: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
renderingCompleted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
}
|
||||||
|
|
||||||
|
publishAppEvent(evtName, evt) {
|
||||||
|
}
|
||||||
|
|
||||||
|
changeView(fullscreen, edit) {
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPanel() {
|
||||||
|
this.changeView(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
editPanel() {
|
||||||
|
this.changeView(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitFullscreen() {
|
||||||
|
this.changeView(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initEditMode() {
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTab(newIndex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
addEditorTab(title, directiveFn, index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenu() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtendedMenu() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
otherPanelInFullscreenMode() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatePanelHeight() {
|
||||||
|
}
|
||||||
|
|
||||||
|
render(payload) {
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEditorHelp(index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnSpan(span) {
|
||||||
|
}
|
||||||
|
|
||||||
|
removePanel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
editPanelJson() {
|
||||||
|
}
|
||||||
|
|
||||||
|
replacePanel(newPanel, oldPanel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
sharePanel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoMode() {
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoContent(options) {
|
||||||
|
}
|
||||||
|
|
||||||
|
openInspector() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetricsPanelCtrl extends PanelCtrl {
|
||||||
|
scope: any;
|
||||||
|
datasource: any;
|
||||||
|
$timeout: any;
|
||||||
|
contextSrv: any;
|
||||||
|
datasourceSrv: any;
|
||||||
|
timeSrv: any;
|
||||||
|
templateSrv: any;
|
||||||
|
range: any;
|
||||||
|
interval: any;
|
||||||
|
intervalMs: any;
|
||||||
|
resolution: any;
|
||||||
|
timeInfo?: string;
|
||||||
|
skipDataOnInit: boolean;
|
||||||
|
dataList: any[];
|
||||||
|
querySubscription?: any;
|
||||||
|
useDataFrames = false;
|
||||||
|
|
||||||
|
constructor($scope, $injector) {
|
||||||
|
super($scope, $injector);
|
||||||
|
|
||||||
|
this.events.on(PanelEvents.refresh, this.onMetricsPanelRefresh.bind(this));
|
||||||
|
|
||||||
|
this.timeSrv = {
|
||||||
|
timeRange: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitMetricsPanelEditMode() {}
|
||||||
|
onMetricsPanelRefresh() {}
|
||||||
|
setTimeQueryStart() {}
|
||||||
|
setTimeQueryEnd() {}
|
||||||
|
updateTimeRange() {}
|
||||||
|
calculateInterval() {}
|
||||||
|
applyPanelTimeOverrides() {}
|
||||||
|
issueQueries(datasource) {}
|
||||||
|
handleQueryResult(result) {}
|
||||||
|
handleDataStream(stream) {}
|
||||||
|
setDatasource(datasource) {}
|
||||||
|
getAdditionalMenuItems() {}
|
||||||
|
explore() {}
|
||||||
|
addQuery(target) {}
|
||||||
|
removeQuery(target) {}
|
||||||
|
moveQuery(target, direction) {}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ module.exports = {
|
|||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{ from: '**/plugin.json' },
|
{ from: '**/plugin.json' },
|
||||||
{ from: '**/*.html' },
|
{ from: '**/*.html' },
|
||||||
|
{ from: '**/*.md' },
|
||||||
{ from: 'dashboards/*' },
|
{ from: 'dashboards/*' },
|
||||||
{ from: '../README.md' },
|
{ from: '../README.md' },
|
||||||
{ from: '**/img/*' },
|
{ from: '**/img/*' },
|
||||||
|
|||||||
30
yarn.lock
30
yarn.lock
@@ -937,22 +937,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||||
|
|
||||||
"@grafana/data@6.7.0", "@grafana/data@^6.7.0":
|
"@grafana/data@6.7.3", "@grafana/data@^6.7.3":
|
||||||
version "6.7.0"
|
version "6.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.7.0.tgz#c413ea0f4ec623a3a5e6a03eb14ba9209a2983fc"
|
resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.7.3.tgz#e9e1bade8c2d35c131df3f3e4d652e5fd6140c23"
|
||||||
integrity sha512-qhEtiLGNVvFqazjihkUiNRVQzhgxXNeQEIk4cLCxDaNPcocrIWtsXcTpsKCABzseKfRuH4gm+mPlk11FF7QnqQ==
|
integrity sha512-5B8bNfSYFVw49It2/nTuw3NBM8jO/BM4bRT5wqVBbKQDOt94GsUTcsI0oVKl/VCQAwT+rlZdaF5/XTme4DiPhg==
|
||||||
dependencies:
|
dependencies:
|
||||||
apache-arrow "0.15.1"
|
apache-arrow "0.15.1"
|
||||||
lodash "4.17.15"
|
lodash "4.17.15"
|
||||||
rxjs "6.5.4"
|
rxjs "6.5.4"
|
||||||
|
|
||||||
"@grafana/runtime@^6.7.0":
|
"@grafana/runtime@^6.7.3":
|
||||||
version "6.7.0"
|
version "6.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-6.7.0.tgz#5b6f6233b5186fcfe2aaac8911b6944d2665366f"
|
resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-6.7.3.tgz#998b35aaf2ab618bbf19358dde15b455d9227b20"
|
||||||
integrity sha512-lE1spLKqalIGysqzIJdknSwhUORciYxIWaTNxu9InVnuH6nlw8sTY9pQ49C5sffzid8DLP4ihH4B3KUVlB9c9A==
|
integrity sha512-6FXusFDrblFAJGAhCscXJxfqMqqBX4A0xHHpaD/rwNTqm6KOFxivQgd2Bf6Cr9W6ws5dNtHPaOC77xsDQIpIMg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@grafana/data" "6.7.0"
|
"@grafana/data" "6.7.3"
|
||||||
"@grafana/ui" "6.7.0"
|
"@grafana/ui" "6.7.3"
|
||||||
systemjs "0.20.19"
|
systemjs "0.20.19"
|
||||||
systemjs-plugin-css "0.1.37"
|
systemjs-plugin-css "0.1.37"
|
||||||
|
|
||||||
@@ -983,13 +983,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@grafana/tsconfig/-/tsconfig-1.0.0-rc1.tgz#d07ea16755a50cae21000113f30546b61647a200"
|
resolved "https://registry.yarnpkg.com/@grafana/tsconfig/-/tsconfig-1.0.0-rc1.tgz#d07ea16755a50cae21000113f30546b61647a200"
|
||||||
integrity sha512-nucKPGyzlSKYSiJk5RA8GzMdVWhdYNdF+Hh65AXxjD9PlY69JKr5wANj8bVdQboag6dgg0BFKqgKPyY+YtV4Iw==
|
integrity sha512-nucKPGyzlSKYSiJk5RA8GzMdVWhdYNdF+Hh65AXxjD9PlY69JKr5wANj8bVdQboag6dgg0BFKqgKPyY+YtV4Iw==
|
||||||
|
|
||||||
"@grafana/ui@6.7.0", "@grafana/ui@^6.7.0":
|
"@grafana/ui@6.7.3", "@grafana/ui@^6.7.3":
|
||||||
version "6.7.0"
|
version "6.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.7.0.tgz#22ab8c242814591ddcb6f4ba22a7587e2f4b96ab"
|
resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.7.3.tgz#509d3a252ea0942f3850821cb028669366656044"
|
||||||
integrity sha512-y1NrMYH24VTM5Tg8Lm3PMM99i9/7MaOJSDjzrWUaUxdljzsDpaN7T8qUvr9pLwP+N5yGvS7A7ZMZ943ihjDfgg==
|
integrity sha512-b4Ltk/GlBUa4d/ahCcxG4NAbIDjqLknEcynNaFI3kOj0DzVdlC+AGrv+5f/pZZ+bEr/ZKUdBJMeH0sZIOP+dOg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/core" "^10.0.27"
|
"@emotion/core" "^10.0.27"
|
||||||
"@grafana/data" "6.7.0"
|
"@grafana/data" "6.7.3"
|
||||||
"@grafana/slate-react" "0.22.9-grafana"
|
"@grafana/slate-react" "0.22.9-grafana"
|
||||||
"@grafana/tsconfig" "^1.0.0-rc1"
|
"@grafana/tsconfig" "^1.0.0-rc1"
|
||||||
"@torkelo/react-select" "3.0.8"
|
"@torkelo/react-select" "3.0.8"
|
||||||
|
|||||||
Reference in New Issue
Block a user