diff --git a/.gitignore b/.gitignore
index dbb0ecd..a3019ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,8 +15,8 @@
docs/site/
# Tests
-dist/test/
-dist/test-setup/
+# dist/test/
+# dist/test-setup/
vendor/npm
src/vendor/npm
diff --git a/dist/test-setup/cssStub.js b/dist/test-setup/cssStub.js
new file mode 100644
index 0000000..f053ebf
--- /dev/null
+++ b/dist/test-setup/cssStub.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/dist/test-setup/jest-setup.js b/dist/test-setup/jest-setup.js
new file mode 100644
index 0000000..ff528b8
--- /dev/null
+++ b/dist/test-setup/jest-setup.js
@@ -0,0 +1,50 @@
+// JSHint options
+/* globals global: false */
+
+import {JSDOM} from 'jsdom';
+
+// Mock Grafana modules that are not available outside of the core project
+// Required for loading module.js
+jest.mock('angular', () => {
+ return {
+ module: function() {
+ return {
+ directive: function() {},
+ service: function() {},
+ factory: function() {}
+ };
+ }
+ };
+}, {virtual: true});
+
+jest.mock('app/plugins/sdk', () => {
+ return {
+ QueryCtrl: null
+ };
+}, {virtual: true});
+
+jest.mock('app/core/utils/datemath', () => {
+ const datemath = require('./modules/datemath');
+ return {
+ parse: datemath.parse,
+ parseDateMath: datemath.parseDateMath,
+ isValid: datemath.isValid
+ };
+}, {virtual: true});
+
+jest.mock('app/core/table_model', () => {
+ return {};
+}, {virtual: true});
+
+jest.mock('./css/query-editor.css!', () => {
+ return "";
+}, {virtual: true});
+
+jest.mock('jquery', () => 'module not found', {virtual: true});
+
+// Required for loading angularjs
+let dom = new JSDOM('
');
+// Setup jsdom
+global.window = dom.window;
+global.document = global.window.document;
+global.Node = window.Node;
diff --git a/dist/test-setup/modules/datemath.js b/dist/test-setup/modules/datemath.js
new file mode 100644
index 0000000..efbf0bf
--- /dev/null
+++ b/dist/test-setup/modules/datemath.js
@@ -0,0 +1,111 @@
+import _ from 'lodash';
+import moment from 'moment';
+
+var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
+
+export function parse(text, roundUp) {
+ if (!text) { return undefined; }
+ if (moment.isMoment(text)) { return text; }
+ if (_.isDate(text)) { return moment(text); }
+
+ var time;
+ var mathString = '';
+ var index;
+ var parseString;
+
+ if (text.substring(0, 3) === 'now') {
+ time = moment();
+ mathString = text.substring('now'.length);
+ } else {
+ index = text.indexOf('||');
+ if (index === -1) {
+ parseString = text;
+ mathString = ''; // nothing else
+ } else {
+ parseString = text.substring(0, index);
+ mathString = text.substring(index + 2);
+ }
+ // We're going to just require ISO8601 timestamps, k?
+ time = moment(parseString, moment.ISO_8601);
+ }
+
+ if (!mathString.length) {
+ return time;
+ }
+
+ return parseDateMath(mathString, time, roundUp);
+}
+
+export function isValid(text) {
+ var date = parse(text);
+ if (!date) {
+ return false;
+ }
+
+ if (moment.isMoment(date)) {
+ return date.isValid();
+ }
+
+ return false;
+}
+
+export function parseDateMath(mathString, time, roundUp) {
+ var dateTime = time;
+ var i = 0;
+ var len = mathString.length;
+
+ while (i < len) {
+ var c = mathString.charAt(i++);
+ var type;
+ var num;
+ var unit;
+
+ if (c === '/') {
+ type = 0;
+ } else if (c === '+') {
+ type = 1;
+ } else if (c === '-') {
+ type = 2;
+ } else {
+ return undefined;
+ }
+
+ if (isNaN(mathString.charAt(i))) {
+ num = 1;
+ } else if (mathString.length === 2) {
+ num = mathString.charAt(i);
+ } else {
+ var numFrom = i;
+ while (!isNaN(mathString.charAt(i))) {
+ i++;
+ if (i > 10) { return undefined; }
+ }
+ num = parseInt(mathString.substring(numFrom, i), 10);
+ }
+
+ if (type === 0) {
+ // rounding is only allowed on whole, single, units (eg M or 1M, not 0.5M or 2M)
+ if (num !== 1) {
+ return undefined;
+ }
+ }
+ unit = mathString.charAt(i++);
+
+ if (!_.includes(units, unit)) {
+ return undefined;
+ } else {
+ if (type === 0) {
+ if (roundUp) {
+ dateTime.endOf(unit);
+ } else {
+ dateTime.startOf(unit);
+ }
+ } else if (type === 1) {
+ dateTime.add(num, unit);
+ } else if (type === 2) {
+ dateTime.subtract(num, unit);
+ }
+ }
+ }
+ return dateTime;
+}
diff --git a/dist/test/components/config.js b/dist/test/components/config.js
new file mode 100644
index 0000000..ad0987c
--- /dev/null
+++ b/dist/test/components/config.js
@@ -0,0 +1,13 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var ZabbixAppConfigCtrl = exports.ZabbixAppConfigCtrl = function ZabbixAppConfigCtrl() {
+ _classCallCheck(this, ZabbixAppConfigCtrl);
+};
+
+ZabbixAppConfigCtrl.templateUrl = 'components/config.html';
diff --git a/dist/test/datasource-zabbix/add-metric-function.directive.js b/dist/test/datasource-zabbix/add-metric-function.directive.js
new file mode 100644
index 0000000..26b55cc
--- /dev/null
+++ b/dist/test/datasource-zabbix/add-metric-function.directive.js
@@ -0,0 +1,116 @@
+'use strict';
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _jquery = require('jquery');
+
+var _jquery2 = _interopRequireDefault(_jquery);
+
+var _metricFunctions = require('./metricFunctions');
+
+var metricFunctions = _interopRequireWildcard(_metricFunctions);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/** @ngInject */
+_angular2.default.module('grafana.directives').directive('addMetricFunction', function ($compile) {
+ var inputTemplate = ' ';
+
+ var buttonTemplate = '' + ' ';
+
+ return {
+ link: function link($scope, elem) {
+ var categories = metricFunctions.getCategories();
+ var allFunctions = getAllFunctionNames(categories);
+
+ $scope.functionMenu = createFunctionDropDownMenu(categories);
+
+ var $input = (0, _jquery2.default)(inputTemplate);
+ var $button = (0, _jquery2.default)(buttonTemplate);
+ $input.appendTo(elem);
+ $button.appendTo(elem);
+
+ $input.attr('data-provide', 'typeahead');
+ $input.typeahead({
+ source: allFunctions,
+ minLength: 1,
+ items: 10,
+ updater: function updater(value) {
+ var funcDef = metricFunctions.getFuncDef(value);
+ if (!funcDef) {
+ // try find close match
+ value = value.toLowerCase();
+ funcDef = _lodash2.default.find(allFunctions, function (funcName) {
+ return funcName.toLowerCase().indexOf(value) === 0;
+ });
+
+ if (!funcDef) {
+ return;
+ }
+ }
+
+ $scope.$apply(function () {
+ $scope.addFunction(funcDef);
+ });
+
+ $input.trigger('blur');
+ return '';
+ }
+ });
+
+ $button.click(function () {
+ $button.hide();
+ $input.show();
+ $input.focus();
+ });
+
+ $input.keyup(function () {
+ elem.toggleClass('open', $input.val() === '');
+ });
+
+ $input.blur(function () {
+ // clicking the function dropdown menu wont
+ // work if you remove class at once
+ setTimeout(function () {
+ $input.val('');
+ $input.hide();
+ $button.show();
+ elem.removeClass('open');
+ }, 200);
+ });
+
+ $compile(elem.contents())($scope);
+ }
+ };
+});
+
+function getAllFunctionNames(categories) {
+ return _lodash2.default.reduce(categories, function (list, category) {
+ _lodash2.default.each(category, function (func) {
+ list.push(func.name);
+ });
+ return list;
+ }, []);
+}
+
+function createFunctionDropDownMenu(categories) {
+ return _lodash2.default.map(categories, function (list, category) {
+ return {
+ text: category,
+ submenu: _lodash2.default.map(list, function (value) {
+ return {
+ text: value.name,
+ click: "ctrl.addFunction('" + value.name + "')"
+ };
+ })
+ };
+ });
+}
diff --git a/dist/test/datasource-zabbix/benchmarks/timeseries_bench.js b/dist/test/datasource-zabbix/benchmarks/timeseries_bench.js
new file mode 100644
index 0000000..8e84417
--- /dev/null
+++ b/dist/test/datasource-zabbix/benchmarks/timeseries_bench.js
@@ -0,0 +1,72 @@
+'use strict';
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _timeseries = require('../timeseries');
+
+var _timeseries2 = _interopRequireDefault(_timeseries);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+var datapoints = [[10.7104, 1498409636085], [10.578, 1498409651011], [10.5985, 1498409666628], [10.6877, 1498409681525], [10.5495, 1498409696586], [10.5981, 1498409711009], [10.5076, 1498409726949], [11.4807, 1498409741853], [11.6165, 1498409756165], [11.8575, 1498409771018], [11.9936, 1498409786056], [10.7566, 1498409801942], [10.7484, 1498409816010], [10.6038, 1498409831018], [10.2932, 1498409846010], [10.4912, 1498409861946], [10.4151, 1498409876871], [10.2401, 1498409891710], [10.4921, 1498409906143], [10.4413, 1498409921477], [10.6318, 1498409936147], [10.5277, 1498409951915], [10.6333, 1498409966052], [10.6417, 1498409981944], [10.4505, 1498409996867], [10.5812, 1498410011770], [10.4934, 1498410026573], [10.5731, 1498410041317], [10.5, 1498410056213], [10.6505, 1498410071013], [9.4035, 1498410086387]];
+
+var series_set = [[[1.0247, 1498409631773], [0.9988, 1498409646697], [0.9817, 1498409661239], [0.9569, 1498409676045], [1.0331, 1498409691922], [1.0755, 1498409706546], [1.1862, 1498409721525], [1.2984, 1498409736175], [1.2389, 1498409751817], [1.1452, 1498409766783], [1.102, 1498409781699], [0.9647, 1498409796664], [1.0063, 1498409811627], [1.0318, 1498409826887], [1.065, 1498409841645], [1.0907, 1498409856647], [1.0229, 1498409871521], [1.0654, 1498409886031], [1.0568, 1498409901544], [1.0818, 1498409916194], [1.1335, 1498409931672], [1.057, 1498409946673], [1.0243, 1498409961669], [1.0329, 1498409976637], [1.1428, 1498409991563], [1.2198, 1498410006441], [1.2192, 1498410021230], [1.2615, 1498410036027], [1.1765, 1498410051907], [1.2352, 1498410066109], [1.0557, 1498410081043]], [[10.7104, 1498409636085], [10.578, 1498409651011], [10.5985, 1498409666628], [10.6877, 1498409681525], [10.5495, 1498409696586], [10.5981, 1498409711009], [10.5076, 1498409726949], [11.4807, 1498409741853], [11.6165, 1498409756165], [11.8575, 1498409771018], [11.9936, 1498409786056], [10.7566, 1498409801942], [10.7484, 1498409816010], [10.6038, 1498409831018], [10.2932, 1498409846010], [10.4912, 1498409861946], [10.4151, 1498409876871], [10.2401, 1498409891710], [10.4921, 1498409906143], [10.4413, 1498409921477], [10.6318, 1498409936147], [10.5277, 1498409951915], [10.6333, 1498409966052], [10.6417, 1498409981944], [10.4505, 1498409996867], [10.5812, 1498410011770], [10.4934, 1498410026573], [10.5731, 1498410041317], [10.5, 1498410056213], [10.6505, 1498410071013], [9.4035, 1498410086387]]];
+
+var growing_series = [[10755200, 1498332216642], [10761200, 1498332276802], [10767200, 1498332336367], [10773200, 1498332396584], [10779200, 1498332456880], [10785200, 1498332516479], [10791200, 1498332576610], [10797200, 1498332636353], [10803200, 1498332696513], [10809200, 1498332756884], [10815200, 1498332816890], [10821200, 1498332876305], [10827200, 1498332936384], [10833200, 1498332996659], [10839200, 1498333056965], [10845200, 1498333116748], [10851200, 1498333176687], [10857200, 1498333236646], [10863200, 1498333297034], [10869200, 1498333356358], [10875200, 1498333416445], [4800, 1498333536686], [17900, 1498333667962], [24000, 1498333729157], [29500, 1498333783662], [34800, 1498333836813], [40700, 1498333896403], [46800, 1498333956953], [52800, 1498334016976], [6000, 1498334136593], [12000, 1498334196567]];
+
+module.exports = [{
+ name: 'groupBy',
+ tests: {
+ 'groupBy(AVERAGE)': function groupByAVERAGE() {
+ _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.AVERAGE);
+ },
+ 'groupBy(MAX)': function groupByMAX() {
+ _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.COUNT);
+ }
+ }
+}, {
+ name: 'sumSeries',
+ tests: {
+ 'sumSeries()': function sumSeries() {
+ _timeseries2.default.sumSeries(series_set);
+ },
+ 'groupBy(MAX)->sumSeries()': function groupByMAXSumSeries() {
+ var prepeared_series = _lodash2.default.map(series_set, function (datapoints) {
+ return _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.MAX);
+ });
+ _timeseries2.default.sumSeries(prepeared_series);
+ }
+ }
+}, {
+ name: 'delta vs rate',
+ tests: {
+ 'delta()': function delta() {
+ _timeseries2.default.delta(growing_series);
+ },
+ 'rate()': function rate() {
+ _timeseries2.default.rate(growing_series);
+ }
+ }
+}, {
+ name: 'scale',
+ tests: {
+ 'scale()': function scale() {
+ _timeseries2.default.scale(datapoints, 42);
+ },
+ 'scale_perf()': function scale_perf() {
+ _timeseries2.default.scale_perf(datapoints, 42);
+ }
+ }
+}, {
+ name: 'groupBy vs groupBy_perf',
+ tests: {
+ 'groupBy()': function groupBy() {
+ _timeseries2.default.groupBy(datapoints, '5m', _timeseries2.default.AVERAGE);
+ },
+ 'groupBy_perf()': function groupBy_perf() {
+ _timeseries2.default.groupBy_perf(datapoints, '5m', _timeseries2.default.AVERAGE);
+ }
+ }
+}];
diff --git a/dist/test/datasource-zabbix/config.controller.js b/dist/test/datasource-zabbix/config.controller.js
new file mode 100644
index 0000000..02e522f
--- /dev/null
+++ b/dist/test/datasource-zabbix/config.controller.js
@@ -0,0 +1,50 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ZabbixDSConfigController = undefined;
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var SUPPORTED_SQL_DS = ['mysql', 'postgres'];
+
+var defaultConfig = {
+ dbConnection: {
+ enable: false
+ }
+};
+
+var ZabbixDSConfigController = exports.ZabbixDSConfigController = function () {
+ /** @ngInject */
+ function ZabbixDSConfigController($scope, $injector, datasourceSrv) {
+ _classCallCheck(this, ZabbixDSConfigController);
+
+ this.datasourceSrv = datasourceSrv;
+
+ _lodash2.default.defaults(this.current.jsonData, defaultConfig);
+ this.sqlDataSources = this.getSupportedSQLDataSources();
+ }
+
+ _createClass(ZabbixDSConfigController, [{
+ key: 'getSupportedSQLDataSources',
+ value: function getSupportedSQLDataSources() {
+ var datasources = this.datasourceSrv.getAll();
+ return _lodash2.default.filter(datasources, function (ds) {
+ return _lodash2.default.includes(SUPPORTED_SQL_DS, ds.type);
+ });
+ }
+ }]);
+
+ return ZabbixDSConfigController;
+}();
+
+ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
diff --git a/dist/test/datasource-zabbix/constants.js b/dist/test/datasource-zabbix/constants.js
new file mode 100644
index 0000000..08418be
--- /dev/null
+++ b/dist/test/datasource-zabbix/constants.js
@@ -0,0 +1,29 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+// Editor modes
+var MODE_METRICS = exports.MODE_METRICS = 0;
+var MODE_ITSERVICE = exports.MODE_ITSERVICE = 1;
+var MODE_TEXT = exports.MODE_TEXT = 2;
+var MODE_ITEMID = exports.MODE_ITEMID = 3;
+var MODE_TRIGGERS = exports.MODE_TRIGGERS = 4;
+
+// Triggers severity
+var SEV_NOT_CLASSIFIED = exports.SEV_NOT_CLASSIFIED = 0;
+var SEV_INFORMATION = exports.SEV_INFORMATION = 1;
+var SEV_WARNING = exports.SEV_WARNING = 2;
+var SEV_AVERAGE = exports.SEV_AVERAGE = 3;
+var SEV_HIGH = exports.SEV_HIGH = 4;
+var SEV_DISASTER = exports.SEV_DISASTER = 5;
+
+var SHOW_ALL_TRIGGERS = exports.SHOW_ALL_TRIGGERS = [0, 1];
+var SHOW_ALL_EVENTS = exports.SHOW_ALL_EVENTS = [0, 1];
+var SHOW_OK_EVENTS = exports.SHOW_OK_EVENTS = 1;
+
+// Data point
+var DATAPOINT_VALUE = exports.DATAPOINT_VALUE = 0;
+var DATAPOINT_TS = exports.DATAPOINT_TS = 1;
+
+var TRIGGER_SEVERITY = exports.TRIGGER_SEVERITY = [{ val: 0, text: 'Not classified' }, { val: 1, text: 'Information' }, { val: 2, text: 'Warning' }, { val: 3, text: 'Average' }, { val: 4, text: 'High' }, { val: 5, text: 'Disaster' }];
diff --git a/dist/test/datasource-zabbix/dataProcessor.js b/dist/test/datasource-zabbix/dataProcessor.js
new file mode 100644
index 0000000..61a2ab1
--- /dev/null
+++ b/dist/test/datasource-zabbix/dataProcessor.js
@@ -0,0 +1,191 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _utils = require('./utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+var _timeseries = require('./timeseries');
+
+var _timeseries2 = _interopRequireDefault(_timeseries);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+var downsampleSeries = _timeseries2.default.downsample;
+var groupBy = _timeseries2.default.groupBy_perf;
+var groupBy_exported = function groupBy_exported(interval, groupFunc, datapoints) {
+ return groupBy(datapoints, interval, groupFunc);
+};
+var sumSeries = _timeseries2.default.sumSeries;
+var delta = _timeseries2.default.delta;
+var rate = _timeseries2.default.rate;
+var scale = function scale(factor, datapoints) {
+ return _timeseries2.default.scale_perf(datapoints, factor);
+};
+var simpleMovingAverage = function simpleMovingAverage(n, datapoints) {
+ return _timeseries2.default.simpleMovingAverage(datapoints, n);
+};
+var expMovingAverage = function expMovingAverage(a, datapoints) {
+ return _timeseries2.default.expMovingAverage(datapoints, a);
+};
+
+var SUM = _timeseries2.default.SUM;
+var COUNT = _timeseries2.default.COUNT;
+var AVERAGE = _timeseries2.default.AVERAGE;
+var MIN = _timeseries2.default.MIN;
+var MAX = _timeseries2.default.MAX;
+var MEDIAN = _timeseries2.default.MEDIAN;
+var PERCENTIL = _timeseries2.default.PERCENTIL;
+
+function limit(order, n, orderByFunc, timeseries) {
+ var orderByCallback = aggregationFunctions[orderByFunc];
+ var sortByIteratee = function sortByIteratee(ts) {
+ var values = _lodash2.default.map(ts.datapoints, function (point) {
+ return point[0];
+ });
+ return orderByCallback(values);
+ };
+ var sortedTimeseries = _lodash2.default.sortBy(timeseries, sortByIteratee);
+ if (order === 'bottom') {
+ return sortedTimeseries.slice(0, n);
+ } else {
+ return sortedTimeseries.slice(-n);
+ }
+}
+
+function sortSeries(direction, timeseries) {
+ return _lodash2.default.orderBy(timeseries, [function (ts) {
+ return ts.target.toLowerCase();
+ }], direction);
+}
+
+function setAlias(alias, timeseries) {
+ timeseries.target = alias;
+ return timeseries;
+}
+
+function replaceAlias(regexp, newAlias, timeseries) {
+ var pattern = void 0;
+ if (utils.isRegex(regexp)) {
+ pattern = utils.buildRegex(regexp);
+ } else {
+ pattern = regexp;
+ }
+
+ var alias = timeseries.target.replace(pattern, newAlias);
+ timeseries.target = alias;
+ return timeseries;
+}
+
+function setAliasByRegex(alias, timeseries) {
+ timeseries.target = extractText(timeseries.target, alias);
+ return timeseries;
+}
+
+function extractText(str, pattern) {
+ var extractPattern = new RegExp(pattern);
+ var extractedValue = extractPattern.exec(str);
+ extractedValue = extractedValue[0];
+ return extractedValue;
+}
+
+function groupByWrapper(interval, groupFunc, datapoints) {
+ var groupByCallback = aggregationFunctions[groupFunc];
+ return groupBy(datapoints, interval, groupByCallback);
+}
+
+function aggregateByWrapper(interval, aggregateFunc, datapoints) {
+ // Flatten all points in frame and then just use groupBy()
+ var flattenedPoints = _lodash2.default.flatten(datapoints, true);
+ var groupByCallback = aggregationFunctions[aggregateFunc];
+ return groupBy(flattenedPoints, interval, groupByCallback);
+}
+
+function aggregateWrapper(groupByCallback, interval, datapoints) {
+ var flattenedPoints = _lodash2.default.flatten(datapoints, true);
+ return groupBy(flattenedPoints, interval, groupByCallback);
+}
+
+function percentil(interval, n, datapoints) {
+ var flattenedPoints = _lodash2.default.flatten(datapoints, true);
+ var groupByCallback = _lodash2.default.partial(PERCENTIL, n);
+ return groupBy(flattenedPoints, interval, groupByCallback);
+}
+
+function timeShift(interval, range) {
+ var shift = utils.parseTimeShiftInterval(interval) / 1000;
+ return _lodash2.default.map(range, function (time) {
+ return time - shift;
+ });
+}
+
+function unShiftTimeSeries(interval, datapoints) {
+ var unshift = utils.parseTimeShiftInterval(interval);
+ return _lodash2.default.map(datapoints, function (dp) {
+ return [dp[0], dp[1] + unshift];
+ });
+}
+
+var metricFunctions = {
+ groupBy: groupByWrapper,
+ scale: scale,
+ delta: delta,
+ rate: rate,
+ movingAverage: simpleMovingAverage,
+ exponentialMovingAverage: expMovingAverage,
+ aggregateBy: aggregateByWrapper,
+ // Predefined aggs
+ percentil: percentil,
+ average: _lodash2.default.partial(aggregateWrapper, AVERAGE),
+ min: _lodash2.default.partial(aggregateWrapper, MIN),
+ max: _lodash2.default.partial(aggregateWrapper, MAX),
+ median: _lodash2.default.partial(aggregateWrapper, MEDIAN),
+ sum: _lodash2.default.partial(aggregateWrapper, SUM),
+ count: _lodash2.default.partial(aggregateWrapper, COUNT),
+ sumSeries: sumSeries,
+ top: _lodash2.default.partial(limit, 'top'),
+ bottom: _lodash2.default.partial(limit, 'bottom'),
+ sortSeries: sortSeries,
+ timeShift: timeShift,
+ setAlias: setAlias,
+ setAliasByRegex: setAliasByRegex,
+ replaceAlias: replaceAlias
+};
+
+var aggregationFunctions = {
+ avg: AVERAGE,
+ min: MIN,
+ max: MAX,
+ median: MEDIAN,
+ sum: SUM,
+ count: COUNT
+};
+
+exports.default = {
+ downsampleSeries: downsampleSeries,
+ groupBy: groupBy_exported,
+ AVERAGE: AVERAGE,
+ MIN: MIN,
+ MAX: MAX,
+ MEDIAN: MEDIAN,
+ SUM: SUM,
+ COUNT: COUNT,
+ unShiftTimeSeries: unShiftTimeSeries,
+
+ get aggregationFunctions() {
+ return aggregationFunctions;
+ },
+
+ get metricFunctions() {
+ return metricFunctions;
+ }
+};
diff --git a/dist/test/datasource-zabbix/datasource.js b/dist/test/datasource-zabbix/datasource.js
new file mode 100644
index 0000000..d626c34
--- /dev/null
+++ b/dist/test/datasource-zabbix/datasource.js
@@ -0,0 +1,879 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.zabbixTemplateFormat = exports.ZabbixAPIDatasource = undefined;
+
+var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _datemath = require('app/core/utils/datemath');
+
+var dateMath = _interopRequireWildcard(_datemath);
+
+var _utils = require('./utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+var _migrations = require('./migrations');
+
+var migrations = _interopRequireWildcard(_migrations);
+
+var _metricFunctions = require('./metricFunctions');
+
+var metricFunctions = _interopRequireWildcard(_metricFunctions);
+
+var _constants = require('./constants');
+
+var c = _interopRequireWildcard(_constants);
+
+var _dataProcessor = require('./dataProcessor');
+
+var _dataProcessor2 = _interopRequireDefault(_dataProcessor);
+
+var _responseHandler = require('./responseHandler');
+
+var _responseHandler2 = _interopRequireDefault(_responseHandler);
+
+require('./zabbix.js');
+
+require('./zabbixAlerting.service.js');
+
+var _zabbixAPICoreService = require('./zabbixAPICore.service.js');
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var ZabbixAPIDatasource = function () {
+
+ /** @ngInject */
+ function ZabbixAPIDatasource(instanceSettings, templateSrv, alertSrv, dashboardSrv, zabbixAlertingSrv, Zabbix) {
+ _classCallCheck(this, ZabbixAPIDatasource);
+
+ this.templateSrv = templateSrv;
+ this.alertSrv = alertSrv;
+ this.dashboardSrv = dashboardSrv;
+ this.zabbixAlertingSrv = zabbixAlertingSrv;
+
+ // Use custom format for template variables
+ this.replaceTemplateVars = _lodash2.default.partial(replaceTemplateVars, this.templateSrv);
+
+ // General data source settings
+ this.name = instanceSettings.name;
+ this.url = instanceSettings.url;
+ this.basicAuth = instanceSettings.basicAuth;
+ this.withCredentials = instanceSettings.withCredentials;
+
+ var jsonData = instanceSettings.jsonData;
+
+ // Zabbix API credentials
+ this.username = jsonData.username;
+ this.password = jsonData.password;
+
+ // Use trends instead history since specified time
+ this.trends = jsonData.trends;
+ this.trendsFrom = jsonData.trendsFrom || '7d';
+ this.trendsRange = jsonData.trendsRange || '4d';
+
+ // Set cache update interval
+ var ttl = jsonData.cacheTTL || '1h';
+ this.cacheTTL = utils.parseInterval(ttl);
+
+ // Alerting options
+ this.alertingEnabled = jsonData.alerting;
+ this.addThresholds = jsonData.addThresholds;
+ this.alertingMinSeverity = jsonData.alertingMinSeverity || c.SEV_WARNING;
+
+ // Direct DB Connection options
+ var dbConnectionOptions = jsonData.dbConnection || {};
+ this.enableDirectDBConnection = dbConnectionOptions.enable;
+ this.sqlDatasourceId = dbConnectionOptions.datasourceId;
+
+ var zabbixOptions = {
+ username: this.username,
+ password: this.password,
+ basicAuth: this.basicAuth,
+ withCredentials: this.withCredentials,
+ cacheTTL: this.cacheTTL,
+ enableDirectDBConnection: this.enableDirectDBConnection,
+ sqlDatasourceId: this.sqlDatasourceId
+ };
+
+ this.zabbix = new Zabbix(this.url, zabbixOptions);
+ }
+
+ ////////////////////////
+ // Datasource methods //
+ ////////////////////////
+
+ /**
+ * Query panel data. Calls for each panel in dashboard.
+ * @param {Object} options Contains time range, targets and other info.
+ * @return {Object} Grafana metrics object with timeseries data for each target.
+ */
+
+
+ _createClass(ZabbixAPIDatasource, [{
+ key: 'query',
+ value: function query(options) {
+ var _this = this;
+
+ // Get alerts for current panel
+ if (this.alertingEnabled) {
+ this.alertQuery(options).then(function (alert) {
+ _this.zabbixAlertingSrv.setPanelAlertState(options.panelId, alert.state);
+
+ _this.zabbixAlertingSrv.removeZabbixThreshold(options.panelId);
+ if (_this.addThresholds) {
+ _lodash2.default.forEach(alert.thresholds, function (threshold) {
+ _this.zabbixAlertingSrv.setPanelThreshold(options.panelId, threshold);
+ });
+ }
+ });
+ }
+
+ // Create request for each target
+ var promises = _lodash2.default.map(options.targets, function (t) {
+ // Don't request undefined and hidden targets
+ if (t.hide) {
+ return [];
+ }
+
+ var timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000);
+ var timeTo = Math.ceil(dateMath.parse(options.range.to) / 1000);
+
+ // Prevent changes of original object
+ var target = _lodash2.default.cloneDeep(t);
+ _this.replaceTargetVariables(target, options);
+
+ // Apply Time-related functions (timeShift(), etc)
+ var timeFunctions = bindFunctionDefs(target.functions, 'Time');
+ if (timeFunctions.length) {
+ var _sequence = sequence(timeFunctions)([timeFrom, timeTo]),
+ _sequence2 = _slicedToArray(_sequence, 2),
+ time_from = _sequence2[0],
+ time_to = _sequence2[1];
+
+ timeFrom = time_from;
+ timeTo = time_to;
+ }
+ var timeRange = [timeFrom, timeTo];
+
+ var useTrends = _this.isUseTrends(timeRange);
+
+ // Metrics or Text query mode
+ if (!target.mode || target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT) {
+ // Migrate old targets
+ target = migrations.migrate(target);
+
+ // Don't request undefined and hidden targets
+ if (target.hide || !target.group || !target.host || !target.item) {
+ return [];
+ }
+
+ if (!target.mode || target.mode === c.MODE_METRICS) {
+ return _this.queryNumericData(target, timeRange, useTrends, options);
+ } else if (target.mode === c.MODE_TEXT) {
+ return _this.queryTextData(target, timeRange);
+ }
+ } else if (target.mode === c.MODE_ITEMID) {
+ // Item ID mode
+ if (!target.itemids) {
+ return [];
+ }
+ return _this.queryItemIdData(target, timeRange, useTrends, options);
+ } else if (target.mode === c.MODE_ITSERVICE) {
+ // IT services mode
+ return _this.queryITServiceData(target, timeRange, options);
+ } else if (target.mode === c.MODE_TRIGGERS) {
+ // Triggers mode
+ return _this.queryTriggersData(target, timeRange);
+ } else {
+ return [];
+ }
+ });
+
+ // Data for panel (all targets)
+ return Promise.all(_lodash2.default.flatten(promises)).then(_lodash2.default.flatten).then(function (data) {
+ return { data: data };
+ });
+ }
+
+ /**
+ * Query target data for Metrics mode
+ */
+
+ }, {
+ key: 'queryNumericData',
+ value: function queryNumericData(target, timeRange, useTrends, options) {
+ var _this2 = this;
+
+ var getItemOptions = {
+ itemtype: 'num'
+ };
+ return this.zabbix.getItemsFromTarget(target, getItemOptions).then(function (items) {
+ return _this2.queryNumericDataForItems(items, target, timeRange, useTrends, options);
+ });
+ }
+
+ /**
+ * Query history for numeric items
+ */
+
+ }, {
+ key: 'queryNumericDataForItems',
+ value: function queryNumericDataForItems(items, target, timeRange, useTrends, options) {
+ var _this3 = this;
+
+ var _timeRange = _slicedToArray(timeRange, 2),
+ timeFrom = _timeRange[0],
+ timeTo = _timeRange[1];
+
+ var getHistoryPromise = void 0;
+ options.consolidateBy = getConsolidateBy(target);
+
+ if (useTrends) {
+ if (this.enableDirectDBConnection) {
+ getHistoryPromise = this.zabbix.getTrendsDB(items, timeFrom, timeTo, options).then(function (history) {
+ return _this3.zabbix.dbConnector.handleGrafanaTSResponse(history, items);
+ });
+ } else {
+ var valueType = this.getTrendValueType(target);
+ getHistoryPromise = this.zabbix.getTrend(items, timeFrom, timeTo).then(function (history) {
+ return _responseHandler2.default.handleTrends(history, items, valueType);
+ }).then(function (timeseries) {
+ // Sort trend data, issue #202
+ _lodash2.default.forEach(timeseries, function (series) {
+ series.datapoints = _lodash2.default.sortBy(series.datapoints, function (point) {
+ return point[c.DATAPOINT_TS];
+ });
+ });
+ return timeseries;
+ });
+ }
+ } else {
+ // Use history
+ if (this.enableDirectDBConnection) {
+ getHistoryPromise = this.zabbix.getHistoryDB(items, timeFrom, timeTo, options).then(function (history) {
+ return _this3.zabbix.dbConnector.handleGrafanaTSResponse(history, items);
+ });
+ } else {
+ getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo).then(function (history) {
+ return _responseHandler2.default.handleHistory(history, items);
+ });
+ }
+ }
+
+ return getHistoryPromise.then(function (timeseries) {
+ return _this3.applyDataProcessingFunctions(timeseries, target);
+ }).then(function (timeseries) {
+ return downsampleSeries(timeseries, options);
+ }).catch(function (error) {
+ console.log(error);
+ return [];
+ });
+ }
+ }, {
+ key: 'getTrendValueType',
+ value: function getTrendValueType(target) {
+ // Find trendValue() function and get specified trend value
+ var trendFunctions = _lodash2.default.map(metricFunctions.getCategories()['Trends'], 'name');
+ var trendValueFunc = _lodash2.default.find(target.functions, function (func) {
+ return _lodash2.default.includes(trendFunctions, func.def.name);
+ });
+ return trendValueFunc ? trendValueFunc.params[0] : "avg";
+ }
+ }, {
+ key: 'applyDataProcessingFunctions',
+ value: function applyDataProcessingFunctions(timeseries_data, target) {
+ var transformFunctions = bindFunctionDefs(target.functions, 'Transform');
+ var aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
+ var filterFunctions = bindFunctionDefs(target.functions, 'Filter');
+ var aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
+
+ // Apply transformation functions
+ timeseries_data = _lodash2.default.cloneDeep(_lodash2.default.map(timeseries_data, function (timeseries) {
+ timeseries.datapoints = sequence(transformFunctions)(timeseries.datapoints);
+ return timeseries;
+ }));
+
+ // Apply filter functions
+ if (filterFunctions.length) {
+ timeseries_data = sequence(filterFunctions)(timeseries_data);
+ }
+
+ // Apply aggregations
+ if (aggregationFunctions.length) {
+ var dp = _lodash2.default.map(timeseries_data, 'datapoints');
+ dp = sequence(aggregationFunctions)(dp);
+
+ var aggFuncNames = _lodash2.default.map(metricFunctions.getCategories()['Aggregate'], 'name');
+ var lastAgg = _lodash2.default.findLast(target.functions, function (func) {
+ return _lodash2.default.includes(aggFuncNames, func.def.name);
+ });
+
+ timeseries_data = [{
+ target: lastAgg.text,
+ datapoints: dp
+ }];
+ }
+
+ // Apply alias functions
+ _lodash2.default.forEach(timeseries_data, sequence(aliasFunctions));
+
+ // Apply Time-related functions (timeShift(), etc)
+ // Find timeShift() function and get specified trend value
+ this.applyTimeShiftFunction(timeseries_data, target);
+
+ return timeseries_data;
+ }
+ }, {
+ key: 'applyTimeShiftFunction',
+ value: function applyTimeShiftFunction(timeseries_data, target) {
+ // Find timeShift() function and get specified interval
+ var timeShiftFunc = _lodash2.default.find(target.functions, function (func) {
+ return func.def.name === 'timeShift';
+ });
+ if (timeShiftFunc) {
+ var shift = timeShiftFunc.params[0];
+ _lodash2.default.forEach(timeseries_data, function (series) {
+ series.datapoints = _dataProcessor2.default.unShiftTimeSeries(shift, series.datapoints);
+ });
+ }
+ }
+
+ /**
+ * Query target data for Text mode
+ */
+
+ }, {
+ key: 'queryTextData',
+ value: function queryTextData(target, timeRange) {
+ var _this4 = this;
+
+ var _timeRange2 = _slicedToArray(timeRange, 2),
+ timeFrom = _timeRange2[0],
+ timeTo = _timeRange2[1];
+
+ var options = {
+ itemtype: 'text'
+ };
+ return this.zabbix.getItemsFromTarget(target, options).then(function (items) {
+ if (items.length) {
+ return _this4.zabbix.getHistory(items, timeFrom, timeTo).then(function (history) {
+ return _responseHandler2.default.handleText(history, items, target);
+ });
+ } else {
+ return Promise.resolve([]);
+ }
+ });
+ }
+
+ /**
+ * Query target data for Item ID mode
+ */
+
+ }, {
+ key: 'queryItemIdData',
+ value: function queryItemIdData(target, timeRange, useTrends, options) {
+ var _this5 = this;
+
+ var itemids = target.itemids;
+ itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
+ itemids = _lodash2.default.map(itemids.split(','), function (itemid) {
+ return itemid.trim();
+ });
+
+ if (!itemids) {
+ return [];
+ }
+
+ return this.zabbix.getItemsByIDs(itemids).then(function (items) {
+ return _this5.queryNumericDataForItems(items, target, timeRange, useTrends, options);
+ });
+ }
+
+ /**
+ * Query target data for IT Services mode
+ */
+
+ }, {
+ key: 'queryITServiceData',
+ value: function queryITServiceData(target, timeRange, options) {
+ var _this6 = this;
+
+ // Don't show undefined and hidden targets
+ if (target.hide || !target.itservice && !target.itServiceFilter || !target.slaProperty) {
+ return [];
+ }
+
+ var itServiceIds = [];
+ var itServices = [];
+ var itServiceFilter = void 0;
+ var isOldVersion = target.itservice && !target.itServiceFilter;
+
+ if (isOldVersion) {
+ // Backward compatibility
+ itServiceFilter = '/.*/';
+ } else {
+ itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars);
+ }
+
+ return this.zabbix.getITServices(itServiceFilter).then(function (itservices) {
+ itServices = itservices;
+ if (isOldVersion) {
+ itServices = _lodash2.default.filter(itServices, { 'serviceid': target.itservice.serviceid });
+ }
+
+ itServiceIds = _lodash2.default.map(itServices, 'serviceid');
+ return itServiceIds;
+ }).then(function (serviceids) {
+ return _this6.zabbix.getSLA(serviceids, timeRange);
+ }).then(function (slaResponse) {
+ return _lodash2.default.map(itServiceIds, function (serviceid) {
+ var itservice = _lodash2.default.find(itServices, { 'serviceid': serviceid });
+ return _responseHandler2.default.handleSLAResponse(itservice, target.slaProperty, slaResponse);
+ });
+ });
+ }
+ }, {
+ key: 'queryTriggersData',
+ value: function queryTriggersData(target, timeRange) {
+ var _this7 = this;
+
+ var _timeRange3 = _slicedToArray(timeRange, 2),
+ timeFrom = _timeRange3[0],
+ timeTo = _timeRange3[1];
+
+ return this.zabbix.getHostsFromTarget(target).then(function (results) {
+ var _results = _slicedToArray(results, 2),
+ hosts = _results[0],
+ apps = _results[1];
+
+ if (hosts.length) {
+ var hostids = _lodash2.default.map(hosts, 'hostid');
+ var appids = _lodash2.default.map(apps, 'applicationid');
+ var options = {
+ minSeverity: target.triggers.minSeverity,
+ acknowledged: target.triggers.acknowledged,
+ count: target.triggers.count,
+ timeFrom: timeFrom,
+ timeTo: timeTo
+ };
+ return _this7.zabbix.getHostAlerts(hostids, appids, options).then(function (triggers) {
+ return _responseHandler2.default.handleTriggersResponse(triggers, timeRange);
+ });
+ } else {
+ return Promise.resolve([]);
+ }
+ });
+ }
+
+ /**
+ * Test connection to Zabbix API
+ * @return {object} Connection status and Zabbix API version
+ */
+
+ }, {
+ key: 'testDatasource',
+ value: function testDatasource() {
+ var _this8 = this;
+
+ var zabbixVersion = void 0;
+ return this.zabbix.getVersion().then(function (version) {
+ zabbixVersion = version;
+ return _this8.zabbix.login();
+ }).then(function () {
+ if (_this8.enableDirectDBConnection) {
+ return _this8.zabbix.dbConnector.testSQLDataSource();
+ } else {
+ return Promise.resolve();
+ }
+ }).then(function () {
+ return {
+ status: "success",
+ title: "Success",
+ message: "Zabbix API version: " + zabbixVersion
+ };
+ }).catch(function (error) {
+ if (error instanceof _zabbixAPICoreService.ZabbixAPIError) {
+ return {
+ status: "error",
+ title: error.message,
+ message: error.message
+ };
+ } else if (error.data && error.data.message) {
+ return {
+ status: "error",
+ title: "Connection failed",
+ message: "Connection failed: " + error.data.message
+ };
+ } else {
+ return {
+ status: "error",
+ title: "Connection failed",
+ message: "Could not connect to given url"
+ };
+ }
+ });
+ }
+
+ ////////////////
+ // Templating //
+ ////////////////
+
+ /**
+ * Find metrics from templated request.
+ *
+ * @param {string} query Query from Templating
+ * @return {string} Metric name - group, host, app or item or list
+ * of metrics in "{metric1,metcic2,...,metricN}" format.
+ */
+
+ }, {
+ key: 'metricFindQuery',
+ value: function metricFindQuery(query) {
+ var _this9 = this;
+
+ var result = void 0;
+ var parts = [];
+
+ // Split query. Query structure: group.host.app.item
+ _lodash2.default.each(utils.splitTemplateQuery(query), function (part) {
+ part = _this9.replaceTemplateVars(part, {});
+
+ // Replace wildcard to regex
+ if (part === '*') {
+ part = '/.*/';
+ }
+ parts.push(part);
+ });
+ var template = _lodash2.default.zipObject(['group', 'host', 'app', 'item'], parts);
+
+ // Get items
+ if (parts.length === 4) {
+ // Search for all items, even it's not belong to any application
+ if (template.app === '/.*/') {
+ template.app = '';
+ }
+ result = this.zabbix.getItems(template.group, template.host, template.app, template.item);
+ } else if (parts.length === 3) {
+ // Get applications
+ result = this.zabbix.getApps(template.group, template.host, template.app);
+ } else if (parts.length === 2) {
+ // Get hosts
+ result = this.zabbix.getHosts(template.group, template.host);
+ } else if (parts.length === 1) {
+ // Get groups
+ result = this.zabbix.getGroups(template.group);
+ } else {
+ result = Promise.resolve([]);
+ }
+
+ return result.then(function (metrics) {
+ return _lodash2.default.map(metrics, formatMetric);
+ });
+ }
+
+ /////////////////
+ // Annotations //
+ /////////////////
+
+ }, {
+ key: 'annotationQuery',
+ value: function annotationQuery(options) {
+ var _this10 = this;
+
+ var timeFrom = Math.ceil(dateMath.parse(options.rangeRaw.from) / 1000);
+ var timeTo = Math.ceil(dateMath.parse(options.rangeRaw.to) / 1000);
+ var annotation = options.annotation;
+ var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS;
+
+ // Show all triggers
+ var triggersOptions = {
+ showTriggers: c.SHOW_ALL_TRIGGERS,
+ hideHostsInMaintenance: false
+ };
+
+ var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}), this.replaceTemplateVars(annotation.host, {}), this.replaceTemplateVars(annotation.application, {}), triggersOptions);
+
+ return getTriggers.then(function (triggers) {
+
+ // Filter triggers by description
+ var triggerName = _this10.replaceTemplateVars(annotation.trigger, {});
+ if (utils.isRegex(triggerName)) {
+ triggers = _lodash2.default.filter(triggers, function (trigger) {
+ return utils.buildRegex(triggerName).test(trigger.description);
+ });
+ } else if (triggerName) {
+ triggers = _lodash2.default.filter(triggers, function (trigger) {
+ return trigger.description === triggerName;
+ });
+ }
+
+ // Remove events below the chose severity
+ triggers = _lodash2.default.filter(triggers, function (trigger) {
+ return Number(trigger.priority) >= Number(annotation.minseverity);
+ });
+
+ var objectids = _lodash2.default.map(triggers, 'triggerid');
+ return _this10.zabbix.getEvents(objectids, timeFrom, timeTo, showOkEvents).then(function (events) {
+ var indexedTriggers = _lodash2.default.keyBy(triggers, 'triggerid');
+
+ // Hide acknowledged events if option enabled
+ if (annotation.hideAcknowledged) {
+ events = _lodash2.default.filter(events, function (event) {
+ return !event.acknowledges.length;
+ });
+ }
+
+ return _lodash2.default.map(events, function (event) {
+ var tags = void 0;
+ if (annotation.showHostname) {
+ tags = _lodash2.default.map(event.hosts, 'name');
+ }
+
+ // Show event type (OK or Problem)
+ var title = Number(event.value) ? 'Problem' : 'OK';
+
+ var formatted_acknowledges = utils.formatAcknowledges(event.acknowledges);
+ return {
+ annotation: annotation,
+ time: event.clock * 1000,
+ title: title,
+ tags: tags,
+ text: indexedTriggers[event.objectid].description + formatted_acknowledges
+ };
+ });
+ });
+ });
+ }
+
+ /**
+ * Get triggers and its details for panel's targets
+ * Returns alert state ('ok' if no fired triggers, or 'alerting' if at least 1 trigger is fired)
+ * or empty object if no related triggers are finded.
+ */
+
+ }, {
+ key: 'alertQuery',
+ value: function alertQuery(options) {
+ var _this11 = this;
+
+ var enabled_targets = filterEnabledTargets(options.targets);
+ var getPanelItems = _lodash2.default.map(enabled_targets, function (t) {
+ var target = _lodash2.default.cloneDeep(t);
+ _this11.replaceTargetVariables(target, options);
+ return _this11.zabbix.getItemsFromTarget(target, { itemtype: 'num' });
+ });
+
+ return Promise.all(getPanelItems).then(function (results) {
+ var items = _lodash2.default.flatten(results);
+ var itemids = _lodash2.default.map(items, 'itemid');
+
+ if (itemids.length === 0) {
+ return [];
+ }
+ return _this11.zabbix.getAlerts(itemids);
+ }).then(function (triggers) {
+ triggers = _lodash2.default.filter(triggers, function (trigger) {
+ return trigger.priority >= _this11.alertingMinSeverity;
+ });
+
+ if (!triggers || triggers.length === 0) {
+ return {};
+ }
+
+ var state = 'ok';
+
+ var firedTriggers = _lodash2.default.filter(triggers, { value: '1' });
+ if (firedTriggers.length) {
+ state = 'alerting';
+ }
+
+ var thresholds = _lodash2.default.map(triggers, function (trigger) {
+ return getTriggerThreshold(trigger.expression);
+ });
+
+ return {
+ panelId: options.panelId,
+ state: state,
+ thresholds: thresholds
+ };
+ });
+ }
+
+ // Replace template variables
+
+ }, {
+ key: 'replaceTargetVariables',
+ value: function replaceTargetVariables(target, options) {
+ var _this12 = this;
+
+ var parts = ['group', 'host', 'application', 'item'];
+ _lodash2.default.forEach(parts, function (p) {
+ if (target[p] && target[p].filter) {
+ target[p].filter = _this12.replaceTemplateVars(target[p].filter, options.scopedVars);
+ }
+ });
+ target.textFilter = this.replaceTemplateVars(target.textFilter, options.scopedVars);
+
+ _lodash2.default.forEach(target.functions, function (func) {
+ func.params = _lodash2.default.map(func.params, function (param) {
+ if (typeof param === 'number') {
+ return +_this12.templateSrv.replace(param.toString(), options.scopedVars);
+ } else {
+ return _this12.templateSrv.replace(param, options.scopedVars);
+ }
+ });
+ });
+ }
+ }, {
+ key: 'isUseTrends',
+ value: function isUseTrends(timeRange) {
+ var _timeRange4 = _slicedToArray(timeRange, 2),
+ timeFrom = _timeRange4[0],
+ timeTo = _timeRange4[1];
+
+ var useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
+ var useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
+ var useTrends = this.trends && (timeFrom <= useTrendsFrom || timeTo - timeFrom >= useTrendsRange);
+ return useTrends;
+ }
+ }]);
+
+ return ZabbixAPIDatasource;
+}();
+
+function bindFunctionDefs(functionDefs, category) {
+ var aggregationFunctions = _lodash2.default.map(metricFunctions.getCategories()[category], 'name');
+ var aggFuncDefs = _lodash2.default.filter(functionDefs, function (func) {
+ return _lodash2.default.includes(aggregationFunctions, func.def.name);
+ });
+
+ return _lodash2.default.map(aggFuncDefs, function (func) {
+ var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
+ return funcInstance.bindFunction(_dataProcessor2.default.metricFunctions);
+ });
+}
+
+function getConsolidateBy(target) {
+ var consolidateBy = 'avg';
+ var funcDef = _lodash2.default.find(target.functions, function (func) {
+ return func.def.name === 'consolidateBy';
+ });
+ if (funcDef && funcDef.params && funcDef.params.length) {
+ consolidateBy = funcDef.params[0];
+ }
+ return consolidateBy;
+}
+
+function downsampleSeries(timeseries_data, options) {
+ var defaultAgg = _dataProcessor2.default.aggregationFunctions['avg'];
+ var consolidateByFunc = _dataProcessor2.default.aggregationFunctions[options.consolidateBy] || defaultAgg;
+ return _lodash2.default.map(timeseries_data, function (timeseries) {
+ if (timeseries.datapoints.length > options.maxDataPoints) {
+ timeseries.datapoints = _dataProcessor2.default.groupBy(options.interval, consolidateByFunc, timeseries.datapoints);
+ }
+ return timeseries;
+ });
+}
+
+function formatMetric(metricObj) {
+ return {
+ text: metricObj.name,
+ expandable: false
+ };
+}
+
+/**
+ * Custom formatter for template variables.
+ * Default Grafana "regex" formatter returns
+ * value1|value2
+ * This formatter returns
+ * (value1|value2)
+ * This format needed for using in complex regex with
+ * template variables, for example
+ * /CPU $cpu_item.*time/ where $cpu_item is system,user,iowait
+ */
+function zabbixTemplateFormat(value) {
+ if (typeof value === 'string') {
+ return utils.escapeRegex(value);
+ }
+
+ var escapedValues = _lodash2.default.map(value, utils.escapeRegex);
+ return '(' + escapedValues.join('|') + ')';
+}
+
+function zabbixItemIdsTemplateFormat(value) {
+ if (typeof value === 'string') {
+ return value;
+ }
+ return value.join(',');
+}
+
+/**
+ * If template variables are used in request, replace it using regex format
+ * and wrap with '/' for proper multi-value work. Example:
+ * $variable selected as a, b, c
+ * We use filter $variable
+ * $variable -> a|b|c -> /a|b|c/
+ * /$variable/ -> /a|b|c/ -> /a|b|c/
+ */
+function replaceTemplateVars(templateSrv, target, scopedVars) {
+ var replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
+ if (target !== replacedTarget && !utils.isRegex(replacedTarget)) {
+ replacedTarget = '/^' + replacedTarget + '$/';
+ }
+ return replacedTarget;
+}
+
+// Apply function one by one:
+// sequence([a(), b(), c()]) = c(b(a()));
+function sequence(funcsArray) {
+ return function (result) {
+ for (var i = 0; i < funcsArray.length; i++) {
+ result = funcsArray[i].call(this, result);
+ }
+ return result;
+ };
+}
+
+function filterEnabledTargets(targets) {
+ return _lodash2.default.filter(targets, function (target) {
+ return !(target.hide || !target.group || !target.host || !target.item);
+ });
+}
+
+function getTriggerThreshold(expression) {
+ var thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
+ var finded_thresholds = expression.match(thresholdPattern);
+ if (finded_thresholds && finded_thresholds.length >= 2) {
+ var threshold = finded_thresholds[1];
+ threshold = Number(threshold);
+ return threshold;
+ } else {
+ return null;
+ }
+}
+
+exports.ZabbixAPIDatasource = ZabbixAPIDatasource;
+exports.zabbixTemplateFormat = zabbixTemplateFormat;
+
+// Fix for backward compatibility with lodash 2.4
+
+if (!_lodash2.default.includes) {
+ _lodash2.default.includes = _lodash2.default.contains;
+}
+if (!_lodash2.default.keyBy) {
+ _lodash2.default.keyBy = _lodash2.default.indexBy;
+}
diff --git a/dist/test/datasource-zabbix/metric-function-editor.directive.js b/dist/test/datasource-zabbix/metric-function-editor.directive.js
new file mode 100644
index 0000000..02135e9
--- /dev/null
+++ b/dist/test/datasource-zabbix/metric-function-editor.directive.js
@@ -0,0 +1,246 @@
+'use strict';
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _jquery = require('jquery');
+
+var _jquery2 = _interopRequireDefault(_jquery);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/** @ngInject */
+_angular2.default.module('grafana.directives').directive('metricFunctionEditor', function ($compile, templateSrv) {
+
+ var funcSpanTemplate = '{{func.def.name}} ( ';
+ var paramTemplate = ' ';
+
+ var funcControlsTemplate = '' + ' ' + ' ' + ' ' + ' ' + '
';
+
+ return {
+ restrict: 'A',
+ link: function postLink($scope, elem) {
+ var $funcLink = (0, _jquery2.default)(funcSpanTemplate);
+ var $funcControls = (0, _jquery2.default)(funcControlsTemplate);
+ var ctrl = $scope.ctrl;
+ var func = $scope.func;
+ var funcDef = func.def;
+ var scheduledRelink = false;
+ var paramCountAtLink = 0;
+
+ function clickFuncParam(paramIndex) {
+ /*jshint validthis:true */
+
+ var $link = (0, _jquery2.default)(this);
+ var $input = $link.next();
+
+ $input.val(func.params[paramIndex]);
+ $input.css('width', $link.width() + 16 + 'px');
+
+ $link.hide();
+ $input.show();
+ $input.focus();
+ $input.select();
+
+ var typeahead = $input.data('typeahead');
+ if (typeahead) {
+ $input.val('');
+ typeahead.lookup();
+ }
+ }
+
+ function scheduledRelinkIfNeeded() {
+ if (paramCountAtLink === func.params.length) {
+ return;
+ }
+
+ if (!scheduledRelink) {
+ scheduledRelink = true;
+ setTimeout(function () {
+ relink();
+ scheduledRelink = false;
+ }, 200);
+ }
+ }
+
+ function inputBlur(paramIndex) {
+ /*jshint validthis:true */
+ var $input = (0, _jquery2.default)(this);
+ var $link = $input.prev();
+ var newValue = $input.val();
+
+ if (newValue !== '' || func.def.params[paramIndex].optional) {
+ $link.html(templateSrv.highlightVariablesAsHtml(newValue));
+
+ func.updateParam($input.val(), paramIndex);
+ scheduledRelinkIfNeeded();
+
+ $scope.$apply(function () {
+ ctrl.targetChanged();
+ });
+
+ $input.hide();
+ $link.show();
+ }
+ }
+
+ function inputKeyPress(paramIndex, e) {
+ /*jshint validthis:true */
+ if (e.which === 13) {
+ inputBlur.call(this, paramIndex);
+ }
+ }
+
+ function inputKeyDown() {
+ /*jshint validthis:true */
+ this.style.width = (3 + this.value.length) * 8 + 'px';
+ }
+
+ function addTypeahead($input, paramIndex) {
+ $input.attr('data-provide', 'typeahead');
+
+ var options = funcDef.params[paramIndex].options;
+ if (funcDef.params[paramIndex].type === 'int' || funcDef.params[paramIndex].type === 'float') {
+ options = _lodash2.default.map(options, function (val) {
+ return val.toString();
+ });
+ }
+
+ $input.typeahead({
+ source: options,
+ minLength: 0,
+ items: 20,
+ updater: function updater(value) {
+ setTimeout(function () {
+ inputBlur.call($input[0], paramIndex);
+ }, 0);
+ return value;
+ }
+ });
+
+ var typeahead = $input.data('typeahead');
+ typeahead.lookup = function () {
+ this.query = this.$element.val() || '';
+ return this.process(this.source);
+ };
+ }
+
+ function toggleFuncControls() {
+ var targetDiv = elem.closest('.tight-form');
+
+ if (elem.hasClass('show-function-controls')) {
+ elem.removeClass('show-function-controls');
+ targetDiv.removeClass('has-open-function');
+ $funcControls.hide();
+ return;
+ }
+
+ elem.addClass('show-function-controls');
+ targetDiv.addClass('has-open-function');
+
+ $funcControls.show();
+ }
+
+ function addElementsAndCompile() {
+ $funcControls.appendTo(elem);
+ $funcLink.appendTo(elem);
+
+ _lodash2.default.each(funcDef.params, function (param, index) {
+ if (param.optional && func.params.length <= index) {
+ return;
+ }
+
+ if (index > 0) {
+ (0, _jquery2.default)(', ').appendTo(elem);
+ }
+
+ var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
+ var $paramLink = (0, _jquery2.default)('' + paramValue + ' ');
+ var $input = (0, _jquery2.default)(paramTemplate);
+
+ paramCountAtLink++;
+
+ $paramLink.appendTo(elem);
+ $input.appendTo(elem);
+
+ $input.blur(_lodash2.default.partial(inputBlur, index));
+ $input.keyup(inputKeyDown);
+ $input.keypress(_lodash2.default.partial(inputKeyPress, index));
+ $paramLink.click(_lodash2.default.partial(clickFuncParam, index));
+
+ if (funcDef.params[index].options) {
+ addTypeahead($input, index);
+ }
+ });
+
+ (0, _jquery2.default)(') ').appendTo(elem);
+
+ $compile(elem.contents())($scope);
+ }
+
+ function ifJustAddedFocusFistParam() {
+ if ($scope.func.added) {
+ $scope.func.added = false;
+ setTimeout(function () {
+ elem.find('.graphite-func-param-link').first().click();
+ }, 10);
+ }
+ }
+
+ function registerFuncControlsToggle() {
+ $funcLink.click(toggleFuncControls);
+ }
+
+ function registerFuncControlsActions() {
+ $funcControls.click(function (e) {
+ var $target = (0, _jquery2.default)(e.target);
+ if ($target.hasClass('fa-remove')) {
+ toggleFuncControls();
+ $scope.$apply(function () {
+ ctrl.removeFunction($scope.func);
+ });
+ return;
+ }
+
+ if ($target.hasClass('fa-arrow-left')) {
+ $scope.$apply(function () {
+ _lodash2.default.move($scope.target.functions, $scope.$index, $scope.$index - 1);
+ ctrl.targetChanged();
+ });
+ return;
+ }
+
+ if ($target.hasClass('fa-arrow-right')) {
+ $scope.$apply(function () {
+ _lodash2.default.move($scope.target.functions, $scope.$index, $scope.$index + 1);
+ ctrl.targetChanged();
+ });
+ return;
+ }
+
+ if ($target.hasClass('fa-question-circle')) {
+ var docSite = "http://docs.grafana-zabbix.org/reference/functions/";
+ window.open(docSite + '#' + funcDef.name.toLowerCase(), '_blank');
+ return;
+ }
+ });
+ }
+
+ function relink() {
+ elem.children().remove();
+
+ addElementsAndCompile();
+ ifJustAddedFocusFistParam();
+ registerFuncControlsToggle();
+ registerFuncControlsActions();
+ }
+
+ relink();
+ }
+ };
+});
diff --git a/dist/test/datasource-zabbix/metricFunctions.js b/dist/test/datasource-zabbix/metricFunctions.js
new file mode 100644
index 0000000..4ed2437
--- /dev/null
+++ b/dist/test/datasource-zabbix/metricFunctions.js
@@ -0,0 +1,357 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+exports.createFuncInstance = createFuncInstance;
+exports.getFuncDef = getFuncDef;
+exports.getCategories = getCategories;
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _jquery = require('jquery');
+
+var _jquery2 = _interopRequireDefault(_jquery);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var index = [];
+var categories = {
+ Transform: [],
+ Aggregate: [],
+ Filter: [],
+ Trends: [],
+ Time: [],
+ Alias: [],
+ Special: []
+};
+
+function addFuncDef(funcDef) {
+ funcDef.params = funcDef.params || [];
+ funcDef.defaultParams = funcDef.defaultParams || [];
+
+ if (funcDef.category) {
+ categories[funcDef.category].push(funcDef);
+ }
+ index[funcDef.name] = funcDef;
+ index[funcDef.shortName || funcDef.name] = funcDef;
+}
+
+// Transform
+
+addFuncDef({
+ name: 'groupBy',
+ category: 'Transform',
+ params: [{ name: 'interval', type: 'string' }, { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }],
+ defaultParams: ['1m', 'avg']
+});
+
+addFuncDef({
+ name: 'scale',
+ category: 'Transform',
+ params: [{ name: 'factor', type: 'float', options: [100, 0.01, 10, -1] }],
+ defaultParams: [100]
+});
+
+addFuncDef({
+ name: 'delta',
+ category: 'Transform',
+ params: [],
+ defaultParams: []
+});
+
+addFuncDef({
+ name: 'rate',
+ category: 'Transform',
+ params: [],
+ defaultParams: []
+});
+
+addFuncDef({
+ name: 'movingAverage',
+ category: 'Transform',
+ params: [{ name: 'factor', type: 'int', options: [6, 10, 60, 100, 600] }],
+ defaultParams: [10]
+});
+
+addFuncDef({
+ name: 'exponentialMovingAverage',
+ category: 'Transform',
+ params: [{ name: 'smoothing', type: 'float', options: [6, 10, 60, 100, 600] }],
+ defaultParams: [0.2]
+});
+
+// Aggregate
+
+addFuncDef({
+ name: 'sumSeries',
+ category: 'Aggregate',
+ params: [],
+ defaultParams: []
+});
+
+addFuncDef({
+ name: 'median',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }],
+ defaultParams: ['1m']
+});
+
+addFuncDef({
+ name: 'average',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }],
+ defaultParams: ['1m']
+});
+
+addFuncDef({
+ name: 'percentil',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }, { name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] }],
+ defaultParams: ['1m', 95]
+});
+
+addFuncDef({
+ name: 'min',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }],
+ defaultParams: ['1m']
+});
+
+addFuncDef({
+ name: 'max',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }],
+ defaultParams: ['1m']
+});
+
+addFuncDef({
+ name: 'sum',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }],
+ defaultParams: ['1m']
+});
+
+addFuncDef({
+ name: 'count',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }],
+ defaultParams: ['1m']
+});
+
+addFuncDef({
+ name: 'aggregateBy',
+ category: 'Aggregate',
+ params: [{ name: 'interval', type: 'string' }, { name: 'function', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }],
+ defaultParams: ['1m', 'avg']
+});
+
+// Filter
+
+addFuncDef({
+ name: 'top',
+ category: 'Filter',
+ params: [{ name: 'number', type: 'int' }, { name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }],
+ defaultParams: [5, 'avg']
+});
+
+addFuncDef({
+ name: 'bottom',
+ category: 'Filter',
+ params: [{ name: 'number', type: 'int' }, { name: 'value', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count', 'median'] }],
+ defaultParams: [5, 'avg']
+});
+
+addFuncDef({
+ name: 'sortSeries',
+ category: 'Filter',
+ params: [{ name: 'direction', type: 'string', options: ['asc', 'desc'] }],
+ defaultParams: ['asc']
+});
+
+// Trends
+
+addFuncDef({
+ name: 'trendValue',
+ category: 'Trends',
+ params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
+ defaultParams: ['avg']
+});
+
+// Time
+
+addFuncDef({
+ name: 'timeShift',
+ category: 'Time',
+ params: [{ name: 'interval', type: 'string', options: ['24h', '7d', '1M', '+24h', '-24h'] }],
+ defaultParams: ['24h']
+});
+
+//Alias
+
+addFuncDef({
+ name: 'setAlias',
+ category: 'Alias',
+ params: [{ name: 'alias', type: 'string' }],
+ defaultParams: []
+});
+
+addFuncDef({
+ name: 'setAliasByRegex',
+ category: 'Alias',
+ params: [{ name: 'aliasByRegex', type: 'string' }],
+ defaultParams: []
+});
+
+addFuncDef({
+ name: 'replaceAlias',
+ category: 'Alias',
+ params: [{ name: 'regexp', type: 'string' }, { name: 'newAlias', type: 'string' }],
+ defaultParams: ['/(.*)/', '$1']
+});
+
+// Special
+addFuncDef({
+ name: 'consolidateBy',
+ category: 'Special',
+ params: [{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }],
+ defaultParams: ['avg']
+});
+
+_lodash2.default.each(categories, function (funcList, catName) {
+ categories[catName] = _lodash2.default.sortBy(funcList, 'name');
+});
+
+var FuncInstance = function () {
+ function FuncInstance(funcDef, params) {
+ _classCallCheck(this, FuncInstance);
+
+ this.def = funcDef;
+
+ if (params) {
+ this.params = params;
+ } else {
+ // Create with default params
+ this.params = [];
+ this.params = funcDef.defaultParams.slice(0);
+ }
+
+ this.updateText();
+ }
+
+ _createClass(FuncInstance, [{
+ key: 'bindFunction',
+ value: function bindFunction(metricFunctions) {
+ var func = metricFunctions[this.def.name];
+ if (func) {
+
+ // Bind function arguments
+ var bindedFunc = func;
+ var param;
+ for (var i = 0; i < this.params.length; i++) {
+ param = this.params[i];
+
+ // Convert numeric params
+ if (this.def.params[i].type === 'int' || this.def.params[i].type === 'float') {
+ param = Number(param);
+ }
+ bindedFunc = _lodash2.default.partial(bindedFunc, param);
+ }
+ return bindedFunc;
+ } else {
+ throw { message: 'Method not found ' + this.def.name };
+ }
+ }
+ }, {
+ key: 'render',
+ value: function render(metricExp) {
+ var str = this.def.name + '(';
+ var parameters = _lodash2.default.map(this.params, function (value, index) {
+
+ var 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' && _jquery2.default.isNumeric(value)) {
+ return value;
+ }
+
+ return "'" + value + "'";
+ }, this);
+
+ if (metricExp) {
+ parameters.unshift(metricExp);
+ }
+
+ return str + parameters.join(', ') + ')';
+ }
+ }, {
+ key: '_hasMultipleParamsInString',
+ value: function _hasMultipleParamsInString(strValue, index) {
+ if (strValue.indexOf(',') === -1) {
+ return false;
+ }
+
+ return this.def.params[index + 1] && this.def.params[index + 1].optional;
+ }
+ }, {
+ key: 'updateParam',
+ value: function updateParam(strValue, index) {
+ // handle optional parameters
+ // if string contains ',' and next param is optional, split and update both
+ if (this._hasMultipleParamsInString(strValue, index)) {
+ _lodash2.default.each(strValue.split(','), function (partVal, idx) {
+ this.updateParam(partVal.trim(), idx);
+ }, this);
+ return;
+ }
+
+ if (strValue === '' && this.def.params[index].optional) {
+ this.params.splice(index, 1);
+ } else {
+ this.params[index] = strValue;
+ }
+
+ this.updateText();
+ }
+ }, {
+ key: 'updateText',
+ value: function updateText() {
+ if (this.params.length === 0) {
+ this.text = this.def.name + '()';
+ return;
+ }
+
+ var text = this.def.name + '(';
+ text += this.params.join(', ');
+ text += ')';
+ this.text = text;
+ }
+ }]);
+
+ return FuncInstance;
+}();
+
+function createFuncInstance(funcDef, params) {
+ if (_lodash2.default.isString(funcDef)) {
+ if (!index[funcDef]) {
+ throw { message: 'Method not found ' + name };
+ }
+ funcDef = index[funcDef];
+ }
+ return new FuncInstance(funcDef, params);
+}
+
+function getFuncDef(name) {
+ return index[name];
+}
+
+function getCategories() {
+ return categories;
+}
diff --git a/dist/test/datasource-zabbix/migrations.js b/dist/test/datasource-zabbix/migrations.js
new file mode 100644
index 0000000..dc422c0
--- /dev/null
+++ b/dist/test/datasource-zabbix/migrations.js
@@ -0,0 +1,48 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.isGrafana2target = isGrafana2target;
+exports.migrateFrom2To3version = migrateFrom2To3version;
+exports.migrate = migrate;
+/**
+ * Query format migration.
+ * This module can detect query format version and make migration.
+ */
+
+function isGrafana2target(target) {
+ if (!target.mode || target.mode === 0 || target.mode === 2) {
+ if ((target.hostFilter || target.itemFilter || target.downsampleFunction || target.host && target.host.host) && target.item.filter === undefined && target.host.filter === undefined) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+function migrateFrom2To3version(target) {
+ target.group.filter = target.group.name === "*" ? "/.*/" : target.group.name;
+ target.host.filter = target.host.name === "*" ? convertToRegex(target.hostFilter) : target.host.name;
+ target.application.filter = target.application.name === "*" ? "" : target.application.name;
+ target.item.filter = target.item.name === "All" ? convertToRegex(target.itemFilter) : target.item.name;
+ return target;
+}
+
+function migrate(target) {
+ if (isGrafana2target(target)) {
+ return migrateFrom2To3version(target);
+ } else {
+ return target;
+ }
+}
+
+function convertToRegex(str) {
+ if (str) {
+ return '/' + str + '/';
+ } else {
+ return '/.*/';
+ }
+}
diff --git a/dist/test/datasource-zabbix/module.js b/dist/test/datasource-zabbix/module.js
new file mode 100644
index 0000000..6a44f8d
--- /dev/null
+++ b/dist/test/datasource-zabbix/module.js
@@ -0,0 +1,32 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.AnnotationsQueryCtrl = exports.QueryOptionsCtrl = exports.QueryCtrl = exports.ConfigCtrl = exports.Datasource = undefined;
+
+var _datasource = require('./datasource');
+
+var _query = require('./query.controller');
+
+var _config = require('./config.controller');
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var ZabbixQueryOptionsController = function ZabbixQueryOptionsController() {
+ _classCallCheck(this, ZabbixQueryOptionsController);
+};
+
+ZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html';
+
+var ZabbixAnnotationsQueryController = function ZabbixAnnotationsQueryController() {
+ _classCallCheck(this, ZabbixAnnotationsQueryController);
+};
+
+ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html';
+
+exports.Datasource = _datasource.ZabbixAPIDatasource;
+exports.ConfigCtrl = _config.ZabbixDSConfigController;
+exports.QueryCtrl = _query.ZabbixQueryController;
+exports.QueryOptionsCtrl = ZabbixQueryOptionsController;
+exports.AnnotationsQueryCtrl = ZabbixAnnotationsQueryController;
diff --git a/dist/test/datasource-zabbix/query.controller.js b/dist/test/datasource-zabbix/query.controller.js
new file mode 100644
index 0000000..da9b36e
--- /dev/null
+++ b/dist/test/datasource-zabbix/query.controller.js
@@ -0,0 +1,393 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ZabbixQueryController = undefined;
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _sdk = require('app/plugins/sdk');
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _constants = require('./constants');
+
+var c = _interopRequireWildcard(_constants);
+
+var _utils = require('./utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+var _metricFunctions = require('./metricFunctions');
+
+var metricFunctions = _interopRequireWildcard(_metricFunctions);
+
+var _migrations = require('./migrations');
+
+var migrations = _interopRequireWildcard(_migrations);
+
+require('./add-metric-function.directive');
+
+require('./metric-function-editor.directive');
+
+require('./css/query-editor.css!');
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var ZabbixQueryController = exports.ZabbixQueryController = function (_QueryCtrl) {
+ _inherits(ZabbixQueryController, _QueryCtrl);
+
+ // ZabbixQueryCtrl constructor
+ function ZabbixQueryController($scope, $injector, $rootScope, $sce, templateSrv) {
+ _classCallCheck(this, ZabbixQueryController);
+
+ var _this = _possibleConstructorReturn(this, (ZabbixQueryController.__proto__ || Object.getPrototypeOf(ZabbixQueryController)).call(this, $scope, $injector));
+
+ _this.zabbix = _this.datasource.zabbix;
+
+ // Use custom format for template variables
+ _this.replaceTemplateVars = _this.datasource.replaceTemplateVars;
+ _this.templateSrv = templateSrv;
+
+ _this.editorModes = [{ value: 'num', text: 'Metrics', mode: c.MODE_METRICS }, { value: 'text', text: 'Text', mode: c.MODE_TEXT }, { value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE }, { value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID }, { value: 'triggers', text: 'Triggers', mode: c.MODE_TRIGGERS }];
+
+ _this.$scope.editorMode = {
+ METRICS: c.MODE_METRICS,
+ TEXT: c.MODE_TEXT,
+ ITSERVICE: c.MODE_ITSERVICE,
+ ITEMID: c.MODE_ITEMID,
+ TRIGGERS: c.MODE_TRIGGERS
+ };
+
+ _this.slaPropertyList = [{ name: "Status", property: "status" }, { name: "SLA", property: "sla" }, { name: "OK time", property: "okTime" }, { name: "Problem time", property: "problemTime" }, { name: "Down time", property: "downtimeTime" }];
+
+ _this.ackFilters = [{ text: 'all triggers', value: 2 }, { text: 'unacknowledged', value: 0 }, { text: 'acknowledged', value: 1 }];
+
+ _this.triggerSeverity = c.TRIGGER_SEVERITY;
+
+ // Map functions for bs-typeahead
+ _this.getGroupNames = _lodash2.default.bind(_this.getMetricNames, _this, 'groupList');
+ _this.getHostNames = _lodash2.default.bind(_this.getMetricNames, _this, 'hostList', true);
+ _this.getApplicationNames = _lodash2.default.bind(_this.getMetricNames, _this, 'appList');
+ _this.getItemNames = _lodash2.default.bind(_this.getMetricNames, _this, 'itemList');
+ _this.getITServices = _lodash2.default.bind(_this.getMetricNames, _this, 'itServiceList');
+ _this.getVariables = _lodash2.default.bind(_this.getTemplateVariables, _this);
+
+ // Update metric suggestion when template variable was changed
+ $rootScope.$on('template-variable-value-updated', function () {
+ return _this.onVariableChange();
+ });
+
+ // Update metrics when item selected from dropdown
+ $scope.$on('typeahead-updated', function () {
+ _this.onTargetBlur();
+ });
+
+ _this.init = function () {
+ var target = this.target;
+
+ // Migrate old targets
+ target = migrations.migrate(target);
+
+ var scopeDefaults = {
+ metric: {},
+ oldTarget: _lodash2.default.cloneDeep(this.target),
+ queryOptionsText: this.renderQueryOptionsText()
+ };
+ _lodash2.default.defaults(this, scopeDefaults);
+
+ // Load default values
+ var targetDefaults = {
+ 'mode': c.MODE_METRICS,
+ 'group': { 'filter': "" },
+ 'host': { 'filter': "" },
+ 'application': { 'filter': "" },
+ 'item': { 'filter': "" },
+ 'functions': [],
+ 'triggers': {
+ 'count': true,
+ 'minSeverity': 3,
+ 'acknowledged': 2
+ },
+ 'options': {
+ 'showDisabledItems': false
+ }
+ };
+ _lodash2.default.defaults(target, targetDefaults);
+
+ // Create function instances from saved JSON
+ target.functions = _lodash2.default.map(target.functions, function (func) {
+ return metricFunctions.createFuncInstance(func.def, func.params);
+ });
+
+ if (target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT || target.mode === c.MODE_TRIGGERS) {
+ this.initFilters();
+ } else if (target.mode === c.MODE_ITSERVICE) {
+ _lodash2.default.defaults(target, { slaProperty: { name: "SLA", property: "sla" } });
+ this.suggestITServices();
+ }
+ };
+
+ _this.init();
+ _this.queryOptionsText = _this.renderQueryOptionsText();
+ return _this;
+ }
+
+ _createClass(ZabbixQueryController, [{
+ key: 'initFilters',
+ value: function initFilters() {
+ var itemtype = _lodash2.default.find(this.editorModes, { 'mode': this.target.mode });
+ itemtype = itemtype ? itemtype.value : null;
+ return Promise.all([this.suggestGroups(), this.suggestHosts(), this.suggestApps(), this.suggestItems(itemtype)]);
+ }
+
+ // Get list of metric names for bs-typeahead directive
+
+ }, {
+ key: 'getMetricNames',
+ value: function getMetricNames(metricList, addAllValue) {
+ var metrics = _lodash2.default.uniq(_lodash2.default.map(this.metric[metricList], 'name'));
+
+ // Add template variables
+ _lodash2.default.forEach(this.templateSrv.variables, function (variable) {
+ metrics.unshift('$' + variable.name);
+ });
+
+ if (addAllValue) {
+ metrics.unshift('/.*/');
+ }
+
+ return metrics;
+ }
+ }, {
+ key: 'getTemplateVariables',
+ value: function getTemplateVariables() {
+ return _lodash2.default.map(this.templateSrv.variables, function (variable) {
+ return '$' + variable.name;
+ });
+ }
+ }, {
+ key: 'suggestGroups',
+ value: function suggestGroups() {
+ var _this2 = this;
+
+ return this.zabbix.getAllGroups().then(function (groups) {
+ _this2.metric.groupList = groups;
+ return groups;
+ });
+ }
+ }, {
+ key: 'suggestHosts',
+ value: function suggestHosts() {
+ var _this3 = this;
+
+ var groupFilter = this.replaceTemplateVars(this.target.group.filter);
+ return this.zabbix.getAllHosts(groupFilter).then(function (hosts) {
+ _this3.metric.hostList = hosts;
+ return hosts;
+ });
+ }
+ }, {
+ key: 'suggestApps',
+ value: function suggestApps() {
+ var _this4 = this;
+
+ var groupFilter = this.replaceTemplateVars(this.target.group.filter);
+ var hostFilter = this.replaceTemplateVars(this.target.host.filter);
+ return this.zabbix.getAllApps(groupFilter, hostFilter).then(function (apps) {
+ _this4.metric.appList = apps;
+ return apps;
+ });
+ }
+ }, {
+ key: 'suggestItems',
+ value: function suggestItems() {
+ var _this5 = this;
+
+ var itemtype = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'num';
+
+ var groupFilter = this.replaceTemplateVars(this.target.group.filter);
+ var hostFilter = this.replaceTemplateVars(this.target.host.filter);
+ var appFilter = this.replaceTemplateVars(this.target.application.filter);
+ var options = {
+ itemtype: itemtype,
+ showDisabledItems: this.target.options.showDisabledItems
+ };
+
+ return this.zabbix.getAllItems(groupFilter, hostFilter, appFilter, options).then(function (items) {
+ _this5.metric.itemList = items;
+ return items;
+ });
+ }
+ }, {
+ key: 'suggestITServices',
+ value: function suggestITServices() {
+ var _this6 = this;
+
+ return this.zabbix.getITService().then(function (itservices) {
+ _this6.metric.itServiceList = itservices;
+ return itservices;
+ });
+ }
+ }, {
+ key: 'isRegex',
+ value: function isRegex(str) {
+ return utils.isRegex(str);
+ }
+ }, {
+ key: 'isVariable',
+ value: function isVariable(str) {
+ return utils.isTemplateVariable(str, this.templateSrv.variables);
+ }
+ }, {
+ key: 'onTargetBlur',
+ value: function onTargetBlur() {
+ var newTarget = _lodash2.default.cloneDeep(this.target);
+ if (!_lodash2.default.isEqual(this.oldTarget, this.target)) {
+ this.oldTarget = newTarget;
+ this.targetChanged();
+ }
+ }
+ }, {
+ key: 'onVariableChange',
+ value: function onVariableChange() {
+ if (this.isContainsVariables()) {
+ this.targetChanged();
+ }
+ }
+
+ /**
+ * Check query for template variables
+ */
+
+ }, {
+ key: 'isContainsVariables',
+ value: function isContainsVariables() {
+ var _this7 = this;
+
+ return _lodash2.default.some(['group', 'host', 'application'], function (field) {
+ if (_this7.target[field] && _this7.target[field].filter) {
+ return utils.isTemplateVariable(_this7.target[field].filter, _this7.templateSrv.variables);
+ } else {
+ return false;
+ }
+ });
+ }
+ }, {
+ key: 'parseTarget',
+ value: function parseTarget() {}
+ // Parse target
+
+
+ // Validate target and set validation info
+
+ }, {
+ key: 'validateTarget',
+ value: function validateTarget() {
+ // validate
+ }
+ }, {
+ key: 'targetChanged',
+ value: function targetChanged() {
+ this.initFilters();
+ this.parseTarget();
+ this.panelCtrl.refresh();
+ }
+ }, {
+ key: 'addFunction',
+ value: function addFunction(funcDef) {
+ var newFunc = metricFunctions.createFuncInstance(funcDef);
+ newFunc.added = true;
+ this.target.functions.push(newFunc);
+
+ this.moveAliasFuncLast();
+
+ if (newFunc.params.length && newFunc.added || newFunc.def.params.length === 0) {
+ this.targetChanged();
+ }
+ }
+ }, {
+ key: 'removeFunction',
+ value: function removeFunction(func) {
+ this.target.functions = _lodash2.default.without(this.target.functions, func);
+ this.targetChanged();
+ }
+ }, {
+ key: 'moveAliasFuncLast',
+ value: function moveAliasFuncLast() {
+ var aliasFunc = _lodash2.default.find(this.target.functions, function (func) {
+ return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric';
+ });
+
+ if (aliasFunc) {
+ this.target.functions = _lodash2.default.without(this.target.functions, aliasFunc);
+ this.target.functions.push(aliasFunc);
+ }
+ }
+ }, {
+ key: 'toggleQueryOptions',
+ value: function toggleQueryOptions() {
+ this.showQueryOptions = !this.showQueryOptions;
+ }
+ }, {
+ key: 'onQueryOptionChange',
+ value: function onQueryOptionChange() {
+ this.queryOptionsText = this.renderQueryOptionsText();
+ this.onTargetBlur();
+ }
+ }, {
+ key: 'renderQueryOptionsText',
+ value: function renderQueryOptionsText() {
+ var optionsMap = {
+ showDisabledItems: "Show disabled items"
+ };
+ var options = [];
+ _lodash2.default.forOwn(this.target.options, function (value, key) {
+ if (value) {
+ 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);
+ }
+ }
+ });
+ return "Options: " + options.join(', ');
+ }
+
+ /**
+ * Switch query editor to specified mode.
+ * Modes:
+ * 0 - items
+ * 1 - IT services
+ * 2 - Text metrics
+ */
+
+ }, {
+ key: 'switchEditorMode',
+ value: function switchEditorMode(mode) {
+ this.target.mode = mode;
+ this.init();
+ this.targetChanged();
+ }
+ }]);
+
+ return ZabbixQueryController;
+}(_sdk.QueryCtrl);
+
+// Set templateUrl as static property
+
+
+ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html';
diff --git a/dist/test/datasource-zabbix/responseHandler.js b/dist/test/datasource-zabbix/responseHandler.js
new file mode 100644
index 0000000..beded2b
--- /dev/null
+++ b/dist/test/datasource-zabbix/responseHandler.js
@@ -0,0 +1,210 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _table_model = require('app/core/table_model');
+
+var _table_model2 = _interopRequireDefault(_table_model);
+
+var _constants = require('./constants');
+
+var c = _interopRequireWildcard(_constants);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+/**
+ * Convert Zabbix API history.get response to Grafana format
+ *
+ * @return {Array} Array of timeseries in Grafana format
+ * {
+ * target: "Metric name",
+ * datapoints: [[, ], ...]
+ * }
+ */
+function convertHistory(history, items, addHostName, convertPointCallback) {
+ /**
+ * Response should be in the format:
+ * data: [
+ * {
+ * target: "Metric name",
+ * datapoints: [[, ], ...]
+ * }, ...
+ * ]
+ */
+
+ // Group history by itemid
+ var grouped_history = _lodash2.default.groupBy(history, 'itemid');
+ var hosts = _lodash2.default.uniqBy(_lodash2.default.flatten(_lodash2.default.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
+
+ return _lodash2.default.map(grouped_history, function (hist, itemid) {
+ var item = _lodash2.default.find(items, { 'itemid': itemid });
+ var alias = item.name;
+ if (_lodash2.default.keys(hosts).length > 1 && addHostName) {
+ //only when actual multi hosts selected
+ var host = _lodash2.default.find(hosts, { 'hostid': item.hostid });
+ alias = host.name + ": " + alias;
+ }
+ return {
+ target: alias,
+ datapoints: _lodash2.default.map(hist, convertPointCallback)
+ };
+ });
+}
+
+function handleHistory(history, items) {
+ var addHostName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
+
+ return convertHistory(history, items, addHostName, convertHistoryPoint);
+}
+
+function handleTrends(history, items, valueType) {
+ var addHostName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
+
+ var convertPointCallback = _lodash2.default.partial(convertTrendPoint, valueType);
+ return convertHistory(history, items, addHostName, convertPointCallback);
+}
+
+function handleText(history, items, target) {
+ var addHostName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
+
+ var convertTextCallback = _lodash2.default.partial(convertText, target);
+ return convertHistory(history, items, addHostName, convertTextCallback);
+}
+
+function convertText(target, point) {
+ var value = point.value;
+
+ // Regex-based extractor
+ if (target.textFilter) {
+ value = extractText(point.value, target.textFilter, target.useCaptureGroups);
+ }
+
+ return [value, point.clock * 1000 + Math.round(point.ns / 1000000)];
+}
+
+function extractText(str, pattern, useCaptureGroups) {
+ var extractPattern = new RegExp(pattern);
+ var extractedValue = extractPattern.exec(str);
+ if (extractedValue) {
+ if (useCaptureGroups) {
+ extractedValue = extractedValue[1];
+ } else {
+ extractedValue = extractedValue[0];
+ }
+ }
+ return extractedValue;
+}
+
+function handleSLAResponse(itservice, slaProperty, slaObject) {
+ var targetSLA = slaObject[itservice.serviceid].sla[0];
+ if (slaProperty.property === 'status') {
+ var targetStatus = parseInt(slaObject[itservice.serviceid].status);
+ return {
+ target: itservice.name + ' ' + slaProperty.name,
+ datapoints: [[targetStatus, targetSLA.to * 1000]]
+ };
+ } else {
+ return {
+ target: itservice.name + ' ' + slaProperty.name,
+ datapoints: [[targetSLA[slaProperty.property], targetSLA.from * 1000], [targetSLA[slaProperty.property], targetSLA.to * 1000]]
+ };
+ }
+}
+
+function handleTriggersResponse(triggers, timeRange) {
+ if (_lodash2.default.isNumber(triggers)) {
+ return {
+ target: "triggers count",
+ datapoints: [[triggers, timeRange[1] * 1000]]
+ };
+ } else {
+ var stats = getTriggerStats(triggers);
+ var table = new _table_model2.default();
+ table.addColumn({ text: 'Host group' });
+ _lodash2.default.each(_lodash2.default.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), function (severity) {
+ table.addColumn({ text: severity.text });
+ });
+ _lodash2.default.each(stats, function (severity_stats, group) {
+ var row = _lodash2.default.map(_lodash2.default.orderBy(_lodash2.default.toPairs(severity_stats), function (s) {
+ return s[0];
+ }, ['desc']), function (s) {
+ return s[1];
+ });
+ row = _lodash2.default.concat.apply(_lodash2.default, [[group]].concat(_toConsumableArray(row)));
+ table.rows.push(row);
+ });
+ return table;
+ }
+}
+
+function getTriggerStats(triggers) {
+ var groups = _lodash2.default.uniq(_lodash2.default.flattenDeep(_lodash2.default.map(triggers, function (trigger) {
+ return _lodash2.default.map(trigger.groups, 'name');
+ })));
+ // let severity = _.map(c.TRIGGER_SEVERITY, 'text');
+ var stats = {};
+ _lodash2.default.each(groups, function (group) {
+ stats[group] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; // severity:count
+ });
+ _lodash2.default.each(triggers, function (trigger) {
+ _lodash2.default.each(trigger.groups, function (group) {
+ stats[group.name][trigger.priority]++;
+ });
+ });
+ return stats;
+}
+
+function convertHistoryPoint(point) {
+ // Value must be a number for properly work
+ return [Number(point.value), point.clock * 1000 + Math.round(point.ns / 1000000)];
+}
+
+function convertTrendPoint(valueType, point) {
+ var value;
+ switch (valueType) {
+ case "min":
+ value = point.value_min;
+ break;
+ case "max":
+ value = point.value_max;
+ break;
+ case "avg":
+ value = point.value_avg;
+ break;
+ case "sum":
+ value = point.value_sum;
+ break;
+ case "count":
+ value = point.value_count;
+ break;
+ default:
+ value = point.value_avg;
+ }
+
+ return [Number(value), point.clock * 1000];
+}
+
+exports.default = {
+ handleHistory: handleHistory,
+ convertHistory: convertHistory,
+ handleTrends: handleTrends,
+ handleText: handleText,
+ handleSLAResponse: handleSLAResponse,
+ handleTriggersResponse: handleTriggersResponse
+};
+
+// Fix for backward compatibility with lodash 2.4
+
+if (!_lodash2.default.uniqBy) {
+ _lodash2.default.uniqBy = _lodash2.default.uniq;
+}
diff --git a/dist/test/datasource-zabbix/specs/datasource.spec.js b/dist/test/datasource-zabbix/specs/datasource.spec.js
new file mode 100644
index 0000000..f4101e6
--- /dev/null
+++ b/dist/test/datasource-zabbix/specs/datasource.spec.js
@@ -0,0 +1,424 @@
+"use strict";
+
+var _lodash = require("lodash");
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _q = require("q");
+
+var _q2 = _interopRequireDefault(_q);
+
+var _module = require("../module");
+
+var _datasource = require("../datasource");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+describe('ZabbixDatasource', function () {
+ var ctx = {};
+
+ beforeEach(function () {
+ ctx.instanceSettings = {
+ jsonData: {
+ alerting: true,
+ username: 'zabbix',
+ password: 'zabbix',
+ trends: true,
+ trendsFrom: '14d',
+ trendsRange: '7d',
+ dbConnection: {
+ enabled: false
+ }
+ }
+ };
+ ctx.templateSrv = {};
+ ctx.alertSrv = {};
+ ctx.dashboardSrv = {};
+ ctx.zabbixAlertingSrv = {};
+ ctx.zabbix = function () {};
+
+ ctx.ds = new _module.Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.alertSrv, ctx.dashboardSrv, ctx.zabbixAlertingSrv, ctx.zabbix);
+ });
+
+ describe('When querying data', function () {
+ beforeEach(function () {
+ ctx.ds.replaceTemplateVars = function (str) {
+ return str;
+ };
+ ctx.ds.alertQuery = function () {
+ return _q2.default.when([]);
+ };
+ });
+
+ ctx.options = {
+ targets: [{
+ group: { filter: "" },
+ host: { filter: "" },
+ application: { filter: "" },
+ item: { filter: "" }
+ }],
+ range: { from: 'now-7d', to: 'now' }
+ };
+
+ it('should return an empty array when no targets are set', function (done) {
+ var options = {
+ targets: [],
+ range: { from: 'now-6h', to: 'now' }
+ };
+ ctx.ds.query(options).then(function (result) {
+ expect(result.data.length).toBe(0);
+ done();
+ });
+ });
+
+ it('should use trends if it enabled and time more than trendsFrom', function (done) {
+ var ranges = ['now-7d', 'now-168h', 'now-1M', 'now-1y'];
+
+ _lodash2.default.forEach(ranges, function (range) {
+ ctx.options.range.from = range;
+ ctx.ds.queryNumericData = jest.fn();
+ ctx.ds.query(ctx.options);
+
+ // Check that useTrends options is true
+ var callArgs = ctx.ds.queryNumericData.mock.calls[0];
+ expect(callArgs[2]).toBe(true);
+ ctx.ds.queryNumericData.mockClear();
+ });
+
+ done();
+ });
+
+ it('shouldnt use trends if it enabled and time less than trendsFrom', function (done) {
+ var ranges = ['now-6d', 'now-167h', 'now-1h', 'now-30m', 'now-30s'];
+
+ _lodash2.default.forEach(ranges, function (range) {
+ ctx.options.range.from = range;
+ ctx.ds.queryNumericData = jest.fn();
+ ctx.ds.query(ctx.options);
+
+ // Check that useTrends options is false
+ var callArgs = ctx.ds.queryNumericData.mock.calls[0];
+ expect(callArgs[2]).toBe(false);
+ ctx.ds.queryNumericData.mockClear();
+ });
+ done();
+ });
+ });
+
+ describe('When replacing template variables', function () {
+
+ function testReplacingVariable(target, varValue, expectedResult, done) {
+ ctx.ds.templateSrv.replace = function () {
+ return (0, _datasource.zabbixTemplateFormat)(varValue);
+ };
+
+ var result = ctx.ds.replaceTemplateVars(target);
+ expect(result).toBe(expectedResult);
+ done();
+ }
+
+ /*
+ * Alphanumerics, spaces, dots, dashes and underscores
+ * are allowed in Zabbix host name.
+ * 'AaBbCc0123 .-_'
+ */
+ it('should return properly escaped regex', function (done) {
+ var target = '$host';
+ var template_var_value = 'AaBbCc0123 .-_';
+ var expected_result = '/^AaBbCc0123 \\.-_$/';
+
+ testReplacingVariable(target, template_var_value, expected_result, done);
+ });
+
+ /*
+ * Single-value variable
+ * $host = backend01
+ * $host => /^backend01|backend01$/
+ */
+ it('should return proper regex for single value', function (done) {
+ var target = '$host';
+ var template_var_value = 'backend01';
+ var expected_result = '/^backend01$/';
+
+ testReplacingVariable(target, template_var_value, expected_result, done);
+ });
+
+ /*
+ * Multi-value variable
+ * $host = [backend01, backend02]
+ * $host => /^(backend01|backend01)$/
+ */
+ it('should return proper regex for multi-value', function (done) {
+ var target = '$host';
+ var template_var_value = ['backend01', 'backend02'];
+ var expected_result = '/^(backend01|backend02)$/';
+
+ testReplacingVariable(target, template_var_value, expected_result, done);
+ });
+ });
+
+ describe('When invoking metricFindQuery()', function () {
+ beforeEach(function () {
+ ctx.ds.replaceTemplateVars = function (str) {
+ return str;
+ };
+ ctx.ds.zabbix = {
+ getGroups: jest.fn().mockReturnValue(_q2.default.when([])),
+ getHosts: jest.fn().mockReturnValue(_q2.default.when([])),
+ getApps: jest.fn().mockReturnValue(_q2.default.when([])),
+ getItems: jest.fn().mockReturnValue(_q2.default.when([]))
+ };
+ });
+
+ it('should return groups', function (done) {
+ var tests = [{ query: '*', expect: '/.*/' }, { query: '', expect: '' }, { query: 'Backend', expect: 'Backend' }, { query: 'Back*', expect: 'Back*' }];
+
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = tests[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var test = _step.value;
+
+ ctx.ds.metricFindQuery(test.query);
+ expect(ctx.ds.zabbix.getGroups).toBeCalledWith(test.expect);
+ ctx.ds.zabbix.getGroups.mockClear();
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator.return) {
+ _iterator.return();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ done();
+ });
+
+ it('should return hosts', function (done) {
+ var tests = [{ query: '*.*', expect: ['/.*/', '/.*/'] }, { query: '.', expect: ['', ''] }, { query: 'Backend.*', expect: ['Backend', '/.*/'] }, { query: 'Back*.', expect: ['Back*', ''] }];
+
+ var _iteratorNormalCompletion2 = true;
+ var _didIteratorError2 = false;
+ var _iteratorError2 = undefined;
+
+ try {
+ for (var _iterator2 = tests[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+ var test = _step2.value;
+
+ ctx.ds.metricFindQuery(test.query);
+ expect(ctx.ds.zabbix.getHosts).toBeCalledWith(test.expect[0], test.expect[1]);
+ ctx.ds.zabbix.getHosts.mockClear();
+ }
+ } catch (err) {
+ _didIteratorError2 = true;
+ _iteratorError2 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion2 && _iterator2.return) {
+ _iterator2.return();
+ }
+ } finally {
+ if (_didIteratorError2) {
+ throw _iteratorError2;
+ }
+ }
+ }
+
+ done();
+ });
+
+ it('should return applications', function (done) {
+ var tests = [{ query: '*.*.*', expect: ['/.*/', '/.*/', '/.*/'] }, { query: '.*.', expect: ['', '/.*/', ''] }, { query: 'Backend.backend01.*', expect: ['Backend', 'backend01', '/.*/'] }, { query: 'Back*.*.', expect: ['Back*', '/.*/', ''] }];
+
+ var _iteratorNormalCompletion3 = true;
+ var _didIteratorError3 = false;
+ var _iteratorError3 = undefined;
+
+ try {
+ for (var _iterator3 = tests[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+ var test = _step3.value;
+
+ ctx.ds.metricFindQuery(test.query);
+ expect(ctx.ds.zabbix.getApps).toBeCalledWith(test.expect[0], test.expect[1], test.expect[2]);
+ ctx.ds.zabbix.getApps.mockClear();
+ }
+ } catch (err) {
+ _didIteratorError3 = true;
+ _iteratorError3 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion3 && _iterator3.return) {
+ _iterator3.return();
+ }
+ } finally {
+ if (_didIteratorError3) {
+ throw _iteratorError3;
+ }
+ }
+ }
+
+ done();
+ });
+
+ it('should return items', function (done) {
+ var tests = [{ query: '*.*.*.*', expect: ['/.*/', '/.*/', '', '/.*/'] }, { query: '.*.*.*', expect: ['', '/.*/', '', '/.*/'] }, { query: 'Backend.backend01.*.*', expect: ['Backend', 'backend01', '', '/.*/'] }, { query: 'Back*.*.cpu.*', expect: ['Back*', '/.*/', 'cpu', '/.*/'] }];
+
+ var _iteratorNormalCompletion4 = true;
+ var _didIteratorError4 = false;
+ var _iteratorError4 = undefined;
+
+ try {
+ for (var _iterator4 = tests[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
+ var test = _step4.value;
+
+ ctx.ds.metricFindQuery(test.query);
+ expect(ctx.ds.zabbix.getItems).toBeCalledWith(test.expect[0], test.expect[1], test.expect[2], test.expect[3]);
+ ctx.ds.zabbix.getItems.mockClear();
+ }
+ } catch (err) {
+ _didIteratorError4 = true;
+ _iteratorError4 = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion4 && _iterator4.return) {
+ _iterator4.return();
+ }
+ } finally {
+ if (_didIteratorError4) {
+ throw _iteratorError4;
+ }
+ }
+ }
+
+ done();
+ });
+
+ it('should invoke method with proper arguments', function (done) {
+ var query = '*.*';
+
+ ctx.ds.metricFindQuery(query);
+ expect(ctx.ds.zabbix.getHosts).toBeCalledWith('/.*/', '/.*/');
+ done();
+ });
+ });
+
+ describe('When querying alerts', function () {
+ var options = {};
+
+ beforeEach(function () {
+ ctx.ds.replaceTemplateVars = function (str) {
+ return str;
+ };
+
+ var targetItems = [{
+ "itemid": "1",
+ "name": "test item",
+ "key_": "test.key",
+ "value_type": "3",
+ "hostid": "10631",
+ "status": "0",
+ "state": "0",
+ "hosts": [{ "hostid": "10631", "name": "Test host" }],
+ "item": "Test item"
+ }];
+ ctx.ds.zabbix.getItemsFromTarget = function () {
+ return Promise.resolve(targetItems);
+ };
+
+ options = {
+ "panelId": 10,
+ "targets": [{
+ "application": { "filter": "" },
+ "group": { "filter": "Test group" },
+ "host": { "filter": "Test host" },
+ "item": { "filter": "Test item" }
+ }]
+ };
+ });
+
+ it('should return threshold when comparative symbol is `less than`', function () {
+
+ var itemTriggers = [{
+ "triggerid": "15383",
+ "priority": "4",
+ "expression": "{15915}<100"
+ }];
+
+ ctx.ds.zabbix.getAlerts = function () {
+ return Promise.resolve(itemTriggers);
+ };
+
+ return ctx.ds.alertQuery(options).then(function (resp) {
+ expect(resp.thresholds).toHaveLength(1);
+ expect(resp.thresholds[0]).toBe(100);
+ return resp;
+ });
+ });
+
+ it('should return threshold when comparative symbol is `less than or equal`', function () {
+
+ var itemTriggers = [{
+ "triggerid": "15383",
+ "priority": "4",
+ "expression": "{15915}<=100"
+ }];
+
+ ctx.ds.zabbix.getAlerts = function () {
+ return Promise.resolve(itemTriggers);
+ };
+
+ return ctx.ds.alertQuery(options).then(function (resp) {
+ expect(resp.thresholds.length).toBe(1);
+ expect(resp.thresholds[0]).toBe(100);
+ return resp;
+ });
+ });
+
+ it('should return threshold when comparative symbol is `greater than or equal`', function () {
+
+ var itemTriggers = [{
+ "triggerid": "15383",
+ "priority": "4",
+ "expression": "{15915}>=30"
+ }];
+
+ ctx.ds.zabbix.getAlerts = function () {
+ return Promise.resolve(itemTriggers);
+ };
+
+ return ctx.ds.alertQuery(options).then(function (resp) {
+ expect(resp.thresholds.length).toBe(1);
+ expect(resp.thresholds[0]).toBe(30);
+ return resp;
+ });
+ });
+
+ it('should return threshold when comparative symbol is `equal`', function () {
+
+ var itemTriggers = [{
+ "triggerid": "15383",
+ "priority": "4",
+ "expression": "{15915}=50"
+ }];
+
+ ctx.ds.zabbix.getAlerts = function () {
+ return Promise.resolve(itemTriggers);
+ };
+
+ return ctx.ds.alertQuery(options).then(function (resp) {
+ expect(resp.thresholds.length).toBe(1);
+ expect(resp.thresholds[0]).toBe(50);
+ return resp;
+ });
+ });
+ });
+});
diff --git a/dist/test/datasource-zabbix/specs/timeseries.spec.js b/dist/test/datasource-zabbix/specs/timeseries.spec.js
new file mode 100644
index 0000000..f5db4a6
--- /dev/null
+++ b/dist/test/datasource-zabbix/specs/timeseries.spec.js
@@ -0,0 +1,33 @@
+'use strict';
+
+var _timeseries = require('../timeseries');
+
+var _timeseries2 = _interopRequireDefault(_timeseries);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+describe('timeseries processing functions', function () {
+
+ describe('sumSeries()', function () {
+ it('should properly sum series', function (done) {
+ var series = [[[0, 1], [1, 2], [1, 3]], [[2, 1], [3, 2], [4, 3]]];
+
+ var expected = [[2, 1], [4, 2], [5, 3]];
+
+ var result = _timeseries2.default.sumSeries(series);
+ expect(result).toEqual(expected);
+ done();
+ });
+
+ it('should properly sum series with nulls', function (done) {
+ // issue #286
+ var series = [[[1, 1], [1, 2], [1, 3]], [[3, 2], [4, 3]]];
+
+ var expected = [[1, 1], [4, 2], [5, 3]];
+
+ var result = _timeseries2.default.sumSeries(series);
+ expect(result).toEqual(expected);
+ done();
+ });
+ });
+}); // import _ from 'lodash';
diff --git a/dist/test/datasource-zabbix/specs/utils.spec.js b/dist/test/datasource-zabbix/specs/utils.spec.js
new file mode 100644
index 0000000..42f2c67
--- /dev/null
+++ b/dist/test/datasource-zabbix/specs/utils.spec.js
@@ -0,0 +1,131 @@
+'use strict';
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _utils = require('../utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+describe('Utils', function () {
+
+ describe('expandItemName()', function () {
+
+ it('should properly expand unquoted params', function (done) {
+ var test_cases = [{
+ name: 'CPU $2 time',
+ key: 'system.cpu.util[,user,avg1]',
+ expected: "CPU user time"
+ }, {
+ name: 'CPU $2 time - $3',
+ key: 'system.cpu.util[,system,avg1]',
+ expected: "CPU system time - avg1"
+ }, {
+ name: 'CPU - $1 - $2 - $3',
+ key: 'system.cpu.util[,system,avg1]',
+ expected: "CPU - - system - avg1"
+ }];
+
+ _lodash2.default.each(test_cases, function (test_case) {
+ var expandedName = utils.expandItemName(test_case.name, test_case.key);
+ expect(expandedName).toBe(test_case.expected);
+ });
+ done();
+ });
+
+ it('should properly expand quoted params with commas', function (done) {
+ var test_cases = [{
+ name: 'CPU $2 time',
+ key: 'system.cpu.util["type=user,value=avg",user]',
+ expected: "CPU user time"
+ }, {
+ name: 'CPU $1 time',
+ key: 'system.cpu.util["type=user,value=avg","user"]',
+ expected: "CPU type=user,value=avg time"
+ }, {
+ name: 'CPU $1 time $3',
+ key: 'system.cpu.util["type=user,value=avg",,"user"]',
+ expected: "CPU type=user,value=avg time user"
+ }, {
+ name: 'CPU $1 $2 $3',
+ key: 'system.cpu.util["type=user,value=avg",time,"user"]',
+ expected: "CPU type=user,value=avg time user"
+ }];
+
+ _lodash2.default.each(test_cases, function (test_case) {
+ var expandedName = utils.expandItemName(test_case.name, test_case.key);
+ expect(expandedName).toBe(test_case.expected);
+ });
+ done();
+ });
+
+ it('should properly expand array params', function (done) {
+ var test_cases = [{
+ name: 'CPU $2 - $3 time',
+ key: 'system.cpu.util[,[user,system],avg1]',
+ expected: "CPU user,system - avg1 time"
+ }, {
+ name: 'CPU $2 - $3 time',
+ key: 'system.cpu.util[,["user,system",iowait],avg1]',
+ expected: 'CPU "user,system",iowait - avg1 time'
+ }, {
+ name: 'CPU - $2 - $3 - $4',
+ key: 'system.cpu.util[,[],["user,system",iowait],avg1]',
+ expected: 'CPU - - "user,system",iowait - avg1'
+ }];
+
+ _lodash2.default.each(test_cases, function (test_case) {
+ var expandedName = utils.expandItemName(test_case.name, test_case.key);
+ expect(expandedName).toBe(test_case.expected);
+ });
+ done();
+ });
+ });
+
+ describe('splitTemplateQuery()', function () {
+
+ // Backward compatibility
+ it('should properly split query in old format', function (done) {
+ var test_cases = [{
+ query: '/alu/./tw-(nyc|que|brx|dwt|brk)-sta_(w|d)*-alu-[0-9{2}/',
+ expected: ['/alu/', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9{2}/']
+ }, {
+ query: 'a.b.c.d',
+ expected: ['a', 'b', 'c', 'd']
+ }];
+
+ _lodash2.default.each(test_cases, function (test_case) {
+ var splitQuery = utils.splitTemplateQuery(test_case.query);
+ expect(splitQuery).toEqual(test_case.expected);
+ });
+ done();
+ });
+
+ it('should properly split query', function (done) {
+ var test_cases = [{
+ query: '{alu}{/tw-(nyc|que|brx|dwt|brk)-sta_(w|d)*-alu-[0-9]*/}',
+ expected: ['alu', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]*/']
+ }, {
+ query: '{alu}{/tw-(nyc|que|brx|dwt|brk)-sta_(w|d)*-alu-[0-9]{2}/}',
+ expected: ['alu', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]{2}/']
+ }, {
+ query: '{a}{b}{c}{d}',
+ expected: ['a', 'b', 'c', 'd']
+ }, {
+ query: '{a}{b.c.d}',
+ expected: ['a', 'b.c.d']
+ }];
+
+ _lodash2.default.each(test_cases, function (test_case) {
+ var splitQuery = utils.splitTemplateQuery(test_case.query);
+ expect(splitQuery).toEqual(test_case.expected);
+ });
+ done();
+ });
+ });
+});
diff --git a/dist/test/datasource-zabbix/timeseries.js b/dist/test/datasource-zabbix/timeseries.js
new file mode 100644
index 0000000..bc0ed69
--- /dev/null
+++ b/dist/test/datasource-zabbix/timeseries.js
@@ -0,0 +1,502 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _utils = require('./utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * timeseries.js
+ *
+ * This module contains functions for working with time series.
+ *
+ * datapoints - array of points where point is [value, timestamp]. In almost all cases (if other wasn't
+ * explicitly said) we assume datapoints are sorted by timestamp. Timestamp is the number of milliseconds
+ * since 1 January 1970 00:00:00 UTC.
+ *
+ */
+
+var POINT_VALUE = 0;
+var 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 = {
+ from: time_to * 1000 - ms_interval,
+ to: time_to * 1000
+ };
+
+ var points_sum = 0;
+ var points_num = 0;
+ var value_avg = 0;
+ var frame = [];
+
+ for (var 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 {
+ value_avg = points_num ? points_sum / points_num : 0;
+
+ if (func === "max") {
+ downsampledSeries.push([_lodash2.default.max(frame), timeWindow.to]);
+ } else if (func === "min") {
+ downsampledSeries.push([_lodash2.default.min(frame), timeWindow.to]);
+ }
+
+ // avg by default
+ else {
+ downsampledSeries.push([value_avg, timeWindow.to]);
+ }
+
+ // Shift time window
+ timeWindow.to = timeWindow.from;
+ timeWindow.from -= ms_interval;
+
+ points_sum = 0;
+ points_num = 0;
+ frame = [];
+
+ // Process point again
+ i++;
+ }
+ }
+ return downsampledSeries.reverse();
+}
+
+/**
+ * Group points by given time interval
+ * datapoints: [[, ], ...]
+ */
+function groupBy(datapoints, interval, groupByCallback) {
+ var ms_interval = utils.parseInterval(interval);
+
+ // Calculate frame timestamps
+ var frames = _lodash2.default.groupBy(datapoints, function (point) {
+ // Calculate time for group of points
+ return Math.floor(point[1] / ms_interval) * ms_interval;
+ });
+
+ // frame: { '': [[, ], ...] }
+ // return [{ '': }, { '': }, ...]
+ var grouped = _lodash2.default.mapValues(frames, function (frame) {
+ var points = _lodash2.default.map(frame, function (point) {
+ return point[0];
+ });
+ return groupByCallback(points);
+ });
+
+ // Convert points to Grafana format
+ return sortByTime(_lodash2.default.map(grouped, function (value, timestamp) {
+ return [Number(value), Number(timestamp)];
+ }));
+}
+
+function groupBy_perf(datapoints, interval, groupByCallback) {
+ if (datapoints.length === 0) {
+ return [];
+ }
+
+ var ms_interval = utils.parseInterval(interval);
+ var grouped_series = [];
+ var frame_values = [];
+ var frame_value = void 0;
+ var frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
+ var point_frame_ts = frame_ts;
+ var point = void 0;
+
+ for (var i = 0; i < datapoints.length; i++) {
+ point = datapoints[i];
+ point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
+ if (point_frame_ts === frame_ts) {
+ frame_values.push(point[POINT_VALUE]);
+ } else if (point_frame_ts > frame_ts) {
+ frame_value = groupByCallback(frame_values);
+ grouped_series.push([frame_value, frame_ts]);
+
+ // Move frame window to next non-empty interval and fill empty by null
+ frame_ts += ms_interval;
+ while (frame_ts < point_frame_ts) {
+ grouped_series.push([null, frame_ts]);
+ frame_ts += ms_interval;
+ }
+ frame_values = [point[POINT_VALUE]];
+ }
+ }
+
+ frame_value = groupByCallback(frame_values);
+ grouped_series.push([frame_value, frame_ts]);
+
+ return grouped_series;
+}
+
+/**
+ * Summarize set of time series into one.
+ * @param {datapoints[]} timeseries array of time series
+ */
+function sumSeries(timeseries) {
+
+ // Calculate new points for interpolation
+ var new_timestamps = _lodash2.default.uniq(_lodash2.default.map(_lodash2.default.flatten(timeseries, true), function (point) {
+ return point[1];
+ }));
+ new_timestamps = _lodash2.default.sortBy(new_timestamps);
+
+ var interpolated_timeseries = _lodash2.default.map(timeseries, function (series) {
+ series = fillZeroes(series, new_timestamps);
+ var timestamps = _lodash2.default.map(series, function (point) {
+ return point[1];
+ });
+ var new_points = _lodash2.default.map(_lodash2.default.difference(new_timestamps, timestamps), function (timestamp) {
+ return [null, timestamp];
+ });
+ var new_series = series.concat(new_points);
+ return sortByTime(new_series);
+ });
+
+ _lodash2.default.each(interpolated_timeseries, interpolateSeries);
+
+ var new_timeseries = [];
+ var sum;
+ for (var i = new_timestamps.length - 1; i >= 0; i--) {
+ sum = 0;
+ for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
+ sum += interpolated_timeseries[j][i][0];
+ }
+ new_timeseries.push([sum, new_timestamps[i]]);
+ }
+
+ return sortByTime(new_timeseries);
+}
+
+function scale(datapoints, factor) {
+ return _lodash2.default.map(datapoints, function (point) {
+ return [point[0] * factor, point[1]];
+ });
+}
+
+function scale_perf(datapoints, factor) {
+ for (var i = 0; i < datapoints.length; i++) {
+ datapoints[i] = [datapoints[i][POINT_VALUE] * factor, datapoints[i][POINT_TIMESTAMP]];
+ }
+
+ return datapoints;
+}
+
+/**
+ * Simple delta. Calculate value delta between points.
+ * @param {*} datapoints
+ */
+function delta(datapoints) {
+ var newSeries = [];
+ var deltaValue = void 0;
+ for (var i = 1; i < datapoints.length; i++) {
+ deltaValue = datapoints[i][0] - datapoints[i - 1][0];
+ newSeries.push([deltaValue, datapoints[i][1]]);
+ }
+ return newSeries;
+}
+
+/**
+ * Calculates rate per second. Resistant to counter reset.
+ * @param {*} datapoints
+ */
+function rate(datapoints) {
+ var newSeries = [];
+ var point = void 0,
+ point_prev = void 0;
+ var valueDelta = 0;
+ var timeDelta = 0;
+ for (var i = 1; i < datapoints.length; i++) {
+ point = datapoints[i];
+ point_prev = datapoints[i - 1];
+
+ // Convert ms to seconds
+ timeDelta = (point[POINT_TIMESTAMP] - point_prev[POINT_TIMESTAMP]) / 1000;
+
+ // Handle counter reset - use previous value
+ if (point[POINT_VALUE] >= point_prev[POINT_VALUE]) {
+ valueDelta = (point[POINT_VALUE] - point_prev[POINT_VALUE]) / timeDelta;
+ }
+
+ newSeries.push([valueDelta, point[POINT_TIMESTAMP]]);
+ }
+ return newSeries;
+}
+
+function simpleMovingAverage(datapoints, n) {
+ var sma = [];
+ var w_sum = void 0;
+ var w_avg = null;
+ var w_count = 0;
+
+ // Initial window
+ for (var j = n; j > 0; j--) {
+ if (datapoints[n - j][POINT_VALUE] !== null) {
+ w_avg += datapoints[n - j][POINT_VALUE];
+ w_count++;
+ }
+ }
+ if (w_count > 0) {
+ w_avg = w_avg / w_count;
+ } else {
+ w_avg = null;
+ }
+ sma.push([w_avg, datapoints[n - 1][POINT_TIMESTAMP]]);
+
+ for (var i = n; i < datapoints.length; i++) {
+ // Insert next value
+ if (datapoints[i][POINT_VALUE] !== null) {
+ w_sum = w_avg * w_count;
+ w_avg = (w_sum + datapoints[i][POINT_VALUE]) / (w_count + 1);
+ w_count++;
+ }
+ // Remove left side point
+ if (datapoints[i - n][POINT_VALUE] !== null) {
+ w_sum = w_avg * w_count;
+ if (w_count > 1) {
+ w_avg = (w_sum - datapoints[i - n][POINT_VALUE]) / (w_count - 1);
+ w_count--;
+ } else {
+ w_avg = null;
+ w_count = 0;
+ }
+ }
+ sma.push([w_avg, datapoints[i][POINT_TIMESTAMP]]);
+ }
+ return sma;
+}
+
+function expMovingAverage(datapoints, n) {
+ var ema = [datapoints[0]];
+ var ema_prev = datapoints[0][POINT_VALUE];
+ var ema_cur = void 0;
+ var a = void 0;
+
+ if (n > 1) {
+ // Calculate a from window size
+ a = 2 / (n + 1);
+
+ // Initial window, use simple moving average
+ var w_avg = null;
+ var w_count = 0;
+ for (var j = n; j > 0; j--) {
+ if (datapoints[n - j][POINT_VALUE] !== null) {
+ w_avg += datapoints[n - j][POINT_VALUE];
+ w_count++;
+ }
+ }
+ if (w_count > 0) {
+ w_avg = w_avg / w_count;
+ // Actually, we should set timestamp from datapoints[n-1] and start calculation of EMA from n.
+ // But in order to start EMA from first point (not from Nth) we should expand time range and request N additional
+ // points outside left side of range. We can't do that, so this trick is used for pretty view of first N points.
+ // We calculate AVG for first N points, but then start from 2nd point, not from Nth. In general, it means we
+ // assume that previous N values (0-N, 0-(N-1), ..., 0-1) have the same average value as a first N values.
+ ema = [[w_avg, datapoints[0][POINT_TIMESTAMP]]];
+ ema_prev = w_avg;
+ n = 1;
+ }
+ } else {
+ // Use predefined a and start from 1st point (use it as initial EMA value)
+ a = n;
+ n = 1;
+ }
+
+ for (var i = n; i < datapoints.length; i++) {
+ if (datapoints[i][POINT_VALUE] !== null) {
+ ema_cur = a * datapoints[i][POINT_VALUE] + (1 - a) * ema_prev;
+ ema_prev = ema_cur;
+ ema.push([ema_cur, datapoints[i][POINT_TIMESTAMP]]);
+ } else {
+ ema.push([null, datapoints[i][POINT_TIMESTAMP]]);
+ }
+ }
+ return ema;
+}
+
+function PERCENTIL(n, values) {
+ var sorted = _lodash2.default.sortBy(values);
+ return sorted[Math.floor(sorted.length * n / 100)];
+}
+
+function COUNT(values) {
+ return values.length;
+}
+
+function SUM(values) {
+ var sum = null;
+ for (var i = 0; i < values.length; i++) {
+ if (values[i] !== null) {
+ sum += values[i];
+ }
+ }
+ return sum;
+}
+
+function AVERAGE(values) {
+ var values_non_null = getNonNullValues(values);
+ if (values_non_null.length === 0) {
+ return null;
+ }
+ return SUM(values_non_null) / values_non_null.length;
+}
+
+function getNonNullValues(values) {
+ var values_non_null = [];
+ for (var i = 0; i < values.length; i++) {
+ if (values[i] !== null) {
+ values_non_null.push(values[i]);
+ }
+ }
+ return values_non_null;
+}
+
+function MIN(values) {
+ return _lodash2.default.min(values);
+}
+
+function MAX(values) {
+ return _lodash2.default.max(values);
+}
+
+function MEDIAN(values) {
+ var sorted = _lodash2.default.sortBy(values);
+ return sorted[Math.floor(sorted.length / 2)];
+}
+
+///////////////////////
+// Utility functions //
+///////////////////////
+
+/**
+ * For given point calculate corresponding time frame.
+ *
+ * |__*_|_*__|___*| -> |*___|*___|*___|
+ *
+ * @param {*} timestamp
+ * @param {*} ms_interval
+ */
+function getPointTimeFrame(timestamp, ms_interval) {
+ return Math.floor(timestamp / ms_interval) * ms_interval;
+}
+
+function sortByTime(series) {
+ return _lodash2.default.sortBy(series, function (point) {
+ return point[1];
+ });
+}
+
+/**
+ * Fill empty front and end of series by zeroes.
+ *
+ * | *** | | *** |
+ * |___ ___| -> |*** ***|
+ * @param {*} series
+ * @param {*} timestamps
+ */
+function fillZeroes(series, timestamps) {
+ var prepend = [];
+ var append = [];
+ var new_point = void 0;
+ for (var i = 0; i < timestamps.length; i++) {
+ if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
+ new_point = [0, timestamps[i]];
+ prepend.push(new_point);
+ } else if (timestamps[i] > series[series.length - 1][POINT_TIMESTAMP]) {
+ new_point = [0, timestamps[i]];
+ append.push(new_point);
+ }
+ }
+ return _lodash2.default.concat(_lodash2.default.concat(prepend, series), append);
+}
+
+/**
+ * Interpolate series with gaps
+ */
+function interpolateSeries(series) {
+ var left, right;
+
+ // Interpolate series
+ for (var i = series.length - 1; i >= 0; i--) {
+ if (!series[i][0]) {
+ left = findNearestLeft(series, i);
+ right = findNearestRight(series, i);
+ if (!left) {
+ left = right;
+ }
+ if (!right) {
+ right = left;
+ }
+ series[i][0] = linearInterpolation(series[i][1], left, right);
+ }
+ }
+ return series;
+}
+
+function linearInterpolation(timestamp, left, right) {
+ if (left[1] === right[1]) {
+ return (left[0] + right[0]) / 2;
+ } else {
+ return left[0] + (right[0] - left[0]) / (right[1] - left[1]) * (timestamp - left[1]);
+ }
+}
+
+function findNearestRight(series, pointIndex) {
+ for (var i = pointIndex; i < series.length; i++) {
+ if (series[i][0] !== null) {
+ return series[i];
+ }
+ }
+ return null;
+}
+
+function findNearestLeft(series, pointIndex) {
+ for (var i = pointIndex; i > 0; i--) {
+ if (series[i][0] !== null) {
+ return series[i];
+ }
+ }
+ return null;
+}
+
+////////////
+// Export //
+////////////
+
+var exportedFunctions = {
+ downsample: downsample,
+ groupBy: groupBy,
+ groupBy_perf: groupBy_perf,
+ sumSeries: sumSeries,
+ scale: scale,
+ scale_perf: scale_perf,
+ delta: delta,
+ rate: rate,
+ simpleMovingAverage: simpleMovingAverage,
+ expMovingAverage: expMovingAverage,
+ SUM: SUM,
+ COUNT: COUNT,
+ AVERAGE: AVERAGE,
+ MIN: MIN,
+ MAX: MAX,
+ MEDIAN: MEDIAN,
+ PERCENTIL: PERCENTIL
+};
+
+exports.default = exportedFunctions;
diff --git a/dist/test/datasource-zabbix/utils.js b/dist/test/datasource-zabbix/utils.js
new file mode 100644
index 0000000..6ca5c96
--- /dev/null
+++ b/dist/test/datasource-zabbix/utils.js
@@ -0,0 +1,256 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.regexPattern = undefined;
+exports.expandItemName = expandItemName;
+exports.expandItems = expandItems;
+exports.containsMacro = containsMacro;
+exports.replaceMacro = replaceMacro;
+exports.splitTemplateQuery = splitTemplateQuery;
+exports.isRegex = isRegex;
+exports.isTemplateVariable = isTemplateVariable;
+exports.buildRegex = buildRegex;
+exports.escapeRegex = escapeRegex;
+exports.parseInterval = parseInterval;
+exports.parseTimeShiftInterval = parseTimeShiftInterval;
+exports.formatAcknowledges = formatAcknowledges;
+exports.convertToZabbixAPIUrl = convertToZabbixAPIUrl;
+exports.callOnce = callOnce;
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _moment = require('moment');
+
+var _moment2 = _interopRequireDefault(_moment);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Expand Zabbix item name
+ *
+ * @param {string} name item name, ie "CPU $2 time"
+ * @param {string} key item key, ie system.cpu.util[,system,avg1]
+ * @return {string} expanded name, ie "CPU system time"
+ */
+function expandItemName(name, key) {
+
+ // extract params from key:
+ // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
+ var key_params_str = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']'));
+ var key_params = splitKeyParams(key_params_str);
+
+ // replace item parameters
+ for (var i = key_params.length; i >= 1; i--) {
+ name = name.replace('$' + i, key_params[i - 1]);
+ }
+ return name;
+}
+
+function expandItems(items) {
+ _lodash2.default.forEach(items, function (item) {
+ item.item = item.name;
+ item.name = expandItemName(item.item, item.key_);
+ return item;
+ });
+ return items;
+}
+
+function splitKeyParams(paramStr) {
+ var params = [];
+ var quoted = false;
+ var in_array = false;
+ var split_symbol = ',';
+ var param = '';
+
+ _lodash2.default.forEach(paramStr, function (symbol) {
+ if (symbol === '"' && in_array) {
+ param += symbol;
+ } else if (symbol === '"' && quoted) {
+ quoted = false;
+ } else if (symbol === '"' && !quoted) {
+ quoted = true;
+ } else if (symbol === '[' && !quoted) {
+ in_array = true;
+ } else if (symbol === ']' && !quoted) {
+ in_array = false;
+ } else if (symbol === split_symbol && !quoted && !in_array) {
+ params.push(param);
+ param = '';
+ } else {
+ param += symbol;
+ }
+ });
+
+ params.push(param);
+ return params;
+}
+
+var MACRO_PATTERN = /{\$[A-Z0-9_\.]+}/g;
+
+function containsMacro(itemName) {
+ return MACRO_PATTERN.test(itemName);
+}
+
+function replaceMacro(item, macros) {
+ var itemName = item.name;
+ var item_macros = itemName.match(MACRO_PATTERN);
+ _lodash2.default.forEach(item_macros, function (macro) {
+ var host_macros = _lodash2.default.filter(macros, function (m) {
+ if (m.hostid) {
+ return m.hostid === item.hostid;
+ } else {
+ // Add global macros
+ return true;
+ }
+ });
+
+ var macro_def = _lodash2.default.find(host_macros, { macro: macro });
+ if (macro_def && macro_def.value) {
+ var macro_value = macro_def.value;
+ var macro_regex = new RegExp(escapeMacro(macro));
+ itemName = itemName.replace(macro_regex, macro_value);
+ }
+ });
+
+ return itemName;
+}
+
+function escapeMacro(macro) {
+ macro = macro.replace(/\$/, '\\\$');
+ return macro;
+}
+
+/**
+ * Split template query to parts of zabbix entities
+ * group.host.app.item -> [group, host, app, item]
+ * {group}{host.com} -> [group, host.com]
+ */
+function splitTemplateQuery(query) {
+ var splitPattern = /\{[^\{\}]*\}|\{\/.*\/\}/g;
+ var split = void 0;
+
+ if (isContainsBraces(query)) {
+ var result = query.match(splitPattern);
+ split = _lodash2.default.map(result, function (part) {
+ return _lodash2.default.trim(part, '{}');
+ });
+ } else {
+ split = query.split('.');
+ }
+
+ return split;
+}
+
+function isContainsBraces(query) {
+ var bracesPattern = /^\{.+\}$/;
+ return bracesPattern.test(query);
+}
+
+// Pattern for testing regex
+var regexPattern = exports.regexPattern = /^\/(.*)\/([gmi]*)$/m;
+
+function isRegex(str) {
+ return regexPattern.test(str);
+}
+
+function isTemplateVariable(str, templateVariables) {
+ var variablePattern = /^\$\w+/;
+ if (variablePattern.test(str)) {
+ var variables = _lodash2.default.map(templateVariables, function (variable) {
+ return '$' + variable.name;
+ });
+ return _lodash2.default.includes(variables, str);
+ } else {
+ return false;
+ }
+}
+
+function buildRegex(str) {
+ var matches = str.match(regexPattern);
+ var pattern = matches[1];
+ var flags = matches[2] !== "" ? matches[2] : undefined;
+ return new RegExp(pattern, flags);
+}
+
+// Need for template variables replace
+// From Grafana's templateSrv.js
+function escapeRegex(value) {
+ return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
+}
+
+function parseInterval(interval) {
+ var intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
+ var momentInterval = intervalPattern.exec(interval);
+ return _moment2.default.duration(Number(momentInterval[1]), momentInterval[2]).valueOf();
+}
+
+function parseTimeShiftInterval(interval) {
+ var intervalPattern = /^([\+\-]*)([\d]+)(y|M|w|d|h|m|s)/g;
+ var momentInterval = intervalPattern.exec(interval);
+ var duration = 0;
+
+ if (momentInterval[1] === '+') {
+ duration = 0 - _moment2.default.duration(Number(momentInterval[2]), momentInterval[3]).valueOf();
+ } else {
+ duration = _moment2.default.duration(Number(momentInterval[2]), momentInterval[3]).valueOf();
+ }
+
+ return duration;
+}
+
+/**
+ * Format acknowledges.
+ *
+ * @param {array} acknowledges array of Zabbix acknowledge objects
+ * @return {string} HTML-formatted table
+ */
+function formatAcknowledges(acknowledges) {
+ if (acknowledges.length) {
+ var formatted_acknowledges = ' Acknowledges:Time ' + 'User Comments ';
+ _lodash2.default.each(_lodash2.default.map(acknowledges, function (ack) {
+ var timestamp = _moment2.default.unix(ack.clock);
+ return '' + timestamp.format("DD MMM YYYY HH:mm:ss") + ' ' + ack.alias + ' (' + ack.name + ' ' + ack.surname + ')' + ' ' + ack.message + ' ';
+ }), function (ack) {
+ formatted_acknowledges = formatted_acknowledges.concat(ack);
+ });
+ formatted_acknowledges = formatted_acknowledges.concat('
');
+ return formatted_acknowledges;
+ } else {
+ return '';
+ }
+}
+
+function convertToZabbixAPIUrl(url) {
+ var zabbixAPIUrlPattern = /.*api_jsonrpc.php$/;
+ var trimSlashPattern = /(.*?)[\/]*$/;
+ if (url.match(zabbixAPIUrlPattern)) {
+ return url;
+ } else {
+ return url.replace(trimSlashPattern, "$1");
+ }
+}
+
+/**
+ * Wrap function to prevent multiple calls
+ * when waiting for result.
+ */
+function callOnce(func, promiseKeeper) {
+ return function () {
+ if (!promiseKeeper) {
+ promiseKeeper = Promise.resolve(func.apply(this, arguments).then(function (result) {
+ promiseKeeper = null;
+ return result;
+ }));
+ }
+ return promiseKeeper;
+ };
+}
+
+// Fix for backward compatibility with lodash 2.4
+if (!_lodash2.default.includes) {
+ _lodash2.default.includes = _lodash2.default.contains;
+}
diff --git a/dist/test/datasource-zabbix/zabbix.js b/dist/test/datasource-zabbix/zabbix.js
new file mode 100644
index 0000000..e90c745
--- /dev/null
+++ b/dist/test/datasource-zabbix/zabbix.js
@@ -0,0 +1,338 @@
+'use strict';
+
+var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _utils = require('./utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+require('./zabbixAPI.service.js');
+
+require('./zabbixCachingProxy.service.js');
+
+require('./zabbixDBConnector');
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+// Use factory() instead service() for multiple data sources support.
+// Each Zabbix data source instance should initialize its own API instance.
+
+/** @ngInject */
+function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy, ZabbixDBConnector) {
+ var Zabbix = function () {
+ function Zabbix(url, options) {
+ _classCallCheck(this, Zabbix);
+
+ var username = options.username,
+ password = options.password,
+ basicAuth = options.basicAuth,
+ withCredentials = options.withCredentials,
+ cacheTTL = options.cacheTTL,
+ enableDirectDBConnection = options.enableDirectDBConnection,
+ sqlDatasourceId = options.sqlDatasourceId;
+
+ // Initialize Zabbix API
+
+ var ZabbixAPI = zabbixAPIService;
+ this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials);
+
+ if (enableDirectDBConnection) {
+ this.dbConnector = new ZabbixDBConnector(sqlDatasourceId);
+ }
+
+ // Initialize caching proxy for requests
+ var cacheOptions = {
+ enabled: true,
+ ttl: cacheTTL
+ };
+ this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, this.dbConnector, cacheOptions);
+
+ // Proxy methods
+ this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy);
+ this.getMacros = this.cachingProxy.getMacros.bind(this.cachingProxy);
+ this.getItemsByIDs = this.cachingProxy.getItemsByIDs.bind(this.cachingProxy);
+
+ if (enableDirectDBConnection) {
+ this.getHistoryDB = this.cachingProxy.getHistoryDB.bind(this.cachingProxy);
+ this.getTrendsDB = this.cachingProxy.getTrendsDB.bind(this.cachingProxy);
+ }
+
+ this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI);
+ this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI);
+ this.getAlerts = this.zabbixAPI.getAlerts.bind(this.zabbixAPI);
+ this.getHostAlerts = this.zabbixAPI.getHostAlerts.bind(this.zabbixAPI);
+ this.getAcknowledges = this.zabbixAPI.getAcknowledges.bind(this.zabbixAPI);
+ this.getITService = this.zabbixAPI.getITService.bind(this.zabbixAPI);
+ this.getSLA = this.zabbixAPI.getSLA.bind(this.zabbixAPI);
+ this.getVersion = this.zabbixAPI.getVersion.bind(this.zabbixAPI);
+ this.login = this.zabbixAPI.login.bind(this.zabbixAPI);
+ }
+
+ _createClass(Zabbix, [{
+ key: 'getItemsFromTarget',
+ value: function getItemsFromTarget(target, options) {
+ var parts = ['group', 'host', 'application', 'item'];
+ var filters = _lodash2.default.map(parts, function (p) {
+ return target[p].filter;
+ });
+ return this.getItems.apply(this, _toConsumableArray(filters).concat([options]));
+ }
+ }, {
+ key: 'getHostsFromTarget',
+ value: function getHostsFromTarget(target) {
+ var parts = ['group', 'host', 'application'];
+ var filters = _lodash2.default.map(parts, function (p) {
+ return target[p].filter;
+ });
+ return Promise.all([this.getHosts.apply(this, _toConsumableArray(filters)), this.getApps.apply(this, _toConsumableArray(filters))]).then(function (results) {
+ var _results = _slicedToArray(results, 2),
+ hosts = _results[0],
+ apps = _results[1];
+
+ if (apps.appFilterEmpty) {
+ apps = [];
+ }
+ return [hosts, apps];
+ });
+ }
+ }, {
+ key: 'getAllGroups',
+ value: function getAllGroups() {
+ return this.cachingProxy.getGroups();
+ }
+ }, {
+ key: 'getGroups',
+ value: function getGroups(groupFilter) {
+ return this.getAllGroups().then(function (groups) {
+ return findByFilter(groups, groupFilter);
+ });
+ }
+
+ /**
+ * Get list of host belonging to given groups.
+ */
+
+ }, {
+ key: 'getAllHosts',
+ value: function getAllHosts(groupFilter) {
+ var _this = this;
+
+ return this.getGroups(groupFilter).then(function (groups) {
+ var groupids = _lodash2.default.map(groups, 'groupid');
+ return _this.cachingProxy.getHosts(groupids);
+ });
+ }
+ }, {
+ key: 'getHosts',
+ value: function getHosts(groupFilter, hostFilter) {
+ return this.getAllHosts(groupFilter).then(function (hosts) {
+ return findByFilter(hosts, hostFilter);
+ });
+ }
+
+ /**
+ * Get list of applications belonging to given groups and hosts.
+ */
+
+ }, {
+ key: 'getAllApps',
+ value: function getAllApps(groupFilter, hostFilter) {
+ var _this2 = this;
+
+ return this.getHosts(groupFilter, hostFilter).then(function (hosts) {
+ var hostids = _lodash2.default.map(hosts, 'hostid');
+ return _this2.cachingProxy.getApps(hostids);
+ });
+ }
+ }, {
+ key: 'getApps',
+ value: function getApps(groupFilter, hostFilter, appFilter) {
+ var _this3 = this;
+
+ return this.getHosts(groupFilter, hostFilter).then(function (hosts) {
+ var hostids = _lodash2.default.map(hosts, 'hostid');
+ if (appFilter) {
+ return _this3.cachingProxy.getApps(hostids).then(function (apps) {
+ return filterByQuery(apps, appFilter);
+ });
+ } else {
+ return {
+ appFilterEmpty: true,
+ hostids: hostids
+ };
+ }
+ });
+ }
+ }, {
+ key: 'getAllItems',
+ value: function getAllItems(groupFilter, hostFilter, appFilter) {
+ var _this4 = this;
+
+ var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
+
+ return this.getApps(groupFilter, hostFilter, appFilter).then(function (apps) {
+ if (apps.appFilterEmpty) {
+ return _this4.cachingProxy.getItems(apps.hostids, undefined, options.itemtype);
+ } else {
+ var appids = _lodash2.default.map(apps, 'applicationid');
+ return _this4.cachingProxy.getItems(undefined, appids, options.itemtype);
+ }
+ }).then(function (items) {
+ if (!options.showDisabledItems) {
+ items = _lodash2.default.filter(items, { 'status': '0' });
+ }
+
+ return items;
+ }).then(this.expandUserMacro.bind(this));
+ }
+ }, {
+ key: 'expandUserMacro',
+ value: function expandUserMacro(items) {
+ var hostids = getHostIds(items);
+ return this.getMacros(hostids).then(function (macros) {
+ _lodash2.default.forEach(items, function (item) {
+ if (utils.containsMacro(item.name)) {
+ item.name = utils.replaceMacro(item, macros);
+ }
+ });
+ return items;
+ });
+ }
+ }, {
+ key: 'getItems',
+ value: function getItems(groupFilter, hostFilter, appFilter, itemFilter) {
+ var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
+
+ return this.getAllItems(groupFilter, hostFilter, appFilter, options).then(function (items) {
+ return filterByQuery(items, itemFilter);
+ });
+ }
+ }, {
+ key: 'getITServices',
+ value: function getITServices(itServiceFilter) {
+ return this.cachingProxy.getITServices().then(function (itServices) {
+ return findByFilter(itServices, itServiceFilter);
+ });
+ }
+
+ /**
+ * Build query - convert target filters to array of Zabbix items
+ */
+
+ }, {
+ key: 'getTriggers',
+ value: function getTriggers(groupFilter, hostFilter, appFilter, options) {
+ var _this5 = this;
+
+ var promises = [this.getGroups(groupFilter), this.getHosts(groupFilter, hostFilter), this.getApps(groupFilter, hostFilter, appFilter)];
+
+ return Promise.all(promises).then(function (results) {
+ var filteredGroups = results[0];
+ var filteredHosts = results[1];
+ var filteredApps = results[2];
+ var query = {};
+
+ if (appFilter) {
+ query.applicationids = _lodash2.default.flatten(_lodash2.default.map(filteredApps, 'applicationid'));
+ }
+ if (hostFilter) {
+ query.hostids = _lodash2.default.map(filteredHosts, 'hostid');
+ }
+ if (groupFilter) {
+ query.groupids = _lodash2.default.map(filteredGroups, 'groupid');
+ }
+
+ return query;
+ }).then(function (query) {
+ return _this5.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options);
+ });
+ }
+ }]);
+
+ return Zabbix;
+ }();
+
+ return Zabbix;
+}
+
+_angular2.default.module('grafana.services').factory('Zabbix', ZabbixFactory);
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Find group, host, app or item by given name.
+ * @param list list of groups, apps or other
+ * @param name visible name
+ * @return array with finded element or empty array
+ */
+function findByName(list, name) {
+ var finded = _lodash2.default.find(list, { 'name': name });
+ if (finded) {
+ return [finded];
+ } else {
+ return [];
+ }
+}
+
+/**
+ * Different hosts can contains applications and items with same name.
+ * For this reason use _.filter, which return all elements instead _.find,
+ * which return only first finded.
+ * @param {[type]} list list of elements
+ * @param {[type]} name app name
+ * @return {[type]} array with finded element or empty array
+ */
+function filterByName(list, name) {
+ var finded = _lodash2.default.filter(list, { 'name': name });
+ if (finded) {
+ return finded;
+ } else {
+ return [];
+ }
+}
+
+function filterByRegex(list, regex) {
+ var filterPattern = utils.buildRegex(regex);
+ return _lodash2.default.filter(list, function (zbx_obj) {
+ return filterPattern.test(zbx_obj.name);
+ });
+}
+
+function findByFilter(list, filter) {
+ if (utils.isRegex(filter)) {
+ return filterByRegex(list, filter);
+ } else {
+ return findByName(list, filter);
+ }
+}
+
+function filterByQuery(list, filter) {
+ if (utils.isRegex(filter)) {
+ return filterByRegex(list, filter);
+ } else {
+ return filterByName(list, filter);
+ }
+}
+
+function getHostIds(items) {
+ var hostIds = _lodash2.default.map(items, function (item) {
+ return _lodash2.default.map(item.hosts, 'hostid');
+ });
+ return _lodash2.default.uniq(_lodash2.default.flatten(hostIds));
+}
diff --git a/dist/test/datasource-zabbix/zabbixAPI.service.js b/dist/test/datasource-zabbix/zabbixAPI.service.js
new file mode 100644
index 0000000..726ad45
--- /dev/null
+++ b/dist/test/datasource-zabbix/zabbixAPI.service.js
@@ -0,0 +1,561 @@
+'use strict';
+
+var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _utils = require('./utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+require('./zabbixAPICore.service');
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+/** @ngInject */
+function ZabbixAPIServiceFactory(alertSrv, zabbixAPICoreService) {
+
+ /**
+ * Zabbix API Wrapper.
+ * Creates Zabbix API instance with given parameters (url, credentials and other).
+ * Wraps API calls and provides high-level methods.
+ */
+ var ZabbixAPI = function () {
+ function ZabbixAPI(api_url, username, password, basicAuth, withCredentials) {
+ _classCallCheck(this, ZabbixAPI);
+
+ this.url = api_url;
+ this.username = username;
+ this.password = password;
+ this.auth = "";
+
+ this.requestOptions = {
+ basicAuth: basicAuth,
+ withCredentials: withCredentials
+ };
+
+ this.loginPromise = null;
+ this.loginErrorCount = 0;
+ this.maxLoginAttempts = 3;
+
+ this.alertSrv = alertSrv;
+ this.zabbixAPICore = zabbixAPICoreService;
+
+ this.getTrend = this.getTrend_ZBXNEXT1193;
+ //getTrend = getTrend_30;
+ }
+
+ //////////////////////////
+ // Core method wrappers //
+ //////////////////////////
+
+ _createClass(ZabbixAPI, [{
+ key: 'request',
+ value: function request(method, params) {
+ var _this = this;
+
+ return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth).catch(function (error) {
+ if (isNotAuthorized(error.data)) {
+ // Handle auth errors
+ _this.loginErrorCount++;
+ if (_this.loginErrorCount > _this.maxLoginAttempts) {
+ _this.loginErrorCount = 0;
+ return null;
+ } else {
+ return _this.loginOnce().then(function () {
+ return _this.request(method, params);
+ });
+ }
+ } else {
+ // Handle API errors
+ var message = error.data ? error.data : error.statusText;
+ _this.alertAPIError(message);
+ }
+ });
+ }
+ }, {
+ key: 'alertAPIError',
+ value: function alertAPIError(message) {
+ var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 5000;
+
+ this.alertSrv.set("Zabbix API Error", message, 'error', timeout);
+ }
+
+ /**
+ * When API unauthenticated or auth token expired each request produce login()
+ * call. But auth token is common to all requests. This function wraps login() method
+ * and call it once. If login() already called just wait for it (return its promise).
+ * @return login promise
+ */
+
+ }, {
+ key: 'loginOnce',
+ value: function loginOnce() {
+ var _this2 = this;
+
+ if (!this.loginPromise) {
+ this.loginPromise = Promise.resolve(this.login().then(function (auth) {
+ _this2.auth = auth;
+ _this2.loginPromise = null;
+ return auth;
+ }));
+ }
+ return this.loginPromise;
+ }
+
+ /**
+ * Get authentication token.
+ */
+
+ }, {
+ key: 'login',
+ value: function login() {
+ return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions);
+ }
+
+ /**
+ * Get Zabbix API version
+ */
+
+ }, {
+ key: 'getVersion',
+ value: function getVersion() {
+ return this.zabbixAPICore.getVersion(this.url, this.requestOptions);
+ }
+
+ ////////////////////////////////
+ // Zabbix API method wrappers //
+ ////////////////////////////////
+
+ }, {
+ key: 'acknowledgeEvent',
+ value: function acknowledgeEvent(eventid, message) {
+ var params = {
+ eventids: eventid,
+ message: message
+ };
+
+ return this.request('event.acknowledge', params);
+ }
+ }, {
+ key: 'getGroups',
+ value: function getGroups() {
+ var params = {
+ output: ['name'],
+ sortfield: 'name',
+ real_hosts: true
+ };
+
+ return this.request('hostgroup.get', params);
+ }
+ }, {
+ key: 'getHosts',
+ value: function getHosts(groupids) {
+ var params = {
+ output: ['name', 'host'],
+ sortfield: 'name'
+ };
+ if (groupids) {
+ params.groupids = groupids;
+ }
+
+ return this.request('host.get', params);
+ }
+ }, {
+ key: 'getApps',
+ value: function getApps(hostids) {
+ var params = {
+ output: 'extend',
+ hostids: hostids
+ };
+
+ return this.request('application.get', params);
+ }
+
+ /**
+ * Get Zabbix items
+ * @param {[type]} hostids host ids
+ * @param {[type]} appids application ids
+ * @param {String} itemtype 'num' or 'text'
+ * @return {[type]} array of items
+ */
+
+ }, {
+ key: 'getItems',
+ value: function getItems(hostids, appids, itemtype) {
+ var params = {
+ output: ['name', 'key_', 'value_type', 'hostid', 'status', 'state'],
+ sortfield: 'name',
+ webitems: true,
+ filter: {},
+ selectHosts: ['hostid', 'name']
+ };
+ if (hostids) {
+ params.hostids = hostids;
+ }
+ if (appids) {
+ params.applicationids = appids;
+ }
+ if (itemtype === 'num') {
+ // Return only numeric metrics
+ params.filter.value_type = [0, 3];
+ }
+ if (itemtype === 'text') {
+ // Return only text metrics
+ params.filter.value_type = [1, 2, 4];
+ }
+
+ return this.request('item.get', params).then(utils.expandItems);
+ }
+ }, {
+ key: 'getItemsByIDs',
+ value: function getItemsByIDs(itemids) {
+ var params = {
+ itemids: itemids,
+ output: ['name', 'key_', 'value_type', 'hostid', 'status', 'state'],
+ webitems: true,
+ selectHosts: ['hostid', 'name']
+ };
+
+ return this.request('item.get', params).then(utils.expandItems);
+ }
+ }, {
+ key: 'getMacros',
+ value: function getMacros(hostids) {
+ var params = {
+ output: 'extend',
+ hostids: hostids
+ };
+
+ return this.request('usermacro.get', params);
+ }
+ }, {
+ key: 'getGlobalMacros',
+ value: function getGlobalMacros() {
+ var params = {
+ output: 'extend',
+ globalmacro: true
+ };
+
+ return this.request('usermacro.get', params);
+ }
+ }, {
+ key: 'getLastValue',
+ value: function getLastValue(itemid) {
+ var params = {
+ output: ['lastvalue'],
+ itemids: itemid
+ };
+ return this.request('item.get', params).then(function (items) {
+ return items.length ? items[0].lastvalue : null;
+ });
+ }
+
+ /**
+ * Perform history query from Zabbix API
+ *
+ * @param {Array} items Array of Zabbix item objects
+ * @param {Number} timeFrom Time in seconds
+ * @param {Number} timeTill Time in seconds
+ * @return {Array} Array of Zabbix history objects
+ */
+
+ }, {
+ key: 'getHistory',
+ value: function getHistory(items, timeFrom, timeTill) {
+ var _this3 = this;
+
+ // Group items by value type and perform request for each value type
+ var grouped_items = _lodash2.default.groupBy(items, 'value_type');
+ var promises = _lodash2.default.map(grouped_items, function (items, value_type) {
+ var itemids = _lodash2.default.map(items, 'itemid');
+ var params = {
+ output: 'extend',
+ history: value_type,
+ itemids: itemids,
+ sortfield: 'clock',
+ sortorder: 'ASC',
+ time_from: timeFrom
+ };
+
+ // Relative queries (e.g. last hour) don't include an end time
+ if (timeTill) {
+ params.time_till = timeTill;
+ }
+
+ return _this3.request('history.get', params);
+ });
+
+ return Promise.all(promises).then(_lodash2.default.flatten);
+ }
+
+ /**
+ * Perform trends query from Zabbix API
+ * Use trends api extension from ZBXNEXT-1193 patch.
+ *
+ * @param {Array} items Array of Zabbix item objects
+ * @param {Number} time_from Time in seconds
+ * @param {Number} time_till Time in seconds
+ * @return {Array} Array of Zabbix trend objects
+ */
+
+ }, {
+ key: 'getTrend_ZBXNEXT1193',
+ value: function getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
+ var _this4 = this;
+
+ // Group items by value type and perform request for each value type
+ var grouped_items = _lodash2.default.groupBy(items, 'value_type');
+ var promises = _lodash2.default.map(grouped_items, function (items, value_type) {
+ var itemids = _lodash2.default.map(items, 'itemid');
+ var params = {
+ output: 'extend',
+ trend: value_type,
+ itemids: itemids,
+ sortfield: 'clock',
+ sortorder: 'ASC',
+ time_from: timeFrom
+ };
+
+ // Relative queries (e.g. last hour) don't include an end time
+ if (timeTill) {
+ params.time_till = timeTill;
+ }
+
+ return _this4.request('trend.get', params);
+ });
+
+ return Promise.all(promises).then(_lodash2.default.flatten);
+ }
+ }, {
+ key: 'getTrend_30',
+ value: function getTrend_30(items, time_from, time_till, value_type) {
+ var self = this;
+ var itemids = _lodash2.default.map(items, 'itemid');
+
+ var params = {
+ output: ["itemid", "clock", value_type],
+ itemids: itemids,
+ time_from: time_from
+ };
+
+ // Relative queries (e.g. last hour) don't include an end time
+ if (time_till) {
+ params.time_till = time_till;
+ }
+
+ return self.request('trend.get', params);
+ }
+ }, {
+ key: 'getITService',
+ value: function getITService(serviceids) {
+ var params = {
+ output: 'extend',
+ serviceids: serviceids
+ };
+ return this.request('service.get', params);
+ }
+ }, {
+ key: 'getSLA',
+ value: function getSLA(serviceids, timeRange) {
+ var _timeRange = _slicedToArray(timeRange, 2),
+ timeFrom = _timeRange[0],
+ timeTo = _timeRange[1];
+
+ var params = {
+ serviceids: serviceids,
+ intervals: [{
+ from: timeFrom,
+ to: timeTo
+ }]
+ };
+ return this.request('service.getsla', params);
+ }
+ }, {
+ key: 'getTriggers',
+ value: function getTriggers(groupids, hostids, applicationids, options) {
+ var showTriggers = options.showTriggers,
+ hideHostsInMaintenance = options.hideHostsInMaintenance,
+ timeFrom = options.timeFrom,
+ timeTo = options.timeTo;
+
+
+ var params = {
+ output: 'extend',
+ groupids: groupids,
+ hostids: hostids,
+ applicationids: applicationids,
+ expandDescription: true,
+ expandData: true,
+ expandComment: true,
+ monitored: true,
+ skipDependent: true,
+ //only_true: true,
+ filter: {
+ value: 1
+ },
+ selectGroups: ['name'],
+ selectHosts: ['name', 'host'],
+ selectItems: ['name', 'key_', 'lastvalue'],
+ selectLastEvent: 'extend'
+ };
+
+ if (showTriggers) {
+ params.filter.value = showTriggers;
+ }
+
+ if (hideHostsInMaintenance) {
+ params.maintenance = false;
+ }
+
+ if (timeFrom || timeTo) {
+ params.lastChangeSince = timeFrom;
+ params.lastChangeTill = timeTo;
+ }
+
+ return this.request('trigger.get', params);
+ }
+ }, {
+ key: 'getEvents',
+ value: function getEvents(objectids, timeFrom, timeTo, showEvents) {
+ var params = {
+ output: 'extend',
+ time_from: timeFrom,
+ time_till: timeTo,
+ objectids: objectids,
+ select_acknowledges: 'extend',
+ selectHosts: 'extend',
+ value: showEvents
+ };
+
+ return this.request('event.get', params);
+ }
+ }, {
+ key: 'getAcknowledges',
+ value: function getAcknowledges(eventids) {
+ var params = {
+ output: 'extend',
+ eventids: eventids,
+ preservekeys: true,
+ select_acknowledges: 'extend',
+ sortfield: 'clock',
+ sortorder: 'DESC'
+ };
+
+ return this.request('event.get', params).then(function (events) {
+ return _lodash2.default.filter(events, function (event) {
+ return event.acknowledges.length;
+ });
+ });
+ }
+ }, {
+ key: 'getAlerts',
+ value: function getAlerts(itemids, timeFrom, timeTo) {
+ var params = {
+ output: 'extend',
+ itemids: itemids,
+ expandDescription: true,
+ expandData: true,
+ expandComment: true,
+ monitored: true,
+ skipDependent: true,
+ //only_true: true,
+ // filter: {
+ // value: 1
+ // },
+ selectLastEvent: 'extend'
+ };
+
+ if (timeFrom || timeTo) {
+ params.lastChangeSince = timeFrom;
+ params.lastChangeTill = timeTo;
+ }
+
+ return this.request('trigger.get', params);
+ }
+ }, {
+ key: 'getHostAlerts',
+ value: function getHostAlerts(hostids, applicationids, options) {
+ var minSeverity = options.minSeverity,
+ acknowledged = options.acknowledged,
+ count = options.count,
+ timeFrom = options.timeFrom,
+ timeTo = options.timeTo;
+
+ var params = {
+ output: 'extend',
+ hostids: hostids,
+ min_severity: minSeverity,
+ filter: { value: 1 },
+ expandDescription: true,
+ expandData: true,
+ expandComment: true,
+ monitored: true,
+ skipDependent: true,
+ selectLastEvent: 'extend',
+ selectGroups: 'extend',
+ selectHosts: ['host', 'name']
+ };
+
+ if (count && acknowledged !== 0 && acknowledged !== 1) {
+ params.countOutput = true;
+ }
+
+ if (applicationids && applicationids.length) {
+ params.applicationids = applicationids;
+ }
+
+ if (timeFrom || timeTo) {
+ params.lastChangeSince = timeFrom;
+ params.lastChangeTill = timeTo;
+ }
+
+ return this.request('trigger.get', params).then(function (triggers) {
+ if (!count || acknowledged === 0 || acknowledged === 1) {
+ triggers = filterTriggersByAcknowledge(triggers, acknowledged);
+ if (count) {
+ triggers = triggers.length;
+ }
+ }
+ return triggers;
+ });
+ }
+ }]);
+
+ return ZabbixAPI;
+ }();
+
+ return ZabbixAPI;
+}
+
+function filterTriggersByAcknowledge(triggers, acknowledged) {
+ if (acknowledged === 0) {
+ return _lodash2.default.filter(triggers, function (trigger) {
+ return trigger.lastEvent.acknowledged === "0";
+ });
+ } else if (acknowledged === 1) {
+ return _lodash2.default.filter(triggers, function (trigger) {
+ return trigger.lastEvent.acknowledged === "1";
+ });
+ } else {
+ return triggers;
+ }
+}
+
+function isNotAuthorized(message) {
+ return message === "Session terminated, re-login, please." || message === "Not authorised." || message === "Not authorized.";
+}
+
+_angular2.default.module('grafana.services').factory('zabbixAPIService', ZabbixAPIServiceFactory);
diff --git a/dist/test/datasource-zabbix/zabbixAPICore.service.js b/dist/test/datasource-zabbix/zabbixAPICore.service.js
new file mode 100644
index 0000000..c5ccee9
--- /dev/null
+++ b/dist/test/datasource-zabbix/zabbixAPICore.service.js
@@ -0,0 +1,142 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ZabbixAPIError = undefined;
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /**
+ * General Zabbix API methods
+ */
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var ZabbixAPICoreService = function () {
+
+ /** @ngInject */
+ function ZabbixAPICoreService(backendSrv) {
+ _classCallCheck(this, ZabbixAPICoreService);
+
+ this.backendSrv = backendSrv;
+ }
+
+ /**
+ * Request data from Zabbix API
+ * @return {object} response.result
+ */
+
+
+ _createClass(ZabbixAPICoreService, [{
+ key: 'request',
+ value: function request(api_url, method, params, options, auth) {
+ var requestData = {
+ jsonrpc: '2.0',
+ method: method,
+ params: params,
+ id: 1
+ };
+
+ if (auth === "") {
+ // Reject immediately if not authenticated
+ return Promise.reject(new ZabbixAPIError({ data: "Not authorised." }));
+ } else if (auth) {
+ // Set auth parameter only if it needed
+ requestData.auth = auth;
+ }
+
+ var requestOptions = {
+ method: 'POST',
+ url: api_url,
+ data: requestData,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ };
+
+ // Set request options for basic auth
+ if (options.basicAuth || options.withCredentials) {
+ requestOptions.withCredentials = true;
+ }
+ if (options.basicAuth) {
+ requestOptions.headers.Authorization = options.basicAuth;
+ }
+
+ return this.datasourceRequest(requestOptions);
+ }
+ }, {
+ key: 'datasourceRequest',
+ value: function datasourceRequest(requestOptions) {
+ return this.backendSrv.datasourceRequest(requestOptions).then(function (response) {
+ if (!response.data) {
+ return Promise.reject(new ZabbixAPIError({ data: "General Error, no data" }));
+ } else if (response.data.error) {
+
+ // Handle Zabbix API errors
+ return Promise.reject(new ZabbixAPIError(response.data.error));
+ }
+
+ // Success
+ return response.data.result;
+ });
+ }
+
+ /**
+ * Get authentication token.
+ * @return {string} auth token
+ */
+
+ }, {
+ key: 'login',
+ value: function login(api_url, username, password, options) {
+ var params = {
+ user: username,
+ password: password
+ };
+ return this.request(api_url, 'user.login', params, options, null);
+ }
+
+ /**
+ * Get Zabbix API version
+ * Matches the version of Zabbix starting from Zabbix 2.0.4
+ */
+
+ }, {
+ key: 'getVersion',
+ value: function getVersion(api_url, options) {
+ return this.request(api_url, 'apiinfo.version', [], options);
+ }
+ }]);
+
+ return ZabbixAPICoreService;
+}();
+
+// Define zabbix API exception type
+
+
+var ZabbixAPIError = exports.ZabbixAPIError = function () {
+ function ZabbixAPIError(error) {
+ _classCallCheck(this, ZabbixAPIError);
+
+ this.code = error.code || null;
+ this.name = error.message || "";
+ this.data = error.data || "";
+ this.message = "Zabbix API Error: " + this.name + " " + this.data;
+ }
+
+ _createClass(ZabbixAPIError, [{
+ key: 'toString',
+ value: function toString() {
+ return this.name + " " + this.data;
+ }
+ }]);
+
+ return ZabbixAPIError;
+}();
+
+_angular2.default.module('grafana.services').service('zabbixAPICoreService', ZabbixAPICoreService);
diff --git a/dist/test/datasource-zabbix/zabbixAlerting.service.js b/dist/test/datasource-zabbix/zabbixAlerting.service.js
new file mode 100644
index 0000000..4f1135e
--- /dev/null
+++ b/dist/test/datasource-zabbix/zabbixAlerting.service.js
@@ -0,0 +1,127 @@
+'use strict';
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _jquery = require('jquery');
+
+var _jquery2 = _interopRequireDefault(_jquery);
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var ZabbixAlertingService = function () {
+
+ /** @ngInject */
+ function ZabbixAlertingService(dashboardSrv) {
+ _classCallCheck(this, ZabbixAlertingService);
+
+ this.dashboardSrv = dashboardSrv;
+ }
+
+ _createClass(ZabbixAlertingService, [{
+ key: 'isFullScreen',
+ value: function isFullScreen() {
+ return this.dashboardSrv.dash.meta.fullscreen;
+ }
+ }, {
+ key: 'setPanelAlertState',
+ value: function setPanelAlertState(panelId, alertState) {
+ var panelIndex = void 0;
+
+ var panelContainers = _lodash2.default.filter((0, _jquery2.default)('.panel-container'), function (elem) {
+ return elem.clientHeight && elem.clientWidth;
+ });
+
+ var panelModels = this.getPanelModels();
+
+ if (this.isFullScreen()) {
+ panelIndex = 0;
+ } else {
+ panelIndex = _lodash2.default.findIndex(panelModels, function (panel) {
+ return panel.id === panelId;
+ });
+ }
+
+ if (panelIndex >= 0) {
+ var alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
+ (0, _jquery2.default)(panelContainers[panelIndex]).removeClass(alertClass);
+
+ if (alertState) {
+ if (alertState === 'alerting') {
+ alertClass = "panel-has-alert panel-alert-state--" + alertState;
+ (0, _jquery2.default)(panelContainers[panelIndex]).addClass(alertClass);
+ }
+ if (alertState === 'ok') {
+ alertClass = "panel-alert-state--" + alertState;
+ (0, _jquery2.default)(panelContainers[panelIndex]).addClass(alertClass);
+ (0, _jquery2.default)(panelContainers[panelIndex]).removeClass("panel-has-alert");
+ }
+ }
+ }
+ }
+ }, {
+ key: 'getPanelModels',
+ value: function getPanelModels() {
+ return _lodash2.default.flatten(_lodash2.default.map(this.dashboardSrv.dash.rows, function (row) {
+ if (row.collapse) {
+ return [];
+ } else {
+ return row.panels;
+ }
+ }));
+ }
+ }, {
+ key: 'getPanelModel',
+ value: function getPanelModel(panelId) {
+ var panelModels = this.getPanelModels();
+
+ return _lodash2.default.find(panelModels, function (panel) {
+ return panel.id === panelId;
+ });
+ }
+ }, {
+ key: 'setPanelThreshold',
+ value: function setPanelThreshold(panelId, threshold) {
+ var panel = this.getPanelModel(panelId);
+ var containsThreshold = _lodash2.default.find(panel.thresholds, { value: threshold });
+
+ if (panel && panel.type === "graph" && !containsThreshold) {
+ var thresholdOptions = {
+ colorMode: "custom",
+ fill: false,
+ line: true,
+ lineColor: "rgb(255, 0, 0)",
+ op: "gt",
+ value: threshold,
+ source: "zabbix"
+ };
+
+ panel.thresholds.push(thresholdOptions);
+ }
+ }
+ }, {
+ key: 'removeZabbixThreshold',
+ value: function removeZabbixThreshold(panelId) {
+ var panel = this.getPanelModel(panelId);
+
+ if (panel && panel.type === "graph") {
+ panel.thresholds = _lodash2.default.filter(panel.thresholds, function (threshold) {
+ return threshold.source !== "zabbix";
+ });
+ }
+ }
+ }]);
+
+ return ZabbixAlertingService;
+}();
+
+_angular2.default.module('grafana.services').service('zabbixAlertingSrv', ZabbixAlertingService);
diff --git a/dist/test/datasource-zabbix/zabbixCachingProxy.service.js b/dist/test/datasource-zabbix/zabbixCachingProxy.service.js
new file mode 100644
index 0000000..a3b12f5
--- /dev/null
+++ b/dist/test/datasource-zabbix/zabbixCachingProxy.service.js
@@ -0,0 +1,263 @@
+'use strict';
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+// Use factory() instead service() for multiple datasources support.
+// Each datasource instance must initialize its own cache.
+
+/** @ngInject */
+function ZabbixCachingProxyFactory() {
+ var ZabbixCachingProxy = function () {
+ function ZabbixCachingProxy(zabbixAPI, zabbixDBConnector, cacheOptions) {
+ _classCallCheck(this, ZabbixCachingProxy);
+
+ this.zabbixAPI = zabbixAPI;
+ this.dbConnector = zabbixDBConnector;
+ this.cacheEnabled = cacheOptions.enabled;
+ this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
+
+ // Internal objects for data storing
+ this.cache = {
+ groups: {},
+ hosts: {},
+ applications: {},
+ items: {},
+ history: {},
+ trends: {},
+ macros: {},
+ globalMacros: {},
+ itServices: {}
+ };
+
+ this.historyPromises = {};
+
+ // Don't run duplicated history requests
+ this.getHistory = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getHistory, this.zabbixAPI), this.historyPromises, getHistoryRequestHash);
+
+ if (this.dbConnector) {
+ this.getHistoryDB = callAPIRequestOnce(_lodash2.default.bind(this.dbConnector.getHistory, this.dbConnector), this.historyPromises, getDBQueryHash);
+ this.getTrendsDB = callAPIRequestOnce(_lodash2.default.bind(this.dbConnector.getTrends, this.dbConnector), this.historyPromises, getDBQueryHash);
+ }
+
+ // Don't run duplicated requests
+ this.groupPromises = {};
+ this.getGroupsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getGroups, this.zabbixAPI), this.groupPromises, getRequestHash);
+
+ this.hostPromises = {};
+ this.getHostsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getHosts, this.zabbixAPI), this.hostPromises, getRequestHash);
+
+ this.appPromises = {};
+ this.getAppsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getApps, this.zabbixAPI), this.appPromises, getRequestHash);
+
+ this.itemPromises = {};
+ this.getItemsOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getItems, this.zabbixAPI), this.itemPromises, getRequestHash);
+
+ this.itemByIdPromises = {};
+ this.getItemsByIdOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getItemsByIDs, this.zabbixAPI), this.itemPromises, getRequestHash);
+
+ this.itServicesPromises = {};
+ this.getITServicesOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getITService, this.zabbixAPI), this.itServicesPromises, getRequestHash);
+
+ this.macroPromises = {};
+ this.getMacrosOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getMacros, this.zabbixAPI), this.macroPromises, getRequestHash);
+
+ this.globalMacroPromises = {};
+ this.getGlobalMacrosOnce = callAPIRequestOnce(_lodash2.default.bind(this.zabbixAPI.getGlobalMacros, this.zabbixAPI), this.globalMacroPromises, getRequestHash);
+ }
+
+ _createClass(ZabbixCachingProxy, [{
+ key: 'isExpired',
+ value: function isExpired(cacheObject) {
+ if (cacheObject) {
+ var object_age = Date.now() - cacheObject.timestamp;
+ return !(cacheObject.timestamp && object_age < this.ttl);
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Check that result is present in cache and up to date
+ * or send request to API.
+ */
+
+ }, {
+ key: 'proxyRequest',
+ value: function proxyRequest(request, params, cacheObject) {
+ var hash = getRequestHash(params);
+ if (this.cacheEnabled && !this.isExpired(cacheObject[hash])) {
+ return Promise.resolve(cacheObject[hash].value);
+ } else {
+ return request.apply(undefined, _toConsumableArray(params)).then(function (result) {
+ cacheObject[hash] = {
+ value: result,
+ timestamp: Date.now()
+ };
+ return result;
+ });
+ }
+ }
+ }, {
+ key: 'getGroups',
+ value: function getGroups() {
+ return this.proxyRequest(this.getGroupsOnce, [], this.cache.groups);
+ }
+ }, {
+ key: 'getHosts',
+ value: function getHosts(groupids) {
+ return this.proxyRequest(this.getHostsOnce, [groupids], this.cache.hosts);
+ }
+ }, {
+ key: 'getApps',
+ value: function getApps(hostids) {
+ return this.proxyRequest(this.getAppsOnce, [hostids], this.cache.applications);
+ }
+ }, {
+ key: 'getItems',
+ value: function getItems(hostids, appids, itemtype) {
+ var params = [hostids, appids, itemtype];
+ return this.proxyRequest(this.getItemsOnce, params, this.cache.items);
+ }
+ }, {
+ key: 'getItemsByIDs',
+ value: function getItemsByIDs(itemids) {
+ var params = [itemids];
+ return this.proxyRequest(this.getItemsByIdOnce, params, this.cache.items);
+ }
+ }, {
+ key: 'getITServices',
+ value: function getITServices() {
+ return this.proxyRequest(this.getITServicesOnce, [], this.cache.itServices);
+ }
+ }, {
+ key: 'getMacros',
+ value: function getMacros(hostids) {
+ // Merge global macros and host macros
+ var promises = [this.proxyRequest(this.getMacrosOnce, [hostids], this.cache.macros), this.proxyRequest(this.getGlobalMacrosOnce, [], this.cache.globalMacros)];
+
+ return Promise.all(promises).then(_lodash2.default.flatten);
+ }
+ }, {
+ key: 'getHistoryFromCache',
+ value: function getHistoryFromCache(items, time_from, time_till) {
+ var historyStorage = this.cache.history;
+ var full_history;
+ var expired = _lodash2.default.filter(_lodash2.default.keyBy(items, 'itemid'), function (item, itemid) {
+ return !historyStorage[itemid];
+ });
+ if (expired.length) {
+ return this.zabbixAPI.getHistory(expired, time_from, time_till).then(function (history) {
+ var grouped_history = _lodash2.default.groupBy(history, 'itemid');
+ _lodash2.default.forEach(expired, function (item) {
+ var itemid = item.itemid;
+ historyStorage[itemid] = item;
+ historyStorage[itemid].time_from = time_from;
+ historyStorage[itemid].time_till = time_till;
+ historyStorage[itemid].history = grouped_history[itemid];
+ });
+ full_history = _lodash2.default.map(items, function (item) {
+ return historyStorage[item.itemid].history;
+ });
+ return _lodash2.default.flatten(full_history, true);
+ });
+ } else {
+ full_history = _lodash2.default.map(items, function (item) {
+ return historyStorage[item.itemid].history;
+ });
+ return Promise.resolve(_lodash2.default.flatten(full_history, true));
+ }
+ }
+ }, {
+ key: 'getHistoryFromAPI',
+ value: function getHistoryFromAPI(items, time_from, time_till) {
+ return this.zabbixAPI.getHistory(items, time_from, time_till);
+ }
+ }]);
+
+ return ZabbixCachingProxy;
+ }();
+
+ return ZabbixCachingProxy;
+}
+
+_angular2.default.module('grafana.services').factory('ZabbixCachingProxy', ZabbixCachingProxyFactory);
+
+/**
+ * Wrap zabbix API request to prevent multiple calls
+ * with same params when waiting for result.
+ */
+function callAPIRequestOnce(func, promiseKeeper, argsHashFunc) {
+ return function () {
+ var hash = argsHashFunc(arguments);
+ if (!promiseKeeper[hash]) {
+ promiseKeeper[hash] = Promise.resolve(func.apply(this, arguments).then(function (result) {
+ promiseKeeper[hash] = null;
+ return result;
+ }));
+ }
+ return promiseKeeper[hash];
+ };
+}
+
+function getRequestHash(args) {
+ var requestStamp = _lodash2.default.map(args, function (arg) {
+ if (arg === undefined) {
+ return 'undefined';
+ } else {
+ if (_lodash2.default.isArray(arg)) {
+ return arg.sort().toString();
+ } else {
+ return arg.toString();
+ }
+ }
+ }).join();
+ return requestStamp.getHash();
+}
+
+function getHistoryRequestHash(args) {
+ var itemids = _lodash2.default.map(args[0], 'itemid');
+ var stamp = itemids.join() + args[1] + args[2];
+ return stamp.getHash();
+}
+
+function getDBQueryHash(args) {
+ var itemids = _lodash2.default.map(args[0], 'itemid');
+ var consolidateBy = args[3].consolidateBy;
+ var intervalMs = args[3].intervalMs;
+ var stamp = itemids.join() + args[1] + args[2] + consolidateBy + intervalMs;
+ return stamp.getHash();
+}
+
+String.prototype.getHash = function () {
+ var hash = 0,
+ i,
+ chr,
+ len;
+ if (this.length !== 0) {
+ for (i = 0, len = this.length; i < len; i++) {
+ chr = this.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ }
+ return hash;
+};
+
+// Fix for backward compatibility with lodash 2.4
+if (!_lodash2.default.keyBy) {
+ _lodash2.default.keyBy = _lodash2.default.indexBy;
+}
diff --git a/dist/test/datasource-zabbix/zabbixDBConnector.js b/dist/test/datasource-zabbix/zabbixDBConnector.js
new file mode 100644
index 0000000..49eb5d0
--- /dev/null
+++ b/dist/test/datasource-zabbix/zabbixDBConnector.js
@@ -0,0 +1,287 @@
+'use strict';
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var DEFAULT_QUERY_LIMIT = 10000;
+var HISTORY_TO_TABLE_MAP = {
+ '0': 'history',
+ '1': 'history_str',
+ '2': 'history_log',
+ '3': 'history_uint',
+ '4': 'history_text'
+};
+
+var TREND_TO_TABLE_MAP = {
+ '0': 'trends',
+ '3': 'trends_uint'
+};
+
+var consolidateByFunc = {
+ 'avg': 'AVG',
+ 'min': 'MIN',
+ 'max': 'MAX',
+ 'sum': 'SUM',
+ 'count': 'COUNT'
+};
+
+var consolidateByTrendColumns = {
+ 'avg': 'value_avg',
+ 'min': 'value_min',
+ 'max': 'value_max'
+};
+
+/** @ngInject */
+function ZabbixDBConnectorFactory(datasourceSrv, backendSrv) {
+ var ZabbixDBConnector = function () {
+ function ZabbixDBConnector(sqlDataSourceId) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+ _classCallCheck(this, ZabbixDBConnector);
+
+ var limit = options.limit;
+
+
+ this.sqlDataSourceId = sqlDataSourceId;
+ this.limit = limit || DEFAULT_QUERY_LIMIT;
+
+ this.loadSQLDataSource(sqlDataSourceId);
+ }
+
+ /**
+ * Try to load DS with given id to check it's exist.
+ * @param {*} datasourceId ID of SQL data source
+ */
+
+
+ _createClass(ZabbixDBConnector, [{
+ key: 'loadSQLDataSource',
+ value: function loadSQLDataSource(datasourceId) {
+ var _this = this;
+
+ var ds = _lodash2.default.find(datasourceSrv.getAll(), { 'id': datasourceId });
+ if (ds) {
+ return datasourceSrv.loadDatasource(ds.name).then(function (ds) {
+ _this.sqlDataSourceType = ds.meta.id;
+ return ds;
+ });
+ } else {
+ return Promise.reject('SQL Data Source with ID ' + datasourceId + ' not found');
+ }
+ }
+
+ /**
+ * Try to invoke test query for one of Zabbix database tables.
+ */
+
+ }, {
+ key: 'testSQLDataSource',
+ value: function testSQLDataSource() {
+ var testQuery = TEST_MYSQL_QUERY;
+ if (this.sqlDataSourceType === 'postgres') {
+ testQuery = TEST_POSTGRES_QUERY;
+ }
+ return this.invokeSQLQuery(testQuery);
+ }
+ }, {
+ key: 'getHistory',
+ value: function getHistory(items, timeFrom, timeTill, options) {
+ var _this2 = this;
+
+ var intervalMs = options.intervalMs,
+ consolidateBy = options.consolidateBy;
+
+ var intervalSec = Math.ceil(intervalMs / 1000);
+
+ consolidateBy = consolidateBy || 'avg';
+ var aggFunction = consolidateByFunc[consolidateBy];
+
+ // Group items by value type and perform request for each value type
+ var grouped_items = _lodash2.default.groupBy(items, 'value_type');
+ var promises = _lodash2.default.map(grouped_items, function (items, value_type) {
+ var itemids = _lodash2.default.map(items, 'itemid').join(', ');
+ var table = HISTORY_TO_TABLE_MAP[value_type];
+
+ var dialect = _this2.sqlDataSourceType;
+ var query = buildSQLHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, dialect);
+
+ query = compactSQLQuery(query);
+ return _this2.invokeSQLQuery(query);
+ });
+
+ return Promise.all(promises).then(function (results) {
+ return _lodash2.default.flatten(results);
+ });
+ }
+ }, {
+ key: 'getTrends',
+ value: function getTrends(items, timeFrom, timeTill, options) {
+ var _this3 = this;
+
+ var intervalMs = options.intervalMs,
+ consolidateBy = options.consolidateBy;
+
+ var intervalSec = Math.ceil(intervalMs / 1000);
+
+ consolidateBy = consolidateBy || 'avg';
+ var aggFunction = consolidateByFunc[consolidateBy];
+
+ // Group items by value type and perform request for each value type
+ var grouped_items = _lodash2.default.groupBy(items, 'value_type');
+ var promises = _lodash2.default.map(grouped_items, function (items, value_type) {
+ var itemids = _lodash2.default.map(items, 'itemid').join(', ');
+ var table = TREND_TO_TABLE_MAP[value_type];
+ var valueColumn = _lodash2.default.includes(['avg', 'min', 'max'], consolidateBy) ? consolidateBy : 'avg';
+ valueColumn = consolidateByTrendColumns[valueColumn];
+
+ var dialect = _this3.sqlDataSourceType;
+ var query = buildSQLTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn, dialect);
+
+ query = compactSQLQuery(query);
+ return _this3.invokeSQLQuery(query);
+ });
+
+ return Promise.all(promises).then(function (results) {
+ return _lodash2.default.flatten(results);
+ });
+ }
+ }, {
+ key: 'handleGrafanaTSResponse',
+ value: function handleGrafanaTSResponse(history, items) {
+ var addHostName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
+
+ return convertGrafanaTSResponse(history, items, addHostName);
+ }
+ }, {
+ key: 'invokeSQLQuery',
+ value: function invokeSQLQuery(query) {
+ var queryDef = {
+ refId: 'A',
+ format: 'time_series',
+ datasourceId: this.sqlDataSourceId,
+ rawSql: query,
+ maxDataPoints: this.limit
+ };
+
+ return backendSrv.datasourceRequest({
+ url: '/api/tsdb/query',
+ method: 'POST',
+ data: {
+ queries: [queryDef]
+ }
+ }).then(function (response) {
+ var results = response.data.results;
+ if (results['A']) {
+ return results['A'].series;
+ } else {
+ return null;
+ }
+ });
+ }
+ }]);
+
+ return ZabbixDBConnector;
+ }();
+
+ return ZabbixDBConnector;
+}
+
+_angular2.default.module('grafana.services').factory('ZabbixDBConnector', ZabbixDBConnectorFactory);
+
+///////////////////////////////////////////////////////////////////////////////
+
+function convertGrafanaTSResponse(time_series, items, addHostName) {
+ var hosts = _lodash2.default.uniqBy(_lodash2.default.flatten(_lodash2.default.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
+ var grafanaSeries = _lodash2.default.map(time_series, function (series) {
+ var itemid = series.name;
+ var item = _lodash2.default.find(items, { 'itemid': itemid });
+ var alias = item.name;
+ if (_lodash2.default.keys(hosts).length > 1 && addHostName) {
+ //only when actual multi hosts selected
+ var host = _lodash2.default.find(hosts, { 'hostid': item.hostid });
+ alias = host.name + ": " + alias;
+ }
+ // zabbixCachingProxy deduplicates requests and returns one time series for equal queries.
+ // Clone is needed to prevent changing of series object shared between all targets.
+ var datapoints = _lodash2.default.cloneDeep(series.points);
+ return {
+ target: alias,
+ datapoints: datapoints
+ };
+ });
+
+ return _lodash2.default.sortBy(grafanaSeries, 'target');
+}
+
+function compactSQLQuery(query) {
+ return query.replace(/\s+/g, ' ');
+}
+
+function buildSQLHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
+ var dialect = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : 'mysql';
+
+ if (dialect === 'postgres') {
+ return buildPostgresHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction);
+ } else {
+ return buildMysqlHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction);
+ }
+}
+
+function buildSQLTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
+ var dialect = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 'mysql';
+
+ if (dialect === 'postgres') {
+ return buildPostgresTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn);
+ } else {
+ return buildMysqlTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn);
+ }
+}
+
+///////////
+// MySQL //
+///////////
+
+function buildMysqlHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
+ var time_expression = 'clock DIV ' + intervalSec + ' * ' + intervalSec;
+ var query = '\n SELECT itemid AS metric, ' + time_expression + ' AS time_sec, ' + aggFunction + '(value) AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY ' + time_expression + ', metric\n ORDER BY time_sec ASC\n ';
+ return query;
+}
+
+function buildMysqlTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
+ var time_expression = 'clock DIV ' + intervalSec + ' * ' + intervalSec;
+ var query = '\n SELECT itemid AS metric, ' + time_expression + ' AS time_sec, ' + aggFunction + '(' + valueColumn + ') AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY ' + time_expression + ', metric\n ORDER BY time_sec ASC\n ';
+ return query;
+}
+
+var TEST_MYSQL_QUERY = 'SELECT itemid AS metric, clock AS time_sec, value_avg AS value FROM trends_uint LIMIT 1';
+
+////////////////
+// PostgreSQL //
+////////////////
+
+var itemid_format = 'FM99999999999999999999';
+
+function buildPostgresHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
+ var time_expression = 'clock / ' + intervalSec + ' * ' + intervalSec;
+ var query = '\n SELECT to_char(itemid, \'' + itemid_format + '\') AS metric, ' + time_expression + ' AS time, ' + aggFunction + '(value) AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY 1, 2\n ORDER BY time ASC\n ';
+ return query;
+}
+
+function buildPostgresTrendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
+ var time_expression = 'clock / ' + intervalSec + ' * ' + intervalSec;
+ var query = '\n SELECT to_char(itemid, \'' + itemid_format + '\') AS metric, ' + time_expression + ' AS time, ' + aggFunction + '(' + valueColumn + ') AS value\n FROM ' + table + '\n WHERE itemid IN (' + itemids + ')\n AND clock > ' + timeFrom + ' AND clock < ' + timeTill + '\n GROUP BY 1, 2\n ORDER BY time ASC\n ';
+ return query;
+}
+
+var TEST_POSTGRES_QUERY = '\n SELECT to_char(itemid, \'' + itemid_format + '\') AS metric, clock AS time, value_avg AS value\n FROM trends_uint LIMIT 1\n';
diff --git a/dist/test/module.js b/dist/test/module.js
new file mode 100644
index 0000000..ff7f441
--- /dev/null
+++ b/dist/test/module.js
@@ -0,0 +1,17 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ConfigCtrl = undefined;
+
+var _config = require('./components/config');
+
+var _sdk = require('app/plugins/sdk');
+
+(0, _sdk.loadPluginCss)({
+ dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
+ light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css'
+});
+
+exports.ConfigCtrl = _config.ZabbixAppConfigCtrl;
diff --git a/dist/test/panel-triggers/ack-tooltip.directive.js b/dist/test/panel-triggers/ack-tooltip.directive.js
new file mode 100644
index 0000000..59e9c8d
--- /dev/null
+++ b/dist/test/panel-triggers/ack-tooltip.directive.js
@@ -0,0 +1,129 @@
+'use strict';
+
+var _angular = require('angular');
+
+var _angular2 = _interopRequireDefault(_angular);
+
+var _jquery = require('jquery');
+
+var _jquery2 = _interopRequireDefault(_jquery);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+System.config({
+ paths: {
+ tether: System.getConfig().baseURL + "plugins/alexanderzobnin-zabbix-app/vendor/npm/tether.min.js"
+ }
+});
+
+var Drop = void 0;
+System.amdRequire(["plugins/alexanderzobnin-zabbix-app/vendor/npm/drop.min.js"], function (drop) {
+ Drop = drop;
+});
+
+/** @ngInject */
+_angular2.default.module('grafana.directives').directive('ackTooltip', function ($sanitize, $compile) {
+ var buttonTemplate = ' ';
+
+ return {
+ scope: {
+ ack: "=",
+ trigger: "=",
+ onAck: "=",
+ context: "="
+ },
+ link: function link(scope, element) {
+ var acknowledges = scope.ack;
+ var $button = (0, _jquery2.default)(buttonTemplate);
+ $button.appendTo(element);
+
+ $button.click(function () {
+ var tooltip = '';
+
+ if (acknowledges && acknowledges.length) {
+ tooltip += '
' + 'Time ' + 'User ' + '' + ' ';
+ var _iteratorNormalCompletion = true;
+ var _didIteratorError = false;
+ var _iteratorError = undefined;
+
+ try {
+ for (var _iterator = acknowledges[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+ var ack = _step.value;
+
+ tooltip += '' + ack.time + ' ' + '' + ack.user + ' ' + '' + ack.message + ' ';
+ }
+ } catch (err) {
+ _didIteratorError = true;
+ _iteratorError = err;
+ } finally {
+ try {
+ if (!_iteratorNormalCompletion && _iterator.return) {
+ _iterator.return();
+ }
+ } finally {
+ if (_didIteratorError) {
+ throw _iteratorError;
+ }
+ }
+ }
+
+ tooltip += '
';
+ } else {
+ tooltip += 'Add acknowledge';
+ }
+
+ var addAckButtonTemplate = '
' + '' + ' ' + '
';
+ tooltip += addAckButtonTemplate;
+ tooltip += '
';
+
+ var drop = new Drop({
+ target: element[0],
+ content: tooltip,
+ position: "bottom left",
+ classes: 'drop-popover ack-tooltip',
+ openOn: 'hover',
+ hoverCloseDelay: 500,
+ tetherOptions: {
+ constraints: [{ to: 'window', pin: true, attachment: "both" }]
+ }
+ });
+
+ drop.open();
+ drop.on('close', closeDrop);
+
+ (0, _jquery2.default)('#add-acknowledge-btn').on('click', onAddAckButtonClick);
+
+ function onAddAckButtonClick() {
+ var inputTemplate = '' + ' ' + '' + 'Acknowledge ' + '' + 'Cancel' + '
';
+
+ var $input = (0, _jquery2.default)(inputTemplate);
+ var $addAckButton = (0, _jquery2.default)('.ack-tooltip .ack-add-button');
+ $addAckButton.replaceWith($input);
+ (0, _jquery2.default)('.ack-tooltip #cancel-ack-button').on('click', onAckCancelButtonClick);
+ (0, _jquery2.default)('.ack-tooltip #send-ack-button').on('click', onAckSendlButtonClick);
+ }
+
+ function onAckCancelButtonClick() {
+ (0, _jquery2.default)('.ack-tooltip .ack-input-group').replaceWith(addAckButtonTemplate);
+ (0, _jquery2.default)('#add-acknowledge-btn').on('click', onAddAckButtonClick);
+ }
+
+ function onAckSendlButtonClick() {
+ var message = (0, _jquery2.default)('.ack-tooltip #ack-message')[0].value;
+ var onAck = scope.onAck.bind(scope.context);
+ onAck(scope.trigger, message).then(function () {
+ closeDrop();
+ });
+ }
+
+ function closeDrop() {
+ setTimeout(function () {
+ drop.destroy();
+ });
+ }
+ });
+
+ $compile(element.contents())(scope);
+ }
+ };
+});
diff --git a/dist/test/panel-triggers/editor.js b/dist/test/panel-triggers/editor.js
new file mode 100644
index 0000000..1403a32
--- /dev/null
+++ b/dist/test/panel-triggers/editor.js
@@ -0,0 +1,224 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /**
+ * Grafana-Zabbix
+ * Zabbix plugin for Grafana.
+ * http://github.com/alexanderzobnin/grafana-zabbix
+ *
+ * Trigger panel.
+ * This feature sponsored by CORE IT
+ * http://www.coreit.fr
+ *
+ * Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com
+ * Licensed under the Apache License, Version 2.0
+ */
+
+exports.triggerPanelEditor = triggerPanelEditor;
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _utils = require('../datasource-zabbix/utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+require('../datasource-zabbix/css/query-editor.css!');
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var TriggerPanelEditorCtrl = function () {
+
+ /** @ngInject */
+ function TriggerPanelEditorCtrl($scope, $rootScope, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) {
+ var _this = this;
+
+ _classCallCheck(this, TriggerPanelEditorCtrl);
+
+ $scope.editor = this;
+ this.panelCtrl = $scope.ctrl;
+ this.panel = this.panelCtrl.panel;
+
+ this.datasourceSrv = datasourceSrv;
+ this.templateSrv = templateSrv;
+ this.popoverSrv = popoverSrv;
+
+ // Map functions for bs-typeahead
+ this.getGroupNames = _lodash2.default.partial(getMetricNames, this, 'groupList');
+ this.getHostNames = _lodash2.default.partial(getMetricNames, this, 'hostList');
+ this.getApplicationNames = _lodash2.default.partial(getMetricNames, this, 'appList');
+
+ // Update metric suggestion when template variable was changed
+ $rootScope.$on('template-variable-value-updated', function () {
+ return _this.onVariableChange();
+ });
+
+ this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
+ this.ackFilters = ['all triggers', 'unacknowledged', 'acknowledged'];
+ this.sortByFields = [{ text: 'last change', value: 'lastchange' }, { text: 'severity', value: 'priority' }];
+ this.showEventsFields = [{ text: 'All', value: [0, 1] }, { text: 'OK', value: [0] }, { text: 'Problems', value: 1 }];
+
+ // Load scope defaults
+ var scopeDefaults = {
+ metric: {},
+ inputStyles: {},
+ oldTarget: _lodash2.default.cloneDeep(this.panel.triggers)
+ };
+ _lodash2.default.defaults(this, scopeDefaults);
+
+ // Set default datasource
+ this.datasources = _lodash2.default.map(this.getZabbixDataSources(), 'name');
+ if (!this.panel.datasource) {
+ this.panel.datasource = this.datasources[0];
+ }
+ // Load datasource
+ this.datasourceSrv.get(this.panel.datasource).then(function (datasource) {
+ _this.datasource = datasource;
+ _this.zabbix = datasource.zabbix;
+ _this.queryBuilder = datasource.queryBuilder;
+ _this.initFilters();
+ _this.panelCtrl.refresh();
+ });
+ }
+
+ _createClass(TriggerPanelEditorCtrl, [{
+ key: 'initFilters',
+ value: function initFilters() {
+ return Promise.all([this.suggestGroups(), this.suggestHosts(), this.suggestApps()]);
+ }
+ }, {
+ key: 'suggestGroups',
+ value: function suggestGroups() {
+ var _this2 = this;
+
+ return this.zabbix.getAllGroups().then(function (groups) {
+ _this2.metric.groupList = groups;
+ return groups;
+ });
+ }
+ }, {
+ key: 'suggestHosts',
+ value: function suggestHosts() {
+ var _this3 = this;
+
+ var groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter);
+ return this.zabbix.getAllHosts(groupFilter).then(function (hosts) {
+ _this3.metric.hostList = hosts;
+ return hosts;
+ });
+ }
+ }, {
+ key: 'suggestApps',
+ value: function suggestApps() {
+ var _this4 = this;
+
+ var groupFilter = this.datasource.replaceTemplateVars(this.panel.triggers.group.filter);
+ var hostFilter = this.datasource.replaceTemplateVars(this.panel.triggers.host.filter);
+ return this.zabbix.getAllApps(groupFilter, hostFilter).then(function (apps) {
+ _this4.metric.appList = apps;
+ return apps;
+ });
+ }
+ }, {
+ key: 'onVariableChange',
+ value: function onVariableChange() {
+ if (this.isContainsVariables()) {
+ this.targetChanged();
+ }
+ }
+
+ /**
+ * Check query for template variables
+ */
+
+ }, {
+ key: 'isContainsVariables',
+ value: function isContainsVariables() {
+ var _this5 = this;
+
+ return _lodash2.default.some(['group', 'host', 'application'], function (field) {
+ return utils.isTemplateVariable(_this5.panel.triggers[field].filter, _this5.templateSrv.variables);
+ });
+ }
+ }, {
+ key: 'targetChanged',
+ value: function targetChanged() {
+ this.initFilters();
+ this.panelCtrl.refresh();
+ }
+ }, {
+ key: 'parseTarget',
+ value: function parseTarget() {
+ this.initFilters();
+ var newTarget = _lodash2.default.cloneDeep(this.panel.triggers);
+ if (!_lodash2.default.isEqual(this.oldTarget, this.panel.triggers)) {
+ this.oldTarget = newTarget;
+ this.panelCtrl.refresh();
+ }
+ }
+ }, {
+ key: 'refreshTriggerSeverity',
+ value: function refreshTriggerSeverity() {
+ _lodash2.default.each(this.triggerList, function (trigger) {
+ trigger.color = this.panel.triggerSeverity[trigger.priority].color;
+ trigger.severity = this.panel.triggerSeverity[trigger.priority].severity;
+ });
+ this.panelCtrl.refresh();
+ }
+ }, {
+ key: 'datasourceChanged',
+ value: function datasourceChanged() {
+ this.panelCtrl.refresh();
+ }
+ }, {
+ key: 'changeTriggerSeverityColor',
+ value: function changeTriggerSeverityColor(trigger, color) {
+ this.panel.triggerSeverity[trigger.priority].color = color;
+ this.refreshTriggerSeverity();
+ }
+ }, {
+ key: 'isRegex',
+ value: function isRegex(str) {
+ return utils.isRegex(str);
+ }
+ }, {
+ key: 'isVariable',
+ value: function isVariable(str) {
+ return utils.isTemplateVariable(str, this.templateSrv.variables);
+ }
+ }, {
+ key: 'getZabbixDataSources',
+ value: function getZabbixDataSources() {
+ var ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
+ return _lodash2.default.filter(this.datasourceSrv.getMetricSources(), function (datasource) {
+ return datasource.meta.id === ZABBIX_DS_ID && datasource.value;
+ });
+ }
+ }]);
+
+ return TriggerPanelEditorCtrl;
+}();
+
+// Get list of metric names for bs-typeahead directive
+
+
+function getMetricNames(scope, metricList) {
+ return _lodash2.default.uniq(_lodash2.default.map(scope.metric[metricList], 'name'));
+}
+
+function triggerPanelEditor() {
+ return {
+ restrict: 'E',
+ scope: true,
+ templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/editor.html',
+ controller: TriggerPanelEditorCtrl
+ };
+}
diff --git a/dist/test/panel-triggers/module.js b/dist/test/panel-triggers/module.js
new file mode 100644
index 0000000..e22f1ca
--- /dev/null
+++ b/dist/test/panel-triggers/module.js
@@ -0,0 +1,425 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PanelCtrl = exports.TriggerPanelCtrl = undefined;
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _jquery = require('jquery');
+
+var _jquery2 = _interopRequireDefault(_jquery);
+
+var _moment = require('moment');
+
+var _moment2 = _interopRequireDefault(_moment);
+
+var _sdk = require('app/plugins/sdk');
+
+var _utils = require('../datasource-zabbix/utils');
+
+var utils = _interopRequireWildcard(_utils);
+
+var _editor = require('./editor');
+
+require('./ack-tooltip.directive');
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /**
+ * Grafana-Zabbix
+ * Zabbix plugin for Grafana.
+ * http://github.com/alexanderzobnin/grafana-zabbix
+ *
+ * Trigger panel.
+ * This feature sponsored by CORE IT
+ * http://www.coreit.fr
+ *
+ * Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com
+ * Licensed under the Apache License, Version 2.0
+ */
+
+(0, _sdk.loadPluginCss)({
+ dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
+ light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css'
+});
+
+var defaultSeverity = [{ priority: 0, severity: 'Not classified', color: '#B7DBAB', show: true }, { priority: 1, severity: 'Information', color: '#82B5D8', show: true }, { priority: 2, severity: 'Warning', color: '#E5AC0E', show: true }, { priority: 3, severity: 'Average', color: '#C15C17', show: true }, { priority: 4, severity: 'High', color: '#BF1B00', show: true }, { priority: 5, severity: 'Disaster', color: '#890F02', show: true }];
+
+var panelDefaults = {
+ datasource: null,
+ triggers: {
+ group: { filter: "" },
+ host: { filter: "" },
+ application: { filter: "" },
+ trigger: { filter: "" }
+ },
+ hostField: true,
+ statusField: false,
+ severityField: false,
+ lastChangeField: true,
+ ageField: true,
+ infoField: true,
+ limit: 10,
+ showTriggers: 'all triggers',
+ hideHostsInMaintenance: false,
+ sortTriggersBy: { text: 'last change', value: 'lastchange' },
+ showEvents: { text: 'Problems', value: '1' },
+ triggerSeverity: defaultSeverity,
+ okEventColor: 'rgba(0, 245, 153, 0.45)',
+ ackEventColor: 'rgba(0, 0, 0, 0)',
+ scroll: true,
+ pageSize: 10,
+ fontSize: '100%'
+};
+
+var triggerStatusMap = {
+ '0': 'OK',
+ '1': 'Problem'
+};
+
+var defaultTimeFormat = "DD MMM YYYY HH:mm:ss";
+
+var TriggerPanelCtrl = function (_PanelCtrl) {
+ _inherits(TriggerPanelCtrl, _PanelCtrl);
+
+ /** @ngInject */
+ function TriggerPanelCtrl($scope, $injector, $element, datasourceSrv, templateSrv, contextSrv, dashboardSrv) {
+ _classCallCheck(this, TriggerPanelCtrl);
+
+ var _this = _possibleConstructorReturn(this, (TriggerPanelCtrl.__proto__ || Object.getPrototypeOf(TriggerPanelCtrl)).call(this, $scope, $injector));
+
+ _this.datasourceSrv = datasourceSrv;
+ _this.templateSrv = templateSrv;
+ _this.contextSrv = contextSrv;
+ _this.dashboardSrv = dashboardSrv;
+
+ _this.triggerStatusMap = triggerStatusMap;
+ _this.defaultTimeFormat = defaultTimeFormat;
+ _this.pageIndex = 0;
+ _this.triggerList = [];
+ _this.currentTriggersPage = [];
+
+ // Load panel defaults
+ // _.cloneDeep() need for prevent changing shared defaultSeverity.
+ // Load object "by value" istead "by reference".
+ _lodash2.default.defaults(_this.panel, _lodash2.default.cloneDeep(panelDefaults));
+
+ _this.events.on('init-edit-mode', _this.onInitEditMode.bind(_this));
+ _this.events.on('refresh', _this.onRefresh.bind(_this));
+ return _this;
+ }
+
+ _createClass(TriggerPanelCtrl, [{
+ key: 'onInitEditMode',
+ value: function onInitEditMode() {
+ this.addEditorTab('Options', _editor.triggerPanelEditor, 2);
+ }
+ }, {
+ key: 'onRefresh',
+ value: function onRefresh() {
+ var _this2 = this;
+
+ // ignore fetching data if another panel is in fullscreen
+ if (this.otherPanelInFullscreenMode()) {
+ return;
+ }
+
+ // clear loading/error state
+ delete this.error;
+ this.loading = true;
+
+ return this.refreshData().then(function (triggerList) {
+ // Limit triggers number
+ _this2.triggerList = triggerList.slice(0, _this2.panel.limit);
+
+ _this2.getCurrentTriggersPage();
+
+ // Notify panel that request is finished
+ _this2.loading = false;
+
+ _this2.render(_this2.triggerList);
+ });
+ }
+ }, {
+ key: 'refreshData',
+ value: function refreshData() {
+ return this.getTriggers().then(this.getAcknowledges.bind(this)).then(this.filterTriggers.bind(this));
+ }
+ }, {
+ key: 'getTriggers',
+ value: function getTriggers() {
+ var _this3 = this;
+
+ return this.datasourceSrv.get(this.panel.datasource).then(function (datasource) {
+ var zabbix = datasource.zabbix;
+ _this3.zabbix = zabbix;
+ _this3.datasource = datasource;
+ var showEvents = _this3.panel.showEvents.value;
+ var triggerFilter = _this3.panel.triggers;
+ var hideHostsInMaintenance = _this3.panel.hideHostsInMaintenance;
+
+ // Replace template variables
+ var groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter);
+ var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter);
+ var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter);
+
+ var triggersOptions = {
+ showTriggers: showEvents,
+ hideHostsInMaintenance: hideHostsInMaintenance
+ };
+
+ return zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions);
+ }).then(function (triggers) {
+ return _lodash2.default.map(triggers, _this3.formatTrigger.bind(_this3));
+ });
+ }
+ }, {
+ key: 'getAcknowledges',
+ value: function getAcknowledges(triggerList) {
+ var _this4 = this;
+
+ // Request acknowledges for trigger
+ var eventids = _lodash2.default.map(triggerList, function (trigger) {
+ return trigger.lastEvent.eventid;
+ });
+
+ return this.zabbix.getAcknowledges(eventids).then(function (events) {
+
+ // Map events to triggers
+ _lodash2.default.each(triggerList, function (trigger) {
+ var event = _lodash2.default.find(events, function (event) {
+ return event.eventid === trigger.lastEvent.eventid;
+ });
+
+ if (event) {
+ trigger.acknowledges = _lodash2.default.map(event.acknowledges, function (ack) {
+ var timestamp = _moment2.default.unix(ack.clock);
+ if (_this4.panel.customLastChangeFormat) {
+ ack.time = timestamp.format(_this4.panel.lastChangeFormat);
+ } else {
+ ack.time = timestamp.format(_this4.defaultTimeFormat);
+ }
+ ack.user = ack.alias + ' (' + ack.name + ' ' + ack.surname + ')';
+ return ack;
+ });
+
+ // Mark acknowledged triggers with different color
+ if (_this4.panel.markAckEvents && trigger.acknowledges.length) {
+ trigger.color = _this4.panel.ackEventColor;
+ }
+ }
+ });
+
+ return triggerList;
+ });
+ }
+ }, {
+ key: 'filterTriggers',
+ value: function filterTriggers(triggerList) {
+ var _this5 = this;
+
+ // Filter triggers by description
+ var triggerFilter = this.panel.triggers.trigger.filter;
+ triggerFilter = this.datasource.replaceTemplateVars(triggerFilter);
+ if (triggerFilter) {
+ triggerList = _filterTriggers(triggerList, triggerFilter);
+ }
+
+ // Filter acknowledged triggers
+ if (this.panel.showTriggers === 'unacknowledged') {
+ triggerList = _lodash2.default.filter(triggerList, function (trigger) {
+ return !trigger.acknowledges;
+ });
+ } else if (this.panel.showTriggers === 'acknowledged') {
+ triggerList = _lodash2.default.filter(triggerList, 'acknowledges');
+ } else {
+ triggerList = triggerList;
+ }
+
+ // Filter triggers by severity
+ triggerList = _lodash2.default.filter(triggerList, function (trigger) {
+ return _this5.panel.triggerSeverity[trigger.priority].show;
+ });
+
+ // Sort triggers
+ if (this.panel.sortTriggersBy.value === 'priority') {
+ triggerList = _lodash2.default.sortBy(triggerList, 'priority').reverse();
+ } else {
+ triggerList = _lodash2.default.sortBy(triggerList, 'lastchangeUnix').reverse();
+ }
+
+ return triggerList;
+ }
+ }, {
+ key: 'formatTrigger',
+ value: function formatTrigger(trigger) {
+ var triggerObj = trigger;
+
+ // Format last change and age
+ trigger.lastchangeUnix = Number(trigger.lastchange);
+ var timestamp = _moment2.default.unix(trigger.lastchangeUnix);
+ if (this.panel.customLastChangeFormat) {
+ // User defined format
+ triggerObj.lastchange = timestamp.format(this.panel.lastChangeFormat);
+ } else {
+ triggerObj.lastchange = timestamp.format(this.defaultTimeFormat);
+ }
+ triggerObj.age = timestamp.fromNow(true);
+
+ // Set host that the trigger belongs
+ if (trigger.hosts.length) {
+ triggerObj.host = trigger.hosts[0].name;
+ triggerObj.hostTechName = trigger.hosts[0].host;
+ }
+
+ // Set color
+ if (trigger.value === '1') {
+ // Problem state
+ triggerObj.color = this.panel.triggerSeverity[trigger.priority].color;
+ } else {
+ // OK state
+ triggerObj.color = this.panel.okEventColor;
+ }
+
+ triggerObj.severity = this.panel.triggerSeverity[trigger.priority].severity;
+ return triggerObj;
+ }
+ }, {
+ key: 'switchComment',
+ value: function switchComment(trigger) {
+ trigger.showComment = !trigger.showComment;
+ }
+ }, {
+ key: 'acknowledgeTrigger',
+ value: function acknowledgeTrigger(trigger, message) {
+ var eventid = trigger.lastEvent.eventid;
+ var grafana_user = this.contextSrv.user.name;
+ var ack_message = grafana_user + ' (Grafana): ' + message;
+ return this.datasourceSrv.get(this.panel.datasource).then(function (datasource) {
+ var zabbixAPI = datasource.zabbix.zabbixAPI;
+ return zabbixAPI.acknowledgeEvent(eventid, ack_message);
+ }).then(this.onRefresh.bind(this));
+ }
+ }, {
+ key: 'getCurrentTriggersPage',
+ value: function getCurrentTriggersPage() {
+ var pageSize = this.panel.pageSize || 10;
+ var startPos = this.pageIndex * pageSize;
+ var endPos = Math.min(startPos + pageSize, this.triggerList.length);
+ this.currentTriggersPage = this.triggerList.slice(startPos, endPos);
+ return this.currentTriggersPage;
+ }
+ }, {
+ key: 'link',
+ value: function link(scope, elem, attrs, ctrl) {
+ var data;
+ var panel = ctrl.panel;
+ var pageCount = 0;
+ data = ctrl.triggerList;
+
+ function getTableHeight() {
+ var panelHeight = ctrl.height;
+
+ if (pageCount > 1) {
+ panelHeight -= 26;
+ }
+
+ return panelHeight - 31 + 'px';
+ }
+
+ function switchPage(e) {
+ var el = (0, _jquery2.default)(e.currentTarget);
+ ctrl.pageIndex = parseInt(el.text(), 10) - 1;
+
+ var pageSize = ctrl.panel.pageSize || 10;
+ var startPos = ctrl.pageIndex * pageSize;
+ var endPos = Math.min(startPos + pageSize, ctrl.triggerList.length);
+ ctrl.currentTriggersPage = ctrl.triggerList.slice(startPos, endPos);
+
+ scope.$apply();
+ renderPanel();
+ }
+
+ function appendPaginationControls(footerElem) {
+ footerElem.empty();
+
+ var pageSize = ctrl.panel.pageSize || 5;
+ pageCount = Math.ceil(data.length / pageSize);
+ if (pageCount === 1) {
+ return;
+ }
+
+ var startPage = Math.max(ctrl.pageIndex - 3, 0);
+ var endPage = Math.min(pageCount, startPage + 9);
+
+ var paginationList = (0, _jquery2.default)('');
+
+ for (var i = startPage; i < endPage; i++) {
+ var activeClass = i === ctrl.pageIndex ? 'active' : '';
+ var pageLinkElem = (0, _jquery2.default)('' + (i + 1) + ' ');
+ paginationList.append(pageLinkElem);
+ }
+
+ footerElem.append(paginationList);
+ }
+
+ function renderPanel() {
+ var panelElem = elem.parents('.panel');
+ var rootElem = elem.find('.triggers-panel-scroll');
+ var footerElem = elem.find('.triggers-panel-footer');
+
+ elem.css({ 'font-size': panel.fontSize });
+ panelElem.addClass('triggers-panel-wrapper');
+ appendPaginationControls(footerElem);
+
+ rootElem.css({ 'max-height': panel.scroll ? getTableHeight() : '' });
+ }
+
+ elem.on('click', '.triggers-panel-page-link', switchPage);
+
+ var unbindDestroy = scope.$on('$destroy', function () {
+ elem.off('click', '.triggers-panel-page-link');
+ unbindDestroy();
+ });
+
+ ctrl.events.on('render', function (renderData) {
+ data = renderData || data;
+ if (data) {
+ renderPanel();
+ }
+ ctrl.renderingCompleted();
+ });
+ }
+ }]);
+
+ return TriggerPanelCtrl;
+}(_sdk.PanelCtrl);
+
+TriggerPanelCtrl.templateUrl = 'panel-triggers/module.html';
+
+function _filterTriggers(triggers, triggerFilter) {
+ if (utils.isRegex(triggerFilter)) {
+ return _lodash2.default.filter(triggers, function (trigger) {
+ return utils.buildRegex(triggerFilter).test(trigger.description);
+ });
+ } else {
+ return _lodash2.default.filter(triggers, function (trigger) {
+ return trigger.description === triggerFilter;
+ });
+ }
+}
+
+exports.TriggerPanelCtrl = TriggerPanelCtrl;
+exports.PanelCtrl = TriggerPanelCtrl;
diff --git a/dist/test/test-setup/cssStub.js b/dist/test/test-setup/cssStub.js
new file mode 100644
index 0000000..1f5824a
--- /dev/null
+++ b/dist/test/test-setup/cssStub.js
@@ -0,0 +1,3 @@
+"use strict";
+
+module.exports = {};
diff --git a/dist/test/test-setup/jest-setup.js b/dist/test/test-setup/jest-setup.js
new file mode 100644
index 0000000..31d1125
--- /dev/null
+++ b/dist/test/test-setup/jest-setup.js
@@ -0,0 +1,52 @@
+'use strict';
+
+var _jsdom = require('jsdom');
+
+// Mock Grafana modules that are not available outside of the core project
+// Required for loading module.js
+jest.mock('angular', function () {
+ return {
+ module: function module() {
+ return {
+ directive: function directive() {},
+ service: function service() {},
+ factory: function factory() {}
+ };
+ }
+ };
+}, { virtual: true }); // JSHint options
+/* globals global: false */
+
+jest.mock('app/plugins/sdk', function () {
+ return {
+ QueryCtrl: null
+ };
+}, { virtual: true });
+
+jest.mock('app/core/utils/datemath', function () {
+ var datemath = require('./modules/datemath');
+ return {
+ parse: datemath.parse,
+ parseDateMath: datemath.parseDateMath,
+ isValid: datemath.isValid
+ };
+}, { virtual: true });
+
+jest.mock('app/core/table_model', function () {
+ return {};
+}, { virtual: true });
+
+jest.mock('./css/query-editor.css!', function () {
+ return "";
+}, { virtual: true });
+
+jest.mock('jquery', function () {
+ return 'module not found';
+}, { virtual: true });
+
+// Required for loading angularjs
+var dom = new _jsdom.JSDOM('');
+// Setup jsdom
+global.window = dom.window;
+global.document = global.window.document;
+global.Node = window.Node;
diff --git a/dist/test/test-setup/modules/datemath.js b/dist/test/test-setup/modules/datemath.js
new file mode 100644
index 0000000..063ad04
--- /dev/null
+++ b/dist/test/test-setup/modules/datemath.js
@@ -0,0 +1,135 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.parse = parse;
+exports.isValid = isValid;
+exports.parseDateMath = parseDateMath;
+
+var _lodash = require('lodash');
+
+var _lodash2 = _interopRequireDefault(_lodash);
+
+var _moment = require('moment');
+
+var _moment2 = _interopRequireDefault(_moment);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
+
+function parse(text, roundUp) {
+ if (!text) {
+ return undefined;
+ }
+ if (_moment2.default.isMoment(text)) {
+ return text;
+ }
+ if (_lodash2.default.isDate(text)) {
+ return (0, _moment2.default)(text);
+ }
+
+ var time;
+ var mathString = '';
+ var index;
+ var parseString;
+
+ if (text.substring(0, 3) === 'now') {
+ time = (0, _moment2.default)();
+ mathString = text.substring('now'.length);
+ } else {
+ index = text.indexOf('||');
+ if (index === -1) {
+ parseString = text;
+ mathString = ''; // nothing else
+ } else {
+ parseString = text.substring(0, index);
+ mathString = text.substring(index + 2);
+ }
+ // We're going to just require ISO8601 timestamps, k?
+ time = (0, _moment2.default)(parseString, _moment2.default.ISO_8601);
+ }
+
+ if (!mathString.length) {
+ return time;
+ }
+
+ return parseDateMath(mathString, time, roundUp);
+}
+
+function isValid(text) {
+ var date = parse(text);
+ if (!date) {
+ return false;
+ }
+
+ if (_moment2.default.isMoment(date)) {
+ return date.isValid();
+ }
+
+ return false;
+}
+
+function parseDateMath(mathString, time, roundUp) {
+ var dateTime = time;
+ var i = 0;
+ var len = mathString.length;
+
+ while (i < len) {
+ var c = mathString.charAt(i++);
+ var type;
+ var num;
+ var unit;
+
+ if (c === '/') {
+ type = 0;
+ } else if (c === '+') {
+ type = 1;
+ } else if (c === '-') {
+ type = 2;
+ } else {
+ return undefined;
+ }
+
+ if (isNaN(mathString.charAt(i))) {
+ num = 1;
+ } else if (mathString.length === 2) {
+ num = mathString.charAt(i);
+ } else {
+ var numFrom = i;
+ while (!isNaN(mathString.charAt(i))) {
+ i++;
+ if (i > 10) {
+ return undefined;
+ }
+ }
+ num = parseInt(mathString.substring(numFrom, i), 10);
+ }
+
+ if (type === 0) {
+ // rounding is only allowed on whole, single, units (eg M or 1M, not 0.5M or 2M)
+ if (num !== 1) {
+ return undefined;
+ }
+ }
+ unit = mathString.charAt(i++);
+
+ if (!_lodash2.default.includes(units, unit)) {
+ return undefined;
+ } else {
+ if (type === 0) {
+ if (roundUp) {
+ dateTime.endOf(unit);
+ } else {
+ dateTime.startOf(unit);
+ }
+ } else if (type === 1) {
+ dateTime.add(num, unit);
+ } else if (type === 2) {
+ dateTime.subtract(num, unit);
+ }
+ }
+ }
+ return dateTime;
+}