Merge branch 'master' into docs

This commit is contained in:
Alexander Zobnin
2019-12-30 10:49:49 +03:00
61 changed files with 4966 additions and 2847 deletions

View File

@@ -1,7 +0,0 @@
{
"presets": [
"env",
"react"
],
"retainLines": true
}

View File

@@ -6,11 +6,17 @@ aliases:
branches:
ignore:
- master
- /^release-[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- docs
- gh-pages
- &filter-only-master
branches:
only: master
- &filter-only-release
branches:
ignore: /.*/
tags:
only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- &filter-docs
branches:
only: docs
@@ -20,7 +26,7 @@ jobs:
build:
working_directory: ~/alexanderzobnin/grafana-zabbix
docker:
- image: circleci/node:8
- image: circleci/node:12
steps:
- checkout
- restore_cache:
@@ -39,7 +45,7 @@ jobs:
lint:
working_directory: ~/alexanderzobnin/grafana-zabbix
docker:
- image: circleci/node:8
- image: circleci/node:12
steps:
- checkout
- restore_cache:
@@ -61,7 +67,7 @@ jobs:
CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
docker:
- image: circleci/node:8
- image: circleci/node:12
steps:
- checkout
# Prepare for artifact and test results collection equivalent to how it was done on 1.0.
@@ -87,6 +93,31 @@ jobs:
- store_artifacts:
path: /tmp/circleci-test-results
make-release:
working_directory: ~/alexanderzobnin/grafana-zabbix
docker:
- image: circleci/node:12
environment:
CI_GIT_USER: CircleCI
CI_GIT_EMAIL: ci@grafana.com
steps:
- add_ssh_keys:
fingerprints:
- "dc:7e:54:e0:aa:56:4d:e5:60:7b:f3:51:24:2d:d3:29"
- checkout
- restore_cache:
keys:
- dependency-cache-{{ checksum "yarn.lock" }}
- run:
name: yarn install
command: 'yarn install --pure-lockfile --no-progress'
no_output_timeout: 15m
- save_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
paths:
- ./node_modules
- run: ./.circleci/make-release.sh
build-docs:
working_directory: ~/grafana-zabbix
docker:
@@ -110,7 +141,7 @@ jobs:
deploy-docs:
working_directory: ~/grafana-zabbix
docker:
- image: circleci/node:8
- image: circleci/node:12
environment:
GH_PAGES_BRANCH: gh-pages
CI_GIT_USER: CircleCI
@@ -124,12 +155,23 @@ jobs:
at: ../gh-pages
- run: ./.circleci/deploy-docs.sh
codespell:
docker:
- image: circleci/python
steps:
- checkout
- run: sudo pip install codespell
- run: codespell -S './.git*,./src/img*' -L que --ignore-words=./.codespell_ignore
workflows:
version: 2
build-master:
jobs:
- build:
filters: *filter-only-master
- codespell:
filters: *filter-only-master
- lint:
filters: *filter-only-master
- test:
@@ -139,11 +181,31 @@ workflows:
jobs:
- build:
filters: *filter-not-release-or-master
- codespell:
filters: *filter-not-release-or-master
- lint:
filters: *filter-not-release-or-master
- test:
filters: *filter-not-release-or-master
build-release:
jobs:
- build:
filters: *filter-only-release
- codespell:
filters: *filter-only-release
- lint:
filters: *filter-only-release
- test:
filters: *filter-only-release
- make-release:
requires:
- build
- codespell
- lint
- test
filters: *filter-only-release
build-docs:
jobs:
- build-docs:

View File

@@ -10,15 +10,15 @@ set -o pipefail
echo "current dir: $(pwd)"
# Setup git env
git config --global user.email $CI_GIT_EMAIL
git config --global user.name $CI_GIT_USER
git config --global user.email "$CI_GIT_EMAIL"
git config --global user.name "$CI_GIT_USER"
echo "git user is $CI_GIT_USER ($CI_GIT_EMAIL)"
git checkout -b $GH_PAGES_BRANCH
rm -rf * || true
git checkout -b "$GH_PAGES_BRANCH"
rm -rf ./* || true
mv ../gh-pages/docs/site/* ./
git add --force .
git commit -m "build docs from commit ${CIRCLE_SHA1:0:7} (branch $CIRCLE_BRANCH)"
git log -n 3
git push origin $GH_PAGES_BRANCH --force
git push origin "$GH_PAGES_BRANCH" --force

42
.circleci/make-release.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Exit script if you try to use an uninitialized variable.
set -o nounset
# Exit script if a statement returns a non-true return value.
set -o errexit
# Use the error status of the first failure, rather than that of the last item in a pipeline.
set -o pipefail
# Setup git env
git config --global user.email "$CI_GIT_EMAIL"
git config --global user.name "$CI_GIT_USER"
echo "git user is $CI_GIT_USER ($CI_GIT_EMAIL)"
RELEASE_VER=$(echo "$CIRCLE_TAG" | grep -Po "(?<=v)[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)")
if [ -z "$RELEASE_VER" ]; then
echo "No release version provided"
exit 1
fi
if [[ $RELEASE_VER =~ ^[0-9]+(\.[0-9]+){2}(-.+|[^-.]*) ]]; then
echo "Preparing release $RELEASE_VER"
else
echo "Release should has format 1.2.3[-meta], got $RELEASE_VER"
exit 1
fi
RELEASE_BRANCH=release-$RELEASE_VER
# Build plugin
git checkout -b "$RELEASE_BRANCH"
yarn install --pure-lockfile && yarn build
# Commit release
git add --force dist/
git commit -m "release $RELEASE_VER"
RELEASE_COMMIT_HASH=$(git log -n 1 | grep -Po "(?<=commit )[0-9a-z]{40}")
echo "$RELEASE_COMMIT_HASH"
# Push release branch
git push origin "$RELEASE_BRANCH"

1
.codespell_ignore Normal file
View File

@@ -0,0 +1 @@
hist

View File

@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [3.10.5] - 2019-12-26
### Added
- SLA over time graphs, [#728](https://github.com/alexanderzobnin/grafana-zabbix/issues/728)
- Additional time ranges in functions, [#531](https://github.com/alexanderzobnin/grafana-zabbix/issues/531)
### Fixed
- Problems panel: query editor broken in Grafana 6.4, [#817](https://github.com/alexanderzobnin/grafana-zabbix/issues/817)
- Datasource: function editor is not working, [#810](https://github.com/alexanderzobnin/grafana-zabbix/issues/810)
- Datasource: cannot add a function to query from typeahead, [#468](https://github.com/alexanderzobnin/grafana-zabbix/issues/468)
- Datasource: annotations editor broken in Grafana 6.x, [#813](https://github.com/alexanderzobnin/grafana-zabbix/issues/813)
- React plugins issue, [#823](https://github.com/alexanderzobnin/grafana-zabbix/issues/823)
## [3.10.4] - 2019-08-08
### Fixed
- Problems panel: query editor broken in Grafana 6.3, [#778](https://github.com/alexanderzobnin/grafana-zabbix/issues/778)
- Problems panel: some heart icons are missing, [#754](https://github.com/alexanderzobnin/grafana-zabbix/issues/754)
## [3.10.3] - 2019-07-26
### Fixed
- Direct DB Connection: can't stay enabled, [#731](https://github.com/alexanderzobnin/grafana-zabbix/issues/731)
- Triggers query mode: count doesn't work with Singlestat, [#726](https://github.com/alexanderzobnin/grafana-zabbix/issues/726)
- Query editor: function editor looks odd in Grafana 6.x, [#765](https://github.com/alexanderzobnin/grafana-zabbix/issues/765)
- Alerting: heart icon on panels in Grafana 6.x, [#715](https://github.com/alexanderzobnin/grafana-zabbix/issues/715)
## [3.10.2] - 2019-04-23
### Fixed
- Direct DB Connection: provisioned datasource fails to load, [#711](https://github.com/alexanderzobnin/grafana-zabbix/issues/711)
- Functions: `sumSeries` doesn't work in couple with other aggregation functions, [#530](https://github.com/alexanderzobnin/grafana-zabbix/issues/530)
- Problems panel: performance and memory issues, [#720](https://github.com/alexanderzobnin/grafana-zabbix/issues/720), [#712](https://github.com/alexanderzobnin/grafana-zabbix/issues/712)
- Problems panel: hide acknowledge button for read-only users, [#722](https://github.com/alexanderzobnin/grafana-zabbix/issues/722)
- Problems panel: "no data" overlaps table header when font size increased, [#717](https://github.com/alexanderzobnin/grafana-zabbix/issues/717)
- Problems panel: problem description does not resize problem bar, [#704](https://github.com/alexanderzobnin/grafana-zabbix/issues/704)
- Triggers query mode: problems not filtered by selected groups, [#709](https://github.com/alexanderzobnin/grafana-zabbix/issues/709)
## [3.10.1] - 2019-03-05
### Fixed
- Problems panel: unable to edit panel in Grafana 6.0, [#685](https://github.com/alexanderzobnin/grafana-zabbix/issues/685)
@@ -189,7 +225,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- setAliasByRegex() function
### Changed
- **Docs**: deprecate special repo with builded plugin.
- **Docs**: deprecate special repo with built plugins.
- **Triggers panel**: remove 'default' from datasources list (cause error), iss [#340](https://github.com/alexanderzobnin/grafana-zabbix/issues/340)
- Add dist/ directory to repo to correspond development guide http://docs.grafana.org/plugins/development/

7
babel.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
"presets": [
[ "@babel/preset-env", { "targets": { "node": "current" } } ],
"@babel/react"
],
"retainLines": true
};

View File

@@ -56,7 +56,7 @@ Direct access is still supported because in some cases it may be useful to acces
Direct DB Connection allows plugin to use existing SQL data source for querying history data directly from Zabbix
database. This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
amount of data transfered.
amount of data transferred.
Read [how to configure](./sql_datasource) SQL data source in Grafana.

View File

@@ -25,7 +25,7 @@ Another case to use regex is comparing the same metrics for different hosts. Use
![Backend system time](../img/getstarting-regex_backend_system_time.png)
## Bar Chart
Let's create a graph wich show queries stats for MySQL database. Select Group, Host, Application (_MySQL_ in my case) and Items. I use `/MySQL .* operations/` regex for filtering different types of operations.
Let's create a graph which show queries stats for MySQL database. Select Group, Host, Application (_MySQL_ in my case) and Items. I use `/MySQL .* operations/` regex for filtering different types of operations.
![MySQL operations 1](../img/getstarting-mysql_operations_1.png)

View File

@@ -14,7 +14,7 @@ consists of two main parts:
- **Alerting execution engine**
The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part of core
Grafana. Only some data soures are supported right now. They include Graphite, Prometheus, InfluxDB and OpenTSDB.
Grafana. Only some data sources are supported right now. They include Graphite, Prometheus, InfluxDB and OpenTSDB.
- **Alerting visualisations**
Alerts highlight panels with problems and it can easily be found on the dashboard.

View File

@@ -1,6 +1,23 @@
Functions reference
===================
## Functions Variables
There are some built-in template variables available for using in functions:
- `$__range_ms` - panel time range in ms
- `$__range_s` - panel time range in seconds
- `$__range` - panel time range, string representation (`30s`, `1m`, `1h`)
- `$__range_series` - invoke function over all series values
Examples:
```
groupBy($__range, avg)
percentile($__range_series, 95) - 95th percentile over all values
```
---
## Transform
@@ -10,7 +27,7 @@ Functions reference
groupBy(interval, function)
```
Takes each timeseries and consolidate its points falled in given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
Takes each timeseries and consolidate its points fallen in the given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
Examples:
```
@@ -124,7 +141,7 @@ Replaces `null` values with N
aggregateBy(interval, function)
```
Takes all timeseries and consolidate all its points falled in given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
Takes all timeseries and consolidate all its points fallen in the given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
Examples:
```
@@ -142,6 +159,20 @@ This will add metrics together and return the sum at each datapoint. This method
---
### _percentile_
```
percentile(interval, N)
```
Takes all timeseries 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 values
```
---
### _average_
```
average(interval)

View File

@@ -1,8 +1,9 @@
{
"name": "grafana-zabbix",
"private": false,
"version": "3.10.1",
"version": "3.10.5",
"description": "Zabbix plugin for Grafana",
"homepage": "http://grafana-zabbix.org",
"scripts": {
"build": "webpack --config webpack/webpack.prod.conf.js --progress --colors",
"dev": "webpack --config webpack/webpack.dev.conf.js --progress --colors",
@@ -24,6 +25,11 @@
"url": "https://github.com/alexanderzobnin/grafana-zabbix/issues"
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/preset-env": "^7.7.7",
"@babel/preset-react": "^7.6.3",
"@grafana/data": "^6.4.2",
"@grafana/ui": "^6.4.2",
"@types/classnames": "^2.2.6",
"@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "^23.1.1",
@@ -33,35 +39,32 @@
"@types/react": "^16.4.6",
"@types/react-dom": "^16.0.11",
"@types/react-transition-group": "^2.0.15",
"babel-core": "^6.26.3",
"babel-jest": "^23.6.0",
"babel-loader": "^7.1.2",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"benchmark": "^2.1.4",
"classnames": "^2.2.6",
"clean-webpack-plugin": "^0.1.19",
"codecov": "^3.1.0",
"copy-webpack-plugin": "^4.5.4",
"css-loader": "^1.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "2.1.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"grunt": "^1.0.3",
"grunt-benchmark": "^1.0.0",
"grunt-cli": "^1.3.1",
"grunt-execute": "^0.2.2",
"html-loader": "^0.5.5",
"jest": "^23.6.0",
"jest": "^24.9.0",
"jscs": "^3.0.7",
"jsdom": "~11.3.0",
"jshint": "^2.9.6",
"jshint-stylish": "^2.1.0",
"load-grunt-tasks": "~3.2.0",
"lodash": "~4.17.5",
"lodash": "~4.17.13",
"moment": "~2.21.0",
"ng-annotate-webpack-plugin": "^0.3.0",
"node-sass": "^4.9.4",
"node-sass": "^4.13.0",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
@@ -69,15 +72,23 @@
"react-table": "^6.8.6",
"react-test-renderer": "^16.7.0",
"react-transition-group": "^2.5.2",
"sass-loader": "^7.1.0",
"rst2html": "github:thoward/rst2html#990cb89",
"sass-loader": "7.1.0",
"style-loader": "^0.23.1",
"tether-drop": "^1.4.2",
"ts-jest": "^23.10.5",
"ts-jest": "^24.2.0",
"ts-loader": "^4.4.1",
"tslint": "^5.11.0",
"typescript": "^2.9.2",
"webpack": "^4.22.0",
"webpack-cli": "^3.1.2"
"tslint": "5.20.1",
"typescript": "3.7.2",
"webpack": "4.29.6",
"webpack-cli": "3.2.3"
},
"homepage": "http://grafana-zabbix.org"
"resolutions": {
"js-yaml": "^3.13.1",
"lodash": "~4.17.13",
"set-value": "^2.0.1",
"mixin-deep": "^1.3.2",
"minimatch": "^3.0.2",
"fstream": "^1.0.12"
}
}

View File

@@ -47,7 +47,7 @@ angular
}
$scope.$apply(function() {
$scope.addFunction(funcDef);
$scope.ctrl.addFunction(funcDef);
});
$input.trigger('blur');
@@ -66,7 +66,7 @@ angular
});
$input.blur(function() {
// clicking the function dropdown menu wont
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(function() {
$input.val('');

View File

@@ -0,0 +1,109 @@
import React from 'react';
// import rst2html from 'rst2html';
import { FunctionDescriptor, FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls';
// @ts-ignore
import { PopoverController, Popover } from '@grafana/ui';
interface FunctionEditorProps extends FunctionEditorControlsProps {
func: FunctionDescriptor;
}
interface FunctionEditorState {
showingDescription: boolean;
}
class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEditorState> {
private triggerRef = React.createRef<HTMLSpanElement>();
constructor(props: FunctionEditorProps) {
super(props);
this.state = {
showingDescription: false,
};
}
renderContent = ({ updatePopperPosition }) => {
const {
onMoveLeft,
onMoveRight,
func: {
def: { name, description },
},
} = this.props;
const { showingDescription } = this.state;
if (showingDescription) {
return (
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
<h4 style={{ color: 'white' }}> {name} </h4>
<div>{description}</div>
/>
</div>
);
}
return (
<FunctionEditorControls
{...this.props}
onMoveLeft={() => {
onMoveLeft(this.props.func);
updatePopperPosition();
}}
onMoveRight={() => {
onMoveRight(this.props.func);
updatePopperPosition();
}}
onDescriptionShow={() => {
this.setState({ showingDescription: true }, () => {
updatePopperPosition();
});
}}
/>
);
};
render() {
return (
<PopoverController content={this.renderContent} placement="top" hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.triggerRef && (
<Popover
{...popperProps}
referenceElement={this.triggerRef.current}
wrapperClassName="popper"
className="popper__background"
onMouseLeave={() => {
this.setState({ showingDescription: false });
hidePopper();
}}
onMouseEnter={showPopper}
renderArrow={({ arrowProps, placement }) => (
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
)}
/>
)}
<span
ref={this.triggerRef}
onClick={popperProps.show ? hidePopper : showPopper}
onMouseLeave={() => {
hidePopper();
this.setState({ showingDescription: false });
}}
style={{ cursor: 'pointer' }}
>
{this.props.func.def.name}
</span>
</>
);
}}
</PopoverController>
);
}
}
export { FunctionEditor };

View File

@@ -0,0 +1,67 @@
import React from 'react';
const DOCS_FUNC_REF_URL = 'https://alexanderzobnin.github.io/grafana-zabbix/reference/functions/';
export interface FunctionDescriptor {
text: string;
params: string[];
def: {
category: string;
defaultParams: string[];
description?: string;
fake: boolean;
name: string;
params: string[];
};
}
export interface FunctionEditorControlsProps {
onMoveLeft: (func: FunctionDescriptor) => void;
onMoveRight: (func: FunctionDescriptor) => void;
onRemove: (func: FunctionDescriptor) => void;
}
const FunctionHelpButton = (props: { description: string; name: string; onDescriptionShow: () => void }) => {
if (props.description) {
return <span className="pointer fa fa-question-circle" onClick={props.onDescriptionShow} />;
}
return (
<span
className="pointer fa fa-question-circle"
onClick={() => {
window.open(
DOCS_FUNC_REF_URL + '#' + props.name,
'_blank'
);
}}
/>
);
};
export const FunctionEditorControls = (
props: FunctionEditorControlsProps & {
func: FunctionDescriptor;
onDescriptionShow: () => void;
}
) => {
const { func, onMoveLeft, onMoveRight, onRemove, onDescriptionShow } = props;
return (
<div
style={{
display: 'flex',
width: '60px',
justifyContent: 'space-between',
}}
>
<span className="pointer fa fa-arrow-left" onClick={() => onMoveLeft(func)} />
<FunctionHelpButton
name={func.def.name}
description={func.def.description}
onDescriptionShow={onDescriptionShow}
/>
<span className="pointer fa fa-remove" onClick={() => onRemove(func)} />
<span className="pointer fa fa-arrow-right" onClick={() => onMoveRight(func)} />
</div>
);
};

View File

@@ -28,9 +28,14 @@ export class ZabbixDSConfigController {
this.current.jsonData = migrateDSConfig(this.current.jsonData);
_.defaults(this.current.jsonData, defaultConfig);
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
this.dbDataSources = this.getSupportedDBDataSources();
this.zabbixVersions = _.cloneDeep(zabbixVersions);
this.autoDetectZabbixVersion();
if (!this.dbConnectionDatasourceId) {
this.loadCurrentDBDatasource();
}
}
getSupportedDBDataSources() {
@@ -41,11 +46,21 @@ export class ZabbixDSConfigController {
}
getCurrentDatasourceType() {
const dsId = this.current.jsonData.dbConnectionDatasourceId;
const dsId = this.dbConnectionDatasourceId;
const currentDs = _.find(this.dbDataSources, { 'id': dsId });
return currentDs ? currentDs.type : null;
}
loadCurrentDBDatasource() {
const dsName= this.current.jsonData.dbConnectionDatasourceName;
this.datasourceSrv.loadDatasource(dsName)
.then(ds => {
if (ds) {
this.dbConnectionDatasourceId = ds.id;
}
});
}
autoDetectZabbixVersion() {
if (!this.current.id) {
return;
@@ -64,4 +79,8 @@ export class ZabbixDSConfigController {
}
});
}
onDBConnectionDatasourceChange() {
this.current.jsonData.dbConnectionDatasourceId = this.dbConnectionDatasourceId;
}
}

View File

@@ -34,3 +34,8 @@ export const TRIGGER_SEVERITY = [
{val: 4, text: 'High'},
{val: 5, text: 'Disaster'}
];
/** Minimum interval for SLA over time (1 hour) */
export const MIN_SLA_INTERVAL = 3600;
export const RANGE_VARIABLE_VALUE = 'range_series';

View File

@@ -1,9 +1,8 @@
import _ from 'lodash';
import * as utils from './utils';
import ts from './timeseries';
import ts, { groupBy_perf as groupBy } from './timeseries';
let downsampleSeries = ts.downsample;
let groupBy = ts.groupBy_perf;
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
let sumSeries = ts.sumSeries;
let delta = ts.delta;
@@ -19,7 +18,7 @@ let AVERAGE = ts.AVERAGE;
let MIN = ts.MIN;
let MAX = ts.MAX;
let MEDIAN = ts.MEDIAN;
let PERCENTIL = ts.PERCENTIL;
let PERCENTILE = ts.PERCENTILE;
function limit(order, n, orderByFunc, timeseries) {
let orderByCallback = aggregationFunctions[orderByFunc];
@@ -107,7 +106,7 @@ function groupByWrapper(interval, groupFunc, datapoints) {
function aggregateByWrapper(interval, aggregateFunc, datapoints) {
// Flatten all points in frame and then just use groupBy()
const flattenedPoints = _.flatten(datapoints, true);
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = aggregationFunctions[aggregateFunc];
@@ -115,15 +114,15 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) {
}
function aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = _.flatten(datapoints, true);
var flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
return groupBy(sortedPoints, interval, groupByCallback);
}
function percentil(interval, n, datapoints) {
var flattenedPoints = _.flatten(datapoints, true);
var groupByCallback = _.partial(PERCENTIL, n);
function percentile(interval, n, datapoints) {
var flattenedPoints = ts.flattenDatapoints(datapoints);
var groupByCallback = _.partial(PERCENTILE, n);
return groupBy(flattenedPoints, interval, groupByCallback);
}
@@ -155,7 +154,7 @@ let metricFunctions = {
transformNull: transformNull,
aggregateBy: aggregateByWrapper,
// Predefined aggs
percentil: percentil,
percentile: percentile,
average: _.partial(aggregateWrapper, AVERAGE),
min: _.partial(aggregateWrapper, MIN),
max: _.partial(aggregateWrapper, MAX),

View File

@@ -111,6 +111,9 @@ export class ZabbixDatasource {
let timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000);
let timeTo = Math.ceil(dateMath.parse(options.range.to) / 1000);
// Add range variables
options.scopedVars = Object.assign({}, options.scopedVars, utils.getRangeScopedVars(options.range));
// Prevent changes of original object
let target = _.cloneDeep(t);
@@ -323,14 +326,14 @@ export class ZabbixDatasource {
return this.zabbix.getITServices(itServiceFilter)
.then(itservices => {
return this.zabbix.getSLA(itservices, timeRange, target, options);
});
return this.zabbix.getSLA(itservices, timeRange, target, options);})
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
}
queryTriggersData(target, timeRange) {
let [timeFrom, timeTo] = timeRange;
return this.zabbix.getHostsFromTarget(target)
.then((results) => {
.then(results => {
let [hosts, apps] = results;
if (hosts.length) {
let hostids = _.map(hosts, 'hostid');
@@ -342,9 +345,13 @@ export class ZabbixDatasource {
timeFrom: timeFrom,
timeTo: timeTo
};
return this.zabbix.getHostAlerts(hostids, appids, options)
.then((triggers) => {
return responseHandler.handleTriggersResponse(triggers, timeRange);
const groupFilter = target.group.filter;
return Promise.all([
this.zabbix.getHostAlerts(hostids, appids, options),
this.zabbix.getGroups(groupFilter)
])
.then(([triggers, groups]) => {
return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
});
} else {
return Promise.resolve([]);

View File

@@ -1,248 +0,0 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
const DOCS_FUNC_REF_URL = 'https://alexanderzobnin.github.io/grafana-zabbix/reference/functions/';
angular
.module('grafana.directives')
.directive('metricFunctionEditor',
/** @ngInject */
function($compile, templateSrv) {
var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
var funcControlsTemplate =
'<div class="tight-form-func-controls">' +
'<span class="pointer fa fa-arrow-left"></span>' +
'<span class="pointer fa fa-question-circle"></span>' +
'<span class="pointer fa fa-remove" ></span>' +
'<span class="pointer fa fa-arrow-right"></span>' +
'</div>';
return {
restrict: 'A',
link: function postLink($scope, elem) {
var $funcLink = $(funcSpanTemplate);
var $funcControls = $(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 = $(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 = $(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 = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: function (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);
_.each(funcDef.params, function(param, index) {
if (param.optional && func.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo(elem);
}
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var $input = $(paramTemplate);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (funcDef.params[index].options) {
addTypeahead($input, index);
}
});
$('<span>)</span>').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 = $(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() {
_.move($scope.target.functions, $scope.$index, $scope.$index - 1);
ctrl.targetChanged();
});
return;
}
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move($scope.target.functions, $scope.$index, $scope.$index + 1);
ctrl.targetChanged();
});
return;
}
if ($target.hasClass('fa-question-circle')) {
var docSite = DOCS_FUNC_REF_URL;
window.open(docSite + '#' + funcDef.name.toLowerCase(),'_blank');
return;
}
});
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFistParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
relink();
}
};
});

View File

@@ -0,0 +1,256 @@
import coreModule from 'grafana/app/core/core_module';
import _ from 'lodash';
import $ from 'jquery';
import { react2AngularDirective } from './react2angular';
import { FunctionEditor } from './components/FunctionEditor';
/** @ngInject */
export function zabbixFunctionEditor($compile, templateSrv) {
const funcSpanTemplate = `
<zbx-function-editor
func="func"
onRemove="ctrl.handleRemoveFunction"
onMoveLeft="ctrl.handleMoveLeft"
onMoveRight="ctrl.handleMoveRight"
/><span>(</span>
`;
const paramTemplate =
'<input type="text" style="display:none"' + ' class="input-small tight-form-func-param"></input>';
return {
restrict: 'A',
link: function postLink($scope, elem) {
const $funcLink = $(funcSpanTemplate);
const ctrl = $scope.ctrl;
const func = $scope.func;
let scheduledRelink = false;
let paramCountAtLink = 0;
let cancelBlur = null;
ctrl.handleRemoveFunction = func => {
ctrl.removeFunction(func);
};
ctrl.handleMoveLeft = func => {
ctrl.moveFunction(func, -1);
};
ctrl.handleMoveRight = func => {
ctrl.moveFunction(func, 1);
};
function clickFuncParam(this: any, paramIndex) {
/*jshint validthis:true */
const $link = $(this);
const $comma = $link.prev('.comma');
const $input = $link.next();
$input.val(func.params[paramIndex]);
$comma.removeClass('query-part__last');
$link.hide();
$input.show();
$input.focus();
$input.select();
const typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function scheduledRelinkIfNeeded() {
if (paramCountAtLink === func.params.length) {
return;
}
if (!scheduledRelink) {
scheduledRelink = true;
setTimeout(() => {
relink();
scheduledRelink = false;
}, 200);
}
}
function paramDef(index) {
if (index < func.def.params.length) {
return func.def.params[index];
}
if ((_.last(func.def.params) as any).multiple) {
return _.assign({}, _.last(func.def.params), { optional: true });
}
return {};
}
function switchToLink(inputElem, paramIndex) {
/*jshint validthis:true */
const $input = $(inputElem);
clearTimeout(cancelBlur);
cancelBlur = null;
const $link = $input.prev();
const $comma = $link.prev('.comma');
const newValue = $input.val();
// remove optional empty params
if (newValue !== '' || paramDef(paramIndex).optional) {
func.updateParam(newValue, paramIndex);
$link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
}
scheduledRelinkIfNeeded();
$scope.$apply(() => {
ctrl.targetChanged();
});
if ($link.hasClass('query-part__last') && newValue === '') {
$comma.addClass('query-part__last');
} else {
$link.removeClass('query-part__last');
}
$input.hide();
$link.show();
}
// this = input element
function inputBlur(this: any, paramIndex) {
/*jshint validthis:true */
const inputElem = this;
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout(() => {
switchToLink(inputElem, paramIndex);
}, 200);
}
function inputKeyPress(this: any, paramIndex, e) {
/*jshint validthis:true */
if (e.which === 13) {
$(this).blur();
}
}
function inputKeyDown(this: any) {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
let options = paramDef(paramIndex).options;
if (paramDef(paramIndex).type === 'int' || paramDef(paramIndex).type === 'float') {
options = _.map(options, val => {
return val.toString();
});
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: value => {
$input.val(value);
switchToLink($input[0], paramIndex);
return value;
},
});
const typeahead = $input.data('typeahead');
typeahead.lookup = function() {
this.query = this.$element.val() || '';
return this.process(this.source);
};
}
function addElementsAndCompile() {
$funcLink.appendTo(elem);
const defParams: any = _.clone(func.def.params);
const lastParam: any = _.last(func.def.params);
while (func.params.length >= defParams.length && lastParam && lastParam.multiple) {
defParams.push(_.assign({}, lastParam, { optional: true }));
}
_.each(defParams, (param: any, index: number) => {
if (param.optional && func.params.length < index) {
return false;
}
let paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
const hasValue = paramValue !== null && paramValue !== undefined;
const last = index >= func.params.length - 1 && param.optional && !hasValue;
if (last && param.multiple) {
paramValue = '+';
}
if (index > 0) {
$('<span class="comma' + (last ? ' query-part__last' : '') + '">, </span>').appendTo(elem);
}
const $paramLink = $(
'<a ng-click="" class="graphite-func-param-link' +
(last ? ' query-part__last' : '') +
'">' +
(hasValue ? paramValue : '&nbsp;') +
'</a>'
);
const $input = $(paramTemplate);
$input.attr('placeholder', param.name);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (param.options) {
addTypeahead($input, index);
}
return true;
});
$('<span>)</span>').appendTo(elem);
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFirstParam() {
if ($scope.func.added) {
$scope.func.added = false;
setTimeout(() => {
elem
.find('.graphite-func-param-link')
.first()
.click();
}, 10);
}
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFirstParam();
}
relink();
},
};
}
coreModule.directive('zabbixFunctionEditor', zabbixFunctionEditor);
react2AngularDirective('zbxFunctionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);

View File

@@ -140,7 +140,7 @@ addFuncDef({
});
addFuncDef({
name: 'percentil',
name: 'percentile',
category: 'Aggregate',
params: [
{ name: 'interval', type: 'string' },

View File

@@ -33,6 +33,7 @@ export function migrate(target) {
if (isGrafana2target(target)) {
return migrateFrom2To3version(target);
}
migratePercentileAgg(target);
return target;
}
@@ -51,6 +52,16 @@ function convertToRegex(str) {
}
}
function migratePercentileAgg(target) {
if (target.functions) {
for (const f of target.functions) {
if (f.def && f.def.name === 'percentil') {
f.def.name = 'percentile';
}
}
}
}
export const DS_CONFIG_SCHEMA = 2;
export function migrateDSConfig(jsonData) {
if (!jsonData) {

View File

@@ -1,5 +1,4 @@
<div class="gf-form-group">
<h6>Filter Triggers</h6>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Group</span>
@@ -36,8 +35,9 @@
</div>
</div>
<div class="gf-form-group">
<h6>Options</h6>
<div class="gf-form">
<span class="gf-form-label width-10">Minimum severity</span>
<span class="gf-form-label width-12">Minimum severity</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto"
ng-init='ctrl.annotation.minseverity = ctrl.annotation.minseverity || 0'
@@ -54,12 +54,16 @@
</select>
</div>
</div>
</div>
<div class="gf-form-group">
<h6>Options</h6>
<div class="gf-form">
<editor-checkbox text="Show OK events" model="ctrl.annotation.showOkEvents"></editor-checkbox>
<editor-checkbox text="Hide acknowledged events" model="ctrl.annotation.hideAcknowledged"></editor-checkbox>
<editor-checkbox text="Show hostname" model="ctrl.annotation.showHostname"></editor-checkbox>
</div>
<gf-form-switch class="gf-form" label-class="width-12"
label="Show OK events"
checked="ctrl.annotation.showOkEvents">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Hide acknowledged events"
checked="ctrl.annotation.hideAcknowledged">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Show hostname"
checked="ctrl.annotation.showHostname">
</gf-form-switch>
</div>

View File

@@ -99,12 +99,14 @@
In order to use this feature it should be <a href="/datasources/new" target="_blank">created</a> and
configured first. Zabbix plugin uses this data source for querying history data directly from the database.
This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
amount of data transfered.
amount of data transferred.
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-16">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnectionDatasourceId"
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources">
<select class="gf-form-input"
ng-model="ctrl.dbConnectionDatasourceId"
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources"
ng-change="ctrl.onDBConnectionDatasourceChange()">
</select>
</div>
</div>

View File

@@ -201,10 +201,12 @@
</div>
<!-- Metric processing functions -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID">
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID || ctrl.target.mode == editorMode.ITSERVICE">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Functions</label>
<div ng-repeat="func in ctrl.target.functions" class="gf-form-label query-part" metric-function-editor></div>
</div>
<div ng-repeat="func in ctrl.target.functions" class="gf-form">
<span zabbix-function-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
</div>
<div class="gf-form dropdown" add-metric-function>
</div>

View File

@@ -275,11 +275,15 @@ export class ZabbixQueryController extends QueryCtrl {
this.targetChanged();
}
moveFunction(func, offset) {
const index = this.target.functions.indexOf(func);
_.move(this.target.functions, index, index + offset);
this.targetChanged();
}
moveAliasFuncLast() {
var aliasFunc = _.find(this.target.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
var aliasFunc = _.find(this.target.functions, func => {
return func.def.category === 'Alias';
});
if (aliasFunc) {

View File

@@ -0,0 +1,10 @@
import coreModule from 'grafana/app/core/core_module';
export function react2AngularDirective(name: string, component: any, options: any) {
coreModule.directive(name, [
'reactDirective',
reactDirective => {
return reactDirective(component, options);
},
]);
}

View File

@@ -123,45 +123,59 @@ function extractText(str, pattern, useCaptureGroups) {
}
function handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla[0];
var targetSLA = slaObject[itservice.serviceid].sla;
if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetStatus, targetSLA.to * 1000]
[targetStatus, targetSLA[0].to * 1000]
]
};
} else {
let i;
let slaArr = [];
for (i = 0; i < targetSLA.length; i++) {
if (i === 0) {
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]);
}
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].to * 1000]);
}
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetSLA[slaProperty.property], targetSLA.from * 1000],
[targetSLA[slaProperty.property], targetSLA.to * 1000]
]
datapoints: slaArr
};
}
}
function handleTriggersResponse(triggers, timeRange) {
if (_.isNumber(triggers)) {
function handleTriggersResponse(triggers, groups, timeRange) {
if (!_.isArray(triggers)) {
let triggersCount = null;
try {
triggersCount = Number(triggers);
} catch (err) {
console.log("Error when handling triggers count: ", err);
}
return {
target: "triggers count",
datapoints: [
[triggers, timeRange[1] * 1000]
[triggersCount, timeRange[1] * 1000]
]
};
} else {
let stats = getTriggerStats(triggers);
const stats = getTriggerStats(triggers);
const groupNames = _.map(groups, 'name');
let table = new TableModel();
table.addColumn({text: 'Host group'});
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
table.addColumn({text: severity.text});
});
_.each(stats, (severity_stats, group) => {
if (_.includes(groupNames, group)) {
let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]);
row = _.concat([group], ...row);
table.rows.push(row);
}
});
return table;
}

View File

@@ -2,6 +2,7 @@ import _ from 'lodash';
import mocks from '../../test-setup/mocks';
import { Datasource } from "../module";
import { zabbixTemplateFormat } from "../datasource";
import { dateMath } from '@grafana/data';
describe('ZabbixDatasource', () => {
let ctx = {};
@@ -41,7 +42,10 @@ describe('ZabbixDatasource', () => {
item: {filter: ""}
}
],
range: {from: 'now-7d', to: 'now'}
range: {
from: dateMath.parse('now-1h'),
to: dateMath.parse('now')
}
};
it('should return an empty array when no targets are set', (done) => {
@@ -59,7 +63,7 @@ describe('ZabbixDatasource', () => {
let ranges = ['now-8d', 'now-169h', 'now-1M', 'now-1y'];
_.forEach(ranges, range => {
ctx.options.range.from = range;
ctx.options.range.from = dateMath.parse(range);
ctx.ds.queryNumericData = jest.fn();
ctx.ds.query(ctx.options);
@@ -76,7 +80,7 @@ describe('ZabbixDatasource', () => {
let ranges = ['now-7d', 'now-168h', 'now-1h', 'now-30m', 'now-30s'];
_.forEach(ranges, range => {
ctx.options.range.from = range;
ctx.options.range.from = dateMath.parse(range);
ctx.ds.queryNumericData = jest.fn();
ctx.ds.query(ctx.options);
@@ -108,10 +112,7 @@ describe('ZabbixDatasource', () => {
}
]));
ctx.options = {
range: {from: 'now-1h', to: 'now'},
targets: [
{
ctx.options.targets = [{
group: {filter: ""},
host: {filter: "Zabbix server"},
application: {filter: ""},
@@ -123,9 +124,7 @@ describe('ZabbixDatasource', () => {
options: {
skipEmptyValues: false
}
}
],
};
}];
});
it('should return data in table format', (done) => {

View File

@@ -138,4 +138,31 @@ describe('Utils', () => {
done();
});
});
describe('getArrayDepth()', () => {
it('should calculate proper array depth', () => {
const test_cases = [
{
array: [],
depth: 1
},
{
array: [1, 2, 3],
depth: 1
},
{
array: [[1, 2], [3, 4]],
depth: 2
},
{
array: [[[1, 2], [3, 4]], [[1, 2], [3, 4]]],
depth: 3
},
];
for (const test_case of test_cases) {
expect(utils.getArrayDepth(test_case.array)).toBe(test_case.depth);
}
});
});
});

View File

@@ -11,6 +11,7 @@
import _ from 'lodash';
import * as utils from './utils';
import * as c from './constants';
const POINT_VALUE = 0;
const POINT_TIMESTAMP = 1;
@@ -94,11 +95,15 @@ function groupBy(datapoints, interval, groupByCallback) {
}));
}
function groupBy_perf(datapoints, interval, groupByCallback) {
export function groupBy_perf(datapoints, interval, groupByCallback) {
if (datapoints.length === 0) {
return [];
}
if (interval === c.RANGE_VARIABLE_VALUE) {
return groupByRange(datapoints, groupByCallback);
}
let ms_interval = utils.parseInterval(interval);
let grouped_series = [];
let frame_values = [];
@@ -132,6 +137,19 @@ function groupBy_perf(datapoints, interval, groupByCallback) {
return grouped_series;
}
export function groupByRange(datapoints, groupByCallback) {
const frame_values = [];
const frame_start = datapoints[0][POINT_TIMESTAMP];
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
let point;
for (let i=0; i < datapoints.length; i++) {
point = datapoints[i];
frame_values.push(point[POINT_VALUE]);
}
const frame_value = groupByCallback(frame_values);
return [[frame_value, frame_start], [frame_value, frame_end]];
}
/**
* Summarize set of time series into one.
* @param {datapoints[]} timeseries array of time series
@@ -333,7 +351,7 @@ function expMovingAverage(datapoints, n) {
return ema;
}
function PERCENTIL(n, values) {
function PERCENTILE(n, values) {
var sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length * n / 100)];
}
@@ -478,6 +496,15 @@ function findNearestLeft(series, pointIndex) {
return null;
}
function flattenDatapoints(datapoints) {
const depth = utils.getArrayDepth(datapoints);
if (depth <= 2) {
// Don't process if datapoints already flattened
return datapoints;
}
return _.flatten(datapoints);
}
////////////
// Export //
////////////
@@ -486,6 +513,7 @@ const exportedFunctions = {
downsample,
groupBy,
groupBy_perf,
groupByRange,
sumSeries,
scale,
offset,
@@ -500,8 +528,9 @@ const exportedFunctions = {
MIN,
MAX,
MEDIAN,
PERCENTIL,
sortByTime
PERCENTILE,
sortByTime,
flattenDatapoints,
};
export default exportedFunctions;

View File

@@ -1,5 +1,7 @@
import _ from 'lodash';
import moment from 'moment';
import kbn from 'grafana/app/core/utils/kbn';
import * as c from './constants';
/**
* Expand Zabbix item name
@@ -141,6 +143,18 @@ export function isTemplateVariable(str, templateVariables) {
}
}
export function getRangeScopedVars(range) {
const msRange = range.to.diff(range.from);
const sRange = Math.round(msRange / 1000);
const regularRange = kbn.secondsToHms(msRange / 1000);
return {
__range_ms: { text: msRange, value: msRange },
__range_s: { text: sRange, value: sRange },
__range: { text: regularRange, value: regularRange },
__range_series: {text: c.RANGE_VARIABLE_VALUE, value: c.RANGE_VARIABLE_VALUE},
};
}
export function buildRegex(str) {
var matches = str.match(regexPattern);
var pattern = matches[1];
@@ -265,6 +279,17 @@ export function compactQuery(query) {
return query.replace(/\s+/g, ' ').trim();
}
export function getArrayDepth(a, level = 0) {
if (a.length === 0) {
return 1;
}
const elem = a[0];
if (_.isArray(elem)) {
return getArrayDepth(elem, level + 1);
}
return level + 1;
}
// Fix for backward compatibility with lodash 2.4
if (!_.includes) {
_.includes = _.contains;

View File

@@ -65,6 +65,9 @@ export class DBConnector {
if (!this.datasourceName) {
this.datasourceName = ds.name;
}
if (!this.datasourceId) {
this.datasourceId = ds.id;
}
return ds;
});
}

View File

@@ -1,7 +1,8 @@
import _ from 'lodash';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE } from '../../../constants';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
/**
* Zabbix API Wrapper.
@@ -316,14 +317,11 @@ export class ZabbixAPIConnector {
return this.request('service.get', params);
}
getSLA(serviceids, timeRange) {
let [timeFrom, timeTo] = timeRange;
var params = {
serviceids: serviceids,
intervals: [{
from: timeFrom,
to: timeTo
}]
getSLA(serviceids, timeRange, options) {
const intervals = buildSLAIntervals(timeRange, options.intervalMs);
const params = {
serviceids,
intervals
};
return this.request('service.getsla', params);
}
@@ -421,7 +419,12 @@ export class ZabbixAPIConnector {
getEventAlerts(eventids) {
const params = {
eventids: eventids,
output: 'extend',
output: [
'eventid',
'message',
'clock',
'error'
],
selectUsers: true,
};
@@ -520,3 +523,30 @@ function isNotAuthorized(message) {
message === "Not authorized."
);
}
function getSLAInterval(intervalMs) {
// Too many intervals may cause significant load on the database, so decrease number of resulting points
const resolutionRatio = 100;
const interval = kbn.round_interval(intervalMs * resolutionRatio) / 1000;
return Math.max(interval, MIN_SLA_INTERVAL);
}
function buildSLAIntervals(timeRange, intervalMs) {
let [timeFrom, timeTo] = timeRange;
const slaInterval = getSLAInterval(intervalMs);
const intervals = [];
// Align time range with calculated interval
timeFrom = Math.floor(timeFrom / slaInterval) * slaInterval;
timeTo = Math.ceil(timeTo / slaInterval) * slaInterval;
for (let i = timeFrom; i <= timeTo - slaInterval; i += slaInterval) {
intervals.push({
from : i,
to : (i + slaInterval)
});
}
return intervals;
}

View File

@@ -363,7 +363,7 @@ export class Zabbix {
itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
}
let itServiceIds = _.map(itServices, 'serviceid');
return this.zabbixAPI.getSLA(itServiceIds, timeRange)
return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
.then(slaResponse => {
return _.map(itServiceIds, serviceid => {
let itservice = _.find(itServices, {'serviceid': serviceid});

View File

@@ -10,7 +10,7 @@ class ZabbixAlertingService {
}
isFullScreen() {
return this.dashboardSrv.dash.meta.fullscreen;
return this.getDashboardModel().meta.fullscreen;
}
setPanelAlertState(panelId, alertState) {
@@ -30,26 +30,24 @@ class ZabbixAlertingService {
});
}
if (panelIndex >= 0) {
// Don't apply alert styles to .panel-container--absolute (it rewrites position from absolute to relative)
if (panelIndex >= 0 && !panelContainers[panelIndex].className.includes('panel-container--absolute')) {
let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
$(panelContainers[panelIndex]).removeClass(alertClass);
if (alertState) {
if (alertState === 'alerting') {
alertClass = "panel-has-alert panel-alert-state--" + alertState;
$(panelContainers[panelIndex]).addClass(alertClass);
}
if (alertState === 'ok') {
alertClass = "panel-alert-state--" + alertState;
$(panelContainers[panelIndex]).addClass(alertClass);
$(panelContainers[panelIndex]).removeClass("panel-has-alert");
}
}
}
}
getDashboardModel() {
return this.dashboardSrv.dash || this.dashboardSrv.dashboard;
}
getPanelModels() {
return _.filter(this.dashboardSrv.dash.panels, panel => panel.type !== 'row');
return _.filter(this.getDashboardModel().panels, panel => panel.type !== 'row');
}
getPanelModel(panelId) {

View File

@@ -36,11 +36,13 @@ export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesPr
{ackRows}
</tbody>
</table>
{problem.showAckButton &&
<div className="ack-add-button">
<button id="add-acknowledge-btn" className="btn btn-mini btn-inverse gf-form-button" onClick={this.handleClick}>
<i className="fa fa-plus"></i>
</button>
</div>
}
</div>
);
}

View File

@@ -13,7 +13,7 @@ import AlertIcon from './AlertIcon';
interface AlertCardProps {
problem: ZBXTrigger;
panelOptions: ProblemsPanelOptions;
onTagClick?: (tag: ZBXTag, datasource: string) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
}
@@ -27,9 +27,9 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
this.state = { showAckDialog: false };
}
handleTagClick = (tag: ZBXTag) => {
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
this.props.onTagClick(tag, this.props.problem.datasource);
this.props.onTagClick(tag, this.props.problem.datasource, ctrlKey, shiftKey);
}
}
@@ -44,8 +44,11 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
}
showAckDialog = () => {
const problem = this.props.problem;
if (problem.showAckButton) {
this.setState({ showAckDialog: true });
}
}
closeAckDialog = () => {
this.setState({ showAckDialog: false });
@@ -53,6 +56,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
render() {
const { problem, panelOptions } = this.props;
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
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 severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority));
@@ -120,9 +124,9 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
</div>
{panelOptions.descriptionField && panelOptions.descriptionAtNewLine && (
<div className="alert-rule-item__text" >
<div className="alert-rule-item__text zbx-description--newline" >
<span
className="alert-rule-item__info zbx-description zbx-description--newline"
className="alert-rule-item__info zbx-description"
dangerouslySetInnerHTML={{ __html: problem.comments }}
/>
</div>
@@ -131,7 +135,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
</div>
</div>
{panelOptions.datasources.length > 1 && (
{showDatasourceName && (
<div className="alert-rule-item__time zabbix-trigger-source">
<span>
<i className="fa fa-database"></i>
@@ -258,14 +262,20 @@ class AlertAcknowledgesButton extends PureComponent<AlertAcknowledgesButtonProps
render() {
const { problem } = this.props;
return (
problem.acknowledges && problem.acknowledges.length ?
let content = null;
if (problem.acknowledges && problem.acknowledges.length) {
content = (
<Tooltip placement="bottom" popperClassName="ack-tooltip" content={this.renderTooltipContent}>
<span><i className="fa fa-comments"></i></span>
</Tooltip> :
</Tooltip>
);
} else if (problem.showAckButton) {
content = (
<Tooltip placement="bottom" content="Acknowledge problem">
<span role="button" onClick={this.handleClick}><i className="fa fa-comments-o"></i></span>
</Tooltip>
);
}
return content;
}
}

View File

@@ -13,12 +13,8 @@ export default function AlertIcon(props: AlertIconProps) {
const { problem, color, blink, highlightBackground } = props;
const priority = Number(problem.priority);
let iconClass = '';
if (problem.value === '1') {
if (priority >= 3) {
if (problem.value === '1' && priority >= 2) {
iconClass = 'icon-gf-critical';
} else {
iconClass = 'icon-gf-warning';
}
} else {
iconClass = 'icon-gf-online';
}

View File

@@ -12,7 +12,7 @@ export interface AlertListProps {
pageSize?: number;
fontSize?: number;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
interface AlertListState {
@@ -45,9 +45,9 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
}
handleTagClick = (tag: ZBXTag, datasource: string) => {
handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
this.props.onTagClick(tag, datasource);
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
}
}
@@ -68,7 +68,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
<ol className={alertListClass}>
{currentProblems.map(problem =>
<AlertCard
key={problem.triggerid}
key={`${problem.triggerid}-${problem.datasource}`}
problem={problem}
panelOptions={panelOptions}
onTagClick={this.handleTagClick}

View File

@@ -9,8 +9,7 @@ interface GFHeartIconProps {
export default function GFHeartIcon(props: GFHeartIconProps) {
const status = props.status;
const className = classNames("icon-gf", props.className,
{ "icon-gf-critical": status === 'critical' || status === 'problem' },
{ "icon-gf-warning": status === 'warning' },
{ "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'},
{ "icon-gf-online": status === 'online' || status === 'ok' },
);
return (

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import moment from 'moment';
import * as utils from '../../../datasource-zabbix/utils';
import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag } from '../../types';
import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag, ZBXAlert } from '../../types';
import { Modal, AckProblemData } from '../Modal';
import EventTag from '../EventTag';
import Tooltip from '../Tooltip/Tooltip';
@@ -15,12 +15,14 @@ interface ProblemDetailsProps extends RTRow<ZBXTrigger> {
timeRange: GFTimeRange;
showTimeline?: boolean;
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
interface ProblemDetailsState {
events: ZBXEvent[];
alerts: ZBXAlert[];
show: boolean;
showAckDialog: boolean;
}
@@ -30,6 +32,7 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
super(props);
this.state = {
events: [],
alerts: [],
show: false,
showAckDialog: false,
};
@@ -39,6 +42,7 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
if (this.props.showTimeline) {
this.fetchProblemEvents();
}
this.fetchProblemAlerts();
requestAnimationFrame(() => {
this.setState({ show: true });
});
@@ -58,6 +62,14 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
});
}
fetchProblemAlerts() {
const problem = this.props.original;
this.props.getProblemAlerts(problem)
.then(alerts => {
this.setState({ alerts });
});
}
ackProblem = (data: AckProblemData) => {
const problem = this.props.original as ZBXTrigger;
return this.props.onProblemAck(problem, data).then(result => {
@@ -78,6 +90,7 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
render() {
const problem = this.props.original as ZBXTrigger;
const alerts = this.state.alerts;
const rootWidth = this.props.rootWidth;
const displayClass = this.state.show ? 'show' : '';
const wideLayout = rootWidth > 1200;
@@ -96,13 +109,15 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
</div>
{problem.items && <ProblemItems items={problem.items} />}
</div>
<ProblemStatusBar problem={problem} className={compactStatusBar && 'compact'} />
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
{problem.showAckButton &&
<div className="problem-actions">
<ProblemActionButton className="navbar-button navbar-button--settings"
icon="reply-all"
tooltip="Acknowledge problem"
onClick={this.showAckDialog} />
</div>
}
</div>
{problem.comments &&
<div className="problem-description">

View File

@@ -1,15 +1,16 @@
import React from 'react';
import FAIcon from '../FAIcon';
import Tooltip from '../Tooltip/Tooltip';
import { ZBXTrigger } from '../../types';
import { ZBXTrigger, ZBXAlert } from '../../types';
export interface ProblemStatusBarProps {
problem: ZBXTrigger;
alerts?: ZBXAlert[];
className?: string;
}
export default function ProblemStatusBar(props: ProblemStatusBarProps) {
const { problem, className } = props;
const { problem, alerts, className } = props;
const multiEvent = problem.type === '1';
const link = problem.url && problem.url !== '';
const maintenance = problem.maintenance;
@@ -17,8 +18,8 @@ export default function ProblemStatusBar(props: ProblemStatusBarProps) {
const error = problem.error && problem.error !== '';
const stateUnknown = problem.state === '1';
const closeByTag = problem.correlation_mode === '1';
const actions = problem.alerts && problem.alerts.length !== 0;
const actionMessage = problem.alerts ? problem.alerts[0].message : '';
const actions = alerts && alerts.length !== 0;
const actionMessage = actions ? alerts[0].message : '';
return (
<div className={`problem-statusbar ${className || ''}`}>

View File

@@ -5,7 +5,7 @@ import _ from 'lodash';
import moment from 'moment';
import * as utils from '../../../datasource-zabbix/utils';
import { isNewProblem } from '../../utils';
import { ProblemsPanelOptions, ZBXTrigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized } from '../../types';
import { ProblemsPanelOptions, ZBXTrigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized, ZBXAlert } from '../../types';
import EventTag from '../EventTag';
import ProblemDetails from './ProblemDetails';
import { AckProblemData } from '../Modal';
@@ -18,7 +18,8 @@ export interface ProblemListProps {
timeRange?: GFTimeRange;
pageSize?: number;
fontSize?: number;
getProblemEvents: (ids: string[]) => ZBXEvent[];
getProblemEvents: (problem: ZBXTrigger) => ZBXEvent[];
getProblemAlerts: (problem: ZBXTrigger) => ZBXAlert[];
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
@@ -159,6 +160,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
timeRange={this.props.timeRange}
showTimeline={panelOptions.problemTimeline}
getProblemEvents={this.props.getProblemEvents}
getProblemAlerts={this.props.getProblemAlerts}
onProblemAck={this.handleProblemAck}
onTagClick={this.handleTagClick}
/>
@@ -258,13 +260,13 @@ function LastChangeCell(props: RTCell<ZBXTrigger>, customFormat?: string) {
}
interface TagCellProps extends RTCell<ZBXTrigger> {
onTagClick: (tag: ZBXTag, datasource: string) => void;
onTagClick: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
class TagCell extends PureComponent<TagCellProps> {
handleTagClick = (tag: ZBXTag) => {
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
this.props.onTagClick(tag, this.props.original.datasource);
this.props.onTagClick(tag, this.props.original.datasource, ctrlKey, shiftKey);
}
}

View File

@@ -1,8 +1,9 @@
import _ from 'lodash';
import { getNextRefIdChar } from './utils';
import { getDefaultTarget } from './triggers_panel_ctrl';
// Actual schema version
export const CURRENT_SCHEMA_VERSION = 6;
export const CURRENT_SCHEMA_VERSION = 7;
export function migratePanelSchema(panel) {
if (isEmptyPanel(panel)) {
@@ -45,6 +46,26 @@ export function migratePanelSchema(panel) {
}
}
if (schemaVersion < 7) {
const updatedTargets = [];
for (const targetKey in panel.targets) {
const target = panel.targets[targetKey];
if (!isEmptyTarget(target) && !isInvalidTarget(target, targetKey)) {
updatedTargets.push({
...target,
datasource: targetKey,
});
}
}
for (const target of updatedTargets) {
if (!target.refId) {
target.refId = getNextRefIdChar(updatedTargets);
}
}
panel.targets = updatedTargets;
delete panel.datasources;
}
return panel;
}
@@ -59,3 +80,11 @@ function isEmptyPanel(panel) {
function isEmptyTargets(targets) {
return !targets || (_.isArray(targets) && (targets.length === 0 || targets.length === 1 && _.isEmpty(targets[0])));
}
function isEmptyTarget(target) {
return !target || !(target.group && target.host && target.application && target.trigger);
}
function isInvalidTarget(target, targetKey) {
return target && target.refId === 'A' && targetKey === '0';
}

View File

@@ -6,7 +6,7 @@
</div>
<div class="gf-form">
<datasource-selector
datasources="ctrl.panel.datasources"
datasources="editor.selectedDatasources"
options="editor.panelCtrl.available_datasources"
on-change="editor.datasourcesChanged()">
</datasource-selector>
@@ -15,50 +15,50 @@
</div>
</div>
<div class="editor-row" ng-repeat="ds in ctrl.panel.datasources">
<div class="editor-row" ng-repeat="target in ctrl.panel.targets">
<div class="section gf-form-group">
<h5 class="section-heading">{{ ds }}</h5>
<h5 class="section-heading">{{ target.datasource }}</h5>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Group</label>
<input type="text"
ng-model="ctrl.panel.targets[ds].group.filter"
bs-typeahead="editor.getGroupNames[ds]"
ng-model="target.group.filter"
bs-typeahead="editor.getGroupNames[target.datasource]"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="gf-form-input width-14"
ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].group.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].group.filter)
'zbx-variable': editor.isVariable(target.group.filter),
'zbx-regex': editor.isRegex(target.group.filter)
}">
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Host</label>
<input type="text"
ng-model="ctrl.panel.targets[ds].host.filter"
bs-typeahead="editor.getHostNames[ds]"
ng-model="target.host.filter"
bs-typeahead="editor.getHostNames[target.datasource]"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="gf-form-input width-14"
ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].host.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].host.filter)
'zbx-variable': editor.isVariable(target.host.filter),
'zbx-regex': editor.isRegex(target.host.filter)
}">
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Proxy</label>
<input type="text"
ng-model="ctrl.panel.targets[ds].proxy.filter"
bs-typeahead="editor.getProxyNames[ds]"
ng-model="target.proxy.filter"
bs-typeahead="editor.getProxyNames[target.datasource]"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="gf-form-input width-14"
ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].proxy.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].proxy.filter)
'zbx-variable': editor.isVariable(target.proxy.filter),
'zbx-regex': editor.isRegex(target.proxy.filter)
}">
</div>
</div>
@@ -67,35 +67,35 @@
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Application</label>
<input type="text"
ng-model="ctrl.panel.targets[ds].application.filter"
bs-typeahead="editor.getApplicationNames[ds]"
ng-model="target.application.filter"
bs-typeahead="editor.getApplicationNames[target.datasource]"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="gf-form-input width-14"
ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].application.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].application.filter)
'zbx-variable': editor.isVariable(target.application.filter),
'zbx-regex': editor.isRegex(target.application.filter)
}">
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Trigger</label>
<input type="text"
ng-model="ctrl.panel.targets[ds].trigger.filter"
ng-model="target.trigger.filter"
ng-blur="editor.parseTarget()"
placeholder="trigger name"
class="gf-form-input width-14"
ng-style="ctrl.panel.targets[ds].trigger.style"
ng-style="target.trigger.style"
ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].trigger.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].trigger.filter)
'zbx-variable': editor.isVariable(target.trigger.filter),
'zbx-regex': editor.isRegex(target.trigger.filter)
}"
empty-to-null>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Tags</label>
<input type="text" class="gf-form-input width-14"
ng-model="ctrl.panel.targets[ds].tags.filter"
ng-model="target.tags.filter"
ng-blur="editor.parseTarget()"
placeholder="tag1:value1, tag2:value2">
</div>

View File

@@ -4,6 +4,7 @@
"id": "alexanderzobnin-zabbix-triggers-panel",
"dataFormats": [],
"skipDataQuery": true,
"info": {
"author": {

View File

@@ -5,16 +5,16 @@ import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_pane
import {CURRENT_SCHEMA_VERSION} from '../migrations';
describe('Triggers Panel schema migration', () => {
let ctx = {};
let ctx: any = {};
let updatePanelCtrl;
let datasourceSrvMock = {
const datasourceSrvMock = {
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
};
let timeoutMock = () => {};
const timeoutMock = () => {};
beforeEach(() => {
ctx = {
@@ -47,14 +47,16 @@ describe('Triggers Panel schema migration', () => {
});
it('should update old panel schema', () => {
let updatedPanelCtrl = updatePanelCtrl(ctx.scope);
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
let expected = _.defaultsDeep({
const expected = _.defaultsDeep({
schemaVersion: CURRENT_SCHEMA_VERSION,
datasources: ['zabbix'],
targets: {
'zabbix': DEFAULT_TARGET
},
targets: [
{
...DEFAULT_TARGET,
datasource: 'zabbix',
}
],
ageField: true,
statusField: false,
severityField: false,
@@ -68,29 +70,29 @@ describe('Triggers Panel schema migration', () => {
it('should create new panel with default schema', () => {
ctx.scope.panel = {};
let updatedPanelCtrl = updatePanelCtrl(ctx.scope);
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
let expected = _.defaultsDeep({
const expected = _.defaultsDeep({
schemaVersion: CURRENT_SCHEMA_VERSION,
datasources: ['zabbix_default'],
targets: {
'zabbix_default': DEFAULT_TARGET
}
targets: [{
...DEFAULT_TARGET,
datasource: 'zabbix_default'
}]
}, PANEL_DEFAULTS);
expect(updatedPanelCtrl.panel).toEqual(expected);
});
it('should set default targets for new panel with empty targets', () => {
ctx.scope.panel = {
targets: [{}]
targets: []
};
let updatedPanelCtrl = updatePanelCtrl(ctx.scope);
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
let expected = _.defaultsDeep({
datasources: ['zabbix_default'],
targets: {
'zabbix_default': DEFAULT_TARGET
},
const expected = _.defaultsDeep({
targets: [{
...DEFAULT_TARGET,
datasource: 'zabbix_default'
}]
}, PANEL_DEFAULTS);
expect(updatedPanelCtrl.panel).toEqual(expected);

View File

@@ -5,9 +5,9 @@ import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl';
// import { create } from 'domain';
describe('TriggerPanelCtrl', () => {
let ctx = {};
let ctx: any = {};
let datasourceSrvMock, zabbixDSMock;
let timeoutMock = () => {};
const timeoutMock = () => {};
let createPanelCtrl;
beforeEach(() => {
@@ -61,7 +61,7 @@ describe('TriggerPanelCtrl', () => {
describe('When adding new panel', () => {
it('should suggest all zabbix data sources', () => {
ctx.scope.panel = {};
let panelCtrl = createPanelCtrl();
const panelCtrl = createPanelCtrl();
expect(panelCtrl.available_datasources).toEqual([
'zabbix_default', 'zabbix'
]);
@@ -69,10 +69,8 @@ describe('TriggerPanelCtrl', () => {
it('should load first zabbix data source as default', () => {
ctx.scope.panel = {};
let panelCtrl = createPanelCtrl();
expect(panelCtrl.panel.datasources).toEqual([
'zabbix_default'
]);
const panelCtrl = createPanelCtrl();
expect(panelCtrl.panel.targets[0].datasource).toEqual('zabbix_default');
});
it('should rewrite default empty target', () => {
@@ -82,7 +80,7 @@ describe('TriggerPanelCtrl', () => {
"refId": "A"
}],
};
let panelCtrl = createPanelCtrl();
const panelCtrl = createPanelCtrl();
expect(panelCtrl.available_datasources).toEqual([
'zabbix_default', 'zabbix'
]);
@@ -92,16 +90,22 @@ describe('TriggerPanelCtrl', () => {
describe('When refreshing panel', () => {
beforeEach(() => {
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
ctx.scope.panel.targets = {
'zabbix_default': DEFAULT_TARGET,
'zabbix': DEFAULT_TARGET
};
ctx.scope.panel.targets = [
{
...DEFAULT_TARGET,
datasource: 'zabbix_default'
},
{
...DEFAULT_TARGET,
datasource: 'zabbix'
},
];
ctx.panelCtrl = createPanelCtrl();
});
it('should format triggers', (done) => {
ctx.panelCtrl.onRefresh().then(() => {
let formattedTrigger = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
expect(formattedTrigger.host).toBe('backend01');
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
expect(formattedTrigger.datasource).toBe('zabbix_default');
@@ -113,7 +117,7 @@ describe('TriggerPanelCtrl', () => {
it('should sort triggers by time by default', (done) => {
ctx.panelCtrl.onRefresh().then(() => {
let trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
expect(trigger_ids).toEqual([
'2', '4', '3', '1'
]);
@@ -124,7 +128,7 @@ describe('TriggerPanelCtrl', () => {
it('should sort triggers by severity', (done) => {
ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' };
ctx.panelCtrl.onRefresh().then(() => {
let trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
expect(trigger_ids).toEqual([
'1', '3', '2', '4'
]);
@@ -134,7 +138,7 @@ describe('TriggerPanelCtrl', () => {
it('should add acknowledges to trigger', (done) => {
ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx);
const trigger = getTriggerById(1, ctx);
expect(trigger.acknowledges).toHaveLength(1);
expect(trigger.acknowledges[0].message).toBe("event ack");
@@ -153,15 +157,15 @@ describe('TriggerPanelCtrl', () => {
it('should handle new lines in trigger description', () => {
ctx.panelCtrl.setTriggerSeverity = jest.fn((trigger) => trigger);
let trigger = {comments: "this is\ndescription"};
const trigger = {comments: "this is\ndescription"};
const formattedTrigger = ctx.panelCtrl.formatTrigger(trigger);
expect(formattedTrigger.comments).toBe("this is<br>description");
});
it('should format host name to display (default)', (done) => {
ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger);
const trigger = getTriggerById(1, ctx);
const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe('backend01');
done();
});
@@ -171,8 +175,8 @@ describe('TriggerPanelCtrl', () => {
ctx.panelCtrl.panel.hostField = false;
ctx.panelCtrl.panel.hostTechNameField = true;
ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger);
const trigger = getTriggerById(1, ctx);
const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe('backend01_tech');
done();
});
@@ -182,8 +186,8 @@ describe('TriggerPanelCtrl', () => {
ctx.panelCtrl.panel.hostField = true;
ctx.panelCtrl.panel.hostTechNameField = true;
ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger);
const trigger = getTriggerById(1, ctx);
const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe('backend01 (backend01_tech)');
done();
});
@@ -193,8 +197,8 @@ describe('TriggerPanelCtrl', () => {
ctx.panelCtrl.panel.hostField = false;
ctx.panelCtrl.panel.hostTechNameField = false;
ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger);
const trigger = getTriggerById(1, ctx);
const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe("");
done();
});
@@ -222,7 +226,7 @@ describe('TriggerPanelCtrl', () => {
});
});
const defaultTrigger = {
const defaultTrigger: any = {
"triggerid": "13565",
"value": "1",
"groups": [{"groupid": "1", "name": "Backend"}] ,
@@ -248,7 +252,7 @@ const defaultTrigger = {
"flags": "0", "type": "0", "items": [] , "error": ""
};
const defaultEvent = {
const defaultEvent: any = {
"eventid": "11",
"acknowledges": [
{
@@ -272,8 +276,8 @@ const defaultEvent = {
"objectid": "1",
};
function generateTrigger(id, timestamp, severity) {
let trigger = _.cloneDeep(defaultTrigger);
function generateTrigger(id, timestamp?, severity?): any {
const trigger = _.cloneDeep(defaultTrigger);
trigger.triggerid = id.toString();
if (severity) {
trigger.priority = severity.toString();
@@ -284,13 +288,13 @@ function generateTrigger(id, timestamp, severity) {
return trigger;
}
function createTrigger(props) {
function createTrigger(props): any {
let trigger = _.cloneDeep(defaultTrigger);
trigger = _.merge(trigger, props);
trigger.lastEvent.objectid = trigger.triggerid;
return trigger;
}
function getTriggerById(id, ctx) {
function getTriggerById(id, ctx): any {
return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()});
}

View File

@@ -10,6 +10,7 @@ import { triggerPanelTriggersTab } from './triggers_tab';
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
import ProblemList from './components/Problems/Problems';
import AlertList from './components/AlertList/AlertList';
import { getNextRefIdChar } from './utils';
const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
const PROBLEM_EVENTS_LIMIT = 100;
@@ -23,7 +24,17 @@ export const DEFAULT_TARGET = {
proxy: {filter: ""},
};
export const getDefaultTarget = () => DEFAULT_TARGET;
export const getDefaultTarget = (targets) => {
return {
group: {filter: ""},
host: {filter: ""},
application: {filter: ""},
trigger: {filter: ""},
tags: {filter: ""},
proxy: {filter: ""},
refId: getNextRefIdChar(targets),
};
};
export const DEFAULT_SEVERITY = [
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
@@ -40,8 +51,7 @@ const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
export const PANEL_DEFAULTS = {
schemaVersion: CURRENT_SCHEMA_VERSION,
datasources: [],
targets: {},
targets: [getDefaultTarget([])],
// Fields
hostField: true,
hostTechNameField: false,
@@ -108,11 +118,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
this.available_datasources = _.map(this.getZabbixDataSources(), 'name');
if (this.panel.datasources.length === 0) {
this.panel.datasources.push(this.available_datasources[0]);
}
if (this.isEmptyTargets()) {
this.panel.targets[this.panel.datasources[0]] = getDefaultTarget();
if (this.panel.targets && !this.panel.targets[0].datasource) {
this.panel.targets[0].datasource = this.available_datasources[0];
}
this.initDatasources();
@@ -138,7 +145,11 @@ export class TriggerPanelCtrl extends PanelCtrl {
}
initDatasources() {
let promises = _.map(this.panel.datasources, (ds) => {
if (!this.panel.targets) {
return;
}
const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource));
let promises = targetDatasources.map(ds => {
// Load datasource
return this.datasourceSrv.get(ds)
.then(datasource => {
@@ -234,16 +245,20 @@ export class TriggerPanelCtrl extends PanelCtrl {
getTriggers() {
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
let promises = _.map(this.panel.datasources, (ds) => {
let promises = _.map(this.panel.targets, (target) => {
const ds = target.datasource;
let proxies;
let showAckButton = true;
return this.datasourceSrv.get(ds)
.then(datasource => {
const zabbix = datasource.zabbix;
const showEvents = this.panel.showEvents.value;
const triggerFilter = this.panel.targets[ds];
const triggerFilter = target;
const showProxy = this.panel.hostProxy;
const getProxiesPromise = showProxy ? zabbix.getProxies() : () => [];
showAckButton = !datasource.disableReadOnlyUsersAck || userIsEditor;
// Replace template variables
const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter);
@@ -271,19 +286,18 @@ export class TriggerPanelCtrl extends PanelCtrl {
}));
return Promise.all([
this.datasources[ds].zabbix.getExtendedEventData(eventids),
this.datasources[ds].zabbix.getEventAlerts(eventids),
Promise.resolve(triggers)
]);
})
.then(([events, alerts, triggers]) => {
.then(([events, triggers]) => {
this.addEventTags(events, triggers);
this.addAcknowledges(events, triggers);
this.addEventAlerts(alerts, triggers);
return triggers;
})
.then(triggers => this.setMaintenanceStatus(triggers))
.then(triggers => this.filterTriggersPre(triggers, ds))
.then(triggers => this.addTriggerDataSource(triggers, ds))
.then(triggers => this.setAckButtonStatus(triggers, showAckButton))
.then(triggers => this.filterTriggersPre(triggers, target))
.then(triggers => this.addTriggerDataSource(triggers, target))
.then(triggers => this.addTriggerHostProxy(triggers, proxies));
});
@@ -337,28 +351,17 @@ export class TriggerPanelCtrl extends PanelCtrl {
return triggers;
}
addEventAlerts(alerts, triggers) {
alerts.forEach(alert => {
const trigger = _.find(triggers, t => {
return t.lastEvent && alert.eventid === t.lastEvent.eventid;
});
if (trigger) {
trigger.alerts = trigger.alerts ? trigger.alerts.concat(alert) : [alert];
}
});
return triggers;
}
filterTriggersPre(triggerList, ds) {
filterTriggersPre(triggerList, target) {
// Filter triggers by description
let triggerFilter = this.panel.targets[ds].trigger.filter;
const ds = target.datasource;
let triggerFilter = target.trigger.filter;
triggerFilter = this.datasources[ds].replaceTemplateVars(triggerFilter);
if (triggerFilter) {
triggerList = filterTriggers(triggerList, triggerFilter);
}
// Filter by tags
const target = this.panel.targets[ds];
// const target = this.panel.targets[ds];
if (target.tags.filter) {
let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter);
// replaceTemplateVars() builds regex-like string, so we should trim it.
@@ -409,9 +412,16 @@ export class TriggerPanelCtrl extends PanelCtrl {
return triggers;
}
addTriggerDataSource(triggers, ds) {
setAckButtonStatus(triggers, showAckButton) {
_.each(triggers, (trigger) => {
trigger.datasource = ds;
trigger.showAckButton = showAckButton;
});
return triggers;
}
addTriggerDataSource(triggers, target) {
_.each(triggers, (trigger) => {
trigger.datasource = target.datasource;
});
return triggers;
}
@@ -482,37 +492,51 @@ export class TriggerPanelCtrl extends PanelCtrl {
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
}
addTagFilter(tag, ds) {
let tagFilter = this.panel.targets[ds].tags.filter;
addTagFilter(tag, datasource) {
const target = this.panel.targets.find(t => t.datasource === datasource);
console.log(target);
let tagFilter = target.tags.filter;
let targetTags = this.parseTags(tagFilter);
let newTag = {tag: tag.tag, value: tag.value};
targetTags.push(newTag);
targetTags = _.uniqWith(targetTags, _.isEqual);
let newFilter = this.tagsToString(targetTags);
this.panel.targets[ds].tags.filter = newFilter;
target.tags.filter = newFilter;
this.refresh();
}
removeTagFilter(tag, ds) {
let tagFilter = this.panel.targets[ds].tags.filter;
removeTagFilter(tag, datasource) {
const target = this.panel.targets.find(t => t.datasource === datasource);
let tagFilter = target.tags.filter;
let targetTags = this.parseTags(tagFilter);
_.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value);
targetTags = _.uniqWith(targetTags, _.isEqual);
let newFilter = this.tagsToString(targetTags);
this.panel.targets[ds].tags.filter = newFilter;
target.tags.filter = newFilter;
this.refresh();
}
getProblemEvents(trigger) {
const triggerids = [trigger.triggerid];
getProblemEvents(problem) {
const triggerids = [problem.triggerid];
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
return this.datasourceSrv.get(trigger.datasource)
return this.datasourceSrv.get(problem.datasource)
.then(datasource => {
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
});
}
getProblemAlerts(problem) {
if (!problem.lastEvent || problem.lastEvent.length === 0) {
return Promise.resolve([]);
}
const eventids = [problem.lastEvent.eventid];
return this.datasourceSrv.get(problem.datasource)
.then(datasource => {
return datasource.zabbix.getEventAlerts(eventids);
});
}
formatHostName(trigger) {
let host = "";
if (this.panel.hostField && this.panel.hostTechNameField) {
@@ -549,12 +573,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
getAlertIconClass(trigger) {
let iconClass = '';
if (trigger.value === '1') {
if (trigger.priority >= 3) {
if (trigger.value === '1' && trigger.priority >= 2) {
iconClass = 'icon-gf-critical';
} else {
iconClass = 'icon-gf-warning';
}
} else {
iconClass = 'icon-gf-online';
}
@@ -566,8 +586,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
}
getAlertIconClassBySeverity(triggerSeverity) {
let iconClass = 'icon-gf-warning';
if (triggerSeverity.priority >= 3) {
let iconClass = 'icon-gf-online';
if (triggerSeverity.priority >= 2) {
iconClass = 'icon-gf-critical';
}
return iconClass;
@@ -663,6 +683,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
pageSize,
fontSize: fontSizeProp,
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
onProblemAck: (trigger, data) => {

View File

@@ -10,7 +10,7 @@ class TriggersTabCtrl {
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
this.templateSrv = templateSrv;
this.datasources = this.panelCtrl.datasources;
this.datasources = {};
// Load scope defaults
var scopeDefaults = {
@@ -21,6 +21,7 @@ class TriggersTabCtrl {
oldTarget: _.cloneDeep(this.panel.targets)
};
_.defaultsDeep(this, scopeDefaults);
this.selectedDatasources = this.getSelectedDatasources();
this.initDatasources();
this.panelCtrl.refresh();
@@ -30,6 +31,7 @@ class TriggersTabCtrl {
return this.panelCtrl.initDatasources()
.then((datasources) => {
_.each(datasources, (datasource) => {
this.datasources[datasource.name] = datasource;
this.bindSuggestionFunctions(datasource);
});
});
@@ -44,6 +46,10 @@ class TriggersTabCtrl {
this.getProxyNames[ds] = _.bind(this.suggestProxies, this, datasource);
}
getSelectedDatasources() {
return _.compact(this.panel.targets.map(target => target.datasource));
}
suggestGroups(datasource, query, callback) {
return datasource.zabbix.getAllGroups()
.then(groups => {
@@ -53,7 +59,8 @@ class TriggersTabCtrl {
}
suggestHosts(datasource, query, callback) {
let groupFilter = datasource.replaceTemplateVars(this.panel.targets[datasource.name].group.filter);
const target = this.panel.targets.find(t => t.datasource === datasource.name);
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
return datasource.zabbix.getAllHosts(groupFilter)
.then(hosts => {
return _.map(hosts, 'name');
@@ -62,8 +69,9 @@ class TriggersTabCtrl {
}
suggestApps(datasource, query, callback) {
let groupFilter = datasource.replaceTemplateVars(this.panel.targets[datasource.name].group.filter);
let hostFilter = datasource.replaceTemplateVars(this.panel.targets[datasource.name].host.filter);
const target = this.panel.targets.find(t => t.datasource === datasource.name);
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
let hostFilter = datasource.replaceTemplateVars(target.host.filter);
return datasource.zabbix.getAllApps(groupFilter, hostFilter)
.then(apps => {
return _.map(apps, 'name');
@@ -78,16 +86,17 @@ class TriggersTabCtrl {
}
datasourcesChanged() {
_.each(this.panel.datasources, (ds) => {
if (!this.panel.targets[ds]) {
this.panel.targets[ds] = getDefaultTarget();
}
});
// Remove unchecked targets
_.each(this.panel.targets, (target, ds) => {
if (!_.includes(this.panel.datasources, ds)) {
delete this.panel.targets[ds];
const newTargets = [];
_.each(this.selectedDatasources, (ds) => {
const dsTarget = this.panel.targets.find((target => target.datasource === ds));
if (dsTarget) {
newTargets.push(dsTarget);
} else {
const newTarget = getDefaultTarget(this.panel.targets);
newTarget.datasource = ds;
newTargets.push(newTarget);
}
this.panel.targets = newTargets;
});
this.parseTarget();
}

View File

@@ -1,7 +1,7 @@
export interface ProblemsPanelOptions {
schemaVersion: number;
datasources: any[];
targets: Map<string, ProblemsPanelTarget>;
targets: ProblemsPanelTarget[];
// Fields
hostField?: boolean;
hostTechNameField?: boolean;
@@ -62,6 +62,7 @@ export interface ProblemsPanelTarget {
proxy: {
filter: string
};
datasource: string;
}
export interface TriggerSeverity {
@@ -75,6 +76,7 @@ export type TriggerColor = string;
export interface ZBXTrigger {
acknowledges?: ZBXAcknowledge[];
showAckButton?: boolean;
alerts?: ZBXAlert[];
age?: string;
color?: TriggerColor;
@@ -162,6 +164,7 @@ export interface ZBXAcknowledge {
}
export interface ZBXAlert {
eventid: string;
clock: string;
message: string;
error: string;

View File

@@ -1,4 +1,6 @@
import _ from 'lodash';
import moment from 'moment';
import { DataQuery } from '@grafana/ui/';
import * as utils from '../datasource-zabbix/utils';
import { ZBXTrigger } from './types';
@@ -20,3 +22,13 @@ export function formatLastChange(lastchangeUnix: number, customFormat?: string)
const lastchange = timestamp.format(format);
return lastchange;
}
export const getNextRefIdChar = (queries: DataQuery[]): string => {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, refId => {
return _.every(queries, other => {
return other.refId !== refId;
});
});
};

View File

@@ -26,8 +26,8 @@
{"name": "Metric Editor", "path": "img/screenshot-metric_editor.png"},
{"name": "Triggers", "path": "img/screenshot-triggers.png"}
],
"version": "3.10.1",
"updated": "2019-03-05"
"version": "3.10.5",
"updated": "2019-12-26"
},
"includes": [
@@ -42,7 +42,7 @@
],
"dependencies": {
"grafanaVersion": "5.x",
"grafanaVersion": "6.x",
"plugins": []
}
}

View File

@@ -69,6 +69,8 @@
.rt-noData {
z-index: 2;
background: unset;
color: $text-muted;
}
.pagination-bottom {
@@ -522,7 +524,8 @@
@for $i from 11 through 25 {
.item-#{$i} { width: 2em * $i; }
&.font-size--#{$i * 10} .rt-table {
&.font-size--#{$i * 10} {
.rt-table {
font-size: 1% * $i * 10;
& .rt-tr .rt-td.custom-expander i {
@@ -533,6 +536,12 @@
font-size: 13px;
}
}
.rt-noData {
top: 4.5em;
font-size: 1% * $i * 10;
}
}
}
}

View File

@@ -24,6 +24,10 @@
&.zbx-description--newline {
max-height: unset;
.zbx-description {
margin-left: 0px;
}
}
.zbx-description {

View File

@@ -18,6 +18,12 @@ jest.mock('angular', () => {
};
}, {virtual: true});
jest.mock('grafana/app/core/core_module', () => {
return {
directive: function() {},
};
}, {virtual: true});
let mockPanelCtrl = PanelCtrl;
jest.mock('grafana/app/plugins/sdk', () => {
return {
@@ -36,6 +42,13 @@ jest.mock('grafana/app/core/utils/datemath', () => {
};
}, {virtual: true});
jest.mock('grafana/app/core/utils/kbn', () => {
return {
round_interval: n => n,
secondsToHms: n => n + 'ms'
};
}, {virtual: true});
jest.mock('grafana/app/core/table_model', () => {
return class TableModel {
constructor() {
@@ -62,6 +75,10 @@ jest.mock('grafana/app/core/config', () => {
jest.mock('jquery', () => 'module not found', {virtual: true});
jest.mock('@grafana/ui', () => {
return {};
}, {virtual: true});
// Required for loading angularjs
let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
// Setup jsdom

View File

@@ -64,7 +64,6 @@
],
"variable-name": [
true,
"check-format",
"ban-keywords",
"allow-leading-underscore",
"allow-trailing-underscore",

View File

@@ -28,7 +28,7 @@ module.exports = {
externals: [
// remove the line below if you don't want to use builtin versions
'jquery', 'lodash', 'moment', 'angular',
'react', 'react-dom',
'react', 'react-dom', '@grafana/ui', '@grafana/data',
function (context, request, callback) {
var prefix = 'grafana/';
if (request.indexOf(prefix) === 0) {
@@ -53,7 +53,7 @@ module.exports = {
ExtractTextPluginDark,
],
resolve: {
extensions: [".js", ".ts", ".tsx", ".html", ".scss"]
extensions: ['.js', '.es6', '.ts', '.tsx', '.html', '.scss']
},
module: {
rules: [
@@ -63,7 +63,7 @@ module.exports = {
use: {
loader: 'babel-loader',
query: {
presets: ['babel-preset-env']
presets: ['@babel/preset-env']
}
}
},

5949
yarn.lock

File diff suppressed because it is too large Load Diff