Merge branch 'master' into docs

This commit is contained in:
Alexander Zobnin
2020-04-13 09:46:57 +03:00
35 changed files with 1653 additions and 390 deletions

View File

@@ -5,7 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## [3.11.0] - 2020-03-23
### Added
- Improve variable query editor, [#705](https://github.com/alexanderzobnin/grafana-zabbix/issues/705)
- Transform/percentile function, [#868](https://github.com/alexanderzobnin/grafana-zabbix/issues/868)
### Fixed
- Problems panel: stopped working in Grafana 6.7.0, [#907](https://github.com/alexanderzobnin/grafana-zabbix/issues/907)
- Problems panel: event severity change, [#870](https://github.com/alexanderzobnin/grafana-zabbix/issues/870)
- Problems panel: color is changed to acknowledged even if there is only message without acknowledgment, [#857](https://github.com/alexanderzobnin/grafana-zabbix/issues/857)
- Percentile function returns incorrect results, [#862](https://github.com/alexanderzobnin/grafana-zabbix/issues/862)
## [3.10.5] - 2019-12-26 ## [3.10.5] - 2019-12-26
### Added ### Added

View File

@@ -56,6 +56,6 @@ First, [configure](https://alexanderzobnin.github.io/grafana-zabbix/configuratio
- Need additional support? Contact me for details [alexanderzobnin@gmail.com](mailto:alexanderzobnin@gmail.com) - Need additional support? Contact me for details [alexanderzobnin@gmail.com](mailto:alexanderzobnin@gmail.com)
--- ---
:copyright: 2015-2019 Alexander Zobnin alexanderzobnin@gmail.com :copyright: 2015-2020 Alexander Zobnin alexanderzobnin@gmail.com
Licensed under the Apache 2.0 License Licensed under the Apache 2.0 License

View File

@@ -105,6 +105,19 @@ calculates moving average over 60 points (if metric has 1 second resolution it m
``` ```
--- ---
### _percentile_
```
percentile(interval, N)
```
Takes a series of values and a window size and consolidate all its points fallen in the given _interval_ into one point by Nth percentile.
Examples:
```
percentile(1h, 99)
percentile($__range_series, 95) - 95th percentile over all series values
```
---
### _removeAboveValue_ ### _removeAboveValue_
``` ```
removeAboveValue(N) removeAboveValue(N)
@@ -159,16 +172,16 @@ This will add metrics together and return the sum at each datapoint. This method
--- ---
### _percentile_ ### _percentileAgg_
``` ```
percentile(interval, N) percentileAgg(interval, N)
``` ```
Takes all timeseries and consolidate all its points fallen in the given _interval_ into one point by Nth percentile. Takes all timeseries and consolidate all its points fallen in the given _interval_ into one point by Nth percentile.
Examples: Examples:
``` ```
percentile(1h, 99) percentileAgg(1h, 99)
percentile($__range_series, 95) - 95th percentile over all values percentileAgg($__range_series, 95) - 95th percentile over all values
``` ```
--- ---

View File

@@ -1,7 +1,7 @@
{ {
"name": "grafana-zabbix", "name": "grafana-zabbix",
"private": false, "private": false,
"version": "3.10.5", "version": "3.11.0",
"description": "Zabbix plugin for Grafana", "description": "Zabbix plugin for Grafana",
"homepage": "http://grafana-zabbix.org", "homepage": "http://grafana-zabbix.org",
"scripts": { "scripts": {
@@ -28,8 +28,10 @@
"@babel/core": "^7.7.7", "@babel/core": "^7.7.7",
"@babel/preset-env": "^7.7.7", "@babel/preset-env": "^7.7.7",
"@babel/preset-react": "^7.6.3", "@babel/preset-react": "^7.6.3",
"@grafana/data": "^6.4.2", "@emotion/core": "^10.0.27",
"@grafana/ui": "^6.4.2", "@grafana/data": "^6.7.0",
"@grafana/ui": "^6.7.0",
"@grafana/runtime": "^6.7.0",
"@types/classnames": "^2.2.6", "@types/classnames": "^2.2.6",
"@types/grafana": "github:CorpGlory/types-grafana", "@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "^23.1.1", "@types/jest": "^23.1.1",
@@ -69,7 +71,7 @@
"react": "^16.7.0", "react": "^16.7.0",
"react-dom": "^16.7.0", "react-dom": "^16.7.0",
"react-popper": "^1.3.2", "react-popper": "^1.3.2",
"react-table": "^6.8.6", "react-table-6": "^6.8.6",
"react-test-renderer": "^16.7.0", "react-test-renderer": "^16.7.0",
"react-transition-group": "^2.5.2", "react-transition-group": "^2.5.2",
"rst2html": "github:thoward/rst2html#990cb89", "rst2html": "github:thoward/rst2html#990cb89",

View File

@@ -0,0 +1,157 @@
import React, { PureComponent } from 'react';
import { parseLegacyVariableQuery } from '../utils';
import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
import { ZabbixInput } from './ZabbixInput';
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
{ value: VariableQueryTypes.Group, label: 'Group'},
{ value: VariableQueryTypes.Host, label: 'Host' },
{ value: VariableQueryTypes.Application, label: 'Application' },
{ value: VariableQueryTypes.Item, label: 'Item' },
];
defaults: VariableQueryData = {
selectedQueryType: { value: VariableQueryTypes.Group, label: 'Group' },
queryType: VariableQueryTypes.Group,
group: '/.*/',
host: '',
application: '',
item: '',
};
constructor(props: VariableQueryProps) {
super(props);
if (this.props.query && typeof this.props.query === 'string') {
// Backward compatibility
const query = parseLegacyVariableQuery(this.props.query);
const selectedQueryType = this.getSelectedQueryType(query.queryType);
this.state = {
selectedQueryType,
legacyQuery: this.props.query,
...query
};
} else if (this.props.query) {
const query = (this.props.query as VariableQuery);
const selectedQueryType = this.getSelectedQueryType(query.queryType);
this.state = {
...this.defaults,
...query,
selectedQueryType,
};
} else {
this.state = this.defaults;
}
}
getSelectedQueryType(queryType: VariableQueryTypes) {
return this.queryTypes.find(q => q.value === queryType);
}
handleQueryUpdate = (evt: React.ChangeEvent<HTMLInputElement>, prop: string) => {
const value = evt.currentTarget.value;
this.setState((prevState: VariableQueryData) => {
const newQuery = {
...prevState,
};
newQuery[prop] = value;
return {
...newQuery,
};
});
}
handleQueryChange = () => {
const { queryType, group, host, application, item } = this.state;
const queryModel = { queryType, group, host, application, item };
this.props.onChange(queryModel, `Zabbix - ${queryType}`);
}
handleQueryTypeChange = (selectedItem: SelectableValue<VariableQueryTypes>) => {
this.setState({
...this.state,
selectedQueryType: selectedItem,
queryType: selectedItem.value,
});
const { group, host, application, item } = this.state;
const queryType = selectedItem.value;
const queryModel = { queryType, group, host, application, item };
this.props.onChange(queryModel, `Zabbix - ${queryType}`);
}
render() {
const { selectedQueryType, legacyQuery, group, host, application, item } = this.state;
return (
<>
<div className="gf-form max-width-21">
<FormLabel width={10}>Query Type</FormLabel>
<Select
width={11}
value={selectedQueryType}
options={this.queryTypes}
onChange={this.handleQueryTypeChange}
/>
</div>
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<FormLabel width={10}>Group</FormLabel>
<ZabbixInput
value={group}
onChange={evt => this.handleQueryUpdate(evt, 'group')}
onBlur={this.handleQueryChange}
/>
</div>
{selectedQueryType.value !== VariableQueryTypes.Group &&
<div className="gf-form max-width-30">
<FormLabel width={10}>Host</FormLabel>
<ZabbixInput
value={host}
onChange={evt => this.handleQueryUpdate(evt, 'host')}
onBlur={this.handleQueryChange}
/>
</div>
}
</div>
{(selectedQueryType.value === VariableQueryTypes.Application ||
selectedQueryType.value === VariableQueryTypes.Item) &&
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<FormLabel width={10}>Application</FormLabel>
<ZabbixInput
value={application}
onChange={evt => this.handleQueryUpdate(evt, 'application')}
onBlur={this.handleQueryChange}
/>
</div>
{selectedQueryType.value === VariableQueryTypes.Item &&
<div className="gf-form max-width-30">
<FormLabel width={10}>Item</FormLabel>
<ZabbixInput
value={item}
onChange={evt => this.handleQueryUpdate(evt, 'item')}
onBlur={this.handleQueryChange}
/>
</div>
}
</div>
}
{legacyQuery &&
<div className="gf-form">
<FormLabel width={10} tooltip="Original query string, read-only">Legacy Query</FormLabel>
<Input
value={legacyQuery}
readOnly={true}
/>
</div>
}
</>
);
}
}

View File

@@ -0,0 +1,70 @@
import React, { FC } from 'react';
import { css, cx } from 'emotion';
import { Themeable, withTheme, Input, EventsWithValidation, ValidationEvents } from '@grafana/ui';
import { isRegex, variableRegex } from '../utils';
import { GrafanaTheme } from '@grafana/data';
const variablePattern = RegExp(`^${variableRegex.source}`);
const getStyles = (theme: GrafanaTheme) => ({
inputRegex: css`
color: ${theme.colors.orange}
`,
inputVariable: css`
color: ${theme.colors.variable}
`,
});
const zabbixInputValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [
{
rule: value => {
if (!value) {
return true;
}
if (value.length > 1 && value[0] === '/') {
if (value[value.length - 1] !== '/') {
return false;
}
}
return true;
},
errorMessage: 'Not a valid regex',
},
{
rule: value => {
if (value === '*') {
return false;
}
return true;
},
errorMessage: 'Wildcards not supported. Use /.*/ instead',
},
],
};
interface Props extends React.ComponentProps<typeof Input>, Themeable {
}
const UnthemedZabbixInput: FC<Props> = ({ theme, value, ref, validationEvents, ...restProps }) => {
const styles = getStyles(theme);
let inputClass;
if (variablePattern.test(value as string)) {
inputClass = styles.inputVariable;
}
if (isRegex(value)) {
inputClass = styles.inputRegex;
}
return (
<Input
className={inputClass}
value={value}
validationEvents={zabbixInputValidationEvents}
{...restProps}
/>
);
};
export const ZabbixInput = withTheme(UnthemedZabbixInput);

View File

@@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { getDataSourceSrv } from '@grafana/runtime';
import { migrateDSConfig } from './migrations'; import { migrateDSConfig } from './migrations';
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb']; const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
@@ -23,9 +24,7 @@ const defaultConfig = {
export class ZabbixDSConfigController { export class ZabbixDSConfigController {
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, datasourceSrv) { constructor() {
this.datasourceSrv = datasourceSrv;
this.current.jsonData = migrateDSConfig(this.current.jsonData); this.current.jsonData = migrateDSConfig(this.current.jsonData);
_.defaults(this.current.jsonData, defaultConfig); _.defaults(this.current.jsonData, defaultConfig);
@@ -39,7 +38,7 @@ export class ZabbixDSConfigController {
} }
getSupportedDBDataSources() { getSupportedDBDataSources() {
let datasources = this.datasourceSrv.getAll(); let datasources = getDataSourceSrv().getAll();
return _.filter(datasources, ds => { return _.filter(datasources, ds => {
return _.includes(SUPPORTED_SQL_DS, ds.type); return _.includes(SUPPORTED_SQL_DS, ds.type);
}); });
@@ -53,7 +52,7 @@ export class ZabbixDSConfigController {
loadCurrentDBDatasource() { loadCurrentDBDatasource() {
const dsName= this.current.jsonData.dbConnectionDatasourceName; const dsName= this.current.jsonData.dbConnectionDatasourceName;
this.datasourceSrv.loadDatasource(dsName) getDataSourceSrv().loadDatasource(dsName)
.then(ds => { .then(ds => {
if (ds) { if (ds) {
this.dbConnectionDatasourceId = ds.id; this.dbConnectionDatasourceId = ds.id;
@@ -66,7 +65,7 @@ export class ZabbixDSConfigController {
return; return;
} }
this.datasourceSrv.loadDatasource(this.current.name) getDataSourceSrv().loadDatasource(this.current.name)
.then(ds => { .then(ds => {
return ds.getVersion(); return ds.getVersion();
}) })

View File

@@ -2,6 +2,14 @@ import _ from 'lodash';
import * as utils from './utils'; import * as utils from './utils';
import ts, { groupBy_perf as groupBy } from './timeseries'; import ts, { groupBy_perf as groupBy } from './timeseries';
let SUM = ts.SUM;
let COUNT = ts.COUNT;
let AVERAGE = ts.AVERAGE;
let MIN = ts.MIN;
let MAX = ts.MAX;
let MEDIAN = ts.MEDIAN;
let PERCENTILE = ts.PERCENTILE;
let downsampleSeries = ts.downsample; let downsampleSeries = ts.downsample;
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc); let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
let sumSeries = ts.sumSeries; let sumSeries = ts.sumSeries;
@@ -11,14 +19,7 @@ let scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
let offset = (delta, datapoints) => ts.offset(datapoints, delta); let offset = (delta, datapoints) => ts.offset(datapoints, delta);
let simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n); let simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
let expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a); let expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
let percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
let SUM = ts.SUM;
let COUNT = ts.COUNT;
let AVERAGE = ts.AVERAGE;
let MIN = ts.MIN;
let MAX = ts.MAX;
let MEDIAN = ts.MEDIAN;
let PERCENTILE = ts.PERCENTILE;
function limit(order, n, orderByFunc, timeseries) { function limit(order, n, orderByFunc, timeseries) {
let orderByCallback = aggregationFunctions[orderByFunc]; let orderByCallback = aggregationFunctions[orderByFunc];
@@ -120,10 +121,12 @@ function aggregateWrapper(groupByCallback, interval, datapoints) {
return groupBy(sortedPoints, interval, groupByCallback); return groupBy(sortedPoints, interval, groupByCallback);
} }
function percentile(interval, n, datapoints) { function percentileAgg(interval, n, datapoints) {
var flattenedPoints = ts.flattenDatapoints(datapoints); const flattenedPoints = ts.flattenDatapoints(datapoints);
var groupByCallback = _.partial(PERCENTILE, n); // groupBy_perf works with sorted series only
return groupBy(flattenedPoints, interval, groupByCallback); const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = _.partial(PERCENTILE, n);
return groupBy(sortedPoints, interval, groupByCallback);
} }
function timeShift(interval, range) { function timeShift(interval, range) {
@@ -151,10 +154,11 @@ let metricFunctions = {
rate: rate, rate: rate,
movingAverage: simpleMovingAverage, movingAverage: simpleMovingAverage,
exponentialMovingAverage: expMovingAverage, exponentialMovingAverage: expMovingAverage,
percentile: percentile,
transformNull: transformNull, transformNull: transformNull,
aggregateBy: aggregateByWrapper, aggregateBy: aggregateByWrapper,
// Predefined aggs // Predefined aggs
percentile: percentile, percentileAgg: percentileAgg,
average: _.partial(aggregateWrapper, AVERAGE), average: _.partial(aggregateWrapper, AVERAGE),
min: _.partial(aggregateWrapper, MIN), min: _.partial(aggregateWrapper, MIN),
max: _.partial(aggregateWrapper, MAX), max: _.partial(aggregateWrapper, MAX),

View File

@@ -9,13 +9,14 @@ import dataProcessor from './dataProcessor';
import responseHandler from './responseHandler'; import responseHandler from './responseHandler';
import { Zabbix } from './zabbix/zabbix'; import { Zabbix } from './zabbix/zabbix';
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore'; import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
import { VariableQueryTypes } from './types';
const DEFAULT_ZABBIX_VERSION = 3; const DEFAULT_ZABBIX_VERSION = 3;
export class ZabbixDatasource { export class ZabbixDatasource {
/** @ngInject */ /** @ngInject */
constructor(instanceSettings, templateSrv, backendSrv, datasourceSrv, zabbixAlertingSrv) { constructor(instanceSettings, templateSrv, zabbixAlertingSrv) {
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
this.zabbixAlertingSrv = zabbixAlertingSrv; this.zabbixAlertingSrv = zabbixAlertingSrv;
@@ -74,7 +75,7 @@ export class ZabbixDatasource {
dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy, dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy,
}; };
this.zabbix = new Zabbix(zabbixOptions, datasourceSrv, backendSrv); this.zabbix = new Zabbix(zabbixOptions);
} }
//////////////////////// ////////////////////////
@@ -432,42 +433,41 @@ export class ZabbixDatasource {
* of metrics in "{metric1,metcic2,...,metricN}" format. * of metrics in "{metric1,metcic2,...,metricN}" format.
*/ */
metricFindQuery(query) { metricFindQuery(query) {
let result; let resultPromise;
let parts = []; let queryModel = _.cloneDeep(query);
// Split query. Query structure: group.host.app.item if (!query) {
_.each(utils.splitTemplateQuery(query), part => { return Promise.resolve([]);
part = this.replaceTemplateVars(part, {});
// Replace wildcard to regex
if (part === '*') {
part = '/.*/';
}
parts.push(part);
});
let template = _.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(metrics => { if (typeof query === 'string') {
// Backward compatibility
queryModel = utils.parseLegacyVariableQuery(query);
}
for (const prop of ['group', 'host', 'application', 'item']) {
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {});
}
switch (queryModel.queryType) {
case VariableQueryTypes.Group:
resultPromise = this.zabbix.getGroups(queryModel.group);
break;
case VariableQueryTypes.Host:
resultPromise = this.zabbix.getHosts(queryModel.group, queryModel.host);
break;
case VariableQueryTypes.Application:
resultPromise = this.zabbix.getApps(queryModel.group, queryModel.host, queryModel.application);
break;
case VariableQueryTypes.Item:
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item);
break;
default:
resultPromise = Promise.resolve([]);
break;
}
return resultPromise.then(metrics => {
return _.map(metrics, formatMetric); return _.map(metrics, formatMetric);
}); });
} }

View File

@@ -85,6 +85,16 @@ addFuncDef({
defaultParams: [0.2], defaultParams: [0.2],
}); });
addFuncDef({
name: 'percentile',
category: 'Transform',
params: [
{ name: 'interval', type: 'string' },
{ name: 'percent', type: 'float', options: [25, 50, 75, 90, 95, 99, 99.9] }
],
defaultParams: ['1m', 95],
});
addFuncDef({ addFuncDef({
name: 'removeAboveValue', name: 'removeAboveValue',
category: 'Transform', category: 'Transform',
@@ -140,7 +150,7 @@ addFuncDef({
}); });
addFuncDef({ addFuncDef({
name: 'percentile', name: 'percentileAgg',
category: 'Aggregate', category: 'Aggregate',
params: [ params: [
{ name: 'interval', type: 'string' }, { name: 'interval', type: 'string' },

View File

@@ -2,15 +2,18 @@ import { loadPluginCss } from 'grafana/app/plugins/sdk';
import { ZabbixDatasource } from './datasource'; import { ZabbixDatasource } from './datasource';
import { ZabbixQueryController } from './query.controller'; import { ZabbixQueryController } from './query.controller';
import { ZabbixDSConfigController } from './config.controller'; import { ZabbixDSConfigController } from './config.controller';
import { ZabbixVariableQueryEditor } from './components/VariableQueryEditor';
import './zabbixAlerting.service.js'; import './zabbixAlerting.service.js';
import './add-metric-function.directive'; import './add-metric-function.directive';
import './metric-function-editor.directive'; import './metric-function-editor.directive';
class ZabbixQueryOptionsController {} class ZabbixQueryOptionsController {
ZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html'; static templateUrl = 'datasource-zabbix/partials/query.options.html';
}
class ZabbixAnnotationsQueryController {} class ZabbixAnnotationsQueryController {
ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html'; static templateUrl = 'datasource-zabbix/partials/annotations.editor.html';
}
ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html'; ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html';
ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html'; ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
@@ -25,5 +28,6 @@ export {
ZabbixDSConfigController as ConfigCtrl, ZabbixDSConfigController as ConfigCtrl,
ZabbixQueryController as QueryCtrl, ZabbixQueryController as QueryCtrl,
ZabbixQueryOptionsController as QueryOptionsCtrl, ZabbixQueryOptionsController as QueryOptionsCtrl,
ZabbixAnnotationsQueryController as AnnotationsQueryCtrl ZabbixAnnotationsQueryController as AnnotationsQueryCtrl,
ZabbixVariableQueryEditor as VariableQueryEditor,
}; };

View File

@@ -21,11 +21,11 @@ describe('ZabbixDatasource', () => {
}; };
ctx.templateSrv = mocks.templateSrvMock; ctx.templateSrv = mocks.templateSrvMock;
ctx.backendSrv = mocks.backendSrvMock; // ctx.backendSrv = mocks.backendSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock; ctx.datasourceSrv = mocks.datasourceSrvMock;
ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock; ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock;
ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.backendSrv, ctx.datasourceSrv, ctx.zabbixAlertingSrv); ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.datasourceSrv, ctx.zabbixAlertingSrv);
}); });
describe('When querying data', () => { describe('When querying data', () => {
@@ -231,7 +231,7 @@ describe('ZabbixDatasource', () => {
}); });
}); });
describe('When invoking metricFindQuery()', () => { describe('When invoking metricFindQuery() with legacy query', () => {
beforeEach(() => { beforeEach(() => {
ctx.ds.replaceTemplateVars = (str) => str; ctx.ds.replaceTemplateVars = (str) => str;
ctx.ds.zabbix = { ctx.ds.zabbix = {
@@ -245,7 +245,6 @@ describe('ZabbixDatasource', () => {
it('should return groups', (done) => { it('should return groups', (done) => {
const tests = [ const tests = [
{query: '*', expect: '/.*/'}, {query: '*', expect: '/.*/'},
{query: '', expect: ''},
{query: 'Backend', expect: 'Backend'}, {query: 'Backend', expect: 'Backend'},
{query: 'Back*', expect: 'Back*'}, {query: 'Back*', expect: 'Back*'},
]; ];
@@ -258,6 +257,16 @@ describe('ZabbixDatasource', () => {
done(); done();
}); });
it('should return empty list for empty query', (done) => {
ctx.ds.metricFindQuery('').then(result => {
expect(ctx.ds.zabbix.getGroups).toBeCalledTimes(0);
ctx.ds.zabbix.getGroups.mockClear();
expect(result).toEqual([]);
done();
});
});
it('should return hosts', (done) => { it('should return hosts', (done) => {
const tests = [ const tests = [
{query: '*.*', expect: ['/.*/', '/.*/']}, {query: '*.*', expect: ['/.*/', '/.*/']},

View File

@@ -1,11 +1,17 @@
import mocks from '../../test-setup/mocks';
import { DBConnector } from '../zabbix/connectors/dbConnector'; import { DBConnector } from '../zabbix/connectors/dbConnector';
const loadDatasourceMock = jest.fn().mockResolvedValue({ id: 42, name: 'foo', meta: {} });
const getAllMock = jest.fn().mockReturnValue([{ id: 42, name: 'foo', meta: {} }]);
jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: () => ({
loadDatasource: loadDatasourceMock,
getAll: getAllMock
}),
}));
describe('DBConnector', () => { describe('DBConnector', () => {
let ctx = {}; const ctx: any = {};
const datasourceSrv = mocks.datasourceSrvMock;
datasourceSrv.loadDatasource.mockResolvedValue({ id: 42, name: 'foo', meta: {} });
datasourceSrv.getAll.mockReturnValue([{ id: 42, name: 'foo' }]);
describe('When init DB connector', () => { describe('When init DB connector', () => {
beforeEach(() => { beforeEach(() => {
@@ -13,34 +19,34 @@ describe('DBConnector', () => {
datasourceId: 42, datasourceId: 42,
datasourceName: undefined datasourceName: undefined
}; };
loadDatasourceMock.mockClear();
getAllMock.mockClear();
}); });
it('should try to load datasource by name first', () => { it('should try to load datasource by name first', () => {
ctx.options = { const dbConnector = new DBConnector({ datasourceName: 'bar' });
datasourceName: 'bar'
};
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
dbConnector.loadDBDataSource(); dbConnector.loadDBDataSource();
expect(datasourceSrv.getAll).not.toHaveBeenCalled(); expect(getAllMock).not.toHaveBeenCalled();
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('bar'); expect(loadDatasourceMock).toHaveBeenCalledWith('bar');
}); });
it('should load datasource by id if name not present', () => { it('should load datasource by id if name not present', () => {
const dbConnector = new DBConnector(ctx.options, datasourceSrv); const dbConnector = new DBConnector({ datasourceId: 42 });
dbConnector.loadDBDataSource(); dbConnector.loadDBDataSource();
expect(datasourceSrv.getAll).toHaveBeenCalled(); expect(getAllMock).toHaveBeenCalled();
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('foo'); expect(loadDatasourceMock).toHaveBeenCalledWith('foo');
}); });
it('should throw error if no name and id specified', () => { it('should throw error if no name and id specified', () => {
ctx.options = {}; ctx.options = {};
const dbConnector = new DBConnector(ctx.options, datasourceSrv); const dbConnector = new DBConnector(ctx.options);
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified'); return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified');
}); });
it('should throw error if datasource with given id is not found', () => { it('should throw error if datasource with given id is not found', () => {
ctx.options.datasourceId = 45; ctx.options.datasourceId = 45;
const dbConnector = new DBConnector(ctx.options, datasourceSrv); const dbConnector = new DBConnector(ctx.options);
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found'); return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found');
}); });
}); });

View File

@@ -1,17 +1,20 @@
import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector'; import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector';
import { compactQuery } from '../utils'; import { compactQuery } from '../utils';
jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: jest.fn(() => ({
loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
})),
}));
describe('InfluxDBConnector', () => { describe('InfluxDBConnector', () => {
let ctx = {}; let ctx = {};
beforeEach(() => { beforeEach(() => {
ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' }; ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
ctx.datasourceSrvMock = { ctx.influxDBConnector = new InfluxDBConnector(ctx.options);
loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
};
ctx.influxDBConnector = new InfluxDBConnector(ctx.options, ctx.datasourceSrvMock);
ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]); ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]);
ctx.defaultQueryParams = { ctx.defaultQueryParams = {
itemids: ['123', '234'], itemids: ['123', '234'],

View File

@@ -0,0 +1,30 @@
import { SelectableValue } from "@grafana/data";
export interface VariableQueryProps {
query: LegacyVariableQuery;
onChange: (query: VariableQuery, definition: string) => void;
datasource: any;
templateSrv: any;
}
export interface VariableQueryData extends VariableQuery {
selectedQueryType: SelectableValue<VariableQueryTypes>;
legacyQuery?: string;
}
export interface VariableQuery {
queryType: VariableQueryTypes;
group?: string;
host?: string;
application?: string;
item?: string;
}
export type LegacyVariableQuery = VariableQuery | string;
export enum VariableQueryTypes {
Group = 'group',
Host = 'host',
Application = 'application',
Item = 'item',
}

View File

@@ -2,6 +2,15 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import kbn from 'grafana/app/core/utils/kbn'; import kbn from 'grafana/app/core/utils/kbn';
import * as c from './constants'; import * as c from './constants';
import { VariableQuery, VariableQueryTypes } from './types';
/*
* This regex matches 3 types of variable reference with an optional format specifier
* \$(\w+) $var1
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
*/
export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::(\w+))?}/g;
/** /**
* Expand Zabbix item name * Expand Zabbix item name
@@ -14,8 +23,8 @@ export function expandItemName(name, key) {
// extract params from key: // extract params from key:
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"] // "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
let key_params_str = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']')); const key_params_str = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']'));
let key_params = splitKeyParams(key_params_str); const key_params = splitKeyParams(key_params_str);
// replace item parameters // replace item parameters
for (let i = key_params.length; i >= 1; i--) { for (let i = key_params.length; i >= 1; i--) {
@@ -34,10 +43,10 @@ export function expandItems(items) {
} }
function splitKeyParams(paramStr) { function splitKeyParams(paramStr) {
let params = []; const params = [];
let quoted = false; let quoted = false;
let in_array = false; let in_array = false;
let split_symbol = ','; const split_symbol = ',';
let param = ''; let param = '';
_.forEach(paramStr, symbol => { _.forEach(paramStr, symbol => {
@@ -71,9 +80,9 @@ export function containsMacro(itemName) {
export function replaceMacro(item, macros) { export function replaceMacro(item, macros) {
let itemName = item.name; let itemName = item.name;
let item_macros = itemName.match(MACRO_PATTERN); const item_macros = itemName.match(MACRO_PATTERN);
_.forEach(item_macros, macro => { _.forEach(item_macros, macro => {
let host_macros = _.filter(macros, m => { const host_macros = _.filter(macros, m => {
if (m.hostid) { if (m.hostid) {
return m.hostid === item.hostid; return m.hostid === item.hostid;
} else { } else {
@@ -82,10 +91,10 @@ export function replaceMacro(item, macros) {
} }
}); });
let macro_def = _.find(host_macros, { macro: macro }); const macro_def = _.find(host_macros, { macro: macro });
if (macro_def && macro_def.value) { if (macro_def && macro_def.value) {
let macro_value = macro_def.value; const macro_value = macro_def.value;
let macro_regex = new RegExp(escapeMacro(macro)); const macro_regex = new RegExp(escapeMacro(macro));
itemName = itemName.replace(macro_regex, macro_value); itemName = itemName.replace(macro_regex, macro_value);
} }
}); });
@@ -98,17 +107,62 @@ function escapeMacro(macro) {
return macro; return macro;
} }
export function parseLegacyVariableQuery(query: string): VariableQuery {
let queryType: VariableQueryTypes;
const parts = [];
// Split query. Query structure: group.host.app.item
_.each(splitTemplateQuery(query), part => {
// Replace wildcard to regex
if (part === '*') {
part = '/.*/';
}
parts.push(part);
});
const template = _.zipObject(['group', 'host', 'app', 'item'], parts);
if (parts.length === 4 && template.app === '/.*/') {
// Search for all items, even it's not belong to any application
template.app = '';
}
switch (parts.length) {
case 1:
queryType = VariableQueryTypes.Group;
break;
case 2:
queryType = VariableQueryTypes.Host;
break;
case 3:
queryType = VariableQueryTypes.Application;
break;
case 4:
queryType = VariableQueryTypes.Item;
break;
}
const variableQuery: VariableQuery = {
queryType,
group: template.group || '',
host: template.host || '',
application: template.app || '',
item: template.item || '',
};
return variableQuery;
}
/** /**
* Split template query to parts of zabbix entities * Split template query to parts of zabbix entities
* group.host.app.item -> [group, host, app, item] * group.host.app.item -> [group, host, app, item]
* {group}{host.com} -> [group, host.com] * {group}{host.com} -> [group, host.com]
*/ */
export function splitTemplateQuery(query) { export function splitTemplateQuery(query) {
let splitPattern = /\{[^\{\}]*\}|\{\/.*\/\}/g; const splitPattern = /\{[^\{\}]*\}|\{\/.*\/\}/g;
let split; let split;
if (isContainsBraces(query)) { if (isContainsBraces(query)) {
let result = query.match(splitPattern); const result = query.match(splitPattern);
split = _.map(result, part => { split = _.map(result, part => {
return _.trim(part, '{}'); return _.trim(part, '{}');
}); });
@@ -120,7 +174,7 @@ export function splitTemplateQuery(query) {
} }
function isContainsBraces(query) { function isContainsBraces(query) {
let bracesPattern = /^\{.+\}$/; const bracesPattern = /^\{.+\}$/;
return bracesPattern.test(query); return bracesPattern.test(query);
} }
@@ -132,9 +186,9 @@ export function isRegex(str) {
} }
export function isTemplateVariable(str, templateVariables) { export function isTemplateVariable(str, templateVariables) {
var variablePattern = /^\$\w+/; const variablePattern = /^\$\w+/;
if (variablePattern.test(str)) { if (variablePattern.test(str)) {
var variables = _.map(templateVariables, variable => { const variables = _.map(templateVariables, variable => {
return '$' + variable.name; return '$' + variable.name;
}); });
return _.includes(variables, str); return _.includes(variables, str);
@@ -156,9 +210,9 @@ export function getRangeScopedVars(range) {
} }
export function buildRegex(str) { export function buildRegex(str) {
var matches = str.match(regexPattern); const matches = str.match(regexPattern);
var pattern = matches[1]; const pattern = matches[1];
var flags = matches[2] !== "" ? matches[2] : undefined; const flags = matches[2] !== "" ? matches[2] : undefined;
return new RegExp(pattern, flags); return new RegExp(pattern, flags);
} }
@@ -169,18 +223,18 @@ export function escapeRegex(value) {
} }
export function parseInterval(interval) { export function parseInterval(interval) {
var intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g; const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
var momentInterval = intervalPattern.exec(interval); const momentInterval: any[] = intervalPattern.exec(interval);
return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf(); return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf();
} }
export function parseTimeShiftInterval(interval) { export function parseTimeShiftInterval(interval) {
let intervalPattern = /^([\+\-]*)([\d]+)(y|M|w|d|h|m|s)/g; const intervalPattern = /^([\+\-]*)([\d]+)(y|M|w|d|h|m|s)/g;
let momentInterval = intervalPattern.exec(interval); const momentInterval: any[] = intervalPattern.exec(interval);
let duration = 0; let duration: any = 0;
if (momentInterval[1] === '+') { if (momentInterval[1] === '+') {
duration = 0 - moment.duration(Number(momentInterval[2]), momentInterval[3]).valueOf(); duration = 0 - (moment.duration(Number(momentInterval[2]), momentInterval[3]).valueOf() as any);
} else { } else {
duration = moment.duration(Number(momentInterval[2]), momentInterval[3]).valueOf(); duration = moment.duration(Number(momentInterval[2]), momentInterval[3]).valueOf();
} }
@@ -196,13 +250,13 @@ export function parseTimeShiftInterval(interval) {
*/ */
export function formatAcknowledges(acknowledges) { export function formatAcknowledges(acknowledges) {
if (acknowledges.length) { if (acknowledges.length) {
var formatted_acknowledges = '<br><br>Acknowledges:<br><table><tr><td><b>Time</b></td>' let formatted_acknowledges = '<br><br>Acknowledges:<br><table><tr><td><b>Time</b></td>'
+ '<td><b>User</b></td><td><b>Comments</b></td></tr>'; + '<td><b>User</b></td><td><b>Comments</b></td></tr>';
_.each(_.map(acknowledges, function (ack) { _.each(_.map(acknowledges, ack => {
var timestamp = moment.unix(ack.clock); const timestamp = moment.unix(ack.clock);
return '<tr><td><i>' + timestamp.format("DD MMM YYYY HH:mm:ss") + '</i></td><td>' + ack.alias return '<tr><td><i>' + timestamp.format("DD MMM YYYY HH:mm:ss") + '</i></td><td>' + ack.alias
+ ' (' + ack.name + ' ' + ack.surname + ')' + '</td><td>' + ack.message + '</td></tr>'; + ' (' + ack.name + ' ' + ack.surname + ')' + '</td><td>' + ack.message + '</td></tr>';
}), function (ack) { }), ack => {
formatted_acknowledges = formatted_acknowledges.concat(ack); formatted_acknowledges = formatted_acknowledges.concat(ack);
}); });
formatted_acknowledges = formatted_acknowledges.concat('</table>'); formatted_acknowledges = formatted_acknowledges.concat('</table>');
@@ -213,8 +267,8 @@ export function formatAcknowledges(acknowledges) {
} }
export function convertToZabbixAPIUrl(url) { export function convertToZabbixAPIUrl(url) {
var zabbixAPIUrlPattern = /.*api_jsonrpc.php$/; const zabbixAPIUrlPattern = /.*api_jsonrpc.php$/;
var trimSlashPattern = /(.*?)[\/]*$/; const trimSlashPattern = /(.*?)[\/]*$/;
if (url.match(zabbixAPIUrlPattern)) { if (url.match(zabbixAPIUrlPattern)) {
return url; return url;
} else { } else {
@@ -247,7 +301,7 @@ export function callOnce(func, promiseKeeper) {
*/ */
export function sequence(funcsArray) { export function sequence(funcsArray) {
return function(result) { return function(result) {
for (var i = 0; i < funcsArray.length; i++) { for (let i = 0; i < funcsArray.length; i++) {
result = funcsArray[i].call(this, result); result = funcsArray[i].call(this, result);
} }
return result; return result;
@@ -292,5 +346,5 @@ export function getArrayDepth(a, level = 0) {
// Fix for backward compatibility with lodash 2.4 // Fix for backward compatibility with lodash 2.4
if (!_.includes) { if (!_.includes) {
_.includes = _.contains; _.includes = (_ as any).contains;
} }

View File

@@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { getDataSourceSrv } from '@grafana/runtime';
export const DEFAULT_QUERY_LIMIT = 10000; export const DEFAULT_QUERY_LIMIT = 10000;
export const HISTORY_TO_TABLE_MAP = { export const HISTORY_TO_TABLE_MAP = {
@@ -34,31 +35,30 @@ export const consolidateByTrendColumns = {
* `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API. * `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API.
*/ */
export class DBConnector { export class DBConnector {
constructor(options, datasourceSrv) { constructor(options) {
this.datasourceSrv = datasourceSrv;
this.datasourceId = options.datasourceId; this.datasourceId = options.datasourceId;
this.datasourceName = options.datasourceName; this.datasourceName = options.datasourceName;
this.datasourceTypeId = null; this.datasourceTypeId = null;
this.datasourceTypeName = null; this.datasourceTypeName = null;
} }
static loadDatasource(dsId, dsName, datasourceSrv) { static loadDatasource(dsId, dsName) {
if (!dsName && dsId !== undefined) { if (!dsName && dsId !== undefined) {
let ds = _.find(datasourceSrv.getAll(), {'id': dsId}); let ds = _.find(getDataSourceSrv().getAll(), {'id': dsId});
if (!ds) { if (!ds) {
return Promise.reject(`Data Source with ID ${dsId} not found`); return Promise.reject(`Data Source with ID ${dsId} not found`);
} }
dsName = ds.name; dsName = ds.name;
} }
if (dsName) { if (dsName) {
return datasourceSrv.loadDatasource(dsName); return getDataSourceSrv().loadDatasource(dsName);
} else { } else {
return Promise.reject(`Data Source name should be specified`); return Promise.reject(`Data Source name should be specified`);
} }
} }
loadDBDataSource() { loadDBDataSource() {
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName, this.datasourceSrv) return DBConnector.loadDatasource(this.datasourceId, this.datasourceName)
.then(ds => { .then(ds => {
this.datasourceTypeId = ds.meta.id; this.datasourceTypeId = ds.meta.id;
this.datasourceTypeName = ds.meta.name; this.datasourceTypeName = ds.meta.name;

View File

@@ -11,8 +11,8 @@ const consolidateByFunc = {
}; };
export class InfluxDBConnector extends DBConnector { export class InfluxDBConnector extends DBConnector {
constructor(options, datasourceSrv) { constructor(options) {
super(options, datasourceSrv); super(options);
this.retentionPolicy = options.retentionPolicy; this.retentionPolicy = options.retentionPolicy;
super.loadDBDataSource().then(ds => { super.loadDBDataSource().then(ds => {
this.influxDS = ds; this.influxDS = ds;
@@ -24,7 +24,14 @@ export class InfluxDBConnector extends DBConnector {
* Try to invoke test query for one of Zabbix database tables. * Try to invoke test query for one of Zabbix database tables.
*/ */
testDataSource() { testDataSource() {
return this.influxDS.testDatasource(); return this.influxDS.testDatasource().then(result => {
if (result.status && result.status === 'error') {
return Promise.reject({ data: {
message: `InfluxDB connection error: ${result.message}`
}});
}
return result;
});
} }
getHistory(items, timeFrom, timeTill, options) { getHistory(items, timeFrom, timeTill, options) {

View File

@@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { compactQuery } from '../../../utils'; import { compactQuery } from '../../../utils';
import mysql from './mysql'; import mysql from './mysql';
import postgres from './postgres'; import postgres from './postgres';
@@ -10,15 +11,14 @@ const supportedDatabases = {
}; };
export class SQLConnector extends DBConnector { export class SQLConnector extends DBConnector {
constructor(options, datasourceSrv) { constructor(options) {
super(options, datasourceSrv); super(options);
this.limit = options.limit || DEFAULT_QUERY_LIMIT; this.limit = options.limit || DEFAULT_QUERY_LIMIT;
this.sqlDialect = null; this.sqlDialect = null;
super.loadDBDataSource() super.loadDBDataSource()
.then(ds => { .then(() => {
this.backendSrv = ds.backendSrv;
this.loadSQLDialect(); this.loadSQLDialect();
}); });
} }
@@ -96,7 +96,7 @@ export class SQLConnector extends DBConnector {
maxDataPoints: this.limit maxDataPoints: this.limit
}; };
return this.backendSrv.datasourceRequest({ return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query', url: '/api/tsdb/query',
method: 'POST', method: 'POST',
data: { data: {

View File

@@ -10,7 +10,7 @@ import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MI
* Wraps API calls and provides high-level methods. * Wraps API calls and provides high-level methods.
*/ */
export class ZabbixAPIConnector { export class ZabbixAPIConnector {
constructor(api_url, username, password, version, basicAuth, withCredentials, backendSrv) { constructor(api_url, username, password, version, basicAuth, withCredentials) {
this.url = api_url; this.url = api_url;
this.username = username; this.username = username;
this.password = password; this.password = password;
@@ -26,7 +26,7 @@ export class ZabbixAPIConnector {
this.loginErrorCount = 0; this.loginErrorCount = 0;
this.maxLoginAttempts = 3; this.maxLoginAttempts = 3;
this.zabbixAPICore = new ZabbixAPICore(backendSrv); this.zabbixAPICore = new ZabbixAPICore();
this.getTrend = this.getTrend_ZBXNEXT1193; this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30; //getTrend = getTrend_30;

View File

@@ -1,12 +1,12 @@
/** /**
* General Zabbix API methods * General Zabbix API methods
*/ */
import { getBackendSrv } from '@grafana/runtime';
export class ZabbixAPICore { export class ZabbixAPICore {
/** @ngInject */ /** @ngInject */
constructor(backendSrv) { constructor() {
this.backendSrv = backendSrv;
} }
/** /**
@@ -50,7 +50,7 @@ export class ZabbixAPICore {
} }
datasourceRequest(requestOptions) { datasourceRequest(requestOptions) {
return this.backendSrv.datasourceRequest(requestOptions) return getBackendSrv().datasourceRequest(requestOptions)
.then((response) => { .then((response) => {
if (!response.data) { if (!response.data) {
return Promise.reject(new ZabbixAPIError({data: "General Error, no data"})); return Promise.reject(new ZabbixAPIError({data: "General Error, no data"}));

View File

@@ -25,7 +25,7 @@ const REQUESTS_TO_BIND = [
]; ];
export class Zabbix { export class Zabbix {
constructor(options, datasourceSrv, backendSrv) { constructor(options) {
let { let {
url, url,
username, username,
@@ -49,7 +49,7 @@ export class Zabbix {
}; };
this.cachingProxy = new CachingProxy(cacheOptions); this.cachingProxy = new CachingProxy(cacheOptions);
this.zabbixAPI = new ZabbixAPIConnector(url, username, password, zabbixVersion, basicAuth, withCredentials, backendSrv); this.zabbixAPI = new ZabbixAPIConnector(url, username, password, zabbixVersion, basicAuth, withCredentials);
this.proxyfyRequests(); this.proxyfyRequests();
this.cacheRequests(); this.cacheRequests();
@@ -57,7 +57,7 @@ export class Zabbix {
if (enableDirectDBConnection) { if (enableDirectDBConnection) {
const connectorOptions = { dbConnectionRetentionPolicy }; const connectorOptions = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv, connectorOptions) this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, connectorOptions)
.then(() => { .then(() => {
this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector); this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector);
this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector); this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector);
@@ -65,15 +65,15 @@ export class Zabbix {
} }
} }
initDBConnector(datasourceId, datasourceName, datasourceSrv, options) { initDBConnector(datasourceId, datasourceName, options) {
return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv) return DBConnector.loadDatasource(datasourceId, datasourceName)
.then(ds => { .then(ds => {
let connectorOptions = { datasourceId, datasourceName }; let connectorOptions = { datasourceId, datasourceName };
if (ds.type === 'influxdb') { if (ds.type === 'influxdb') {
connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy; connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
this.dbConnector = new InfluxDBConnector(connectorOptions, datasourceSrv); this.dbConnector = new InfluxDBConnector(connectorOptions);
} else { } else {
this.dbConnector = new SQLConnector(connectorOptions, datasourceSrv); this.dbConnector = new SQLConnector(connectorOptions);
} }
return this.dbConnector; return this.dbConnector;
}); });

View File

@@ -13,9 +13,9 @@ describe('Zabbix', () => {
beforeEach(() => { beforeEach(() => {
ctx.options = options; ctx.options = options;
ctx.backendSrv = mocks.backendSrvMock; // ctx.backendSrv = mocks.backendSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock; ctx.datasourceSrv = mocks.datasourceSrvMock;
zabbix = new Zabbix(ctx.options, ctx.backendSrvMock, ctx.datasourceSrvMock); zabbix = new Zabbix(ctx.options, ctx.datasourceSrvMock);
}); });
describe('When querying proxies', () => { describe('When querying proxies', () => {

View File

@@ -3,7 +3,7 @@ import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { isNewProblem, formatLastChange } from '../../utils'; import { isNewProblem, formatLastChange } from '../../utils';
import { ProblemsPanelOptions, ZBXTrigger, ZBXTag } from '../../types'; import { ProblemsPanelOptions, ZBXTrigger, TriggerSeverity, ZBXTag } from '../../types';
import { AckProblemData, Modal } from '.././Modal'; import { AckProblemData, Modal } from '.././Modal';
import EventTag from '../EventTag'; import EventTag from '../EventTag';
import Tooltip from '.././Tooltip/Tooltip'; import Tooltip from '.././Tooltip/Tooltip';
@@ -59,7 +59,13 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1; const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground }); const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground });
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine }); const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine });
const severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority));
let severityDesc: TriggerSeverity;
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority));
if (problem.lastEvent && problem.lastEvent.severity) {
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.lastEvent.severity));
}
const lastchange = formatLastChange(problem.lastchangeUnix, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat); const lastchange = formatLastChange(problem.lastchangeUnix, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat);
const age = moment.unix(problem.lastchangeUnix).fromNow(true); const age = moment.unix(problem.lastchangeUnix).fromNow(true);
@@ -72,7 +78,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
let problemColor: string; let problemColor: string;
if (problem.value === '0') { if (problem.value === '0') {
problemColor = panelOptions.okEventColor; problemColor = panelOptions.okEventColor;
} else if (panelOptions.markAckEvents && problem.acknowledges && problem.acknowledges.length) { } else if (panelOptions.markAckEvents && problem.lastEvent.acknowledged === "1") {
problemColor = panelOptions.ackEventColor; problemColor = panelOptions.ackEventColor;
} else { } else {
problemColor = severityDesc.color; problemColor = severityDesc.color;

View File

@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table-6';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
@@ -18,8 +18,8 @@ export interface ProblemListProps {
timeRange?: GFTimeRange; timeRange?: GFTimeRange;
pageSize?: number; pageSize?: number;
fontSize?: number; fontSize?: number;
getProblemEvents: (problem: ZBXTrigger) => ZBXEvent[]; getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ZBXTrigger) => ZBXAlert[]; getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onPageSizeChange?: (pageSize: number, pageIndex: number) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
@@ -163,6 +163,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
getProblemAlerts={this.props.getProblemAlerts} getProblemAlerts={this.props.getProblemAlerts}
onProblemAck={this.handleProblemAck} onProblemAck={this.handleProblemAck}
onTagClick={this.handleTagClick} onTagClick={this.handleTagClick}
subRows={false}
/> />
} }
expanded={this.getExpandedPage(this.state.page)} expanded={this.getExpandedPage(this.state.page)}
@@ -179,11 +180,17 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) { function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) {
const problem = props.original; const problem = props.original;
let color: string; let color: string;
const severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(props.original.priority));
let severityDesc: TriggerSeverity;
severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(props.original.priority));
if (problem.lastEvent && problem.lastEvent.severity) {
severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(problem.lastEvent.severity));
}
color = severityDesc.color; color = severityDesc.color;
// Mark acknowledged triggers with different color // Mark acknowledged triggers with different color
if (markAckEvents && problem.acknowledges && problem.acknowledges.length) { if (markAckEvents && problem.lastEvent.acknowledged === "1") {
color = ackEventColor; color = ackEventColor;
} }

View File

@@ -4,15 +4,20 @@ import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl'; import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl';
import {CURRENT_SCHEMA_VERSION} from '../migrations'; import {CURRENT_SCHEMA_VERSION} from '../migrations';
jest.mock('@grafana/runtime', () => {
return {
getDataSourceSrv: () => ({
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
}),
};
}, {virtual: true});
describe('Triggers Panel schema migration', () => { describe('Triggers Panel schema migration', () => {
let ctx: any = {}; let ctx: any = {};
let updatePanelCtrl; let updatePanelCtrl;
const datasourceSrvMock = {
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
};
const timeoutMock = () => {}; const timeoutMock = () => {};
@@ -43,7 +48,7 @@ describe('Triggers Panel schema migration', () => {
} }
}; };
updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock); updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock, {}, {}, {}, mocks.timeSrvMock);
}); });
it('should update old panel schema', () => { it('should update old panel schema', () => {

View File

@@ -4,9 +4,16 @@ import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl'; import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl';
// import { create } from 'domain'; // import { create } from 'domain';
let datasourceSrvMock, zabbixDSMock;
jest.mock('@grafana/runtime', () => {
return {
getDataSourceSrv: () => datasourceSrvMock,
};
}, {virtual: true});
describe('TriggerPanelCtrl', () => { describe('TriggerPanelCtrl', () => {
let ctx: any = {}; let ctx: any = {};
let datasourceSrvMock, zabbixDSMock;
const timeoutMock = () => {}; const timeoutMock = () => {};
let createPanelCtrl; let createPanelCtrl;
@@ -31,7 +38,8 @@ describe('TriggerPanelCtrl', () => {
}, },
get: () => Promise.resolve(zabbixDSMock) get: () => Promise.resolve(zabbixDSMock)
}; };
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock);
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, {}, {}, {}, mocks.timeSrvMock);
const getTriggersResp = [ const getTriggersResp = [
[ [

View File

@@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { getDataSourceSrv } from '@grafana/runtime';
import * as dateMath from 'grafana/app/core/utils/datemath'; import * as dateMath from 'grafana/app/core/utils/datemath';
import * as utils from '../datasource-zabbix/utils'; import * as utils from '../datasource-zabbix/utils';
import { PanelCtrl } from 'grafana/app/plugins/sdk'; import { PanelCtrl } from 'grafana/app/plugins/sdk';
@@ -96,9 +97,8 @@ const triggerStatusMap = {
export class TriggerPanelCtrl extends PanelCtrl { export class TriggerPanelCtrl extends PanelCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, $timeout, datasourceSrv, templateSrv, contextSrv, dashboardSrv, timeSrv) { constructor($scope, $injector, $timeout, templateSrv, contextSrv, dashboardSrv, timeSrv) {
super($scope, $injector); super($scope, $injector);
this.datasourceSrv = datasourceSrv;
this.templateSrv = templateSrv; this.templateSrv = templateSrv;
this.contextSrv = contextSrv; this.contextSrv = contextSrv;
this.dashboardSrv = dashboardSrv; this.dashboardSrv = dashboardSrv;
@@ -151,7 +151,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource)); const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource));
let promises = targetDatasources.map(ds => { let promises = targetDatasources.map(ds => {
// Load datasource // Load datasource
return this.datasourceSrv.get(ds) return getDataSourceSrv().get(ds)
.then(datasource => { .then(datasource => {
this.datasources[ds] = datasource; this.datasources[ds] = datasource;
return datasource; return datasource;
@@ -161,7 +161,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
} }
getZabbixDataSources() { getZabbixDataSources() {
return _.filter(this.datasourceSrv.getMetricSources(), datasource => { return _.filter(getDataSourceSrv().getMetricSources(), datasource => {
return datasource.meta.id === ZABBIX_DS_ID && datasource.value; return datasource.meta.id === ZABBIX_DS_ID && datasource.value;
}); });
} }
@@ -251,7 +251,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
const ds = target.datasource; const ds = target.datasource;
let proxies; let proxies;
let showAckButton = true; let showAckButton = true;
return this.datasourceSrv.get(ds) return getDataSourceSrv().get(ds)
.then(datasource => { .then(datasource => {
const zabbix = datasource.zabbix; const zabbix = datasource.zabbix;
const showEvents = this.panel.showEvents.value; const showEvents = this.panel.showEvents.value;
@@ -398,7 +398,11 @@ export class TriggerPanelCtrl extends PanelCtrl {
// Filter triggers by severity // Filter triggers by severity
triggerList = _.filter(triggerList, trigger => { triggerList = _.filter(triggerList, trigger => {
return this.panel.triggerSeverity[trigger.priority].show; if (trigger.lastEvent && trigger.lastEvent.severity) {
return this.panel.triggerSeverity[trigger.lastEvent.severity].show;
} else {
return this.panel.triggerSeverity[trigger.priority].show;
}
}); });
return triggerList; return triggerList;
@@ -520,7 +524,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
const triggerids = [problem.triggerid]; const triggerids = [problem.triggerid];
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
return this.datasourceSrv.get(problem.datasource) return getDataSourceSrv().get(problem.datasource)
.then(datasource => { .then(datasource => {
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT); return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
}); });
@@ -531,7 +535,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
return Promise.resolve([]); return Promise.resolve([]);
} }
const eventids = [problem.lastEvent.eventid]; const eventids = [problem.lastEvent.eventid];
return this.datasourceSrv.get(problem.datasource) return getDataSourceSrv().get(problem.datasource)
.then(datasource => { .then(datasource => {
return datasource.zabbix.getEventAlerts(eventids); return datasource.zabbix.getEventAlerts(eventids);
}); });
@@ -618,7 +622,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
let eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null; let eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null;
let grafana_user = this.contextSrv.user.name; let grafana_user = this.contextSrv.user.name;
let ack_message = grafana_user + ' (Grafana): ' + message; let ack_message = grafana_user + ' (Grafana): ' + message;
return this.datasourceSrv.get(trigger.datasource) return getDataSourceSrv().get(trigger.datasource)
.then(datasource => { .then(datasource => {
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin; const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
if (datasource.disableReadOnlyUsersAck && !userIsEditor) { if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
@@ -705,7 +709,13 @@ export class TriggerPanelCtrl extends PanelCtrl {
} else { } else {
problemsReactElem = React.createElement(ProblemList, problemsListProps); problemsReactElem = React.createElement(ProblemList, problemsListProps);
} }
ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]);
const panelContainerElem = elem.find('.panel-content');
if (panelContainerElem && panelContainerElem.length) {
ReactDOM.render(problemsReactElem, panelContainerElem[0]);
} else {
ReactDOM.render(problemsReactElem, elem[0]);
}
} }
} }
} }

View File

@@ -140,6 +140,7 @@ export interface ZBXEvent {
object?: string; object?: string;
objectid?: string; objectid?: string;
acknowledged?: string; acknowledged?: string;
severity?: string;
hosts?: ZBXHost[]; hosts?: ZBXHost[];
acknowledges?: ZBXAcknowledge[]; acknowledges?: ZBXAcknowledge[];
} }

View File

@@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { DataQuery } from '@grafana/ui/'; import { DataQuery } from '@grafana/data';
import * as utils from '../datasource-zabbix/utils'; import * as utils from '../datasource-zabbix/utils';
import { ZBXTrigger } from './types'; import { ZBXTrigger } from './types';

View File

@@ -26,8 +26,8 @@
{"name": "Metric Editor", "path": "img/screenshot-metric_editor.png"}, {"name": "Metric Editor", "path": "img/screenshot-metric_editor.png"},
{"name": "Triggers", "path": "img/screenshot-triggers.png"} {"name": "Triggers", "path": "img/screenshot-triggers.png"}
], ],
"version": "3.10.5", "version": "3.11.0",
"updated": "2019-12-26" "updated": "2020-03-23"
}, },
"includes": [ "includes": [

View File

@@ -1,5 +1,5 @@
// DEPENDENCIES // DEPENDENCIES
@import '../../node_modules/react-table/react-table.css'; @import '../../node_modules/react-table-6/react-table.css';
@import 'variables'; @import 'variables';
@import 'panel-triggers'; @import 'panel-triggers';

View File

@@ -75,9 +75,9 @@ jest.mock('grafana/app/core/config', () => {
jest.mock('jquery', () => 'module not found', {virtual: true}); jest.mock('jquery', () => 'module not found', {virtual: true});
jest.mock('@grafana/ui', () => { // jest.mock('@grafana/ui', () => {
return {}; // return {};
}, {virtual: true}); // }, {virtual: true});
// Required for loading angularjs // Required for loading angularjs
let dom = new JSDOM('<html><head><script></script></head><body></body></html>'); let dom = new JSDOM('<html><head><script></script></head><body></body></html>');

View File

@@ -17,7 +17,7 @@ module.exports = {
entry: { entry: {
'./module': './module.js', './module': './module.js',
'components/config': './components/config.js', 'components/config': './components/config.js',
'datasource-zabbix/module': './datasource-zabbix/module.js', 'datasource-zabbix/module': './datasource-zabbix/module.ts',
'panel-triggers/module': './panel-triggers/module.js', 'panel-triggers/module': './panel-triggers/module.js',
}, },
output: { output: {
@@ -27,8 +27,8 @@ module.exports = {
}, },
externals: [ externals: [
// remove the line below if you don't want to use builtin versions // remove the line below if you don't want to use builtin versions
'jquery', 'lodash', 'moment', 'angular', 'jquery', 'lodash', 'moment', 'angular', 'emotion',
'react', 'react-dom', '@grafana/ui', '@grafana/data', 'react', 'react-dom', '@grafana/ui', '@grafana/data', '@grafana/runtime',
function (context, request, callback) { function (context, request, callback) {
var prefix = 'grafana/'; var prefix = 'grafana/';
if (request.indexOf(prefix) === 0) { if (request.indexOf(prefix) === 0) {

1245
yarn.lock

File diff suppressed because it is too large Load Diff