diff --git a/package.json b/package.json index 4a17694..87cf2dc 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "@babel/preset-env": "^7.7.7", "@babel/preset-react": "^7.6.3", "@emotion/core": "^10.0.27", - "@grafana/data": "^6.7.0", - "@grafana/runtime": "^6.7.0", - "@grafana/ui": "^6.7.0", + "@grafana/data": "^6.7.3", + "@grafana/runtime": "^6.7.3", + "@grafana/ui": "^6.7.3", "@popperjs/core": "^2.4.0", "@types/classnames": "^2.2.6", "@types/grafana": "github:CorpGlory/types-grafana", diff --git a/src/datasource-zabbix/components/VariableQueryEditor.tsx b/src/datasource-zabbix/components/VariableQueryEditor.tsx index a9b5616..e129443 100644 --- a/src/datasource-zabbix/components/VariableQueryEditor.tsx +++ b/src/datasource-zabbix/components/VariableQueryEditor.tsx @@ -1,10 +1,14 @@ import React, { PureComponent } from 'react'; import { parseLegacyVariableQuery } from '../utils'; -import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui'; +import { Select, Input } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types'; 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 { queryTypes: Array> = [ { value: VariableQueryTypes.Group, label: 'Group'}, diff --git a/src/datasource-zabbix/constants.ts b/src/datasource-zabbix/constants.ts index 79d2edc..a30aa4c 100644 --- a/src/datasource-zabbix/constants.ts +++ b/src/datasource-zabbix/constants.ts @@ -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 export const DATAPOINT_VALUE = 0; export const DATAPOINT_TS = 1; @@ -8,6 +12,7 @@ export const MODE_ITSERVICE = 1; export const MODE_TEXT = 2; export const MODE_ITEMID = 3; export const MODE_TRIGGERS = 4; +export const MODE_PROBLEMS = 5; // Triggers severity export const SEV_NOT_CLASSIFIED = 0; diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.ts similarity index 74% rename from src/datasource-zabbix/datasource.js rename to src/datasource-zabbix/datasource.ts index cd70e96..c163fb0 100644 --- a/src/datasource-zabbix/datasource.js +++ b/src/datasource-zabbix/datasource.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; 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 utils from './utils'; import * as migrations from './migrations'; @@ -7,16 +8,41 @@ import * as metricFunctions from './metricFunctions'; import * as c from './constants'; import dataProcessor from './dataProcessor'; import responseHandler from './responseHandler'; +import problemsHandler from './problemsHandler'; import { Zabbix } from './zabbix/zabbix'; import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore'; -import { VariableQueryTypes } from './types'; +import { VariableQueryTypes, ShowProblemTypes } from './types'; const DEFAULT_ZABBIX_VERSION = 3; 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 */ - constructor(instanceSettings, templateSrv, zabbixAlertingSrv) { + constructor(instanceSettings, private templateSrv, private zabbixAlertingSrv) { this.templateSrv = templateSrv; this.zabbixAlertingSrv = zabbixAlertingSrv; @@ -43,7 +69,7 @@ export class ZabbixDatasource { this.trendsRange = jsonData.trendsRange || '4d'; // Set cache update interval - var ttl = jsonData.cacheTTL || '1h'; + const ttl = jsonData.cacheTTL || '1h'; this.cacheTTL = utils.parseInterval(ttl); // Alerting options @@ -61,7 +87,7 @@ export class ZabbixDatasource { this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName; this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy; - let zabbixOptions = { + const zabbixOptions = { url: this.url, username: this.username, password: this.password, @@ -103,7 +129,7 @@ export class ZabbixDatasource { } // Create request for each target - let promises = _.map(options.targets, t => { + const promises = _.map(options.targets, t => { // Don't request for hidden targets if (t.hide) { return []; @@ -123,15 +149,15 @@ export class ZabbixDatasource { this.replaceTargetVariables(target, options); // Apply Time-related functions (timeShift(), etc) - let timeFunctions = bindFunctionDefs(target.functions, 'Time'); + const timeFunctions = bindFunctionDefs(target.functions, 'Time'); if (timeFunctions.length) { const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]); timeFrom = time_from; 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 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) { // Triggers query return this.queryTriggersData(target, timeRange); + } else if (target.queryType === c.MODE_PROBLEMS) { + // Problems query + return this.queryProblems(target, timeRange, options); } else { return []; } @@ -175,7 +204,7 @@ export class ZabbixDatasource { */ queryNumericData(target, timeRange, useTrends, options) { let queryStart, queryEnd; - let getItemOptions = { + const getItemOptions = { itemtype: 'num' }; return this.zabbix.getItemsFromTarget(target, getItemOptions) @@ -185,7 +214,7 @@ export class ZabbixDatasource { }).then(result => { queryEnd = new Date().getTime(); 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; }); @@ -212,18 +241,18 @@ export class ZabbixDatasource { getTrendValueType(target) { // Find trendValue() function and get specified trend value - var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); - var trendValueFunc = _.find(target.functions, func => { + const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name'); + const trendValueFunc = _.find(target.functions, func => { return _.includes(trendFunctions, func.def.name); }); return trendValueFunc ? trendValueFunc.params[0] : "avg"; } applyDataProcessingFunctions(timeseries_data, target) { - let transformFunctions = bindFunctionDefs(target.functions, 'Transform'); - let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); - let filterFunctions = bindFunctionDefs(target.functions, 'Filter'); - let aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); + const transformFunctions = bindFunctionDefs(target.functions, 'Transform'); + const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate'); + const filterFunctions = bindFunctionDefs(target.functions, 'Filter'); + const aliasFunctions = bindFunctionDefs(target.functions, 'Alias'); // Apply transformation functions timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => { @@ -241,8 +270,8 @@ export class ZabbixDatasource { let dp = _.map(timeseries_data, 'datapoints'); dp = utils.sequence(aggregationFunctions)(dp); - let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); - let lastAgg = _.findLast(target.functions, func => { + const aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name'); + const lastAgg = _.findLast(target.functions, func => { return _.includes(aggFuncNames, func.def.name); }); @@ -264,11 +293,11 @@ export class ZabbixDatasource { applyTimeShiftFunction(timeseries_data, target) { // Find timeShift() function and get specified interval - let timeShiftFunc = _.find(target.functions, (func) => { + const timeShiftFunc = _.find(target.functions, (func) => { return func.def.name === 'timeShift'; }); if (timeShiftFunc) { - let shift = timeShiftFunc.params[0]; + const shift = timeShiftFunc.params[0]; _.forEach(timeseries_data, (series) => { series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints); }); @@ -279,7 +308,7 @@ export class ZabbixDatasource { * Query target data for Text */ queryTextData(target, timeRange) { - let options = { + const options = { itemtype: 'text' }; return this.zabbix.getItemsFromTarget(target, options) @@ -332,14 +361,14 @@ export class ZabbixDatasource { } queryTriggersData(target, timeRange) { - let [timeFrom, timeTo] = timeRange; + const [timeFrom, timeTo] = timeRange; return this.zabbix.getHostsFromTarget(target) .then(results => { - let [hosts, apps] = results; + const [hosts, apps] = results; if (hosts.length) { - let hostids = _.map(hosts, 'hostid'); - let appids = _.map(apps, 'applicationid'); - let options = { + const hostids = _.map(hosts, 'hostid'); + const appids = _.map(apps, 'applicationid'); + const options = { minSeverity: target.triggers.minSeverity, acknowledged: target.triggers.acknowledged, 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. */ @@ -480,16 +577,16 @@ export class ZabbixDatasource { const timeRange = options.range || options.rangeRaw; const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000); const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000); - var annotation = options.annotation; - var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS; + const annotation = options.annotation; + const showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS; // Show all triggers - let triggersOptions = { + const triggersOptions = { showTriggers: c.SHOW_ALL_TRIGGERS, 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.application, {}), triggersOptions); @@ -497,7 +594,7 @@ export class ZabbixDatasource { return getTriggers.then(triggers => { // Filter triggers by description - let triggerName = this.replaceTemplateVars(annotation.trigger, {}); + const triggerName = this.replaceTemplateVars(annotation.trigger, {}); if (utils.isRegex(triggerName)) { triggers = _.filter(triggers, trigger => { return utils.buildRegex(triggerName).test(trigger.description); @@ -513,11 +610,11 @@ export class ZabbixDatasource { return Number(trigger.priority) >= Number(annotation.minseverity); }); - var objectids = _.map(triggers, 'triggerid'); + const objectids = _.map(triggers, 'triggerid'); return this.zabbix .getEvents(objectids, timeFrom, timeTo, showOkEvents) .then(events => { - var indexedTriggers = _.keyBy(triggers, 'triggerid'); + const indexedTriggers = _.keyBy(triggers, 'triggerid'); // Hide acknowledged events if option enabled if (annotation.hideAcknowledged) { @@ -533,10 +630,10 @@ export class ZabbixDatasource { } // 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); - let eventName = event.name || indexedTriggers[event.objectid].description; + const formattedAcknowledges = utils.formatAcknowledges(event.acknowledges); + const eventName = event.name || indexedTriggers[event.objectid].description; return { annotation: annotation, time: event.clock * 1000, @@ -555,8 +652,8 @@ export class ZabbixDatasource { * or empty object if no related triggers are finded. */ alertQuery(options) { - let enabled_targets = filterEnabledTargets(options.targets); - let getPanelItems = _.map(enabled_targets, t => { + const enabled_targets = filterEnabledTargets(options.targets); + const getPanelItems = _.map(enabled_targets, t => { let target = _.cloneDeep(t); target = migrations.migrate(target); this.replaceTargetVariables(target, options); @@ -565,8 +662,8 @@ export class ZabbixDatasource { return Promise.all(getPanelItems) .then(results => { - let items = _.flatten(results); - let itemids = _.map(items, 'itemid'); + const items = _.flatten(results); + const itemids = _.map(items, 'itemid'); if (itemids.length === 0) { return []; @@ -584,12 +681,12 @@ export class ZabbixDatasource { let state = 'ok'; - let firedTriggers = _.filter(triggers, {value: '1'}); + const firedTriggers = _.filter(triggers, {value: '1'}); if (firedTriggers.length) { state = 'alerting'; } - let thresholds = _.map(triggers, trigger => { + const thresholds = _.map(triggers, trigger => { return getTriggerThreshold(trigger.expression); }); @@ -603,7 +700,7 @@ export class ZabbixDatasource { // Replace template variables replaceTargetVariables(target, options) { - let parts = ['group', 'host', 'application', 'item']; + const parts = ['group', 'host', 'application', 'item']; _.forEach(parts, p => { if (target[p] && target[p].filter) { target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars); @@ -623,10 +720,10 @@ export class ZabbixDatasource { } isUseTrends(timeRange) { - let [timeFrom, timeTo] = timeRange; - let useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); - let useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000); - let useTrends = this.trends && ( + const [timeFrom, timeTo] = timeRange; + const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000); + const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000); + const useTrends = this.trends && ( (timeFrom < useTrendsFrom) || (timeTo - timeFrom > useTrendsRange) ); @@ -635,20 +732,20 @@ export class ZabbixDatasource { } function bindFunctionDefs(functionDefs, category) { - var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); - var aggFuncDefs = _.filter(functionDefs, function(func) { + const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name'); + const aggFuncDefs = _.filter(functionDefs, func => { return _.includes(aggregationFunctions, func.def.name); }); - return _.map(aggFuncDefs, function(func) { - var funcInstance = metricFunctions.createFuncInstance(func.def, func.params); + return _.map(aggFuncDefs, func => { + const funcInstance = metricFunctions.createFuncInstance(func.def, func.params); return funcInstance.bindFunction(dataProcessor.metricFunctions); }); } function getConsolidateBy(target) { let consolidateBy; - let funcDef = _.find(target.functions, func => { + const funcDef = _.find(target.functions, func => { return func.def.name === 'consolidateBy'; }); if (funcDef && funcDef.params && funcDef.params.length) { @@ -658,8 +755,8 @@ function getConsolidateBy(target) { } function downsampleSeries(timeseries_data, options) { - let defaultAgg = dataProcessor.aggregationFunctions['avg']; - let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg; + const defaultAgg = dataProcessor.aggregationFunctions['avg']; + const consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg; return _.map(timeseries_data, timeseries => { if (timeseries.datapoints.length > options.maxDataPoints) { timeseries.datapoints = dataProcessor @@ -691,7 +788,7 @@ export function zabbixTemplateFormat(value) { return utils.escapeRegex(value); } - var escapedValues = _.map(value, utils.escapeRegex); + const escapedValues = _.map(value, utils.escapeRegex); return '(' + escapedValues.join('|') + ')'; } @@ -711,7 +808,7 @@ function zabbixItemIdsTemplateFormat(value) { * /$variable/ -> /a|b|c/ -> /a|b|c/ */ 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)) { replacedTarget = '/^' + replacedTarget + '$/'; } @@ -725,8 +822,8 @@ function filterEnabledTargets(targets) { } function getTriggerThreshold(expression) { - let thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/; - let finded_thresholds = expression.match(thresholdPattern); + const thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/; + const finded_thresholds = expression.match(thresholdPattern); if (finded_thresholds && finded_thresholds.length >= 2) { let threshold = finded_thresholds[1]; threshold = Number(threshold); @@ -735,7 +832,3 @@ function getTriggerThreshold(expression) { return null; } } - -// Fix for backward compatibility with lodash 2.4 -if (!_.includes) {_.includes = _.contains;} -if (!_.keyBy) {_.keyBy = _.indexBy;} diff --git a/src/datasource-zabbix/metricFunctions.js b/src/datasource-zabbix/metricFunctions.ts similarity index 92% rename from src/datasource-zabbix/metricFunctions.js rename to src/datasource-zabbix/metricFunctions.ts index 150e03a..fdad893 100644 --- a/src/datasource-zabbix/metricFunctions.js +++ b/src/datasource-zabbix/metricFunctions.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; -import $ from 'jquery'; +import { isNumeric } from './utils'; -var index = []; -var categories = { +const index = []; +const categories = { Transform: [], Aggregate: [], Filter: [], @@ -298,11 +298,15 @@ addFuncDef({ defaultParams: ['avg'], }); -_.each(categories, function(funcList, catName) { +_.each(categories, (funcList, catName) => { categories[catName] = _.sortBy(funcList, 'name'); }); class FuncInstance { + def: any; + params: any; + text: string; + constructor(funcDef, params) { this.def = funcDef; @@ -318,13 +322,13 @@ class FuncInstance { } bindFunction(metricFunctions) { - var func = metricFunctions[this.def.name]; + const func = metricFunctions[this.def.name]; if (func) { // Bind function arguments - var bindedFunc = func; - var param; - for (var i = 0; i < this.params.length; i++) { + let bindedFunc = func; + let param; + for (let i = 0; i < this.params.length; i++) { param = this.params[i]; // Convert numeric params @@ -341,23 +345,21 @@ class FuncInstance { } render(metricExp) { - var str = this.def.name + '('; - var parameters = _.map(this.params, function(value, index) { - - var paramType = this.def.params[index].type; + const str = this.def.name + '('; + const parameters = _.map(this.params, (value, index) => { + const paramType = this.def.params[index].type; if (paramType === 'int' || paramType === 'float' || paramType === 'value_or_series' || paramType === 'boolean') { return value; - } - else if (paramType === 'int_or_interval' && $.isNumeric(value)) { + } else if (paramType === 'int_or_interval' && isNumeric(value)) { return value; } return "'" + value + "'"; - }, this); + }); if (metricExp) { parameters.unshift(metricExp); @@ -378,16 +380,15 @@ class FuncInstance { // handle optional parameters // if string contains ',' and next param is optional, split and update both if (this._hasMultipleParamsInString(strValue, index)) { - _.each(strValue.split(','), function(partVal, idx) { + _.each(strValue.split(','), (partVal, idx) => { this.updateParam(partVal.trim(), idx); - }, this); + }); return; } if (strValue === '' && this.def.params[index].optional) { this.params.splice(index, 1); - } - else { + }else { this.params[index] = strValue; } @@ -400,7 +401,7 @@ class FuncInstance { return; } - var text = this.def.name + '('; + let text = this.def.name + '('; text += this.params.join(', '); text += ')'; this.text = text; diff --git a/src/datasource-zabbix/partials/query.editor.html b/src/datasource-zabbix/partials/query.editor.html index c713b74..fe26b4b 100644 --- a/src/datasource-zabbix/partials/query.editor.html +++ b/src/datasource-zabbix/partials/query.editor.html @@ -12,7 +12,7 @@
- +
@@ -54,7 +54,7 @@
-
+
@@ -71,8 +71,8 @@ }">
-
- +
+
+
+ + +
+
-
+
@@ -109,8 +124,8 @@
-
- +
+
-
- -
+
+ + +
+ +
+ + +
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+ +
- -
-
- - -
- -
-
- - + +
+ +
+ +
-
- - + + + +
+
@@ -215,23 +251,82 @@
- -
- -
- - + + + +
+
+ + +
+
+ +
- - -
-
+
+ + +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ + + + +
+ + +
+
diff --git a/src/datasource-zabbix/problemsHandler.ts b/src/datasource-zabbix/problemsHandler.ts new file mode 100644 index 0000000..6ed4c72 --- /dev/null +++ b/src/datasource-zabbix/problemsHandler.ts @@ -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 = { + 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; diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js index c6b8002..eecc31a 100644 --- a/src/datasource-zabbix/query.controller.js +++ b/src/datasource-zabbix/query.controller.js @@ -4,6 +4,51 @@ import * as c from './constants'; import * as utils from './utils'; import * as metricFunctions from './metricFunctions'; 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 { @@ -21,7 +66,8 @@ export class ZabbixQueryController extends QueryCtrl { {value: 'text', text: 'Text', queryType: c.MODE_TEXT}, {value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE}, {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 = { @@ -29,7 +75,8 @@ export class ZabbixQueryController extends QueryCtrl { TEXT: c.MODE_TEXT, ITSERVICE: c.MODE_ITSERVICE, ITEMID: c.MODE_ITEMID, - TRIGGERS: c.MODE_TRIGGERS + TRIGGERS: c.MODE_TRIGGERS, + PROBLEMS: c.MODE_PROBLEMS, }; this.slaPropertyList = [ @@ -46,6 +93,30 @@ export class ZabbixQueryController extends QueryCtrl { {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.triggerSeverity = c.TRIGGER_SEVERITY; @@ -56,6 +127,7 @@ export class ZabbixQueryController extends QueryCtrl { this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList'); this.getItemNames = _.bind(this.getMetricNames, this, 'itemList'); this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList'); + this.getProxyNames = _.bind(this.getMetricNames, this, 'proxyList'); this.getVariables = _.bind(this.getTemplateVariables, this); // Update metric suggestion when template variable was changed @@ -80,40 +152,32 @@ export class ZabbixQueryController extends QueryCtrl { _.defaults(this, scopeDefaults); // Load default values - var targetDefaults = { - 'queryType': c.MODE_METRICS, - 'group': { 'filter': "" }, - 'host': { 'filter': "" }, - 'application': { 'filter': "" }, - 'item': { 'filter': "" }, - 'functions': [], - 'triggers': { - 'count': true, - 'minSeverity': 3, - 'acknowledged': 2 - }, - 'options': { - 'showDisabledItems': false, - 'skipEmptyValues': false - }, - 'table': { - 'skipEmptyValues': false - } - }; - _.defaults(target, targetDefaults); + const targetDefaults = getTargetDefaults(); + _.defaultsDeep(target, targetDefaults); + + if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) { + target.queryType = c.MODE_PROBLEMS; + } // Create function instances from saved JSON target.functions = _.map(target.functions, function(func) { 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 || target.queryType === c.MODE_TEXT || - target.queryType === c.MODE_TRIGGERS) { + target.queryType === c.MODE_TRIGGERS || + target.queryType === c.MODE_PROBLEMS) { this.initFilters(); - } - else if (target.queryType === c.MODE_ITSERVICE) { - _.defaults(target, {slaProperty: {name: "SLA", property: "sla"}}); + } else if (target.queryType === c.MODE_ITSERVICE) { this.suggestITServices(); } }; @@ -125,12 +189,18 @@ export class ZabbixQueryController extends QueryCtrl { initFilters() { let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType}); itemtype = itemtype ? itemtype.value : null; - return Promise.all([ + const promises = [ this.suggestGroups(), this.suggestHosts(), 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 @@ -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) { return utils.isRegex(str); } @@ -302,19 +381,42 @@ export class ZabbixQueryController extends QueryCtrl { } renderQueryOptionsText() { - var optionsMap = { + const metricOptionsMap = { 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) => { - if (value) { + if (value && optionsMap[key]) { if (value === true) { // Show only option name (if enabled) for boolean options options.push(optionsMap[key]); } else { // 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) { this.target.queryType = mode; + this.queryOptionsText = this.renderQueryOptionsText(); this.init(); this.targetChanged(); } diff --git a/src/datasource-zabbix/query_help.md b/src/datasource-zabbix/query_help.md index bb6c526..0b0fd31 100644 --- a/src/datasource-zabbix/query_help.md +++ b/src/datasource-zabbix/query_help.md @@ -29,3 +29,7 @@ This mode is suitable for rendering charts in grafana by passing itemids as url ##### Triggers 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) diff --git a/src/datasource-zabbix/responseHandler.ts b/src/datasource-zabbix/responseHandler.ts index ccb6576..cb46c65 100644 --- a/src/datasource-zabbix/responseHandler.ts +++ b/src/datasource-zabbix/responseHandler.ts @@ -258,6 +258,3 @@ export default { handleTriggersResponse, sortTimeseries }; - -// Fix for backward compatibility with lodash 2.4 -if (!_.uniqBy) {_.uniqBy = _.uniq;} diff --git a/src/datasource-zabbix/timeseries.js b/src/datasource-zabbix/timeseries.ts similarity index 87% rename from src/datasource-zabbix/timeseries.js rename to src/datasource-zabbix/timeseries.ts index 109fa41..c40ee91 100644 --- a/src/datasource-zabbix/timeseries.js +++ b/src/datasource-zabbix/timeseries.ts @@ -20,35 +20,30 @@ const POINT_TIMESTAMP = 1; * Downsample time series by using given function (avg, min, max). */ function downsample(datapoints, time_to, ms_interval, func) { - var downsampledSeries = []; - var timeWindow = { + const downsampledSeries = []; + const timeWindow = { from: time_to * 1000 - ms_interval, to: time_to * 1000 }; - var points_sum = 0; - var points_num = 0; - var value_avg = 0; - var frame = []; + let points_sum = 0; + let points_num = 0; + let value_avg = 0; + 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) { points_sum += datapoints[i][0]; points_num++; frame.push(datapoints[i][0]); - } - else { + } else { value_avg = points_num ? points_sum / points_num : 0; if (func === "max") { downsampledSeries.push([_.max(frame), timeWindow.to]); - } - else if (func === "min") { + } else if (func === "min") { downsampledSeries.push([_.min(frame), timeWindow.to]); - } - - // avg by default - else { + } else { downsampledSeries.push([value_avg, timeWindow.to]); } @@ -72,25 +67,25 @@ function downsample(datapoints, time_to, ms_interval, func) { * datapoints: [[, ], ...] */ function groupBy(datapoints, interval, groupByCallback) { - var ms_interval = utils.parseInterval(interval); + const ms_interval = utils.parseInterval(interval); // Calculate frame timestamps - var frames = _.groupBy(datapoints, function (point) { + const frames = _.groupBy(datapoints, point => { // Calculate time for group of points return Math.floor(point[1] / ms_interval) * ms_interval; }); // frame: { '': [[, ], ...] } // return [{ '': }, { '': }, ...] - var grouped = _.mapValues(frames, function (frame) { - var points = _.map(frame, function (point) { + const grouped = _.mapValues(frames, frame => { + const points = _.map(frame, point => { return point[0]; }); return groupByCallback(points); }); // Convert points to Grafana format - return sortByTime(_.map(grouped, function (value, timestamp) { + return sortByTime(_.map(grouped, (value, timestamp) => { return [Number(value), Number(timestamp)]; })); } @@ -104,15 +99,15 @@ export function groupBy_perf(datapoints, interval, groupByCallback) { return groupByRange(datapoints, groupByCallback); } - let ms_interval = utils.parseInterval(interval); - let grouped_series = []; + const ms_interval = utils.parseInterval(interval); + const grouped_series = []; let frame_values = []; let frame_value; let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0; let point_frame_ts = frame_ts; let point; - for (let i=0; i < datapoints.length; i++) { + for (let i = 0; i < datapoints.length; i++) { point = datapoints[i]; point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval); if (point_frame_ts === frame_ts) { @@ -142,7 +137,7 @@ export function groupByRange(datapoints, groupByCallback) { const frame_start = datapoints[0][POINT_TIMESTAMP]; const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP]; let point; - for (let i=0; i < datapoints.length; i++) { + for (let i = 0; i < datapoints.length; i++) { point = datapoints[i]; frame_values.push(point[POINT_VALUE]); } @@ -157,30 +152,30 @@ export function groupByRange(datapoints, groupByCallback) { function sumSeries(timeseries) { // 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]; })); new_timestamps = _.sortBy(new_timestamps); - var interpolated_timeseries = _.map(timeseries, function (series) { + const interpolated_timeseries = _.map(timeseries, series => { series = fillZeroes(series, new_timestamps); - var timestamps = _.map(series, function (point) { + const timestamps = _.map(series, point => { 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]; }); - var new_series = series.concat(new_points); + const new_series = series.concat(new_points); return sortByTime(new_series); }); _.each(interpolated_timeseries, interpolateSeries); - var new_timeseries = []; - var sum; - for (var i = new_timestamps.length - 1; i >= 0; i--) { + const new_timeseries = []; + let sum; + for (let i = new_timestamps.length - 1; i >= 0; i--) { 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]; } new_timeseries.push([sum, new_timestamps[i]]); @@ -225,9 +220,9 @@ function offset(datapoints, delta) { * @param {*} datapoints */ function delta(datapoints) { - let newSeries = []; + const newSeries = []; 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]; newSeries.push([deltaValue, datapoints[i][1]]); } @@ -239,7 +234,7 @@ function delta(datapoints) { * @param {*} datapoints */ function rate(datapoints) { - let newSeries = []; + const newSeries = []; let point, point_prev; let valueDelta = 0; let timeDelta = 0; @@ -261,7 +256,7 @@ function rate(datapoints) { } function simpleMovingAverage(datapoints, n) { - let sma = []; + const sma = []; let w_sum; let w_avg = null; let w_count = 0; @@ -352,7 +347,7 @@ function expMovingAverage(datapoints, n) { } function PERCENTILE(n, values) { - var sorted = _.sortBy(values); + const sorted = _.sortBy(values); return sorted[Math.floor(sorted.length * n / 100)]; } @@ -361,7 +356,7 @@ function COUNT(values) { } function SUM(values) { - var sum = null; + let sum = null; for (let i = 0; i < values.length; i++) { if (values[i] !== null) { sum += values[i]; @@ -371,7 +366,7 @@ function SUM(values) { } function AVERAGE(values) { - let values_non_null = getNonNullValues(values); + const values_non_null = getNonNullValues(values); if (values_non_null.length === 0) { return null; } @@ -379,7 +374,7 @@ function AVERAGE(values) { } function getNonNullValues(values) { - let values_non_null = []; + const values_non_null = []; for (let i = 0; i < values.length; i++) { if (values[i] !== null) { values_non_null.push(values[i]); @@ -397,7 +392,7 @@ function MAX(values) { } function MEDIAN(values) { - var sorted = _.sortBy(values); + const sorted = _.sortBy(values); return sorted[Math.floor(sorted.length / 2)]; } @@ -418,7 +413,7 @@ function getPointTimeFrame(timestamp, ms_interval) { } function sortByTime(series) { - return _.sortBy(series, function (point) { + return _.sortBy(series, point => { return point[1]; }); } @@ -432,8 +427,8 @@ function sortByTime(series) { * @param {*} timestamps */ function fillZeroes(series, timestamps) { - let prepend = []; - let append = []; + const prepend = []; + const append = []; let new_point; for (let i = 0; i < timestamps.length; i++) { if (timestamps[i] < series[0][POINT_TIMESTAMP]) { @@ -451,10 +446,10 @@ function fillZeroes(series, timestamps) { * Interpolate series with gaps */ function interpolateSeries(series) { - var left, right; + let left, right; // Interpolate series - for (var i = series.length - 1; i >= 0; i--) { + for (let i = series.length - 1; i >= 0; i--) { if (!series[i][0]) { left = findNearestLeft(series, i); right = findNearestRight(series, i); @@ -479,7 +474,7 @@ function linearInterpolation(timestamp, left, right) { } 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) { return series[i]; } @@ -488,7 +483,7 @@ function findNearestRight(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) { return series[i]; } diff --git a/src/datasource-zabbix/types.ts b/src/datasource-zabbix/types.ts index 739e25f..ba2d45f 100644 --- a/src/datasource-zabbix/types.ts +++ b/src/datasource-zabbix/types.ts @@ -28,3 +28,9 @@ export enum VariableQueryTypes { Application = 'application', Item = 'item', } + +export enum ShowProblemTypes { + Problems = 'problems', + Recent = 'recent', + History = 'history', +} diff --git a/src/datasource-zabbix/utils.ts b/src/datasource-zabbix/utils.ts index 44bc5a5..5eb89af 100644 --- a/src/datasource-zabbix/utils.ts +++ b/src/datasource-zabbix/utils.ts @@ -222,10 +222,11 @@ export function escapeRegex(value) { 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 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) { @@ -344,7 +345,25 @@ export function getArrayDepth(a, level = 0) { return level + 1; } -// Fix for backward compatibility with lodash 2.4 -if (!_.includes) { - _.includes = (_ as any).contains; +/** + * Checks whether its argument represents a numeric value. + */ +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; } diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js index 792a477..3264a95 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js @@ -3,6 +3,7 @@ import kbn from 'grafana/app/core/utils/kbn'; import * as utils from '../../../utils'; import { ZabbixAPICore } from './zabbixAPICore'; 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. @@ -350,8 +351,10 @@ export class ZabbixAPIConnector { selectTags: 'extend' }; - if (showTriggers) { - params.filter.value = showTriggers; + if (showTriggers === ShowProblemTypes.Problems) { + params.filter.value = 1; + } else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) { + params.filter.value = [0, 1]; } if (maintenance) { diff --git a/src/panel-triggers/components/Problems/Problems.tsx b/src/panel-triggers/components/Problems/Problems.tsx index a83d818..c5bc3c5 100644 --- a/src/panel-triggers/components/Problems/Problems.tsx +++ b/src/panel-triggers/components/Problems/Problems.tsx @@ -183,14 +183,14 @@ function SeverityCell(props: RTCell, problemSeverityDesc: TriggerSev let severityDesc: TriggerSeverity; 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)); } color = severityDesc.color; // Mark acknowledged triggers with different color - if (markAckEvents && problem.lastEvent.acknowledged === "1") { + if (markAckEvents && problem.lastEvent?.acknowledged === "1") { color = ackEventColor; } diff --git a/src/panel-triggers/datasource-selector.directive.js b/src/panel-triggers/datasource-selector.directive.js deleted file mode 100644 index 0d55483..0000000 --- a/src/panel-triggers/datasource-selector.directive.js +++ /dev/null @@ -1,57 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; - -const template = ` - - -`; - -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(); - }); - } -} diff --git a/src/panel-triggers/migrations.ts b/src/panel-triggers/migrations.ts index f929160..8b83b32 100644 --- a/src/panel-triggers/migrations.ts +++ b/src/panel-triggers/migrations.ts @@ -1,9 +1,27 @@ import _ from 'lodash'; import { getNextRefIdChar } from './utils'; -import { getDefaultTarget } from './triggers_panel_ctrl'; +import { ShowProblemTypes } from '../datasource-zabbix/types'; // 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) { if (isEmptyPanel(panel)) { @@ -12,7 +30,7 @@ export function migratePanelSchema(panel) { } const schemaVersion = getSchemaVersion(panel); - panel.schemaVersion = CURRENT_SCHEMA_VERSION; + // panel.schemaVersion = CURRENT_SCHEMA_VERSION; if (schemaVersion < 2) { panel.datasources = [panel.datasource]; @@ -66,9 +84,62 @@ export function migratePanelSchema(panel) { 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; } +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) { return panel.schemaVersion || 1; } diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js index 2ea2c17..a6f7f15 100644 --- a/src/panel-triggers/module.js +++ b/src/panel-triggers/module.js @@ -11,9 +11,8 @@ * Licensed under the Apache License, Version 2.0 */ -import {TriggerPanelCtrl} from './triggers_panel_ctrl'; -import {loadPluginCss} from 'grafana/app/plugins/sdk'; -import './datasource-selector.directive'; +import { TriggerPanelCtrl } from './triggers_panel_ctrl'; +import { loadPluginCss } from 'grafana/app/plugins/sdk'; loadPluginCss({ dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', diff --git a/src/panel-triggers/options_tab.js b/src/panel-triggers/options_tab.js index acf7c50..c67521f 100644 --- a/src/panel-triggers/options_tab.js +++ b/src/panel-triggers/options_tab.js @@ -29,10 +29,13 @@ class TriggerPanelOptionsCtrl { 'unacknowledged', 'acknowledged' ]; - this.sortByFields = [ - { text: 'last change', value: 'lastchange' }, - { text: 'severity', value: 'priority' } + + this.sortingOptions = [ + { text: 'Default', value: 'default' }, + { text: 'Last change', value: 'lastchange' }, + { text: 'Severity', value: 'priority' }, ]; + this.showEventsFields = [ { text: 'All', value: [0,1] }, { text: 'OK', value: [0] }, diff --git a/src/panel-triggers/partials/options_tab.html b/src/panel-triggers/partials/options_tab.html index b7253f0..799c6e5 100644 --- a/src/panel-triggers/partials/options_tab.html +++ b/src/panel-triggers/partials/options_tab.html @@ -1,6 +1,6 @@
-
Show fields
+
Fields
+ on-change="ctrl.render()">
-
-
Options
- - -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- - -
-
-
View options
@@ -130,6 +83,16 @@ ng-change="ctrl.render()">
+
+ +
+ +
+
@@ -202,7 +165,7 @@
-
Triggers severity and colors
+
Problems severity and colors
@@ -224,7 +187,7 @@ label-class="width-0" label="Show" checked="trigger.show" - on-change="ctrl.refresh()"> + on-change="ctrl.reRenderProblems()">
@@ -246,7 +209,7 @@ label-class="width-0" label="Show" checked="ctrl.panel.markAckEvents" - on-change="ctrl.refresh()"> + on-change="ctrl.reRenderProblems()">
diff --git a/src/panel-triggers/partials/triggers_tab.html b/src/panel-triggers/partials/triggers_tab.html deleted file mode 100644 index e7df9fe..0000000 --- a/src/panel-triggers/partials/triggers_tab.html +++ /dev/null @@ -1,104 +0,0 @@ -
-
-
-
- -
-
- - -
-
-
-
- -
-
-
{{ target.datasource }}
-
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
-
-
diff --git a/src/panel-triggers/plugin.json b/src/panel-triggers/plugin.json index 6639999..ebd070a 100644 --- a/src/panel-triggers/plugin.json +++ b/src/panel-triggers/plugin.json @@ -3,9 +3,6 @@ "name": "Zabbix Problems", "id": "alexanderzobnin-zabbix-triggers-panel", - "dataFormats": [], - "skipDataQuery": true, - "info": { "author": { "name": "Alexander Zobnin", diff --git a/src/panel-triggers/specs/migrations.spec.ts b/src/panel-triggers/specs/migrations.spec.ts index a309907..10336ec 100644 --- a/src/panel-triggers/specs/migrations.spec.ts +++ b/src/panel-triggers/specs/migrations.spec.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import mocks from '../../test-setup/mocks'; import {TriggerPanelCtrl} from '../triggers_panel_ctrl'; -import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl'; -import {CURRENT_SCHEMA_VERSION} from '../migrations'; +import { DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS } from '../triggers_panel_ctrl'; +import { CURRENT_SCHEMA_VERSION } from '../migrations'; jest.mock('@grafana/runtime', () => { return { @@ -34,8 +34,9 @@ describe('Triggers Panel schema migration', () => { ageField: true, infoField: true, limit: 10, - showTriggers: 'all triggers', + showTriggers: 'unacknowledged', hideHostsInMaintenance: false, + hostsInMaintenance: false, sortTriggersBy: { text: 'last change', value: 'lastchange' }, showEvents: { text: 'Problems', value: '1' }, 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', () => { @@ -56,12 +57,20 @@ describe('Triggers Panel schema migration', () => { const expected = _.defaultsDeep({ schemaVersion: CURRENT_SCHEMA_VERSION, + datasource: 'zabbix', targets: [ { ...DEFAULT_TARGET, - datasource: 'zabbix', + queryType: 5, + showProblems: 'problems', + options: { + hostsInMaintenance: false, + acknowledged: 0, + sortProblems: 'default', + }, } ], + sortProblems: 'lastchange', ageField: true, statusField: false, severityField: false, @@ -79,27 +88,7 @@ describe('Triggers Panel schema migration', () => { const expected = _.defaultsDeep({ schemaVersion: CURRENT_SCHEMA_VERSION, - targets: [{ - ...DEFAULT_TARGET, - datasource: 'zabbix_default' - }] }, PANEL_DEFAULTS); 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); - }); }); diff --git a/src/panel-triggers/specs/panel_ctrl.spec.ts b/src/panel-triggers/specs/panel_ctrl.spec.ts index a52707d..01ac557 100644 --- a/src/panel-triggers/specs/panel_ctrl.spec.ts +++ b/src/panel-triggers/specs/panel_ctrl.spec.ts @@ -1,8 +1,6 @@ import _ from 'lodash'; -import mocks from '../../test-setup/mocks'; -import {TriggerPanelCtrl} from '../triggers_panel_ctrl'; -import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl'; -// import { create } from 'domain'; +import { TriggerPanelCtrl } from '../triggers_panel_ctrl'; +import { PANEL_DEFAULTS, DEFAULT_TARGET } from '../triggers_panel_ctrl'; let datasourceSrvMock, zabbixDSMock; @@ -14,85 +12,42 @@ jest.mock('@grafana/runtime', () => { describe('TriggerPanelCtrl', () => { let ctx: any = {}; - const timeoutMock = () => {}; - let createPanelCtrl; + let createPanelCtrl: () => any; beforeEach(() => { - ctx = {scope: {panel: PANEL_DEFAULTS}}; + ctx = { scope: { + panel: { + ...PANEL_DEFAULTS, + sortProblems: 'lastchange', + } + }}; + ctx.scope.panel.targets = [{ + ...DEFAULT_TARGET, + datasource: 'zabbix_default', + }]; + zabbixDSMock = { - replaceTemplateVars: () => {}, zabbix: { - getTriggers: jest.fn().mockReturnValue([generateTrigger("1"), generateTrigger("1")]), getExtendedEventData: jest.fn().mockResolvedValue([]), getEventAlerts: jest.fn().mockResolvedValue([]), } }; 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) }; - createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, {}, {}, {}, mocks.timeSrvMock); - - 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])); + const timeoutMock = (fn: () => any) => Promise.resolve(fn()); + createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock); ctx.panelCtrl = createPanelCtrl(); - }); - describe('When adding new panel', () => { - it('should suggest all zabbix data sources', () => { - ctx.scope.panel = {}; - const panelCtrl = createPanelCtrl(); - expect(panelCtrl.available_datasources).toEqual([ - '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' - ]); - }); + ctx.dataFramesReceived = generateDataFramesResponse([ + {id: "1", lastchange: "1510000010", priority: 5}, + {id: "2", lastchange: "1510000040", priority: 3}, + {id: "3", lastchange: "1510000020", priority: 4}, + {id: "4", lastchange: "1510000030", priority: 2}, + ]); }); describe('When refreshing panel', () => { @@ -112,8 +67,8 @@ describe('TriggerPanelCtrl', () => { }); it('should format triggers', (done) => { - ctx.panelCtrl.onRefresh().then(() => { - const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"}); + ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { + const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, {triggerid: "1"}); expect(formattedTrigger.host).toBe('backend01'); expect(formattedTrigger.hostTechName).toBe('backend01_tech'); expect(formattedTrigger.datasource).toBe('zabbix_default'); @@ -124,8 +79,8 @@ describe('TriggerPanelCtrl', () => { }); it('should sort triggers by time by default', (done) => { - ctx.panelCtrl.onRefresh().then(() => { - const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid'); + ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { + const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid'); expect(trigger_ids).toEqual([ '2', '4', '3', '1' ]); @@ -134,175 +89,119 @@ describe('TriggerPanelCtrl', () => { }); it('should sort triggers by severity', (done) => { - ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' }; - ctx.panelCtrl.onRefresh().then(() => { - const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid'); + ctx.panelCtrl.panel.sortProblems = 'priority'; + ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { + const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid'); expect(trigger_ids).toEqual([ '1', '3', '2', '4' ]); 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
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 = { - "triggerid": "13565", - "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", +const defaultProblem: any = { + "acknowledges": [], "comments": "It probably means that the systems requires\nmore physical memory.", - "url": "https://host.local/path", - "templateid": "0", "expression": "{13174}<50", "manual_close": "0", "correlation_mode": "0", - "correlation_tag": "", "recovery_mode": "0", "recovery_expression": "", "state": "0", "status": "0", - "flags": "0", "type": "0", "items": [] , "error": "" -}; - -const defaultEvent: any = { - "eventid": "11", - "acknowledges": [ + "correlation_mode": "0", + "correlation_tag": "", + "datasource": "zabbix_default", + "description": "Lack of free swap space on server", + "error": "", + "expression": "{13297}>20", + "flags": "0", + "groups": [ { - "acknowledgeid": "185", - "action": "0", - "alias": "api", - "clock": "1512382246", - "eventid": "11", - "message": "event ack", - "name": "api", - "surname": "user", - "userid": "3" + "groupid": "2", + "name": "Linux servers" + }, + { + "groupid": "9", + "name": "Backend" } ], - "clock": "1507229064", - "ns": "556202037", - "acknowledged": "1", - "value": "1", - "object": "0", - "source": "0", - "objectid": "1", + "hosts": [ + { + "host": "backend01_tech", + "hostid": "10111", + "maintenance_status": "1", + "name": "backend01", + "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 { - const trigger = _.cloneDeep(defaultTrigger); - trigger.triggerid = id.toString(); +function generateDataFramesResponse(problemDescs: any[] = [{id: 1}]): any { + const problems = problemDescs.map(problem => generateProblem(problem.id, problem.lastchange, problem.priority)); + + 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) { - trigger.priority = severity.toString(); + problem.priority = severity.toString(); } if (timestamp) { - trigger.lastchange = timestamp; + problem.lastchange = timestamp; } - return trigger; + return problem; } -function createTrigger(props): any { - let trigger = _.cloneDeep(defaultTrigger); - trigger = _.merge(trigger, props); - trigger.lastEvent.objectid = trigger.triggerid; - return trigger; -} - -function getTriggerById(id, ctx): any { - return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()}); +function getProblemById(id, ctx): any { + return _.find(ctx.panelCtrl.renderData, {triggerid: id.toString()}); } diff --git a/src/panel-triggers/triggers_panel_ctrl.js b/src/panel-triggers/triggers_panel_ctrl.js deleted file mode 100644 index ecee7a4..0000000 --- a/src/panel-triggers/triggers_panel_ctrl.js +++ /dev/null @@ -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', '
'); - } - - 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; - }); - } -} diff --git a/src/panel-triggers/triggers_panel_ctrl.ts b/src/panel-triggers/triggers_panel_ctrl.ts new file mode 100644 index 0000000..e62331a --- /dev/null +++ b/src/panel-triggers/triggers_panel_ctrl.ts @@ -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 { + 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', '
'); + } + + 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'; diff --git a/src/panel-triggers/triggers_tab.js b/src/panel-triggers/triggers_tab.js deleted file mode 100644 index 9b3032b..0000000 --- a/src/panel-triggers/triggers_tab.js +++ /dev/null @@ -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, - }; -} diff --git a/src/test-setup/jest-setup.js b/src/test-setup/jest-setup.js index 9e4b81c..9cae61e 100644 --- a/src/test-setup/jest-setup.js +++ b/src/test-setup/jest-setup.js @@ -2,7 +2,7 @@ /* globals global: false */ 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 // Required for loading module.js @@ -24,12 +24,19 @@ jest.mock('grafana/app/core/core_module', () => { }; }, {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', () => { return { QueryCtrl: null, loadPluginCss: () => {}, - PanelCtrl: mockPanelCtrl + PanelCtrl: mockPanelCtrl, + MetricsPanelCtrl: mockMetricsPanelCtrl, }; }, {virtual: true}); @@ -85,3 +92,7 @@ let dom = new JSDOM(''); global.window = dom.window; global.document = global.window.document; global.Node = window.Node; + +// Mock Canvas.getContext(), fixes +// Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package) +window.HTMLCanvasElement.prototype.getContext = () => {}; diff --git a/src/test-setup/panelStub.js b/src/test-setup/panelStub.js deleted file mode 100644 index 9c66080..0000000 --- a/src/test-setup/panelStub.js +++ /dev/null @@ -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() { - } -} diff --git a/src/test-setup/panelStub.ts b/src/test-setup/panelStub.ts new file mode 100644 index 0000000..667e96f --- /dev/null +++ b/src/test-setup/panelStub.ts @@ -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) {} +} diff --git a/webpack/webpack.base.conf.js b/webpack/webpack.base.conf.js index 9526fd9..841c39e 100644 --- a/webpack/webpack.base.conf.js +++ b/webpack/webpack.base.conf.js @@ -42,6 +42,7 @@ module.exports = { new CopyWebpackPlugin([ { from: '**/plugin.json' }, { from: '**/*.html' }, + { from: '**/*.md' }, { from: 'dashboards/*' }, { from: '../README.md' }, { from: '**/img/*' }, diff --git a/yarn.lock b/yarn.lock index cd56d25..5800def 100644 --- a/yarn.lock +++ b/yarn.lock @@ -937,22 +937,22 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@grafana/data@6.7.0", "@grafana/data@^6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.7.0.tgz#c413ea0f4ec623a3a5e6a03eb14ba9209a2983fc" - integrity sha512-qhEtiLGNVvFqazjihkUiNRVQzhgxXNeQEIk4cLCxDaNPcocrIWtsXcTpsKCABzseKfRuH4gm+mPlk11FF7QnqQ== +"@grafana/data@6.7.3", "@grafana/data@^6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.7.3.tgz#e9e1bade8c2d35c131df3f3e4d652e5fd6140c23" + integrity sha512-5B8bNfSYFVw49It2/nTuw3NBM8jO/BM4bRT5wqVBbKQDOt94GsUTcsI0oVKl/VCQAwT+rlZdaF5/XTme4DiPhg== dependencies: apache-arrow "0.15.1" lodash "4.17.15" rxjs "6.5.4" -"@grafana/runtime@^6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-6.7.0.tgz#5b6f6233b5186fcfe2aaac8911b6944d2665366f" - integrity sha512-lE1spLKqalIGysqzIJdknSwhUORciYxIWaTNxu9InVnuH6nlw8sTY9pQ49C5sffzid8DLP4ihH4B3KUVlB9c9A== +"@grafana/runtime@^6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-6.7.3.tgz#998b35aaf2ab618bbf19358dde15b455d9227b20" + integrity sha512-6FXusFDrblFAJGAhCscXJxfqMqqBX4A0xHHpaD/rwNTqm6KOFxivQgd2Bf6Cr9W6ws5dNtHPaOC77xsDQIpIMg== dependencies: - "@grafana/data" "6.7.0" - "@grafana/ui" "6.7.0" + "@grafana/data" "6.7.3" + "@grafana/ui" "6.7.3" systemjs "0.20.19" systemjs-plugin-css "0.1.37" @@ -983,13 +983,13 @@ resolved "https://registry.yarnpkg.com/@grafana/tsconfig/-/tsconfig-1.0.0-rc1.tgz#d07ea16755a50cae21000113f30546b61647a200" integrity sha512-nucKPGyzlSKYSiJk5RA8GzMdVWhdYNdF+Hh65AXxjD9PlY69JKr5wANj8bVdQboag6dgg0BFKqgKPyY+YtV4Iw== -"@grafana/ui@6.7.0", "@grafana/ui@^6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.7.0.tgz#22ab8c242814591ddcb6f4ba22a7587e2f4b96ab" - integrity sha512-y1NrMYH24VTM5Tg8Lm3PMM99i9/7MaOJSDjzrWUaUxdljzsDpaN7T8qUvr9pLwP+N5yGvS7A7ZMZ943ihjDfgg== +"@grafana/ui@6.7.3", "@grafana/ui@^6.7.3": + version "6.7.3" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.7.3.tgz#509d3a252ea0942f3850821cb028669366656044" + integrity sha512-b4Ltk/GlBUa4d/ahCcxG4NAbIDjqLknEcynNaFI3kOj0DzVdlC+AGrv+5f/pZZ+bEr/ZKUdBJMeH0sZIOP+dOg== dependencies: "@emotion/core" "^10.0.27" - "@grafana/data" "6.7.0" + "@grafana/data" "6.7.3" "@grafana/slate-react" "0.22.9-grafana" "@grafana/tsconfig" "^1.0.0-rc1" "@torkelo/react-select" "3.0.8"