Merge branch 'master' into docs
This commit is contained in:
@@ -6,11 +6,17 @@ aliases:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- /^release-[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
|
||||
- docs
|
||||
- gh-pages
|
||||
- &filter-only-master
|
||||
branches:
|
||||
only: master
|
||||
- &filter-only-release
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
|
||||
- &filter-docs
|
||||
branches:
|
||||
only: docs
|
||||
@@ -20,7 +26,7 @@ jobs:
|
||||
build:
|
||||
working_directory: ~/alexanderzobnin/grafana-zabbix
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:12
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
@@ -39,7 +45,7 @@ jobs:
|
||||
lint:
|
||||
working_directory: ~/alexanderzobnin/grafana-zabbix
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:12
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
@@ -61,7 +67,7 @@ jobs:
|
||||
CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
|
||||
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:12
|
||||
steps:
|
||||
- checkout
|
||||
# Prepare for artifact and test results collection equivalent to how it was done on 1.0.
|
||||
@@ -87,6 +93,31 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
make-release:
|
||||
working_directory: ~/alexanderzobnin/grafana-zabbix
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
environment:
|
||||
CI_GIT_USER: CircleCI
|
||||
CI_GIT_EMAIL: ci@grafana.com
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "dc:7e:54:e0:aa:56:4d:e5:60:7b:f3:51:24:2d:d3:29"
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- dependency-cache-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: yarn install
|
||||
command: 'yarn install --pure-lockfile --no-progress'
|
||||
no_output_timeout: 15m
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules
|
||||
- run: ./.circleci/make-release.sh
|
||||
|
||||
build-docs:
|
||||
working_directory: ~/grafana-zabbix
|
||||
docker:
|
||||
@@ -110,7 +141,7 @@ jobs:
|
||||
deploy-docs:
|
||||
working_directory: ~/grafana-zabbix
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:12
|
||||
environment:
|
||||
GH_PAGES_BRANCH: gh-pages
|
||||
CI_GIT_USER: CircleCI
|
||||
@@ -124,12 +155,23 @@ jobs:
|
||||
at: ../gh-pages
|
||||
- run: ./.circleci/deploy-docs.sh
|
||||
|
||||
codespell:
|
||||
docker:
|
||||
- image: circleci/python
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- run: sudo pip install codespell
|
||||
- run: codespell -S './.git*,./src/img*' -L que --ignore-words=./.codespell_ignore
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-master:
|
||||
jobs:
|
||||
- build:
|
||||
filters: *filter-only-master
|
||||
- codespell:
|
||||
filters: *filter-only-master
|
||||
- lint:
|
||||
filters: *filter-only-master
|
||||
- test:
|
||||
@@ -139,11 +181,31 @@ workflows:
|
||||
jobs:
|
||||
- build:
|
||||
filters: *filter-not-release-or-master
|
||||
- codespell:
|
||||
filters: *filter-not-release-or-master
|
||||
- lint:
|
||||
filters: *filter-not-release-or-master
|
||||
- test:
|
||||
filters: *filter-not-release-or-master
|
||||
|
||||
build-release:
|
||||
jobs:
|
||||
- build:
|
||||
filters: *filter-only-release
|
||||
- codespell:
|
||||
filters: *filter-only-release
|
||||
- lint:
|
||||
filters: *filter-only-release
|
||||
- test:
|
||||
filters: *filter-only-release
|
||||
- make-release:
|
||||
requires:
|
||||
- build
|
||||
- codespell
|
||||
- lint
|
||||
- test
|
||||
filters: *filter-only-release
|
||||
|
||||
build-docs:
|
||||
jobs:
|
||||
- build-docs:
|
||||
|
||||
@@ -10,15 +10,15 @@ set -o pipefail
|
||||
echo "current dir: $(pwd)"
|
||||
|
||||
# Setup git env
|
||||
git config --global user.email $CI_GIT_EMAIL
|
||||
git config --global user.name $CI_GIT_USER
|
||||
git config --global user.email "$CI_GIT_EMAIL"
|
||||
git config --global user.name "$CI_GIT_USER"
|
||||
echo "git user is $CI_GIT_USER ($CI_GIT_EMAIL)"
|
||||
|
||||
git checkout -b $GH_PAGES_BRANCH
|
||||
rm -rf * || true
|
||||
git checkout -b "$GH_PAGES_BRANCH"
|
||||
rm -rf ./* || true
|
||||
mv ../gh-pages/docs/site/* ./
|
||||
git add --force .
|
||||
git commit -m "build docs from commit ${CIRCLE_SHA1:0:7} (branch $CIRCLE_BRANCH)"
|
||||
git log -n 3
|
||||
|
||||
git push origin $GH_PAGES_BRANCH --force
|
||||
git push origin "$GH_PAGES_BRANCH" --force
|
||||
|
||||
42
.circleci/make-release.sh
Executable file
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/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [3.10.5] - 2019-12-26
|
||||
### Added
|
||||
- SLA over time graphs, [#728](https://github.com/alexanderzobnin/grafana-zabbix/issues/728)
|
||||
- Additional time ranges in functions, [#531](https://github.com/alexanderzobnin/grafana-zabbix/issues/531)
|
||||
|
||||
### Fixed
|
||||
- Problems panel: query editor broken in Grafana 6.4, [#817](https://github.com/alexanderzobnin/grafana-zabbix/issues/817)
|
||||
- Datasource: function editor is not working, [#810](https://github.com/alexanderzobnin/grafana-zabbix/issues/810)
|
||||
- Datasource: cannot add a function to query from typeahead, [#468](https://github.com/alexanderzobnin/grafana-zabbix/issues/468)
|
||||
- Datasource: annotations editor broken in Grafana 6.x, [#813](https://github.com/alexanderzobnin/grafana-zabbix/issues/813)
|
||||
- React plugins issue, [#823](https://github.com/alexanderzobnin/grafana-zabbix/issues/823)
|
||||
|
||||
## [3.10.4] - 2019-08-08
|
||||
### Fixed
|
||||
- Problems panel: query editor broken in Grafana 6.3, [#778](https://github.com/alexanderzobnin/grafana-zabbix/issues/778)
|
||||
- Problems panel: some heart icons are missing, [#754](https://github.com/alexanderzobnin/grafana-zabbix/issues/754)
|
||||
|
||||
## [3.10.3] - 2019-07-26
|
||||
### Fixed
|
||||
- Direct DB Connection: can't stay enabled, [#731](https://github.com/alexanderzobnin/grafana-zabbix/issues/731)
|
||||
- Triggers query mode: count doesn't work with Singlestat, [#726](https://github.com/alexanderzobnin/grafana-zabbix/issues/726)
|
||||
- Query editor: function editor looks odd in Grafana 6.x, [#765](https://github.com/alexanderzobnin/grafana-zabbix/issues/765)
|
||||
- Alerting: heart icon on panels in Grafana 6.x, [#715](https://github.com/alexanderzobnin/grafana-zabbix/issues/715)
|
||||
|
||||
## [3.10.2] - 2019-04-23
|
||||
### Fixed
|
||||
- Direct DB Connection: provisioned datasource fails to load, [#711](https://github.com/alexanderzobnin/grafana-zabbix/issues/711)
|
||||
- Functions: `sumSeries` doesn't work in couple with other aggregation functions, [#530](https://github.com/alexanderzobnin/grafana-zabbix/issues/530)
|
||||
- Problems panel: performance and memory issues, [#720](https://github.com/alexanderzobnin/grafana-zabbix/issues/720), [#712](https://github.com/alexanderzobnin/grafana-zabbix/issues/712)
|
||||
- Problems panel: hide acknowledge button for read-only users, [#722](https://github.com/alexanderzobnin/grafana-zabbix/issues/722)
|
||||
- Problems panel: "no data" overlaps table header when font size increased, [#717](https://github.com/alexanderzobnin/grafana-zabbix/issues/717)
|
||||
- Problems panel: problem description does not resize problem bar, [#704](https://github.com/alexanderzobnin/grafana-zabbix/issues/704)
|
||||
- Triggers query mode: problems not filtered by selected groups, [#709](https://github.com/alexanderzobnin/grafana-zabbix/issues/709)
|
||||
|
||||
## [3.10.1] - 2019-03-05
|
||||
### Fixed
|
||||
- Problems panel: unable to edit panel in Grafana 6.0, [#685](https://github.com/alexanderzobnin/grafana-zabbix/issues/685)
|
||||
@@ -189,7 +225,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- setAliasByRegex() function
|
||||
|
||||
### Changed
|
||||
- **Docs**: deprecate special repo with builded plugin.
|
||||
- **Docs**: deprecate special repo with built plugins.
|
||||
- **Triggers panel**: remove 'default' from datasources list (cause error), iss [#340](https://github.com/alexanderzobnin/grafana-zabbix/issues/340)
|
||||
- Add dist/ directory to repo to correspond development guide http://docs.grafana.org/plugins/development/
|
||||
|
||||
|
||||
7
babel.config.js
Normal file
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
|
||||
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.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Another case to use regex is comparing the same metrics for different hosts. Use
|
||||

|
||||
|
||||
## 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**
|
||||
The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part of core
|
||||
Grafana. Only some data soures are supported right now. They include Graphite, Prometheus, InfluxDB and OpenTSDB.
|
||||
Grafana. Only some data sources are supported right now. They include Graphite, Prometheus, InfluxDB and OpenTSDB.
|
||||
- **Alerting visualisations**
|
||||
Alerts highlight panels with problems and it can easily be found on the dashboard.
|
||||
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
Functions reference
|
||||
===================
|
||||
|
||||
## Functions Variables
|
||||
|
||||
There are some built-in template variables available for using in functions:
|
||||
|
||||
- `$__range_ms` - panel time range in ms
|
||||
- `$__range_s` - panel time range in seconds
|
||||
- `$__range` - panel time range, string representation (`30s`, `1m`, `1h`)
|
||||
- `$__range_series` - invoke function over all series values
|
||||
|
||||
Examples:
|
||||
```
|
||||
groupBy($__range, avg)
|
||||
percentile($__range_series, 95) - 95th percentile over all values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transform
|
||||
|
||||
|
||||
@@ -10,7 +27,7 @@ Functions reference
|
||||
groupBy(interval, function)
|
||||
```
|
||||
|
||||
Takes each timeseries and consolidate its points falled in given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
|
||||
Takes each timeseries and consolidate its points fallen in the given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
|
||||
|
||||
Examples:
|
||||
```
|
||||
@@ -124,7 +141,7 @@ Replaces `null` values with N
|
||||
aggregateBy(interval, function)
|
||||
```
|
||||
|
||||
Takes all timeseries and consolidate all its points falled in given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
|
||||
Takes all timeseries and consolidate all its points fallen in the given _interval_ into one point using _function_, which can be one of: _avg_, _min_, _max_, _median_.
|
||||
|
||||
Examples:
|
||||
```
|
||||
@@ -142,6 +159,20 @@ This will add metrics together and return the sum at each datapoint. This method
|
||||
|
||||
---
|
||||
|
||||
### _percentile_
|
||||
```
|
||||
percentile(interval, N)
|
||||
```
|
||||
Takes all timeseries and consolidate all its points fallen in the given _interval_ into one point by Nth percentile.
|
||||
|
||||
Examples:
|
||||
```
|
||||
percentile(1h, 99)
|
||||
percentile($__range_series, 95) - 95th percentile over all values
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### _average_
|
||||
```
|
||||
average(interval)
|
||||
|
||||
47
package.json
47
package.json
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "grafana-zabbix",
|
||||
"private": false,
|
||||
"version": "3.10.1",
|
||||
"version": "3.10.5",
|
||||
"description": "Zabbix plugin for Grafana",
|
||||
"homepage": "http://grafana-zabbix.org",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack/webpack.prod.conf.js --progress --colors",
|
||||
"dev": "webpack --config webpack/webpack.dev.conf.js --progress --colors",
|
||||
@@ -24,6 +25,11 @@
|
||||
"url": "https://github.com/alexanderzobnin/grafana-zabbix/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/preset-env": "^7.7.7",
|
||||
"@babel/preset-react": "^7.6.3",
|
||||
"@grafana/data": "^6.4.2",
|
||||
"@grafana/ui": "^6.4.2",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/grafana": "github:CorpGlory/types-grafana",
|
||||
"@types/jest": "^23.1.1",
|
||||
@@ -33,35 +39,32 @@
|
||||
"@types/react": "^16.4.6",
|
||||
"@types/react-dom": "^16.0.11",
|
||||
"@types/react-transition-group": "^2.0.15",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"classnames": "^2.2.6",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"codecov": "^3.1.0",
|
||||
"copy-webpack-plugin": "^4.5.4",
|
||||
"css-loader": "^1.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "2.1.1",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"grunt": "^1.0.3",
|
||||
"grunt-benchmark": "^1.0.0",
|
||||
"grunt-cli": "^1.3.1",
|
||||
"grunt-execute": "^0.2.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"jest": "^23.6.0",
|
||||
"jest": "^24.9.0",
|
||||
"jscs": "^3.0.7",
|
||||
"jsdom": "~11.3.0",
|
||||
"jshint": "^2.9.6",
|
||||
"jshint-stylish": "^2.1.0",
|
||||
"load-grunt-tasks": "~3.2.0",
|
||||
"lodash": "~4.17.5",
|
||||
"lodash": "~4.17.13",
|
||||
"moment": "~2.21.0",
|
||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||
"node-sass": "^4.9.4",
|
||||
"node-sass": "^4.13.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
@@ -69,15 +72,23 @@
|
||||
"react-table": "^6.8.6",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"react-transition-group": "^2.5.2",
|
||||
"sass-loader": "^7.1.0",
|
||||
"rst2html": "github:thoward/rst2html#990cb89",
|
||||
"sass-loader": "7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"tether-drop": "^1.4.2",
|
||||
"ts-jest": "^23.10.5",
|
||||
"ts-jest": "^24.2.0",
|
||||
"ts-loader": "^4.4.1",
|
||||
"tslint": "^5.11.0",
|
||||
"typescript": "^2.9.2",
|
||||
"webpack": "^4.22.0",
|
||||
"webpack-cli": "^3.1.2"
|
||||
"tslint": "5.20.1",
|
||||
"typescript": "3.7.2",
|
||||
"webpack": "4.29.6",
|
||||
"webpack-cli": "3.2.3"
|
||||
},
|
||||
"homepage": "http://grafana-zabbix.org"
|
||||
"resolutions": {
|
||||
"js-yaml": "^3.13.1",
|
||||
"lodash": "~4.17.13",
|
||||
"set-value": "^2.0.1",
|
||||
"mixin-deep": "^1.3.2",
|
||||
"minimatch": "^3.0.2",
|
||||
"fstream": "^1.0.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ angular
|
||||
}
|
||||
|
||||
$scope.$apply(function() {
|
||||
$scope.addFunction(funcDef);
|
||||
$scope.ctrl.addFunction(funcDef);
|
||||
});
|
||||
|
||||
$input.trigger('blur');
|
||||
@@ -66,7 +66,7 @@ angular
|
||||
});
|
||||
|
||||
$input.blur(function() {
|
||||
// clicking the function dropdown menu wont
|
||||
// clicking the function dropdown menu won't
|
||||
// work if you remove class at once
|
||||
setTimeout(function() {
|
||||
$input.val('');
|
||||
|
||||
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);
|
||||
_.defaults(this.current.jsonData, defaultConfig);
|
||||
|
||||
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
|
||||
this.dbDataSources = this.getSupportedDBDataSources();
|
||||
this.zabbixVersions = _.cloneDeep(zabbixVersions);
|
||||
this.autoDetectZabbixVersion();
|
||||
if (!this.dbConnectionDatasourceId) {
|
||||
this.loadCurrentDBDatasource();
|
||||
}
|
||||
}
|
||||
|
||||
getSupportedDBDataSources() {
|
||||
@@ -41,11 +46,21 @@ export class ZabbixDSConfigController {
|
||||
}
|
||||
|
||||
getCurrentDatasourceType() {
|
||||
const dsId = this.current.jsonData.dbConnectionDatasourceId;
|
||||
const dsId = this.dbConnectionDatasourceId;
|
||||
const currentDs = _.find(this.dbDataSources, { 'id': dsId });
|
||||
return currentDs ? currentDs.type : null;
|
||||
}
|
||||
|
||||
loadCurrentDBDatasource() {
|
||||
const dsName= this.current.jsonData.dbConnectionDatasourceName;
|
||||
this.datasourceSrv.loadDatasource(dsName)
|
||||
.then(ds => {
|
||||
if (ds) {
|
||||
this.dbConnectionDatasourceId = ds.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
autoDetectZabbixVersion() {
|
||||
if (!this.current.id) {
|
||||
return;
|
||||
@@ -64,4 +79,8 @@ export class ZabbixDSConfigController {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDBConnectionDatasourceChange() {
|
||||
this.current.jsonData.dbConnectionDatasourceId = this.dbConnectionDatasourceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,8 @@ export const TRIGGER_SEVERITY = [
|
||||
{val: 4, text: 'High'},
|
||||
{val: 5, text: 'Disaster'}
|
||||
];
|
||||
|
||||
/** Minimum interval for SLA over time (1 hour) */
|
||||
export const MIN_SLA_INTERVAL = 3600;
|
||||
|
||||
export const RANGE_VARIABLE_VALUE = 'range_series';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import * as utils from './utils';
|
||||
import ts from './timeseries';
|
||||
import ts, { groupBy_perf as groupBy } from './timeseries';
|
||||
|
||||
let downsampleSeries = ts.downsample;
|
||||
let groupBy = ts.groupBy_perf;
|
||||
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
|
||||
let sumSeries = ts.sumSeries;
|
||||
let delta = ts.delta;
|
||||
@@ -19,7 +18,7 @@ let AVERAGE = ts.AVERAGE;
|
||||
let MIN = ts.MIN;
|
||||
let MAX = ts.MAX;
|
||||
let MEDIAN = ts.MEDIAN;
|
||||
let PERCENTIL = ts.PERCENTIL;
|
||||
let PERCENTILE = ts.PERCENTILE;
|
||||
|
||||
function limit(order, n, orderByFunc, timeseries) {
|
||||
let orderByCallback = aggregationFunctions[orderByFunc];
|
||||
@@ -107,7 +106,7 @@ function groupByWrapper(interval, groupFunc, datapoints) {
|
||||
|
||||
function aggregateByWrapper(interval, aggregateFunc, datapoints) {
|
||||
// Flatten all points in frame and then just use groupBy()
|
||||
const flattenedPoints = _.flatten(datapoints, true);
|
||||
const flattenedPoints = ts.flattenDatapoints(datapoints);
|
||||
// groupBy_perf works with sorted series only
|
||||
const sortedPoints = ts.sortByTime(flattenedPoints);
|
||||
let groupByCallback = aggregationFunctions[aggregateFunc];
|
||||
@@ -115,15 +114,15 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) {
|
||||
}
|
||||
|
||||
function aggregateWrapper(groupByCallback, interval, datapoints) {
|
||||
var flattenedPoints = _.flatten(datapoints, true);
|
||||
var flattenedPoints = ts.flattenDatapoints(datapoints);
|
||||
// groupBy_perf works with sorted series only
|
||||
const sortedPoints = ts.sortByTime(flattenedPoints);
|
||||
return groupBy(sortedPoints, interval, groupByCallback);
|
||||
}
|
||||
|
||||
function percentil(interval, n, datapoints) {
|
||||
var flattenedPoints = _.flatten(datapoints, true);
|
||||
var groupByCallback = _.partial(PERCENTIL, n);
|
||||
function percentile(interval, n, datapoints) {
|
||||
var flattenedPoints = ts.flattenDatapoints(datapoints);
|
||||
var groupByCallback = _.partial(PERCENTILE, n);
|
||||
return groupBy(flattenedPoints, interval, groupByCallback);
|
||||
}
|
||||
|
||||
@@ -155,7 +154,7 @@ let metricFunctions = {
|
||||
transformNull: transformNull,
|
||||
aggregateBy: aggregateByWrapper,
|
||||
// Predefined aggs
|
||||
percentil: percentil,
|
||||
percentile: percentile,
|
||||
average: _.partial(aggregateWrapper, AVERAGE),
|
||||
min: _.partial(aggregateWrapper, MIN),
|
||||
max: _.partial(aggregateWrapper, MAX),
|
||||
|
||||
@@ -111,6 +111,9 @@ export class ZabbixDatasource {
|
||||
let timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000);
|
||||
let timeTo = Math.ceil(dateMath.parse(options.range.to) / 1000);
|
||||
|
||||
// Add range variables
|
||||
options.scopedVars = Object.assign({}, options.scopedVars, utils.getRangeScopedVars(options.range));
|
||||
|
||||
// Prevent changes of original object
|
||||
let target = _.cloneDeep(t);
|
||||
|
||||
@@ -323,14 +326,14 @@ export class ZabbixDatasource {
|
||||
|
||||
return this.zabbix.getITServices(itServiceFilter)
|
||||
.then(itservices => {
|
||||
return this.zabbix.getSLA(itservices, timeRange, target, options);
|
||||
});
|
||||
return this.zabbix.getSLA(itservices, timeRange, target, options);})
|
||||
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
|
||||
}
|
||||
|
||||
queryTriggersData(target, timeRange) {
|
||||
let [timeFrom, timeTo] = timeRange;
|
||||
return this.zabbix.getHostsFromTarget(target)
|
||||
.then((results) => {
|
||||
.then(results => {
|
||||
let [hosts, apps] = results;
|
||||
if (hosts.length) {
|
||||
let hostids = _.map(hosts, 'hostid');
|
||||
@@ -342,9 +345,13 @@ export class ZabbixDatasource {
|
||||
timeFrom: timeFrom,
|
||||
timeTo: timeTo
|
||||
};
|
||||
return this.zabbix.getHostAlerts(hostids, appids, options)
|
||||
.then((triggers) => {
|
||||
return responseHandler.handleTriggersResponse(triggers, timeRange);
|
||||
const groupFilter = target.group.filter;
|
||||
return Promise.all([
|
||||
this.zabbix.getHostAlerts(hostids, appids, options),
|
||||
this.zabbix.getGroups(groupFilter)
|
||||
])
|
||||
.then(([triggers, groups]) => {
|
||||
return responseHandler.handleTriggersResponse(triggers, groups, timeRange);
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve([]);
|
||||
|
||||
@@ -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({
|
||||
name: 'percentil',
|
||||
name: 'percentile',
|
||||
category: 'Aggregate',
|
||||
params: [
|
||||
{ name: 'interval', type: 'string' },
|
||||
|
||||
@@ -33,6 +33,7 @@ export function migrate(target) {
|
||||
if (isGrafana2target(target)) {
|
||||
return migrateFrom2To3version(target);
|
||||
}
|
||||
migratePercentileAgg(target);
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -51,6 +52,16 @@ function convertToRegex(str) {
|
||||
}
|
||||
}
|
||||
|
||||
function migratePercentileAgg(target) {
|
||||
if (target.functions) {
|
||||
for (const f of target.functions) {
|
||||
if (f.def && f.def.name === 'percentil') {
|
||||
f.def.name = 'percentile';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DS_CONFIG_SCHEMA = 2;
|
||||
export function migrateDSConfig(jsonData) {
|
||||
if (!jsonData) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<div class="gf-form-group">
|
||||
<h6>Filter Triggers</h6>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Group</span>
|
||||
@@ -36,8 +35,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h6>Options</h6>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Minimum severity</span>
|
||||
<span class="gf-form-label width-12">Minimum severity</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto"
|
||||
ng-init='ctrl.annotation.minseverity = ctrl.annotation.minseverity || 0'
|
||||
@@ -54,12 +54,16 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h6>Options</h6>
|
||||
<div class="gf-form">
|
||||
<editor-checkbox text="Show OK events" model="ctrl.annotation.showOkEvents"></editor-checkbox>
|
||||
<editor-checkbox text="Hide acknowledged events" model="ctrl.annotation.hideAcknowledged"></editor-checkbox>
|
||||
<editor-checkbox text="Show hostname" model="ctrl.annotation.showHostname"></editor-checkbox>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Show OK events"
|
||||
checked="ctrl.annotation.showOkEvents">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Hide acknowledged events"
|
||||
checked="ctrl.annotation.hideAcknowledged">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-12"
|
||||
label="Show hostname"
|
||||
checked="ctrl.annotation.showHostname">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
@@ -99,12 +99,14 @@
|
||||
In order to use this feature it should be <a href="/datasources/new" target="_blank">created</a> and
|
||||
configured first. Zabbix plugin uses this data source for querying history data directly from the database.
|
||||
This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
|
||||
amount of data transfered.
|
||||
amount of data transferred.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-16">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnectionDatasourceId"
|
||||
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.dbConnectionDatasourceId"
|
||||
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources"
|
||||
ng-change="ctrl.onDBConnectionDatasourceChange()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -201,10 +201,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Metric processing functions -->
|
||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID">
|
||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID || ctrl.target.mode == editorMode.ITSERVICE">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Functions</label>
|
||||
<div ng-repeat="func in ctrl.target.functions" class="gf-form-label query-part" metric-function-editor></div>
|
||||
</div>
|
||||
<div ng-repeat="func in ctrl.target.functions" class="gf-form">
|
||||
<span zabbix-function-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
|
||||
</div>
|
||||
<div class="gf-form dropdown" add-metric-function>
|
||||
</div>
|
||||
|
||||
@@ -275,11 +275,15 @@ export class ZabbixQueryController extends QueryCtrl {
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
moveFunction(func, offset) {
|
||||
const index = this.target.functions.indexOf(func);
|
||||
_.move(this.target.functions, index, index + offset);
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
moveAliasFuncLast() {
|
||||
var aliasFunc = _.find(this.target.functions, function(func) {
|
||||
return func.def.name === 'alias' ||
|
||||
func.def.name === 'aliasByNode' ||
|
||||
func.def.name === 'aliasByMetric';
|
||||
var aliasFunc = _.find(this.target.functions, func => {
|
||||
return func.def.category === 'Alias';
|
||||
});
|
||||
|
||||
if (aliasFunc) {
|
||||
|
||||
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) {
|
||||
var targetSLA = slaObject[itservice.serviceid].sla[0];
|
||||
var targetSLA = slaObject[itservice.serviceid].sla;
|
||||
if (slaProperty.property === 'status') {
|
||||
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
|
||||
return {
|
||||
target: itservice.name + ' ' + slaProperty.name,
|
||||
datapoints: [
|
||||
[targetStatus, targetSLA.to * 1000]
|
||||
[targetStatus, targetSLA[0].to * 1000]
|
||||
]
|
||||
};
|
||||
} else {
|
||||
let i;
|
||||
let slaArr = [];
|
||||
for (i = 0; i < targetSLA.length; i++) {
|
||||
if (i === 0) {
|
||||
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]);
|
||||
}
|
||||
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].to * 1000]);
|
||||
}
|
||||
return {
|
||||
target: itservice.name + ' ' + slaProperty.name,
|
||||
datapoints: [
|
||||
[targetSLA[slaProperty.property], targetSLA.from * 1000],
|
||||
[targetSLA[slaProperty.property], targetSLA.to * 1000]
|
||||
]
|
||||
datapoints: slaArr
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function handleTriggersResponse(triggers, timeRange) {
|
||||
if (_.isNumber(triggers)) {
|
||||
function handleTriggersResponse(triggers, groups, timeRange) {
|
||||
if (!_.isArray(triggers)) {
|
||||
let triggersCount = null;
|
||||
try {
|
||||
triggersCount = Number(triggers);
|
||||
} catch (err) {
|
||||
console.log("Error when handling triggers count: ", err);
|
||||
}
|
||||
return {
|
||||
target: "triggers count",
|
||||
datapoints: [
|
||||
[triggers, timeRange[1] * 1000]
|
||||
[triggersCount, timeRange[1] * 1000]
|
||||
]
|
||||
};
|
||||
} else {
|
||||
let stats = getTriggerStats(triggers);
|
||||
const stats = getTriggerStats(triggers);
|
||||
const groupNames = _.map(groups, 'name');
|
||||
let table = new TableModel();
|
||||
table.addColumn({text: 'Host group'});
|
||||
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
|
||||
table.addColumn({text: severity.text});
|
||||
});
|
||||
_.each(stats, (severity_stats, group) => {
|
||||
if (_.includes(groupNames, group)) {
|
||||
let row = _.map(_.orderBy(_.toPairs(severity_stats), (s) => s[0], ['desc']), (s) => s[1]);
|
||||
row = _.concat([group], ...row);
|
||||
table.rows.push(row);
|
||||
}
|
||||
});
|
||||
return table;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
import mocks from '../../test-setup/mocks';
|
||||
import { Datasource } from "../module";
|
||||
import { zabbixTemplateFormat } from "../datasource";
|
||||
import { dateMath } from '@grafana/data';
|
||||
|
||||
describe('ZabbixDatasource', () => {
|
||||
let ctx = {};
|
||||
@@ -41,7 +42,10 @@ describe('ZabbixDatasource', () => {
|
||||
item: {filter: ""}
|
||||
}
|
||||
],
|
||||
range: {from: 'now-7d', to: 'now'}
|
||||
range: {
|
||||
from: dateMath.parse('now-1h'),
|
||||
to: dateMath.parse('now')
|
||||
}
|
||||
};
|
||||
|
||||
it('should return an empty array when no targets are set', (done) => {
|
||||
@@ -59,7 +63,7 @@ describe('ZabbixDatasource', () => {
|
||||
let ranges = ['now-8d', 'now-169h', 'now-1M', 'now-1y'];
|
||||
|
||||
_.forEach(ranges, range => {
|
||||
ctx.options.range.from = range;
|
||||
ctx.options.range.from = dateMath.parse(range);
|
||||
ctx.ds.queryNumericData = jest.fn();
|
||||
ctx.ds.query(ctx.options);
|
||||
|
||||
@@ -76,7 +80,7 @@ describe('ZabbixDatasource', () => {
|
||||
let ranges = ['now-7d', 'now-168h', 'now-1h', 'now-30m', 'now-30s'];
|
||||
|
||||
_.forEach(ranges, range => {
|
||||
ctx.options.range.from = range;
|
||||
ctx.options.range.from = dateMath.parse(range);
|
||||
ctx.ds.queryNumericData = jest.fn();
|
||||
ctx.ds.query(ctx.options);
|
||||
|
||||
@@ -108,10 +112,7 @@ describe('ZabbixDatasource', () => {
|
||||
}
|
||||
]));
|
||||
|
||||
ctx.options = {
|
||||
range: {from: 'now-1h', to: 'now'},
|
||||
targets: [
|
||||
{
|
||||
ctx.options.targets = [{
|
||||
group: {filter: ""},
|
||||
host: {filter: "Zabbix server"},
|
||||
application: {filter: ""},
|
||||
@@ -123,9 +124,7 @@ describe('ZabbixDatasource', () => {
|
||||
options: {
|
||||
skipEmptyValues: false
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
}];
|
||||
});
|
||||
|
||||
it('should return data in table format', (done) => {
|
||||
|
||||
@@ -138,4 +138,31 @@ describe('Utils', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArrayDepth()', () => {
|
||||
it('should calculate proper array depth', () => {
|
||||
const test_cases = [
|
||||
{
|
||||
array: [],
|
||||
depth: 1
|
||||
},
|
||||
{
|
||||
array: [1, 2, 3],
|
||||
depth: 1
|
||||
},
|
||||
{
|
||||
array: [[1, 2], [3, 4]],
|
||||
depth: 2
|
||||
},
|
||||
{
|
||||
array: [[[1, 2], [3, 4]], [[1, 2], [3, 4]]],
|
||||
depth: 3
|
||||
},
|
||||
];
|
||||
|
||||
for (const test_case of test_cases) {
|
||||
expect(utils.getArrayDepth(test_case.array)).toBe(test_case.depth);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
import * as utils from './utils';
|
||||
import * as c from './constants';
|
||||
|
||||
const POINT_VALUE = 0;
|
||||
const POINT_TIMESTAMP = 1;
|
||||
@@ -94,11 +95,15 @@ function groupBy(datapoints, interval, groupByCallback) {
|
||||
}));
|
||||
}
|
||||
|
||||
function groupBy_perf(datapoints, interval, groupByCallback) {
|
||||
export function groupBy_perf(datapoints, interval, groupByCallback) {
|
||||
if (datapoints.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (interval === c.RANGE_VARIABLE_VALUE) {
|
||||
return groupByRange(datapoints, groupByCallback);
|
||||
}
|
||||
|
||||
let ms_interval = utils.parseInterval(interval);
|
||||
let grouped_series = [];
|
||||
let frame_values = [];
|
||||
@@ -132,6 +137,19 @@ function groupBy_perf(datapoints, interval, groupByCallback) {
|
||||
return grouped_series;
|
||||
}
|
||||
|
||||
export function groupByRange(datapoints, groupByCallback) {
|
||||
const frame_values = [];
|
||||
const frame_start = datapoints[0][POINT_TIMESTAMP];
|
||||
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
|
||||
let point;
|
||||
for (let i=0; i < datapoints.length; i++) {
|
||||
point = datapoints[i];
|
||||
frame_values.push(point[POINT_VALUE]);
|
||||
}
|
||||
const frame_value = groupByCallback(frame_values);
|
||||
return [[frame_value, frame_start], [frame_value, frame_end]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize set of time series into one.
|
||||
* @param {datapoints[]} timeseries array of time series
|
||||
@@ -333,7 +351,7 @@ function expMovingAverage(datapoints, n) {
|
||||
return ema;
|
||||
}
|
||||
|
||||
function PERCENTIL(n, values) {
|
||||
function PERCENTILE(n, values) {
|
||||
var sorted = _.sortBy(values);
|
||||
return sorted[Math.floor(sorted.length * n / 100)];
|
||||
}
|
||||
@@ -478,6 +496,15 @@ function findNearestLeft(series, pointIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function flattenDatapoints(datapoints) {
|
||||
const depth = utils.getArrayDepth(datapoints);
|
||||
if (depth <= 2) {
|
||||
// Don't process if datapoints already flattened
|
||||
return datapoints;
|
||||
}
|
||||
return _.flatten(datapoints);
|
||||
}
|
||||
|
||||
////////////
|
||||
// Export //
|
||||
////////////
|
||||
@@ -486,6 +513,7 @@ const exportedFunctions = {
|
||||
downsample,
|
||||
groupBy,
|
||||
groupBy_perf,
|
||||
groupByRange,
|
||||
sumSeries,
|
||||
scale,
|
||||
offset,
|
||||
@@ -500,8 +528,9 @@ const exportedFunctions = {
|
||||
MIN,
|
||||
MAX,
|
||||
MEDIAN,
|
||||
PERCENTIL,
|
||||
sortByTime
|
||||
PERCENTILE,
|
||||
sortByTime,
|
||||
flattenDatapoints,
|
||||
};
|
||||
|
||||
export default exportedFunctions;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import * as c from './constants';
|
||||
|
||||
/**
|
||||
* Expand Zabbix item name
|
||||
@@ -141,6 +143,18 @@ export function isTemplateVariable(str, templateVariables) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getRangeScopedVars(range) {
|
||||
const msRange = range.to.diff(range.from);
|
||||
const sRange = Math.round(msRange / 1000);
|
||||
const regularRange = kbn.secondsToHms(msRange / 1000);
|
||||
return {
|
||||
__range_ms: { text: msRange, value: msRange },
|
||||
__range_s: { text: sRange, value: sRange },
|
||||
__range: { text: regularRange, value: regularRange },
|
||||
__range_series: {text: c.RANGE_VARIABLE_VALUE, value: c.RANGE_VARIABLE_VALUE},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRegex(str) {
|
||||
var matches = str.match(regexPattern);
|
||||
var pattern = matches[1];
|
||||
@@ -265,6 +279,17 @@ export function compactQuery(query) {
|
||||
return query.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
export function getArrayDepth(a, level = 0) {
|
||||
if (a.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
const elem = a[0];
|
||||
if (_.isArray(elem)) {
|
||||
return getArrayDepth(elem, level + 1);
|
||||
}
|
||||
return level + 1;
|
||||
}
|
||||
|
||||
// Fix for backward compatibility with lodash 2.4
|
||||
if (!_.includes) {
|
||||
_.includes = _.contains;
|
||||
|
||||
@@ -65,6 +65,9 @@ export class DBConnector {
|
||||
if (!this.datasourceName) {
|
||||
this.datasourceName = ds.name;
|
||||
}
|
||||
if (!this.datasourceId) {
|
||||
this.datasourceId = ds.id;
|
||||
}
|
||||
return ds;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import kbn from 'grafana/app/core/utils/kbn';
|
||||
import * as utils from '../../../utils';
|
||||
import { ZabbixAPICore } from './zabbixAPICore';
|
||||
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE } from '../../../constants';
|
||||
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Zabbix API Wrapper.
|
||||
@@ -316,14 +317,11 @@ export class ZabbixAPIConnector {
|
||||
return this.request('service.get', params);
|
||||
}
|
||||
|
||||
getSLA(serviceids, timeRange) {
|
||||
let [timeFrom, timeTo] = timeRange;
|
||||
var params = {
|
||||
serviceids: serviceids,
|
||||
intervals: [{
|
||||
from: timeFrom,
|
||||
to: timeTo
|
||||
}]
|
||||
getSLA(serviceids, timeRange, options) {
|
||||
const intervals = buildSLAIntervals(timeRange, options.intervalMs);
|
||||
const params = {
|
||||
serviceids,
|
||||
intervals
|
||||
};
|
||||
return this.request('service.getsla', params);
|
||||
}
|
||||
@@ -421,7 +419,12 @@ export class ZabbixAPIConnector {
|
||||
getEventAlerts(eventids) {
|
||||
const params = {
|
||||
eventids: eventids,
|
||||
output: 'extend',
|
||||
output: [
|
||||
'eventid',
|
||||
'message',
|
||||
'clock',
|
||||
'error'
|
||||
],
|
||||
selectUsers: true,
|
||||
};
|
||||
|
||||
@@ -520,3 +523,30 @@ function isNotAuthorized(message) {
|
||||
message === "Not authorized."
|
||||
);
|
||||
}
|
||||
|
||||
function getSLAInterval(intervalMs) {
|
||||
// Too many intervals may cause significant load on the database, so decrease number of resulting points
|
||||
const resolutionRatio = 100;
|
||||
const interval = kbn.round_interval(intervalMs * resolutionRatio) / 1000;
|
||||
return Math.max(interval, MIN_SLA_INTERVAL);
|
||||
}
|
||||
|
||||
function buildSLAIntervals(timeRange, intervalMs) {
|
||||
let [timeFrom, timeTo] = timeRange;
|
||||
const slaInterval = getSLAInterval(intervalMs);
|
||||
const intervals = [];
|
||||
|
||||
// Align time range with calculated interval
|
||||
timeFrom = Math.floor(timeFrom / slaInterval) * slaInterval;
|
||||
timeTo = Math.ceil(timeTo / slaInterval) * slaInterval;
|
||||
|
||||
for (let i = timeFrom; i <= timeTo - slaInterval; i += slaInterval) {
|
||||
intervals.push({
|
||||
from : i,
|
||||
to : (i + slaInterval)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return intervals;
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export class Zabbix {
|
||||
itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
|
||||
}
|
||||
let itServiceIds = _.map(itServices, 'serviceid');
|
||||
return this.zabbixAPI.getSLA(itServiceIds, timeRange)
|
||||
return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
|
||||
.then(slaResponse => {
|
||||
return _.map(itServiceIds, serviceid => {
|
||||
let itservice = _.find(itServices, {'serviceid': serviceid});
|
||||
|
||||
@@ -10,7 +10,7 @@ class ZabbixAlertingService {
|
||||
}
|
||||
|
||||
isFullScreen() {
|
||||
return this.dashboardSrv.dash.meta.fullscreen;
|
||||
return this.getDashboardModel().meta.fullscreen;
|
||||
}
|
||||
|
||||
setPanelAlertState(panelId, alertState) {
|
||||
@@ -30,26 +30,24 @@ class ZabbixAlertingService {
|
||||
});
|
||||
}
|
||||
|
||||
if (panelIndex >= 0) {
|
||||
// Don't apply alert styles to .panel-container--absolute (it rewrites position from absolute to relative)
|
||||
if (panelIndex >= 0 && !panelContainers[panelIndex].className.includes('panel-container--absolute')) {
|
||||
let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
|
||||
$(panelContainers[panelIndex]).removeClass(alertClass);
|
||||
|
||||
if (alertState) {
|
||||
if (alertState === 'alerting') {
|
||||
alertClass = "panel-has-alert panel-alert-state--" + alertState;
|
||||
$(panelContainers[panelIndex]).addClass(alertClass);
|
||||
}
|
||||
if (alertState === 'ok') {
|
||||
alertClass = "panel-alert-state--" + alertState;
|
||||
$(panelContainers[panelIndex]).addClass(alertClass);
|
||||
$(panelContainers[panelIndex]).removeClass("panel-has-alert");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDashboardModel() {
|
||||
return this.dashboardSrv.dash || this.dashboardSrv.dashboard;
|
||||
}
|
||||
|
||||
getPanelModels() {
|
||||
return _.filter(this.dashboardSrv.dash.panels, panel => panel.type !== 'row');
|
||||
return _.filter(this.getDashboardModel().panels, panel => panel.type !== 'row');
|
||||
}
|
||||
|
||||
getPanelModel(panelId) {
|
||||
|
||||
@@ -36,11 +36,13 @@ export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesPr
|
||||
{ackRows}
|
||||
</tbody>
|
||||
</table>
|
||||
{problem.showAckButton &&
|
||||
<div className="ack-add-button">
|
||||
<button id="add-acknowledge-btn" className="btn btn-mini btn-inverse gf-form-button" onClick={this.handleClick}>
|
||||
<i className="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import AlertIcon from './AlertIcon';
|
||||
interface AlertCardProps {
|
||||
problem: ZBXTrigger;
|
||||
panelOptions: ProblemsPanelOptions;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string) => void;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
||||
this.state = { showAckDialog: false };
|
||||
}
|
||||
|
||||
handleTagClick = (tag: ZBXTag) => {
|
||||
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, this.props.problem.datasource);
|
||||
this.props.onTagClick(tag, this.props.problem.datasource, ctrlKey, shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,11 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
||||
}
|
||||
|
||||
showAckDialog = () => {
|
||||
const problem = this.props.problem;
|
||||
if (problem.showAckButton) {
|
||||
this.setState({ showAckDialog: true });
|
||||
}
|
||||
}
|
||||
|
||||
closeAckDialog = () => {
|
||||
this.setState({ showAckDialog: false });
|
||||
@@ -53,6 +56,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
||||
|
||||
render() {
|
||||
const { problem, panelOptions } = this.props;
|
||||
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
|
||||
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground });
|
||||
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine });
|
||||
const severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority));
|
||||
@@ -120,9 +124,9 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
||||
</div>
|
||||
|
||||
{panelOptions.descriptionField && panelOptions.descriptionAtNewLine && (
|
||||
<div className="alert-rule-item__text" >
|
||||
<div className="alert-rule-item__text zbx-description--newline" >
|
||||
<span
|
||||
className="alert-rule-item__info zbx-description zbx-description--newline"
|
||||
className="alert-rule-item__info zbx-description"
|
||||
dangerouslySetInnerHTML={{ __html: problem.comments }}
|
||||
/>
|
||||
</div>
|
||||
@@ -131,7 +135,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{panelOptions.datasources.length > 1 && (
|
||||
{showDatasourceName && (
|
||||
<div className="alert-rule-item__time zabbix-trigger-source">
|
||||
<span>
|
||||
<i className="fa fa-database"></i>
|
||||
@@ -258,14 +262,20 @@ class AlertAcknowledgesButton extends PureComponent<AlertAcknowledgesButtonProps
|
||||
|
||||
render() {
|
||||
const { problem } = this.props;
|
||||
return (
|
||||
problem.acknowledges && problem.acknowledges.length ?
|
||||
let content = null;
|
||||
if (problem.acknowledges && problem.acknowledges.length) {
|
||||
content = (
|
||||
<Tooltip placement="bottom" popperClassName="ack-tooltip" content={this.renderTooltipContent}>
|
||||
<span><i className="fa fa-comments"></i></span>
|
||||
</Tooltip> :
|
||||
</Tooltip>
|
||||
);
|
||||
} else if (problem.showAckButton) {
|
||||
content = (
|
||||
<Tooltip placement="bottom" content="Acknowledge problem">
|
||||
<span role="button" onClick={this.handleClick}><i className="fa fa-comments-o"></i></span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,8 @@ export default function AlertIcon(props: AlertIconProps) {
|
||||
const { problem, color, blink, highlightBackground } = props;
|
||||
const priority = Number(problem.priority);
|
||||
let iconClass = '';
|
||||
if (problem.value === '1') {
|
||||
if (priority >= 3) {
|
||||
if (problem.value === '1' && priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
} else {
|
||||
iconClass = 'icon-gf-warning';
|
||||
}
|
||||
} else {
|
||||
iconClass = 'icon-gf-online';
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface AlertListProps {
|
||||
pageSize?: number;
|
||||
fontSize?: number;
|
||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string) => void;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
}
|
||||
|
||||
interface AlertListState {
|
||||
@@ -45,9 +45,9 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
|
||||
}
|
||||
|
||||
|
||||
handleTagClick = (tag: ZBXTag, datasource: string) => {
|
||||
handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, datasource);
|
||||
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
|
||||
<ol className={alertListClass}>
|
||||
{currentProblems.map(problem =>
|
||||
<AlertCard
|
||||
key={problem.triggerid}
|
||||
key={`${problem.triggerid}-${problem.datasource}`}
|
||||
problem={problem}
|
||||
panelOptions={panelOptions}
|
||||
onTagClick={this.handleTagClick}
|
||||
|
||||
@@ -9,8 +9,7 @@ interface GFHeartIconProps {
|
||||
export default function GFHeartIcon(props: GFHeartIconProps) {
|
||||
const status = props.status;
|
||||
const className = classNames("icon-gf", props.className,
|
||||
{ "icon-gf-critical": status === 'critical' || status === 'problem' },
|
||||
{ "icon-gf-warning": status === 'warning' },
|
||||
{ "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'},
|
||||
{ "icon-gf-online": status === 'online' || status === 'ok' },
|
||||
);
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import moment from 'moment';
|
||||
import * as utils from '../../../datasource-zabbix/utils';
|
||||
import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag } from '../../types';
|
||||
import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag, ZBXAlert } from '../../types';
|
||||
import { Modal, AckProblemData } from '../Modal';
|
||||
import EventTag from '../EventTag';
|
||||
import Tooltip from '../Tooltip/Tooltip';
|
||||
@@ -15,12 +15,14 @@ interface ProblemDetailsProps extends RTRow<ZBXTrigger> {
|
||||
timeRange: GFTimeRange;
|
||||
showTimeline?: boolean;
|
||||
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
|
||||
getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
|
||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
}
|
||||
|
||||
interface ProblemDetailsState {
|
||||
events: ZBXEvent[];
|
||||
alerts: ZBXAlert[];
|
||||
show: boolean;
|
||||
showAckDialog: boolean;
|
||||
}
|
||||
@@ -30,6 +32,7 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
||||
super(props);
|
||||
this.state = {
|
||||
events: [],
|
||||
alerts: [],
|
||||
show: false,
|
||||
showAckDialog: false,
|
||||
};
|
||||
@@ -39,6 +42,7 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
||||
if (this.props.showTimeline) {
|
||||
this.fetchProblemEvents();
|
||||
}
|
||||
this.fetchProblemAlerts();
|
||||
requestAnimationFrame(() => {
|
||||
this.setState({ show: true });
|
||||
});
|
||||
@@ -58,6 +62,14 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
||||
});
|
||||
}
|
||||
|
||||
fetchProblemAlerts() {
|
||||
const problem = this.props.original;
|
||||
this.props.getProblemAlerts(problem)
|
||||
.then(alerts => {
|
||||
this.setState({ alerts });
|
||||
});
|
||||
}
|
||||
|
||||
ackProblem = (data: AckProblemData) => {
|
||||
const problem = this.props.original as ZBXTrigger;
|
||||
return this.props.onProblemAck(problem, data).then(result => {
|
||||
@@ -78,6 +90,7 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
||||
|
||||
render() {
|
||||
const problem = this.props.original as ZBXTrigger;
|
||||
const alerts = this.state.alerts;
|
||||
const rootWidth = this.props.rootWidth;
|
||||
const displayClass = this.state.show ? 'show' : '';
|
||||
const wideLayout = rootWidth > 1200;
|
||||
@@ -96,13 +109,15 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
||||
</div>
|
||||
{problem.items && <ProblemItems items={problem.items} />}
|
||||
</div>
|
||||
<ProblemStatusBar problem={problem} className={compactStatusBar && 'compact'} />
|
||||
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
|
||||
{problem.showAckButton &&
|
||||
<div className="problem-actions">
|
||||
<ProblemActionButton className="navbar-button navbar-button--settings"
|
||||
icon="reply-all"
|
||||
tooltip="Acknowledge problem"
|
||||
onClick={this.showAckDialog} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{problem.comments &&
|
||||
<div className="problem-description">
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import FAIcon from '../FAIcon';
|
||||
import Tooltip from '../Tooltip/Tooltip';
|
||||
import { ZBXTrigger } from '../../types';
|
||||
import { ZBXTrigger, ZBXAlert } from '../../types';
|
||||
|
||||
export interface ProblemStatusBarProps {
|
||||
problem: ZBXTrigger;
|
||||
alerts?: ZBXAlert[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProblemStatusBar(props: ProblemStatusBarProps) {
|
||||
const { problem, className } = props;
|
||||
const { problem, alerts, className } = props;
|
||||
const multiEvent = problem.type === '1';
|
||||
const link = problem.url && problem.url !== '';
|
||||
const maintenance = problem.maintenance;
|
||||
@@ -17,8 +18,8 @@ export default function ProblemStatusBar(props: ProblemStatusBarProps) {
|
||||
const error = problem.error && problem.error !== '';
|
||||
const stateUnknown = problem.state === '1';
|
||||
const closeByTag = problem.correlation_mode === '1';
|
||||
const actions = problem.alerts && problem.alerts.length !== 0;
|
||||
const actionMessage = problem.alerts ? problem.alerts[0].message : '';
|
||||
const actions = alerts && alerts.length !== 0;
|
||||
const actionMessage = actions ? alerts[0].message : '';
|
||||
|
||||
return (
|
||||
<div className={`problem-statusbar ${className || ''}`}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import * as utils from '../../../datasource-zabbix/utils';
|
||||
import { isNewProblem } from '../../utils';
|
||||
import { ProblemsPanelOptions, ZBXTrigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized } from '../../types';
|
||||
import { ProblemsPanelOptions, ZBXTrigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized, ZBXAlert } from '../../types';
|
||||
import EventTag from '../EventTag';
|
||||
import ProblemDetails from './ProblemDetails';
|
||||
import { AckProblemData } from '../Modal';
|
||||
@@ -18,7 +18,8 @@ export interface ProblemListProps {
|
||||
timeRange?: GFTimeRange;
|
||||
pageSize?: number;
|
||||
fontSize?: number;
|
||||
getProblemEvents: (ids: string[]) => ZBXEvent[];
|
||||
getProblemEvents: (problem: ZBXTrigger) => ZBXEvent[];
|
||||
getProblemAlerts: (problem: ZBXTrigger) => ZBXAlert[];
|
||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
|
||||
@@ -159,6 +160,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
timeRange={this.props.timeRange}
|
||||
showTimeline={panelOptions.problemTimeline}
|
||||
getProblemEvents={this.props.getProblemEvents}
|
||||
getProblemAlerts={this.props.getProblemAlerts}
|
||||
onProblemAck={this.handleProblemAck}
|
||||
onTagClick={this.handleTagClick}
|
||||
/>
|
||||
@@ -258,13 +260,13 @@ function LastChangeCell(props: RTCell<ZBXTrigger>, customFormat?: string) {
|
||||
}
|
||||
|
||||
interface TagCellProps extends RTCell<ZBXTrigger> {
|
||||
onTagClick: (tag: ZBXTag, datasource: string) => void;
|
||||
onTagClick: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||
}
|
||||
|
||||
class TagCell extends PureComponent<TagCellProps> {
|
||||
handleTagClick = (tag: ZBXTag) => {
|
||||
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, this.props.original.datasource);
|
||||
this.props.onTagClick(tag, this.props.original.datasource, ctrlKey, shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { getNextRefIdChar } from './utils';
|
||||
import { getDefaultTarget } from './triggers_panel_ctrl';
|
||||
|
||||
// Actual schema version
|
||||
export const CURRENT_SCHEMA_VERSION = 6;
|
||||
export const CURRENT_SCHEMA_VERSION = 7;
|
||||
|
||||
export function migratePanelSchema(panel) {
|
||||
if (isEmptyPanel(panel)) {
|
||||
@@ -45,6 +46,26 @@ export function migratePanelSchema(panel) {
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaVersion < 7) {
|
||||
const updatedTargets = [];
|
||||
for (const targetKey in panel.targets) {
|
||||
const target = panel.targets[targetKey];
|
||||
if (!isEmptyTarget(target) && !isInvalidTarget(target, targetKey)) {
|
||||
updatedTargets.push({
|
||||
...target,
|
||||
datasource: targetKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const target of updatedTargets) {
|
||||
if (!target.refId) {
|
||||
target.refId = getNextRefIdChar(updatedTargets);
|
||||
}
|
||||
}
|
||||
panel.targets = updatedTargets;
|
||||
delete panel.datasources;
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -59,3 +80,11 @@ function isEmptyPanel(panel) {
|
||||
function isEmptyTargets(targets) {
|
||||
return !targets || (_.isArray(targets) && (targets.length === 0 || targets.length === 1 && _.isEmpty(targets[0])));
|
||||
}
|
||||
|
||||
function isEmptyTarget(target) {
|
||||
return !target || !(target.group && target.host && target.application && target.trigger);
|
||||
}
|
||||
|
||||
function isInvalidTarget(target, targetKey) {
|
||||
return target && target.refId === 'A' && targetKey === '0';
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<datasource-selector
|
||||
datasources="ctrl.panel.datasources"
|
||||
datasources="editor.selectedDatasources"
|
||||
options="editor.panelCtrl.available_datasources"
|
||||
on-change="editor.datasourcesChanged()">
|
||||
</datasource-selector>
|
||||
@@ -15,50 +15,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row" ng-repeat="ds in ctrl.panel.datasources">
|
||||
<div class="editor-row" ng-repeat="target in ctrl.panel.targets">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">{{ ds }}</h5>
|
||||
<h5 class="section-heading">{{ target.datasource }}</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Group</label>
|
||||
<input type="text"
|
||||
ng-model="ctrl.panel.targets[ds].group.filter"
|
||||
bs-typeahead="editor.getGroupNames[ds]"
|
||||
ng-model="target.group.filter"
|
||||
bs-typeahead="editor.getGroupNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].group.filter),
|
||||
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].group.filter)
|
||||
'zbx-variable': editor.isVariable(target.group.filter),
|
||||
'zbx-regex': editor.isRegex(target.group.filter)
|
||||
}">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Host</label>
|
||||
<input type="text"
|
||||
ng-model="ctrl.panel.targets[ds].host.filter"
|
||||
bs-typeahead="editor.getHostNames[ds]"
|
||||
ng-model="target.host.filter"
|
||||
bs-typeahead="editor.getHostNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].host.filter),
|
||||
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].host.filter)
|
||||
'zbx-variable': editor.isVariable(target.host.filter),
|
||||
'zbx-regex': editor.isRegex(target.host.filter)
|
||||
}">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Proxy</label>
|
||||
<input type="text"
|
||||
ng-model="ctrl.panel.targets[ds].proxy.filter"
|
||||
bs-typeahead="editor.getProxyNames[ds]"
|
||||
ng-model="target.proxy.filter"
|
||||
bs-typeahead="editor.getProxyNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].proxy.filter),
|
||||
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].proxy.filter)
|
||||
'zbx-variable': editor.isVariable(target.proxy.filter),
|
||||
'zbx-regex': editor.isRegex(target.proxy.filter)
|
||||
}">
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,35 +67,35 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Application</label>
|
||||
<input type="text"
|
||||
ng-model="ctrl.panel.targets[ds].application.filter"
|
||||
bs-typeahead="editor.getApplicationNames[ds]"
|
||||
ng-model="target.application.filter"
|
||||
bs-typeahead="editor.getApplicationNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].application.filter),
|
||||
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].application.filter)
|
||||
'zbx-variable': editor.isVariable(target.application.filter),
|
||||
'zbx-regex': editor.isRegex(target.application.filter)
|
||||
}">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Trigger</label>
|
||||
<input type="text"
|
||||
ng-model="ctrl.panel.targets[ds].trigger.filter"
|
||||
ng-model="target.trigger.filter"
|
||||
ng-blur="editor.parseTarget()"
|
||||
placeholder="trigger name"
|
||||
class="gf-form-input width-14"
|
||||
ng-style="ctrl.panel.targets[ds].trigger.style"
|
||||
ng-style="target.trigger.style"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(ctrl.panel.targets[ds].trigger.filter),
|
||||
'zbx-regex': editor.isRegex(ctrl.panel.targets[ds].trigger.filter)
|
||||
'zbx-variable': editor.isVariable(target.trigger.filter),
|
||||
'zbx-regex': editor.isRegex(target.trigger.filter)
|
||||
}"
|
||||
empty-to-null>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Tags</label>
|
||||
<input type="text" class="gf-form-input width-14"
|
||||
ng-model="ctrl.panel.targets[ds].tags.filter"
|
||||
ng-model="target.tags.filter"
|
||||
ng-blur="editor.parseTarget()"
|
||||
placeholder="tag1:value1, tag2:value2">
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"id": "alexanderzobnin-zabbix-triggers-panel",
|
||||
|
||||
"dataFormats": [],
|
||||
"skipDataQuery": true,
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
|
||||
@@ -5,16 +5,16 @@ import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_pane
|
||||
import {CURRENT_SCHEMA_VERSION} from '../migrations';
|
||||
|
||||
describe('Triggers Panel schema migration', () => {
|
||||
let ctx = {};
|
||||
let ctx: any = {};
|
||||
let updatePanelCtrl;
|
||||
let datasourceSrvMock = {
|
||||
const datasourceSrvMock = {
|
||||
getMetricSources: () => {
|
||||
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
|
||||
},
|
||||
get: () => Promise.resolve({})
|
||||
};
|
||||
|
||||
let timeoutMock = () => {};
|
||||
const timeoutMock = () => {};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = {
|
||||
@@ -47,14 +47,16 @@ describe('Triggers Panel schema migration', () => {
|
||||
});
|
||||
|
||||
it('should update old panel schema', () => {
|
||||
let updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
|
||||
let expected = _.defaultsDeep({
|
||||
const expected = _.defaultsDeep({
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
datasources: ['zabbix'],
|
||||
targets: {
|
||||
'zabbix': DEFAULT_TARGET
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix',
|
||||
}
|
||||
],
|
||||
ageField: true,
|
||||
statusField: false,
|
||||
severityField: false,
|
||||
@@ -68,29 +70,29 @@ describe('Triggers Panel schema migration', () => {
|
||||
|
||||
it('should create new panel with default schema', () => {
|
||||
ctx.scope.panel = {};
|
||||
let updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
|
||||
let expected = _.defaultsDeep({
|
||||
const expected = _.defaultsDeep({
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
datasources: ['zabbix_default'],
|
||||
targets: {
|
||||
'zabbix_default': DEFAULT_TARGET
|
||||
}
|
||||
targets: [{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix_default'
|
||||
}]
|
||||
}, PANEL_DEFAULTS);
|
||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should set default targets for new panel with empty targets', () => {
|
||||
ctx.scope.panel = {
|
||||
targets: [{}]
|
||||
targets: []
|
||||
};
|
||||
let updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
|
||||
let expected = _.defaultsDeep({
|
||||
datasources: ['zabbix_default'],
|
||||
targets: {
|
||||
'zabbix_default': DEFAULT_TARGET
|
||||
},
|
||||
const expected = _.defaultsDeep({
|
||||
targets: [{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix_default'
|
||||
}]
|
||||
}, PANEL_DEFAULTS);
|
||||
|
||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
||||
@@ -5,9 +5,9 @@ import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl';
|
||||
// import { create } from 'domain';
|
||||
|
||||
describe('TriggerPanelCtrl', () => {
|
||||
let ctx = {};
|
||||
let ctx: any = {};
|
||||
let datasourceSrvMock, zabbixDSMock;
|
||||
let timeoutMock = () => {};
|
||||
const timeoutMock = () => {};
|
||||
let createPanelCtrl;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -61,7 +61,7 @@ describe('TriggerPanelCtrl', () => {
|
||||
describe('When adding new panel', () => {
|
||||
it('should suggest all zabbix data sources', () => {
|
||||
ctx.scope.panel = {};
|
||||
let panelCtrl = createPanelCtrl();
|
||||
const panelCtrl = createPanelCtrl();
|
||||
expect(panelCtrl.available_datasources).toEqual([
|
||||
'zabbix_default', 'zabbix'
|
||||
]);
|
||||
@@ -69,10 +69,8 @@ describe('TriggerPanelCtrl', () => {
|
||||
|
||||
it('should load first zabbix data source as default', () => {
|
||||
ctx.scope.panel = {};
|
||||
let panelCtrl = createPanelCtrl();
|
||||
expect(panelCtrl.panel.datasources).toEqual([
|
||||
'zabbix_default'
|
||||
]);
|
||||
const panelCtrl = createPanelCtrl();
|
||||
expect(panelCtrl.panel.targets[0].datasource).toEqual('zabbix_default');
|
||||
});
|
||||
|
||||
it('should rewrite default empty target', () => {
|
||||
@@ -82,7 +80,7 @@ describe('TriggerPanelCtrl', () => {
|
||||
"refId": "A"
|
||||
}],
|
||||
};
|
||||
let panelCtrl = createPanelCtrl();
|
||||
const panelCtrl = createPanelCtrl();
|
||||
expect(panelCtrl.available_datasources).toEqual([
|
||||
'zabbix_default', 'zabbix'
|
||||
]);
|
||||
@@ -92,16 +90,22 @@ describe('TriggerPanelCtrl', () => {
|
||||
describe('When refreshing panel', () => {
|
||||
beforeEach(() => {
|
||||
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
|
||||
ctx.scope.panel.targets = {
|
||||
'zabbix_default': DEFAULT_TARGET,
|
||||
'zabbix': DEFAULT_TARGET
|
||||
};
|
||||
ctx.scope.panel.targets = [
|
||||
{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix_default'
|
||||
},
|
||||
{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix'
|
||||
},
|
||||
];
|
||||
ctx.panelCtrl = createPanelCtrl();
|
||||
});
|
||||
|
||||
it('should format triggers', (done) => {
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let formattedTrigger = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
|
||||
const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
|
||||
expect(formattedTrigger.host).toBe('backend01');
|
||||
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
|
||||
expect(formattedTrigger.datasource).toBe('zabbix_default');
|
||||
@@ -113,7 +117,7 @@ describe('TriggerPanelCtrl', () => {
|
||||
|
||||
it('should sort triggers by time by default', (done) => {
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
||||
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
||||
expect(trigger_ids).toEqual([
|
||||
'2', '4', '3', '1'
|
||||
]);
|
||||
@@ -124,7 +128,7 @@ describe('TriggerPanelCtrl', () => {
|
||||
it('should sort triggers by severity', (done) => {
|
||||
ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' };
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
||||
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
||||
expect(trigger_ids).toEqual([
|
||||
'1', '3', '2', '4'
|
||||
]);
|
||||
@@ -134,7 +138,7 @@ describe('TriggerPanelCtrl', () => {
|
||||
|
||||
it('should add acknowledges to trigger', (done) => {
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger = getTriggerById(1, ctx);
|
||||
const trigger = getTriggerById(1, ctx);
|
||||
expect(trigger.acknowledges).toHaveLength(1);
|
||||
expect(trigger.acknowledges[0].message).toBe("event ack");
|
||||
|
||||
@@ -153,15 +157,15 @@ describe('TriggerPanelCtrl', () => {
|
||||
|
||||
it('should handle new lines in trigger description', () => {
|
||||
ctx.panelCtrl.setTriggerSeverity = jest.fn((trigger) => trigger);
|
||||
let trigger = {comments: "this is\ndescription"};
|
||||
const trigger = {comments: "this is\ndescription"};
|
||||
const formattedTrigger = ctx.panelCtrl.formatTrigger(trigger);
|
||||
expect(formattedTrigger.comments).toBe("this is<br>description");
|
||||
});
|
||||
|
||||
it('should format host name to display (default)', (done) => {
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger = getTriggerById(1, ctx);
|
||||
let hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
const trigger = getTriggerById(1, ctx);
|
||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
expect(hostname).toBe('backend01');
|
||||
done();
|
||||
});
|
||||
@@ -171,8 +175,8 @@ describe('TriggerPanelCtrl', () => {
|
||||
ctx.panelCtrl.panel.hostField = false;
|
||||
ctx.panelCtrl.panel.hostTechNameField = true;
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger = getTriggerById(1, ctx);
|
||||
let hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
const trigger = getTriggerById(1, ctx);
|
||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
expect(hostname).toBe('backend01_tech');
|
||||
done();
|
||||
});
|
||||
@@ -182,8 +186,8 @@ describe('TriggerPanelCtrl', () => {
|
||||
ctx.panelCtrl.panel.hostField = true;
|
||||
ctx.panelCtrl.panel.hostTechNameField = true;
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger = getTriggerById(1, ctx);
|
||||
let hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
const trigger = getTriggerById(1, ctx);
|
||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
expect(hostname).toBe('backend01 (backend01_tech)');
|
||||
done();
|
||||
});
|
||||
@@ -193,8 +197,8 @@ describe('TriggerPanelCtrl', () => {
|
||||
ctx.panelCtrl.panel.hostField = false;
|
||||
ctx.panelCtrl.panel.hostTechNameField = false;
|
||||
ctx.panelCtrl.onRefresh().then(() => {
|
||||
let trigger = getTriggerById(1, ctx);
|
||||
let hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
const trigger = getTriggerById(1, ctx);
|
||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
||||
expect(hostname).toBe("");
|
||||
done();
|
||||
});
|
||||
@@ -222,7 +226,7 @@ describe('TriggerPanelCtrl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const defaultTrigger = {
|
||||
const defaultTrigger: any = {
|
||||
"triggerid": "13565",
|
||||
"value": "1",
|
||||
"groups": [{"groupid": "1", "name": "Backend"}] ,
|
||||
@@ -248,7 +252,7 @@ const defaultTrigger = {
|
||||
"flags": "0", "type": "0", "items": [] , "error": ""
|
||||
};
|
||||
|
||||
const defaultEvent = {
|
||||
const defaultEvent: any = {
|
||||
"eventid": "11",
|
||||
"acknowledges": [
|
||||
{
|
||||
@@ -272,8 +276,8 @@ const defaultEvent = {
|
||||
"objectid": "1",
|
||||
};
|
||||
|
||||
function generateTrigger(id, timestamp, severity) {
|
||||
let trigger = _.cloneDeep(defaultTrigger);
|
||||
function generateTrigger(id, timestamp?, severity?): any {
|
||||
const trigger = _.cloneDeep(defaultTrigger);
|
||||
trigger.triggerid = id.toString();
|
||||
if (severity) {
|
||||
trigger.priority = severity.toString();
|
||||
@@ -284,13 +288,13 @@ function generateTrigger(id, timestamp, severity) {
|
||||
return trigger;
|
||||
}
|
||||
|
||||
function createTrigger(props) {
|
||||
function createTrigger(props): any {
|
||||
let trigger = _.cloneDeep(defaultTrigger);
|
||||
trigger = _.merge(trigger, props);
|
||||
trigger.lastEvent.objectid = trigger.triggerid;
|
||||
return trigger;
|
||||
}
|
||||
|
||||
function getTriggerById(id, ctx) {
|
||||
function getTriggerById(id, ctx): any {
|
||||
return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { triggerPanelTriggersTab } from './triggers_tab';
|
||||
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||
import ProblemList from './components/Problems/Problems';
|
||||
import AlertList from './components/AlertList/AlertList';
|
||||
import { getNextRefIdChar } from './utils';
|
||||
|
||||
const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
|
||||
const PROBLEM_EVENTS_LIMIT = 100;
|
||||
@@ -23,7 +24,17 @@ export const DEFAULT_TARGET = {
|
||||
proxy: {filter: ""},
|
||||
};
|
||||
|
||||
export const getDefaultTarget = () => DEFAULT_TARGET;
|
||||
export const getDefaultTarget = (targets) => {
|
||||
return {
|
||||
group: {filter: ""},
|
||||
host: {filter: ""},
|
||||
application: {filter: ""},
|
||||
trigger: {filter: ""},
|
||||
tags: {filter: ""},
|
||||
proxy: {filter: ""},
|
||||
refId: getNextRefIdChar(targets),
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_SEVERITY = [
|
||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
|
||||
@@ -40,8 +51,7 @@ const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||
|
||||
export const PANEL_DEFAULTS = {
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
datasources: [],
|
||||
targets: {},
|
||||
targets: [getDefaultTarget([])],
|
||||
// Fields
|
||||
hostField: true,
|
||||
hostTechNameField: false,
|
||||
@@ -108,11 +118,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||
|
||||
this.available_datasources = _.map(this.getZabbixDataSources(), 'name');
|
||||
if (this.panel.datasources.length === 0) {
|
||||
this.panel.datasources.push(this.available_datasources[0]);
|
||||
}
|
||||
if (this.isEmptyTargets()) {
|
||||
this.panel.targets[this.panel.datasources[0]] = getDefaultTarget();
|
||||
if (this.panel.targets && !this.panel.targets[0].datasource) {
|
||||
this.panel.targets[0].datasource = this.available_datasources[0];
|
||||
}
|
||||
|
||||
this.initDatasources();
|
||||
@@ -138,7 +145,11 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
}
|
||||
|
||||
initDatasources() {
|
||||
let promises = _.map(this.panel.datasources, (ds) => {
|
||||
if (!this.panel.targets) {
|
||||
return;
|
||||
}
|
||||
const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource));
|
||||
let promises = targetDatasources.map(ds => {
|
||||
// Load datasource
|
||||
return this.datasourceSrv.get(ds)
|
||||
.then(datasource => {
|
||||
@@ -234,16 +245,20 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
getTriggers() {
|
||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||
|
||||
let promises = _.map(this.panel.datasources, (ds) => {
|
||||
let promises = _.map(this.panel.targets, (target) => {
|
||||
const ds = target.datasource;
|
||||
let proxies;
|
||||
let showAckButton = true;
|
||||
return this.datasourceSrv.get(ds)
|
||||
.then(datasource => {
|
||||
const zabbix = datasource.zabbix;
|
||||
const showEvents = this.panel.showEvents.value;
|
||||
const triggerFilter = this.panel.targets[ds];
|
||||
const triggerFilter = target;
|
||||
const showProxy = this.panel.hostProxy;
|
||||
const getProxiesPromise = showProxy ? zabbix.getProxies() : () => [];
|
||||
showAckButton = !datasource.disableReadOnlyUsersAck || userIsEditor;
|
||||
|
||||
// Replace template variables
|
||||
const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter);
|
||||
@@ -271,19 +286,18 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
}));
|
||||
return Promise.all([
|
||||
this.datasources[ds].zabbix.getExtendedEventData(eventids),
|
||||
this.datasources[ds].zabbix.getEventAlerts(eventids),
|
||||
Promise.resolve(triggers)
|
||||
]);
|
||||
})
|
||||
.then(([events, alerts, triggers]) => {
|
||||
.then(([events, triggers]) => {
|
||||
this.addEventTags(events, triggers);
|
||||
this.addAcknowledges(events, triggers);
|
||||
this.addEventAlerts(alerts, triggers);
|
||||
return triggers;
|
||||
})
|
||||
.then(triggers => this.setMaintenanceStatus(triggers))
|
||||
.then(triggers => this.filterTriggersPre(triggers, ds))
|
||||
.then(triggers => this.addTriggerDataSource(triggers, ds))
|
||||
.then(triggers => this.setAckButtonStatus(triggers, showAckButton))
|
||||
.then(triggers => this.filterTriggersPre(triggers, target))
|
||||
.then(triggers => this.addTriggerDataSource(triggers, target))
|
||||
.then(triggers => this.addTriggerHostProxy(triggers, proxies));
|
||||
});
|
||||
|
||||
@@ -337,28 +351,17 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
return triggers;
|
||||
}
|
||||
|
||||
addEventAlerts(alerts, triggers) {
|
||||
alerts.forEach(alert => {
|
||||
const trigger = _.find(triggers, t => {
|
||||
return t.lastEvent && alert.eventid === t.lastEvent.eventid;
|
||||
});
|
||||
if (trigger) {
|
||||
trigger.alerts = trigger.alerts ? trigger.alerts.concat(alert) : [alert];
|
||||
}
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
filterTriggersPre(triggerList, ds) {
|
||||
filterTriggersPre(triggerList, target) {
|
||||
// Filter triggers by description
|
||||
let triggerFilter = this.panel.targets[ds].trigger.filter;
|
||||
const ds = target.datasource;
|
||||
let triggerFilter = target.trigger.filter;
|
||||
triggerFilter = this.datasources[ds].replaceTemplateVars(triggerFilter);
|
||||
if (triggerFilter) {
|
||||
triggerList = filterTriggers(triggerList, triggerFilter);
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
const target = this.panel.targets[ds];
|
||||
// const target = this.panel.targets[ds];
|
||||
if (target.tags.filter) {
|
||||
let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter);
|
||||
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
||||
@@ -409,9 +412,16 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
return triggers;
|
||||
}
|
||||
|
||||
addTriggerDataSource(triggers, ds) {
|
||||
setAckButtonStatus(triggers, showAckButton) {
|
||||
_.each(triggers, (trigger) => {
|
||||
trigger.datasource = ds;
|
||||
trigger.showAckButton = showAckButton;
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
addTriggerDataSource(triggers, target) {
|
||||
_.each(triggers, (trigger) => {
|
||||
trigger.datasource = target.datasource;
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
@@ -482,37 +492,51 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||
}
|
||||
|
||||
addTagFilter(tag, ds) {
|
||||
let tagFilter = this.panel.targets[ds].tags.filter;
|
||||
addTagFilter(tag, datasource) {
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource);
|
||||
console.log(target);
|
||||
let tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
let newTag = {tag: tag.tag, value: tag.value};
|
||||
targetTags.push(newTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
let newFilter = this.tagsToString(targetTags);
|
||||
this.panel.targets[ds].tags.filter = newFilter;
|
||||
target.tags.filter = newFilter;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
removeTagFilter(tag, ds) {
|
||||
let tagFilter = this.panel.targets[ds].tags.filter;
|
||||
removeTagFilter(tag, datasource) {
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource);
|
||||
let tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
_.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
let newFilter = this.tagsToString(targetTags);
|
||||
this.panel.targets[ds].tags.filter = newFilter;
|
||||
target.tags.filter = newFilter;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getProblemEvents(trigger) {
|
||||
const triggerids = [trigger.triggerid];
|
||||
getProblemEvents(problem) {
|
||||
const triggerids = [problem.triggerid];
|
||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||
return this.datasourceSrv.get(trigger.datasource)
|
||||
return this.datasourceSrv.get(problem.datasource)
|
||||
.then(datasource => {
|
||||
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||
});
|
||||
}
|
||||
|
||||
getProblemAlerts(problem) {
|
||||
if (!problem.lastEvent || problem.lastEvent.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const eventids = [problem.lastEvent.eventid];
|
||||
return this.datasourceSrv.get(problem.datasource)
|
||||
.then(datasource => {
|
||||
return datasource.zabbix.getEventAlerts(eventids);
|
||||
});
|
||||
}
|
||||
|
||||
formatHostName(trigger) {
|
||||
let host = "";
|
||||
if (this.panel.hostField && this.panel.hostTechNameField) {
|
||||
@@ -549,12 +573,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
|
||||
getAlertIconClass(trigger) {
|
||||
let iconClass = '';
|
||||
if (trigger.value === '1') {
|
||||
if (trigger.priority >= 3) {
|
||||
if (trigger.value === '1' && trigger.priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
} else {
|
||||
iconClass = 'icon-gf-warning';
|
||||
}
|
||||
} else {
|
||||
iconClass = 'icon-gf-online';
|
||||
}
|
||||
@@ -566,8 +586,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
}
|
||||
|
||||
getAlertIconClassBySeverity(triggerSeverity) {
|
||||
let iconClass = 'icon-gf-warning';
|
||||
if (triggerSeverity.priority >= 3) {
|
||||
let iconClass = 'icon-gf-online';
|
||||
if (triggerSeverity.priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
}
|
||||
return iconClass;
|
||||
@@ -663,6 +683,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
||||
pageSize,
|
||||
fontSize: fontSizeProp,
|
||||
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
||||
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||
onProblemAck: (trigger, data) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ class TriggersTabCtrl {
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
this.templateSrv = templateSrv;
|
||||
this.datasources = this.panelCtrl.datasources;
|
||||
this.datasources = {};
|
||||
|
||||
// Load scope defaults
|
||||
var scopeDefaults = {
|
||||
@@ -21,6 +21,7 @@ class TriggersTabCtrl {
|
||||
oldTarget: _.cloneDeep(this.panel.targets)
|
||||
};
|
||||
_.defaultsDeep(this, scopeDefaults);
|
||||
this.selectedDatasources = this.getSelectedDatasources();
|
||||
|
||||
this.initDatasources();
|
||||
this.panelCtrl.refresh();
|
||||
@@ -30,6 +31,7 @@ class TriggersTabCtrl {
|
||||
return this.panelCtrl.initDatasources()
|
||||
.then((datasources) => {
|
||||
_.each(datasources, (datasource) => {
|
||||
this.datasources[datasource.name] = datasource;
|
||||
this.bindSuggestionFunctions(datasource);
|
||||
});
|
||||
});
|
||||
@@ -44,6 +46,10 @@ class TriggersTabCtrl {
|
||||
this.getProxyNames[ds] = _.bind(this.suggestProxies, this, datasource);
|
||||
}
|
||||
|
||||
getSelectedDatasources() {
|
||||
return _.compact(this.panel.targets.map(target => target.datasource));
|
||||
}
|
||||
|
||||
suggestGroups(datasource, query, callback) {
|
||||
return datasource.zabbix.getAllGroups()
|
||||
.then(groups => {
|
||||
@@ -53,7 +59,8 @@ class TriggersTabCtrl {
|
||||
}
|
||||
|
||||
suggestHosts(datasource, query, callback) {
|
||||
let groupFilter = datasource.replaceTemplateVars(this.panel.targets[datasource.name].group.filter);
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource.name);
|
||||
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
|
||||
return datasource.zabbix.getAllHosts(groupFilter)
|
||||
.then(hosts => {
|
||||
return _.map(hosts, 'name');
|
||||
@@ -62,8 +69,9 @@ class TriggersTabCtrl {
|
||||
}
|
||||
|
||||
suggestApps(datasource, query, callback) {
|
||||
let groupFilter = datasource.replaceTemplateVars(this.panel.targets[datasource.name].group.filter);
|
||||
let hostFilter = datasource.replaceTemplateVars(this.panel.targets[datasource.name].host.filter);
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource.name);
|
||||
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
|
||||
let hostFilter = datasource.replaceTemplateVars(target.host.filter);
|
||||
return datasource.zabbix.getAllApps(groupFilter, hostFilter)
|
||||
.then(apps => {
|
||||
return _.map(apps, 'name');
|
||||
@@ -78,16 +86,17 @@ class TriggersTabCtrl {
|
||||
}
|
||||
|
||||
datasourcesChanged() {
|
||||
_.each(this.panel.datasources, (ds) => {
|
||||
if (!this.panel.targets[ds]) {
|
||||
this.panel.targets[ds] = getDefaultTarget();
|
||||
}
|
||||
});
|
||||
// Remove unchecked targets
|
||||
_.each(this.panel.targets, (target, ds) => {
|
||||
if (!_.includes(this.panel.datasources, ds)) {
|
||||
delete this.panel.targets[ds];
|
||||
const newTargets = [];
|
||||
_.each(this.selectedDatasources, (ds) => {
|
||||
const dsTarget = this.panel.targets.find((target => target.datasource === ds));
|
||||
if (dsTarget) {
|
||||
newTargets.push(dsTarget);
|
||||
} else {
|
||||
const newTarget = getDefaultTarget(this.panel.targets);
|
||||
newTarget.datasource = ds;
|
||||
newTargets.push(newTarget);
|
||||
}
|
||||
this.panel.targets = newTargets;
|
||||
});
|
||||
this.parseTarget();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface ProblemsPanelOptions {
|
||||
schemaVersion: number;
|
||||
datasources: any[];
|
||||
targets: Map<string, ProblemsPanelTarget>;
|
||||
targets: ProblemsPanelTarget[];
|
||||
// Fields
|
||||
hostField?: boolean;
|
||||
hostTechNameField?: boolean;
|
||||
@@ -62,6 +62,7 @@ export interface ProblemsPanelTarget {
|
||||
proxy: {
|
||||
filter: string
|
||||
};
|
||||
datasource: string;
|
||||
}
|
||||
|
||||
export interface TriggerSeverity {
|
||||
@@ -75,6 +76,7 @@ export type TriggerColor = string;
|
||||
|
||||
export interface ZBXTrigger {
|
||||
acknowledges?: ZBXAcknowledge[];
|
||||
showAckButton?: boolean;
|
||||
alerts?: ZBXAlert[];
|
||||
age?: string;
|
||||
color?: TriggerColor;
|
||||
@@ -162,6 +164,7 @@ export interface ZBXAcknowledge {
|
||||
}
|
||||
|
||||
export interface ZBXAlert {
|
||||
eventid: string;
|
||||
clock: string;
|
||||
message: string;
|
||||
error: string;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { DataQuery } from '@grafana/ui/';
|
||||
import * as utils from '../datasource-zabbix/utils';
|
||||
import { ZBXTrigger } from './types';
|
||||
|
||||
@@ -20,3 +22,13 @@ export function formatLastChange(lastchangeUnix: number, customFormat?: string)
|
||||
const lastchange = timestamp.format(format);
|
||||
return lastchange;
|
||||
}
|
||||
|
||||
export const getNextRefIdChar = (queries: DataQuery[]): string => {
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return _.find(letters, refId => {
|
||||
return _.every(queries, other => {
|
||||
return other.refId !== refId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
{"name": "Metric Editor", "path": "img/screenshot-metric_editor.png"},
|
||||
{"name": "Triggers", "path": "img/screenshot-triggers.png"}
|
||||
],
|
||||
"version": "3.10.1",
|
||||
"updated": "2019-03-05"
|
||||
"version": "3.10.5",
|
||||
"updated": "2019-12-26"
|
||||
},
|
||||
|
||||
"includes": [
|
||||
@@ -42,7 +42,7 @@
|
||||
],
|
||||
|
||||
"dependencies": {
|
||||
"grafanaVersion": "5.x",
|
||||
"grafanaVersion": "6.x",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
|
||||
.rt-noData {
|
||||
z-index: 2;
|
||||
background: unset;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.pagination-bottom {
|
||||
@@ -522,7 +524,8 @@
|
||||
|
||||
@for $i from 11 through 25 {
|
||||
.item-#{$i} { width: 2em * $i; }
|
||||
&.font-size--#{$i * 10} .rt-table {
|
||||
&.font-size--#{$i * 10} {
|
||||
.rt-table {
|
||||
font-size: 1% * $i * 10;
|
||||
|
||||
& .rt-tr .rt-td.custom-expander i {
|
||||
@@ -533,6 +536,12 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.rt-noData {
|
||||
top: 4.5em;
|
||||
font-size: 1% * $i * 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
|
||||
&.zbx-description--newline {
|
||||
max-height: unset;
|
||||
|
||||
.zbx-description {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.zbx-description {
|
||||
|
||||
@@ -18,6 +18,12 @@ jest.mock('angular', () => {
|
||||
};
|
||||
}, {virtual: true});
|
||||
|
||||
jest.mock('grafana/app/core/core_module', () => {
|
||||
return {
|
||||
directive: function() {},
|
||||
};
|
||||
}, {virtual: true});
|
||||
|
||||
let mockPanelCtrl = PanelCtrl;
|
||||
jest.mock('grafana/app/plugins/sdk', () => {
|
||||
return {
|
||||
@@ -36,6 +42,13 @@ jest.mock('grafana/app/core/utils/datemath', () => {
|
||||
};
|
||||
}, {virtual: true});
|
||||
|
||||
jest.mock('grafana/app/core/utils/kbn', () => {
|
||||
return {
|
||||
round_interval: n => n,
|
||||
secondsToHms: n => n + 'ms'
|
||||
};
|
||||
}, {virtual: true});
|
||||
|
||||
jest.mock('grafana/app/core/table_model', () => {
|
||||
return class TableModel {
|
||||
constructor() {
|
||||
@@ -62,6 +75,10 @@ jest.mock('grafana/app/core/config', () => {
|
||||
|
||||
jest.mock('jquery', () => 'module not found', {virtual: true});
|
||||
|
||||
jest.mock('@grafana/ui', () => {
|
||||
return {};
|
||||
}, {virtual: true});
|
||||
|
||||
// Required for loading angularjs
|
||||
let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
|
||||
// Setup jsdom
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"check-format",
|
||||
"ban-keywords",
|
||||
"allow-leading-underscore",
|
||||
"allow-trailing-underscore",
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
externals: [
|
||||
// remove the line below if you don't want to use builtin versions
|
||||
'jquery', 'lodash', 'moment', 'angular',
|
||||
'react', 'react-dom',
|
||||
'react', 'react-dom', '@grafana/ui', '@grafana/data',
|
||||
function (context, request, callback) {
|
||||
var prefix = 'grafana/';
|
||||
if (request.indexOf(prefix) === 0) {
|
||||
@@ -53,7 +53,7 @@ module.exports = {
|
||||
ExtractTextPluginDark,
|
||||
],
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".tsx", ".html", ".scss"]
|
||||
extensions: ['.js', '.es6', '.ts', '.tsx', '.html', '.scss']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -63,7 +63,7 @@ module.exports = {
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['babel-preset-env']
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user