Merge branch 'master' into docs
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
42
.circleci/make-release.sh
Executable 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
1
.codespell_ignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hist
|
||||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -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
7
babel.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
"presets": [
|
||||||
|
[ "@babel/preset-env", { "targets": { "node": "current" } } ],
|
||||||
|
"@babel/react"
|
||||||
|
],
|
||||||
|
"retainLines": true
|
||||||
|
};
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Another case to use regex is comparing the same metrics for different hosts. Use
|
|||||||

|

|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
47
package.json
47
package.json
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('');
|
||||||
|
|||||||
109
src/datasource-zabbix/components/FunctionEditor.tsx
Normal file
109
src/datasource-zabbix/components/FunctionEditor.tsx
Normal 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 };
|
||||||
67
src/datasource-zabbix/components/FunctionEditorControls.tsx
Normal file
67
src/datasource-zabbix/components/FunctionEditorControls.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
256
src/datasource-zabbix/metric-function-editor.directive.ts
Normal file
256
src/datasource-zabbix/metric-function-editor.directive.ts
Normal 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) : ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : ' ') +
|
||||||
|
'</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']);
|
||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
10
src/datasource-zabbix/react2angular.ts
Normal file
10
src/datasource-zabbix/react2angular.ts
Normal 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);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]);
|
if (_.includes(groupNames, group)) {
|
||||||
row = _.concat([group], ...row);
|
let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]);
|
||||||
table.rows.push(row);
|
row = _.concat([group], ...row);
|
||||||
|
table.rows.push(row);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,24 +112,19 @@ describe('ZabbixDatasource', () => {
|
|||||||
}
|
}
|
||||||
]));
|
]));
|
||||||
|
|
||||||
ctx.options = {
|
ctx.options.targets = [{
|
||||||
range: {from: 'now-1h', to: 'now'},
|
group: {filter: ""},
|
||||||
targets: [
|
host: {filter: "Zabbix server"},
|
||||||
{
|
application: {filter: ""},
|
||||||
group: {filter: ""},
|
item: {filter: "System information"},
|
||||||
host: {filter: "Zabbix server"},
|
textFilter: "",
|
||||||
application: {filter: ""},
|
useCaptureGroups: true,
|
||||||
item: {filter: "System information"},
|
mode: 2,
|
||||||
textFilter: "",
|
resultFormat: "table",
|
||||||
useCaptureGroups: true,
|
options: {
|
||||||
mode: 2,
|
skipEmptyValues: false
|
||||||
resultFormat: "table",
|
}
|
||||||
options: {
|
}];
|
||||||
skipEmptyValues: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return data in table format', (done) => {
|
it('should return data in table format', (done) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesPr
|
|||||||
{ackRows}
|
{ackRows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="ack-add-button">
|
{problem.showAckButton &&
|
||||||
<button id="add-acknowledge-btn" className="btn btn-mini btn-inverse gf-form-button" onClick={this.handleClick}>
|
<div className="ack-add-button">
|
||||||
<i className="fa fa-plus"></i>
|
<button id="add-acknowledge-btn" className="btn btn-mini btn-inverse gf-form-button" onClick={this.handleClick}>
|
||||||
</button>
|
<i className="fa fa-plus"></i>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +44,10 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
showAckDialog = () => {
|
showAckDialog = () => {
|
||||||
this.setState({ showAckDialog: true });
|
const problem = this.props.problem;
|
||||||
|
if (problem.showAckButton) {
|
||||||
|
this.setState({ showAckDialog: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAckDialog = () => {
|
closeAckDialog = () => {
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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'} />
|
||||||
<div className="problem-actions">
|
{problem.showAckButton &&
|
||||||
<ProblemActionButton className="navbar-button navbar-button--settings"
|
<div className="problem-actions">
|
||||||
icon="reply-all"
|
<ProblemActionButton className="navbar-button navbar-button--settings"
|
||||||
tooltip="Acknowledge problem"
|
icon="reply-all"
|
||||||
onClick={this.showAckDialog} />
|
tooltip="Acknowledge problem"
|
||||||
</div>
|
onClick={this.showAckDialog} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{problem.comments &&
|
{problem.comments &&
|
||||||
<div className="problem-description">
|
<div className="problem-description">
|
||||||
|
|||||||
@@ -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 || ''}`}>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"id": "alexanderzobnin-zabbix-triggers-panel",
|
"id": "alexanderzobnin-zabbix-triggers-panel",
|
||||||
|
|
||||||
"dataFormats": [],
|
"dataFormats": [],
|
||||||
|
"skipDataQuery": true,
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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()});
|
||||||
}
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
|
|
||||||
.rt-noData {
|
.rt-noData {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
background: unset;
|
||||||
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-bottom {
|
.pagination-bottom {
|
||||||
@@ -522,15 +524,22 @@
|
|||||||
|
|
||||||
@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} {
|
||||||
font-size: 1% * $i * 10;
|
.rt-table {
|
||||||
|
font-size: 1% * $i * 10;
|
||||||
|
|
||||||
& .rt-tr .rt-td.custom-expander i {
|
& .rt-tr .rt-td.custom-expander i {
|
||||||
font-size: 1.2rem * $i / 10;
|
font-size: 1.2rem * $i / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-details-container.show {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-details-container.show {
|
.rt-noData {
|
||||||
font-size: 13px;
|
top: 4.5em;
|
||||||
|
font-size: 1% * $i * 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user