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: branches:
ignore: ignore:
- master - master
- /^release-[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- docs - docs
- gh-pages - gh-pages
- &filter-only-master - &filter-only-master
branches: branches:
only: master only: master
- &filter-only-release
branches:
ignore: /.*/
tags:
only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
- &filter-docs - &filter-docs
branches: branches:
only: docs only: docs
@@ -20,7 +26,7 @@ jobs:
build: build:
working_directory: ~/alexanderzobnin/grafana-zabbix working_directory: ~/alexanderzobnin/grafana-zabbix
docker: docker:
- image: circleci/node:8 - image: circleci/node:12
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -39,7 +45,7 @@ jobs:
lint: lint:
working_directory: ~/alexanderzobnin/grafana-zabbix working_directory: ~/alexanderzobnin/grafana-zabbix
docker: docker:
- image: circleci/node:8 - image: circleci/node:12
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -61,7 +67,7 @@ jobs:
CIRCLE_ARTIFACTS: /tmp/circleci-artifacts CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
docker: docker:
- image: circleci/node:8 - image: circleci/node:12
steps: steps:
- checkout - checkout
# Prepare for artifact and test results collection equivalent to how it was done on 1.0. # Prepare for artifact and test results collection equivalent to how it was done on 1.0.
@@ -87,6 +93,31 @@ jobs:
- store_artifacts: - store_artifacts:
path: /tmp/circleci-test-results 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: build-docs:
working_directory: ~/grafana-zabbix working_directory: ~/grafana-zabbix
docker: docker:
@@ -110,7 +141,7 @@ jobs:
deploy-docs: deploy-docs:
working_directory: ~/grafana-zabbix working_directory: ~/grafana-zabbix
docker: docker:
- image: circleci/node:8 - image: circleci/node:12
environment: environment:
GH_PAGES_BRANCH: gh-pages GH_PAGES_BRANCH: gh-pages
CI_GIT_USER: CircleCI CI_GIT_USER: CircleCI
@@ -124,12 +155,23 @@ jobs:
at: ../gh-pages at: ../gh-pages
- run: ./.circleci/deploy-docs.sh - 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: workflows:
version: 2 version: 2
build-master: build-master:
jobs: jobs:
- build: - build:
filters: *filter-only-master filters: *filter-only-master
- codespell:
filters: *filter-only-master
- lint: - lint:
filters: *filter-only-master filters: *filter-only-master
- test: - test:
@@ -139,11 +181,31 @@ workflows:
jobs: jobs:
- build: - build:
filters: *filter-not-release-or-master filters: *filter-not-release-or-master
- codespell:
filters: *filter-not-release-or-master
- lint: - lint:
filters: *filter-not-release-or-master filters: *filter-not-release-or-master
- test: - test:
filters: *filter-not-release-or-master 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: build-docs:
jobs: jobs:
- build-docs: - build-docs:

View File

@@ -10,15 +10,15 @@ set -o pipefail
echo "current dir: $(pwd)" echo "current dir: $(pwd)"
# Setup git env # Setup git env
git config --global user.email $CI_GIT_EMAIL git config --global user.email "$CI_GIT_EMAIL"
git config --global user.name $CI_GIT_USER git config --global user.name "$CI_GIT_USER"
echo "git user is $CI_GIT_USER ($CI_GIT_EMAIL)" echo "git user is $CI_GIT_USER ($CI_GIT_EMAIL)"
git checkout -b $GH_PAGES_BRANCH git checkout -b "$GH_PAGES_BRANCH"
rm -rf * || true rm -rf ./* || true
mv ../gh-pages/docs/site/* ./ mv ../gh-pages/docs/site/* ./
git add --force . git add --force .
git commit -m "build docs from commit ${CIRCLE_SHA1:0:7} (branch $CIRCLE_BRANCH)" git commit -m "build docs from commit ${CIRCLE_SHA1:0:7} (branch $CIRCLE_BRANCH)"
git log -n 3 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/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [3.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 ## [3.10.1] - 2019-03-05
### Fixed ### Fixed
- Problems panel: unable to edit panel in Grafana 6.0, [#685](https://github.com/alexanderzobnin/grafana-zabbix/issues/685) - 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 - setAliasByRegex() function
### Changed ### 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) - **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/ - 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 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 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. 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) ![Backend system time](../img/getstarting-regex_backend_system_time.png)
## Bar Chart ## 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) ![MySQL operations 1](../img/getstarting-mysql_operations_1.png)

View File

@@ -14,7 +14,7 @@ consists of two main parts:
- **Alerting execution engine** - **Alerting execution engine**
The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part of core 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** - **Alerting visualisations**
Alerts highlight panels with problems and it can easily be found on the dashboard. Alerts highlight panels with problems and it can easily be found on the dashboard.

View File

@@ -1,6 +1,23 @@
Functions reference 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 ## Transform
@@ -10,7 +27,7 @@ Functions reference
groupBy(interval, function) 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: Examples:
``` ```
@@ -124,7 +141,7 @@ Replaces `null` values with N
aggregateBy(interval, function) 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: 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_
``` ```
average(interval) average(interval)

View File

@@ -1,8 +1,9 @@
{ {
"name": "grafana-zabbix", "name": "grafana-zabbix",
"private": false, "private": false,
"version": "3.10.1", "version": "3.10.5",
"description": "Zabbix plugin for Grafana", "description": "Zabbix plugin for Grafana",
"homepage": "http://grafana-zabbix.org",
"scripts": { "scripts": {
"build": "webpack --config webpack/webpack.prod.conf.js --progress --colors", "build": "webpack --config webpack/webpack.prod.conf.js --progress --colors",
"dev": "webpack --config webpack/webpack.dev.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" "url": "https://github.com/alexanderzobnin/grafana-zabbix/issues"
}, },
"devDependencies": { "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/classnames": "^2.2.6",
"@types/grafana": "github:CorpGlory/types-grafana", "@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "^23.1.1", "@types/jest": "^23.1.1",
@@ -33,35 +39,32 @@
"@types/react": "^16.4.6", "@types/react": "^16.4.6",
"@types/react-dom": "^16.0.11", "@types/react-dom": "^16.0.11",
"@types/react-transition-group": "^2.0.15", "@types/react-transition-group": "^2.0.15",
"babel-core": "^6.26.3", "babel-jest": "^24.9.0",
"babel-jest": "^23.6.0", "babel-loader": "^8.0.6",
"babel-loader": "^7.1.2",
"babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "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", "benchmark": "^2.1.4",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^0.1.19",
"codecov": "^3.1.0", "codecov": "^3.1.0",
"copy-webpack-plugin": "^4.5.4", "copy-webpack-plugin": "^5.1.1",
"css-loader": "^1.0.0", "css-loader": "2.1.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0", "extract-text-webpack-plugin": "^4.0.0-beta.0",
"grunt": "^1.0.3", "grunt": "^1.0.3",
"grunt-benchmark": "^1.0.0", "grunt-benchmark": "^1.0.0",
"grunt-cli": "^1.3.1", "grunt-cli": "^1.3.1",
"grunt-execute": "^0.2.2", "grunt-execute": "^0.2.2",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"jest": "^23.6.0", "jest": "^24.9.0",
"jscs": "^3.0.7", "jscs": "^3.0.7",
"jsdom": "~11.3.0", "jsdom": "~11.3.0",
"jshint": "^2.9.6", "jshint": "^2.9.6",
"jshint-stylish": "^2.1.0", "jshint-stylish": "^2.1.0",
"load-grunt-tasks": "~3.2.0", "load-grunt-tasks": "~3.2.0",
"lodash": "~4.17.5", "lodash": "~4.17.13",
"moment": "~2.21.0", "moment": "~2.21.0",
"ng-annotate-webpack-plugin": "^0.3.0", "ng-annotate-webpack-plugin": "^0.3.0",
"node-sass": "^4.9.4", "node-sass": "^4.13.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.7.0", "react": "^16.7.0",
"react-dom": "^16.7.0", "react-dom": "^16.7.0",
@@ -69,15 +72,23 @@
"react-table": "^6.8.6", "react-table": "^6.8.6",
"react-test-renderer": "^16.7.0", "react-test-renderer": "^16.7.0",
"react-transition-group": "^2.5.2", "react-transition-group": "^2.5.2",
"sass-loader": "^7.1.0", "rst2html": "github:thoward/rst2html#990cb89",
"sass-loader": "7.1.0",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"tether-drop": "^1.4.2", "tether-drop": "^1.4.2",
"ts-jest": "^23.10.5", "ts-jest": "^24.2.0",
"ts-loader": "^4.4.1", "ts-loader": "^4.4.1",
"tslint": "^5.11.0", "tslint": "5.20.1",
"typescript": "^2.9.2", "typescript": "3.7.2",
"webpack": "^4.22.0", "webpack": "4.29.6",
"webpack-cli": "^3.1.2" "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.$apply(function() {
$scope.addFunction(funcDef); $scope.ctrl.addFunction(funcDef);
}); });
$input.trigger('blur'); $input.trigger('blur');
@@ -66,7 +66,7 @@ angular
}); });
$input.blur(function() { $input.blur(function() {
// clicking the function dropdown menu wont // clicking the function dropdown menu won't
// work if you remove class at once // work if you remove class at once
setTimeout(function() { setTimeout(function() {
$input.val(''); $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); this.current.jsonData = migrateDSConfig(this.current.jsonData);
_.defaults(this.current.jsonData, defaultConfig); _.defaults(this.current.jsonData, defaultConfig);
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
this.dbDataSources = this.getSupportedDBDataSources(); this.dbDataSources = this.getSupportedDBDataSources();
this.zabbixVersions = _.cloneDeep(zabbixVersions); this.zabbixVersions = _.cloneDeep(zabbixVersions);
this.autoDetectZabbixVersion(); this.autoDetectZabbixVersion();
if (!this.dbConnectionDatasourceId) {
this.loadCurrentDBDatasource();
}
} }
getSupportedDBDataSources() { getSupportedDBDataSources() {
@@ -41,11 +46,21 @@ export class ZabbixDSConfigController {
} }
getCurrentDatasourceType() { getCurrentDatasourceType() {
const dsId = this.current.jsonData.dbConnectionDatasourceId; const dsId = this.dbConnectionDatasourceId;
const currentDs = _.find(this.dbDataSources, { 'id': dsId }); const currentDs = _.find(this.dbDataSources, { 'id': dsId });
return currentDs ? currentDs.type : null; 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() { autoDetectZabbixVersion() {
if (!this.current.id) { if (!this.current.id) {
return; 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: 4, text: 'High'},
{val: 5, text: 'Disaster'} {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 _ from 'lodash';
import * as utils from './utils'; import * as utils from './utils';
import ts from './timeseries'; import ts, { groupBy_perf as groupBy } from './timeseries';
let downsampleSeries = ts.downsample; let downsampleSeries = ts.downsample;
let groupBy = ts.groupBy_perf;
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc); let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
let sumSeries = ts.sumSeries; let sumSeries = ts.sumSeries;
let delta = ts.delta; let delta = ts.delta;
@@ -19,7 +18,7 @@ let AVERAGE = ts.AVERAGE;
let MIN = ts.MIN; let MIN = ts.MIN;
let MAX = ts.MAX; let MAX = ts.MAX;
let MEDIAN = ts.MEDIAN; let MEDIAN = ts.MEDIAN;
let PERCENTIL = ts.PERCENTIL; let PERCENTILE = ts.PERCENTILE;
function limit(order, n, orderByFunc, timeseries) { function limit(order, n, orderByFunc, timeseries) {
let orderByCallback = aggregationFunctions[orderByFunc]; let orderByCallback = aggregationFunctions[orderByFunc];
@@ -107,7 +106,7 @@ function groupByWrapper(interval, groupFunc, datapoints) {
function aggregateByWrapper(interval, aggregateFunc, datapoints) { function aggregateByWrapper(interval, aggregateFunc, datapoints) {
// Flatten all points in frame and then just use groupBy() // 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 // groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints); const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = aggregationFunctions[aggregateFunc]; let groupByCallback = aggregationFunctions[aggregateFunc];
@@ -115,15 +114,15 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) {
} }
function aggregateWrapper(groupByCallback, interval, datapoints) { function aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = _.flatten(datapoints, true); var flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only // groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints); const sortedPoints = ts.sortByTime(flattenedPoints);
return groupBy(sortedPoints, interval, groupByCallback); return groupBy(sortedPoints, interval, groupByCallback);
} }
function percentil(interval, n, datapoints) { function percentile(interval, n, datapoints) {
var flattenedPoints = _.flatten(datapoints, true); var flattenedPoints = ts.flattenDatapoints(datapoints);
var groupByCallback = _.partial(PERCENTIL, n); var groupByCallback = _.partial(PERCENTILE, n);
return groupBy(flattenedPoints, interval, groupByCallback); return groupBy(flattenedPoints, interval, groupByCallback);
} }
@@ -155,7 +154,7 @@ let metricFunctions = {
transformNull: transformNull, transformNull: transformNull,
aggregateBy: aggregateByWrapper, aggregateBy: aggregateByWrapper,
// Predefined aggs // Predefined aggs
percentil: percentil, percentile: percentile,
average: _.partial(aggregateWrapper, AVERAGE), average: _.partial(aggregateWrapper, AVERAGE),
min: _.partial(aggregateWrapper, MIN), min: _.partial(aggregateWrapper, MIN),
max: _.partial(aggregateWrapper, MAX), max: _.partial(aggregateWrapper, MAX),

View File

@@ -111,6 +111,9 @@ export class ZabbixDatasource {
let timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000); let timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000);
let timeTo = Math.ceil(dateMath.parse(options.range.to) / 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 // Prevent changes of original object
let target = _.cloneDeep(t); let target = _.cloneDeep(t);
@@ -323,14 +326,14 @@ export class ZabbixDatasource {
return this.zabbix.getITServices(itServiceFilter) return this.zabbix.getITServices(itServiceFilter)
.then(itservices => { .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) { queryTriggersData(target, timeRange) {
let [timeFrom, timeTo] = timeRange; let [timeFrom, timeTo] = timeRange;
return this.zabbix.getHostsFromTarget(target) return this.zabbix.getHostsFromTarget(target)
.then((results) => { .then(results => {
let [hosts, apps] = results; let [hosts, apps] = results;
if (hosts.length) { if (hosts.length) {
let hostids = _.map(hosts, 'hostid'); let hostids = _.map(hosts, 'hostid');
@@ -342,9 +345,13 @@ export class ZabbixDatasource {
timeFrom: timeFrom, timeFrom: timeFrom,
timeTo: timeTo timeTo: timeTo
}; };
return this.zabbix.getHostAlerts(hostids, appids, options) const groupFilter = target.group.filter;
.then((triggers) => { return Promise.all([
return responseHandler.handleTriggersResponse(triggers, timeRange); this.zabbix.getHostAlerts(hostids, appids, options),
this.zabbix.getGroups(groupFilter)
])
.then(([triggers, groups]) => {
return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
}); });
} else { } else {
return Promise.resolve([]); 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({ addFuncDef({
name: 'percentil', name: 'percentile',
category: 'Aggregate', category: 'Aggregate',
params: [ params: [
{ name: 'interval', type: 'string' }, { name: 'interval', type: 'string' },

View File

@@ -33,6 +33,7 @@ export function migrate(target) {
if (isGrafana2target(target)) { if (isGrafana2target(target)) {
return migrateFrom2To3version(target); return migrateFrom2To3version(target);
} }
migratePercentileAgg(target);
return 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 const DS_CONFIG_SCHEMA = 2;
export function migrateDSConfig(jsonData) { export function migrateDSConfig(jsonData) {
if (!jsonData) { if (!jsonData) {

View File

@@ -1,5 +1,4 @@
<div class="gf-form-group"> <div class="gf-form-group">
<h6>Filter Triggers</h6>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-10">Group</span> <span class="gf-form-label width-10">Group</span>
@@ -36,8 +35,9 @@
</div> </div>
</div> </div>
<div class="gf-form-group"> <div class="gf-form-group">
<h6>Options</h6>
<div class="gf-form"> <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"> <div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" <select class="gf-form-input gf-size-auto"
ng-init='ctrl.annotation.minseverity = ctrl.annotation.minseverity || 0' ng-init='ctrl.annotation.minseverity = ctrl.annotation.minseverity || 0'
@@ -54,12 +54,16 @@
</select> </select>
</div> </div>
</div> </div>
</div> <gf-form-switch class="gf-form" label-class="width-12"
<div class="gf-form-group"> label="Show OK events"
<h6>Options</h6> checked="ctrl.annotation.showOkEvents">
<div class="gf-form"> </gf-form-switch>
<editor-checkbox text="Show OK events" model="ctrl.annotation.showOkEvents"></editor-checkbox> <gf-form-switch class="gf-form" label-class="width-12"
<editor-checkbox text="Hide acknowledged events" model="ctrl.annotation.hideAcknowledged"></editor-checkbox> label="Hide acknowledged events"
<editor-checkbox text="Show hostname" model="ctrl.annotation.showHostname"></editor-checkbox> checked="ctrl.annotation.hideAcknowledged">
</div> </gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-12"
label="Show hostname"
checked="ctrl.annotation.showHostname">
</gf-form-switch>
</div> </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 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. 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 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> </info-popover>
</span> </span>
<div class="gf-form-select-wrapper max-width-16"> <div class="gf-form-select-wrapper max-width-16">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnectionDatasourceId" <select class="gf-form-input"
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources"> ng-model="ctrl.dbConnectionDatasourceId"
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources"
ng-change="ctrl.onDBConnectionDatasourceChange()">
</select> </select>
</div> </div>
</div> </div>

View File

@@ -201,10 +201,12 @@
</div> </div>
<!-- Metric processing functions --> <!-- 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"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Functions</label> <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>
<div class="gf-form dropdown" add-metric-function> <div class="gf-form dropdown" add-metric-function>
</div> </div>

View File

@@ -275,11 +275,15 @@ export class ZabbixQueryController extends QueryCtrl {
this.targetChanged(); this.targetChanged();
} }
moveFunction(func, offset) {
const index = this.target.functions.indexOf(func);
_.move(this.target.functions, index, index + offset);
this.targetChanged();
}
moveAliasFuncLast() { moveAliasFuncLast() {
var aliasFunc = _.find(this.target.functions, function(func) { var aliasFunc = _.find(this.target.functions, func => {
return func.def.name === 'alias' || return func.def.category === 'Alias';
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
}); });
if (aliasFunc) { 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) { function handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla[0]; var targetSLA = slaObject[itservice.serviceid].sla;
if (slaProperty.property === 'status') { if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status); var targetStatus = parseInt(slaObject[itservice.serviceid].status);
return { return {
target: itservice.name + ' ' + slaProperty.name, target: itservice.name + ' ' + slaProperty.name,
datapoints: [ datapoints: [
[targetStatus, targetSLA.to * 1000] [targetStatus, targetSLA[0].to * 1000]
] ]
}; };
} else { } 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 { return {
target: itservice.name + ' ' + slaProperty.name, target: itservice.name + ' ' + slaProperty.name,
datapoints: [ datapoints: slaArr
[targetSLA[slaProperty.property], targetSLA.from * 1000],
[targetSLA[slaProperty.property], targetSLA.to * 1000]
]
}; };
} }
} }
function handleTriggersResponse(triggers, timeRange) { function handleTriggersResponse(triggers, groups, timeRange) {
if (_.isNumber(triggers)) { if (!_.isArray(triggers)) {
let triggersCount = null;
try {
triggersCount = Number(triggers);
} catch (err) {
console.log("Error when handling triggers count: ", err);
}
return { return {
target: "triggers count", target: "triggers count",
datapoints: [ datapoints: [
[triggers, timeRange[1] * 1000] [triggersCount, timeRange[1] * 1000]
] ]
}; };
} else { } else {
let stats = getTriggerStats(triggers); const stats = getTriggerStats(triggers);
const groupNames = _.map(groups, 'name');
let table = new TableModel(); let table = new TableModel();
table.addColumn({text: 'Host group'}); table.addColumn({text: 'Host group'});
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => { _.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
table.addColumn({text: severity.text}); table.addColumn({text: severity.text});
}); });
_.each(stats, (severity_stats, group) => { _.each(stats, (severity_stats, group) => {
if (_.includes(groupNames, group)) {
let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]); let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]);
row = _.concat([group], ...row); row = _.concat([group], ...row);
table.rows.push(row); table.rows.push(row);
}
}); });
return table; return table;
} }

View File

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

View File

@@ -138,4 +138,31 @@ describe('Utils', () => {
done(); 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 _ from 'lodash';
import * as utils from './utils'; import * as utils from './utils';
import * as c from './constants';
const POINT_VALUE = 0; const POINT_VALUE = 0;
const POINT_TIMESTAMP = 1; 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) { if (datapoints.length === 0) {
return []; return [];
} }
if (interval === c.RANGE_VARIABLE_VALUE) {
return groupByRange(datapoints, groupByCallback);
}
let ms_interval = utils.parseInterval(interval); let ms_interval = utils.parseInterval(interval);
let grouped_series = []; let grouped_series = [];
let frame_values = []; let frame_values = [];
@@ -132,6 +137,19 @@ function groupBy_perf(datapoints, interval, groupByCallback) {
return grouped_series; 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. * Summarize set of time series into one.
* @param {datapoints[]} timeseries array of time series * @param {datapoints[]} timeseries array of time series
@@ -333,7 +351,7 @@ function expMovingAverage(datapoints, n) {
return ema; return ema;
} }
function PERCENTIL(n, values) { function PERCENTILE(n, values) {
var sorted = _.sortBy(values); var sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length * n / 100)]; return sorted[Math.floor(sorted.length * n / 100)];
} }
@@ -478,6 +496,15 @@ function findNearestLeft(series, pointIndex) {
return null; 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 // // Export //
//////////// ////////////
@@ -486,6 +513,7 @@ const exportedFunctions = {
downsample, downsample,
groupBy, groupBy,
groupBy_perf, groupBy_perf,
groupByRange,
sumSeries, sumSeries,
scale, scale,
offset, offset,
@@ -500,8 +528,9 @@ const exportedFunctions = {
MIN, MIN,
MAX, MAX,
MEDIAN, MEDIAN,
PERCENTIL, PERCENTILE,
sortByTime sortByTime,
flattenDatapoints,
}; };
export default exportedFunctions; export default exportedFunctions;

View File

@@ -1,5 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import kbn from 'grafana/app/core/utils/kbn';
import * as c from './constants';
/** /**
* Expand Zabbix item name * 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) { export function buildRegex(str) {
var matches = str.match(regexPattern); var matches = str.match(regexPattern);
var pattern = matches[1]; var pattern = matches[1];
@@ -265,6 +279,17 @@ export function compactQuery(query) {
return query.replace(/\s+/g, ' ').trim(); 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 // Fix for backward compatibility with lodash 2.4
if (!_.includes) { if (!_.includes) {
_.includes = _.contains; _.includes = _.contains;

View File

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

View File

@@ -1,7 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils'; import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore'; import { ZabbixAPICore } from './zabbixAPICore';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE } from '../../../constants'; import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
/** /**
* Zabbix API Wrapper. * Zabbix API Wrapper.
@@ -316,14 +317,11 @@ export class ZabbixAPIConnector {
return this.request('service.get', params); return this.request('service.get', params);
} }
getSLA(serviceids, timeRange) { getSLA(serviceids, timeRange, options) {
let [timeFrom, timeTo] = timeRange; const intervals = buildSLAIntervals(timeRange, options.intervalMs);
var params = { const params = {
serviceids: serviceids, serviceids,
intervals: [{ intervals
from: timeFrom,
to: timeTo
}]
}; };
return this.request('service.getsla', params); return this.request('service.getsla', params);
} }
@@ -421,7 +419,12 @@ export class ZabbixAPIConnector {
getEventAlerts(eventids) { getEventAlerts(eventids) {
const params = { const params = {
eventids: eventids, eventids: eventids,
output: 'extend', output: [
'eventid',
'message',
'clock',
'error'
],
selectUsers: true, selectUsers: true,
}; };
@@ -520,3 +523,30 @@ function isNotAuthorized(message) {
message === "Not authorized." 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}); itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
} }
let itServiceIds = _.map(itServices, 'serviceid'); let itServiceIds = _.map(itServices, 'serviceid');
return this.zabbixAPI.getSLA(itServiceIds, timeRange) return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
.then(slaResponse => { .then(slaResponse => {
return _.map(itServiceIds, serviceid => { return _.map(itServiceIds, serviceid => {
let itservice = _.find(itServices, {'serviceid': serviceid}); let itservice = _.find(itServices, {'serviceid': serviceid});

View File

@@ -10,7 +10,7 @@ class ZabbixAlertingService {
} }
isFullScreen() { isFullScreen() {
return this.dashboardSrv.dash.meta.fullscreen; return this.getDashboardModel().meta.fullscreen;
} }
setPanelAlertState(panelId, alertState) { 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"; let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
$(panelContainers[panelIndex]).removeClass(alertClass); $(panelContainers[panelIndex]).removeClass(alertClass);
if (alertState) { if (alertState) {
if (alertState === 'alerting') {
alertClass = "panel-has-alert panel-alert-state--" + alertState; alertClass = "panel-has-alert panel-alert-state--" + alertState;
$(panelContainers[panelIndex]).addClass(alertClass); $(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() { getPanelModels() {
return _.filter(this.dashboardSrv.dash.panels, panel => panel.type !== 'row'); return _.filter(this.getDashboardModel().panels, panel => panel.type !== 'row');
} }
getPanelModel(panelId) { getPanelModel(panelId) {

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ export interface AlertListProps {
pageSize?: number; pageSize?: number;
fontSize?: number; fontSize?: number;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
} }
interface AlertListState { 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) { 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}> <ol className={alertListClass}>
{currentProblems.map(problem => {currentProblems.map(problem =>
<AlertCard <AlertCard
key={problem.triggerid} key={`${problem.triggerid}-${problem.datasource}`}
problem={problem} problem={problem}
panelOptions={panelOptions} panelOptions={panelOptions}
onTagClick={this.handleTagClick} onTagClick={this.handleTagClick}

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as utils from '../../../datasource-zabbix/utils'; import * as utils from '../../../datasource-zabbix/utils';
import { isNewProblem } from '../../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 EventTag from '../EventTag';
import ProblemDetails from './ProblemDetails'; import ProblemDetails from './ProblemDetails';
import { AckProblemData } from '../Modal'; import { AckProblemData } from '../Modal';
@@ -18,7 +18,8 @@ export interface ProblemListProps {
timeRange?: GFTimeRange; timeRange?: GFTimeRange;
pageSize?: number; pageSize?: number;
fontSize?: number; fontSize?: number;
getProblemEvents: (ids: string[]) => ZBXEvent[]; getProblemEvents: (problem: ZBXTrigger) => ZBXEvent[];
getProblemAlerts: (problem: ZBXTrigger) => ZBXAlert[];
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onPageSizeChange?: (pageSize: number, pageIndex: number) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
@@ -159,6 +160,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
timeRange={this.props.timeRange} timeRange={this.props.timeRange}
showTimeline={panelOptions.problemTimeline} showTimeline={panelOptions.problemTimeline}
getProblemEvents={this.props.getProblemEvents} getProblemEvents={this.props.getProblemEvents}
getProblemAlerts={this.props.getProblemAlerts}
onProblemAck={this.handleProblemAck} onProblemAck={this.handleProblemAck}
onTagClick={this.handleTagClick} onTagClick={this.handleTagClick}
/> />
@@ -258,13 +260,13 @@ function LastChangeCell(props: RTCell<ZBXTrigger>, customFormat?: string) {
} }
interface TagCellProps extends RTCell<ZBXTrigger> { 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> { class TagCell extends PureComponent<TagCellProps> {
handleTagClick = (tag: ZBXTag) => { handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) { 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 _ from 'lodash';
import { getNextRefIdChar } from './utils';
import { getDefaultTarget } from './triggers_panel_ctrl'; import { getDefaultTarget } from './triggers_panel_ctrl';
// Actual schema version // Actual schema version
export const CURRENT_SCHEMA_VERSION = 6; export const CURRENT_SCHEMA_VERSION = 7;
export function migratePanelSchema(panel) { export function migratePanelSchema(panel) {
if (isEmptyPanel(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; return panel;
} }
@@ -59,3 +80,11 @@ function isEmptyPanel(panel) {
function isEmptyTargets(targets) { function isEmptyTargets(targets) {
return !targets || (_.isArray(targets) && (targets.length === 0 || targets.length === 1 && _.isEmpty(targets[0]))); 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>
<div class="gf-form"> <div class="gf-form">
<datasource-selector <datasource-selector
datasources="ctrl.panel.datasources" datasources="editor.selectedDatasources"
options="editor.panelCtrl.available_datasources" options="editor.panelCtrl.available_datasources"
on-change="editor.datasourcesChanged()"> on-change="editor.datasourcesChanged()">
</datasource-selector> </datasource-selector>
@@ -15,50 +15,50 @@
</div> </div>
</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"> <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-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Group</label> <label class="gf-form-label query-keyword width-7">Group</label>
<input type="text" <input type="text"
ng-model="ctrl.panel.targets[ds].group.filter" ng-model="target.group.filter"
bs-typeahead="editor.getGroupNames[ds]" bs-typeahead="editor.getGroupNames[target.datasource]"
ng-blur="editor.parseTarget()" ng-blur="editor.parseTarget()"
data-min-length=0 data-min-length=0
data-items=100 data-items=100
class="gf-form-input width-14" class="gf-form-input width-14"
ng-class="{ ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].group.filter), 'zbx-variable': editor.isVariable(target.group.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].group.filter) 'zbx-regex': editor.isRegex(target.group.filter)
}"> }">
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Host</label> <label class="gf-form-label query-keyword width-7">Host</label>
<input type="text" <input type="text"
ng-model="ctrl.panel.targets[ds].host.filter" ng-model="target.host.filter"
bs-typeahead="editor.getHostNames[ds]" bs-typeahead="editor.getHostNames[target.datasource]"
ng-blur="editor.parseTarget()" ng-blur="editor.parseTarget()"
data-min-length=0 data-min-length=0
data-items=100 data-items=100
class="gf-form-input width-14" class="gf-form-input width-14"
ng-class="{ ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].host.filter), 'zbx-variable': editor.isVariable(target.host.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].host.filter) 'zbx-regex': editor.isRegex(target.host.filter)
}"> }">
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Proxy</label> <label class="gf-form-label query-keyword width-7">Proxy</label>
<input type="text" <input type="text"
ng-model="ctrl.panel.targets[ds].proxy.filter" ng-model="target.proxy.filter"
bs-typeahead="editor.getProxyNames[ds]" bs-typeahead="editor.getProxyNames[target.datasource]"
ng-blur="editor.parseTarget()" ng-blur="editor.parseTarget()"
data-min-length=0 data-min-length=0
data-items=100 data-items=100
class="gf-form-input width-14" class="gf-form-input width-14"
ng-class="{ ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].proxy.filter), 'zbx-variable': editor.isVariable(target.proxy.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].proxy.filter) 'zbx-regex': editor.isRegex(target.proxy.filter)
}"> }">
</div> </div>
</div> </div>
@@ -67,35 +67,35 @@
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Application</label> <label class="gf-form-label query-keyword width-7">Application</label>
<input type="text" <input type="text"
ng-model="ctrl.panel.targets[ds].application.filter" ng-model="target.application.filter"
bs-typeahead="editor.getApplicationNames[ds]" bs-typeahead="editor.getApplicationNames[target.datasource]"
ng-blur="editor.parseTarget()" ng-blur="editor.parseTarget()"
data-min-length=0 data-min-length=0
data-items=100 data-items=100
class="gf-form-input width-14" class="gf-form-input width-14"
ng-class="{ ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].application.filter), 'zbx-variable': editor.isVariable(target.application.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].application.filter) 'zbx-regex': editor.isRegex(target.application.filter)
}"> }">
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Trigger</label> <label class="gf-form-label query-keyword width-7">Trigger</label>
<input type="text" <input type="text"
ng-model="ctrl.panel.targets[ds].trigger.filter" ng-model="target.trigger.filter"
ng-blur="editor.parseTarget()" ng-blur="editor.parseTarget()"
placeholder="trigger name" placeholder="trigger name"
class="gf-form-input width-14" class="gf-form-input width-14"
ng-style="ctrl.panel.targets[ds].trigger.style" ng-style="target.trigger.style"
ng-class="{ ng-class="{
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].trigger.filter), 'zbx-variable': editor.isVariable(target.trigger.filter),
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].trigger.filter) 'zbx-regex': editor.isRegex(target.trigger.filter)
}" }"
empty-to-null> empty-to-null>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Tags</label> <label class="gf-form-label query-keyword width-7">Tags</label>
<input type="text" class="gf-form-input width-14" <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()" ng-blur="editor.parseTarget()"
placeholder="tag1:value1, tag2:value2"> placeholder="tag1:value1, tag2:value2">
</div> </div>

View File

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

View File

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

View File

@@ -5,9 +5,9 @@ import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl';
// import { create } from 'domain'; // import { create } from 'domain';
describe('TriggerPanelCtrl', () => { describe('TriggerPanelCtrl', () => {
let ctx = {}; let ctx: any = {};
let datasourceSrvMock, zabbixDSMock; let datasourceSrvMock, zabbixDSMock;
let timeoutMock = () => {}; const timeoutMock = () => {};
let createPanelCtrl; let createPanelCtrl;
beforeEach(() => { beforeEach(() => {
@@ -61,7 +61,7 @@ describe('TriggerPanelCtrl', () => {
describe('When adding new panel', () => { describe('When adding new panel', () => {
it('should suggest all zabbix data sources', () => { it('should suggest all zabbix data sources', () => {
ctx.scope.panel = {}; ctx.scope.panel = {};
let panelCtrl = createPanelCtrl(); const panelCtrl = createPanelCtrl();
expect(panelCtrl.available_datasources).toEqual([ expect(panelCtrl.available_datasources).toEqual([
'zabbix_default', 'zabbix' 'zabbix_default', 'zabbix'
]); ]);
@@ -69,10 +69,8 @@ describe('TriggerPanelCtrl', () => {
it('should load first zabbix data source as default', () => { it('should load first zabbix data source as default', () => {
ctx.scope.panel = {}; ctx.scope.panel = {};
let panelCtrl = createPanelCtrl(); const panelCtrl = createPanelCtrl();
expect(panelCtrl.panel.datasources).toEqual([ expect(panelCtrl.panel.targets[0].datasource).toEqual('zabbix_default');
'zabbix_default'
]);
}); });
it('should rewrite default empty target', () => { it('should rewrite default empty target', () => {
@@ -82,7 +80,7 @@ describe('TriggerPanelCtrl', () => {
"refId": "A" "refId": "A"
}], }],
}; };
let panelCtrl = createPanelCtrl(); const panelCtrl = createPanelCtrl();
expect(panelCtrl.available_datasources).toEqual([ expect(panelCtrl.available_datasources).toEqual([
'zabbix_default', 'zabbix' 'zabbix_default', 'zabbix'
]); ]);
@@ -92,16 +90,22 @@ describe('TriggerPanelCtrl', () => {
describe('When refreshing panel', () => { describe('When refreshing panel', () => {
beforeEach(() => { beforeEach(() => {
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix']; ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
ctx.scope.panel.targets = { ctx.scope.panel.targets = [
'zabbix_default': DEFAULT_TARGET, {
'zabbix': DEFAULT_TARGET ...DEFAULT_TARGET,
}; datasource: 'zabbix_default'
},
{
...DEFAULT_TARGET,
datasource: 'zabbix'
},
];
ctx.panelCtrl = createPanelCtrl(); ctx.panelCtrl = createPanelCtrl();
}); });
it('should format triggers', (done) => { it('should format triggers', (done) => {
ctx.panelCtrl.onRefresh().then(() => { 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.host).toBe('backend01');
expect(formattedTrigger.hostTechName).toBe('backend01_tech'); expect(formattedTrigger.hostTechName).toBe('backend01_tech');
expect(formattedTrigger.datasource).toBe('zabbix_default'); expect(formattedTrigger.datasource).toBe('zabbix_default');
@@ -113,7 +117,7 @@ describe('TriggerPanelCtrl', () => {
it('should sort triggers by time by default', (done) => { it('should sort triggers by time by default', (done) => {
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid'); const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
expect(trigger_ids).toEqual([ expect(trigger_ids).toEqual([
'2', '4', '3', '1' '2', '4', '3', '1'
]); ]);
@@ -124,7 +128,7 @@ describe('TriggerPanelCtrl', () => {
it('should sort triggers by severity', (done) => { it('should sort triggers by severity', (done) => {
ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' }; ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' };
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid'); const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
expect(trigger_ids).toEqual([ expect(trigger_ids).toEqual([
'1', '3', '2', '4' '1', '3', '2', '4'
]); ]);
@@ -134,7 +138,7 @@ describe('TriggerPanelCtrl', () => {
it('should add acknowledges to trigger', (done) => { it('should add acknowledges to trigger', (done) => {
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx); const trigger = getTriggerById(1, ctx);
expect(trigger.acknowledges).toHaveLength(1); expect(trigger.acknowledges).toHaveLength(1);
expect(trigger.acknowledges[0].message).toBe("event ack"); expect(trigger.acknowledges[0].message).toBe("event ack");
@@ -153,15 +157,15 @@ describe('TriggerPanelCtrl', () => {
it('should handle new lines in trigger description', () => { it('should handle new lines in trigger description', () => {
ctx.panelCtrl.setTriggerSeverity = jest.fn((trigger) => trigger); 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); const formattedTrigger = ctx.panelCtrl.formatTrigger(trigger);
expect(formattedTrigger.comments).toBe("this is<br>description"); expect(formattedTrigger.comments).toBe("this is<br>description");
}); });
it('should format host name to display (default)', (done) => { it('should format host name to display (default)', (done) => {
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx); const trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger); const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe('backend01'); expect(hostname).toBe('backend01');
done(); done();
}); });
@@ -171,8 +175,8 @@ describe('TriggerPanelCtrl', () => {
ctx.panelCtrl.panel.hostField = false; ctx.panelCtrl.panel.hostField = false;
ctx.panelCtrl.panel.hostTechNameField = true; ctx.panelCtrl.panel.hostTechNameField = true;
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx); const trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger); const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe('backend01_tech'); expect(hostname).toBe('backend01_tech');
done(); done();
}); });
@@ -182,8 +186,8 @@ describe('TriggerPanelCtrl', () => {
ctx.panelCtrl.panel.hostField = true; ctx.panelCtrl.panel.hostField = true;
ctx.panelCtrl.panel.hostTechNameField = true; ctx.panelCtrl.panel.hostTechNameField = true;
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx); const trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger); const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe('backend01 (backend01_tech)'); expect(hostname).toBe('backend01 (backend01_tech)');
done(); done();
}); });
@@ -193,8 +197,8 @@ describe('TriggerPanelCtrl', () => {
ctx.panelCtrl.panel.hostField = false; ctx.panelCtrl.panel.hostField = false;
ctx.panelCtrl.panel.hostTechNameField = false; ctx.panelCtrl.panel.hostTechNameField = false;
ctx.panelCtrl.onRefresh().then(() => { ctx.panelCtrl.onRefresh().then(() => {
let trigger = getTriggerById(1, ctx); const trigger = getTriggerById(1, ctx);
let hostname = ctx.panelCtrl.formatHostName(trigger); const hostname = ctx.panelCtrl.formatHostName(trigger);
expect(hostname).toBe(""); expect(hostname).toBe("");
done(); done();
}); });
@@ -222,7 +226,7 @@ describe('TriggerPanelCtrl', () => {
}); });
}); });
const defaultTrigger = { const defaultTrigger: any = {
"triggerid": "13565", "triggerid": "13565",
"value": "1", "value": "1",
"groups": [{"groupid": "1", "name": "Backend"}] , "groups": [{"groupid": "1", "name": "Backend"}] ,
@@ -248,7 +252,7 @@ const defaultTrigger = {
"flags": "0", "type": "0", "items": [] , "error": "" "flags": "0", "type": "0", "items": [] , "error": ""
}; };
const defaultEvent = { const defaultEvent: any = {
"eventid": "11", "eventid": "11",
"acknowledges": [ "acknowledges": [
{ {
@@ -272,8 +276,8 @@ const defaultEvent = {
"objectid": "1", "objectid": "1",
}; };
function generateTrigger(id, timestamp, severity) { function generateTrigger(id, timestamp?, severity?): any {
let trigger = _.cloneDeep(defaultTrigger); const trigger = _.cloneDeep(defaultTrigger);
trigger.triggerid = id.toString(); trigger.triggerid = id.toString();
if (severity) { if (severity) {
trigger.priority = severity.toString(); trigger.priority = severity.toString();
@@ -284,13 +288,13 @@ function generateTrigger(id, timestamp, severity) {
return trigger; return trigger;
} }
function createTrigger(props) { function createTrigger(props): any {
let trigger = _.cloneDeep(defaultTrigger); let trigger = _.cloneDeep(defaultTrigger);
trigger = _.merge(trigger, props); trigger = _.merge(trigger, props);
trigger.lastEvent.objectid = trigger.triggerid; trigger.lastEvent.objectid = trigger.triggerid;
return trigger; return trigger;
} }
function getTriggerById(id, ctx) { function getTriggerById(id, ctx): any {
return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()}); 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 { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
import ProblemList from './components/Problems/Problems'; import ProblemList from './components/Problems/Problems';
import AlertList from './components/AlertList/AlertList'; import AlertList from './components/AlertList/AlertList';
import { getNextRefIdChar } from './utils';
const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource'; const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
const PROBLEM_EVENTS_LIMIT = 100; const PROBLEM_EVENTS_LIMIT = 100;
@@ -23,7 +24,17 @@ export const DEFAULT_TARGET = {
proxy: {filter: ""}, 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 = [ export const DEFAULT_SEVERITY = [
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true}, { 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 = { export const PANEL_DEFAULTS = {
schemaVersion: CURRENT_SCHEMA_VERSION, schemaVersion: CURRENT_SCHEMA_VERSION,
datasources: [], targets: [getDefaultTarget([])],
targets: {},
// Fields // Fields
hostField: true, hostField: true,
hostTechNameField: false, hostTechNameField: false,
@@ -108,11 +118,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS)); _.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
this.available_datasources = _.map(this.getZabbixDataSources(), 'name'); this.available_datasources = _.map(this.getZabbixDataSources(), 'name');
if (this.panel.datasources.length === 0) { if (this.panel.targets && !this.panel.targets[0].datasource) {
this.panel.datasources.push(this.available_datasources[0]); this.panel.targets[0].datasource = this.available_datasources[0];
}
if (this.isEmptyTargets()) {
this.panel.targets[this.panel.datasources[0]] = getDefaultTarget();
} }
this.initDatasources(); this.initDatasources();
@@ -138,7 +145,11 @@ export class TriggerPanelCtrl extends PanelCtrl {
} }
initDatasources() { 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 // Load datasource
return this.datasourceSrv.get(ds) return this.datasourceSrv.get(ds)
.then(datasource => { .then(datasource => {
@@ -234,16 +245,20 @@ export class TriggerPanelCtrl extends PanelCtrl {
getTriggers() { getTriggers() {
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
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 proxies;
let showAckButton = true;
return this.datasourceSrv.get(ds) return this.datasourceSrv.get(ds)
.then(datasource => { .then(datasource => {
const zabbix = datasource.zabbix; const zabbix = datasource.zabbix;
const showEvents = this.panel.showEvents.value; const showEvents = this.panel.showEvents.value;
const triggerFilter = this.panel.targets[ds]; const triggerFilter = target;
const showProxy = this.panel.hostProxy; const showProxy = this.panel.hostProxy;
const getProxiesPromise = showProxy ? zabbix.getProxies() : () => []; const getProxiesPromise = showProxy ? zabbix.getProxies() : () => [];
showAckButton = !datasource.disableReadOnlyUsersAck || userIsEditor;
// Replace template variables // Replace template variables
const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter); const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter);
@@ -271,19 +286,18 @@ export class TriggerPanelCtrl extends PanelCtrl {
})); }));
return Promise.all([ return Promise.all([
this.datasources[ds].zabbix.getExtendedEventData(eventids), this.datasources[ds].zabbix.getExtendedEventData(eventids),
this.datasources[ds].zabbix.getEventAlerts(eventids),
Promise.resolve(triggers) Promise.resolve(triggers)
]); ]);
}) })
.then(([events, alerts, triggers]) => { .then(([events, triggers]) => {
this.addEventTags(events, triggers); this.addEventTags(events, triggers);
this.addAcknowledges(events, triggers); this.addAcknowledges(events, triggers);
this.addEventAlerts(alerts, triggers);
return triggers; return triggers;
}) })
.then(triggers => this.setMaintenanceStatus(triggers)) .then(triggers => this.setMaintenanceStatus(triggers))
.then(triggers => this.filterTriggersPre(triggers, ds)) .then(triggers => this.setAckButtonStatus(triggers, showAckButton))
.then(triggers => this.addTriggerDataSource(triggers, ds)) .then(triggers => this.filterTriggersPre(triggers, target))
.then(triggers => this.addTriggerDataSource(triggers, target))
.then(triggers => this.addTriggerHostProxy(triggers, proxies)); .then(triggers => this.addTriggerHostProxy(triggers, proxies));
}); });
@@ -337,28 +351,17 @@ export class TriggerPanelCtrl extends PanelCtrl {
return triggers; return triggers;
} }
addEventAlerts(alerts, triggers) { filterTriggersPre(triggerList, target) {
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) {
// Filter triggers by description // 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); triggerFilter = this.datasources[ds].replaceTemplateVars(triggerFilter);
if (triggerFilter) { if (triggerFilter) {
triggerList = filterTriggers(triggerList, triggerFilter); triggerList = filterTriggers(triggerList, triggerFilter);
} }
// Filter by tags // Filter by tags
const target = this.panel.targets[ds]; // const target = this.panel.targets[ds];
if (target.tags.filter) { if (target.tags.filter) {
let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter); let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter);
// replaceTemplateVars() builds regex-like string, so we should trim it. // replaceTemplateVars() builds regex-like string, so we should trim it.
@@ -409,9 +412,16 @@ export class TriggerPanelCtrl extends PanelCtrl {
return triggers; return triggers;
} }
addTriggerDataSource(triggers, ds) { setAckButtonStatus(triggers, showAckButton) {
_.each(triggers, (trigger) => { _.each(triggers, (trigger) => {
trigger.datasource = ds; trigger.showAckButton = showAckButton;
});
return triggers;
}
addTriggerDataSource(triggers, target) {
_.each(triggers, (trigger) => {
trigger.datasource = target.datasource;
}); });
return triggers; return triggers;
} }
@@ -482,37 +492,51 @@ export class TriggerPanelCtrl extends PanelCtrl {
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', '); return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
} }
addTagFilter(tag, ds) { addTagFilter(tag, datasource) {
let tagFilter = this.panel.targets[ds].tags.filter; const target = this.panel.targets.find(t => t.datasource === datasource);
console.log(target);
let tagFilter = target.tags.filter;
let targetTags = this.parseTags(tagFilter); let targetTags = this.parseTags(tagFilter);
let newTag = {tag: tag.tag, value: tag.value}; let newTag = {tag: tag.tag, value: tag.value};
targetTags.push(newTag); targetTags.push(newTag);
targetTags = _.uniqWith(targetTags, _.isEqual); targetTags = _.uniqWith(targetTags, _.isEqual);
let newFilter = this.tagsToString(targetTags); let newFilter = this.tagsToString(targetTags);
this.panel.targets[ds].tags.filter = newFilter; target.tags.filter = newFilter;
this.refresh(); this.refresh();
} }
removeTagFilter(tag, ds) { removeTagFilter(tag, datasource) {
let tagFilter = this.panel.targets[ds].tags.filter; const target = this.panel.targets.find(t => t.datasource === datasource);
let tagFilter = target.tags.filter;
let targetTags = this.parseTags(tagFilter); let targetTags = this.parseTags(tagFilter);
_.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value); _.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value);
targetTags = _.uniqWith(targetTags, _.isEqual); targetTags = _.uniqWith(targetTags, _.isEqual);
let newFilter = this.tagsToString(targetTags); let newFilter = this.tagsToString(targetTags);
this.panel.targets[ds].tags.filter = newFilter; target.tags.filter = newFilter;
this.refresh(); this.refresh();
} }
getProblemEvents(trigger) { getProblemEvents(problem) {
const triggerids = [trigger.triggerid]; const triggerids = [problem.triggerid];
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
return this.datasourceSrv.get(trigger.datasource) return this.datasourceSrv.get(problem.datasource)
.then(datasource => { .then(datasource => {
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT); return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
}); });
} }
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) { formatHostName(trigger) {
let host = ""; let host = "";
if (this.panel.hostField && this.panel.hostTechNameField) { if (this.panel.hostField && this.panel.hostTechNameField) {
@@ -549,12 +573,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
getAlertIconClass(trigger) { getAlertIconClass(trigger) {
let iconClass = ''; let iconClass = '';
if (trigger.value === '1') { if (trigger.value === '1' && trigger.priority >= 2) {
if (trigger.priority >= 3) {
iconClass = 'icon-gf-critical'; iconClass = 'icon-gf-critical';
} else {
iconClass = 'icon-gf-warning';
}
} else { } else {
iconClass = 'icon-gf-online'; iconClass = 'icon-gf-online';
} }
@@ -566,8 +586,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
} }
getAlertIconClassBySeverity(triggerSeverity) { getAlertIconClassBySeverity(triggerSeverity) {
let iconClass = 'icon-gf-warning'; let iconClass = 'icon-gf-online';
if (triggerSeverity.priority >= 3) { if (triggerSeverity.priority >= 2) {
iconClass = 'icon-gf-critical'; iconClass = 'icon-gf-critical';
} }
return iconClass; return iconClass;
@@ -663,6 +683,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
pageSize, pageSize,
fontSize: fontSizeProp, fontSize: fontSizeProp,
getProblemEvents: ctrl.getProblemEvents.bind(ctrl), getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl), onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
onColumnResize: ctrl.handleColumnResize.bind(ctrl), onColumnResize: ctrl.handleColumnResize.bind(ctrl),
onProblemAck: (trigger, data) => { onProblemAck: (trigger, data) => {

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { DataQuery } from '@grafana/ui/';
import * as utils from '../datasource-zabbix/utils'; import * as utils from '../datasource-zabbix/utils';
import { ZBXTrigger } from './types'; import { ZBXTrigger } from './types';
@@ -20,3 +22,13 @@ export function formatLastChange(lastchangeUnix: number, customFormat?: string)
const lastchange = timestamp.format(format); const lastchange = timestamp.format(format);
return lastchange; 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": "Metric Editor", "path": "img/screenshot-metric_editor.png"},
{"name": "Triggers", "path": "img/screenshot-triggers.png"} {"name": "Triggers", "path": "img/screenshot-triggers.png"}
], ],
"version": "3.10.1", "version": "3.10.5",
"updated": "2019-03-05" "updated": "2019-12-26"
}, },
"includes": [ "includes": [
@@ -42,7 +42,7 @@
], ],
"dependencies": { "dependencies": {
"grafanaVersion": "5.x", "grafanaVersion": "6.x",
"plugins": [] "plugins": []
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

5949
yarn.lock

File diff suppressed because it is too large Load Diff