Merge branch 'master' into backend
This commit is contained in:
@@ -197,7 +197,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: sudo pip install codespell
|
- run: sudo pip install codespell
|
||||||
- run: codespell -S './.git*,./src/img*,./go.sum,yarn.lock' -L que --ignore-words=./.codespell_ignore
|
- run: codespell -S './.git*, ./src/img*, ./go.sum, ./yarn.lock' -L que --ignore-words=./.codespell_ignore
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
|||||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,11 +1,54 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
## [3.12.2] - 2020-05-28
|
||||||
|
### Fixed
|
||||||
|
- Annotations feature doesn't work, [#964](https://github.com/alexanderzobnin/grafana-zabbix/issues/964)
|
||||||
|
- Alias variables do not work with direct DB connection enabled, [#965](https://github.com/alexanderzobnin/grafana-zabbix/issues/965)
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
## [3.12.1] - 2020-05-25
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
### Fixed
|
||||||
|
- Problems: panel fails with error (cannot read property 'description' of undefined), [#954](https://github.com/alexanderzobnin/grafana-zabbix/issues/954)
|
||||||
|
- Problems: problem name filter doesn't work, [#962](https://github.com/alexanderzobnin/grafana-zabbix/issues/962)
|
||||||
|
- Problems: acknowledged filter doesn't work, [#961](https://github.com/alexanderzobnin/grafana-zabbix/issues/961)
|
||||||
|
|
||||||
## Unreleased
|
## [3.12.0] - 2020-05-21
|
||||||
|
### Added
|
||||||
|
- Variables: able to query item values, [#417](https://github.com/alexanderzobnin/grafana-zabbix/issues/417)
|
||||||
|
- Functions: expose host, item, app to the alias functions, [#619](https://github.com/alexanderzobnin/grafana-zabbix/issues/619)
|
||||||
|
- Problems: navigate to Explore and show graphs for the problem, [#948](https://github.com/alexanderzobnin/grafana-zabbix/issues/948)
|
||||||
|
- Problems: able to show Problems/Recent problems/History, [#495](https://github.com/alexanderzobnin/grafana-zabbix/issues/495)
|
||||||
|
- Problems: icon with acknowledges count, [#946](https://github.com/alexanderzobnin/grafana-zabbix/issues/946)
|
||||||
|
- IT Services: support SLA intervals, [#885](https://github.com/alexanderzobnin/grafana-zabbix/issues/885)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Explore doesn't work with Zabbix datasource, [#888](https://github.com/alexanderzobnin/grafana-zabbix/issues/888)
|
||||||
|
- SLA value is incorrect, [#885](https://github.com/alexanderzobnin/grafana-zabbix/issues/885)
|
||||||
|
- Graph panel randomly shows no data, [#861](https://github.com/alexanderzobnin/grafana-zabbix/issues/861)
|
||||||
|
- Variables: unable to edit variables in Grafana 7.0.0, [#949](https://github.com/alexanderzobnin/grafana-zabbix/issues/949)
|
||||||
|
- Variables: wrong variable scope inside repeated rows, [#912](https://github.com/alexanderzobnin/grafana-zabbix/issues/912)
|
||||||
|
- Problems: resolve macros in URLs, [#190](https://github.com/alexanderzobnin/grafana-zabbix/issues/190)
|
||||||
|
- Problems: unable to acknowledge resolved problem, [#942](https://github.com/alexanderzobnin/grafana-zabbix/issues/942)
|
||||||
|
- Problems: resolved problems color and severity set to Not classified, [#909](https://github.com/alexanderzobnin/grafana-zabbix/issues/909)
|
||||||
|
- Problems: can't acknowledge alert in panel with a single problem, [#900](https://github.com/alexanderzobnin/grafana-zabbix/issues/900)
|
||||||
|
- Annotations: `ITEM.VALUE` behaves like `ITEM.LASTVALUE` in annotations, [#891](https://github.com/alexanderzobnin/grafana-zabbix/issues/891)
|
||||||
|
- Alert state on the panel (heart icon) doesn't work in Grafana 6.7, [#931](https://github.com/alexanderzobnin/grafana-zabbix/issues/931)
|
||||||
|
- Consolidated average is not accurate with direct DB connection, [#752](https://github.com/alexanderzobnin/grafana-zabbix/issues/752)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Problems panel uses new `problem.get` API which is not compatible with Zabbix 3.x, [#495](https://github.com/alexanderzobnin/grafana-zabbix/issues/495)
|
||||||
|
- Problems panel is metrics panel now, problems query editor moved to the data source.
|
||||||
|
- Zabbix version is auto detected now, [#727](https://github.com/alexanderzobnin/grafana-zabbix/issues/727)
|
||||||
|
|
||||||
|
## [3.11.0] - 2020-03-23
|
||||||
|
### Added
|
||||||
|
- Improve variable query editor, [#705](https://github.com/alexanderzobnin/grafana-zabbix/issues/705)
|
||||||
|
- Transform/percentile function, [#868](https://github.com/alexanderzobnin/grafana-zabbix/issues/868)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Problems panel: stopped working in Grafana 6.7.0, [#907](https://github.com/alexanderzobnin/grafana-zabbix/issues/907)
|
||||||
|
- Problems panel: event severity change, [#870](https://github.com/alexanderzobnin/grafana-zabbix/issues/870)
|
||||||
|
- Problems panel: color is changed to acknowledged even if there is only message without acknowledgment, [#857](https://github.com/alexanderzobnin/grafana-zabbix/issues/857)
|
||||||
|
- Percentile function returns incorrect results, [#862](https://github.com/alexanderzobnin/grafana-zabbix/issues/862)
|
||||||
|
|
||||||
## [3.10.5] - 2019-12-26
|
## [3.10.5] - 2019-12-26
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ site_description: Documentation for Grafana-Zabbix, Zabbix monitoring system plu
|
|||||||
site_url: https://alexanderzobnin.github.io/grafana-zabbix/
|
site_url: https://alexanderzobnin.github.io/grafana-zabbix/
|
||||||
repo_url: https://github.com/alexanderzobnin/grafana-zabbix/
|
repo_url: https://github.com/alexanderzobnin/grafana-zabbix/
|
||||||
edit_uri: blob/docs/docs/sources/
|
edit_uri: blob/docs/docs/sources/
|
||||||
copyright: Copyright © 2015-2019, Alexander Zobnin
|
copyright: Copyright © 2015-2020, Alexander Zobnin
|
||||||
|
|
||||||
docs_dir: sources
|
docs_dir: sources
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Direct DB Connection allows plugin to use existing SQL data source for querying
|
|||||||
database. This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
|
database. This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
|
||||||
amount of data transferred.
|
amount of data transferred.
|
||||||
|
|
||||||
Read [how to configure](./sql_datasource) SQL data source in Grafana.
|
Read [how to configure](./direct_db_datasource) SQL data source in Grafana.
|
||||||
|
|
||||||
- **Enable**: enable Direct DB Connection.
|
- **Enable**: enable Direct DB Connection.
|
||||||
- **Data Source**: Select Data Source for Zabbix history database.
|
- **Data Source**: Select Data Source for Zabbix history database.
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ grafana-cli plugins install alexanderzobnin-zabbix-app
|
|||||||
|
|
||||||
Restart grafana after installing plugins
|
Restart grafana after installing plugins
|
||||||
```sh
|
```sh
|
||||||
service grafana-server restart
|
systemctl restart grafana-server
|
||||||
```
|
```
|
||||||
|
|
||||||
Read more about installing plugins in [Grafana docs](http://docs.grafana.org/plugins/installation/)
|
Read more about installing plugins in [Grafana docs](https://grafana.com/docs/plugins/installation/)
|
||||||
|
|
||||||
**WARNING!** The only reliable installation method is `grafana-cli`. Any other ways should be treated as a workaround an don't provide any backward-compatibulity guaranties.
|
**WARNING!** The only reliable installation method is `grafana-cli`. Any other way should be treated as a workaround and doesn't provide any backward-compatibility guaranties.
|
||||||
|
|
||||||
## From github repo
|
## From github repo
|
||||||
**WARNING!** This way doesn't work anymore (`dist/` folder was removed from git). Use `grafana-cli` or build plugin from sources.
|
**WARNING!** This way doesn't work anymore (`dist/` folder was removed from git). Use `grafana-cli` or build plugin from sources.
|
||||||
|
|||||||
@@ -269,6 +269,20 @@ timeShift(+1d) - shift metric forward in 1 day
|
|||||||
|
|
||||||
## Alias
|
## Alias
|
||||||
|
|
||||||
|
Following template variables available for using in `setAlias()` and `replaceAlias()` functions:
|
||||||
|
|
||||||
|
- `$__zbx_item`, `$__zbx_item_name` - item name
|
||||||
|
- `$__zbx_item_key` - item key
|
||||||
|
- `$__zbx_host_name` - visible name of the host
|
||||||
|
- `$__zbx_host` - technical name of the host
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
setAlias($__zbx_host_name: $__zbx_item) -> backend01: CPU user time
|
||||||
|
setAlias(Item key: $__zbx_item_key) -> Item key: system.cpu.load[percpu,avg1]
|
||||||
|
setAlias($__zbx_host_name) -> backend01
|
||||||
|
```
|
||||||
|
|
||||||
### _setAlias_
|
### _setAlias_
|
||||||
```
|
```
|
||||||
setAlias(alias)
|
setAlias(alias)
|
||||||
@@ -310,7 +324,7 @@ Replace metric name using pattern. Pattern is regex or regular string. If regex
|
|||||||
|$' | Inserts the portion of the string that follows the matched substring. |
|
|$' | Inserts the portion of the string that follows the matched substring. |
|
||||||
|$n | Where n is a non-negative integer less than 100, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object. |
|
|$n | Where n is a non-negative integer less than 100, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object. |
|
||||||
|
|
||||||
For more detais see [String.prototype.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) function.
|
For more details see [String.prototype.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) function.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```
|
```
|
||||||
|
|||||||
23
package.json
23
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "grafana-zabbix",
|
"name": "grafana-zabbix",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "3.10.5",
|
"version": "3.11.0",
|
||||||
"description": "Zabbix plugin for Grafana",
|
"description": "Zabbix plugin for Grafana",
|
||||||
"homepage": "http://grafana-zabbix.org",
|
"homepage": "http://grafana-zabbix.org",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,10 +30,11 @@
|
|||||||
"@babel/preset-env": "^7.8.3",
|
"@babel/preset-env": "^7.8.3",
|
||||||
"@babel/preset-react": "^7.8.3",
|
"@babel/preset-react": "^7.8.3",
|
||||||
"@emotion/core": "^10.0.27",
|
"@emotion/core": "^10.0.27",
|
||||||
"@grafana/data": "canary",
|
"@grafana/data": "^6.7.3",
|
||||||
"@grafana/runtime": "canary",
|
"@grafana/runtime": "^6.7.3",
|
||||||
"@grafana/toolkit": "canary",
|
"@grafana/ui": "^6.7.3",
|
||||||
"@grafana/ui": "canary",
|
"@grafana/toolkit": "^6.7.3",
|
||||||
|
"@popperjs/core": "^2.4.0",
|
||||||
"@types/classnames": "^2.2.9",
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/grafana": "github:CorpGlory/types-grafana",
|
"@types/grafana": "github:CorpGlory/types-grafana",
|
||||||
"@types/jest": "^23.1.1",
|
"@types/jest": "^23.1.1",
|
||||||
@@ -69,24 +70,26 @@
|
|||||||
"jshint-stylish": "^2.1.0",
|
"jshint-stylish": "^2.1.0",
|
||||||
"load-grunt-tasks": "~3.2.0",
|
"load-grunt-tasks": "~3.2.0",
|
||||||
"lodash": "~4.17.13",
|
"lodash": "~4.17.13",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
"moment": "~2.21.0",
|
"moment": "~2.21.0",
|
||||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||||
"node-sass": "^4.13.0",
|
"node-sass": "^4.13.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.7.0",
|
"react": "16.12.0",
|
||||||
"react-dom": "^16.7.0",
|
"react-dom": "16.12.0",
|
||||||
"react-popper": "^1.3.2",
|
"react-popper": "^2.2.3",
|
||||||
"react-table": "^6.8.6",
|
"react-table-6": "^6.8.6",
|
||||||
"react-test-renderer": "^16.7.0",
|
"react-test-renderer": "^16.7.0",
|
||||||
"react-transition-group": "^2.5.2",
|
"react-transition-group": "^2.5.2",
|
||||||
"rst2html": "github:thoward/rst2html#990cb89",
|
"rst2html": "github:thoward/rst2html#990cb89",
|
||||||
"sass-loader": "^8.0.0",
|
"sass-loader": "^8.0.0",
|
||||||
|
"semver": "^7.3.2",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"tether-drop": "^1.4.2",
|
"tether-drop": "^1.4.2",
|
||||||
"ts-jest": "^24.2.0",
|
"ts-jest": "^24.2.0",
|
||||||
"ts-loader": "^6.2.0",
|
"ts-loader": "^6.2.0",
|
||||||
"tslint": "5.20.1",
|
"tslint": "5.20.1",
|
||||||
"typescript": "3.7.2",
|
"typescript": "^3.9.2",
|
||||||
"webpack": "4.29.6",
|
"webpack": "4.29.6",
|
||||||
"webpack-cli": "3.2.3"
|
"webpack-cli": "3.2.3"
|
||||||
},
|
},
|
||||||
|
|||||||
4
src/app_config_ctrl/config.js
Normal file
4
src/app_config_ctrl/config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export class ZabbixAppConfigCtrl {
|
||||||
|
constructor() { }
|
||||||
|
}
|
||||||
|
ZabbixAppConfigCtrl.templateUrl = 'app_config_ctrl/config.html';
|
||||||
13
src/components/AckButton/AckButton.tsx
Normal file
13
src/components/AckButton/AckButton.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { ActionButton } from '../ActionButton/ActionButton';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
onClick(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AckButton: FC<Props> = ({ className, onClick }) => {
|
||||||
|
return (
|
||||||
|
<ActionButton className={className} icon="reply-all" tooltip="Acknowledge problem" onClick={onClick} />
|
||||||
|
);
|
||||||
|
};
|
||||||
71
src/components/ActionButton/ActionButton.tsx
Normal file
71
src/components/ActionButton/ActionButton.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { cx, css } from 'emotion';
|
||||||
|
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme, GrafanaThemeType } from '@grafana/data';
|
||||||
|
import { FAIcon } from '../FAIcon/FAIcon';
|
||||||
|
import { Tooltip } from '../Tooltip/Tooltip';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: string;
|
||||||
|
width?: number;
|
||||||
|
tooltip?: string;
|
||||||
|
className?: string;
|
||||||
|
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionButton: FC<Props> = ({ icon, width, tooltip, className, children, onClick }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const buttonClass = cx(
|
||||||
|
'btn',
|
||||||
|
styles.button,
|
||||||
|
css`width: ${width || 3}rem`,
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
let button = (
|
||||||
|
<button className={buttonClass} onClick={onClick}>
|
||||||
|
{icon && <FAIcon icon={icon} customClass={styles.icon} />}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
button = (
|
||||||
|
<Tooltip placement="bottom" content={tooltip}>
|
||||||
|
{button}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
const actionBlue = theme.type === GrafanaThemeType.Light ? '#497dc0' : '#005f81';
|
||||||
|
const hoverBlue = theme.type === GrafanaThemeType.Light ? '#456ba4' : '#354f77';
|
||||||
|
|
||||||
|
return {
|
||||||
|
button: css`
|
||||||
|
height: 2rem;
|
||||||
|
background-image: none;
|
||||||
|
background-color: ${actionBlue};
|
||||||
|
border: 1px solid ${theme.colors.gray1 || (theme as any).palette.gray1};
|
||||||
|
border-radius: 1px;
|
||||||
|
color: ${theme.colors.text};
|
||||||
|
|
||||||
|
i {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${hoverBlue};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
i {
|
||||||
|
color: ${theme.colors.text};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
34
src/components/ConfigProvider/ConfigProvider.tsx
Normal file
34
src/components/ConfigProvider/ConfigProvider.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { config, GrafanaBootConfig } from '@grafana/runtime';
|
||||||
|
import { ThemeContext, getTheme } from '@grafana/ui';
|
||||||
|
import { GrafanaThemeType } from '@grafana/data';
|
||||||
|
|
||||||
|
export const ConfigContext = React.createContext<GrafanaBootConfig>(config);
|
||||||
|
export const ConfigConsumer = ConfigContext.Consumer;
|
||||||
|
|
||||||
|
export const provideConfig = (component: React.ComponentType<any>) => {
|
||||||
|
const ConfigProvider = (props: any) => (
|
||||||
|
<ConfigContext.Provider value={config}>{React.createElement(component, { ...props })}</ConfigContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return ConfigProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentThemeName = () =>
|
||||||
|
config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
|
||||||
|
|
||||||
|
export const getCurrentTheme = () => getTheme(getCurrentThemeName());
|
||||||
|
|
||||||
|
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<ConfigConsumer>
|
||||||
|
{config => {
|
||||||
|
return <ThemeContext.Provider value={getCurrentTheme()}>{children}</ThemeContext.Provider>;
|
||||||
|
}}
|
||||||
|
</ConfigConsumer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const provideTheme = (component: React.ComponentType<any>) => {
|
||||||
|
return provideConfig((props: any) => <ThemeProvider>{React.createElement(component, { ...props })}</ThemeProvider>);
|
||||||
|
};
|
||||||
54
src/components/ExploreButton/ExploreButton.tsx
Normal file
54
src/components/ExploreButton/ExploreButton.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { getLocationSrv } from '@grafana/runtime';
|
||||||
|
import { MODE_METRICS, MODE_ITEMID } from '../../datasource-zabbix/constants';
|
||||||
|
import { renderUrl } from '../../panel-triggers/utils';
|
||||||
|
import { expandItemName } from '../../datasource-zabbix/utils';
|
||||||
|
import { ProblemDTO } from '../../datasource-zabbix/types';
|
||||||
|
import { ActionButton } from '../ActionButton/ActionButton';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
problem: ProblemDTO;
|
||||||
|
panelId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExploreButton: FC<Props> = ({ problem, panelId }) => {
|
||||||
|
return (
|
||||||
|
<ActionButton icon="compass" width={6} onClick={() => openInExplore(problem, panelId)}>
|
||||||
|
Explore
|
||||||
|
</ActionButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openInExplore = (problem: ProblemDTO, panelId: number) => {
|
||||||
|
let query: any = {};
|
||||||
|
|
||||||
|
if (problem.items?.length === 1 && problem.hosts?.length === 1) {
|
||||||
|
const item = problem.items[0];
|
||||||
|
const host = problem.hosts[0];
|
||||||
|
const itemName = expandItemName(item.name, item.key_);
|
||||||
|
query = {
|
||||||
|
queryType: MODE_METRICS,
|
||||||
|
group: { filter: '/.*/' },
|
||||||
|
application: { filter: '' },
|
||||||
|
host: { filter: host.name },
|
||||||
|
item: { filter: itemName },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const itemids = problem.items?.map(p => p.itemid).join(',');
|
||||||
|
query = {
|
||||||
|
queryType: MODE_ITEMID,
|
||||||
|
itemids: itemids,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const state: any = {
|
||||||
|
datasource: problem.datasource,
|
||||||
|
context: 'explore',
|
||||||
|
originPanelId: panelId,
|
||||||
|
queries: [query],
|
||||||
|
};
|
||||||
|
|
||||||
|
const exploreState = JSON.stringify(state);
|
||||||
|
const url = renderUrl('/explore', { left: exploreState });
|
||||||
|
getLocationSrv().update({ path: url, query: {} });
|
||||||
|
};
|
||||||
19
src/components/FAIcon/FAIcon.tsx
Normal file
19
src/components/FAIcon/FAIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { cx } from 'emotion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: string;
|
||||||
|
customClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FAIcon: FC<Props> = ({ icon, customClass }) => {
|
||||||
|
const wrapperClass = cx(customClass, 'fa-icon-container');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={wrapperClass}>
|
||||||
|
<i className={`fa fa-${icon}`}></i>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FAIcon;
|
||||||
22
src/components/GFHeartIcon/GFHeartIcon.tsx
Normal file
22
src/components/GFHeartIcon/GFHeartIcon.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { cx } from 'emotion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
status: 'critical' | 'warning' | 'online' | 'ok' | 'problem';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GFHeartIcon: FC<Props> = ({ status, className }) => {
|
||||||
|
const iconClass = cx(
|
||||||
|
className,
|
||||||
|
'icon-gf',
|
||||||
|
{ "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'},
|
||||||
|
{ "icon-gf-online": status === 'online' || status === 'ok' },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<i className={iconClass}></i>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GFHeartIcon;
|
||||||
70
src/components/Modal/ModalController.tsx
Normal file
70
src/components/Modal/ModalController.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { provideTheme } from '../ConfigProvider/ConfigProvider';
|
||||||
|
|
||||||
|
interface ModalWrapperProps {
|
||||||
|
showModal: <T>(component: React.ComponentType<T>, props: T) => void;
|
||||||
|
hideModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalWrapper<T> = FC<ModalWrapperProps>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ModalWrapper<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
component: React.ComponentType<any> | null;
|
||||||
|
props: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModalController extends React.Component<Props, State> {
|
||||||
|
modalRoot = document.body;
|
||||||
|
modalNode = document.createElement('div');
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
component: null,
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal = (component: React.ComponentType<any>, props: any) => {
|
||||||
|
this.setState({
|
||||||
|
component,
|
||||||
|
props
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
hideModal = () => {
|
||||||
|
this.modalRoot.removeChild(this.modalNode);
|
||||||
|
this.setState({
|
||||||
|
component: null,
|
||||||
|
props: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderModal() {
|
||||||
|
const { component, props } = this.state;
|
||||||
|
if (!component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modalRoot.appendChild(this.modalNode);
|
||||||
|
const modal = React.createElement(provideTheme(component), props);
|
||||||
|
return ReactDOM.createPortal(modal, this.modalNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const ChildrenComponent = children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ChildrenComponent showModal={this.showModal} hideModal={this.hideModal} />
|
||||||
|
{this.renderModal()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/components/Tooltip/Popper.tsx
Normal file
75
src/components/Tooltip/Popper.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { cx, css } from 'emotion';
|
||||||
|
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
|
||||||
|
import Transition from 'react-transition-group/Transition';
|
||||||
|
import { stylesFactory } from '@grafana/ui';
|
||||||
|
import BodyPortal from './Portal';
|
||||||
|
|
||||||
|
const getStyles = stylesFactory(() => ({
|
||||||
|
defaultTransitionStyles: css`
|
||||||
|
transition: opacity 200ms linear;
|
||||||
|
opacity: 0;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const transitionStyles = {
|
||||||
|
exited: { opacity: 0 },
|
||||||
|
entering: { opacity: 0 },
|
||||||
|
entered: { opacity: 1 },
|
||||||
|
exiting: { opacity: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
renderContent: (content: any) => any;
|
||||||
|
show: boolean;
|
||||||
|
placement?: any;
|
||||||
|
content: string | ((props: any) => JSX.Element);
|
||||||
|
refClassName?: string;
|
||||||
|
popperClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Popper: FC<Props> = ({ show, placement, popperClassName, refClassName, content, children, renderContent }) => {
|
||||||
|
const refClass = cx('popper_ref', refClassName);
|
||||||
|
const styles = getStyles();
|
||||||
|
const popperClass = cx('popper', popperClassName, styles.defaultTransitionStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => (
|
||||||
|
<div className={refClass} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Reference>
|
||||||
|
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||||
|
{transitionState => (
|
||||||
|
<BodyPortal>
|
||||||
|
<ReactPopper placement={placement}>
|
||||||
|
{({ ref, style, placement, arrowProps }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
...transitionStyles[transitionState],
|
||||||
|
}}
|
||||||
|
data-placement={placement}
|
||||||
|
className={popperClass}
|
||||||
|
>
|
||||||
|
<div className="popper__background">
|
||||||
|
{renderContent(content)}
|
||||||
|
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ReactPopper>
|
||||||
|
</BodyPortal>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
</Manager>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Popper;
|
||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class BodyPortal extends PureComponent<Props> {
|
export default class BodyPortal extends PureComponent<Props> {
|
||||||
node: HTMLElement = document.createElement('div');
|
node: HTMLElement;
|
||||||
portalRoot: HTMLElement;
|
portalRoot: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -17,6 +17,7 @@ export default class BodyPortal extends PureComponent<Props> {
|
|||||||
root = document.body
|
root = document.body
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
this.node = document.createElement('div');
|
||||||
if (className) {
|
if (className) {
|
||||||
this.node.classList.add(className);
|
this.node.classList.add(className);
|
||||||
}
|
}
|
||||||
15
src/components/Tooltip/Tooltip.tsx
Normal file
15
src/components/Tooltip/Tooltip.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import Popper from './Popper';
|
||||||
|
import withPopper, { UsingPopperProps } from './withPopper';
|
||||||
|
|
||||||
|
const TooltipWrapper: FC<UsingPopperProps> = ({ hidePopper, showPopper, className, children, ...restProps }) => {
|
||||||
|
return (
|
||||||
|
<div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
|
||||||
|
<Popper {...restProps}>{children}</Popper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tooltip = withPopper(TooltipWrapper);
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
@@ -21,44 +21,28 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
placement: string;
|
|
||||||
show: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function withPopper(WrappedComponent) {
|
export const withPopper = (WrappedComponent) => {
|
||||||
return class extends React.Component<Props, State> {
|
return class extends React.Component<Props, State> {
|
||||||
|
static defaultProps: Partial<Props> = {
|
||||||
|
placement: 'auto',
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.setState = this.setState.bind(this);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
placement: this.props.placement || 'auto',
|
|
||||||
show: false,
|
show: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.placement && nextProps.placement !== this.state.placement) {
|
|
||||||
this.setState(prevState => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
placement: nextProps.placement,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showPopper = () => {
|
showPopper = () => {
|
||||||
this.setState(prevState => ({
|
this.setState({ show: true });
|
||||||
...prevState,
|
|
||||||
show: true,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hidePopper = () => {
|
hidePopper = () => {
|
||||||
this.setState(prevState => ({
|
this.setState({ show: false });
|
||||||
...prevState,
|
|
||||||
show: false,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderContent(content) {
|
renderContent(content) {
|
||||||
@@ -71,8 +55,8 @@ export default function withPopper(WrappedComponent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { show, placement } = this.state;
|
const { show } = this.state;
|
||||||
const className = this.props.className || '';
|
const { placement, className } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WrappedComponent
|
<WrappedComponent
|
||||||
@@ -80,11 +64,13 @@ export default function withPopper(WrappedComponent) {
|
|||||||
showPopper={this.showPopper}
|
showPopper={this.showPopper}
|
||||||
hidePopper={this.hidePopper}
|
hidePopper={this.hidePopper}
|
||||||
renderContent={this.renderContent}
|
renderContent={this.renderContent}
|
||||||
show={show}
|
|
||||||
placement={placement}
|
placement={placement}
|
||||||
className={className}
|
className={className}
|
||||||
|
show={show}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default withPopper;
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export class ZabbixAppConfigCtrl {
|
|
||||||
constructor() { }
|
|
||||||
}
|
|
||||||
ZabbixAppConfigCtrl.templateUrl = 'components/config.html';
|
|
||||||
6
src/components/index.ts
Normal file
6
src/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { GFHeartIcon } from './GFHeartIcon/GFHeartIcon';
|
||||||
|
export { FAIcon } from './FAIcon/FAIcon';
|
||||||
|
export { AckButton } from './AckButton/AckButton';
|
||||||
|
export { ExploreButton } from './ExploreButton/ExploreButton';
|
||||||
|
export { Tooltip } from './Tooltip/Tooltip';
|
||||||
|
export { ModalController } from './Modal/ModalController';
|
||||||
@@ -39,7 +39,6 @@ class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEd
|
|||||||
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
|
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
|
||||||
<h4 style={{ color: 'white' }}> {name} </h4>
|
<h4 style={{ color: 'white' }}> {name} </h4>
|
||||||
<div>{description}</div>
|
<div>{description}</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { parseLegacyVariableQuery } from '../utils';
|
import { parseLegacyVariableQuery } from '../utils';
|
||||||
import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui';
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
|
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
|
||||||
import { ZabbixInput } from './ZabbixInput';
|
import { ZabbixInput } from './ZabbixInput';
|
||||||
|
|
||||||
|
// FormLabel was renamed to InlineFormLabel in Grafana 7.0
|
||||||
|
import * as grafanaUi from '@grafana/ui';
|
||||||
|
const FormLabel = grafanaUi.FormLabel || (grafanaUi as any).InlineFormLabel;
|
||||||
|
const Select = (grafanaUi as any).LegacyForms?.Select || (grafanaUi as any).Select;
|
||||||
|
const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
|
||||||
|
|
||||||
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
||||||
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
|
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
|
||||||
{ value: VariableQueryTypes.Group, label: 'Group'},
|
{ value: VariableQueryTypes.Group, label: 'Group'},
|
||||||
{ value: VariableQueryTypes.Host, label: 'Host' },
|
{ value: VariableQueryTypes.Host, label: 'Host' },
|
||||||
{ value: VariableQueryTypes.Application, label: 'Application' },
|
{ value: VariableQueryTypes.Application, label: 'Application' },
|
||||||
{ value: VariableQueryTypes.Item, label: 'Item' },
|
{ value: VariableQueryTypes.Item, label: 'Item' },
|
||||||
|
{ value: VariableQueryTypes.ItemValues, label: 'Item values' },
|
||||||
];
|
];
|
||||||
|
|
||||||
defaults: VariableQueryData = {
|
defaults: VariableQueryData = {
|
||||||
@@ -119,7 +125,8 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{(selectedQueryType.value === VariableQueryTypes.Application ||
|
{(selectedQueryType.value === VariableQueryTypes.Application ||
|
||||||
selectedQueryType.value === VariableQueryTypes.Item) &&
|
selectedQueryType.value === VariableQueryTypes.Item ||
|
||||||
|
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form max-width-30">
|
<div className="gf-form max-width-30">
|
||||||
<FormLabel width={10}>Application</FormLabel>
|
<FormLabel width={10}>Application</FormLabel>
|
||||||
@@ -129,7 +136,8 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
|||||||
onBlur={this.handleQueryChange}
|
onBlur={this.handleQueryChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedQueryType.value === VariableQueryTypes.Item &&
|
{(selectedQueryType.value === VariableQueryTypes.Item ||
|
||||||
|
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
|
||||||
<div className="gf-form max-width-30">
|
<div className="gf-form max-width-30">
|
||||||
<FormLabel width={10}>Item</FormLabel>
|
<FormLabel width={10}>Item</FormLabel>
|
||||||
<ZabbixInput
|
<ZabbixInput
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { withTheme, Input, EventsWithValidation, ValidationEvents, Themeable } from '@grafana/ui';
|
import { EventsWithValidation, ValidationEvents, useTheme } from '@grafana/ui';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { isRegex, variableRegex } from '../utils';
|
import { isRegex, variableRegex } from '../utils';
|
||||||
|
|
||||||
|
import * as grafanaUi from '@grafana/ui';
|
||||||
|
const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
|
||||||
|
|
||||||
const variablePattern = RegExp(`^${variableRegex.source}`);
|
const variablePattern = RegExp(`^${variableRegex.source}`);
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
inputRegex: css`
|
inputRegex: css`
|
||||||
color: ${theme.colors.orange}
|
color: ${theme.colors.orange || (theme as any).palette.orange}
|
||||||
`,
|
`,
|
||||||
inputVariable: css`
|
inputVariable: css`
|
||||||
color: ${theme.colors.variable}
|
color: ${theme.colors.variable || (theme as any).palette.variable}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,17 +46,14 @@ const zabbixInputValidationEvents: ValidationEvents = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Input>, Themeable {
|
export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => {
|
||||||
}
|
const theme = useTheme();
|
||||||
|
|
||||||
const UnthemedZabbixInput: FC<Props> = ({ theme, value, ref, validationEvents, ...restProps }) => {
|
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
let inputClass;
|
let inputClass = styles.inputRegex;
|
||||||
if (variablePattern.test(value as string)) {
|
if (variablePattern.test(value as string)) {
|
||||||
inputClass = styles.inputVariable;
|
inputClass = styles.inputVariable;
|
||||||
}
|
} else if (isRegex(value)) {
|
||||||
if (isRegex(value)) {
|
|
||||||
inputClass = styles.inputRegex;
|
inputClass = styles.inputRegex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,5 +66,3 @@ const UnthemedZabbixInput: FC<Props> = ({ theme, value, ref, validationEvents, .
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZabbixInput = withTheme(UnthemedZabbixInput);
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { migrateDSConfig } from './migrations';
|
import { migrateDSConfig } from './migrations';
|
||||||
|
|
||||||
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
|
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
|
||||||
|
|
||||||
const zabbixVersions = [
|
|
||||||
{ name: '2.x', value: 2 },
|
|
||||||
{ name: '3.x', value: 3 },
|
|
||||||
{ name: '4.x', value: 4 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
trends: false,
|
trends: false,
|
||||||
dbConnectionEnable: false,
|
dbConnectionEnable: false,
|
||||||
@@ -17,29 +12,24 @@ const defaultConfig = {
|
|||||||
addThresholds: false,
|
addThresholds: false,
|
||||||
alertingMinSeverity: 3,
|
alertingMinSeverity: 3,
|
||||||
disableReadOnlyUsersAck: false,
|
disableReadOnlyUsersAck: false,
|
||||||
zabbixVersion: 3,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ZabbixDSConfigController {
|
export class ZabbixDSConfigController {
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, $injector, datasourceSrv) {
|
constructor() {
|
||||||
this.datasourceSrv = datasourceSrv;
|
|
||||||
|
|
||||||
this.current.jsonData = migrateDSConfig(this.current.jsonData);
|
this.current.jsonData = migrateDSConfig(this.current.jsonData);
|
||||||
_.defaults(this.current.jsonData, defaultConfig);
|
_.defaults(this.current.jsonData, defaultConfig);
|
||||||
|
|
||||||
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
|
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
|
||||||
this.dbDataSources = this.getSupportedDBDataSources();
|
this.dbDataSources = this.getSupportedDBDataSources();
|
||||||
this.zabbixVersions = _.cloneDeep(zabbixVersions);
|
|
||||||
this.autoDetectZabbixVersion();
|
|
||||||
if (!this.dbConnectionDatasourceId) {
|
if (!this.dbConnectionDatasourceId) {
|
||||||
this.loadCurrentDBDatasource();
|
this.loadCurrentDBDatasource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportedDBDataSources() {
|
getSupportedDBDataSources() {
|
||||||
let datasources = this.datasourceSrv.getAll();
|
let datasources = getDataSourceSrv().getAll();
|
||||||
return _.filter(datasources, ds => {
|
return _.filter(datasources, ds => {
|
||||||
return _.includes(SUPPORTED_SQL_DS, ds.type);
|
return _.includes(SUPPORTED_SQL_DS, ds.type);
|
||||||
});
|
});
|
||||||
@@ -53,7 +43,7 @@ export class ZabbixDSConfigController {
|
|||||||
|
|
||||||
loadCurrentDBDatasource() {
|
loadCurrentDBDatasource() {
|
||||||
const dsName= this.current.jsonData.dbConnectionDatasourceName;
|
const dsName= this.current.jsonData.dbConnectionDatasourceName;
|
||||||
this.datasourceSrv.loadDatasource(dsName)
|
getDataSourceSrv().loadDatasource(dsName)
|
||||||
.then(ds => {
|
.then(ds => {
|
||||||
if (ds) {
|
if (ds) {
|
||||||
this.dbConnectionDatasourceId = ds.id;
|
this.dbConnectionDatasourceId = ds.id;
|
||||||
@@ -61,25 +51,6 @@ export class ZabbixDSConfigController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
autoDetectZabbixVersion() {
|
|
||||||
if (!this.current.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.datasourceSrv.loadDatasource(this.current.name)
|
|
||||||
.then(ds => {
|
|
||||||
return ds.getVersion();
|
|
||||||
})
|
|
||||||
.then(version => {
|
|
||||||
if (version) {
|
|
||||||
if (!_.find(zabbixVersions, ['value', version])) {
|
|
||||||
this.zabbixVersions.push({ name: version + '.x', value: version });
|
|
||||||
}
|
|
||||||
this.current.jsonData.zabbixVersion = version;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDBConnectionDatasourceChange() {
|
onDBConnectionDatasourceChange() {
|
||||||
this.current.jsonData.dbConnectionDatasourceId = this.dbConnectionDatasourceId;
|
this.current.jsonData.dbConnectionDatasourceId = this.dbConnectionDatasourceId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Plugin IDs
|
||||||
|
export const ZABBIX_PROBLEMS_PANEL_ID = 'alexanderzobnin-zabbix-triggers-panel';
|
||||||
|
export const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
|
||||||
|
|
||||||
// Data point
|
// Data point
|
||||||
export const DATAPOINT_VALUE = 0;
|
export const DATAPOINT_VALUE = 0;
|
||||||
export const DATAPOINT_TS = 1;
|
export const DATAPOINT_TS = 1;
|
||||||
@@ -8,6 +12,7 @@ export const MODE_ITSERVICE = 1;
|
|||||||
export const MODE_TEXT = 2;
|
export const MODE_TEXT = 2;
|
||||||
export const MODE_ITEMID = 3;
|
export const MODE_ITEMID = 3;
|
||||||
export const MODE_TRIGGERS = 4;
|
export const MODE_TRIGGERS = 4;
|
||||||
|
export const MODE_PROBLEMS = 5;
|
||||||
|
|
||||||
// Triggers severity
|
// Triggers severity
|
||||||
export const SEV_NOT_CLASSIFIED = 0;
|
export const SEV_NOT_CLASSIFIED = 0;
|
||||||
@@ -23,8 +28,10 @@ export const SHOW_OK_EVENTS = 1;
|
|||||||
|
|
||||||
// Acknowledge
|
// Acknowledge
|
||||||
export const ZBX_ACK_ACTION_NONE = 0;
|
export const ZBX_ACK_ACTION_NONE = 0;
|
||||||
|
export const ZBX_ACK_ACTION_CLOSE = 1;
|
||||||
export const ZBX_ACK_ACTION_ACK = 2;
|
export const ZBX_ACK_ACTION_ACK = 2;
|
||||||
export const ZBX_ACK_ACTION_ADD_MESSAGE = 4;
|
export const ZBX_ACK_ACTION_ADD_MESSAGE = 4;
|
||||||
|
export const ZBX_ACK_ACTION_CHANGE_SEVERITY = 8;
|
||||||
|
|
||||||
export const TRIGGER_SEVERITY = [
|
export const TRIGGER_SEVERITY = [
|
||||||
{val: 0, text: 'Not classified'},
|
{val: 0, text: 'Not classified'},
|
||||||
@@ -39,3 +46,5 @@ export const TRIGGER_SEVERITY = [
|
|||||||
export const MIN_SLA_INTERVAL = 3600;
|
export const MIN_SLA_INTERVAL = 3600;
|
||||||
|
|
||||||
export const RANGE_VARIABLE_VALUE = 'range_series';
|
export const RANGE_VARIABLE_VALUE = 'range_series';
|
||||||
|
|
||||||
|
export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001;
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
// Available in 7.0
|
||||||
|
// import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import ts, { groupBy_perf as groupBy } from './timeseries';
|
import ts, { groupBy_perf as groupBy } from './timeseries';
|
||||||
|
|
||||||
let SUM = ts.SUM;
|
const SUM = ts.SUM;
|
||||||
let COUNT = ts.COUNT;
|
const COUNT = ts.COUNT;
|
||||||
let AVERAGE = ts.AVERAGE;
|
const AVERAGE = ts.AVERAGE;
|
||||||
let MIN = ts.MIN;
|
const MIN = ts.MIN;
|
||||||
let MAX = ts.MAX;
|
const MAX = ts.MAX;
|
||||||
let MEDIAN = ts.MEDIAN;
|
const MEDIAN = ts.MEDIAN;
|
||||||
let PERCENTILE = ts.PERCENTILE;
|
const PERCENTILE = ts.PERCENTILE;
|
||||||
|
|
||||||
let downsampleSeries = ts.downsample;
|
const downsampleSeries = ts.downsample;
|
||||||
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
|
const groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
|
||||||
let sumSeries = ts.sumSeries;
|
const sumSeries = ts.sumSeries;
|
||||||
let delta = ts.delta;
|
const delta = ts.delta;
|
||||||
let rate = ts.rate;
|
const rate = ts.rate;
|
||||||
let scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
|
const scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
|
||||||
let offset = (delta, datapoints) => ts.offset(datapoints, delta);
|
const offset = (delta, datapoints) => ts.offset(datapoints, delta);
|
||||||
let simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
|
const simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
|
||||||
let expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
|
const expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
|
||||||
let percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
|
const percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
|
||||||
|
|
||||||
function limit(order, n, orderByFunc, timeseries) {
|
function limit(order, n, orderByFunc, timeseries) {
|
||||||
let orderByCallback = aggregationFunctions[orderByFunc];
|
const orderByCallback = aggregationFunctions[orderByFunc];
|
||||||
let sortByIteratee = (ts) => {
|
const sortByIteratee = (ts) => {
|
||||||
let values = _.map(ts.datapoints, (point) => {
|
const values = _.map(ts.datapoints, (point) => {
|
||||||
return point[0];
|
return point[0];
|
||||||
});
|
});
|
||||||
return orderByCallback(values);
|
return orderByCallback(values);
|
||||||
};
|
};
|
||||||
let sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
|
const sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
|
||||||
if (order === 'bottom') {
|
if (order === 'bottom') {
|
||||||
return sortedTimeseries.slice(0, n);
|
return sortedTimeseries.slice(0, n);
|
||||||
} else {
|
} else {
|
||||||
@@ -64,13 +66,17 @@ function transformNull(n, datapoints) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortSeries(direction, timeseries) {
|
function sortSeries(direction, timeseries: any[]) {
|
||||||
return _.orderBy(timeseries, [function (ts) {
|
return _.orderBy(timeseries, [ts => {
|
||||||
return ts.target.toLowerCase();
|
return ts.target.toLowerCase();
|
||||||
}], direction);
|
}], direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAlias(alias, timeseries) {
|
function setAlias(alias, timeseries) {
|
||||||
|
// TODO: use getTemplateSrv() when available (since 7.0)
|
||||||
|
if (this.templateSrv && timeseries && timeseries.scopedVars) {
|
||||||
|
alias = this.templateSrv.replace(alias, timeseries.scopedVars);
|
||||||
|
}
|
||||||
timeseries.target = alias;
|
timeseries.target = alias;
|
||||||
return timeseries;
|
return timeseries;
|
||||||
}
|
}
|
||||||
@@ -84,6 +90,10 @@ function replaceAlias(regexp, newAlias, timeseries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let alias = timeseries.target.replace(pattern, newAlias);
|
let alias = timeseries.target.replace(pattern, newAlias);
|
||||||
|
// TODO: use getTemplateSrv() when available (since 7.0)
|
||||||
|
if (this.templateSrv && timeseries && timeseries.scopedVars) {
|
||||||
|
alias = this.templateSrv.replace(alias, timeseries.scopedVars);
|
||||||
|
}
|
||||||
timeseries.target = alias;
|
timeseries.target = alias;
|
||||||
return timeseries;
|
return timeseries;
|
||||||
}
|
}
|
||||||
@@ -94,14 +104,13 @@ function setAliasByRegex(alias, timeseries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractText(str, pattern) {
|
function extractText(str, pattern) {
|
||||||
var extractPattern = new RegExp(pattern);
|
const extractPattern = new RegExp(pattern);
|
||||||
var extractedValue = extractPattern.exec(str);
|
const extractedValue = extractPattern.exec(str);
|
||||||
extractedValue = extractedValue[0];
|
return extractedValue[0];
|
||||||
return extractedValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupByWrapper(interval, groupFunc, datapoints) {
|
function groupByWrapper(interval, groupFunc, datapoints) {
|
||||||
var groupByCallback = aggregationFunctions[groupFunc];
|
const groupByCallback = aggregationFunctions[groupFunc];
|
||||||
return groupBy(datapoints, interval, groupByCallback);
|
return groupBy(datapoints, interval, groupByCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +119,12 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) {
|
|||||||
const flattenedPoints = ts.flattenDatapoints(datapoints);
|
const flattenedPoints = ts.flattenDatapoints(datapoints);
|
||||||
// groupBy_perf works with sorted series only
|
// groupBy_perf works with sorted series only
|
||||||
const sortedPoints = ts.sortByTime(flattenedPoints);
|
const sortedPoints = ts.sortByTime(flattenedPoints);
|
||||||
let groupByCallback = aggregationFunctions[aggregateFunc];
|
const groupByCallback = aggregationFunctions[aggregateFunc];
|
||||||
return groupBy(sortedPoints, interval, groupByCallback);
|
return groupBy(sortedPoints, interval, groupByCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function aggregateWrapper(groupByCallback, interval, datapoints) {
|
function aggregateWrapper(groupByCallback, interval, datapoints) {
|
||||||
var flattenedPoints = ts.flattenDatapoints(datapoints);
|
const flattenedPoints = ts.flattenDatapoints(datapoints);
|
||||||
// groupBy_perf works with sorted series only
|
// groupBy_perf works with sorted series only
|
||||||
const sortedPoints = ts.sortByTime(flattenedPoints);
|
const sortedPoints = ts.sortByTime(flattenedPoints);
|
||||||
return groupBy(sortedPoints, interval, groupByCallback);
|
return groupBy(sortedPoints, interval, groupByCallback);
|
||||||
@@ -125,19 +134,19 @@ function percentileAgg(interval, n, datapoints) {
|
|||||||
const flattenedPoints = ts.flattenDatapoints(datapoints);
|
const flattenedPoints = ts.flattenDatapoints(datapoints);
|
||||||
// groupBy_perf works with sorted series only
|
// groupBy_perf works with sorted series only
|
||||||
const sortedPoints = ts.sortByTime(flattenedPoints);
|
const sortedPoints = ts.sortByTime(flattenedPoints);
|
||||||
let groupByCallback = _.partial(PERCENTILE, n);
|
const groupByCallback = _.partial(PERCENTILE, n);
|
||||||
return groupBy(sortedPoints, interval, groupByCallback);
|
return groupBy(sortedPoints, interval, groupByCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeShift(interval, range) {
|
function timeShift(interval, range) {
|
||||||
let shift = utils.parseTimeShiftInterval(interval) / 1000;
|
const shift = utils.parseTimeShiftInterval(interval) / 1000;
|
||||||
return _.map(range, time => {
|
return _.map(range, time => {
|
||||||
return time - shift;
|
return time - shift;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function unShiftTimeSeries(interval, datapoints) {
|
function unShiftTimeSeries(interval, datapoints) {
|
||||||
let unshift = utils.parseTimeShiftInterval(interval);
|
const unshift = utils.parseTimeShiftInterval(interval);
|
||||||
return _.map(datapoints, dp => {
|
return _.map(datapoints, dp => {
|
||||||
return [
|
return [
|
||||||
dp[0],
|
dp[0],
|
||||||
@@ -146,7 +155,7 @@ function unShiftTimeSeries(interval, datapoints) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let metricFunctions = {
|
const metricFunctions = {
|
||||||
groupBy: groupByWrapper,
|
groupBy: groupByWrapper,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
@@ -177,7 +186,7 @@ let metricFunctions = {
|
|||||||
replaceAlias: replaceAlias
|
replaceAlias: replaceAlias
|
||||||
};
|
};
|
||||||
|
|
||||||
let aggregationFunctions = {
|
const aggregationFunctions = {
|
||||||
avg: AVERAGE,
|
avg: AVERAGE,
|
||||||
min: MIN,
|
min: MIN,
|
||||||
max: MAX,
|
max: MAX,
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import config from 'grafana/app/core/config';
|
import config from 'grafana/app/core/config';
|
||||||
|
import { contextSrv } from 'grafana/app/core/core';
|
||||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import * as migrations from './migrations';
|
import * as migrations from './migrations';
|
||||||
@@ -7,34 +8,44 @@ import * as metricFunctions from './metricFunctions';
|
|||||||
import * as c from './constants';
|
import * as c from './constants';
|
||||||
import dataProcessor from './dataProcessor';
|
import dataProcessor from './dataProcessor';
|
||||||
import responseHandler from './responseHandler';
|
import responseHandler from './responseHandler';
|
||||||
|
import problemsHandler from './problemsHandler';
|
||||||
import { Zabbix } from './zabbix/zabbix';
|
import { Zabbix } from './zabbix/zabbix';
|
||||||
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
|
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
|
||||||
import {
|
import { VariableQueryTypes, ShowProblemTypes } from './types';
|
||||||
DataSourceApi,
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
// DataSourceInstanceSettings,
|
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
|
||||||
} from '@grafana/data';
|
|
||||||
// import { BackendSrv, DataSourceSrv } from '@grafana/runtime';
|
|
||||||
// import { ZabbixAlertingService } from './zabbixAlerting.service';
|
|
||||||
// import { ZabbixConnectionTestQuery, ZabbixConnectionInfo, TemplateSrv, TSDBResponse } from './types';
|
|
||||||
import { VariableQueryTypes } from './types';
|
|
||||||
|
|
||||||
const DEFAULT_ZABBIX_VERSION = 3;
|
|
||||||
|
|
||||||
export class ZabbixDatasource extends DataSourceApi {
|
export class ZabbixDatasource extends DataSourceApi {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
basicAuth: any;
|
||||||
|
withCredentials: any;
|
||||||
|
|
||||||
/**
|
username: string;
|
||||||
* @ngInject
|
password: string;
|
||||||
* @param {DataSourceInstanceSettings} instanceSettings
|
trends: boolean;
|
||||||
* @param {TemplateSrv} templateSrv
|
trendsFrom: string;
|
||||||
* @param {BackendSrv} backendSrv
|
trendsRange: string;
|
||||||
* @param {DataSourceSrv} datasourceSrv
|
cacheTTL: any;
|
||||||
* @param {ZabbixAlertingService} zabbixAlertingSrv
|
alertingEnabled: boolean;
|
||||||
*/
|
addThresholds: boolean;
|
||||||
constructor(instanceSettings, templateSrv, backendSrv, datasourceSrv, zabbixAlertingSrv) {
|
alertingMinSeverity: string;
|
||||||
|
disableReadOnlyUsersAck: boolean;
|
||||||
|
enableDirectDBConnection: boolean;
|
||||||
|
dbConnectionDatasourceId: number;
|
||||||
|
dbConnectionDatasourceName: string;
|
||||||
|
dbConnectionRetentionPolicy: string;
|
||||||
|
enableDebugLog: boolean;
|
||||||
|
datasourceId: number;
|
||||||
|
zabbix: Zabbix;
|
||||||
|
|
||||||
|
replaceTemplateVars: (target: any, scopedVars?: any) => any;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor(instanceSettings: DataSourceInstanceSettings, private templateSrv, private zabbixAlertingSrv) {
|
||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
this.type = 'zabbix';
|
|
||||||
this.templateSrv = templateSrv;
|
this.templateSrv = templateSrv;
|
||||||
this.backendSrv = backendSrv;
|
|
||||||
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
this.zabbixAlertingSrv = zabbixAlertingSrv;
|
||||||
|
|
||||||
this.enableDebugLog = config.buildInfo.env === 'development';
|
this.enableDebugLog = config.buildInfo.env === 'development';
|
||||||
@@ -61,7 +72,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
this.trendsRange = jsonData.trendsRange || '4d';
|
this.trendsRange = jsonData.trendsRange || '4d';
|
||||||
|
|
||||||
// Set cache update interval
|
// Set cache update interval
|
||||||
var ttl = jsonData.cacheTTL || '1h';
|
const ttl = jsonData.cacheTTL || '1h';
|
||||||
this.cacheTTL = utils.parseInterval(ttl);
|
this.cacheTTL = utils.parseInterval(ttl);
|
||||||
|
|
||||||
// Alerting options
|
// Alerting options
|
||||||
@@ -71,7 +82,6 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
|
|
||||||
// Other options
|
// Other options
|
||||||
this.disableReadOnlyUsersAck = jsonData.disableReadOnlyUsersAck;
|
this.disableReadOnlyUsersAck = jsonData.disableReadOnlyUsersAck;
|
||||||
this.zabbixVersion = jsonData.zabbixVersion || DEFAULT_ZABBIX_VERSION;
|
|
||||||
|
|
||||||
// Direct DB Connection options
|
// Direct DB Connection options
|
||||||
this.enableDirectDBConnection = jsonData.dbConnectionEnable || false;
|
this.enableDirectDBConnection = jsonData.dbConnectionEnable || false;
|
||||||
@@ -79,21 +89,21 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
|
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
|
||||||
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
|
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
|
||||||
|
|
||||||
let zabbixOptions = {
|
const zabbixOptions = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
basicAuth: this.basicAuth,
|
basicAuth: this.basicAuth,
|
||||||
withCredentials: this.withCredentials,
|
withCredentials: this.withCredentials,
|
||||||
zabbixVersion: this.zabbixVersion,
|
|
||||||
cacheTTL: this.cacheTTL,
|
cacheTTL: this.cacheTTL,
|
||||||
enableDirectDBConnection: this.enableDirectDBConnection,
|
enableDirectDBConnection: this.enableDirectDBConnection,
|
||||||
dbConnectionDatasourceId: this.dbConnectionDatasourceId,
|
dbConnectionDatasourceId: this.dbConnectionDatasourceId,
|
||||||
dbConnectionDatasourceName: this.dbConnectionDatasourceName,
|
dbConnectionDatasourceName: this.dbConnectionDatasourceName,
|
||||||
dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy,
|
dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy,
|
||||||
|
datasourceId: this.datasourceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.zabbix = new Zabbix(zabbixOptions, datasourceSrv, backendSrv, this.datasourceId);
|
this.zabbix = new Zabbix(zabbixOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////
|
////////////////////////
|
||||||
@@ -124,7 +134,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create request for each target
|
// Create request for each target
|
||||||
let promises = _.map(options.targets, t => {
|
const promises = _.map(options.targets, t => {
|
||||||
// Don't request for hidden targets
|
// Don't request for hidden targets
|
||||||
if (t.hide) {
|
if (t.hide) {
|
||||||
return [];
|
return [];
|
||||||
@@ -144,40 +154,45 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
this.replaceTargetVariables(target, options);
|
this.replaceTargetVariables(target, options);
|
||||||
|
|
||||||
// Apply Time-related functions (timeShift(), etc)
|
// Apply Time-related functions (timeShift(), etc)
|
||||||
let timeFunctions = bindFunctionDefs(target.functions, 'Time');
|
const timeFunctions = bindFunctionDefs(target.functions, 'Time');
|
||||||
if (timeFunctions.length) {
|
if (timeFunctions.length) {
|
||||||
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
|
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
|
||||||
timeFrom = time_from;
|
timeFrom = time_from;
|
||||||
timeTo = time_to;
|
timeTo = time_to;
|
||||||
}
|
}
|
||||||
let timeRange = [timeFrom, timeTo];
|
const timeRange = [timeFrom, timeTo];
|
||||||
|
|
||||||
let useTrends = this.isUseTrends(timeRange);
|
const useTrends = this.isUseTrends(timeRange);
|
||||||
|
|
||||||
// Metrics or Text query mode
|
// Metrics or Text query
|
||||||
if (!target.mode || target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT) {
|
if (!target.queryType || target.queryType === c.MODE_METRICS || target.queryType === c.MODE_TEXT) {
|
||||||
// Don't request undefined targets
|
// Don't request undefined targets
|
||||||
if (!target.group || !target.host || !target.item) {
|
if (!target.group || !target.host || !target.item) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target.mode || target.mode === c.MODE_METRICS) {
|
if (!target.queryType || target.queryType === c.MODE_METRICS) {
|
||||||
return this.queryNumericData(target, timeRange, useTrends, options);
|
return this.queryNumericData(target, timeRange, useTrends, options);
|
||||||
} else if (target.mode === c.MODE_TEXT) {
|
} else if (target.queryType === c.MODE_TEXT) {
|
||||||
return this.queryTextData(target, timeRange);
|
return this.queryTextData(target, timeRange);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
} else if (target.mode === c.MODE_ITEMID) {
|
} else if (target.queryType === c.MODE_ITEMID) {
|
||||||
// Item ID mode
|
// Item ID query
|
||||||
if (!target.itemids) {
|
if (!target.itemids) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return this.queryItemIdData(target, timeRange, useTrends, options);
|
return this.queryItemIdData(target, timeRange, useTrends, options);
|
||||||
} else if (target.mode === c.MODE_ITSERVICE) {
|
} else if (target.queryType === c.MODE_ITSERVICE) {
|
||||||
// IT services mode
|
// IT services query
|
||||||
return this.queryITServiceData(target, timeRange, options);
|
return this.queryITServiceData(target, timeRange, options);
|
||||||
} else if (target.mode === c.MODE_TRIGGERS) {
|
} else if (target.queryType === c.MODE_TRIGGERS) {
|
||||||
// Triggers mode
|
// Triggers query
|
||||||
return this.queryTriggersData(target, timeRange);
|
return this.queryTriggersData(target, timeRange);
|
||||||
|
} else if (target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
// Problems query
|
||||||
|
return this.queryProblems(target, timeRange, options);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -192,7 +207,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
doTsdbRequest(options) {
|
doTsdbRequest(options) {
|
||||||
const tsdbRequestData = {
|
const tsdbRequestData: any = {
|
||||||
queries: options.targets.map(target => {
|
queries: options.targets.map(target => {
|
||||||
target.datasourceId = this.datasourceId;
|
target.datasourceId = this.datasourceId;
|
||||||
target.queryType = 'zabbixAPI';
|
target.queryType = 'zabbixAPI';
|
||||||
@@ -205,7 +220,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
tsdbRequestData.to = options.range.to.valueOf().toString();
|
tsdbRequestData.to = options.range.to.valueOf().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.backendSrv.post('/api/tsdb/query', tsdbRequestData);
|
return getBackendSrv().post('/api/tsdb/query', tsdbRequestData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -224,15 +239,15 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.backendSrv.post('/api/tsdb/query', tsdbRequestData);
|
return getBackendSrv().post('/api/tsdb/query', tsdbRequestData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query target data for Metrics mode
|
* Query target data for Metrics
|
||||||
*/
|
*/
|
||||||
queryNumericData(target, timeRange, useTrends, options) {
|
queryNumericData(target, timeRange, useTrends, options) {
|
||||||
let queryStart, queryEnd;
|
let queryStart, queryEnd;
|
||||||
let getItemOptions = {
|
const getItemOptions = {
|
||||||
itemtype: 'num'
|
itemtype: 'num'
|
||||||
};
|
};
|
||||||
return this.zabbix.getItemsFromTarget(target, getItemOptions)
|
return this.zabbix.getItemsFromTarget(target, getItemOptions)
|
||||||
@@ -242,7 +257,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}).then(result => {
|
}).then(result => {
|
||||||
queryEnd = new Date().getTime();
|
queryEnd = new Date().getTime();
|
||||||
if (this.enableDebugLog) {
|
if (this.enableDebugLog) {
|
||||||
console.debug(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
|
console.log(`Datasource::Performance Query Time (${this.name}): ${queryEnd - queryStart}`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@@ -269,18 +284,18 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
|
|
||||||
getTrendValueType(target) {
|
getTrendValueType(target) {
|
||||||
// Find trendValue() function and get specified trend value
|
// Find trendValue() function and get specified trend value
|
||||||
var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
|
const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
|
||||||
var trendValueFunc = _.find(target.functions, func => {
|
const trendValueFunc = _.find(target.functions, func => {
|
||||||
return _.includes(trendFunctions, func.def.name);
|
return _.includes(trendFunctions, func.def.name);
|
||||||
});
|
});
|
||||||
return trendValueFunc ? trendValueFunc.params[0] : "avg";
|
return trendValueFunc ? trendValueFunc.params[0] : "avg";
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDataProcessingFunctions(timeseries_data, target) {
|
applyDataProcessingFunctions(timeseries_data, target) {
|
||||||
let transformFunctions = bindFunctionDefs(target.functions, 'Transform');
|
const transformFunctions = bindFunctionDefs(target.functions, 'Transform');
|
||||||
let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
|
const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
|
||||||
let filterFunctions = bindFunctionDefs(target.functions, 'Filter');
|
const filterFunctions = bindFunctionDefs(target.functions, 'Filter');
|
||||||
let aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
|
const aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
|
||||||
|
|
||||||
// Apply transformation functions
|
// Apply transformation functions
|
||||||
timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => {
|
timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => {
|
||||||
@@ -298,8 +313,8 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
let dp = _.map(timeseries_data, 'datapoints');
|
let dp = _.map(timeseries_data, 'datapoints');
|
||||||
dp = utils.sequence(aggregationFunctions)(dp);
|
dp = utils.sequence(aggregationFunctions)(dp);
|
||||||
|
|
||||||
let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
|
const aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
|
||||||
let lastAgg = _.findLast(target.functions, func => {
|
const lastAgg = _.findLast(target.functions, func => {
|
||||||
return _.includes(aggFuncNames, func.def.name);
|
return _.includes(aggFuncNames, func.def.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,7 +325,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply alias functions
|
// Apply alias functions
|
||||||
_.forEach(timeseries_data, utils.sequence(aliasFunctions));
|
_.forEach(timeseries_data, utils.sequence(aliasFunctions).bind(this));
|
||||||
|
|
||||||
// Apply Time-related functions (timeShift(), etc)
|
// Apply Time-related functions (timeShift(), etc)
|
||||||
// Find timeShift() function and get specified trend value
|
// Find timeShift() function and get specified trend value
|
||||||
@@ -321,11 +336,11 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
|
|
||||||
applyTimeShiftFunction(timeseries_data, target) {
|
applyTimeShiftFunction(timeseries_data, target) {
|
||||||
// Find timeShift() function and get specified interval
|
// Find timeShift() function and get specified interval
|
||||||
let timeShiftFunc = _.find(target.functions, (func) => {
|
const timeShiftFunc = _.find(target.functions, (func) => {
|
||||||
return func.def.name === 'timeShift';
|
return func.def.name === 'timeShift';
|
||||||
});
|
});
|
||||||
if (timeShiftFunc) {
|
if (timeShiftFunc) {
|
||||||
let shift = timeShiftFunc.params[0];
|
const shift = timeShiftFunc.params[0];
|
||||||
_.forEach(timeseries_data, (series) => {
|
_.forEach(timeseries_data, (series) => {
|
||||||
series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints);
|
series.datapoints = dataProcessor.unShiftTimeSeries(shift, series.datapoints);
|
||||||
});
|
});
|
||||||
@@ -333,10 +348,10 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query target data for Text mode
|
* Query target data for Text
|
||||||
*/
|
*/
|
||||||
queryTextData(target, timeRange) {
|
queryTextData(target, timeRange) {
|
||||||
let options = {
|
const options = {
|
||||||
itemtype: 'text'
|
itemtype: 'text'
|
||||||
};
|
};
|
||||||
return this.zabbix.getItemsFromTarget(target, options)
|
return this.zabbix.getItemsFromTarget(target, options)
|
||||||
@@ -346,7 +361,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query target data for Item ID mode
|
* Query target data for Item ID
|
||||||
*/
|
*/
|
||||||
queryItemIdData(target, timeRange, useTrends, options) {
|
queryItemIdData(target, timeRange, useTrends, options) {
|
||||||
let itemids = target.itemids;
|
let itemids = target.itemids;
|
||||||
@@ -364,7 +379,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query target data for IT Services mode
|
* Query target data for IT Services
|
||||||
*/
|
*/
|
||||||
queryITServiceData(target, timeRange, options) {
|
queryITServiceData(target, timeRange, options) {
|
||||||
// Don't show undefined and hidden targets
|
// Don't show undefined and hidden targets
|
||||||
@@ -382,21 +397,26 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars);
|
itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.slaInterval = target.slaInterval;
|
||||||
|
|
||||||
return this.zabbix.getITServices(itServiceFilter)
|
return this.zabbix.getITServices(itServiceFilter)
|
||||||
.then(itservices => {
|
.then(itservices => {
|
||||||
|
if (options.isOldVersion) {
|
||||||
|
itservices = _.filter(itservices, {'serviceid': target.itservice?.serviceid});
|
||||||
|
}
|
||||||
return this.zabbix.getSLA(itservices, timeRange, target, options);})
|
return this.zabbix.getSLA(itservices, timeRange, target, options);})
|
||||||
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
|
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
|
||||||
}
|
}
|
||||||
|
|
||||||
queryTriggersData(target, timeRange) {
|
queryTriggersData(target, timeRange) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
return this.zabbix.getHostsFromTarget(target)
|
return this.zabbix.getHostsFromTarget(target)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
let [hosts, apps] = results;
|
const [hosts, apps] = results;
|
||||||
if (hosts.length) {
|
if (hosts.length) {
|
||||||
let hostids = _.map(hosts, 'hostid');
|
const hostids = _.map(hosts, 'hostid');
|
||||||
let appids = _.map(apps, 'applicationid');
|
const appids = _.map(apps, 'applicationid');
|
||||||
let options = {
|
const options = {
|
||||||
minSeverity: target.triggers.minSeverity,
|
minSeverity: target.triggers.minSeverity,
|
||||||
acknowledged: target.triggers.acknowledged,
|
acknowledged: target.triggers.acknowledged,
|
||||||
count: target.triggers.count,
|
count: target.triggers.count,
|
||||||
@@ -417,6 +437,78 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryProblems(target, timeRange, options) {
|
||||||
|
const [timeFrom, timeTo] = timeRange;
|
||||||
|
const userIsEditor = contextSrv.isEditor || contextSrv.isGrafanaAdmin;
|
||||||
|
|
||||||
|
let proxies;
|
||||||
|
let showAckButton = true;
|
||||||
|
|
||||||
|
const showProblems = target.showProblems || ShowProblemTypes.Problems;
|
||||||
|
const showProxy = target.options.hostProxy;
|
||||||
|
|
||||||
|
const getProxiesPromise = showProxy ? this.zabbix.getProxies() : () => [];
|
||||||
|
showAckButton = !this.disableReadOnlyUsersAck || userIsEditor;
|
||||||
|
|
||||||
|
// Replace template variables
|
||||||
|
const groupFilter = this.replaceTemplateVars(target.group?.filter, options.scopedVars);
|
||||||
|
const hostFilter = this.replaceTemplateVars(target.host?.filter, options.scopedVars);
|
||||||
|
const appFilter = this.replaceTemplateVars(target.application?.filter, options.scopedVars);
|
||||||
|
const proxyFilter = this.replaceTemplateVars(target.proxy?.filter, options.scopedVars);
|
||||||
|
|
||||||
|
const triggerFilter = this.replaceTemplateVars(target.trigger?.filter, options.scopedVars);
|
||||||
|
const tagsFilter = this.replaceTemplateVars(target.tags?.filter, options.scopedVars);
|
||||||
|
|
||||||
|
const replacedTarget = {
|
||||||
|
...target,
|
||||||
|
trigger: { filter: triggerFilter },
|
||||||
|
tags: { filter: tagsFilter },
|
||||||
|
};
|
||||||
|
|
||||||
|
const problemsOptions: any = {
|
||||||
|
recent: showProblems === ShowProblemTypes.Recent,
|
||||||
|
minSeverity: target.options?.minSeverity,
|
||||||
|
limit: target.options?.limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (target.options?.acknowledged === 0 || target.options?.acknowledged === 1) {
|
||||||
|
problemsOptions.acknowledged = target.options?.acknowledged ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.options?.minSeverity) {
|
||||||
|
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= target.options?.minSeverity);
|
||||||
|
problemsOptions.severities = severities;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showProblems === ShowProblemTypes.History) {
|
||||||
|
problemsOptions.timeFrom = timeFrom;
|
||||||
|
problemsOptions.timeTo = timeTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProblemsPromise = showProblems === ShowProblemTypes.History ?
|
||||||
|
this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions) :
|
||||||
|
this.zabbix.getProblems(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions);
|
||||||
|
|
||||||
|
const problemsPromises = Promise.all([
|
||||||
|
getProblemsPromise,
|
||||||
|
getProxiesPromise
|
||||||
|
])
|
||||||
|
.then(([problems, sourceProxies]) => {
|
||||||
|
proxies = _.keyBy(sourceProxies, 'proxyid');
|
||||||
|
return problems;
|
||||||
|
})
|
||||||
|
.then(problems => problemsHandler.setMaintenanceStatus(problems))
|
||||||
|
.then(problems => problemsHandler.setAckButtonStatus(problems, showAckButton))
|
||||||
|
.then(problems => problemsHandler.filterTriggersPre(problems, replacedTarget))
|
||||||
|
.then(problems => problemsHandler.addTriggerDataSource(problems, target))
|
||||||
|
.then(problems => problemsHandler.addTriggerHostProxy(problems, proxies));
|
||||||
|
|
||||||
|
return problemsPromises.then(problems => {
|
||||||
|
const problemsDataFrame = problemsHandler.toDataFrame(problems);
|
||||||
|
return problemsDataFrame;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test connection to Zabbix API and external history DB.
|
* Test connection to Zabbix API and external history DB.
|
||||||
*/
|
*/
|
||||||
@@ -436,30 +528,26 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
message: message
|
message: message
|
||||||
};
|
};
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
|
||||||
if (error instanceof ZabbixAPIError) {
|
if (error instanceof ZabbixAPIError) {
|
||||||
return {
|
return {
|
||||||
status: "error",
|
status: "error",
|
||||||
title: error.message,
|
title: error.message,
|
||||||
message: error.message
|
message: error.message
|
||||||
};
|
};
|
||||||
}
|
} else if (error.data && error.data.message) {
|
||||||
else if (error.data && error.data.message) {
|
|
||||||
return {
|
return {
|
||||||
status: "error",
|
status: "error",
|
||||||
title: "Zabbix Client Error",
|
title: "Zabbix Client Error",
|
||||||
message: error.data.message
|
message: error.data.message
|
||||||
};
|
};
|
||||||
}
|
} else if (typeof (error) === 'string') {
|
||||||
else if (typeof (error) === 'string') {
|
|
||||||
return {
|
return {
|
||||||
status: "error",
|
status: "error",
|
||||||
title: "Unknown Error",
|
title: "Unknown Error",
|
||||||
message: error
|
message: error
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return {
|
return {
|
||||||
status: "error",
|
status: "error",
|
||||||
@@ -470,20 +558,6 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Zabbix version
|
|
||||||
*/
|
|
||||||
getVersion() {
|
|
||||||
return this.zabbix.getVersion()
|
|
||||||
.then(version => {
|
|
||||||
const zabbixVersion = utils.parseVersion(version);
|
|
||||||
if (!zabbixVersion) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return zabbixVersion.major;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// Templating //
|
// Templating //
|
||||||
////////////////
|
////////////////
|
||||||
@@ -495,7 +569,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
* @return {string} Metric name - group, host, app or item or list
|
* @return {string} Metric name - group, host, app or item or list
|
||||||
* of metrics in "{metric1,metcic2,...,metricN}" format.
|
* of metrics in "{metric1,metcic2,...,metricN}" format.
|
||||||
*/
|
*/
|
||||||
metricFindQuery(query) {
|
metricFindQuery(query, options) {
|
||||||
let resultPromise;
|
let resultPromise;
|
||||||
let queryModel = _.cloneDeep(query);
|
let queryModel = _.cloneDeep(query);
|
||||||
|
|
||||||
@@ -512,6 +586,8 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {});
|
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { group, host, application, item } = queryModel;
|
||||||
|
|
||||||
switch (queryModel.queryType) {
|
switch (queryModel.queryType) {
|
||||||
case VariableQueryTypes.Group:
|
case VariableQueryTypes.Group:
|
||||||
resultPromise = this.zabbix.getGroups(queryModel.group);
|
resultPromise = this.zabbix.getGroups(queryModel.group);
|
||||||
@@ -525,6 +601,10 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
case VariableQueryTypes.Item:
|
case VariableQueryTypes.Item:
|
||||||
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item);
|
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item);
|
||||||
break;
|
break;
|
||||||
|
case VariableQueryTypes.ItemValues:
|
||||||
|
const range = options?.range;
|
||||||
|
resultPromise = this.zabbix.getItemValues(group, host, application, item, { range });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
resultPromise = Promise.resolve([]);
|
resultPromise = Promise.resolve([]);
|
||||||
break;
|
break;
|
||||||
@@ -543,72 +623,64 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
const timeRange = options.range || options.rangeRaw;
|
const timeRange = options.range || options.rangeRaw;
|
||||||
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
|
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
|
||||||
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
|
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
|
||||||
var annotation = options.annotation;
|
const annotation = options.annotation;
|
||||||
var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS;
|
|
||||||
|
|
||||||
// Show all triggers
|
// Show all triggers
|
||||||
let triggersOptions = {
|
const problemsOptions: any = {
|
||||||
showTriggers: c.SHOW_ALL_TRIGGERS,
|
value: annotation.showOkEvents ? ['0', '1'] : '1',
|
||||||
hideHostsInMaintenance: false
|
valueFromEvent: true,
|
||||||
|
timeFrom,
|
||||||
|
timeTo,
|
||||||
};
|
};
|
||||||
|
|
||||||
var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}),
|
if (annotation.minseverity) {
|
||||||
this.replaceTemplateVars(annotation.host, {}),
|
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity));
|
||||||
this.replaceTemplateVars(annotation.application, {}),
|
problemsOptions.severities = severities;
|
||||||
triggersOptions);
|
|
||||||
|
|
||||||
return getTriggers.then(triggers => {
|
|
||||||
|
|
||||||
// Filter triggers by description
|
|
||||||
let triggerName = this.replaceTemplateVars(annotation.trigger, {});
|
|
||||||
if (utils.isRegex(triggerName)) {
|
|
||||||
triggers = _.filter(triggers, trigger => {
|
|
||||||
return utils.buildRegex(triggerName).test(trigger.description);
|
|
||||||
});
|
|
||||||
} else if (triggerName) {
|
|
||||||
triggers = _.filter(triggers, trigger => {
|
|
||||||
return trigger.description === triggerName;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove events below the chose severity
|
const groupFilter = this.replaceTemplateVars(annotation.group, {});
|
||||||
triggers = _.filter(triggers, trigger => {
|
const hostFilter = this.replaceTemplateVars(annotation.host, {});
|
||||||
return Number(trigger.priority) >= Number(annotation.minseverity);
|
const appFilter = this.replaceTemplateVars(annotation.application, {});
|
||||||
});
|
const proxyFilter = undefined;
|
||||||
|
|
||||||
var objectids = _.map(triggers, 'triggerid');
|
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
|
||||||
return this.zabbix
|
.then(problems => {
|
||||||
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
|
// Filter triggers by description
|
||||||
.then(events => {
|
const problemName = this.replaceTemplateVars(annotation.trigger, {});
|
||||||
var indexedTriggers = _.keyBy(triggers, 'triggerid');
|
if (utils.isRegex(problemName)) {
|
||||||
|
problems = _.filter(problems, p => {
|
||||||
|
return utils.buildRegex(problemName).test(p.description);
|
||||||
|
});
|
||||||
|
} else if (problemName) {
|
||||||
|
problems = _.filter(problems, p => {
|
||||||
|
return p.description === problemName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Hide acknowledged events if option enabled
|
// Hide acknowledged events if option enabled
|
||||||
if (annotation.hideAcknowledged) {
|
if (annotation.hideAcknowledged) {
|
||||||
events = _.filter(events, event => {
|
problems = _.filter(problems, p => {
|
||||||
return !event.acknowledges.length;
|
return !p.acknowledges?.length;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.map(events, event => {
|
return _.map(problems, p => {
|
||||||
let tags;
|
const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges);
|
||||||
|
|
||||||
|
let annotationTags: string[] = [];
|
||||||
if (annotation.showHostname) {
|
if (annotation.showHostname) {
|
||||||
tags = _.map(event.hosts, 'name');
|
annotationTags = _.map(p.hosts, 'name');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show event type (OK or Problem)
|
|
||||||
let title = Number(event.value) ? 'Problem' : 'OK';
|
|
||||||
|
|
||||||
let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges);
|
|
||||||
return {
|
return {
|
||||||
|
title: p.value === '1' ? 'Problem' : 'OK',
|
||||||
|
time: p.timestamp * 1000,
|
||||||
annotation: annotation,
|
annotation: annotation,
|
||||||
time: event.clock * 1000,
|
text: p.name + formattedAcknowledges,
|
||||||
title: title,
|
tags: annotationTags,
|
||||||
tags: tags,
|
|
||||||
text: indexedTriggers[event.objectid].description + formatted_acknowledges
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -617,8 +689,8 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
* or empty object if no related triggers are finded.
|
* or empty object if no related triggers are finded.
|
||||||
*/
|
*/
|
||||||
alertQuery(options) {
|
alertQuery(options) {
|
||||||
let enabled_targets = filterEnabledTargets(options.targets);
|
const enabled_targets = filterEnabledTargets(options.targets);
|
||||||
let getPanelItems = _.map(enabled_targets, t => {
|
const getPanelItems = _.map(enabled_targets, t => {
|
||||||
let target = _.cloneDeep(t);
|
let target = _.cloneDeep(t);
|
||||||
target = migrations.migrate(target);
|
target = migrations.migrate(target);
|
||||||
this.replaceTargetVariables(target, options);
|
this.replaceTargetVariables(target, options);
|
||||||
@@ -627,8 +699,8 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
|
|
||||||
return Promise.all(getPanelItems)
|
return Promise.all(getPanelItems)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
let items = _.flatten(results);
|
const items = _.flatten(results);
|
||||||
let itemids = _.map(items, 'itemid');
|
const itemids = _.map(items, 'itemid');
|
||||||
|
|
||||||
if (itemids.length === 0) {
|
if (itemids.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -646,12 +718,12 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
|
|
||||||
let state = 'ok';
|
let state = 'ok';
|
||||||
|
|
||||||
let firedTriggers = _.filter(triggers, {value: '1'});
|
const firedTriggers = _.filter(triggers, {value: '1'});
|
||||||
if (firedTriggers.length) {
|
if (firedTriggers.length) {
|
||||||
state = 'alerting';
|
state = 'alerting';
|
||||||
}
|
}
|
||||||
|
|
||||||
let thresholds = _.map(triggers, trigger => {
|
const thresholds = _.map(triggers, trigger => {
|
||||||
return getTriggerThreshold(trigger.expression);
|
return getTriggerThreshold(trigger.expression);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -665,7 +737,7 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
|
|
||||||
// Replace template variables
|
// Replace template variables
|
||||||
replaceTargetVariables(target, options) {
|
replaceTargetVariables(target, options) {
|
||||||
let parts = ['group', 'host', 'application', 'item'];
|
const parts = ['group', 'host', 'application', 'item'];
|
||||||
_.forEach(parts, p => {
|
_.forEach(parts, p => {
|
||||||
if (target[p] && target[p].filter) {
|
if (target[p] && target[p].filter) {
|
||||||
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
|
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
|
||||||
@@ -685,10 +757,10 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isUseTrends(timeRange) {
|
isUseTrends(timeRange) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
let useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
|
const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
|
||||||
let useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
|
const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
|
||||||
let useTrends = this.trends && (
|
const useTrends = this.trends && (
|
||||||
(timeFrom < useTrendsFrom) ||
|
(timeFrom < useTrendsFrom) ||
|
||||||
(timeTo - timeFrom > useTrendsRange)
|
(timeTo - timeFrom > useTrendsRange)
|
||||||
);
|
);
|
||||||
@@ -697,20 +769,20 @@ export class ZabbixDatasource extends DataSourceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindFunctionDefs(functionDefs, category) {
|
function bindFunctionDefs(functionDefs, category) {
|
||||||
var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
|
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
|
||||||
var aggFuncDefs = _.filter(functionDefs, function(func) {
|
const aggFuncDefs = _.filter(functionDefs, func => {
|
||||||
return _.includes(aggregationFunctions, func.def.name);
|
return _.includes(aggregationFunctions, func.def.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
return _.map(aggFuncDefs, function(func) {
|
return _.map(aggFuncDefs, func => {
|
||||||
var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
|
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
|
||||||
return funcInstance.bindFunction(dataProcessor.metricFunctions);
|
return funcInstance.bindFunction(dataProcessor.metricFunctions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConsolidateBy(target) {
|
function getConsolidateBy(target) {
|
||||||
let consolidateBy;
|
let consolidateBy;
|
||||||
let funcDef = _.find(target.functions, func => {
|
const funcDef = _.find(target.functions, func => {
|
||||||
return func.def.name === 'consolidateBy';
|
return func.def.name === 'consolidateBy';
|
||||||
});
|
});
|
||||||
if (funcDef && funcDef.params && funcDef.params.length) {
|
if (funcDef && funcDef.params && funcDef.params.length) {
|
||||||
@@ -720,8 +792,8 @@ function getConsolidateBy(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downsampleSeries(timeseries_data, options) {
|
function downsampleSeries(timeseries_data, options) {
|
||||||
let defaultAgg = dataProcessor.aggregationFunctions['avg'];
|
const defaultAgg = dataProcessor.aggregationFunctions['avg'];
|
||||||
let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
const consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
|
||||||
return _.map(timeseries_data, timeseries => {
|
return _.map(timeseries_data, timeseries => {
|
||||||
if (timeseries.datapoints.length > options.maxDataPoints) {
|
if (timeseries.datapoints.length > options.maxDataPoints) {
|
||||||
timeseries.datapoints = dataProcessor
|
timeseries.datapoints = dataProcessor
|
||||||
@@ -753,7 +825,7 @@ export function zabbixTemplateFormat(value) {
|
|||||||
return utils.escapeRegex(value);
|
return utils.escapeRegex(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
var escapedValues = _.map(value, utils.escapeRegex);
|
const escapedValues = _.map(value, utils.escapeRegex);
|
||||||
return '(' + escapedValues.join('|') + ')';
|
return '(' + escapedValues.join('|') + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +845,7 @@ function zabbixItemIdsTemplateFormat(value) {
|
|||||||
* /$variable/ -> /a|b|c/ -> /a|b|c/
|
* /$variable/ -> /a|b|c/ -> /a|b|c/
|
||||||
*/
|
*/
|
||||||
function replaceTemplateVars(templateSrv, target, scopedVars) {
|
function replaceTemplateVars(templateSrv, target, scopedVars) {
|
||||||
var replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
|
let replacedTarget = templateSrv.replace(target, scopedVars, zabbixTemplateFormat);
|
||||||
if (target !== replacedTarget && !utils.isRegex(replacedTarget)) {
|
if (target !== replacedTarget && !utils.isRegex(replacedTarget)) {
|
||||||
replacedTarget = '/^' + replacedTarget + '$/';
|
replacedTarget = '/^' + replacedTarget + '$/';
|
||||||
}
|
}
|
||||||
@@ -787,8 +859,8 @@ function filterEnabledTargets(targets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerThreshold(expression) {
|
function getTriggerThreshold(expression) {
|
||||||
let thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
|
const thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
|
||||||
let finded_thresholds = expression.match(thresholdPattern);
|
const finded_thresholds = expression.match(thresholdPattern);
|
||||||
if (finded_thresholds && finded_thresholds.length >= 2) {
|
if (finded_thresholds && finded_thresholds.length >= 2) {
|
||||||
let threshold = finded_thresholds[1];
|
let threshold = finded_thresholds[1];
|
||||||
threshold = Number(threshold);
|
threshold = Number(threshold);
|
||||||
@@ -797,7 +869,3 @@ function getTriggerThreshold(expression) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for backward compatibility with lodash 2.4
|
|
||||||
if (!_.includes) {_.includes = _.contains;}
|
|
||||||
if (!_.keyBy) {_.keyBy = _.indexBy;}
|
|
||||||
1
src/datasource-zabbix/img/icn-zabbix-datasource.svg
Normal file
1
src/datasource-zabbix/img/icn-zabbix-datasource.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
@@ -1,107 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
version="1.0"
|
|
||||||
id="Layer_1"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
width="100px"
|
|
||||||
height="100px"
|
|
||||||
viewBox="692 0 100 100"
|
|
||||||
style="enable-background:new 692 0 100 100;"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="0.91 r"
|
|
||||||
sodipodi:docname="zabbix_app_logo.svg"
|
|
||||||
enable-background="new"><metadata
|
|
||||||
id="metadata13"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs11"><linearGradient
|
|
||||||
id="SVGID_1_"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="2.6005001"
|
|
||||||
y1="65.475197"
|
|
||||||
x2="94.377701"
|
|
||||||
y2="30.245199"><stop
|
|
||||||
id="stop34"
|
|
||||||
style="stop-color:#58595B"
|
|
||||||
offset="0.2583" /><stop
|
|
||||||
id="stop32"
|
|
||||||
style="stop-color:#646C70"
|
|
||||||
offset="0.2917" /><stop
|
|
||||||
id="stop30"
|
|
||||||
style="stop-color:#6C8087"
|
|
||||||
offset="0.3398" /><stop
|
|
||||||
id="stop28"
|
|
||||||
style="stop-color:#6D8F9B"
|
|
||||||
offset="0.3927" /><stop
|
|
||||||
id="stop26"
|
|
||||||
style="stop-color:#689BAA"
|
|
||||||
offset="0.4499" /><stop
|
|
||||||
id="stop24"
|
|
||||||
style="stop-color:#5FA3B5"
|
|
||||||
offset="0.5128" /><stop
|
|
||||||
id="stop22"
|
|
||||||
style="stop-color:#53A8BD"
|
|
||||||
offset="0.5837" /><stop
|
|
||||||
id="stop20"
|
|
||||||
style="stop-color:#47ABC2"
|
|
||||||
offset="0.6674" /><stop
|
|
||||||
id="stop18"
|
|
||||||
style="stop-color:#3FAEC5"
|
|
||||||
offset="0.7759" /><stop
|
|
||||||
id="stop16"
|
|
||||||
style="stop-color:#3CAFC7"
|
|
||||||
offset="1" /><stop
|
|
||||||
id="stop14"
|
|
||||||
style="stop-color:#3BB0C9"
|
|
||||||
offset="1" /></linearGradient></defs><sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1615"
|
|
||||||
inkscape:window-height="1026"
|
|
||||||
id="namedview9"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="4.285"
|
|
||||||
inkscape:cx="50.424685"
|
|
||||||
inkscape:cy="23.581186"
|
|
||||||
inkscape:window-x="65"
|
|
||||||
inkscape:window-y="24"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="g5194" /><style
|
|
||||||
type="text/css"
|
|
||||||
id="style3">
|
|
||||||
.st0{fill:#787878;}
|
|
||||||
</style><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="g5194"
|
|
||||||
inkscape:label="Zabbix BG Original"
|
|
||||||
style="display:inline"><rect
|
|
||||||
style="fill:#d40000;fill-opacity:1"
|
|
||||||
id="rect5196"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
x="692"
|
|
||||||
y="0" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6"
|
|
||||||
inkscape:label="Zabbix Original Z"
|
|
||||||
style="display:inline"><path
|
|
||||||
d="m 715.54426,16.689227 52.91147,0 0,6.87033 -42.58255,52.167008 43.62047,0 0,7.584207 -54.9873,0 0,-6.871516 42.58255,-52.166552 -41.54464,0 0,-7.583477 z"
|
|
||||||
style="display:inline;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
id="path4169-6"
|
|
||||||
inkscape:connector-curvature="0" /></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import { isNumeric } from './utils';
|
||||||
|
|
||||||
var index = [];
|
const index = [];
|
||||||
var categories = {
|
const categories = {
|
||||||
Transform: [],
|
Transform: [],
|
||||||
Aggregate: [],
|
Aggregate: [],
|
||||||
Filter: [],
|
Filter: [],
|
||||||
@@ -298,11 +298,15 @@ addFuncDef({
|
|||||||
defaultParams: ['avg'],
|
defaultParams: ['avg'],
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(categories, function(funcList, catName) {
|
_.each(categories, (funcList, catName) => {
|
||||||
categories[catName] = _.sortBy(funcList, 'name');
|
categories[catName] = _.sortBy(funcList, 'name');
|
||||||
});
|
});
|
||||||
|
|
||||||
class FuncInstance {
|
class FuncInstance {
|
||||||
|
def: any;
|
||||||
|
params: any;
|
||||||
|
text: string;
|
||||||
|
|
||||||
constructor(funcDef, params) {
|
constructor(funcDef, params) {
|
||||||
this.def = funcDef;
|
this.def = funcDef;
|
||||||
|
|
||||||
@@ -318,13 +322,13 @@ class FuncInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bindFunction(metricFunctions) {
|
bindFunction(metricFunctions) {
|
||||||
var func = metricFunctions[this.def.name];
|
const func = metricFunctions[this.def.name];
|
||||||
if (func) {
|
if (func) {
|
||||||
|
|
||||||
// Bind function arguments
|
// Bind function arguments
|
||||||
var bindedFunc = func;
|
let bindedFunc = func;
|
||||||
var param;
|
let param;
|
||||||
for (var i = 0; i < this.params.length; i++) {
|
for (let i = 0; i < this.params.length; i++) {
|
||||||
param = this.params[i];
|
param = this.params[i];
|
||||||
|
|
||||||
// Convert numeric params
|
// Convert numeric params
|
||||||
@@ -341,23 +345,21 @@ class FuncInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(metricExp) {
|
render(metricExp) {
|
||||||
var str = this.def.name + '(';
|
const str = this.def.name + '(';
|
||||||
var parameters = _.map(this.params, function(value, index) {
|
const parameters = _.map(this.params, (value, index) => {
|
||||||
|
const paramType = this.def.params[index].type;
|
||||||
var paramType = this.def.params[index].type;
|
|
||||||
if (paramType === 'int' ||
|
if (paramType === 'int' ||
|
||||||
paramType === 'float' ||
|
paramType === 'float' ||
|
||||||
paramType === 'value_or_series' ||
|
paramType === 'value_or_series' ||
|
||||||
paramType === 'boolean') {
|
paramType === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
}
|
} else if (paramType === 'int_or_interval' && isNumeric(value)) {
|
||||||
else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "'" + value + "'";
|
return "'" + value + "'";
|
||||||
|
|
||||||
}, this);
|
});
|
||||||
|
|
||||||
if (metricExp) {
|
if (metricExp) {
|
||||||
parameters.unshift(metricExp);
|
parameters.unshift(metricExp);
|
||||||
@@ -378,16 +380,15 @@ class FuncInstance {
|
|||||||
// handle optional parameters
|
// handle optional parameters
|
||||||
// if string contains ',' and next param is optional, split and update both
|
// if string contains ',' and next param is optional, split and update both
|
||||||
if (this._hasMultipleParamsInString(strValue, index)) {
|
if (this._hasMultipleParamsInString(strValue, index)) {
|
||||||
_.each(strValue.split(','), function(partVal, idx) {
|
_.each(strValue.split(','), (partVal, idx) => {
|
||||||
this.updateParam(partVal.trim(), idx);
|
this.updateParam(partVal.trim(), idx);
|
||||||
}, this);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strValue === '' && this.def.params[index].optional) {
|
if (strValue === '' && this.def.params[index].optional) {
|
||||||
this.params.splice(index, 1);
|
this.params.splice(index, 1);
|
||||||
}
|
}else {
|
||||||
else {
|
|
||||||
this.params[index] = strValue;
|
this.params[index] = strValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +401,7 @@ class FuncInstance {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = this.def.name + '(';
|
let text = this.def.name + '(';
|
||||||
text += this.params.join(', ');
|
text += this.params.join(', ');
|
||||||
text += ')';
|
text += ')';
|
||||||
this.text = text;
|
this.text = text;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ZabbixMetricsQuery } from './types';
|
import { ZabbixMetricsQuery } from './types';
|
||||||
|
import * as c from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query format migration.
|
* Query format migration.
|
||||||
@@ -28,6 +29,34 @@ export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migratePercentileAgg(target) {
|
||||||
|
if (target.functions) {
|
||||||
|
for (const f of target.functions) {
|
||||||
|
if (f.def && f.def.name === 'percentil') {
|
||||||
|
f.def.name = 'percentile';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateQueryType(target) {
|
||||||
|
if (target.queryType === undefined) {
|
||||||
|
if (target.mode === 'Metrics') {
|
||||||
|
// Explore mode
|
||||||
|
target.queryType = c.MODE_METRICS;
|
||||||
|
} else if (target.mode !== undefined) {
|
||||||
|
target.queryType = target.mode;
|
||||||
|
delete target.mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSLA(target) {
|
||||||
|
if (target.queryType === c.MODE_ITSERVICE && !target.slaInterval) {
|
||||||
|
target.slaInterval = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function migrate(target) {
|
export function migrate(target) {
|
||||||
target.resultFormat = target.resultFormat || 'time_series';
|
target.resultFormat = target.resultFormat || 'time_series';
|
||||||
target = fixTargetGroup(target);
|
target = fixTargetGroup(target);
|
||||||
@@ -35,6 +64,8 @@ export function migrate(target) {
|
|||||||
return migrateFrom2To3version(target);
|
return migrateFrom2To3version(target);
|
||||||
}
|
}
|
||||||
migratePercentileAgg(target);
|
migratePercentileAgg(target);
|
||||||
|
migrateQueryType(target);
|
||||||
|
migrateSLA(target);
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,16 +84,6 @@ function convertToRegex(str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migratePercentileAgg(target) {
|
|
||||||
if (target.functions) {
|
|
||||||
for (const f of target.functions) {
|
|
||||||
if (f.def && f.def.name === 'percentil') {
|
|
||||||
f.def.name = 'percentile';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DS_CONFIG_SCHEMA = 2;
|
export const DS_CONFIG_SCHEMA = 2;
|
||||||
export function migrateDSConfig(jsonData) {
|
export function migrateDSConfig(jsonData) {
|
||||||
if (!jsonData) {
|
if (!jsonData) {
|
||||||
|
|||||||
@@ -73,15 +73,6 @@
|
|||||||
placeholder="1h">
|
placeholder="1h">
|
||||||
</input>
|
</input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form max-width-20">
|
|
||||||
<span class="gf-form-label width-12">Zabbix version</span>
|
|
||||||
<div class="gf-form-select-wrapper max-width-7">
|
|
||||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.zabbixVersion"
|
|
||||||
ng-options="s.value as s.name for s in ctrl.zabbixVersions">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<label class="gf-form-label width-7">Query Mode</label>
|
<label class="gf-form-label width-7">Query Mode</label>
|
||||||
<div class="gf-form-select-wrapper max-width-20">
|
<div class="gf-form-select-wrapper max-width-20">
|
||||||
<select class="gf-form-input"
|
<select class="gf-form-input"
|
||||||
ng-change="ctrl.switchEditorMode(ctrl.target.mode)"
|
ng-change="ctrl.switchEditorMode(ctrl.target.queryType)"
|
||||||
ng-model="ctrl.target.mode"
|
ng-model="ctrl.target.queryType"
|
||||||
ng-options="m.mode as m.text for m in ctrl.editorModes">
|
ng-options="m.queryType as m.text for m in ctrl.editorModes">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form" ng-show="ctrl.target.mode == editorMode.TEXT">
|
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
|
||||||
<label class="gf-form-label query-keyword width-8">Format As</label>
|
<label class="gf-form-label query-keyword width-7">Format As</label>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
|
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- IT Service editor -->
|
<!-- IT Service editor -->
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.ITSERVICE">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.ITSERVICE">
|
||||||
<div class="gf-form max-width-20">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-7">IT Service</label>
|
<label class="gf-form-label query-keyword width-7">IT Service</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</input>
|
</input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label query-keyword">Property</label>
|
<label class="gf-form-label query-keyword width-7">Property</label>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select class="gf-form-input"
|
<select class="gf-form-input"
|
||||||
ng-change="ctrl.onTargetBlur()"
|
ng-change="ctrl.onTargetBlur()"
|
||||||
@@ -49,12 +49,22 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Interval</label>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-change="ctrl.onTargetBlur()"
|
||||||
|
ng-model="ctrl.target.slaInterval"
|
||||||
|
ng-options="i.value as i.text for i in ctrl.slaIntervals">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT || ctrl.target.mode == editorMode.TRIGGERS">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
<!-- Select Group -->
|
<!-- Select Group -->
|
||||||
<div class="gf-form max-width-20">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-7">Group</label>
|
<label class="gf-form-label query-keyword width-7">Group</label>
|
||||||
@@ -71,8 +81,8 @@
|
|||||||
}"></input>
|
}"></input>
|
||||||
</div>
|
</div>
|
||||||
<!-- Select Host -->
|
<!-- Select Host -->
|
||||||
<div class="gf-form">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-8">Host</label>
|
<label class="gf-form-label query-keyword width-7">Host</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
ng-model="ctrl.target.host.filter"
|
ng-model="ctrl.target.host.filter"
|
||||||
bs-typeahead="ctrl.getHostNames"
|
bs-typeahead="ctrl.getHostNames"
|
||||||
@@ -86,12 +96,27 @@
|
|||||||
}">
|
}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Proxy</label>
|
||||||
|
<input type="text"
|
||||||
|
ng-model="ctrl.target.proxy.filter"
|
||||||
|
bs-typeahead="ctrl.getProxyNames"
|
||||||
|
ng-blur="ctrl.onTargetBlur()"
|
||||||
|
data-min-length=0
|
||||||
|
data-items=100
|
||||||
|
class="gf-form-input width-14"
|
||||||
|
ng-class="{
|
||||||
|
'zbx-variable': ctrl.isVariable(ctrl.target.proxy.filter),
|
||||||
|
'zbx-regex': ctrl.isRegex(ctrl.target.proxy.filter)
|
||||||
|
}">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT || ctrl.target.mode == editorMode.TRIGGERS">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
<!-- Select Application -->
|
<!-- Select Application -->
|
||||||
<div class="gf-form max-width-20">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-7">Application</label>
|
<label class="gf-form-label query-keyword width-7">Application</label>
|
||||||
@@ -109,8 +134,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Select Item -->
|
<!-- Select Item -->
|
||||||
<div class="gf-form" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT">
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT">
|
||||||
<label class="gf-form-label query-keyword width-8">Item</label>
|
<label class="gf-form-label query-keyword width-7">Item</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
ng-model="ctrl.target.item.filter"
|
ng-model="ctrl.target.item.filter"
|
||||||
bs-typeahead="ctrl.getItemNames"
|
bs-typeahead="ctrl.getItemNames"
|
||||||
@@ -124,62 +149,93 @@
|
|||||||
}">
|
}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form max-width-23" ng-show="ctrl.target.mode == editorMode.TRIGGERS">
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
<label class="gf-form-label query-keyword width-8">Min Severity</label>
|
<label class="gf-form-label query-keyword width-7">Problem</label>
|
||||||
<div class="gf-form-select-wrapper width-16">
|
<input type="text"
|
||||||
|
ng-model="ctrl.target.trigger.filter"
|
||||||
|
ng-blur="ctrl.onTargetBlur()"
|
||||||
|
placeholder="Problem name"
|
||||||
|
class="gf-form-input"
|
||||||
|
ng-style="ctrl.target.trigger.style"
|
||||||
|
ng-class="{
|
||||||
|
'zbx-variable': ctrl.isVariable(ctrl.target.trigger.filter),
|
||||||
|
'zbx-regex': ctrl.isRegex(ctrl.target.trigger.filter)
|
||||||
|
}"
|
||||||
|
empty-to-null>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Tags</label>
|
||||||
|
<input type="text" class="gf-form-input width-14"
|
||||||
|
ng-model="ctrl.target.tags.filter"
|
||||||
|
ng-blur="ctrl.onTargetBlur()"
|
||||||
|
placeholder="tag1:value1, tag2:value2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Show</label>
|
||||||
|
<div class="gf-form-select-wrapper max-width-20">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.target.showProblems"
|
||||||
|
ng-options="v.value as v.text for v in ctrl.showProblemsOptions"
|
||||||
|
ng-change="ctrl.onTargetBlur()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Min severity</label>
|
||||||
|
<div class="gf-form-select-wrapper max-width-20">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.target.options.minSeverity"
|
||||||
|
ng-options="v.val as v.text for v in ctrl.severityOptions"
|
||||||
|
ng-change="ctrl.onTargetBlur()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
|
||||||
|
<label class="gf-form-label query-keyword width-7">Min Severity</label>
|
||||||
|
<div class="gf-form-select-wrapper width-14">
|
||||||
<select class="gf-form-input"
|
<select class="gf-form-input"
|
||||||
ng-change="ctrl.onTargetBlur()"
|
ng-change="ctrl.onTargetBlur()"
|
||||||
ng-model="ctrl.target.triggers.minSeverity"
|
ng-model="ctrl.target.triggers.minSeverity"
|
||||||
ng-options="s.val as s.text for s in ctrl.triggerSeverity">
|
ng-options="s.val as s.text for s in ctrl.severityOptions">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form max-width-20" ng-show="ctrl.target.mode == editorMode.TRIGGERS">
|
|
||||||
<label class="gf-form-label query-keyword width-8">Acknowledged</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-change="ctrl.onTargetBlur()"
|
|
||||||
ng-model="ctrl.target.triggers.acknowledged"
|
|
||||||
ng-options="a.value as a.text for a in ctrl.ackFilters">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<gf-form-switch class="gf-form" label="Count" ng-show="ctrl.target.mode == editorMode.TRIGGERS"
|
|
||||||
checked="ctrl.target.triggers.count" on-change="ctrl.onTargetBlur()">
|
|
||||||
</gf-form-switch>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<label class="gf-form-label gf-form-label--grow">
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
<a ng-click="ctrl.toggleQueryOptions()" ng-hide="ctrl.target.mode == editorMode.TRIGGERS">
|
|
||||||
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
|
|
||||||
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
|
|
||||||
{{ctrl.queryOptionsText}}
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Query options -->
|
<!-- Text mode options -->
|
||||||
<div class="gf-form-group" ng-if="ctrl.showQueryOptions">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
|
||||||
<div class="gf-form offset-width-7" ng-hide="ctrl.target.mode == editorMode.TRIGGERS">
|
<!-- Text metric regex -->
|
||||||
<gf-form-switch class="gf-form" label-class="width-10"
|
<div class="gf-form max-width-20">
|
||||||
label="Show disabled items"
|
<label class="gf-form-label query-keyword width-7">Text filter</label>
|
||||||
checked="ctrl.target.options.showDisabledItems"
|
<input type="text"
|
||||||
on-change="ctrl.onQueryOptionChange()">
|
class="gf-form-input"
|
||||||
</gf-form-switch>
|
ng-model="ctrl.target.textFilter"
|
||||||
|
spellcheck='false'
|
||||||
|
placeholder="Text filter (regex)"
|
||||||
|
ng-blur="ctrl.onTargetBlur()">
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form offset-width-7" ng-show="ctrl.target.mode === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
|
|
||||||
<gf-form-switch class="gf-form" label-class="width-10"
|
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
|
||||||
label="Skip empty values"
|
|
||||||
checked="ctrl.target.options.skipEmptyValues"
|
|
||||||
on-change="ctrl.onQueryOptionChange()">
|
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Item IDs editor mode -->
|
<!-- Item IDs editor mode -->
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.ITEMID">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.ITEMID">
|
||||||
<div class="gf-form max-width-20">
|
<div class="gf-form max-width-20">
|
||||||
<label class="gf-form-label query-keyword width-7">Item IDs</label>
|
<label class="gf-form-label query-keyword width-7">Item IDs</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -201,7 +257,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metric processing functions -->
|
<!-- Metric processing functions -->
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID || ctrl.target.mode == editorMode.ITSERVICE">
|
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.ITEMID || ctrl.target.queryType == editorMode.ITSERVICE">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label query-keyword width-7">Functions</label>
|
<label class="gf-form-label query-keyword width-7">Functions</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,23 +271,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text mode options -->
|
<div class="gf-form gf-form--grow">
|
||||||
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.TEXT">
|
<label class="gf-form-label gf-form-label--grow">
|
||||||
<!-- Text metric regex -->
|
<a ng-click="ctrl.toggleQueryOptions()">
|
||||||
<div class="gf-form max-width-20">
|
<i class="fa fa-caret-down" ng-show="ctrl.showQueryOptions"></i>
|
||||||
<label class="gf-form-label query-keyword width-7">Text filter</label>
|
<i class="fa fa-caret-right" ng-hide="ctrl.showQueryOptions"></i>
|
||||||
<input type="text"
|
{{ctrl.queryOptionsText}}
|
||||||
class="gf-form-input"
|
</a>
|
||||||
ng-model="ctrl.target.textFilter"
|
</label>
|
||||||
spellcheck='false'
|
|
||||||
placeholder="Text filter (regex)"
|
|
||||||
ng-blur="ctrl.onTargetBlur()">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
|
<!-- Query options -->
|
||||||
|
<div class="gf-form-group offset-width-7" ng-if="ctrl.showQueryOptions">
|
||||||
|
<div class="gf-form" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-10"
|
||||||
|
label="Show disabled items"
|
||||||
|
checked="ctrl.target.options.showDisabledItems"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
<div class="gf-form gf-form--grow">
|
</div>
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
<div class="gf-form" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
|
||||||
|
<gf-form-switch class="gf-form" label-class="width-10"
|
||||||
|
label="Skip empty values"
|
||||||
|
checked="ctrl.target.options.skipEmptyValues"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
|
||||||
|
<gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
|
||||||
|
label-class="width-9"
|
||||||
|
label="Count"
|
||||||
|
checked="ctrl.target.triggers.count"
|
||||||
|
on-change="ctrl.onTargetBlur()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
|
||||||
|
<label class="gf-form-label width-9">Acknowledged</label>
|
||||||
|
<div class="gf-form-select-wrapper width-12">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-change="ctrl.onQueryOptionChange()"
|
||||||
|
ng-model="ctrl.target.options.acknowledged"
|
||||||
|
ng-options="a.value as a.text for a in ctrl.ackFilters">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">Sort by</label>
|
||||||
|
<div class="gf-form-select-wrapper width-12">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.target.options.sortProblems"
|
||||||
|
ng-options="f.value as f.text for f in ctrl.sortByFields"
|
||||||
|
ng-change="ctrl.onQueryOptionChange()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<gf-form-switch class="gf-form"
|
||||||
|
label-class="width-9"
|
||||||
|
label="Hosts in maintenance"
|
||||||
|
checked="ctrl.target.options.hostsInMaintenance"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<gf-form-switch class="gf-form"
|
||||||
|
label-class="width-9"
|
||||||
|
label="Host proxy"
|
||||||
|
checked="ctrl.target.options.hostProxy"
|
||||||
|
on-change="ctrl.onQueryOptionChange()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">Limit triggers</label>
|
||||||
|
<input class="gf-form-input width-5"
|
||||||
|
type="number" placeholder="100"
|
||||||
|
ng-model="ctrl.target.options.limit"
|
||||||
|
ng-model-onblur ng-change="ctrl.onQueryOptionChange()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</query-editor-row>
|
</query-editor-row>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
|
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
|
||||||
},
|
},
|
||||||
"logos": {
|
"logos": {
|
||||||
"small": "img/zabbix_app_logo.svg",
|
"small": "img/icn-zabbix-datasource.svg",
|
||||||
"large": "img/zabbix_app_logo.svg"
|
"large": "img/icn-zabbix-datasource.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
201
src/datasource-zabbix/problemsHandler.ts
Normal file
201
src/datasource-zabbix/problemsHandler.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import * as utils from '../datasource-zabbix/utils';
|
||||||
|
import { DataFrame, Field, FieldType, ArrayVector } from '@grafana/data';
|
||||||
|
import { ZBXProblem, ZBXTrigger, ProblemDTO, ZBXEvent } from './types';
|
||||||
|
|
||||||
|
export function joinTriggersWithProblems(problems: ZBXProblem[], triggers: ZBXTrigger[]): ProblemDTO[] {
|
||||||
|
const problemDTOList: ProblemDTO[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < problems.length; i++) {
|
||||||
|
const p = problems[i];
|
||||||
|
const triggerId = Number(p.objectid);
|
||||||
|
const t = triggers[triggerId];
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
const problemDTO: ProblemDTO = {
|
||||||
|
timestamp: Number(p.clock),
|
||||||
|
triggerid: p.objectid,
|
||||||
|
eventid: p.eventid,
|
||||||
|
name: p.name,
|
||||||
|
severity: p.severity,
|
||||||
|
acknowledged: p.acknowledged,
|
||||||
|
acknowledges: p.acknowledges,
|
||||||
|
tags: p.tags,
|
||||||
|
suppressed: p.suppressed,
|
||||||
|
suppression_data: p.suppression_data,
|
||||||
|
description: t.description,
|
||||||
|
comments: t.comments,
|
||||||
|
value: t.value,
|
||||||
|
groups: t.groups,
|
||||||
|
hosts: t.hosts,
|
||||||
|
items: t.items,
|
||||||
|
alerts: t.alerts,
|
||||||
|
url: t.url,
|
||||||
|
expression: t.expression,
|
||||||
|
correlation_mode: t.correlation_mode,
|
||||||
|
correlation_tag: t.correlation_tag,
|
||||||
|
manual_close: t.manual_close,
|
||||||
|
state: t.state,
|
||||||
|
error: t.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
problemDTOList.push(problemDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return problemDTOList;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JoinOptions {
|
||||||
|
valueFromEvent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinTriggersWithEvents(events: ZBXEvent[], triggers: ZBXTrigger[], options?: JoinOptions): ProblemDTO[] {
|
||||||
|
const { valueFromEvent } = options;
|
||||||
|
const problemDTOList: ProblemDTO[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
const e = events[i];
|
||||||
|
const triggerId = Number(e.objectid);
|
||||||
|
const t = triggers[triggerId];
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
const problemDTO: ProblemDTO = {
|
||||||
|
value: valueFromEvent ? e.value : t.value,
|
||||||
|
timestamp: Number(e.clock),
|
||||||
|
triggerid: e.objectid,
|
||||||
|
eventid: e.eventid,
|
||||||
|
name: e.name,
|
||||||
|
severity: e.severity,
|
||||||
|
acknowledged: e.acknowledged,
|
||||||
|
acknowledges: e.acknowledges,
|
||||||
|
tags: e.tags,
|
||||||
|
suppressed: e.suppressed,
|
||||||
|
description: t.description,
|
||||||
|
comments: t.comments,
|
||||||
|
groups: t.groups,
|
||||||
|
hosts: t.hosts,
|
||||||
|
items: t.items,
|
||||||
|
alerts: t.alerts,
|
||||||
|
url: t.url,
|
||||||
|
expression: t.expression,
|
||||||
|
correlation_mode: t.correlation_mode,
|
||||||
|
correlation_tag: t.correlation_tag,
|
||||||
|
manual_close: t.manual_close,
|
||||||
|
state: t.state,
|
||||||
|
error: t.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
problemDTOList.push(problemDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return problemDTOList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMaintenanceStatus(triggers) {
|
||||||
|
_.each(triggers, (trigger) => {
|
||||||
|
const maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1');
|
||||||
|
trigger.maintenance = maintenance_status;
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAckButtonStatus(triggers, showAckButton) {
|
||||||
|
_.each(triggers, (trigger) => {
|
||||||
|
trigger.showAckButton = showAckButton;
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTriggerDataSource(triggers, target) {
|
||||||
|
_.each(triggers, (trigger) => {
|
||||||
|
trigger.datasource = target.datasource;
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTriggerHostProxy(triggers, proxies) {
|
||||||
|
triggers.forEach(trigger => {
|
||||||
|
if (trigger.hosts && trigger.hosts.length) {
|
||||||
|
const host = trigger.hosts[0];
|
||||||
|
if (host.proxy_hostid !== '0') {
|
||||||
|
const hostProxy = proxies[host.proxy_hostid];
|
||||||
|
host.proxy = hostProxy ? hostProxy.host : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterTriggersPre(triggerList, replacedTarget) {
|
||||||
|
// Filter triggers by description
|
||||||
|
const triggerFilter = replacedTarget.trigger.filter;
|
||||||
|
if (triggerFilter) {
|
||||||
|
triggerList = filterTriggers(triggerList, triggerFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by tags
|
||||||
|
if (replacedTarget.tags.filter) {
|
||||||
|
let tagsFilter = replacedTarget.tags.filter;
|
||||||
|
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
||||||
|
tagsFilter = tagsFilter.replace('/^', '').replace('$/', '');
|
||||||
|
const tags = utils.parseTags(tagsFilter);
|
||||||
|
triggerList = _.filter(triggerList, trigger => {
|
||||||
|
return _.every(tags, tag => {
|
||||||
|
return _.find(trigger.tags, t => t.tag === tag.tag && (!tag.value || t.value === tag.value));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by maintenance status
|
||||||
|
if (!replacedTarget.options.hostsInMaintenance) {
|
||||||
|
triggerList = _.filter(triggerList, (trigger) => !trigger.maintenance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTriggers(triggers, triggerFilter) {
|
||||||
|
if (utils.isRegex(triggerFilter)) {
|
||||||
|
return _.filter(triggers, trigger => {
|
||||||
|
return utils.buildRegex(triggerFilter).test(trigger.description);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _.filter(triggers, trigger => {
|
||||||
|
return trigger.description === triggerFilter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDataFrame(problems: any[]): DataFrame {
|
||||||
|
const problemsField: Field<any> = {
|
||||||
|
name: 'Problems',
|
||||||
|
type: FieldType.other,
|
||||||
|
values: new ArrayVector(problems),
|
||||||
|
config: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: DataFrame = {
|
||||||
|
name: 'problems',
|
||||||
|
fields: [problemsField],
|
||||||
|
length: problems.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const problemsHandler = {
|
||||||
|
addTriggerDataSource,
|
||||||
|
addTriggerHostProxy,
|
||||||
|
setMaintenanceStatus,
|
||||||
|
setAckButtonStatus,
|
||||||
|
filterTriggersPre,
|
||||||
|
toDataFrame,
|
||||||
|
joinTriggersWithProblems,
|
||||||
|
joinTriggersWithEvents,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default problemsHandler;
|
||||||
@@ -4,6 +4,58 @@ import * as c from './constants';
|
|||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import * as metricFunctions from './metricFunctions';
|
import * as metricFunctions from './metricFunctions';
|
||||||
import * as migrations from './migrations';
|
import * as migrations from './migrations';
|
||||||
|
import { ShowProblemTypes } from './types';
|
||||||
|
|
||||||
|
function getTargetDefaults() {
|
||||||
|
return {
|
||||||
|
queryType: c.MODE_METRICS,
|
||||||
|
group: { 'filter': "" },
|
||||||
|
host: { 'filter': "" },
|
||||||
|
application: { 'filter': "" },
|
||||||
|
item: { 'filter': "" },
|
||||||
|
functions: [],
|
||||||
|
triggers: {
|
||||||
|
'count': true,
|
||||||
|
'minSeverity': 3,
|
||||||
|
'acknowledged': 2
|
||||||
|
},
|
||||||
|
trigger: {filter: ""},
|
||||||
|
tags: {filter: ""},
|
||||||
|
proxy: {filter: ""},
|
||||||
|
options: {
|
||||||
|
showDisabledItems: false,
|
||||||
|
skipEmptyValues: false,
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
'skipEmptyValues': false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSLATargetDefaults() {
|
||||||
|
return {
|
||||||
|
slaProperty: { name: "SLA", property: "sla" },
|
||||||
|
slaInterval: 'none',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProblemsTargetDefaults() {
|
||||||
|
return {
|
||||||
|
showProblems: ShowProblemTypes.Problems,
|
||||||
|
options: {
|
||||||
|
minSeverity: 0,
|
||||||
|
sortProblems: 'default',
|
||||||
|
acknowledged: 2,
|
||||||
|
hostsInMaintenance: false,
|
||||||
|
hostProxy: false,
|
||||||
|
limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeverityOptions() {
|
||||||
|
return c.TRIGGER_SEVERITY;
|
||||||
|
}
|
||||||
|
|
||||||
export class ZabbixQueryController extends QueryCtrl {
|
export class ZabbixQueryController extends QueryCtrl {
|
||||||
|
|
||||||
@@ -17,11 +69,12 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
this.templateSrv = templateSrv;
|
this.templateSrv = templateSrv;
|
||||||
|
|
||||||
this.editorModes = [
|
this.editorModes = [
|
||||||
{value: 'num', text: 'Metrics', mode: c.MODE_METRICS},
|
{value: 'num', text: 'Metrics', queryType: c.MODE_METRICS},
|
||||||
{value: 'text', text: 'Text', mode: c.MODE_TEXT},
|
{value: 'text', text: 'Text', queryType: c.MODE_TEXT},
|
||||||
{value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE},
|
{value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE},
|
||||||
{value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID},
|
{value: 'itemid', text: 'Item ID', queryType: c.MODE_ITEMID},
|
||||||
{value: 'triggers', text: 'Triggers', mode: c.MODE_TRIGGERS}
|
{value: 'triggers', text: 'Triggers', queryType: c.MODE_TRIGGERS},
|
||||||
|
{value: 'problems', text: 'Problems', queryType: c.MODE_PROBLEMS},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.$scope.editorMode = {
|
this.$scope.editorMode = {
|
||||||
@@ -29,7 +82,8 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
TEXT: c.MODE_TEXT,
|
TEXT: c.MODE_TEXT,
|
||||||
ITSERVICE: c.MODE_ITSERVICE,
|
ITSERVICE: c.MODE_ITSERVICE,
|
||||||
ITEMID: c.MODE_ITEMID,
|
ITEMID: c.MODE_ITEMID,
|
||||||
TRIGGERS: c.MODE_TRIGGERS
|
TRIGGERS: c.MODE_TRIGGERS,
|
||||||
|
PROBLEMS: c.MODE_PROBLEMS,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.slaPropertyList = [
|
this.slaPropertyList = [
|
||||||
@@ -40,15 +94,49 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
{name: "Down time", property: "downtimeTime"}
|
{name: "Down time", property: "downtimeTime"}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.slaIntervals = [
|
||||||
|
{ text: 'No interval', value: 'none' },
|
||||||
|
{ text: 'Auto', value: 'auto' },
|
||||||
|
{ text: '1 hour', value: '1h' },
|
||||||
|
{ text: '12 hours', value: '12h' },
|
||||||
|
{ text: '24 hours', value: '1d' },
|
||||||
|
{ text: '1 week', value: '1w' },
|
||||||
|
{ text: '1 month', value: '1M' },
|
||||||
|
];
|
||||||
|
|
||||||
this.ackFilters = [
|
this.ackFilters = [
|
||||||
{text: 'all triggers', value: 2},
|
{text: 'all triggers', value: 2},
|
||||||
{text: 'unacknowledged', value: 0},
|
{text: 'unacknowledged', value: 0},
|
||||||
{text: 'acknowledged', value: 1},
|
{text: 'acknowledged', value: 1},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.problemAckFilters = [
|
||||||
|
'all triggers',
|
||||||
|
'unacknowledged',
|
||||||
|
'acknowledged'
|
||||||
|
];
|
||||||
|
|
||||||
|
this.sortByFields = [
|
||||||
|
{ text: 'Default', value: 'default' },
|
||||||
|
{ text: 'Last change', value: 'lastchange' },
|
||||||
|
{ text: 'Severity', value: 'priority' },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.showEventsFields = [
|
||||||
|
{ text: 'All', value: [0,1] },
|
||||||
|
{ text: 'OK', value: [0] },
|
||||||
|
{ text: 'Problems', value: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
this.showProblemsOptions = [
|
||||||
|
{ text: 'Problems', value: 'problems' },
|
||||||
|
{ text: 'Recent problems', value: 'recent' },
|
||||||
|
{ text: 'History', value: 'history' },
|
||||||
|
];
|
||||||
|
|
||||||
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
|
this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
|
||||||
|
|
||||||
this.triggerSeverity = c.TRIGGER_SEVERITY;
|
this.severityOptions = getSeverityOptions();
|
||||||
|
|
||||||
// Map functions for bs-typeahead
|
// Map functions for bs-typeahead
|
||||||
this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList');
|
this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList');
|
||||||
@@ -56,6 +144,7 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
|
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
|
||||||
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
|
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
|
||||||
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
|
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
|
||||||
|
this.getProxyNames = _.bind(this.getMetricNames, this, 'proxyList');
|
||||||
this.getVariables = _.bind(this.getTemplateVariables, this);
|
this.getVariables = _.bind(this.getTemplateVariables, this);
|
||||||
|
|
||||||
// Update metric suggestion when template variable was changed
|
// Update metric suggestion when template variable was changed
|
||||||
@@ -80,40 +169,32 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
_.defaults(this, scopeDefaults);
|
_.defaults(this, scopeDefaults);
|
||||||
|
|
||||||
// Load default values
|
// Load default values
|
||||||
var targetDefaults = {
|
const targetDefaults = getTargetDefaults();
|
||||||
'mode': c.MODE_METRICS,
|
_.defaultsDeep(target, targetDefaults);
|
||||||
'group': { 'filter': "" },
|
|
||||||
'host': { 'filter': "" },
|
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
|
||||||
'application': { 'filter': "" },
|
target.queryType = c.MODE_PROBLEMS;
|
||||||
'item': { 'filter': "" },
|
|
||||||
'functions': [],
|
|
||||||
'triggers': {
|
|
||||||
'count': true,
|
|
||||||
'minSeverity': 3,
|
|
||||||
'acknowledged': 2
|
|
||||||
},
|
|
||||||
'options': {
|
|
||||||
'showDisabledItems': false,
|
|
||||||
'skipEmptyValues': false
|
|
||||||
},
|
|
||||||
'table': {
|
|
||||||
'skipEmptyValues': false
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
_.defaults(target, targetDefaults);
|
|
||||||
|
|
||||||
// Create function instances from saved JSON
|
// Create function instances from saved JSON
|
||||||
target.functions = _.map(target.functions, function(func) {
|
target.functions = _.map(target.functions, function(func) {
|
||||||
return metricFunctions.createFuncInstance(func.def, func.params);
|
return metricFunctions.createFuncInstance(func.def, func.params);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (target.mode === c.MODE_METRICS ||
|
if (target.queryType === c.MODE_ITSERVICE) {
|
||||||
target.mode === c.MODE_TEXT ||
|
_.defaultsDeep(target, getSLATargetDefaults());
|
||||||
target.mode === c.MODE_TRIGGERS) {
|
|
||||||
this.initFilters();
|
|
||||||
}
|
}
|
||||||
else if (target.mode === c.MODE_ITSERVICE) {
|
|
||||||
_.defaults(target, {slaProperty: {name: "SLA", property: "sla"}});
|
if (target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
_.defaultsDeep(target, getProblemsTargetDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.queryType === c.MODE_METRICS ||
|
||||||
|
target.queryType === c.MODE_TEXT ||
|
||||||
|
target.queryType === c.MODE_TRIGGERS ||
|
||||||
|
target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
this.initFilters();
|
||||||
|
} else if (target.queryType === c.MODE_ITSERVICE) {
|
||||||
this.suggestITServices();
|
this.suggestITServices();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -123,14 +204,20 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initFilters() {
|
initFilters() {
|
||||||
let itemtype = _.find(this.editorModes, {'mode': this.target.mode});
|
let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType});
|
||||||
itemtype = itemtype ? itemtype.value : null;
|
itemtype = itemtype ? itemtype.value : null;
|
||||||
return Promise.all([
|
const promises = [
|
||||||
this.suggestGroups(),
|
this.suggestGroups(),
|
||||||
this.suggestHosts(),
|
this.suggestHosts(),
|
||||||
this.suggestApps(),
|
this.suggestApps(),
|
||||||
this.suggestItems(itemtype)
|
this.suggestItems(itemtype),
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if (this.target.queryType === c.MODE_PROBLEMS) {
|
||||||
|
promises.push(this.suggestProxies());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get list of metric names for bs-typeahead directive
|
// Get list of metric names for bs-typeahead directive
|
||||||
@@ -207,6 +294,15 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suggestProxies() {
|
||||||
|
return this.zabbix.getProxies()
|
||||||
|
.then(response => {
|
||||||
|
const proxies = _.map(response, 'host');
|
||||||
|
this.metric.proxyList = proxies;
|
||||||
|
return proxies;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isRegex(str) {
|
isRegex(str) {
|
||||||
return utils.isRegex(str);
|
return utils.isRegex(str);
|
||||||
}
|
}
|
||||||
@@ -302,19 +398,42 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderQueryOptionsText() {
|
renderQueryOptionsText() {
|
||||||
var optionsMap = {
|
const metricOptionsMap = {
|
||||||
showDisabledItems: "Show disabled items",
|
showDisabledItems: "Show disabled items",
|
||||||
skipEmptyValues: "Skip empty values"
|
|
||||||
};
|
};
|
||||||
var options = [];
|
|
||||||
|
const problemsOptionsMap = {
|
||||||
|
sortProblems: "Sort problems",
|
||||||
|
acknowledged: "Acknowledged",
|
||||||
|
skipEmptyValues: "Skip empty values",
|
||||||
|
hostsInMaintenance: "Show hosts in maintenance",
|
||||||
|
limit: "Limit problems",
|
||||||
|
hostProxy: "Show proxy",
|
||||||
|
};
|
||||||
|
|
||||||
|
let optionsMap = {};
|
||||||
|
|
||||||
|
if (this.target.queryType === c.MODE_METRICS) {
|
||||||
|
optionsMap = metricOptionsMap;
|
||||||
|
} else if (this.target.queryType === c.MODE_PROBLEMS || this.target.queryType === c.MODE_TRIGGERS) {
|
||||||
|
optionsMap = problemsOptionsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [];
|
||||||
_.forOwn(this.target.options, (value, key) => {
|
_.forOwn(this.target.options, (value, key) => {
|
||||||
if (value) {
|
if (value && optionsMap[key]) {
|
||||||
if (value === true) {
|
if (value === true) {
|
||||||
// Show only option name (if enabled) for boolean options
|
// Show only option name (if enabled) for boolean options
|
||||||
options.push(optionsMap[key]);
|
options.push(optionsMap[key]);
|
||||||
} else {
|
} else {
|
||||||
// Show "option = value" for another options
|
// Show "option = value" for another options
|
||||||
options.push(optionsMap[key] + " = " + value);
|
let optionValue = value;
|
||||||
|
if (value && value.text) {
|
||||||
|
optionValue = value.text;
|
||||||
|
} else if (value && value.value) {
|
||||||
|
optionValue = value.value;
|
||||||
|
}
|
||||||
|
options.push(optionsMap[key] + " = " + optionValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -329,7 +448,8 @@ export class ZabbixQueryController extends QueryCtrl {
|
|||||||
* 2 - Text metrics
|
* 2 - Text metrics
|
||||||
*/
|
*/
|
||||||
switchEditorMode(mode) {
|
switchEditorMode(mode) {
|
||||||
this.target.mode = mode;
|
this.target.queryType = mode;
|
||||||
|
this.queryOptionsText = this.renderQueryOptionsText();
|
||||||
this.init();
|
this.init();
|
||||||
this.targetChanged();
|
this.targetChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,3 +29,7 @@ This mode is suitable for rendering charts in grafana by passing itemids as url
|
|||||||
|
|
||||||
##### Triggers
|
##### Triggers
|
||||||
Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard.
|
Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard.
|
||||||
|
|
||||||
|
#### Documentation links:
|
||||||
|
|
||||||
|
[Grafana-Zabbix Documentation](https://alexanderzobnin.github.io/grafana-zabbix)
|
||||||
|
|||||||
@@ -23,19 +23,35 @@ function convertHistory(history, items, addHostName, convertPointCallback) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Group history by itemid
|
// Group history by itemid
|
||||||
var grouped_history = _.groupBy(history, 'itemid');
|
const grouped_history = _.groupBy(history, 'itemid');
|
||||||
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
|
const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
|
||||||
|
|
||||||
return _.map(grouped_history, function(historyPoint, itemid) {
|
return _.map(grouped_history, (hist, itemid) => {
|
||||||
var item = _.find(items, {'itemid': itemid});
|
const item = _.find(items, {'itemid': itemid}) as any;
|
||||||
var alias = item.name;
|
let alias = item.name;
|
||||||
if (_.keys(hosts).length > 1 && addHostName) { //only when actual multi hosts selected
|
|
||||||
var host = _.find(hosts, {'hostid': item.hostid});
|
// Add scopedVars for using in alias functions
|
||||||
|
const scopedVars: any = {
|
||||||
|
'__zbx_item': { value: item.name },
|
||||||
|
'__zbx_item_name': { value: item.name },
|
||||||
|
'__zbx_item_key': { value: item.key_ },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_.keys(hosts).length > 0) {
|
||||||
|
const host = _.find(hosts, {'hostid': item.hostid});
|
||||||
|
scopedVars['__zbx_host'] = { value: host.host };
|
||||||
|
scopedVars['__zbx_host_name'] = { value: host.name };
|
||||||
|
|
||||||
|
// Only add host when multiple hosts selected
|
||||||
|
if (_.keys(hosts).length > 1 && addHostName) {
|
||||||
alias = host.name + ": " + alias;
|
alias = host.name + ": " + alias;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
target: alias,
|
target: alias,
|
||||||
datapoints: _.map(historyPoint, convertPointCallback)
|
datapoints: _.map(hist, convertPointCallback),
|
||||||
|
scopedVars,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -53,29 +69,29 @@ function handleHistory(history, items, addHostName = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTrends(history, items, valueType, addHostName = true) {
|
function handleTrends(history, items, valueType, addHostName = true) {
|
||||||
var convertPointCallback = _.partial(convertTrendPoint, valueType);
|
const convertPointCallback = _.partial(convertTrendPoint, valueType);
|
||||||
return convertHistory(history, items, addHostName, convertPointCallback);
|
return convertHistory(history, items, addHostName, convertPointCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleText(history, items, target, addHostName = true) {
|
function handleText(history, items, target, addHostName = true) {
|
||||||
let convertTextCallback = _.partial(convertText, target);
|
const convertTextCallback = _.partial(convertText, target);
|
||||||
return convertHistory(history, items, addHostName, convertTextCallback);
|
return convertHistory(history, items, addHostName, convertTextCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleHistoryAsTable(history, items, target) {
|
function handleHistoryAsTable(history, items, target) {
|
||||||
let table = new TableModel();
|
const table: any = new TableModel();
|
||||||
table.addColumn({text: 'Host'});
|
table.addColumn({text: 'Host'});
|
||||||
table.addColumn({text: 'Item'});
|
table.addColumn({text: 'Item'});
|
||||||
table.addColumn({text: 'Key'});
|
table.addColumn({text: 'Key'});
|
||||||
table.addColumn({text: 'Last value'});
|
table.addColumn({text: 'Last value'});
|
||||||
|
|
||||||
let grouped_history = _.groupBy(history, 'itemid');
|
const grouped_history = _.groupBy(history, 'itemid');
|
||||||
_.each(items, (item) => {
|
_.each(items, (item) => {
|
||||||
let itemHistory = grouped_history[item.itemid] || [];
|
const itemHistory = grouped_history[item.itemid] || [];
|
||||||
let lastPoint = _.last(itemHistory);
|
const lastPoint = _.last(itemHistory);
|
||||||
let lastValue = lastPoint ? lastPoint.value : null;
|
let lastValue = lastPoint ? lastPoint.value : null;
|
||||||
|
|
||||||
if(target.options.skipEmptyValues && (!lastValue || lastValue === '')) {
|
if (target.options.skipEmptyValues && (!lastValue || lastValue === '')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +100,7 @@ function handleHistoryAsTable(history, items, target) {
|
|||||||
lastValue = extractText(lastValue, target.textFilter, target.useCaptureGroups);
|
lastValue = extractText(lastValue, target.textFilter, target.useCaptureGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
let host = _.first(item.hosts);
|
let host: any = _.first(item.hosts);
|
||||||
host = host ? host.name : "";
|
host = host ? host.name : "";
|
||||||
|
|
||||||
table.rows.push([
|
table.rows.push([
|
||||||
@@ -110,22 +126,22 @@ function convertText(target, point) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractText(str, pattern, useCaptureGroups) {
|
function extractText(str, pattern, useCaptureGroups) {
|
||||||
let extractPattern = new RegExp(pattern);
|
const extractPattern = new RegExp(pattern);
|
||||||
let extractedValue = extractPattern.exec(str);
|
const extractedValue = extractPattern.exec(str);
|
||||||
if (extractedValue) {
|
if (extractedValue) {
|
||||||
if (useCaptureGroups) {
|
if (useCaptureGroups) {
|
||||||
extractedValue = extractedValue[1];
|
return extractedValue[1];
|
||||||
} else {
|
} else {
|
||||||
extractedValue = extractedValue[0];
|
return extractedValue[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return extractedValue;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSLAResponse(itservice, slaProperty, slaObject) {
|
function handleSLAResponse(itservice, slaProperty, slaObject) {
|
||||||
var targetSLA = slaObject[itservice.serviceid].sla;
|
const targetSLA = slaObject[itservice.serviceid].sla;
|
||||||
if (slaProperty.property === 'status') {
|
if (slaProperty.property === 'status') {
|
||||||
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
|
const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10);
|
||||||
return {
|
return {
|
||||||
target: itservice.name + ' ' + slaProperty.name,
|
target: itservice.name + ' ' + slaProperty.name,
|
||||||
datapoints: [
|
datapoints: [
|
||||||
@@ -134,7 +150,7 @@ function handleSLAResponse(itservice, slaProperty, slaObject) {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let i;
|
let i;
|
||||||
let slaArr = [];
|
const slaArr = [];
|
||||||
for (i = 0; i < targetSLA.length; i++) {
|
for (i = 0; i < targetSLA.length; i++) {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]);
|
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]);
|
||||||
@@ -165,7 +181,7 @@ function handleTriggersResponse(triggers, groups, timeRange) {
|
|||||||
} else {
|
} else {
|
||||||
const stats = getTriggerStats(triggers);
|
const stats = getTriggerStats(triggers);
|
||||||
const groupNames = _.map(groups, 'name');
|
const groupNames = _.map(groups, 'name');
|
||||||
let table = new TableModel();
|
const table: any = new TableModel();
|
||||||
table.addColumn({text: 'Host group'});
|
table.addColumn({text: 'Host group'});
|
||||||
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
|
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
|
||||||
table.addColumn({text: severity.text});
|
table.addColumn({text: severity.text});
|
||||||
@@ -182,11 +198,11 @@ function handleTriggersResponse(triggers, groups, timeRange) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTriggerStats(triggers) {
|
function getTriggerStats(triggers) {
|
||||||
let groups = _.uniq(_.flattenDeep(_.map(triggers, (trigger) => _.map(trigger.groups, 'name'))));
|
const groups = _.uniq(_.flattenDeep(_.map(triggers, (trigger) => _.map(trigger.groups, 'name'))));
|
||||||
// let severity = _.map(c.TRIGGER_SEVERITY, 'text');
|
// let severity = _.map(c.TRIGGER_SEVERITY, 'text');
|
||||||
let stats = {};
|
const stats = {};
|
||||||
_.each(groups, (group) => {
|
_.each(groups, (group) => {
|
||||||
stats[group] = {0:0, 1:0, 2:0, 3:0, 4:0, 5:0}; // severity:count
|
stats[group] = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0}; // severity:count
|
||||||
});
|
});
|
||||||
_.each(triggers, (trigger) => {
|
_.each(triggers, (trigger) => {
|
||||||
_.each(trigger.groups, (group) => {
|
_.each(trigger.groups, (group) => {
|
||||||
@@ -205,7 +221,7 @@ function convertHistoryPoint(point) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function convertTrendPoint(valueType, point) {
|
function convertTrendPoint(valueType, point) {
|
||||||
var value;
|
let value;
|
||||||
switch (valueType) {
|
switch (valueType) {
|
||||||
case "min":
|
case "min":
|
||||||
value = point.value_min;
|
value = point.value_min;
|
||||||
@@ -242,6 +258,3 @@ export default {
|
|||||||
handleTriggersResponse,
|
handleTriggersResponse,
|
||||||
sortTimeseries
|
sortTimeseries
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fix for backward compatibility with lodash 2.4
|
|
||||||
if (!_.uniqBy) {_.uniqBy = _.uniq;}
|
|
||||||
@@ -4,6 +4,12 @@ import { Datasource } from "../module";
|
|||||||
import { zabbixTemplateFormat } from "../datasource";
|
import { zabbixTemplateFormat } from "../datasource";
|
||||||
import { dateMath } from '@grafana/data';
|
import { dateMath } from '@grafana/data';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
getBackendSrv: () => ({
|
||||||
|
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
|
||||||
|
}),
|
||||||
|
}), {virtual: true});
|
||||||
|
|
||||||
describe('ZabbixDatasource', () => {
|
describe('ZabbixDatasource', () => {
|
||||||
let ctx = {};
|
let ctx = {};
|
||||||
|
|
||||||
@@ -21,11 +27,11 @@ describe('ZabbixDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ctx.templateSrv = mocks.templateSrvMock;
|
ctx.templateSrv = mocks.templateSrvMock;
|
||||||
ctx.backendSrv = mocks.backendSrvMock;
|
// ctx.backendSrv = mocks.backendSrvMock;
|
||||||
ctx.datasourceSrv = mocks.datasourceSrvMock;
|
ctx.datasourceSrv = mocks.datasourceSrvMock;
|
||||||
ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock;
|
ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock;
|
||||||
|
|
||||||
ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.backendSrv, ctx.datasourceSrv, ctx.zabbixAlertingSrv);
|
ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.zabbixAlertingSrv);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When querying data', () => {
|
describe('When querying data', () => {
|
||||||
@@ -119,7 +125,7 @@ describe('ZabbixDatasource', () => {
|
|||||||
item: {filter: "System information"},
|
item: {filter: "System information"},
|
||||||
textFilter: "",
|
textFilter: "",
|
||||||
useCaptureGroups: true,
|
useCaptureGroups: true,
|
||||||
mode: 2,
|
queryType: 2,
|
||||||
resultFormat: "table",
|
resultFormat: "table",
|
||||||
options: {
|
options: {
|
||||||
skipEmptyValues: false
|
skipEmptyValues: false
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import mocks from '../../test-setup/mocks';
|
|
||||||
import { DBConnector } from '../zabbix/connectors/dbConnector';
|
import { DBConnector } from '../zabbix/connectors/dbConnector';
|
||||||
|
|
||||||
|
const loadDatasourceMock = jest.fn().mockResolvedValue({ id: 42, name: 'foo', meta: {} });
|
||||||
|
const getAllMock = jest.fn().mockReturnValue([{ id: 42, name: 'foo', meta: {} }]);
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
getDataSourceSrv: () => ({
|
||||||
|
loadDatasource: loadDatasourceMock,
|
||||||
|
getAll: getAllMock
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('DBConnector', () => {
|
describe('DBConnector', () => {
|
||||||
let ctx = {};
|
const ctx: any = {};
|
||||||
const datasourceSrv = mocks.datasourceSrvMock;
|
|
||||||
datasourceSrv.loadDatasource.mockResolvedValue({ id: 42, name: 'foo', meta: {} });
|
|
||||||
datasourceSrv.getAll.mockReturnValue([{ id: 42, name: 'foo' }]);
|
|
||||||
|
|
||||||
describe('When init DB connector', () => {
|
describe('When init DB connector', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -13,34 +19,34 @@ describe('DBConnector', () => {
|
|||||||
datasourceId: 42,
|
datasourceId: 42,
|
||||||
datasourceName: undefined
|
datasourceName: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadDatasourceMock.mockClear();
|
||||||
|
getAllMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should try to load datasource by name first', () => {
|
it('should try to load datasource by name first', () => {
|
||||||
ctx.options = {
|
const dbConnector = new DBConnector({ datasourceName: 'bar' });
|
||||||
datasourceName: 'bar'
|
|
||||||
};
|
|
||||||
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
|
|
||||||
dbConnector.loadDBDataSource();
|
dbConnector.loadDBDataSource();
|
||||||
expect(datasourceSrv.getAll).not.toHaveBeenCalled();
|
expect(getAllMock).not.toHaveBeenCalled();
|
||||||
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('bar');
|
expect(loadDatasourceMock).toHaveBeenCalledWith('bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load datasource by id if name not present', () => {
|
it('should load datasource by id if name not present', () => {
|
||||||
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
|
const dbConnector = new DBConnector({ datasourceId: 42 });
|
||||||
dbConnector.loadDBDataSource();
|
dbConnector.loadDBDataSource();
|
||||||
expect(datasourceSrv.getAll).toHaveBeenCalled();
|
expect(getAllMock).toHaveBeenCalled();
|
||||||
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('foo');
|
expect(loadDatasourceMock).toHaveBeenCalledWith('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if no name and id specified', () => {
|
it('should throw error if no name and id specified', () => {
|
||||||
ctx.options = {};
|
ctx.options = {};
|
||||||
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
|
const dbConnector = new DBConnector(ctx.options);
|
||||||
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified');
|
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if datasource with given id is not found', () => {
|
it('should throw error if datasource with given id is not found', () => {
|
||||||
ctx.options.datasourceId = 45;
|
ctx.options.datasourceId = 45;
|
||||||
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
|
const dbConnector = new DBConnector(ctx.options);
|
||||||
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found');
|
return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector';
|
import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector';
|
||||||
import { compactQuery } from '../utils';
|
import { compactQuery } from '../utils';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
getDataSourceSrv: jest.fn(() => ({
|
||||||
|
loadDatasource: jest.fn().mockResolvedValue(
|
||||||
|
{ id: 42, name: 'InfluxDB DS', meta: {} }
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('InfluxDBConnector', () => {
|
describe('InfluxDBConnector', () => {
|
||||||
let ctx = {};
|
let ctx = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
|
ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
|
||||||
ctx.datasourceSrvMock = {
|
ctx.influxDBConnector = new InfluxDBConnector(ctx.options);
|
||||||
loadDatasource: jest.fn().mockResolvedValue(
|
|
||||||
{ id: 42, name: 'InfluxDB DS', meta: {} }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
ctx.influxDBConnector = new InfluxDBConnector(ctx.options, ctx.datasourceSrvMock);
|
|
||||||
ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]);
|
ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]);
|
||||||
ctx.defaultQueryParams = {
|
ctx.defaultQueryParams = {
|
||||||
itemids: ['123', '234'],
|
itemids: ['123', '234'],
|
||||||
|
|||||||
@@ -20,35 +20,30 @@ const POINT_TIMESTAMP = 1;
|
|||||||
* Downsample time series by using given function (avg, min, max).
|
* Downsample time series by using given function (avg, min, max).
|
||||||
*/
|
*/
|
||||||
function downsample(datapoints, time_to, ms_interval, func) {
|
function downsample(datapoints, time_to, ms_interval, func) {
|
||||||
var downsampledSeries = [];
|
const downsampledSeries = [];
|
||||||
var timeWindow = {
|
const timeWindow = {
|
||||||
from: time_to * 1000 - ms_interval,
|
from: time_to * 1000 - ms_interval,
|
||||||
to: time_to * 1000
|
to: time_to * 1000
|
||||||
};
|
};
|
||||||
|
|
||||||
var points_sum = 0;
|
let points_sum = 0;
|
||||||
var points_num = 0;
|
let points_num = 0;
|
||||||
var value_avg = 0;
|
let value_avg = 0;
|
||||||
var frame = [];
|
let frame = [];
|
||||||
|
|
||||||
for (var i = datapoints.length - 1; i >= 0; i -= 1) {
|
for (let i = datapoints.length - 1; i >= 0; i -= 1) {
|
||||||
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
|
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
|
||||||
points_sum += datapoints[i][0];
|
points_sum += datapoints[i][0];
|
||||||
points_num++;
|
points_num++;
|
||||||
frame.push(datapoints[i][0]);
|
frame.push(datapoints[i][0]);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
value_avg = points_num ? points_sum / points_num : 0;
|
value_avg = points_num ? points_sum / points_num : 0;
|
||||||
|
|
||||||
if (func === "max") {
|
if (func === "max") {
|
||||||
downsampledSeries.push([_.max(frame), timeWindow.to]);
|
downsampledSeries.push([_.max(frame), timeWindow.to]);
|
||||||
}
|
} else if (func === "min") {
|
||||||
else if (func === "min") {
|
|
||||||
downsampledSeries.push([_.min(frame), timeWindow.to]);
|
downsampledSeries.push([_.min(frame), timeWindow.to]);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// avg by default
|
|
||||||
else {
|
|
||||||
downsampledSeries.push([value_avg, timeWindow.to]);
|
downsampledSeries.push([value_avg, timeWindow.to]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,25 +67,25 @@ function downsample(datapoints, time_to, ms_interval, func) {
|
|||||||
* datapoints: [[<value>, <unixtime>], ...]
|
* datapoints: [[<value>, <unixtime>], ...]
|
||||||
*/
|
*/
|
||||||
function groupBy(datapoints, interval, groupByCallback) {
|
function groupBy(datapoints, interval, groupByCallback) {
|
||||||
var ms_interval = utils.parseInterval(interval);
|
const ms_interval = utils.parseInterval(interval);
|
||||||
|
|
||||||
// Calculate frame timestamps
|
// Calculate frame timestamps
|
||||||
var frames = _.groupBy(datapoints, function (point) {
|
const frames = _.groupBy(datapoints, point => {
|
||||||
// Calculate time for group of points
|
// Calculate time for group of points
|
||||||
return Math.floor(point[1] / ms_interval) * ms_interval;
|
return Math.floor(point[1] / ms_interval) * ms_interval;
|
||||||
});
|
});
|
||||||
|
|
||||||
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
|
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
|
||||||
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
|
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
|
||||||
var grouped = _.mapValues(frames, function (frame) {
|
const grouped = _.mapValues(frames, frame => {
|
||||||
var points = _.map(frame, function (point) {
|
const points = _.map(frame, point => {
|
||||||
return point[0];
|
return point[0];
|
||||||
});
|
});
|
||||||
return groupByCallback(points);
|
return groupByCallback(points);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert points to Grafana format
|
// Convert points to Grafana format
|
||||||
return sortByTime(_.map(grouped, function (value, timestamp) {
|
return sortByTime(_.map(grouped, (value, timestamp) => {
|
||||||
return [Number(value), Number(timestamp)];
|
return [Number(value), Number(timestamp)];
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -104,15 +99,15 @@ export function groupBy_perf(datapoints, interval, groupByCallback) {
|
|||||||
return groupByRange(datapoints, groupByCallback);
|
return groupByRange(datapoints, groupByCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ms_interval = utils.parseInterval(interval);
|
const ms_interval = utils.parseInterval(interval);
|
||||||
let grouped_series = [];
|
const grouped_series = [];
|
||||||
let frame_values = [];
|
let frame_values = [];
|
||||||
let frame_value;
|
let frame_value;
|
||||||
let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
|
let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
|
||||||
let point_frame_ts = frame_ts;
|
let point_frame_ts = frame_ts;
|
||||||
let point;
|
let point;
|
||||||
|
|
||||||
for (let i=0; i < datapoints.length; i++) {
|
for (let i = 0; i < datapoints.length; i++) {
|
||||||
point = datapoints[i];
|
point = datapoints[i];
|
||||||
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
|
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
|
||||||
if (point_frame_ts === frame_ts) {
|
if (point_frame_ts === frame_ts) {
|
||||||
@@ -142,7 +137,7 @@ export function groupByRange(datapoints, groupByCallback) {
|
|||||||
const frame_start = datapoints[0][POINT_TIMESTAMP];
|
const frame_start = datapoints[0][POINT_TIMESTAMP];
|
||||||
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
|
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
|
||||||
let point;
|
let point;
|
||||||
for (let i=0; i < datapoints.length; i++) {
|
for (let i = 0; i < datapoints.length; i++) {
|
||||||
point = datapoints[i];
|
point = datapoints[i];
|
||||||
frame_values.push(point[POINT_VALUE]);
|
frame_values.push(point[POINT_VALUE]);
|
||||||
}
|
}
|
||||||
@@ -157,30 +152,30 @@ export function groupByRange(datapoints, groupByCallback) {
|
|||||||
function sumSeries(timeseries) {
|
function sumSeries(timeseries) {
|
||||||
|
|
||||||
// Calculate new points for interpolation
|
// Calculate new points for interpolation
|
||||||
var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function (point) {
|
let new_timestamps = _.uniq(_.map(_.flatten(timeseries), point => {
|
||||||
return point[1];
|
return point[1];
|
||||||
}));
|
}));
|
||||||
new_timestamps = _.sortBy(new_timestamps);
|
new_timestamps = _.sortBy(new_timestamps);
|
||||||
|
|
||||||
var interpolated_timeseries = _.map(timeseries, function (series) {
|
const interpolated_timeseries = _.map(timeseries, series => {
|
||||||
series = fillZeroes(series, new_timestamps);
|
series = fillZeroes(series, new_timestamps);
|
||||||
var timestamps = _.map(series, function (point) {
|
const timestamps = _.map(series, point => {
|
||||||
return point[1];
|
return point[1];
|
||||||
});
|
});
|
||||||
var new_points = _.map(_.difference(new_timestamps, timestamps), function (timestamp) {
|
const new_points = _.map(_.difference(new_timestamps, timestamps), timestamp => {
|
||||||
return [null, timestamp];
|
return [null, timestamp];
|
||||||
});
|
});
|
||||||
var new_series = series.concat(new_points);
|
const new_series = series.concat(new_points);
|
||||||
return sortByTime(new_series);
|
return sortByTime(new_series);
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(interpolated_timeseries, interpolateSeries);
|
_.each(interpolated_timeseries, interpolateSeries);
|
||||||
|
|
||||||
var new_timeseries = [];
|
const new_timeseries = [];
|
||||||
var sum;
|
let sum;
|
||||||
for (var i = new_timestamps.length - 1; i >= 0; i--) {
|
for (let i = new_timestamps.length - 1; i >= 0; i--) {
|
||||||
sum = 0;
|
sum = 0;
|
||||||
for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
|
for (let j = interpolated_timeseries.length - 1; j >= 0; j--) {
|
||||||
sum += interpolated_timeseries[j][i][0];
|
sum += interpolated_timeseries[j][i][0];
|
||||||
}
|
}
|
||||||
new_timeseries.push([sum, new_timestamps[i]]);
|
new_timeseries.push([sum, new_timestamps[i]]);
|
||||||
@@ -225,9 +220,9 @@ function offset(datapoints, delta) {
|
|||||||
* @param {*} datapoints
|
* @param {*} datapoints
|
||||||
*/
|
*/
|
||||||
function delta(datapoints) {
|
function delta(datapoints) {
|
||||||
let newSeries = [];
|
const newSeries = [];
|
||||||
let deltaValue;
|
let deltaValue;
|
||||||
for (var i = 1; i < datapoints.length; i++) {
|
for (let i = 1; i < datapoints.length; i++) {
|
||||||
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
|
deltaValue = datapoints[i][0] - datapoints[i - 1][0];
|
||||||
newSeries.push([deltaValue, datapoints[i][1]]);
|
newSeries.push([deltaValue, datapoints[i][1]]);
|
||||||
}
|
}
|
||||||
@@ -239,7 +234,7 @@ function delta(datapoints) {
|
|||||||
* @param {*} datapoints
|
* @param {*} datapoints
|
||||||
*/
|
*/
|
||||||
function rate(datapoints) {
|
function rate(datapoints) {
|
||||||
let newSeries = [];
|
const newSeries = [];
|
||||||
let point, point_prev;
|
let point, point_prev;
|
||||||
let valueDelta = 0;
|
let valueDelta = 0;
|
||||||
let timeDelta = 0;
|
let timeDelta = 0;
|
||||||
@@ -261,7 +256,7 @@ function rate(datapoints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function simpleMovingAverage(datapoints, n) {
|
function simpleMovingAverage(datapoints, n) {
|
||||||
let sma = [];
|
const sma = [];
|
||||||
let w_sum;
|
let w_sum;
|
||||||
let w_avg = null;
|
let w_avg = null;
|
||||||
let w_count = 0;
|
let w_count = 0;
|
||||||
@@ -352,7 +347,7 @@ function expMovingAverage(datapoints, n) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PERCENTILE(n, values) {
|
function PERCENTILE(n, values) {
|
||||||
var sorted = _.sortBy(values);
|
const sorted = _.sortBy(values);
|
||||||
return sorted[Math.floor(sorted.length * n / 100)];
|
return sorted[Math.floor(sorted.length * n / 100)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +356,7 @@ function COUNT(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SUM(values) {
|
function SUM(values) {
|
||||||
var sum = null;
|
let sum = null;
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
if (values[i] !== null) {
|
if (values[i] !== null) {
|
||||||
sum += values[i];
|
sum += values[i];
|
||||||
@@ -371,7 +366,7 @@ function SUM(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AVERAGE(values) {
|
function AVERAGE(values) {
|
||||||
let values_non_null = getNonNullValues(values);
|
const values_non_null = getNonNullValues(values);
|
||||||
if (values_non_null.length === 0) {
|
if (values_non_null.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -379,7 +374,7 @@ function AVERAGE(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNonNullValues(values) {
|
function getNonNullValues(values) {
|
||||||
let values_non_null = [];
|
const values_non_null = [];
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
if (values[i] !== null) {
|
if (values[i] !== null) {
|
||||||
values_non_null.push(values[i]);
|
values_non_null.push(values[i]);
|
||||||
@@ -397,7 +392,7 @@ function MAX(values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MEDIAN(values) {
|
function MEDIAN(values) {
|
||||||
var sorted = _.sortBy(values);
|
const sorted = _.sortBy(values);
|
||||||
return sorted[Math.floor(sorted.length / 2)];
|
return sorted[Math.floor(sorted.length / 2)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +413,7 @@ function getPointTimeFrame(timestamp, ms_interval) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortByTime(series) {
|
function sortByTime(series) {
|
||||||
return _.sortBy(series, function (point) {
|
return _.sortBy(series, point => {
|
||||||
return point[1];
|
return point[1];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -432,8 +427,8 @@ function sortByTime(series) {
|
|||||||
* @param {*} timestamps
|
* @param {*} timestamps
|
||||||
*/
|
*/
|
||||||
function fillZeroes(series, timestamps) {
|
function fillZeroes(series, timestamps) {
|
||||||
let prepend = [];
|
const prepend = [];
|
||||||
let append = [];
|
const append = [];
|
||||||
let new_point;
|
let new_point;
|
||||||
for (let i = 0; i < timestamps.length; i++) {
|
for (let i = 0; i < timestamps.length; i++) {
|
||||||
if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
|
if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
|
||||||
@@ -451,10 +446,10 @@ function fillZeroes(series, timestamps) {
|
|||||||
* Interpolate series with gaps
|
* Interpolate series with gaps
|
||||||
*/
|
*/
|
||||||
function interpolateSeries(series) {
|
function interpolateSeries(series) {
|
||||||
var left, right;
|
let left, right;
|
||||||
|
|
||||||
// Interpolate series
|
// Interpolate series
|
||||||
for (var i = series.length - 1; i >= 0; i--) {
|
for (let i = series.length - 1; i >= 0; i--) {
|
||||||
if (!series[i][0]) {
|
if (!series[i][0]) {
|
||||||
left = findNearestLeft(series, i);
|
left = findNearestLeft(series, i);
|
||||||
right = findNearestRight(series, i);
|
right = findNearestRight(series, i);
|
||||||
@@ -479,7 +474,7 @@ function linearInterpolation(timestamp, left, right) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNearestRight(series, pointIndex) {
|
function findNearestRight(series, pointIndex) {
|
||||||
for (var i = pointIndex; i < series.length; i++) {
|
for (let i = pointIndex; i < series.length; i++) {
|
||||||
if (series[i][0] !== null) {
|
if (series[i][0] !== null) {
|
||||||
return series[i];
|
return series[i];
|
||||||
}
|
}
|
||||||
@@ -488,7 +483,7 @@ function findNearestRight(series, pointIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNearestLeft(series, pointIndex) {
|
function findNearestLeft(series, pointIndex) {
|
||||||
for (var i = pointIndex; i > 0; i--) {
|
for (let i = pointIndex; i > 0; i--) {
|
||||||
if (series[i][0] !== null) {
|
if (series[i][0] !== null) {
|
||||||
return series[i];
|
return series[i];
|
||||||
}
|
}
|
||||||
@@ -111,4 +111,183 @@ export enum VariableQueryTypes {
|
|||||||
Host = 'host',
|
Host = 'host',
|
||||||
Application = 'application',
|
Application = 'application',
|
||||||
Item = 'item',
|
Item = 'item',
|
||||||
|
ItemValues = 'itemValues',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ShowProblemTypes {
|
||||||
|
Problems = 'problems',
|
||||||
|
Recent = 'recent',
|
||||||
|
History = 'history',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemDTO {
|
||||||
|
triggerid?: string;
|
||||||
|
eventid?: string;
|
||||||
|
timestamp: number;
|
||||||
|
|
||||||
|
/** Name of the trigger. */
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/** Same as a name. */
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/** Whether the trigger is in OK or problem state. */
|
||||||
|
value?: string;
|
||||||
|
|
||||||
|
datasource?: string;
|
||||||
|
comments?: string;
|
||||||
|
host?: string;
|
||||||
|
hostTechName?: string;
|
||||||
|
proxy?: string;
|
||||||
|
severity?: string;
|
||||||
|
|
||||||
|
acknowledged?: '1' | '0';
|
||||||
|
acknowledges?: ZBXAcknowledge[];
|
||||||
|
|
||||||
|
groups?: ZBXGroup[];
|
||||||
|
hosts?: ZBXHost[];
|
||||||
|
items?: ZBXItem[];
|
||||||
|
alerts?: ZBXAlert[];
|
||||||
|
tags?: ZBXTag[];
|
||||||
|
url?: string;
|
||||||
|
|
||||||
|
expression?: string;
|
||||||
|
correlation_mode?: string;
|
||||||
|
correlation_tag?: string;
|
||||||
|
suppressed?: string;
|
||||||
|
suppression_data?: any[];
|
||||||
|
state?: string;
|
||||||
|
maintenance?: boolean;
|
||||||
|
manual_close?: string;
|
||||||
|
error?: string;
|
||||||
|
|
||||||
|
showAckButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXProblem {
|
||||||
|
acknowledged?: '1' | '0';
|
||||||
|
acknowledges?: ZBXAcknowledge[];
|
||||||
|
clock: string;
|
||||||
|
ns: string;
|
||||||
|
correlationid?: string;
|
||||||
|
datasource?: string;
|
||||||
|
name?: string;
|
||||||
|
eventid?: string;
|
||||||
|
maintenance?: boolean;
|
||||||
|
object?: string;
|
||||||
|
objectid?: string;
|
||||||
|
opdata?: any;
|
||||||
|
r_eventid?: string;
|
||||||
|
r_clock?: string;
|
||||||
|
r_ns?: string;
|
||||||
|
severity?: string;
|
||||||
|
showAckButton?: boolean;
|
||||||
|
source?: string;
|
||||||
|
suppressed?: string;
|
||||||
|
suppression_data?: any[];
|
||||||
|
tags?: ZBXTag[];
|
||||||
|
userid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXTrigger {
|
||||||
|
acknowledges?: ZBXAcknowledge[];
|
||||||
|
showAckButton?: boolean;
|
||||||
|
alerts?: ZBXAlert[];
|
||||||
|
age?: string;
|
||||||
|
color?: string;
|
||||||
|
comments?: string;
|
||||||
|
correlation_mode?: string;
|
||||||
|
correlation_tag?: string;
|
||||||
|
datasource?: string;
|
||||||
|
description?: string;
|
||||||
|
error?: string;
|
||||||
|
expression?: string;
|
||||||
|
flags?: string;
|
||||||
|
groups?: ZBXGroup[];
|
||||||
|
host?: string;
|
||||||
|
hostTechName?: string;
|
||||||
|
hosts?: ZBXHost[];
|
||||||
|
items?: ZBXItem[];
|
||||||
|
lastEvent?: ZBXEvent;
|
||||||
|
lastchange?: string;
|
||||||
|
lastchangeUnix?: number;
|
||||||
|
maintenance?: boolean;
|
||||||
|
manual_close?: string;
|
||||||
|
priority?: string;
|
||||||
|
proxy?: string;
|
||||||
|
recovery_expression?: string;
|
||||||
|
recovery_mode?: string;
|
||||||
|
severity?: string;
|
||||||
|
state?: string;
|
||||||
|
status?: string;
|
||||||
|
tags?: ZBXTag[];
|
||||||
|
templateid?: string;
|
||||||
|
triggerid?: string;
|
||||||
|
/** Whether the trigger can generate multiple problem events. */
|
||||||
|
type?: string;
|
||||||
|
url?: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXGroup {
|
||||||
|
groupid: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXHost {
|
||||||
|
hostid: string;
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
maintenance_status?: string;
|
||||||
|
proxy_hostid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXItem {
|
||||||
|
itemid: string;
|
||||||
|
name: string;
|
||||||
|
key_: string;
|
||||||
|
lastvalue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXEvent {
|
||||||
|
eventid: string;
|
||||||
|
clock: string;
|
||||||
|
ns?: string;
|
||||||
|
value?: string;
|
||||||
|
name?: string;
|
||||||
|
source?: string;
|
||||||
|
object?: string;
|
||||||
|
objectid?: string;
|
||||||
|
severity?: string;
|
||||||
|
hosts?: ZBXHost[];
|
||||||
|
acknowledged?: '1' | '0';
|
||||||
|
acknowledges?: ZBXAcknowledge[];
|
||||||
|
tags?: ZBXTag[];
|
||||||
|
suppressed?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXTag {
|
||||||
|
tag: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXAcknowledge {
|
||||||
|
acknowledgeid: string;
|
||||||
|
eventid: string;
|
||||||
|
userid: string;
|
||||||
|
action: string;
|
||||||
|
clock: string;
|
||||||
|
time: string;
|
||||||
|
message?: string;
|
||||||
|
user: string;
|
||||||
|
alias: string;
|
||||||
|
name: string;
|
||||||
|
surname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZBXAlert {
|
||||||
|
eventid: string;
|
||||||
|
clock: string;
|
||||||
|
message: string;
|
||||||
|
error: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\
|
|||||||
* @param {string} key item key, ie system.cpu.util[,system,avg1]
|
* @param {string} key item key, ie system.cpu.util[,system,avg1]
|
||||||
* @return {string} expanded name, ie "CPU system time"
|
* @return {string} expanded name, ie "CPU system time"
|
||||||
*/
|
*/
|
||||||
export function expandItemName(name, key) {
|
export function expandItemName(name: string, key: string): string {
|
||||||
|
|
||||||
// extract params from key:
|
// extract params from key:
|
||||||
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
|
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
|
||||||
@@ -78,13 +78,26 @@ export function containsMacro(itemName) {
|
|||||||
return MACRO_PATTERN.test(itemName);
|
return MACRO_PATTERN.test(itemName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceMacro(item, macros) {
|
export function replaceMacro(item, macros, isTriggerItem?) {
|
||||||
let itemName = item.name;
|
let itemName = isTriggerItem ? item.url : item.name;
|
||||||
const item_macros = itemName.match(MACRO_PATTERN);
|
const item_macros = itemName.match(MACRO_PATTERN);
|
||||||
_.forEach(item_macros, macro => {
|
_.forEach(item_macros, macro => {
|
||||||
const host_macros = _.filter(macros, m => {
|
const host_macros = _.filter(macros, m => {
|
||||||
if (m.hostid) {
|
if (m.hostid) {
|
||||||
|
if (isTriggerItem) {
|
||||||
|
// Trigger item can have multiple hosts
|
||||||
|
// Check all trigger host ids against macro host id
|
||||||
|
let hostIdFound = false;
|
||||||
|
_.forEach(item.hosts, h => {
|
||||||
|
if (h.hostid === m.hostid) {
|
||||||
|
hostIdFound = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hostIdFound;
|
||||||
|
} else {
|
||||||
|
// Check app host id against macro host id
|
||||||
return m.hostid === item.hostid;
|
return m.hostid === item.hostid;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add global macros
|
// Add global macros
|
||||||
return true;
|
return true;
|
||||||
@@ -222,10 +235,11 @@ export function escapeRegex(value) {
|
|||||||
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
|
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseInterval(interval) {
|
export function parseInterval(interval: string): number {
|
||||||
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
|
const intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
|
||||||
const momentInterval: any[] = intervalPattern.exec(interval);
|
const momentInterval: any[] = intervalPattern.exec(interval);
|
||||||
return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf();
|
const duration = moment.duration(Number(momentInterval[1]), momentInterval[2]);
|
||||||
|
return (duration.valueOf() as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTimeShiftInterval(interval) {
|
export function parseTimeShiftInterval(interval) {
|
||||||
@@ -314,7 +328,7 @@ export function isValidVersion(version) {
|
|||||||
return versionPattern.exec(version);
|
return versionPattern.exec(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseVersion(version) {
|
export function parseVersion(version: string) {
|
||||||
const match = versionPattern.exec(version);
|
const match = versionPattern.exec(version);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
@@ -344,7 +358,29 @@ export function getArrayDepth(a, level = 0) {
|
|||||||
return level + 1;
|
return level + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for backward compatibility with lodash 2.4
|
/**
|
||||||
if (!_.includes) {
|
* Checks whether its argument represents a numeric value.
|
||||||
_.includes = (_ as any).contains;
|
*/
|
||||||
|
export function isNumeric(n: any): boolean {
|
||||||
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses tags string into array of {tag: value} objects
|
||||||
|
*/
|
||||||
|
export function parseTags(tagStr: string): any[] {
|
||||||
|
if (!tagStr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags: any[] = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||||
|
tags = _.map(tags, (tag) => {
|
||||||
|
const tagParts = tag.split(':');
|
||||||
|
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
||||||
|
});
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mustArray(result: any): any[] {
|
||||||
|
return result || [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
export const DEFAULT_QUERY_LIMIT = 10000;
|
export const DEFAULT_QUERY_LIMIT = 10000;
|
||||||
export const HISTORY_TO_TABLE_MAP = {
|
export const HISTORY_TO_TABLE_MAP = {
|
||||||
@@ -34,31 +35,30 @@ export const consolidateByTrendColumns = {
|
|||||||
* `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API.
|
* `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API.
|
||||||
*/
|
*/
|
||||||
export class DBConnector {
|
export class DBConnector {
|
||||||
constructor(options, datasourceSrv) {
|
constructor(options) {
|
||||||
this.datasourceSrv = datasourceSrv;
|
|
||||||
this.datasourceId = options.datasourceId;
|
this.datasourceId = options.datasourceId;
|
||||||
this.datasourceName = options.datasourceName;
|
this.datasourceName = options.datasourceName;
|
||||||
this.datasourceTypeId = null;
|
this.datasourceTypeId = null;
|
||||||
this.datasourceTypeName = null;
|
this.datasourceTypeName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadDatasource(dsId, dsName, datasourceSrv) {
|
static loadDatasource(dsId, dsName) {
|
||||||
if (!dsName && dsId !== undefined) {
|
if (!dsName && dsId !== undefined) {
|
||||||
let ds = _.find(datasourceSrv.getAll(), {'id': dsId});
|
let ds = _.find(getDataSourceSrv().getAll(), {'id': dsId});
|
||||||
if (!ds) {
|
if (!ds) {
|
||||||
return Promise.reject(`Data Source with ID ${dsId} not found`);
|
return Promise.reject(`Data Source with ID ${dsId} not found`);
|
||||||
}
|
}
|
||||||
dsName = ds.name;
|
dsName = ds.name;
|
||||||
}
|
}
|
||||||
if (dsName) {
|
if (dsName) {
|
||||||
return datasourceSrv.loadDatasource(dsName);
|
return getDataSourceSrv().loadDatasource(dsName);
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(`Data Source name should be specified`);
|
return Promise.reject(`Data Source name should be specified`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDBDataSource() {
|
loadDBDataSource() {
|
||||||
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName, this.datasourceSrv)
|
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName)
|
||||||
.then(ds => {
|
.then(ds => {
|
||||||
this.datasourceTypeId = ds.meta.id;
|
this.datasourceTypeId = ds.meta.id;
|
||||||
this.datasourceTypeName = ds.meta.name;
|
this.datasourceTypeName = ds.meta.name;
|
||||||
@@ -123,22 +123,36 @@ export class ZabbixNotImplemented {
|
|||||||
*/
|
*/
|
||||||
function convertGrafanaTSResponse(time_series, items, addHostName) {
|
function convertGrafanaTSResponse(time_series, items, addHostName) {
|
||||||
//uniqBy is needed to deduplicate
|
//uniqBy is needed to deduplicate
|
||||||
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid');
|
const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid');
|
||||||
let grafanaSeries = _.map(_.compact(time_series), series => {
|
let grafanaSeries = _.map(_.compact(time_series), series => {
|
||||||
let itemid = series.name;
|
const itemid = series.name;
|
||||||
var item = _.find(items, {'itemid': itemid});
|
const item = _.find(items, {'itemid': itemid});
|
||||||
var alias = item.name;
|
let alias = item.name;
|
||||||
//only when actual multi hosts selected
|
|
||||||
|
// Add scopedVars for using in alias functions
|
||||||
|
const scopedVars = {
|
||||||
|
'__zbx_item': { value: item.name },
|
||||||
|
'__zbx_item_name': { value: item.name },
|
||||||
|
'__zbx_item_key': { value: item.key_ },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_.keys(hosts).length > 0) {
|
||||||
|
const host = _.find(hosts, {'hostid': item.hostid});
|
||||||
|
scopedVars['__zbx_host'] = { value: host.host };
|
||||||
|
scopedVars['__zbx_host_name'] = { value: host.name };
|
||||||
|
|
||||||
|
// Only add host when multiple hosts selected
|
||||||
if (_.keys(hosts).length > 1 && addHostName) {
|
if (_.keys(hosts).length > 1 && addHostName) {
|
||||||
var host = _.find(hosts, {'hostid': item.hostid});
|
|
||||||
alias = host.name + ": " + alias;
|
alias = host.name + ": " + alias;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// CachingProxy deduplicates requests and returns one time series for equal queries.
|
// CachingProxy deduplicates requests and returns one time series for equal queries.
|
||||||
// Clone is needed to prevent changing of series object shared between all targets.
|
// Clone is needed to prevent changing of series object shared between all targets.
|
||||||
let datapoints = _.cloneDeep(series.points);
|
const datapoints = _.cloneDeep(series.points);
|
||||||
return {
|
return {
|
||||||
target: alias,
|
target: alias,
|
||||||
datapoints: datapoints
|
datapoints,
|
||||||
|
scopedVars,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const consolidateByFunc = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class InfluxDBConnector extends DBConnector {
|
export class InfluxDBConnector extends DBConnector {
|
||||||
constructor(options, datasourceSrv) {
|
constructor(options) {
|
||||||
super(options, datasourceSrv);
|
super(options);
|
||||||
this.retentionPolicy = options.retentionPolicy;
|
this.retentionPolicy = options.retentionPolicy;
|
||||||
super.loadDBDataSource().then(ds => {
|
super.loadDBDataSource().then(ds => {
|
||||||
this.influxDS = ds;
|
this.influxDS = ds;
|
||||||
@@ -24,7 +24,14 @@ export class InfluxDBConnector extends DBConnector {
|
|||||||
* Try to invoke test query for one of Zabbix database tables.
|
* Try to invoke test query for one of Zabbix database tables.
|
||||||
*/
|
*/
|
||||||
testDataSource() {
|
testDataSource() {
|
||||||
return this.influxDS.testDatasource();
|
return this.influxDS.testDatasource().then(result => {
|
||||||
|
if (result.status && result.status === 'error') {
|
||||||
|
return Promise.reject({ data: {
|
||||||
|
message: `InfluxDB connection error: ${result.message}`
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHistory(items, timeFrom, timeTill, options) {
|
getHistory(items, timeFrom, timeTill, options) {
|
||||||
|
|||||||
@@ -3,26 +3,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
|
function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
|
||||||
let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
|
|
||||||
let query = `
|
let query = `
|
||||||
SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(value) AS value
|
SELECT CAST(itemid AS CHAR) AS metric, MIN(clock) AS time_sec, ${aggFunction}(value) AS value
|
||||||
FROM ${table}
|
FROM ${table}
|
||||||
WHERE itemid IN (${itemids})
|
WHERE itemid IN (${itemids})
|
||||||
AND clock > ${timeFrom} AND clock < ${timeTill}
|
AND clock > ${timeFrom} AND clock < ${timeTill}
|
||||||
GROUP BY ${time_expression}, metric
|
GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric
|
||||||
ORDER BY time_sec ASC
|
ORDER BY time_sec ASC
|
||||||
`;
|
`;
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
|
function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
|
||||||
let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
|
|
||||||
let query = `
|
let query = `
|
||||||
SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(${valueColumn}) AS value
|
SELECT CAST(itemid AS CHAR) AS metric, MIN(clock) AS time_sec, ${aggFunction}(${valueColumn}) AS value
|
||||||
FROM ${table}
|
FROM ${table}
|
||||||
WHERE itemid IN (${itemids})
|
WHERE itemid IN (${itemids})
|
||||||
AND clock > ${timeFrom} AND clock < ${timeTill}
|
AND clock > ${timeFrom} AND clock < ${timeTill}
|
||||||
GROUP BY ${time_expression}, metric
|
GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric
|
||||||
ORDER BY time_sec ASC
|
ORDER BY time_sec ASC
|
||||||
`;
|
`;
|
||||||
return query;
|
return query;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { compactQuery } from '../../../utils';
|
import { compactQuery } from '../../../utils';
|
||||||
import mysql from './mysql';
|
import mysql from './mysql';
|
||||||
import postgres from './postgres';
|
import postgres from './postgres';
|
||||||
@@ -10,15 +11,14 @@ const supportedDatabases = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class SQLConnector extends DBConnector {
|
export class SQLConnector extends DBConnector {
|
||||||
constructor(options, datasourceSrv) {
|
constructor(options) {
|
||||||
super(options, datasourceSrv);
|
super(options);
|
||||||
|
|
||||||
this.limit = options.limit || DEFAULT_QUERY_LIMIT;
|
this.limit = options.limit || DEFAULT_QUERY_LIMIT;
|
||||||
this.sqlDialect = null;
|
this.sqlDialect = null;
|
||||||
|
|
||||||
super.loadDBDataSource()
|
super.loadDBDataSource()
|
||||||
.then(ds => {
|
.then(() => {
|
||||||
this.backendSrv = ds.backendSrv;
|
|
||||||
this.loadSQLDialect();
|
this.loadSQLDialect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,12 @@ export class SQLConnector extends DBConnector {
|
|||||||
let {intervalMs, consolidateBy} = options;
|
let {intervalMs, consolidateBy} = options;
|
||||||
let intervalSec = Math.ceil(intervalMs / 1000);
|
let intervalSec = Math.ceil(intervalMs / 1000);
|
||||||
|
|
||||||
|
// The interval must match the time range exactly n times, otherwise
|
||||||
|
// the resulting first and last data points will yield invalid values in the
|
||||||
|
// calculated average value in downsampleSeries - when using consolidateBy(avg)
|
||||||
|
let numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec);
|
||||||
|
intervalSec = (timeTill - timeFrom) / numOfIntervals;
|
||||||
|
|
||||||
consolidateBy = consolidateBy || 'avg';
|
consolidateBy = consolidateBy || 'avg';
|
||||||
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
|
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
|
||||||
|
|
||||||
@@ -66,6 +72,12 @@ export class SQLConnector extends DBConnector {
|
|||||||
let { intervalMs, consolidateBy } = options;
|
let { intervalMs, consolidateBy } = options;
|
||||||
let intervalSec = Math.ceil(intervalMs / 1000);
|
let intervalSec = Math.ceil(intervalMs / 1000);
|
||||||
|
|
||||||
|
// The interval must match the time range exactly n times, otherwise
|
||||||
|
// the resulting first and last data points will yield invalid values in the
|
||||||
|
// calculated average value in downsampleSeries - when using consolidateBy(avg)
|
||||||
|
let numOfIntervals = Math.ceil((timeTill - timeFrom) / intervalSec);
|
||||||
|
intervalSec = (timeTill - timeFrom) / numOfIntervals;
|
||||||
|
|
||||||
consolidateBy = consolidateBy || 'avg';
|
consolidateBy = consolidateBy || 'avg';
|
||||||
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
|
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
|
||||||
|
|
||||||
@@ -96,7 +108,7 @@ export class SQLConnector extends DBConnector {
|
|||||||
maxDataPoints: this.limit
|
maxDataPoints: this.limit
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest({
|
return getBackendSrv().datasourceRequest({
|
||||||
url: '/api/tsdb/query',
|
url: '/api/tsdb/query',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
42
src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts
Normal file
42
src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export interface JSONRPCRequest {
|
||||||
|
jsonrpc: '2.0' | string;
|
||||||
|
method: string;
|
||||||
|
id: number;
|
||||||
|
auth?: string | null;
|
||||||
|
params?: JSONRPCRequestParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JSONRPCResponse<T> {
|
||||||
|
jsonrpc: '2.0' | string;
|
||||||
|
id: number;
|
||||||
|
result?: T;
|
||||||
|
error?: JSONRPCError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JSONRPCError {
|
||||||
|
code?: number;
|
||||||
|
message?: string;
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GFHTTPRequest {
|
||||||
|
method: HTTPMethod;
|
||||||
|
url: string;
|
||||||
|
data?: any;
|
||||||
|
headers?: {[key: string]: string};
|
||||||
|
withCredentials?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JSONRPCRequestParams = {[key: string]: any};
|
||||||
|
|
||||||
|
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE';
|
||||||
|
|
||||||
|
export type GFRequestOptions = {[key: string]: any};
|
||||||
|
|
||||||
|
export interface ZabbixRequestResponse {
|
||||||
|
data?: JSONRPCResponse<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ZabbixAPIResponse<T> = T;
|
||||||
|
|
||||||
|
export type APILoginResponse = string;
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import semver from 'semver';
|
||||||
import kbn from 'grafana/app/core/utils/kbn';
|
import kbn from 'grafana/app/core/utils/kbn';
|
||||||
import * as utils from '../../../utils';
|
import * as utils from '../../../utils';
|
||||||
import { ZabbixAPICore } from './zabbixAPICore';
|
import { ZabbixAPICore } from './zabbixAPICore';
|
||||||
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
|
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
|
||||||
|
import { ShowProblemTypes, ZBXProblem } from '../../../types';
|
||||||
|
import { JSONRPCRequestParams } from './types';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
const DEFAULT_ZABBIX_VERSION = '3.0.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zabbix API Wrapper.
|
* Zabbix API Wrapper.
|
||||||
@@ -10,12 +16,25 @@ import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MI
|
|||||||
* Wraps API calls and provides high-level methods.
|
* Wraps API calls and provides high-level methods.
|
||||||
*/
|
*/
|
||||||
export class ZabbixAPIConnector {
|
export class ZabbixAPIConnector {
|
||||||
constructor(api_url, username, password, version, basicAuth, withCredentials, backendSrv, datasourceId) {
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
auth: string;
|
||||||
|
requestOptions: { basicAuth: any; withCredentials: boolean; };
|
||||||
|
loginPromise: Promise<string>;
|
||||||
|
loginErrorCount: number;
|
||||||
|
maxLoginAttempts: number;
|
||||||
|
zabbixAPICore: ZabbixAPICore;
|
||||||
|
getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>;
|
||||||
|
version: string;
|
||||||
|
getVersionPromise: Promise<string>;
|
||||||
|
datasourceId: number;
|
||||||
|
|
||||||
|
constructor(api_url: string, username: string, password: string, basicAuth: any, withCredentials: boolean, datasourceId: number) {
|
||||||
this.url = api_url;
|
this.url = api_url;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.auth = '';
|
this.auth = '';
|
||||||
this.version = version;
|
|
||||||
|
|
||||||
this.requestOptions = {
|
this.requestOptions = {
|
||||||
basicAuth: basicAuth,
|
basicAuth: basicAuth,
|
||||||
@@ -23,16 +42,17 @@ export class ZabbixAPIConnector {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.datasourceId = datasourceId;
|
this.datasourceId = datasourceId;
|
||||||
this.backendSrv = backendSrv;
|
|
||||||
|
|
||||||
this.loginPromise = null;
|
this.loginPromise = null;
|
||||||
this.loginErrorCount = 0;
|
this.loginErrorCount = 0;
|
||||||
this.maxLoginAttempts = 3;
|
this.maxLoginAttempts = 3;
|
||||||
|
|
||||||
this.zabbixAPICore = new ZabbixAPICore(backendSrv);
|
this.zabbixAPICore = new ZabbixAPICore();
|
||||||
|
|
||||||
this.getTrend = this.getTrend_ZBXNEXT1193;
|
this.getTrend = this.getTrend_ZBXNEXT1193;
|
||||||
//getTrend = getTrend_30;
|
//getTrend = getTrend_30;
|
||||||
|
|
||||||
|
this.initVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
@@ -59,13 +79,40 @@ export class ZabbixAPIConnector {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest({
|
return getBackendSrv().datasourceRequest({
|
||||||
url: '/api/tsdb/query',
|
url: '/api/tsdb/query',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: tsdbRequestData
|
data: tsdbRequestData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_request(method: string, params: JSONRPCRequestParams): Promise<any> {
|
||||||
|
if (!this.version) {
|
||||||
|
return this.initVersion().then(() => this.request(method, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth)
|
||||||
|
.catch(error => {
|
||||||
|
if (isNotInitialized(error.data)) {
|
||||||
|
// If API not initialized yet (auth is empty), login first
|
||||||
|
return this.loginOnce()
|
||||||
|
.then(() => this.request(method, params));
|
||||||
|
} else if (isNotAuthorized(error.data)) {
|
||||||
|
// Handle auth errors
|
||||||
|
this.loginErrorCount++;
|
||||||
|
if (this.loginErrorCount > this.maxLoginAttempts) {
|
||||||
|
this.loginErrorCount = 0;
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return this.loginOnce()
|
||||||
|
.then(() => this.request(method, params));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleTsdbResponse(response) {
|
handleTsdbResponse(response) {
|
||||||
if (!response || !response.data || !response.data.results) {
|
if (!response || !response.data || !response.data.results) {
|
||||||
return [];
|
return [];
|
||||||
@@ -78,9 +125,8 @@ export class ZabbixAPIConnector {
|
|||||||
* When API unauthenticated or auth token expired each request produce login()
|
* When API unauthenticated or auth token expired each request produce login()
|
||||||
* call. But auth token is common to all requests. This function wraps login() method
|
* call. But auth token is common to all requests. This function wraps login() method
|
||||||
* and call it once. If login() already called just wait for it (return its promise).
|
* and call it once. If login() already called just wait for it (return its promise).
|
||||||
* @return login promise
|
|
||||||
*/
|
*/
|
||||||
loginOnce() {
|
loginOnce(): Promise<string> {
|
||||||
if (!this.loginPromise) {
|
if (!this.loginPromise) {
|
||||||
this.loginPromise = Promise.resolve(
|
this.loginPromise = Promise.resolve(
|
||||||
this.login().then(auth => {
|
this.login().then(auth => {
|
||||||
@@ -96,7 +142,7 @@ export class ZabbixAPIConnector {
|
|||||||
/**
|
/**
|
||||||
* Get authentication token.
|
* Get authentication token.
|
||||||
*/
|
*/
|
||||||
login() {
|
login(): Promise<string> {
|
||||||
return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions);
|
return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,23 +153,49 @@ export class ZabbixAPIConnector {
|
|||||||
return this.zabbixAPICore.getVersion(this.url, this.requestOptions);
|
return this.zabbixAPICore.getVersion(this.url, this.requestOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initVersion(): Promise<string> {
|
||||||
|
if (!this.getVersionPromise) {
|
||||||
|
this.getVersionPromise = Promise.resolve(
|
||||||
|
this.getVersion().then(version => {
|
||||||
|
if (version) {
|
||||||
|
console.log(`Zabbix version detected: ${version}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Failed to detect Zabbix version, use default ${DEFAULT_ZABBIX_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.version = version || DEFAULT_ZABBIX_VERSION;
|
||||||
|
this.getVersionPromise = null;
|
||||||
|
return version;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.getVersionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Zabbix API method wrappers //
|
// Zabbix API method wrappers //
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
|
|
||||||
acknowledgeEvent(eventid, message) {
|
acknowledgeEvent(eventid: string, message: string, action?: number, severity?: number) {
|
||||||
const action = this.version >= 4 ? ZBX_ACK_ACTION_ACK + ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE;
|
if (!action) {
|
||||||
const params = {
|
action = semver.gte(this.version, '4.0.0') ? ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
eventids: eventid,
|
eventids: eventid,
|
||||||
message: message,
|
message: message,
|
||||||
action: action
|
action: action
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (severity) {
|
||||||
|
params.severity = severity;
|
||||||
|
}
|
||||||
|
|
||||||
return this.request('event.acknowledge', params);
|
return this.request('event.acknowledge', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroups() {
|
getGroups() {
|
||||||
var params = {
|
const params = {
|
||||||
output: ['name'],
|
output: ['name'],
|
||||||
sortfield: 'name',
|
sortfield: 'name',
|
||||||
real_hosts: true
|
real_hosts: true
|
||||||
@@ -133,7 +205,7 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHosts(groupids) {
|
getHosts(groupids) {
|
||||||
var params = {
|
const params: any = {
|
||||||
output: ['name', 'host'],
|
output: ['name', 'host'],
|
||||||
sortfield: 'name'
|
sortfield: 'name'
|
||||||
};
|
};
|
||||||
@@ -144,8 +216,8 @@ export class ZabbixAPIConnector {
|
|||||||
return this.request('host.get', params);
|
return this.request('host.get', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApps(hostids) {
|
getApps(hostids): Promise<any[]> {
|
||||||
var params = {
|
const params = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
hostids: hostids
|
hostids: hostids
|
||||||
};
|
};
|
||||||
@@ -161,7 +233,7 @@ export class ZabbixAPIConnector {
|
|||||||
* @return {[type]} array of items
|
* @return {[type]} array of items
|
||||||
*/
|
*/
|
||||||
getItems(hostids, appids, itemtype) {
|
getItems(hostids, appids, itemtype) {
|
||||||
var params = {
|
const params: any = {
|
||||||
output: [
|
output: [
|
||||||
'name', 'key_',
|
'name', 'key_',
|
||||||
'value_type',
|
'value_type',
|
||||||
@@ -172,7 +244,7 @@ export class ZabbixAPIConnector {
|
|||||||
sortfield: 'name',
|
sortfield: 'name',
|
||||||
webitems: true,
|
webitems: true,
|
||||||
filter: {},
|
filter: {},
|
||||||
selectHosts: ['hostid', 'name']
|
selectHosts: ['hostid', 'name', 'host']
|
||||||
};
|
};
|
||||||
if (hostids) {
|
if (hostids) {
|
||||||
params.hostids = hostids;
|
params.hostids = hostids;
|
||||||
@@ -194,7 +266,7 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getItemsByIDs(itemids) {
|
getItemsByIDs(itemids) {
|
||||||
var params = {
|
const params = {
|
||||||
itemids: itemids,
|
itemids: itemids,
|
||||||
output: [
|
output: [
|
||||||
'name', 'key_',
|
'name', 'key_',
|
||||||
@@ -208,11 +280,11 @@ export class ZabbixAPIConnector {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return this.request('item.get', params)
|
return this.request('item.get', params)
|
||||||
.then(utils.expandItems);
|
.then(items => utils.expandItems(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
getMacros(hostids) {
|
getMacros(hostids) {
|
||||||
var params = {
|
const params = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
hostids: hostids
|
hostids: hostids
|
||||||
};
|
};
|
||||||
@@ -221,7 +293,7 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGlobalMacros() {
|
getGlobalMacros() {
|
||||||
var params = {
|
const params = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
globalmacro: true
|
globalmacro: true
|
||||||
};
|
};
|
||||||
@@ -230,7 +302,7 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLastValue(itemid) {
|
getLastValue(itemid) {
|
||||||
var params = {
|
const params = {
|
||||||
output: ['lastvalue'],
|
output: ['lastvalue'],
|
||||||
itemids: itemid
|
itemids: itemid
|
||||||
};
|
};
|
||||||
@@ -249,10 +321,10 @@ export class ZabbixAPIConnector {
|
|||||||
getHistory(items, timeFrom, timeTill) {
|
getHistory(items, timeFrom, timeTill) {
|
||||||
|
|
||||||
// Group items by value type and perform request for each value type
|
// Group items by value type and perform request for each value type
|
||||||
let grouped_items = _.groupBy(items, 'value_type');
|
const grouped_items = _.groupBy(items, 'value_type');
|
||||||
let promises = _.map(grouped_items, (items, value_type) => {
|
const promises = _.map(grouped_items, (items, value_type) => {
|
||||||
let itemids = _.map(items, 'itemid');
|
const itemids = _.map(items, 'itemid');
|
||||||
let params = {
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
history: value_type,
|
history: value_type,
|
||||||
itemids: itemids,
|
itemids: itemids,
|
||||||
@@ -284,10 +356,10 @@ export class ZabbixAPIConnector {
|
|||||||
getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
|
getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
|
||||||
|
|
||||||
// Group items by value type and perform request for each value type
|
// Group items by value type and perform request for each value type
|
||||||
let grouped_items = _.groupBy(items, 'value_type');
|
const grouped_items = _.groupBy(items, 'value_type');
|
||||||
let promises = _.map(grouped_items, (items, value_type) => {
|
const promises = _.map(grouped_items, (items, value_type) => {
|
||||||
let itemids = _.map(items, 'itemid');
|
const itemids = _.map(items, 'itemid');
|
||||||
let params = {
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
trend: value_type,
|
trend: value_type,
|
||||||
itemids: itemids,
|
itemids: itemids,
|
||||||
@@ -308,10 +380,10 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTrend_30(items, time_from, time_till, value_type) {
|
getTrend_30(items, time_from, time_till, value_type) {
|
||||||
var self = this;
|
const self = this;
|
||||||
var itemids = _.map(items, 'itemid');
|
const itemids = _.map(items, 'itemid');
|
||||||
|
|
||||||
var params = {
|
const params: any = {
|
||||||
output: ["itemid",
|
output: ["itemid",
|
||||||
"clock",
|
"clock",
|
||||||
value_type
|
value_type
|
||||||
@@ -328,8 +400,8 @@ export class ZabbixAPIConnector {
|
|||||||
return self.request('trend.get', params);
|
return self.request('trend.get', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
getITService(serviceids) {
|
getITService(serviceids?) {
|
||||||
var params = {
|
const params = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
serviceids: serviceids
|
serviceids: serviceids
|
||||||
};
|
};
|
||||||
@@ -337,18 +409,88 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSLA(serviceids, timeRange, options) {
|
getSLA(serviceids, timeRange, options) {
|
||||||
const intervals = buildSLAIntervals(timeRange, options.intervalMs);
|
const [timeFrom, timeTo] = timeRange;
|
||||||
const params = {
|
let intervals = [{ from: timeFrom, to: timeTo }];
|
||||||
|
if (options.slaInterval === 'auto') {
|
||||||
|
const interval = getSLAInterval(options.intervalMs);
|
||||||
|
intervals = buildSLAIntervals(timeRange, interval);
|
||||||
|
} else if (options.slaInterval !== 'none') {
|
||||||
|
const interval = utils.parseInterval(options.slaInterval) / 1000;
|
||||||
|
intervals = buildSLAIntervals(timeRange, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
serviceids,
|
serviceids,
|
||||||
intervals
|
intervals
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.request('service.getsla', params);
|
return this.request('service.getsla', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTriggers(groupids, hostids, applicationids, options) {
|
getProblems(groupids, hostids, applicationids, options): Promise<ZBXProblem[]> {
|
||||||
let {showTriggers, maintenance, timeFrom, timeTo} = options;
|
const { timeFrom, timeTo, recent, severities, limit, acknowledged } = options;
|
||||||
|
|
||||||
let params = {
|
const params: any = {
|
||||||
|
output: 'extend',
|
||||||
|
selectAcknowledges: 'extend',
|
||||||
|
selectSuppressionData: 'extend',
|
||||||
|
selectTags: 'extend',
|
||||||
|
source: '0',
|
||||||
|
object: '0',
|
||||||
|
sortfield: ['eventid'],
|
||||||
|
sortorder: 'ASC',
|
||||||
|
evaltype: '0',
|
||||||
|
// preservekeys: '1',
|
||||||
|
groupids,
|
||||||
|
hostids,
|
||||||
|
applicationids,
|
||||||
|
recent,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (severities) {
|
||||||
|
params.severities = severities;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acknowledged !== undefined) {
|
||||||
|
params.acknowledged = acknowledged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
params.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeFrom || timeTo) {
|
||||||
|
params.time_from = timeFrom;
|
||||||
|
params.time_till = timeTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request('problem.get', params).then(utils.mustArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTriggersByIds(triggerids: string[]) {
|
||||||
|
const params: any = {
|
||||||
|
output: 'extend',
|
||||||
|
triggerids: triggerids,
|
||||||
|
expandDescription: true,
|
||||||
|
expandData: true,
|
||||||
|
expandComment: true,
|
||||||
|
monitored: true,
|
||||||
|
skipDependent: true,
|
||||||
|
selectGroups: ['name'],
|
||||||
|
selectHosts: ['name', 'host', 'maintenance_status', 'proxy_hostid'],
|
||||||
|
selectItems: ['name', 'key_', 'lastvalue'],
|
||||||
|
// selectLastEvent: 'extend',
|
||||||
|
// selectTags: 'extend',
|
||||||
|
preservekeys: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.request('trigger.get', params).then(utils.mustArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTriggers(groupids, hostids, applicationids, options) {
|
||||||
|
const {showTriggers, maintenance, timeFrom, timeTo} = options;
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
groupids: groupids,
|
groupids: groupids,
|
||||||
hostids: hostids,
|
hostids: hostids,
|
||||||
@@ -369,8 +511,10 @@ export class ZabbixAPIConnector {
|
|||||||
selectTags: 'extend'
|
selectTags: 'extend'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showTriggers) {
|
if (showTriggers === ShowProblemTypes.Problems) {
|
||||||
params.filter.value = showTriggers;
|
params.filter.value = 1;
|
||||||
|
} else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) {
|
||||||
|
params.filter.value = [0, 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maintenance) {
|
if (maintenance) {
|
||||||
@@ -386,7 +530,7 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getEvents(objectids, timeFrom, timeTo, showEvents, limit) {
|
getEvents(objectids, timeFrom, timeTo, showEvents, limit) {
|
||||||
var params = {
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
time_from: timeFrom,
|
time_from: timeFrom,
|
||||||
time_till: timeTo,
|
time_till: timeTo,
|
||||||
@@ -402,27 +546,47 @@ export class ZabbixAPIConnector {
|
|||||||
params.sortorder = 'DESC';
|
params.sortorder = 'DESC';
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.request('event.get', params);
|
return this.request('event.get', params).then(utils.mustArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAcknowledges(eventids) {
|
getEventsHistory(groupids, hostids, applicationids, options) {
|
||||||
var params = {
|
const { timeFrom, timeTo, severities, limit, value } = options;
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
eventids: eventids,
|
time_from: timeFrom,
|
||||||
preservekeys: true,
|
time_till: timeTo,
|
||||||
|
value: '1',
|
||||||
|
source: '0',
|
||||||
|
object: '0',
|
||||||
|
evaltype: '0',
|
||||||
|
sortfield: ['eventid'],
|
||||||
|
sortorder: 'ASC',
|
||||||
select_acknowledges: 'extend',
|
select_acknowledges: 'extend',
|
||||||
sortfield: 'clock',
|
selectTags: 'extend',
|
||||||
sortorder: 'DESC'
|
selectSuppressionData: ['maintenanceid', 'suppress_until'],
|
||||||
|
groupids,
|
||||||
|
hostids,
|
||||||
|
applicationids,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.request('event.get', params)
|
if (limit) {
|
||||||
.then(events => {
|
params.limit = limit;
|
||||||
return _.filter(events, (event) => event.acknowledges.length);
|
}
|
||||||
});
|
|
||||||
|
if (severities) {
|
||||||
|
params.severities = severities;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
params.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request('event.get', params).then(utils.mustArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtendedEventData(eventids) {
|
getExtendedEventData(eventids) {
|
||||||
var params = {
|
const params = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
eventids: eventids,
|
eventids: eventids,
|
||||||
preservekeys: true,
|
preservekeys: true,
|
||||||
@@ -450,8 +614,24 @@ export class ZabbixAPIConnector {
|
|||||||
return this.request('alert.get', params);
|
return this.request('alert.get', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAcknowledges(eventids) {
|
||||||
|
const params = {
|
||||||
|
output: 'extend',
|
||||||
|
eventids: eventids,
|
||||||
|
preservekeys: true,
|
||||||
|
select_acknowledges: 'extend',
|
||||||
|
sortfield: 'clock',
|
||||||
|
sortorder: 'DESC'
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.request('event.get', params)
|
||||||
|
.then(events => {
|
||||||
|
return _.filter(events, (event) => event.acknowledges.length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getAlerts(itemids, timeFrom, timeTo) {
|
getAlerts(itemids, timeFrom, timeTo) {
|
||||||
var params = {
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
itemids: itemids,
|
itemids: itemids,
|
||||||
expandDescription: true,
|
expandDescription: true,
|
||||||
@@ -475,8 +655,8 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHostAlerts(hostids, applicationids, options) {
|
getHostAlerts(hostids, applicationids, options) {
|
||||||
let {minSeverity, acknowledged, count, timeFrom, timeTo} = options;
|
const {minSeverity, acknowledged, count, timeFrom, timeTo} = options;
|
||||||
let params = {
|
const params: any = {
|
||||||
output: 'extend',
|
output: 'extend',
|
||||||
hostids: hostids,
|
hostids: hostids,
|
||||||
min_severity: minSeverity,
|
min_severity: minSeverity,
|
||||||
@@ -517,7 +697,7 @@ export class ZabbixAPIConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getProxies() {
|
getProxies() {
|
||||||
var params = {
|
const params = {
|
||||||
output: ['proxyid', 'host'],
|
output: ['proxyid', 'host'],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -535,13 +715,17 @@ function filterTriggersByAcknowledge(triggers, acknowledged) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function isNotAuthorized(message) {
|
function isNotAuthorized(message) {
|
||||||
// return (
|
return (
|
||||||
// message === "Session terminated, re-login, please." ||
|
message === "Session terminated, re-login, please." ||
|
||||||
// message === "Not authorised." ||
|
message === "Not authorised." ||
|
||||||
// message === "Not authorized."
|
message === "Not authorized."
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
function isNotInitialized(message) {
|
||||||
|
return message === "Not initialized";
|
||||||
|
}
|
||||||
|
|
||||||
function getSLAInterval(intervalMs) {
|
function getSLAInterval(intervalMs) {
|
||||||
// Too many intervals may cause significant load on the database, so decrease number of resulting points
|
// Too many intervals may cause significant load on the database, so decrease number of resulting points
|
||||||
@@ -550,19 +734,18 @@ function getSLAInterval(intervalMs) {
|
|||||||
return Math.max(interval, MIN_SLA_INTERVAL);
|
return Math.max(interval, MIN_SLA_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSLAIntervals(timeRange, intervalMs) {
|
function buildSLAIntervals(timeRange, interval) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
let [timeFrom, timeTo] = timeRange;
|
||||||
const slaInterval = getSLAInterval(intervalMs);
|
|
||||||
const intervals = [];
|
const intervals = [];
|
||||||
|
|
||||||
// Align time range with calculated interval
|
// Align time range with calculated interval
|
||||||
timeFrom = Math.floor(timeFrom / slaInterval) * slaInterval;
|
timeFrom = Math.floor(timeFrom / interval) * interval;
|
||||||
timeTo = Math.ceil(timeTo / slaInterval) * slaInterval;
|
timeTo = Math.ceil(timeTo / interval) * interval;
|
||||||
|
|
||||||
for (let i = timeFrom; i <= timeTo - slaInterval; i += slaInterval) {
|
for (let i = timeFrom; i <= timeTo - interval; i += interval) {
|
||||||
intervals.push({
|
intervals.push({
|
||||||
from : i,
|
from : i,
|
||||||
to : (i + slaInterval)
|
to : (i + interval)
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* General Zabbix API methods
|
* General Zabbix API methods
|
||||||
*/
|
*/
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { JSONRPCRequest, ZabbixRequestResponse, JSONRPCError, APILoginResponse, GFHTTPRequest, GFRequestOptions } from './types';
|
||||||
|
|
||||||
export class ZabbixAPICore {
|
export class ZabbixAPICore {
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(backendSrv) {
|
|
||||||
this.backendSrv = backendSrv;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request data from Zabbix API
|
* Request data from Zabbix API
|
||||||
* @return {object} response.result
|
* @return {object} response.result
|
||||||
*/
|
*/
|
||||||
request(api_url, method, params, options, auth) {
|
request(api_url: string, method: string, params: any, options: GFRequestOptions, auth?: string) {
|
||||||
let requestData = {
|
const requestData: JSONRPCRequest = {
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
method: method,
|
method: method,
|
||||||
params: params,
|
params: params,
|
||||||
@@ -23,13 +19,13 @@ export class ZabbixAPICore {
|
|||||||
|
|
||||||
if (auth === "") {
|
if (auth === "") {
|
||||||
// Reject immediately if not authenticated
|
// Reject immediately if not authenticated
|
||||||
return Promise.reject(new ZabbixAPIError({data: "Not authorised."}));
|
return Promise.reject(new ZabbixAPIError({data: "Not initialized"}));
|
||||||
} else if (auth) {
|
} else if (auth) {
|
||||||
// Set auth parameter only if it needed
|
// Set auth parameter only if it needed
|
||||||
requestData.auth = auth;
|
requestData.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestOptions = {
|
const requestOptions: GFHTTPRequest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: api_url,
|
url: api_url,
|
||||||
data: requestData,
|
data: requestData,
|
||||||
@@ -50,18 +46,18 @@ export class ZabbixAPICore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
datasourceRequest(requestOptions) {
|
datasourceRequest(requestOptions) {
|
||||||
return this.backendSrv.datasourceRequest(requestOptions)
|
return getBackendSrv().datasourceRequest(requestOptions)
|
||||||
.then((response) => {
|
.then((response: ZabbixRequestResponse) => {
|
||||||
if (!response.data) {
|
if (!response?.data) {
|
||||||
return Promise.reject(new ZabbixAPIError({data: "General Error, no data"}));
|
return Promise.reject(new ZabbixAPIError({data: "General Error, no data"}));
|
||||||
} else if (response.data.error) {
|
} else if (response?.data.error) {
|
||||||
|
|
||||||
// Handle Zabbix API errors
|
// Handle Zabbix API errors
|
||||||
return Promise.reject(new ZabbixAPIError(response.data.error));
|
return Promise.reject(new ZabbixAPIError(response.data.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success
|
// Success
|
||||||
return response.data.result;
|
return response?.data.result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,8 +65,8 @@ export class ZabbixAPICore {
|
|||||||
* Get authentication token.
|
* Get authentication token.
|
||||||
* @return {string} auth token
|
* @return {string} auth token
|
||||||
*/
|
*/
|
||||||
login(api_url, username, password, options) {
|
login(api_url: string, username: string, password: string, options: GFRequestOptions): Promise<APILoginResponse> {
|
||||||
let params = {
|
const params = {
|
||||||
user: username,
|
user: username,
|
||||||
password: password
|
password: password
|
||||||
};
|
};
|
||||||
@@ -81,14 +77,22 @@ export class ZabbixAPICore {
|
|||||||
* Get Zabbix API version
|
* Get Zabbix API version
|
||||||
* Matches the version of Zabbix starting from Zabbix 2.0.4
|
* Matches the version of Zabbix starting from Zabbix 2.0.4
|
||||||
*/
|
*/
|
||||||
getVersion(api_url, options) {
|
getVersion(api_url: string, options: GFRequestOptions): Promise<string> {
|
||||||
return this.request(api_url, 'apiinfo.version', [], options);
|
return this.request(api_url, 'apiinfo.version', [], options).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define zabbix API exception type
|
// Define zabbix API exception type
|
||||||
export class ZabbixAPIError {
|
export class ZabbixAPIError {
|
||||||
constructor(error) {
|
code: number;
|
||||||
|
name: string;
|
||||||
|
data: string;
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
constructor(error: JSONRPCError) {
|
||||||
this.code = error.code || null;
|
this.code = error.code || null;
|
||||||
this.name = error.message || "";
|
this.name = error.message || "";
|
||||||
this.data = error.data || "";
|
this.data = error.data || "";
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export class CachingProxy {
|
export class CachingProxy {
|
||||||
|
cacheEnabled: boolean;
|
||||||
|
ttl: number;
|
||||||
|
cache: any;
|
||||||
|
promises: any;
|
||||||
|
|
||||||
constructor(cacheOptions) {
|
constructor(cacheOptions) {
|
||||||
this.cacheEnabled = cacheOptions.enabled;
|
this.cacheEnabled = cacheOptions.enabled;
|
||||||
@@ -33,13 +37,13 @@ export class CachingProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxyfyWithCache(func, funcName, funcScope) {
|
proxyfyWithCache(func, funcName, funcScope) {
|
||||||
let proxyfied = this.proxyfy(func, funcName, funcScope);
|
const proxyfied = this.proxyfy(func, funcName, funcScope);
|
||||||
return this.cacheRequest(proxyfied, funcName, funcScope);
|
return this.cacheRequest(proxyfied, funcName, funcScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isExpired(cacheObject) {
|
_isExpired(cacheObject) {
|
||||||
if (cacheObject) {
|
if (cacheObject) {
|
||||||
let object_age = Date.now() - cacheObject.timestamp;
|
const object_age = Date.now() - cacheObject.timestamp;
|
||||||
return !(cacheObject.timestamp && object_age < this.ttl);
|
return !(cacheObject.timestamp && object_age < this.ttl);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
@@ -52,8 +56,9 @@ export class CachingProxy {
|
|||||||
* with same params when waiting for result.
|
* with same params when waiting for result.
|
||||||
*/
|
*/
|
||||||
function callOnce(func, promiseKeeper, funcScope) {
|
function callOnce(func, promiseKeeper, funcScope) {
|
||||||
|
// tslint:disable-next-line: only-arrow-functions
|
||||||
return function() {
|
return function() {
|
||||||
var hash = getRequestHash(arguments);
|
const hash = getRequestHash(arguments);
|
||||||
if (!promiseKeeper[hash]) {
|
if (!promiseKeeper[hash]) {
|
||||||
promiseKeeper[hash] = Promise.resolve(
|
promiseKeeper[hash] = Promise.resolve(
|
||||||
func.apply(funcScope, arguments)
|
func.apply(funcScope, arguments)
|
||||||
@@ -68,22 +73,25 @@ function callOnce(func, promiseKeeper, funcScope) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cacheRequest(func, funcName, funcScope, self) {
|
function cacheRequest(func, funcName, funcScope, self) {
|
||||||
|
// tslint:disable-next-line: only-arrow-functions
|
||||||
return function() {
|
return function() {
|
||||||
if (!self.cache[funcName]) {
|
if (!self.cache[funcName]) {
|
||||||
self.cache[funcName] = {};
|
self.cache[funcName] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
let cacheObject = self.cache[funcName];
|
const cacheObject = self.cache[funcName];
|
||||||
let hash = getRequestHash(arguments);
|
const hash = getRequestHash(arguments);
|
||||||
if (self.cacheEnabled && !self._isExpired(cacheObject[hash])) {
|
if (self.cacheEnabled && !self._isExpired(cacheObject[hash])) {
|
||||||
return Promise.resolve(cacheObject[hash].value);
|
return Promise.resolve(cacheObject[hash].value);
|
||||||
} else {
|
} else {
|
||||||
return func.apply(funcScope, arguments)
|
return func.apply(funcScope, arguments)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
|
if (result !== undefined) {
|
||||||
cacheObject[hash] = {
|
cacheObject[hash] = {
|
||||||
value: result,
|
value: result,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -92,17 +100,17 @@ function cacheRequest(func, funcName, funcScope, self) {
|
|||||||
|
|
||||||
function getRequestHash(args) {
|
function getRequestHash(args) {
|
||||||
const argsJson = JSON.stringify(args);
|
const argsJson = JSON.stringify(args);
|
||||||
return argsJson.getHash();
|
return getHash(argsJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
String.prototype.getHash = function() {
|
function getHash(str: string): number {
|
||||||
var hash = 0, i, chr, len;
|
let hash = 0, i, chr, len;
|
||||||
if (this.length !== 0) {
|
if (str.length !== 0) {
|
||||||
for (i = 0, len = this.length; i < len; i++) {
|
for (i = 0, len = str.length; i < len; i++) {
|
||||||
chr = this.charCodeAt(i);
|
chr = str.charCodeAt(i);
|
||||||
hash = ((hash << 5) - hash) + chr;
|
hash = ((hash << 5) - hash) + chr;
|
||||||
hash |= 0; // Convert to 32bit integer
|
hash |= 0; // Convert to 32bit integer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
};
|
}
|
||||||
23
src/datasource-zabbix/zabbix/types.ts
Normal file
23
src/datasource-zabbix/zabbix/types.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface ZabbixConnector {
|
||||||
|
getHistory: (items, timeFrom, timeTill) => Promise<any>;
|
||||||
|
getTrend: (items, timeFrom, timeTill) => Promise<any>;
|
||||||
|
getItemsByIDs: (itemids) => Promise<any>;
|
||||||
|
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
|
||||||
|
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
|
||||||
|
getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
|
||||||
|
getAcknowledges: (eventids) => Promise<any>;
|
||||||
|
getITService: (serviceids?) => Promise<any>;
|
||||||
|
acknowledgeEvent: (eventid, message) => Promise<any>;
|
||||||
|
getProxies: () => Promise<any>;
|
||||||
|
getEventAlerts: (eventids) => Promise<any>;
|
||||||
|
getExtendedEventData: (eventids) => Promise<any>;
|
||||||
|
getMacros: (hostids: any[]) => Promise<any>;
|
||||||
|
getVersion: () => Promise<string>;
|
||||||
|
login: () => Promise<any>;
|
||||||
|
|
||||||
|
getGroups: (groupFilter?) => any;
|
||||||
|
getHosts: (groupFilter?, hostFilter?) => any;
|
||||||
|
getApps: (groupFilter?, hostFilter?, appFilter?) => any;
|
||||||
|
getItems: (groupFilter?, hostFilter?, appFilter?, itemFilter?, options?) => any;
|
||||||
|
getSLA: (itservices, timeRange, target, options?) => any;
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import mocks from '../../test-setup/mocks';
|
|
||||||
import { Zabbix } from './zabbix';
|
import { Zabbix } from './zabbix';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
getBackendSrv: () => ({
|
||||||
|
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
|
||||||
|
}),
|
||||||
|
}), {virtual: true});
|
||||||
|
|
||||||
describe('Zabbix', () => {
|
describe('Zabbix', () => {
|
||||||
let ctx = {};
|
let ctx = {};
|
||||||
let zabbix;
|
let zabbix;
|
||||||
@@ -8,14 +13,13 @@ describe('Zabbix', () => {
|
|||||||
url: 'http://localhost',
|
url: 'http://localhost',
|
||||||
username: 'zabbix',
|
username: 'zabbix',
|
||||||
password: 'zabbix',
|
password: 'zabbix',
|
||||||
zabbixVersion: 4,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.options = options;
|
ctx.options = options;
|
||||||
ctx.backendSrv = mocks.backendSrvMock;
|
// ctx.backendSrv = mocks.backendSrvMock;
|
||||||
ctx.datasourceSrv = mocks.datasourceSrvMock;
|
// ctx.datasourceSrv = mocks.datasourceSrvMock;
|
||||||
zabbix = new Zabbix(ctx.options, ctx.backendSrvMock, ctx.datasourceSrvMock);
|
zabbix = new Zabbix(ctx.options);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When querying proxies', () => {
|
describe('When querying proxies', () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import * as utils from '../utils';
|
import * as utils from '../utils';
|
||||||
import responseHandler from '../responseHandler';
|
import responseHandler from '../responseHandler';
|
||||||
import { CachingProxy } from './proxy/cachingProxy';
|
import { CachingProxy } from './proxy/cachingProxy';
|
||||||
@@ -7,11 +8,19 @@ import { DBConnector } from './connectors/dbConnector';
|
|||||||
import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector';
|
import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector';
|
||||||
import { SQLConnector } from './connectors/sql/sqlConnector';
|
import { SQLConnector } from './connectors/sql/sqlConnector';
|
||||||
import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector';
|
import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector';
|
||||||
|
import { ZabbixConnector } from './types';
|
||||||
|
import { joinTriggersWithProblems, joinTriggersWithEvents } from '../problemsHandler';
|
||||||
|
import { ProblemDTO } from '../types';
|
||||||
|
|
||||||
|
interface AppsResponse extends Array<any> {
|
||||||
|
appFilterEmpty?: boolean;
|
||||||
|
hostids?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
const REQUESTS_TO_PROXYFY = [
|
const REQUESTS_TO_PROXYFY = [
|
||||||
'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs',
|
'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs',
|
||||||
'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies',
|
'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies',
|
||||||
'getEventAlerts', 'getExtendedEventData'
|
'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds'
|
||||||
];
|
];
|
||||||
|
|
||||||
const REQUESTS_TO_CACHE = [
|
const REQUESTS_TO_CACHE = [
|
||||||
@@ -24,40 +33,63 @@ const REQUESTS_TO_BIND = [
|
|||||||
'getExtendedEventData'
|
'getExtendedEventData'
|
||||||
];
|
];
|
||||||
|
|
||||||
export class Zabbix {
|
export class Zabbix implements ZabbixConnector {
|
||||||
constructor(options, datasourceSrv, backendSrv, datasourceId) {
|
enableDirectDBConnection: boolean;
|
||||||
let {
|
cachingProxy: CachingProxy;
|
||||||
|
zabbixAPI: ZabbixAPIConnector;
|
||||||
|
getHistoryDB: any;
|
||||||
|
dbConnector: any;
|
||||||
|
getTrendsDB: any;
|
||||||
|
|
||||||
|
getHistory: (items, timeFrom, timeTill) => Promise<any>;
|
||||||
|
getTrend: (items, timeFrom, timeTill) => Promise<any>;
|
||||||
|
getItemsByIDs: (itemids) => Promise<any>;
|
||||||
|
getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise<any>;
|
||||||
|
getAlerts: (itemids, timeFrom?, timeTo?) => Promise<any>;
|
||||||
|
getHostAlerts: (hostids, applicationids, options?) => Promise<any>;
|
||||||
|
getAcknowledges: (eventids) => Promise<any>;
|
||||||
|
getITService: (serviceids?) => Promise<any>;
|
||||||
|
acknowledgeEvent: (eventid, message) => Promise<any>;
|
||||||
|
getProxies: () => Promise<any>;
|
||||||
|
getEventAlerts: (eventids) => Promise<any>;
|
||||||
|
getExtendedEventData: (eventids) => Promise<any>;
|
||||||
|
getMacros: (hostids: any[]) => Promise<any>;
|
||||||
|
getVersion: () => Promise<string>;
|
||||||
|
login: () => Promise<any>;
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
const {
|
||||||
url,
|
url,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
basicAuth,
|
basicAuth,
|
||||||
withCredentials,
|
withCredentials,
|
||||||
zabbixVersion,
|
|
||||||
cacheTTL,
|
cacheTTL,
|
||||||
enableDirectDBConnection,
|
enableDirectDBConnection,
|
||||||
dbConnectionDatasourceId,
|
dbConnectionDatasourceId,
|
||||||
dbConnectionDatasourceName,
|
dbConnectionDatasourceName,
|
||||||
dbConnectionRetentionPolicy,
|
dbConnectionRetentionPolicy,
|
||||||
|
datasourceId,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
this.enableDirectDBConnection = enableDirectDBConnection;
|
this.enableDirectDBConnection = enableDirectDBConnection;
|
||||||
|
|
||||||
// Initialize caching proxy for requests
|
// Initialize caching proxy for requests
|
||||||
let cacheOptions = {
|
const cacheOptions = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
ttl: cacheTTL
|
ttl: cacheTTL
|
||||||
};
|
};
|
||||||
this.cachingProxy = new CachingProxy(cacheOptions);
|
this.cachingProxy = new CachingProxy(cacheOptions);
|
||||||
|
|
||||||
this.zabbixAPI = new ZabbixAPIConnector(url, username, password, zabbixVersion, basicAuth, withCredentials, backendSrv, datasourceId);
|
this.zabbixAPI = new ZabbixAPIConnector(url, username, password, basicAuth, withCredentials, datasourceId);
|
||||||
|
|
||||||
this.proxyfyRequests();
|
this.proxyfyRequests();
|
||||||
this.cacheRequests();
|
this.cacheRequests();
|
||||||
this.bindRequests();
|
this.bindRequests();
|
||||||
|
|
||||||
if (enableDirectDBConnection) {
|
if (enableDirectDBConnection) {
|
||||||
const connectorOptions = { dbConnectionRetentionPolicy };
|
const connectorOptions: any = { dbConnectionRetentionPolicy };
|
||||||
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv, connectorOptions)
|
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, connectorOptions)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector);
|
this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector);
|
||||||
this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector);
|
this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector);
|
||||||
@@ -65,34 +97,34 @@ export class Zabbix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initDBConnector(datasourceId, datasourceName, datasourceSrv, options) {
|
initDBConnector(datasourceId, datasourceName, options) {
|
||||||
return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv)
|
return DBConnector.loadDatasource(datasourceId, datasourceName)
|
||||||
.then(ds => {
|
.then(ds => {
|
||||||
let connectorOptions = { datasourceId, datasourceName };
|
const connectorOptions: any = { datasourceId, datasourceName };
|
||||||
if (ds.type === 'influxdb') {
|
if (ds.type === 'influxdb') {
|
||||||
connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
|
connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
|
||||||
this.dbConnector = new InfluxDBConnector(connectorOptions, datasourceSrv);
|
this.dbConnector = new InfluxDBConnector(connectorOptions);
|
||||||
} else {
|
} else {
|
||||||
this.dbConnector = new SQLConnector(connectorOptions, datasourceSrv);
|
this.dbConnector = new SQLConnector(connectorOptions);
|
||||||
}
|
}
|
||||||
return this.dbConnector;
|
return this.dbConnector;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyfyRequests() {
|
proxyfyRequests() {
|
||||||
for (let request of REQUESTS_TO_PROXYFY) {
|
for (const request of REQUESTS_TO_PROXYFY) {
|
||||||
this.zabbixAPI[request] = this.cachingProxy.proxyfy(this.zabbixAPI[request], request, this.zabbixAPI);
|
this.zabbixAPI[request] = this.cachingProxy.proxyfy(this.zabbixAPI[request], request, this.zabbixAPI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheRequests() {
|
cacheRequests() {
|
||||||
for (let request of REQUESTS_TO_CACHE) {
|
for (const request of REQUESTS_TO_CACHE) {
|
||||||
this.zabbixAPI[request] = this.cachingProxy.cacheRequest(this.zabbixAPI[request], request, this.zabbixAPI);
|
this.zabbixAPI[request] = this.cachingProxy.cacheRequest(this.zabbixAPI[request], request, this.zabbixAPI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bindRequests() {
|
bindRequests() {
|
||||||
for (let request of REQUESTS_TO_BIND) {
|
for (const request of REQUESTS_TO_BIND) {
|
||||||
this[request] = this.zabbixAPI[request].bind(this.zabbixAPI);
|
this[request] = this.zabbixAPI[request].bind(this.zabbixAPI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,14 +133,14 @@ export class Zabbix {
|
|||||||
* Perform test query for Zabbix API and external history DB.
|
* Perform test query for Zabbix API and external history DB.
|
||||||
* @return {object} test result object:
|
* @return {object} test result object:
|
||||||
* ```
|
* ```
|
||||||
{
|
* {
|
||||||
zabbixVersion,
|
* zabbixVersion,
|
||||||
dbConnectorStatus: {
|
* dbConnectorStatus: {
|
||||||
dsType,
|
* dsType,
|
||||||
dsName
|
* dsName
|
||||||
}
|
* }
|
||||||
}
|
* }
|
||||||
```
|
* ```
|
||||||
*/
|
*/
|
||||||
// testDataSource() {
|
// testDataSource() {
|
||||||
// let zabbixVersion;
|
// let zabbixVersion;
|
||||||
@@ -143,19 +175,20 @@ export class Zabbix {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
getItemsFromTarget(target, options) {
|
getItemsFromTarget(target, options) {
|
||||||
let parts = ['group', 'host', 'application', 'item'];
|
const parts = ['group', 'host', 'application', 'item'];
|
||||||
let filters = _.map(parts, p => target[p].filter);
|
const filters = _.map(parts, p => target[p].filter);
|
||||||
return this.getItems(...filters, options);
|
return this.getItems(...filters, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHostsFromTarget(target) {
|
getHostsFromTarget(target) {
|
||||||
let parts = ['group', 'host', 'application'];
|
const parts = ['group', 'host', 'application'];
|
||||||
let filters = _.map(parts, p => target[p].filter);
|
const filters = _.map(parts, p => target[p].filter);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.getHosts(...filters),
|
this.getHosts(...filters),
|
||||||
this.getApps(...filters),
|
this.getApps(...filters),
|
||||||
]).then((results) => {
|
]).then(results => {
|
||||||
let [hosts, apps] = results;
|
const hosts = results[0];
|
||||||
|
let apps: AppsResponse = results[1];
|
||||||
if (apps.appFilterEmpty) {
|
if (apps.appFilterEmpty) {
|
||||||
apps = [];
|
apps = [];
|
||||||
}
|
}
|
||||||
@@ -178,12 +211,12 @@ export class Zabbix {
|
|||||||
getAllHosts(groupFilter) {
|
getAllHosts(groupFilter) {
|
||||||
return this.getGroups(groupFilter)
|
return this.getGroups(groupFilter)
|
||||||
.then(groups => {
|
.then(groups => {
|
||||||
let groupids = _.map(groups, 'groupid');
|
const groupids = _.map(groups, 'groupid');
|
||||||
return this.zabbixAPI.getHosts(groupids);
|
return this.zabbixAPI.getHosts(groupids);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHosts(groupFilter, hostFilter) {
|
getHosts(groupFilter?, hostFilter?) {
|
||||||
return this.getAllHosts(groupFilter)
|
return this.getAllHosts(groupFilter)
|
||||||
.then(hosts => findByFilter(hosts, hostFilter));
|
.then(hosts => findByFilter(hosts, hostFilter));
|
||||||
}
|
}
|
||||||
@@ -194,34 +227,34 @@ export class Zabbix {
|
|||||||
getAllApps(groupFilter, hostFilter) {
|
getAllApps(groupFilter, hostFilter) {
|
||||||
return this.getHosts(groupFilter, hostFilter)
|
return this.getHosts(groupFilter, hostFilter)
|
||||||
.then(hosts => {
|
.then(hosts => {
|
||||||
let hostids = _.map(hosts, 'hostid');
|
const hostids = _.map(hosts, 'hostid');
|
||||||
return this.zabbixAPI.getApps(hostids);
|
return this.zabbixAPI.getApps(hostids);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getApps(groupFilter, hostFilter, appFilter) {
|
getApps(groupFilter?, hostFilter?, appFilter?): Promise<AppsResponse> {
|
||||||
return this.getHosts(groupFilter, hostFilter)
|
return this.getHosts(groupFilter, hostFilter)
|
||||||
.then(hosts => {
|
.then(hosts => {
|
||||||
let hostids = _.map(hosts, 'hostid');
|
const hostids = _.map(hosts, 'hostid');
|
||||||
if (appFilter) {
|
if (appFilter) {
|
||||||
return this.zabbixAPI.getApps(hostids)
|
return this.zabbixAPI.getApps(hostids)
|
||||||
.then(apps => filterByQuery(apps, appFilter));
|
.then(apps => filterByQuery(apps, appFilter));
|
||||||
} else {
|
} else {
|
||||||
return {
|
const appsResponse: AppsResponse = hostids;
|
||||||
appFilterEmpty: true,
|
appsResponse.hostids = hostids;
|
||||||
hostids: hostids
|
appsResponse.appFilterEmpty = true;
|
||||||
};
|
return Promise.resolve(appsResponse);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllItems(groupFilter, hostFilter, appFilter, options = {}) {
|
getAllItems(groupFilter, hostFilter, appFilter, options: any = {}) {
|
||||||
return this.getApps(groupFilter, hostFilter, appFilter)
|
return this.getApps(groupFilter, hostFilter, appFilter)
|
||||||
.then(apps => {
|
.then(apps => {
|
||||||
if (apps.appFilterEmpty) {
|
if (apps.appFilterEmpty) {
|
||||||
return this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
|
return this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
|
||||||
} else {
|
} else {
|
||||||
let appids = _.map(apps, 'applicationid');
|
const appids = _.map(apps, 'applicationid');
|
||||||
return this.zabbixAPI.getItems(undefined, appids, options.itemtype);
|
return this.zabbixAPI.getItems(undefined, appids, options.itemtype);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -235,34 +268,54 @@ export class Zabbix {
|
|||||||
.then(this.expandUserMacro.bind(this));
|
.then(this.expandUserMacro.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
expandUserMacro(items) {
|
expandUserMacro(items, isTriggerItem) {
|
||||||
let hostids = getHostIds(items);
|
const hostids = getHostIds(items);
|
||||||
return this.getMacros(hostids)
|
return this.getMacros(hostids)
|
||||||
.then(macros => {
|
.then(macros => {
|
||||||
_.forEach(items, item => {
|
_.forEach(items, item => {
|
||||||
if (utils.containsMacro(item.name)) {
|
if (utils.containsMacro(isTriggerItem ? item.url : item.name)) {
|
||||||
|
if (isTriggerItem) {
|
||||||
|
item.url = utils.replaceMacro(item, macros, isTriggerItem);
|
||||||
|
} else {
|
||||||
item.name = utils.replaceMacro(item, macros);
|
item.name = utils.replaceMacro(item, macros);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getItems(groupFilter, hostFilter, appFilter, itemFilter, options = {}) {
|
getItems(groupFilter?, hostFilter?, appFilter?, itemFilter?, options = {}) {
|
||||||
return this.getAllItems(groupFilter, hostFilter, appFilter, options)
|
return this.getAllItems(groupFilter, hostFilter, appFilter, options)
|
||||||
.then(items => filterByQuery(items, itemFilter));
|
.then(items => filterByQuery(items, itemFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItemValues(groupFilter?, hostFilter?, appFilter?, itemFilter?, options: any = {}) {
|
||||||
|
return this.getItems(groupFilter, hostFilter, appFilter, itemFilter, options).then(items => {
|
||||||
|
let timeRange = [moment().subtract(2, 'h').unix(), moment().unix()];
|
||||||
|
if (options.range) {
|
||||||
|
timeRange = [options.range.from.unix(), options.range.to.unix()];
|
||||||
|
}
|
||||||
|
const [timeFrom, timeTo] = timeRange;
|
||||||
|
|
||||||
|
return this.zabbixAPI.getHistory(items, timeFrom, timeTo).then(history => {
|
||||||
|
if (history) {
|
||||||
|
const values = _.uniq(history.map(v => v.value));
|
||||||
|
return values.map(value => ({ name: value }));
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getITServices(itServiceFilter) {
|
getITServices(itServiceFilter) {
|
||||||
return this.zabbixAPI.getITService()
|
return this.zabbixAPI.getITService()
|
||||||
.then(itServices => findByFilter(itServices, itServiceFilter));
|
.then(itServices => findByFilter(itServices, itServiceFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?) {
|
||||||
* Build query - convert target filters to array of Zabbix items
|
const promises = [
|
||||||
*/
|
|
||||||
getTriggers(groupFilter, hostFilter, appFilter, options, proxyFilter) {
|
|
||||||
let promises = [
|
|
||||||
this.getGroups(groupFilter),
|
this.getGroups(groupFilter),
|
||||||
this.getHosts(groupFilter, hostFilter),
|
this.getHosts(groupFilter, hostFilter),
|
||||||
this.getApps(groupFilter, hostFilter, appFilter)
|
this.getApps(groupFilter, hostFilter, appFilter)
|
||||||
@@ -270,8 +323,8 @@ export class Zabbix {
|
|||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
.then(results => {
|
.then(results => {
|
||||||
let [filteredGroups, filteredHosts, filteredApps] = results;
|
const [filteredGroups, filteredHosts, filteredApps] = results;
|
||||||
let query = {};
|
const query: any = {};
|
||||||
|
|
||||||
if (appFilter) {
|
if (appFilter) {
|
||||||
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
|
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
|
||||||
@@ -285,8 +338,53 @@ export class Zabbix {
|
|||||||
|
|
||||||
return query;
|
return query;
|
||||||
})
|
})
|
||||||
.then(query => this.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options))
|
.then(query => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options))
|
||||||
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter));
|
.then(problems => {
|
||||||
|
const triggerids = problems?.map(problem => problem.objectid);
|
||||||
|
return Promise.all([
|
||||||
|
Promise.resolve(problems),
|
||||||
|
this.zabbixAPI.getTriggersByIds(triggerids)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(([problems, triggers]) => joinTriggersWithProblems(problems, triggers))
|
||||||
|
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter))
|
||||||
|
.then(triggers => this.expandUserMacro.bind(this)(triggers, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter?, options?): Promise<ProblemDTO[]> {
|
||||||
|
const { valueFromEvent } = options;
|
||||||
|
|
||||||
|
const promises = [
|
||||||
|
this.getGroups(groupFilter),
|
||||||
|
this.getHosts(groupFilter, hostFilter),
|
||||||
|
this.getApps(groupFilter, hostFilter, appFilter)
|
||||||
|
];
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
.then(results => {
|
||||||
|
const [filteredGroups, filteredHosts, filteredApps] = results;
|
||||||
|
const query: any = {};
|
||||||
|
|
||||||
|
if (appFilter) {
|
||||||
|
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
|
||||||
|
}
|
||||||
|
if (hostFilter) {
|
||||||
|
query.hostids = _.map(filteredHosts, 'hostid');
|
||||||
|
}
|
||||||
|
if (groupFilter) {
|
||||||
|
query.groupids = _.map(filteredGroups, 'groupid');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
})
|
||||||
|
.then(query => this.zabbixAPI.getEventsHistory(query.groupids, query.hostids, query.applicationids, options))
|
||||||
|
.then(problems => {
|
||||||
|
const triggerids = problems?.map(problem => problem.objectid);
|
||||||
|
return Promise.all([Promise.resolve(problems), this.zabbixAPI.getTriggersByIds(triggerids)]);
|
||||||
|
})
|
||||||
|
.then(([problems, triggers]) => joinTriggersWithEvents(problems, triggers, { valueFromEvent }))
|
||||||
|
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter))
|
||||||
|
.then(triggers => this.expandUserMacro.bind(this)(triggers, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
filterTriggersByProxy(triggers, proxyFilter) {
|
filterTriggersByProxy(triggers, proxyFilter) {
|
||||||
@@ -295,14 +393,13 @@ export class Zabbix {
|
|||||||
if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
|
if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
|
||||||
const proxy_ids = proxies.map(proxy => proxy.proxyid);
|
const proxy_ids = proxies.map(proxy => proxy.proxyid);
|
||||||
triggers = triggers.filter(trigger => {
|
triggers = triggers.filter(trigger => {
|
||||||
let filtered = false;
|
for (let i = 0; i < trigger.hosts.length; i++) {
|
||||||
for(let i = 0; i < trigger.hosts.length; i++) {
|
|
||||||
const host = trigger.hosts[i];
|
const host = trigger.hosts[i];
|
||||||
if (proxy_ids.includes(host.proxy_hostid)) {
|
if (proxy_ids.includes(host.proxy_hostid)) {
|
||||||
filtered = true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filtered;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return triggers;
|
return triggers;
|
||||||
@@ -318,7 +415,7 @@ export class Zabbix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHistoryTS(items, timeRange, options) {
|
getHistoryTS(items, timeRange, options) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
if (this.enableDirectDBConnection) {
|
if (this.enableDirectDBConnection) {
|
||||||
return this.getHistoryDB(items, timeFrom, timeTo, options)
|
return this.getHistoryDB(items, timeFrom, timeTo, options)
|
||||||
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
|
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
|
||||||
@@ -329,12 +426,12 @@ export class Zabbix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTrends(items, timeRange, options) {
|
getTrends(items, timeRange, options) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
if (this.enableDirectDBConnection) {
|
if (this.enableDirectDBConnection) {
|
||||||
return this.getTrendsDB(items, timeFrom, timeTo, options)
|
return this.getTrendsDB(items, timeFrom, timeTo, options)
|
||||||
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
|
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
|
||||||
} else {
|
} else {
|
||||||
let valueType = options.consolidateBy || options.valueType;
|
const valueType = options.consolidateBy || options.valueType;
|
||||||
return this.zabbixAPI.getTrend(items, timeFrom, timeTo)
|
return this.zabbixAPI.getTrend(items, timeFrom, timeTo)
|
||||||
.then(history => responseHandler.handleTrends(history, items, valueType))
|
.then(history => responseHandler.handleTrends(history, items, valueType))
|
||||||
.then(responseHandler.sortTimeseries); // Sort trend data, issue #202
|
.then(responseHandler.sortTimeseries); // Sort trend data, issue #202
|
||||||
@@ -342,7 +439,7 @@ export class Zabbix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHistoryText(items, timeRange, target) {
|
getHistoryText(items, timeRange, target) {
|
||||||
let [timeFrom, timeTo] = timeRange;
|
const [timeFrom, timeTo] = timeRange;
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
return this.zabbixAPI.getHistory(items, timeFrom, timeTo)
|
return this.zabbixAPI.getHistory(items, timeFrom, timeTo)
|
||||||
.then(history => {
|
.then(history => {
|
||||||
@@ -358,15 +455,11 @@ export class Zabbix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSLA(itservices, timeRange, target, options) {
|
getSLA(itservices, timeRange, target, options) {
|
||||||
let itServices = itservices;
|
const itServiceIds = _.map(itservices, 'serviceid');
|
||||||
if (options.isOldVersion) {
|
|
||||||
itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
|
|
||||||
}
|
|
||||||
let itServiceIds = _.map(itServices, 'serviceid');
|
|
||||||
return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
|
return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
|
||||||
.then(slaResponse => {
|
.then(slaResponse => {
|
||||||
return _.map(itServiceIds, serviceid => {
|
return _.map(itServiceIds, serviceid => {
|
||||||
let itservice = _.find(itServices, {'serviceid': serviceid});
|
const itservice = _.find(itservices, {'serviceid': serviceid});
|
||||||
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
|
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -382,7 +475,7 @@ export class Zabbix {
|
|||||||
* @return array with finded element or empty array
|
* @return array with finded element or empty array
|
||||||
*/
|
*/
|
||||||
function findByName(list, name) {
|
function findByName(list, name) {
|
||||||
var finded = _.find(list, {'name': name});
|
const finded = _.find(list, {'name': name});
|
||||||
if (finded) {
|
if (finded) {
|
||||||
return [finded];
|
return [finded];
|
||||||
} else {
|
} else {
|
||||||
@@ -399,7 +492,7 @@ function findByName(list, name) {
|
|||||||
* @return {[type]} array with finded element or empty array
|
* @return {[type]} array with finded element or empty array
|
||||||
*/
|
*/
|
||||||
function filterByName(list, name) {
|
function filterByName(list, name) {
|
||||||
var finded = _.filter(list, {'name': name});
|
const finded = _.filter(list, {'name': name});
|
||||||
if (finded) {
|
if (finded) {
|
||||||
return finded;
|
return finded;
|
||||||
} else {
|
} else {
|
||||||
@@ -408,8 +501,8 @@ function filterByName(list, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filterByRegex(list, regex) {
|
function filterByRegex(list, regex) {
|
||||||
var filterPattern = utils.buildRegex(regex);
|
const filterPattern = utils.buildRegex(regex);
|
||||||
return _.filter(list, function (zbx_obj) {
|
return _.filter(list, (zbx_obj) => {
|
||||||
return filterPattern.test(zbx_obj.name);
|
return filterPattern.test(zbx_obj.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -431,7 +524,7 @@ function filterByQuery(list, filter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getHostIds(items) {
|
function getHostIds(items) {
|
||||||
let hostIds = _.map(items, item => {
|
const hostIds = _.map(items, item => {
|
||||||
return _.map(item.hosts, 'hostid');
|
return _.map(item.hosts, 'hostid');
|
||||||
});
|
});
|
||||||
return _.uniq(_.flatten(hostIds));
|
return _.uniq(_.flatten(hostIds));
|
||||||
@@ -16,8 +16,11 @@ export class ZabbixAlertingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPanelAlertState(panelId, alertState) {
|
setPanelAlertState(panelId, alertState) {
|
||||||
let panelIndex;
|
if (!alertState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let panelIndex;
|
||||||
let panelContainers = _.filter($('.panel-container'), elem => {
|
let panelContainers = _.filter($('.panel-container'), elem => {
|
||||||
return elem.clientHeight && elem.clientWidth;
|
return elem.clientHeight && elem.clientWidth;
|
||||||
});
|
});
|
||||||
@@ -32,8 +35,7 @@ export class ZabbixAlertingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't apply alert styles to .panel-container--absolute (it rewrites position from absolute to relative)
|
if (panelIndex >= 0) {
|
||||||
if (panelIndex >= 0 && !panelContainers[panelIndex].className.includes('panel-container--absolute')) {
|
|
||||||
let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
|
let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
|
||||||
$(panelContainers[panelIndex]).removeClass(alertClass);
|
$(panelContainers[panelIndex]).removeClass(alertClass);
|
||||||
|
|
||||||
|
|||||||
1
src/img/icn-zabbix-app.svg
Normal file
1
src/img/icn-zabbix-app.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
@@ -1,107 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
version="1.0"
|
|
||||||
id="Layer_1"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
width="100px"
|
|
||||||
height="100px"
|
|
||||||
viewBox="692 0 100 100"
|
|
||||||
style="enable-background:new 692 0 100 100;"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="0.91 r"
|
|
||||||
sodipodi:docname="zabbix_app_logo.svg"
|
|
||||||
enable-background="new"><metadata
|
|
||||||
id="metadata13"><rdf:RDF><cc:Work
|
|
||||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
|
||||||
id="defs11"><linearGradient
|
|
||||||
id="SVGID_1_"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="2.6005001"
|
|
||||||
y1="65.475197"
|
|
||||||
x2="94.377701"
|
|
||||||
y2="30.245199"><stop
|
|
||||||
id="stop34"
|
|
||||||
style="stop-color:#58595B"
|
|
||||||
offset="0.2583" /><stop
|
|
||||||
id="stop32"
|
|
||||||
style="stop-color:#646C70"
|
|
||||||
offset="0.2917" /><stop
|
|
||||||
id="stop30"
|
|
||||||
style="stop-color:#6C8087"
|
|
||||||
offset="0.3398" /><stop
|
|
||||||
id="stop28"
|
|
||||||
style="stop-color:#6D8F9B"
|
|
||||||
offset="0.3927" /><stop
|
|
||||||
id="stop26"
|
|
||||||
style="stop-color:#689BAA"
|
|
||||||
offset="0.4499" /><stop
|
|
||||||
id="stop24"
|
|
||||||
style="stop-color:#5FA3B5"
|
|
||||||
offset="0.5128" /><stop
|
|
||||||
id="stop22"
|
|
||||||
style="stop-color:#53A8BD"
|
|
||||||
offset="0.5837" /><stop
|
|
||||||
id="stop20"
|
|
||||||
style="stop-color:#47ABC2"
|
|
||||||
offset="0.6674" /><stop
|
|
||||||
id="stop18"
|
|
||||||
style="stop-color:#3FAEC5"
|
|
||||||
offset="0.7759" /><stop
|
|
||||||
id="stop16"
|
|
||||||
style="stop-color:#3CAFC7"
|
|
||||||
offset="1" /><stop
|
|
||||||
id="stop14"
|
|
||||||
style="stop-color:#3BB0C9"
|
|
||||||
offset="1" /></linearGradient></defs><sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1615"
|
|
||||||
inkscape:window-height="1026"
|
|
||||||
id="namedview9"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="4.285"
|
|
||||||
inkscape:cx="50.424685"
|
|
||||||
inkscape:cy="23.581186"
|
|
||||||
inkscape:window-x="65"
|
|
||||||
inkscape:window-y="24"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="g5194" /><style
|
|
||||||
type="text/css"
|
|
||||||
id="style3">
|
|
||||||
.st0{fill:#787878;}
|
|
||||||
</style><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="g5194"
|
|
||||||
inkscape:label="Zabbix BG Original"
|
|
||||||
style="display:inline"><rect
|
|
||||||
style="fill:#d40000;fill-opacity:1"
|
|
||||||
id="rect5196"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
x="692"
|
|
||||||
y="0" /></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6"
|
|
||||||
inkscape:label="Zabbix Original Z"
|
|
||||||
style="display:inline"><path
|
|
||||||
d="m 715.54426,16.689227 52.91147,0 0,6.87033 -42.58255,52.167008 43.62047,0 0,7.584207 -54.9873,0 0,-6.871516 42.58255,-52.166552 -41.54464,0 0,-7.583477 z"
|
|
||||||
style="display:inline;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
|
||||||
id="path4169-6"
|
|
||||||
inkscape:connector-curvature="0" /></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import './sass/grafana-zabbix.dark.scss';
|
import './sass/grafana-zabbix.dark.scss';
|
||||||
import './sass/grafana-zabbix.light.scss';
|
import './sass/grafana-zabbix.light.scss';
|
||||||
|
|
||||||
import {ZabbixAppConfigCtrl} from './components/config';
|
import {ZabbixAppConfigCtrl} from './app_config_ctrl/config';
|
||||||
import {loadPluginCss} from 'grafana/app/plugins/sdk';
|
import {loadPluginCss} from 'grafana/app/plugins/sdk';
|
||||||
|
|
||||||
loadPluginCss({
|
loadPluginCss({
|
||||||
|
|||||||
277
src/panel-triggers/components/AckModal.tsx
Normal file
277
src/panel-triggers/components/AckModal.tsx
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { cx, css } from 'emotion';
|
||||||
|
import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants';
|
||||||
|
import { Button, VerticalGroup, Spinner, Modal, Input, Forms, stylesFactory, withTheme, Themeable } from '@grafana/ui';
|
||||||
|
import { FAIcon } from '../../components';
|
||||||
|
|
||||||
|
import * as grafanaUi from '@grafana/ui';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
const Checkbox: any = Forms?.Checkbox || (grafanaUi as any).Checkbox;
|
||||||
|
const RadioButtonGroup: any = Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup;
|
||||||
|
|
||||||
|
const KEYBOARD_ENTER_KEY = 13;
|
||||||
|
const KEYBOARD_ESCAPE_KEY = 27;
|
||||||
|
|
||||||
|
interface Props extends Themeable {
|
||||||
|
canAck?: boolean;
|
||||||
|
canClose?: boolean;
|
||||||
|
severity?: number;
|
||||||
|
withBackdrop?: boolean;
|
||||||
|
onSubmit: (data?: AckProblemData) => Promise<any> | any;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
value: string;
|
||||||
|
error: boolean;
|
||||||
|
errorMessage: string;
|
||||||
|
ackError: string;
|
||||||
|
acknowledge: boolean;
|
||||||
|
closeProblem: boolean;
|
||||||
|
changeSeverity: boolean;
|
||||||
|
selectedSeverity: number;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AckProblemData {
|
||||||
|
message: string;
|
||||||
|
closeProblem?: boolean;
|
||||||
|
action?: number;
|
||||||
|
severity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const severityOptions = [
|
||||||
|
{value: 0, label: 'Not classified'},
|
||||||
|
{value: 1, label: 'Information'},
|
||||||
|
{value: 2, label: 'Warning'},
|
||||||
|
{value: 3, label: 'Average'},
|
||||||
|
{value: 4, label: 'High'},
|
||||||
|
{value: 5, label: 'Disaster'}
|
||||||
|
];
|
||||||
|
|
||||||
|
export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||||
|
static defaultProps: Partial<Props> = {
|
||||||
|
withBackdrop: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
ackError: '',
|
||||||
|
acknowledge: false,
|
||||||
|
closeProblem: false,
|
||||||
|
changeSeverity: false,
|
||||||
|
selectedSeverity: props.severity || 0,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ value: event.target.value, error: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') {
|
||||||
|
this.submit();
|
||||||
|
} else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') {
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBackdropClick = () => {
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAcknowledgeToggle = () => {
|
||||||
|
this.setState({ acknowledge: !this.state.acknowledge, error: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeSeverityToggle = () => {
|
||||||
|
this.setState({ changeSeverity: !this.state.changeSeverity, error: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseProblemToggle = () => {
|
||||||
|
this.setState({ closeProblem: !this.state.closeProblem, error: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeSelectedSeverity = v => {
|
||||||
|
this.setState({ selectedSeverity: v });
|
||||||
|
};
|
||||||
|
|
||||||
|
dismiss = () => {
|
||||||
|
this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false });
|
||||||
|
this.props.onDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = () => {
|
||||||
|
const { acknowledge, changeSeverity, closeProblem } = this.state;
|
||||||
|
|
||||||
|
const actionSelected = acknowledge || changeSeverity || closeProblem;
|
||||||
|
if (!this.state.value && !actionSelected) {
|
||||||
|
return this.setState({
|
||||||
|
error: true,
|
||||||
|
errorMessage: 'Enter message text or select an action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ ackError: '', loading: true });
|
||||||
|
|
||||||
|
const ackData: AckProblemData = {
|
||||||
|
message: this.state.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
let action = ZBX_ACK_ACTION_ADD_MESSAGE;
|
||||||
|
if (this.state.acknowledge) {
|
||||||
|
action += ZBX_ACK_ACTION_ACK;
|
||||||
|
}
|
||||||
|
if (this.state.changeSeverity) {
|
||||||
|
action += ZBX_ACK_ACTION_CHANGE_SEVERITY;
|
||||||
|
ackData.severity = this.state.selectedSeverity;
|
||||||
|
}
|
||||||
|
if (this.state.closeProblem) {
|
||||||
|
action += ZBX_ACK_ACTION_CLOSE;
|
||||||
|
}
|
||||||
|
ackData.action = action;
|
||||||
|
|
||||||
|
this.props.onSubmit(ackData).then(() => {
|
||||||
|
this.dismiss();
|
||||||
|
}).catch(err => {
|
||||||
|
this.setState({
|
||||||
|
ackError: err.message || err.data,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActions() {
|
||||||
|
const { canClose } = this.props;
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
<Checkbox key="ack" label="Acknowledge" value={this.state.acknowledge} onChange={this.onAcknowledgeToggle} />,
|
||||||
|
<Checkbox
|
||||||
|
key="change-severity"
|
||||||
|
label="Change severity"
|
||||||
|
description=""
|
||||||
|
value={this.state.changeSeverity}
|
||||||
|
onChange={this.onChangeSeverityToggle}
|
||||||
|
/>,
|
||||||
|
this.state.changeSeverity &&
|
||||||
|
<RadioButtonGroup
|
||||||
|
key="severity"
|
||||||
|
size="sm"
|
||||||
|
options={severityOptions}
|
||||||
|
value={this.state.selectedSeverity}
|
||||||
|
onChange={this.onChangeSelectedSeverity}
|
||||||
|
/>,
|
||||||
|
canClose &&
|
||||||
|
<Checkbox key="close" label="Close problem" disabled={!canClose} value={this.state.closeProblem} onChange={this.onCloseProblemToggle} />,
|
||||||
|
];
|
||||||
|
|
||||||
|
// <VerticalGroup /> doesn't handle empty elements properly, so don't return it
|
||||||
|
return actions.filter(e => e);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { theme } = this.props;
|
||||||
|
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const modalClass = cx(styles.modal);
|
||||||
|
const modalTitleClass = cx(styles.modalHeaderTitle);
|
||||||
|
const inputGroupClass = cx('gf-form', styles.inputGroup);
|
||||||
|
const inputClass = cx(this.state.error && styles.input);
|
||||||
|
const inputHintClass = cx('gf-form-hint-text', styles.inputHint);
|
||||||
|
const inputErrorClass = cx('gf-form-hint-text', styles.inputError);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={true}
|
||||||
|
onDismiss={this.dismiss}
|
||||||
|
className={modalClass}
|
||||||
|
title={
|
||||||
|
<div className={modalTitleClass}>
|
||||||
|
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="reply-all" />}
|
||||||
|
<span className="p-l-1">Acknowledge Problem</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={inputGroupClass}>
|
||||||
|
<label className="gf-form-hint">
|
||||||
|
<Input className={inputClass}
|
||||||
|
type="text"
|
||||||
|
name="message"
|
||||||
|
placeholder="Message"
|
||||||
|
maxLength={64}
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus={true}
|
||||||
|
value={this.state.value}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onKeyUp={this.handleKeyUp}>
|
||||||
|
</Input>
|
||||||
|
<small className={inputHintClass}>Press Enter to submit</small>
|
||||||
|
{this.state.error &&
|
||||||
|
<small className={inputErrorClass}>{this.state.errorMessage}</small>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form">
|
||||||
|
<VerticalGroup>
|
||||||
|
{this.renderActions()}
|
||||||
|
</VerticalGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.ackError &&
|
||||||
|
<div className="gf-form ack-request-error">
|
||||||
|
<span className={styles.ackError}>{this.state.ackError}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className="gf-form-button-row text-center">
|
||||||
|
<Button variant="primary" onClick={this.submit}>Update</Button>
|
||||||
|
<Button variant="secondary" onClick={this.dismiss}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
const red = theme.colors.red || (theme as any).palette.red;
|
||||||
|
return {
|
||||||
|
modal: css`
|
||||||
|
width: 500px;
|
||||||
|
`,
|
||||||
|
modalHeaderTitle: css`
|
||||||
|
font-size: ${theme.typography.heading.h3};
|
||||||
|
padding-top: ${theme.spacing.sm};
|
||||||
|
margin: 0 ${theme.spacing.md};
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
inputGroup: css`
|
||||||
|
margin-bottom: 16px;
|
||||||
|
`,
|
||||||
|
input: css`
|
||||||
|
border-color: ${red};
|
||||||
|
border-radius: 2px;
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 2px ${theme.colors.pageBg || (theme as any).colors.bg1}, 0 0 0px 4px ${red};
|
||||||
|
`,
|
||||||
|
inputHint: css`
|
||||||
|
display: inherit;
|
||||||
|
float: right;
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
`,
|
||||||
|
inputError: css`
|
||||||
|
float: left;
|
||||||
|
color: ${red};
|
||||||
|
`,
|
||||||
|
ackError: css`
|
||||||
|
color: ${red};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AckModal = withTheme(AckModalUnthemed);
|
||||||
@@ -3,29 +3,22 @@ import classNames from 'classnames';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { isNewProblem, formatLastChange } from '../../utils';
|
import { isNewProblem, formatLastChange } from '../../utils';
|
||||||
import { ProblemsPanelOptions, ZBXTrigger, ZBXTag } from '../../types';
|
import { ProblemsPanelOptions, TriggerSeverity } from '../../types';
|
||||||
import { AckProblemData, Modal } from '.././Modal';
|
import { AckProblemData, AckModal } from '../AckModal';
|
||||||
import EventTag from '../EventTag';
|
import EventTag from '../EventTag';
|
||||||
import Tooltip from '.././Tooltip/Tooltip';
|
|
||||||
import AlertAcknowledges from './AlertAcknowledges';
|
import AlertAcknowledges from './AlertAcknowledges';
|
||||||
import AlertIcon from './AlertIcon';
|
import AlertIcon from './AlertIcon';
|
||||||
|
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
|
||||||
|
import { ModalController, Tooltip } from '../../../components';
|
||||||
|
|
||||||
interface AlertCardProps {
|
interface AlertCardProps {
|
||||||
problem: ZBXTrigger;
|
problem: ProblemDTO;
|
||||||
panelOptions: ProblemsPanelOptions;
|
panelOptions: ProblemsPanelOptions;
|
||||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
|
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AlertCardState {
|
export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||||
showAckDialog: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AlertCard extends PureComponent<AlertCardProps, AlertCardState> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = { showAckDialog: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
|
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||||
if (this.props.onTagClick) {
|
if (this.props.onTagClick) {
|
||||||
@@ -35,23 +28,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
|||||||
|
|
||||||
ackProblem = (data: AckProblemData) => {
|
ackProblem = (data: AckProblemData) => {
|
||||||
const problem = this.props.problem;
|
const problem = this.props.problem;
|
||||||
return this.props.onProblemAck(problem, data).then(result => {
|
return this.props.onProblemAck(problem, data);
|
||||||
this.closeAckDialog();
|
|
||||||
}).catch(err => {
|
|
||||||
console.log(err);
|
|
||||||
this.closeAckDialog();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showAckDialog = () => {
|
|
||||||
const problem = this.props.problem;
|
|
||||||
if (problem.showAckButton) {
|
|
||||||
this.setState({ showAckDialog: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAckDialog = () => {
|
|
||||||
this.setState({ showAckDialog: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -59,9 +36,16 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
|||||||
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
|
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
|
||||||
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground });
|
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground });
|
||||||
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine });
|
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine });
|
||||||
const severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority));
|
|
||||||
const lastchange = formatLastChange(problem.lastchangeUnix, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat);
|
const problemSeverity = Number(problem.severity);
|
||||||
const age = moment.unix(problem.lastchangeUnix).fromNow(true);
|
let severityDesc: TriggerSeverity;
|
||||||
|
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity);
|
||||||
|
if (problem.severity) {
|
||||||
|
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastchange = formatLastChange(problem.timestamp, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat);
|
||||||
|
const age = moment.unix(problem.timestamp).fromNow(true);
|
||||||
|
|
||||||
let newProblem = false;
|
let newProblem = false;
|
||||||
if (panelOptions.highlightNewerThan) {
|
if (panelOptions.highlightNewerThan) {
|
||||||
@@ -72,7 +56,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
|||||||
let problemColor: string;
|
let problemColor: string;
|
||||||
if (problem.value === '0') {
|
if (problem.value === '0') {
|
||||||
problemColor = panelOptions.okEventColor;
|
problemColor = panelOptions.okEventColor;
|
||||||
} else if (panelOptions.markAckEvents && problem.lastEvent.acknowledged === "1") {
|
} else if (panelOptions.markAckEvents && problem.acknowledged === "1") {
|
||||||
problemColor = panelOptions.ackEventColor;
|
problemColor = panelOptions.ackEventColor;
|
||||||
} else {
|
} else {
|
||||||
problemColor = severityDesc.color;
|
problemColor = severityDesc.color;
|
||||||
@@ -153,22 +137,32 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
|||||||
<span><i className="fa fa-question-circle"></i></span>
|
<span><i className="fa fa-question-circle"></i></span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{problem.lastEvent && (
|
{problem.eventid && (
|
||||||
<AlertAcknowledgesButton problem={problem} onClick={this.showAckDialog} />
|
<ModalController>
|
||||||
|
{({ showModal, hideModal }) => (
|
||||||
|
<AlertAcknowledgesButton
|
||||||
|
problem={problem}
|
||||||
|
onClick={() => {
|
||||||
|
showModal(AckModal, {
|
||||||
|
canClose: problem.manual_close === '1',
|
||||||
|
severity: problemSeverity,
|
||||||
|
onSubmit: this.ackProblem,
|
||||||
|
onDismiss: hideModal,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalController>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal withBackdrop={true}
|
|
||||||
isOpen={this.state.showAckDialog}
|
|
||||||
onSubmit={this.ackProblem}
|
|
||||||
onClose={this.closeAckDialog} />
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AlertHostProps {
|
interface AlertHostProps {
|
||||||
problem: ZBXTrigger;
|
problem: ProblemDTO;
|
||||||
panelOptions: ProblemsPanelOptions;
|
panelOptions: ProblemsPanelOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +188,7 @@ function AlertHost(props: AlertHostProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AlertGroupProps {
|
interface AlertGroupProps {
|
||||||
problem: ZBXTrigger;
|
problem: ProblemDTO;
|
||||||
panelOptions: ProblemsPanelOptions;
|
panelOptions: ProblemsPanelOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +241,7 @@ function AlertSeverity(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AlertAcknowledgesButtonProps {
|
interface AlertAcknowledgesButtonProps {
|
||||||
problem: ZBXTrigger;
|
problem: ProblemDTO;
|
||||||
onClick: (event?) => void;
|
onClick: (event?) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { FC } from 'react';
|
||||||
import classNames from 'classnames';
|
import { cx, css } from 'emotion';
|
||||||
import { ZBXTrigger } from '../../types';
|
import { GFHeartIcon } from '../../../components';
|
||||||
|
import { ProblemDTO } from '../../../datasource-zabbix/types';
|
||||||
|
|
||||||
interface AlertIconProps {
|
interface Props {
|
||||||
problem: ZBXTrigger;
|
problem: ProblemDTO;
|
||||||
color: string;
|
color: string;
|
||||||
blink?: boolean;
|
blink?: boolean;
|
||||||
highlightBackground?: boolean;
|
highlightBackground?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AlertIcon(props: AlertIconProps) {
|
export const AlertIcon: FC<Props> = ({ problem, color, blink, highlightBackground }) => {
|
||||||
const { problem, color, blink, highlightBackground } = props;
|
const severity = Number(problem.severity);
|
||||||
const priority = Number(problem.priority);
|
const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online';
|
||||||
let iconClass = '';
|
|
||||||
if (problem.value === '1' && priority >= 2) {
|
|
||||||
iconClass = 'icon-gf-critical';
|
|
||||||
} else {
|
|
||||||
iconClass = 'icon-gf-online';
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = classNames('icon-gf', iconClass, { 'zabbix-trigger--blinked': blink });
|
const iconClass = cx(
|
||||||
const style: CSSProperties = {};
|
'icon-gf',
|
||||||
if (!highlightBackground) {
|
blink && 'zabbix-trigger--blinked',
|
||||||
style.color = color;
|
);
|
||||||
}
|
|
||||||
|
const wrapperClass = cx(
|
||||||
|
'alert-rule-item__icon',
|
||||||
|
!highlightBackground && css`color: ${color}`
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="alert-rule-item__icon" style={style}>
|
<div className={wrapperClass}>
|
||||||
<i className={className}></i>
|
<GFHeartIcon status={status} className={iconClass} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default AlertIcon;
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import React, { PureComponent, CSSProperties } from 'react';
|
import React, { PureComponent, CSSProperties } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ProblemsPanelOptions, ZBXTrigger, GFTimeRange, ZBXTag } from '../../types';
|
import { ProblemsPanelOptions, GFTimeRange } from '../../types';
|
||||||
import { AckProblemData } from '.././Modal';
|
import { AckProblemData } from '../AckModal';
|
||||||
import AlertCard from './AlertCard';
|
import AlertCard from './AlertCard';
|
||||||
|
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
|
||||||
|
|
||||||
export interface AlertListProps {
|
export interface AlertListProps {
|
||||||
problems: ZBXTrigger[];
|
problems: ProblemDTO[];
|
||||||
panelOptions: ProblemsPanelOptions;
|
panelOptions: ProblemsPanelOptions;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
timeRange?: GFTimeRange;
|
timeRange?: GFTimeRange;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
|
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
|
||||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AlertListState {
|
interface AlertListState {
|
||||||
page: number;
|
page: number;
|
||||||
currentProblems: ZBXTrigger[];
|
currentProblems: ProblemDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AlertList extends PureComponent<AlertListProps, AlertListState> {
|
export default class AlertList extends PureComponent<AlertListProps, AlertListState> {
|
||||||
@@ -51,7 +52,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProblemAck = (problem: ZBXTrigger, data: AckProblemData) => {
|
handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => {
|
||||||
return this.props.onProblemAck(problem, data);
|
return this.props.onProblemAck(problem, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
|
|||||||
<ol className={alertListClass}>
|
<ol className={alertListClass}>
|
||||||
{currentProblems.map(problem =>
|
{currentProblems.map(problem =>
|
||||||
<AlertCard
|
<AlertCard
|
||||||
key={`${problem.triggerid}-${problem.datasource}`}
|
key={`${problem.triggerid}-${problem.eventid}-${problem.datasource}`}
|
||||||
problem={problem}
|
problem={problem}
|
||||||
panelOptions={panelOptions}
|
panelOptions={panelOptions}
|
||||||
onTagClick={this.handleTagClick}
|
onTagClick={this.handleTagClick}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { ZBXTag } from '../types';
|
import { ZBXTag } from '../types';
|
||||||
import Tooltip from './Tooltip/Tooltip';
|
import Tooltip from '../../components/Tooltip/Tooltip';
|
||||||
|
|
||||||
const TAG_COLORS = [
|
const TAG_COLORS = [
|
||||||
'#E24D42',
|
'#E24D42',
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface FAIconProps {
|
|
||||||
icon: string;
|
|
||||||
customClass?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FAIcon(props: FAIconProps) {
|
|
||||||
return (
|
|
||||||
<span className={`fa-icon-container ${props.customClass || ''}`}>
|
|
||||||
<i className={`fa fa-${props.icon}`}></i>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
interface GFHeartIconProps {
|
|
||||||
status: 'critical' | 'warning' | 'online' | 'ok' | 'problem';
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GFHeartIcon(props: GFHeartIconProps) {
|
|
||||||
const status = props.status;
|
|
||||||
const className = classNames("icon-gf", props.className,
|
|
||||||
{ "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'},
|
|
||||||
{ "icon-gf-online": status === 'online' || status === 'ok' },
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<i className={className}></i>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
const KEYBOARD_ENTER_KEY = 13;
|
|
||||||
const KEYBOARD_ESCAPE_KEY = 27;
|
|
||||||
|
|
||||||
interface ModalProps {
|
|
||||||
isOpen?: boolean;
|
|
||||||
withBackdrop?: boolean;
|
|
||||||
onSubmit: (data?: AckProblemData) => Promise<any> | any;
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModalState {
|
|
||||||
value: string;
|
|
||||||
error: boolean;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AckProblemData {
|
|
||||||
message: string;
|
|
||||||
closeProblem?: boolean;
|
|
||||||
action?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Modal extends PureComponent<ModalProps, ModalState> {
|
|
||||||
modalContainer: HTMLElement;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
message: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.modalContainer = document.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({ value: event.target.value, error: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') {
|
|
||||||
this.submit();
|
|
||||||
} else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') {
|
|
||||||
this.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBackdropClick = () => {
|
|
||||||
this.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss = () => {
|
|
||||||
this.setState({ value: '', error: false, message: '' });
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
submit = () => {
|
|
||||||
if (!this.state.value) {
|
|
||||||
return this.setState({
|
|
||||||
error: true,
|
|
||||||
message: 'Enter message text'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.props.onSubmit({
|
|
||||||
message: this.state.value
|
|
||||||
}).then(() => {
|
|
||||||
this.dismiss();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.props.isOpen || !this.modalContainer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputClass = classNames('gf-form-input', { 'zbx-ack-error': this.state.error });
|
|
||||||
|
|
||||||
const modalNode = (
|
|
||||||
<div className="modal modal--narrow zbx-ack-modal" key="modal">
|
|
||||||
<div className="modal-body">
|
|
||||||
<div className="modal-header">
|
|
||||||
<h2 className="modal-header-title">
|
|
||||||
<i className="fa fa-reply-all"></i>
|
|
||||||
<span className="p-l-1">Acknowledge Problem</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<a className="modal-header-close" onClick={this.dismiss}>
|
|
||||||
<i className="fa fa-remove"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="modal-content">
|
|
||||||
<div className="gf-form">
|
|
||||||
<label className="gf-form-hint">
|
|
||||||
<input className={inputClass}
|
|
||||||
type="text"
|
|
||||||
name="message"
|
|
||||||
placeholder="Message"
|
|
||||||
maxLength={64}
|
|
||||||
autoComplete="off"
|
|
||||||
autoFocus={true}
|
|
||||||
value={this.state.value}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
onKeyUp={this.handleKeyUp}>
|
|
||||||
</input>
|
|
||||||
<small className="gf-form-hint-text muted">Press Enter to submit</small>
|
|
||||||
{this.state.error &&
|
|
||||||
<small className="gf-form-hint-text muted ack-error-message">{this.state.message}</small>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="gf-form-button-row text-center">
|
|
||||||
<button className="btn btn-success" onClick={this.submit}>Acknowledge</button>
|
|
||||||
<button className="btn btn-inverse" onClick={this.dismiss}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const modalNodeWithBackdrop = [
|
|
||||||
modalNode,
|
|
||||||
<div className="modal-backdrop in" key="modal-backdrop" onClick={this.handleBackdropClick}></div>
|
|
||||||
];
|
|
||||||
|
|
||||||
const modal = this.props.withBackdrop ? modalNodeWithBackdrop : modalNode;
|
|
||||||
return ReactDOM.createPortal(modal, this.modalContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
src/panel-triggers/components/Problems/AckCell.tsx
Normal file
34
src/panel-triggers/components/Problems/AckCell.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { RTCell } from '../../types';
|
||||||
|
import { ProblemDTO } from '../../../datasource-zabbix/types';
|
||||||
|
import { FAIcon } from '../../../components';
|
||||||
|
import { useTheme, stylesFactory } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
countLabel: css`
|
||||||
|
font-size: ${theme.typography.size.sm};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AckCell: React.FC<RTCell<ProblemDTO>> = (props: RTCell<ProblemDTO>) => {
|
||||||
|
const problem = props.original;
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{problem.acknowledges?.length > 0 &&
|
||||||
|
<>
|
||||||
|
<FAIcon icon="comments" />
|
||||||
|
<span className={styles.countLabel}> ({problem.acknowledges?.length})</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AckCell;
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as utils from '../../../datasource-zabbix/utils';
|
import * as utils from '../../../datasource-zabbix/utils';
|
||||||
import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag, ZBXAlert } from '../../types';
|
import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
|
||||||
import { Modal, AckProblemData } from '../Modal';
|
import { ZBXItem, GFTimeRange, RTRow } from '../../types';
|
||||||
|
import { AckModal, AckProblemData } from '../AckModal';
|
||||||
import EventTag from '../EventTag';
|
import EventTag from '../EventTag';
|
||||||
import Tooltip from '../Tooltip/Tooltip';
|
|
||||||
import ProblemStatusBar from './ProblemStatusBar';
|
import ProblemStatusBar from './ProblemStatusBar';
|
||||||
import AcknowledgesList from './AcknowledgesList';
|
import AcknowledgesList from './AcknowledgesList';
|
||||||
import ProblemTimeline from './ProblemTimeline';
|
import ProblemTimeline from './ProblemTimeline';
|
||||||
import FAIcon from '../FAIcon';
|
import { FAIcon, ExploreButton, AckButton, Tooltip, ModalController } from '../../../components';
|
||||||
|
|
||||||
interface ProblemDetailsProps extends RTRow<ZBXTrigger> {
|
interface ProblemDetailsProps extends RTRow<ProblemDTO> {
|
||||||
rootWidth: number;
|
rootWidth: number;
|
||||||
timeRange: GFTimeRange;
|
timeRange: GFTimeRange;
|
||||||
showTimeline?: boolean;
|
showTimeline?: boolean;
|
||||||
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
|
panelId?: number;
|
||||||
getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
|
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
|
||||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
|
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
|
||||||
|
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
|
||||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,17 +25,15 @@ interface ProblemDetailsState {
|
|||||||
events: ZBXEvent[];
|
events: ZBXEvent[];
|
||||||
alerts: ZBXAlert[];
|
alerts: ZBXAlert[];
|
||||||
show: boolean;
|
show: boolean;
|
||||||
showAckDialog: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> {
|
export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
events: [],
|
events: [],
|
||||||
alerts: [],
|
alerts: [],
|
||||||
show: false,
|
show: false,
|
||||||
showAckDialog: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,32 +70,20 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
|||||||
}
|
}
|
||||||
|
|
||||||
ackProblem = (data: AckProblemData) => {
|
ackProblem = (data: AckProblemData) => {
|
||||||
const problem = this.props.original as ZBXTrigger;
|
const problem = this.props.original as ProblemDTO;
|
||||||
return this.props.onProblemAck(problem, data).then(result => {
|
return this.props.onProblemAck(problem, data);
|
||||||
this.closeAckDialog();
|
|
||||||
}).catch(err => {
|
|
||||||
console.log(err);
|
|
||||||
this.closeAckDialog();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showAckDialog = () => {
|
|
||||||
this.setState({ showAckDialog: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAckDialog = () => {
|
|
||||||
this.setState({ showAckDialog: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const problem = this.props.original as ZBXTrigger;
|
const problem = this.props.original as ProblemDTO;
|
||||||
const alerts = this.state.alerts;
|
const alerts = this.state.alerts;
|
||||||
const rootWidth = this.props.rootWidth;
|
const rootWidth = this.props.rootWidth;
|
||||||
const displayClass = this.state.show ? 'show' : '';
|
const displayClass = this.state.show ? 'show' : '';
|
||||||
const wideLayout = rootWidth > 1200;
|
const wideLayout = rootWidth > 1200;
|
||||||
const compactStatusBar = rootWidth < 800 || problem.acknowledges && wideLayout && rootWidth < 1400;
|
const compactStatusBar = rootWidth < 800 || problem.acknowledges && wideLayout && rootWidth < 1400;
|
||||||
const age = moment.unix(problem.lastchangeUnix).fromNow(true);
|
const age = moment.unix(problem.timestamp).fromNow(true);
|
||||||
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
|
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
|
||||||
|
const problemSeverity = Number(problem.severity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`problem-details-container ${displayClass}`}>
|
<div className={`problem-details-container ${displayClass}`}>
|
||||||
@@ -109,21 +96,37 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
|||||||
</div>
|
</div>
|
||||||
{problem.items && <ProblemItems items={problem.items} />}
|
{problem.items && <ProblemItems items={problem.items} />}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="problem-actions-left">
|
||||||
|
<ExploreButton problem={problem} panelId={this.props.panelId} />
|
||||||
|
</div>
|
||||||
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
|
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
|
||||||
{problem.showAckButton &&
|
{problem.showAckButton &&
|
||||||
<div className="problem-actions">
|
<div className="problem-actions">
|
||||||
<ProblemActionButton className="navbar-button navbar-button--settings"
|
<ModalController>
|
||||||
icon="reply-all"
|
{({ showModal, hideModal }) => (
|
||||||
tooltip="Acknowledge problem"
|
<AckButton
|
||||||
onClick={this.showAckDialog} />
|
className="navbar-button navbar-button--settings"
|
||||||
|
onClick={() => {
|
||||||
|
showModal(AckModal, {
|
||||||
|
canClose: problem.manual_close === '1',
|
||||||
|
severity: problemSeverity,
|
||||||
|
onSubmit: this.ackProblem,
|
||||||
|
onDismiss: hideModal,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalController>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{problem.comments &&
|
{problem.comments &&
|
||||||
|
<div className="problem-description-row">
|
||||||
<div className="problem-description">
|
<div className="problem-description">
|
||||||
<span className="description-label">Description: </span>
|
<span className="description-label">Description: </span>
|
||||||
<span>{problem.comments}</span>
|
<span>{problem.comments}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
{problem.tags && problem.tags.length > 0 &&
|
{problem.tags && problem.tags.length > 0 &&
|
||||||
<div className="problem-tags">
|
<div className="problem-tags">
|
||||||
@@ -169,10 +172,6 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
|||||||
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
|
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
|
||||||
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
|
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
|
||||||
</div>
|
</div>
|
||||||
<Modal withBackdrop={true}
|
|
||||||
isOpen={this.state.showAckDialog}
|
|
||||||
onSubmit={this.ackProblem}
|
|
||||||
onClose={this.closeAckDialog} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -242,33 +241,3 @@ class ProblemHosts extends PureComponent<ProblemHostsProps> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProblemActionButtonProps {
|
|
||||||
icon: string;
|
|
||||||
tooltip?: string;
|
|
||||||
className?: string;
|
|
||||||
onClick?: (event?) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProblemActionButton extends PureComponent<ProblemActionButtonProps> {
|
|
||||||
handleClick = (event) => {
|
|
||||||
this.props.onClick(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { icon, tooltip, className } = this.props;
|
|
||||||
let button = (
|
|
||||||
<button className={`btn problem-action-button ${className || ''}`} onClick={this.handleClick}>
|
|
||||||
<FAIcon icon={icon} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
if (tooltip) {
|
|
||||||
button = (
|
|
||||||
<Tooltip placement="bottom" content={tooltip}>
|
|
||||||
{button}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FAIcon from '../FAIcon';
|
import FAIcon from '../../../components/FAIcon/FAIcon';
|
||||||
import Tooltip from '../Tooltip/Tooltip';
|
import Tooltip from '../../../components/Tooltip/Tooltip';
|
||||||
import { ZBXTrigger, ZBXAlert } from '../../types';
|
import { ZBXTrigger, ZBXAlert } from '../../types';
|
||||||
|
|
||||||
export interface ProblemStatusBarProps {
|
export interface ProblemStatusBarProps {
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<rect key={event.eventid} className={className} {...attributes} />
|
<rect key={`${event.eventid}-${index}`} className={className} {...attributes} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -480,7 +480,7 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelinePoint
|
<TimelinePoint
|
||||||
key={event.eventid}
|
key={`${event.eventid}-${i}`}
|
||||||
className={className}
|
className={className}
|
||||||
x={posLeft}
|
x={posLeft}
|
||||||
r={pointR}
|
r={pointR}
|
||||||
@@ -611,7 +611,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelineAck
|
<TimelineAck
|
||||||
key={ack.eventid}
|
key={`${ack.eventid}-${i}`}
|
||||||
x={posLeft}
|
x={posLeft}
|
||||||
r={pointR}
|
r={pointR}
|
||||||
highlighted={highlighted}
|
highlighted={highlighted}
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table-6';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as utils from '../../../datasource-zabbix/utils';
|
|
||||||
import { isNewProblem } from '../../utils';
|
import { isNewProblem } from '../../utils';
|
||||||
import { ProblemsPanelOptions, ZBXTrigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized, ZBXAlert } from '../../types';
|
|
||||||
import EventTag from '../EventTag';
|
import EventTag from '../EventTag';
|
||||||
import ProblemDetails from './ProblemDetails';
|
import { ProblemDetails } from './ProblemDetails';
|
||||||
import { AckProblemData } from '../Modal';
|
import { AckProblemData } from '../AckModal';
|
||||||
import GFHeartIcon from '../GFHeartIcon';
|
import { GFHeartIcon, FAIcon } from '../../../components';
|
||||||
|
import { ProblemsPanelOptions, GFTimeRange, RTCell, TriggerSeverity, RTResized } from '../../types';
|
||||||
|
import { ProblemDTO, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
|
||||||
|
import { AckCell } from './AckCell';
|
||||||
|
|
||||||
export interface ProblemListProps {
|
export interface ProblemListProps {
|
||||||
problems: ZBXTrigger[];
|
problems: ProblemDTO[];
|
||||||
panelOptions: ProblemsPanelOptions;
|
panelOptions: ProblemsPanelOptions;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
timeRange?: GFTimeRange;
|
timeRange?: GFTimeRange;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
|
panelId?: number;
|
||||||
getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
|
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
|
||||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
|
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
|
||||||
|
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
|
||||||
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||||
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
|
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
|
||||||
onColumnResize?: (newResized: RTResized) => void;
|
onColumnResize?: (newResized: RTResized) => void;
|
||||||
@@ -47,7 +49,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
|||||||
this.rootRef = ref;
|
this.rootRef = ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProblemAck = (problem: ZBXTrigger, data: AckProblemData) => {
|
handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => {
|
||||||
return this.props.onProblemAck(problem, data);
|
return this.props.onProblemAck(problem, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,19 +87,21 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
|||||||
const result = [];
|
const result = [];
|
||||||
const options = this.props.panelOptions;
|
const options = this.props.panelOptions;
|
||||||
const highlightNewerThan = options.highlightNewEvents && options.highlightNewerThan;
|
const highlightNewerThan = options.highlightNewEvents && options.highlightNewerThan;
|
||||||
const statusCell = props => StatusCell(props, options.okEventColor, DEFAULT_PROBLEM_COLOR, highlightNewerThan);
|
const statusCell = props => StatusCell(props, highlightNewerThan);
|
||||||
const statusIconCell = props => StatusIconCell(props, highlightNewerThan);
|
const statusIconCell = props => StatusIconCell(props, highlightNewerThan);
|
||||||
|
const hostNameCell = props => <HostCell name={props.original.host} maintenance={props.original.maintenance} />;
|
||||||
|
const hostTechNameCell = props => <HostCell name={props.original.hostTechName} maintenance={props.original.maintenance} />;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ Header: 'Host', accessor: 'host', show: options.hostField },
|
{ Header: 'Host', id: 'host', show: options.hostField, Cell: hostNameCell },
|
||||||
{ Header: 'Host (Technical Name)', accessor: 'hostTechName', show: options.hostTechNameField },
|
{ Header: 'Host (Technical Name)', id: 'hostTechName', show: options.hostTechNameField, Cell: hostTechNameCell },
|
||||||
{ Header: 'Host Groups', accessor: 'groups', show: options.hostGroups, Cell: GroupCell },
|
{ Header: 'Host Groups', accessor: 'groups', show: options.hostGroups, Cell: GroupCell },
|
||||||
{ Header: 'Proxy', accessor: 'proxy', show: options.hostProxy },
|
{ Header: 'Proxy', accessor: 'proxy', show: options.hostProxy },
|
||||||
{
|
{
|
||||||
Header: 'Severity', show: options.severityField, className: 'problem-severity', width: 120,
|
Header: 'Severity', show: options.severityField, className: 'problem-severity', width: 120,
|
||||||
accessor: problem => problem.priority,
|
accessor: problem => problem.priority,
|
||||||
id: 'severity',
|
id: 'severity',
|
||||||
Cell: props => SeverityCell(props, options.triggerSeverity, options.markAckEvents, options.ackEventColor),
|
Cell: props => SeverityCell(props, options.triggerSeverity, options.markAckEvents, options.ackEventColor, options.okEventColor),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: '', id: 'statusIcon', show: options.statusIcon, className: 'problem-status-icon', width: 50,
|
Header: '', id: 'statusIcon', show: options.statusIcon, className: 'problem-status-icon', width: 50,
|
||||||
@@ -106,17 +110,21 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
|||||||
},
|
},
|
||||||
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
|
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
|
||||||
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell},
|
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell},
|
||||||
|
{
|
||||||
|
Header: 'Ack', id: 'ack', show: options.ackField, width: 70,
|
||||||
|
Cell: props => <AckCell {...props} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: 'Tags', accessor: 'tags', show: options.showTags, className: 'problem-tags',
|
Header: 'Tags', accessor: 'tags', show: options.showTags, className: 'problem-tags',
|
||||||
Cell: props => <TagCell {...props} onTagClick={this.handleTagClick} />
|
Cell: props => <TagCell {...props} onTagClick={this.handleTagClick} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Age', className: 'problem-age', width: 100, show: options.ageField, accessor: 'lastchangeUnix',
|
Header: 'Age', className: 'problem-age', width: 100, show: options.ageField, accessor: 'timestamp',
|
||||||
id: 'age',
|
id: 'age',
|
||||||
Cell: AgeCell,
|
Cell: AgeCell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Time', className: 'last-change', width: 150, accessor: 'lastchangeUnix',
|
Header: 'Time', className: 'last-change', width: 150, accessor: 'timestamp',
|
||||||
id: 'lastchange',
|
id: 'lastchange',
|
||||||
Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat),
|
Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat),
|
||||||
},
|
},
|
||||||
@@ -159,10 +167,12 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
|||||||
rootWidth={this.rootWidth}
|
rootWidth={this.rootWidth}
|
||||||
timeRange={this.props.timeRange}
|
timeRange={this.props.timeRange}
|
||||||
showTimeline={panelOptions.problemTimeline}
|
showTimeline={panelOptions.problemTimeline}
|
||||||
|
panelId={this.props.panelId}
|
||||||
getProblemEvents={this.props.getProblemEvents}
|
getProblemEvents={this.props.getProblemEvents}
|
||||||
getProblemAlerts={this.props.getProblemAlerts}
|
getProblemAlerts={this.props.getProblemAlerts}
|
||||||
onProblemAck={this.handleProblemAck}
|
onProblemAck={this.handleProblemAck}
|
||||||
onTagClick={this.handleTagClick}
|
onTagClick={this.handleTagClick}
|
||||||
|
subRows={false}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
expanded={this.getExpandedPage(this.state.page)}
|
expanded={this.getExpandedPage(this.state.page)}
|
||||||
@@ -176,14 +186,41 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) {
|
interface HostCellProps {
|
||||||
|
name: string;
|
||||||
|
maintenance: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HostCell: React.FC<HostCellProps> = ({ name, maintenance }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span style={{ paddingRight: '0.4rem' }}>{name}</span>
|
||||||
|
{maintenance && <FAIcon customClass="fired" icon="wrench" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SeverityCell(
|
||||||
|
props: RTCell<ProblemDTO>,
|
||||||
|
problemSeverityDesc: TriggerSeverity[],
|
||||||
|
markAckEvents?: boolean,
|
||||||
|
ackEventColor?: string,
|
||||||
|
okColor = DEFAULT_OK_COLOR
|
||||||
|
) {
|
||||||
const problem = props.original;
|
const problem = props.original;
|
||||||
let color: string;
|
let color: string;
|
||||||
const severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(props.original.priority));
|
|
||||||
color = severityDesc.color;
|
let severityDesc: TriggerSeverity;
|
||||||
|
const severity = Number(problem.severity);
|
||||||
|
severityDesc = _.find(problemSeverityDesc, s => s.priority === severity);
|
||||||
|
if (problem.severity && problem.value === '1') {
|
||||||
|
severityDesc = _.find(problemSeverityDesc, s => s.priority === severity);
|
||||||
|
}
|
||||||
|
|
||||||
|
color = problem.value === '0' ? okColor : severityDesc.color;
|
||||||
|
|
||||||
// Mark acknowledged triggers with different color
|
// Mark acknowledged triggers with different color
|
||||||
if (markAckEvents && problem.lastEvent.acknowledged === "1") {
|
if (markAckEvents && problem.acknowledged === "1") {
|
||||||
color = ackEventColor;
|
color = ackEventColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,9 +234,9 @@ function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSev
|
|||||||
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
|
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
|
||||||
const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)';
|
const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)';
|
||||||
|
|
||||||
function StatusCell(props: RTCell<ZBXTrigger>, okColor = DEFAULT_OK_COLOR, problemColor = DEFAULT_PROBLEM_COLOR, highlightNewerThan?: string) {
|
function StatusCell(props: RTCell<ProblemDTO>, highlightNewerThan?: string) {
|
||||||
const status = props.value === '0' ? 'RESOLVED' : 'PROBLEM';
|
const status = props.value === '0' ? 'RESOLVED' : 'PROBLEM';
|
||||||
const color = props.value === '0' ? okColor : problemColor;
|
const color = props.value === '0' ? DEFAULT_OK_COLOR : DEFAULT_PROBLEM_COLOR;
|
||||||
let newProblem = false;
|
let newProblem = false;
|
||||||
if (highlightNewerThan) {
|
if (highlightNewerThan) {
|
||||||
newProblem = isNewProblem(props.original, highlightNewerThan);
|
newProblem = isNewProblem(props.original, highlightNewerThan);
|
||||||
@@ -209,7 +246,7 @@ function StatusCell(props: RTCell<ZBXTrigger>, okColor = DEFAULT_OK_COLOR, probl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusIconCell(props: RTCell<ZBXTrigger>, highlightNewerThan?: string) {
|
function StatusIconCell(props: RTCell<ProblemDTO>, highlightNewerThan?: string) {
|
||||||
const status = props.value === '0' ? 'ok' : 'problem';
|
const status = props.value === '0' ? 'ok' : 'problem';
|
||||||
let newProblem = false;
|
let newProblem = false;
|
||||||
if (highlightNewerThan) {
|
if (highlightNewerThan) {
|
||||||
@@ -223,7 +260,7 @@ function StatusIconCell(props: RTCell<ZBXTrigger>, highlightNewerThan?: string)
|
|||||||
return <GFHeartIcon status={status} className={className} />;
|
return <GFHeartIcon status={status} className={className} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupCell(props: RTCell<ZBXTrigger>) {
|
function GroupCell(props: RTCell<ProblemDTO>) {
|
||||||
let groups = "";
|
let groups = "";
|
||||||
if (props.value && props.value.length) {
|
if (props.value && props.value.length) {
|
||||||
groups = props.value.map(g => g.name).join(', ');
|
groups = props.value.map(g => g.name).join(', ');
|
||||||
@@ -233,7 +270,7 @@ function GroupCell(props: RTCell<ZBXTrigger>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProblemCell(props: RTCell<ZBXTrigger>) {
|
function ProblemCell(props: RTCell<ProblemDTO>) {
|
||||||
const comments = props.original.comments;
|
const comments = props.original.comments;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -243,23 +280,23 @@ function ProblemCell(props: RTCell<ZBXTrigger>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AgeCell(props: RTCell<ZBXTrigger>) {
|
function AgeCell(props: RTCell<ProblemDTO>) {
|
||||||
const problem = props.original;
|
const problem = props.original;
|
||||||
const timestamp = moment.unix(problem.lastchangeUnix);
|
const timestamp = moment.unix(problem.timestamp);
|
||||||
const age = timestamp.fromNow(true);
|
const age = timestamp.fromNow(true);
|
||||||
return <span>{age}</span>;
|
return <span>{age}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LastChangeCell(props: RTCell<ZBXTrigger>, customFormat?: string) {
|
function LastChangeCell(props: RTCell<ProblemDTO>, customFormat?: string) {
|
||||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||||
const problem = props.original;
|
const problem = props.original;
|
||||||
const timestamp = moment.unix(problem.lastchangeUnix);
|
const timestamp = moment.unix(problem.timestamp);
|
||||||
const format = customFormat || DEFAULT_TIME_FORMAT;
|
const format = customFormat || DEFAULT_TIME_FORMAT;
|
||||||
const lastchange = timestamp.format(format);
|
const lastchange = timestamp.format(format);
|
||||||
return <span>{lastchange}</span>;
|
return <span>{lastchange}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TagCellProps extends RTCell<ZBXTrigger> {
|
interface TagCellProps extends RTCell<ProblemDTO> {
|
||||||
onTagClick: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
onTagClick: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import BodyPortal from './Portal';
|
|
||||||
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
|
|
||||||
import Transition from 'react-transition-group/Transition';
|
|
||||||
|
|
||||||
const defaultTransitionStyles = {
|
|
||||||
transition: 'opacity 200ms linear',
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const transitionStyles = {
|
|
||||||
exited: { opacity: 0 },
|
|
||||||
entering: { opacity: 0 },
|
|
||||||
entered: { opacity: 1 },
|
|
||||||
exiting: { opacity: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
renderContent: (content: any) => any;
|
|
||||||
show: boolean;
|
|
||||||
placement?: any;
|
|
||||||
content: string | ((props: any) => JSX.Element);
|
|
||||||
refClassName?: string;
|
|
||||||
popperClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Popper extends PureComponent<Props> {
|
|
||||||
render() {
|
|
||||||
const { children, renderContent, show, placement, refClassName } = this.props;
|
|
||||||
const { content } = this.props;
|
|
||||||
const popperClassName = classNames('popper', this.props.popperClassName);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Manager>
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
|
||||||
{transitionState => (
|
|
||||||
<BodyPortal>
|
|
||||||
<ReactPopper placement={placement}>
|
|
||||||
{({ ref, style, placement, arrowProps }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
...defaultTransitionStyles,
|
|
||||||
...transitionStyles[transitionState],
|
|
||||||
}}
|
|
||||||
data-placement={placement}
|
|
||||||
className={popperClassName}
|
|
||||||
>
|
|
||||||
<div className="popper__background">
|
|
||||||
{renderContent(content)}
|
|
||||||
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</ReactPopper>
|
|
||||||
</BodyPortal>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
</Manager>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Popper;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import Popper from './Popper';
|
|
||||||
import withPopper, { UsingPopperProps } from './withPopper';
|
|
||||||
|
|
||||||
class Tooltip extends PureComponent<UsingPopperProps> {
|
|
||||||
render() {
|
|
||||||
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
|
|
||||||
<Popper {...restProps}>{children}</Popper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withPopper(Tooltip);
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<value-select-dropdown
|
|
||||||
variable="ctrl.dsOptions"
|
|
||||||
on-updated="ctrl.onChange(ctrl.dsOptions)"
|
|
||||||
dashboard="ctrl.dashboard">
|
|
||||||
</value-select-dropdown>
|
|
||||||
`;
|
|
||||||
|
|
||||||
angular
|
|
||||||
.module('grafana.directives')
|
|
||||||
.directive('datasourceSelector', () => {
|
|
||||||
return {
|
|
||||||
scope: {
|
|
||||||
datasources: "=",
|
|
||||||
options: "=",
|
|
||||||
onChange: "&"
|
|
||||||
},
|
|
||||||
controller: DatasourceSelectorCtrl,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
template: template
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
class DatasourceSelectorCtrl {
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope) {
|
|
||||||
this.scope = $scope;
|
|
||||||
let datasources = $scope.datasources;
|
|
||||||
let options = $scope.options;
|
|
||||||
this.dsOptions = {
|
|
||||||
multi: true,
|
|
||||||
current: {value: datasources, text: datasources.join(" + ")},
|
|
||||||
options: _.map(options, (ds) => {
|
|
||||||
return {text: ds, value: ds, selected: _.includes(datasources, ds)};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
// Fix for Grafana 6.0
|
|
||||||
// https://github.com/grafana/grafana/blob/v6.0.0/public/app/core/directives/value_select_dropdown.ts#L291
|
|
||||||
this.dashboard = {
|
|
||||||
on: () => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(updatedOptions) {
|
|
||||||
let newDataSources = updatedOptions.current.value;
|
|
||||||
this.scope.datasources = newDataSources;
|
|
||||||
|
|
||||||
// Run after model was changed
|
|
||||||
this.scope.$$postDigest(() => {
|
|
||||||
this.scope.onChange();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
src/panel-triggers/img/icn-zabbix-problems-panel.svg
Normal file
1
src/panel-triggers/img/icn-zabbix-problems-panel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 336 336" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Layer_2"><g id="Layer_1-2"><path d="M71.833,181.833c-8.666,-8.666 -27.375,-5.916 -32.25,10.667c-4.875,-15.5 -26.5,-18.958 -34.666,-8c-7.34,9.842 -6.304,23.732 2.416,32.375l29.417,29.458c1.578,1.424 4.005,1.424 5.583,0l29.459,-29.458c9.494,-9.664 9.494,-25.378 0,-35.042l0.041,0Z" style="fill:#0b9d4d;fill-rule:nonzero;"/><path d="M71.833,91.958c-8.333,-8.333 -25.916,-5.958 -31.541,9.042l-2.792,6.958l12.083,2.875c1.471,0.358 2.514,1.686 2.514,3.199c0,0.478 -0.104,0.951 -0.305,1.385l-7.875,16.666c-0.519,1.161 -1.688,1.902 -2.959,1.875c-0.474,0.005 -0.943,-0.095 -1.375,-0.291c-1.507,-0.776 -2.195,-2.586 -1.583,-4.167l6.167,-13l-11.834,-2.833c-0.918,-0.216 -1.696,-0.826 -2.125,-1.667c-0.394,-0.859 -0.394,-1.849 0,-2.708l5.292,-13.5c-8.125,-9.084 -23.792,-9.959 -30.458,-0.959c-7.34,9.842 -6.304,23.732 2.416,32.375l29.417,29.459c1.578,1.424 4.005,1.424 5.583,0l29.459,-29.459c9.606,-9.709 9.568,-25.586 -0.084,-35.25Z" style="fill:#d40000;fill-rule:nonzero;"/><path d="M71.649,270.203c-8.333,-8.334 -25.917,-5.959 -31.542,9.041l-2.791,6.959l12.083,2.875c1.47,0.357 2.514,1.685 2.514,3.198c0,0.478 -0.104,0.951 -0.306,1.385l-7.875,16.667c-0.518,1.16 -1.687,1.901 -2.958,1.875c-0.474,0.004 -0.943,-0.096 -1.375,-0.292c-1.508,-0.776 -2.195,-2.585 -1.583,-4.167l6.166,-13l-11.833,-2.833c-0.918,-0.216 -1.697,-0.826 -2.125,-1.667c-0.394,-0.859 -0.394,-1.849 0,-2.708l5.292,-13.5c-8.125,-9.083 -23.792,-9.958 -30.459,-0.958c-7.34,9.841 -6.303,23.732 2.417,32.375l29.417,29.458c1.578,1.424 4.005,1.424 5.583,0l29.458,-29.458c9.607,-9.71 9.569,-25.587 -0.083,-35.25Z" style="fill:#d40000;fill-rule:nonzero;"/><path d="M71.649,4.685c-8.333,-8.334 -25.917,-5.959 -31.542,9.041l-2.791,6.959l12.083,2.875c1.47,0.357 2.514,1.685 2.514,3.198c0,0.479 -0.104,0.951 -0.306,1.385l-7.875,16.667c-0.518,1.161 -1.687,1.902 -2.958,1.875c-0.474,0.004 -0.943,-0.096 -1.375,-0.292c-1.508,-0.776 -2.195,-2.585 -1.583,-4.167l6.166,-13l-11.833,-2.833c-0.918,-0.216 -1.697,-0.826 -2.125,-1.667c-0.394,-0.859 -0.394,-1.849 0,-2.708l5.292,-13.5c-8.125,-9.083 -23.792,-9.958 -30.459,-0.958c-7.34,9.841 -6.303,23.732 2.417,32.375l29.417,29.458c1.578,1.424 4.005,1.424 5.583,0l29.458,-29.458c9.607,-9.71 9.569,-25.586 -0.083,-35.25Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="266.595" width="108.333" height="68.447" style="fill:#8e0000;"/><path d="M109.556,318.667l0,-35.834l208.055,0l0,35.834l-208.055,0m-17.889,16.666l239.361,0c2.453,0 4.472,-1.881 4.472,-4.166l0,-60.834c0,-2.285 -2.019,-4.166 -4.472,-4.166l-239.361,0l0,69.166Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="179.125" width="108.333" height="69.167" style="fill:#0b9d4d;"/><path d="M109.556,231.5l0,-35.667l208.055,0l0,35.792l-208.055,0m-17.889,16.667l239.361,0c2.453,0 4.472,-1.881 4.472,-4.167l0,-60.792c0,-2.285 -2.019,-4.166 -4.472,-4.166l-239.361,0l0,69.125Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="89.167" width="108.333" height="69.167" style="fill:#bdac03;"/><path d="M109.556,141.667l0,-35.834l208.055,0l0,35.834l-208.055,0m-17.889,16.666l239.361,0c2.453,0 4.472,-1.881 4.472,-4.166l0,-60.834c0,-2.285 -2.019,-4.166 -4.472,-4.166l-239.361,0l0,69.166Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="-0.072" width="108.333" height="68.405" style="fill:#8e0000;"/><path d="M109.556,52.428l0,-35.833l208.055,0l0,35.833l-208.055,0m-17.889,16.667l239.361,0c2.453,0 4.472,-1.881 4.472,-4.167l0,-60.833c0,-2.286 -2.019,-4.167 -4.472,-4.167l-239.361,0l0,69.167Z" style="fill:#d40000;fill-rule:nonzero;"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,9 +1,27 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { getNextRefIdChar } from './utils';
|
import { getNextRefIdChar } from './utils';
|
||||||
import { getDefaultTarget } from './triggers_panel_ctrl';
|
import { ShowProblemTypes } from '../datasource-zabbix/types';
|
||||||
|
|
||||||
// Actual schema version
|
// Actual schema version
|
||||||
export const CURRENT_SCHEMA_VERSION = 7;
|
export const CURRENT_SCHEMA_VERSION = 8;
|
||||||
|
|
||||||
|
export const getDefaultTarget = (targets?) => {
|
||||||
|
return {
|
||||||
|
group: {filter: ""},
|
||||||
|
host: {filter: ""},
|
||||||
|
application: {filter: ""},
|
||||||
|
trigger: {filter: ""},
|
||||||
|
tags: {filter: ""},
|
||||||
|
proxy: {filter: ""},
|
||||||
|
refId: getNextRefIdChar(targets),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDefaultTargetOptions() {
|
||||||
|
return {
|
||||||
|
hostsInMaintenance: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function migratePanelSchema(panel) {
|
export function migratePanelSchema(panel) {
|
||||||
if (isEmptyPanel(panel)) {
|
if (isEmptyPanel(panel)) {
|
||||||
@@ -12,7 +30,7 @@ export function migratePanelSchema(panel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const schemaVersion = getSchemaVersion(panel);
|
const schemaVersion = getSchemaVersion(panel);
|
||||||
panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
// panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||||
|
|
||||||
if (schemaVersion < 2) {
|
if (schemaVersion < 2) {
|
||||||
panel.datasources = [panel.datasource];
|
panel.datasources = [panel.datasource];
|
||||||
@@ -66,9 +84,70 @@ export function migratePanelSchema(panel) {
|
|||||||
delete panel.datasources;
|
delete panel.datasources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schemaVersion < 8) {
|
||||||
|
if (panel.targets.length === 1) {
|
||||||
|
if (panel.targets[0].datasource) {
|
||||||
|
panel.datasource = panel.targets[0].datasource;
|
||||||
|
delete panel.targets[0].datasource;
|
||||||
|
}
|
||||||
|
} else if (panel.targets.length > 1) {
|
||||||
|
// Mixed data sources
|
||||||
|
panel.datasource = '-- Mixed --';
|
||||||
|
}
|
||||||
|
for (const target of panel.targets) {
|
||||||
|
// set queryType to PROBLEMS
|
||||||
|
target.queryType = 5;
|
||||||
|
target.showProblems = migrateShowEvents(panel);
|
||||||
|
target.options = migrateOptions(panel);
|
||||||
|
|
||||||
|
_.defaults(target.options, getDefaultTargetOptions());
|
||||||
|
_.defaults(target, { tags: { filter: "" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange';
|
||||||
|
|
||||||
|
delete panel.showEvents;
|
||||||
|
delete panel.showTriggers;
|
||||||
|
delete panel.hostsInMaintenance;
|
||||||
|
delete panel.sortTriggersBy;
|
||||||
|
}
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateOptions(panel) {
|
||||||
|
let acknowledged = 2;
|
||||||
|
if (panel.showTriggers === 'acknowledged') {
|
||||||
|
acknowledged = 1;
|
||||||
|
} else if (panel.showTriggers === 'unacknowledged') {
|
||||||
|
acknowledged = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default limit in Zabbix
|
||||||
|
let limit = 1001;
|
||||||
|
if (panel.limit && panel.limit !== 100) {
|
||||||
|
limit = panel.limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostsInMaintenance: panel.hostsInMaintenance,
|
||||||
|
sortProblems: panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'default',
|
||||||
|
minSeverity: 0,
|
||||||
|
acknowledged: acknowledged,
|
||||||
|
limit: limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateShowEvents(panel) {
|
||||||
|
if (panel.showEvents?.value === 1) {
|
||||||
|
return ShowProblemTypes.Problems;
|
||||||
|
} else if (panel.showEvents?.value === 0 || panel.showEvents?.value?.length > 1) {
|
||||||
|
return ShowProblemTypes.History;
|
||||||
|
} else {
|
||||||
|
return ShowProblemTypes.Problems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSchemaVersion(panel) {
|
function getSchemaVersion(panel) {
|
||||||
return panel.schemaVersion || 1;
|
return panel.schemaVersion || 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
* Licensed under the Apache License, Version 2.0
|
* Licensed under the Apache License, Version 2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TriggerPanelCtrl} from './triggers_panel_ctrl';
|
import { TriggerPanelCtrl } from './triggers_panel_ctrl';
|
||||||
import {loadPluginCss} from 'grafana/app/plugins/sdk';
|
import { loadPluginCss } from 'grafana/app/plugins/sdk';
|
||||||
import './datasource-selector.directive';
|
|
||||||
|
|
||||||
loadPluginCss({
|
loadPluginCss({
|
||||||
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
|
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ class TriggerPanelOptionsCtrl {
|
|||||||
'unacknowledged',
|
'unacknowledged',
|
||||||
'acknowledged'
|
'acknowledged'
|
||||||
];
|
];
|
||||||
this.sortByFields = [
|
|
||||||
{ text: 'last change', value: 'lastchange' },
|
this.sortingOptions = [
|
||||||
{ text: 'severity', value: 'priority' }
|
{ text: 'Default', value: 'default' },
|
||||||
|
{ text: 'Last change', value: 'lastchange' },
|
||||||
|
{ text: 'Severity', value: 'priority' },
|
||||||
];
|
];
|
||||||
|
|
||||||
this.showEventsFields = [
|
this.showEventsFields = [
|
||||||
{ text: 'All', value: [0,1] },
|
{ text: 'All', value: [0,1] },
|
||||||
{ text: 'OK', value: [0] },
|
{ text: 'OK', value: [0] },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="editor-row">
|
<div class="editor-row">
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Show fields</h5>
|
<h5 class="section-heading">Fields</h5>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
label="Host name"
|
label="Host name"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
label="Host proxy"
|
label="Host proxy"
|
||||||
checked="ctrl.panel.hostProxy"
|
checked="ctrl.panel.hostProxy"
|
||||||
on-change="ctrl.refresh()">
|
on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
@@ -50,6 +50,12 @@
|
|||||||
checked="ctrl.panel.severityField"
|
checked="ctrl.panel.severityField"
|
||||||
on-change="ctrl.render()">
|
on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
|
<gf-form-switch class="gf-form"
|
||||||
|
label-class="width-9"
|
||||||
|
label="Ack"
|
||||||
|
checked="ctrl.panel.ackField"
|
||||||
|
on-change="ctrl.render()">
|
||||||
|
</gf-form-switch>
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label-class="width-9"
|
label-class="width-9"
|
||||||
label="Age"
|
label="Age"
|
||||||
@@ -72,53 +78,6 @@
|
|||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<h5 class="section-heading">Options</h5>
|
|
||||||
<gf-form-switch class="gf-form"
|
|
||||||
label-class="width-15"
|
|
||||||
label="Show hosts in maintenance"
|
|
||||||
checked="ctrl.panel.hostsInMaintenance"
|
|
||||||
on-change="ctrl.render()">
|
|
||||||
</gf-form-switch>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Acknowledged</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-model="ctrl.panel.showTriggers"
|
|
||||||
ng-options="f for f in editor.ackFilters"
|
|
||||||
ng-change="ctrl.refresh()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Sort by</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-model="ctrl.panel.sortTriggersBy"
|
|
||||||
ng-options="f.text for f in editor.sortByFields track by f.value"
|
|
||||||
ng-change="ctrl.render()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Show events</label>
|
|
||||||
<div class="gf-form-select-wrapper width-12">
|
|
||||||
<select class="gf-form-input"
|
|
||||||
ng-model="ctrl.panel.showEvents"
|
|
||||||
ng-options="f.text for f in editor.showEventsFields track by f.value"
|
|
||||||
ng-change="ctrl.refresh()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-8">Limit triggers</label>
|
|
||||||
<input class="gf-form-input width-5"
|
|
||||||
type="number" placeholder="100"
|
|
||||||
ng-model="ctrl.panel.limit"
|
|
||||||
ng-model-onblur ng-change="ctrl.refresh()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">View options</h5>
|
<h5 class="section-heading">View options</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
@@ -130,6 +89,16 @@
|
|||||||
ng-change="ctrl.render()"></select>
|
ng-change="ctrl.render()"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-10">Sort by</label>
|
||||||
|
<div class="gf-form-select-wrapper max-width-8">
|
||||||
|
<select class="gf-form-input"
|
||||||
|
ng-model="ctrl.panel.sortProblems"
|
||||||
|
ng-options="f.value as f.text for f in editor.sortingOptions"
|
||||||
|
ng-change="ctrl.reRenderProblems()">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-10">Font size</label>
|
<label class="gf-form-label width-10">Font size</label>
|
||||||
<div class="gf-form-select-wrapper max-width-8">
|
<div class="gf-form-select-wrapper max-width-8">
|
||||||
@@ -202,7 +171,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Triggers severity and colors</h5>
|
<h5 class="section-heading">Problems severity and colors</h5>
|
||||||
<div class="gf-form-inline" ng-repeat="trigger in ctrl.panel.triggerSeverity">
|
<div class="gf-form-inline" ng-repeat="trigger in ctrl.panel.triggerSeverity">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-3">{{ trigger.priority }}</label>
|
<label class="gf-form-label width-3">{{ trigger.priority }}</label>
|
||||||
@@ -224,7 +193,7 @@
|
|||||||
label-class="width-0"
|
label-class="width-0"
|
||||||
label="Show"
|
label="Show"
|
||||||
checked="trigger.show"
|
checked="trigger.show"
|
||||||
on-change="ctrl.refresh()">
|
on-change="ctrl.reRenderProblems()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -246,7 +215,7 @@
|
|||||||
label-class="width-0"
|
label-class="width-0"
|
||||||
label="Show"
|
label="Show"
|
||||||
checked="ctrl.panel.markAckEvents"
|
checked="ctrl.panel.markAckEvents"
|
||||||
on-change="ctrl.refresh()">
|
on-change="ctrl.reRenderProblems()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
<div class="editor-row">
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-9">Data sources</label>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form">
|
|
||||||
<datasource-selector
|
|
||||||
datasources="editor.selectedDatasources"
|
|
||||||
options="editor.panelCtrl.available_datasources"
|
|
||||||
on-change="editor.datasourcesChanged()">
|
|
||||||
</datasource-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-row" ng-repeat="target in ctrl.panel.targets">
|
|
||||||
<div class="section gf-form-group">
|
|
||||||
<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="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(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="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(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="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(target.proxy.filter),
|
|
||||||
'zbx-regex': editor.isRegex(target.proxy.filter)
|
|
||||||
}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label query-keyword width-7">Application</label>
|
|
||||||
<input type="text"
|
|
||||||
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(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="target.trigger.filter"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
placeholder="trigger name"
|
|
||||||
class="gf-form-input width-14"
|
|
||||||
ng-style="target.trigger.style"
|
|
||||||
ng-class="{
|
|
||||||
'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="target.tags.filter"
|
|
||||||
ng-blur="editor.parseTarget()"
|
|
||||||
placeholder="tag1:value1, tag2:value2">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -3,13 +3,14 @@
|
|||||||
"name": "Zabbix Problems",
|
"name": "Zabbix Problems",
|
||||||
"id": "alexanderzobnin-zabbix-triggers-panel",
|
"id": "alexanderzobnin-zabbix-triggers-panel",
|
||||||
|
|
||||||
"dataFormats": [],
|
|
||||||
"skipDataQuery": true,
|
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alexander Zobnin",
|
"name": "Alexander Zobnin",
|
||||||
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
|
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/icn-zabbix-problems-panel.svg",
|
||||||
|
"large": "img/icn-zabbix-problems-panel.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mocks from '../../test-setup/mocks';
|
import mocks from '../../test-setup/mocks';
|
||||||
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
|
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
|
||||||
import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl';
|
import { DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS } from '../triggers_panel_ctrl';
|
||||||
import {CURRENT_SCHEMA_VERSION} from '../migrations';
|
import { CURRENT_SCHEMA_VERSION } from '../migrations';
|
||||||
|
|
||||||
describe('Triggers Panel schema migration', () => {
|
jest.mock('@grafana/runtime', () => {
|
||||||
let ctx: any = {};
|
return {
|
||||||
let updatePanelCtrl;
|
getDataSourceSrv: () => ({
|
||||||
const datasourceSrvMock = {
|
|
||||||
getMetricSources: () => {
|
getMetricSources: () => {
|
||||||
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
|
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
|
||||||
},
|
},
|
||||||
get: () => Promise.resolve({})
|
get: () => Promise.resolve({})
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
}, {virtual: true});
|
||||||
|
|
||||||
|
describe('Triggers Panel schema migration', () => {
|
||||||
|
let ctx: any = {};
|
||||||
|
let updatePanelCtrl;
|
||||||
|
|
||||||
const timeoutMock = () => {};
|
const timeoutMock = () => {};
|
||||||
|
|
||||||
@@ -29,8 +34,9 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
ageField: true,
|
ageField: true,
|
||||||
infoField: true,
|
infoField: true,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
showTriggers: 'all triggers',
|
showTriggers: 'unacknowledged',
|
||||||
hideHostsInMaintenance: false,
|
hideHostsInMaintenance: false,
|
||||||
|
hostsInMaintenance: false,
|
||||||
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
||||||
showEvents: { text: 'Problems', value: '1' },
|
showEvents: { text: 'Problems', value: '1' },
|
||||||
triggerSeverity: DEFAULT_SEVERITY,
|
triggerSeverity: DEFAULT_SEVERITY,
|
||||||
@@ -43,7 +49,7 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock);
|
updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update old panel schema', () => {
|
it('should update old panel schema', () => {
|
||||||
@@ -51,12 +57,22 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
|
|
||||||
const expected = _.defaultsDeep({
|
const expected = _.defaultsDeep({
|
||||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||||
|
datasource: 'zabbix',
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
...DEFAULT_TARGET,
|
...DEFAULT_TARGET,
|
||||||
datasource: 'zabbix',
|
queryType: 5,
|
||||||
|
showProblems: 'problems',
|
||||||
|
options: {
|
||||||
|
hostsInMaintenance: false,
|
||||||
|
acknowledged: 0,
|
||||||
|
sortProblems: 'default',
|
||||||
|
minSeverity: 0,
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
sortProblems: 'lastchange',
|
||||||
ageField: true,
|
ageField: true,
|
||||||
statusField: false,
|
statusField: false,
|
||||||
severityField: false,
|
severityField: false,
|
||||||
@@ -74,27 +90,7 @@ describe('Triggers Panel schema migration', () => {
|
|||||||
|
|
||||||
const expected = _.defaultsDeep({
|
const expected = _.defaultsDeep({
|
||||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||||
targets: [{
|
|
||||||
...DEFAULT_TARGET,
|
|
||||||
datasource: 'zabbix_default'
|
|
||||||
}]
|
|
||||||
}, PANEL_DEFAULTS);
|
}, PANEL_DEFAULTS);
|
||||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
expect(updatedPanelCtrl.panel).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set default targets for new panel with empty targets', () => {
|
|
||||||
ctx.scope.panel = {
|
|
||||||
targets: []
|
|
||||||
};
|
|
||||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
|
||||||
|
|
||||||
const expected = _.defaultsDeep({
|
|
||||||
targets: [{
|
|
||||||
...DEFAULT_TARGET,
|
|
||||||
datasource: 'zabbix_default'
|
|
||||||
}]
|
|
||||||
}, PANEL_DEFAULTS);
|
|
||||||
|
|
||||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,92 +1,55 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mocks from '../../test-setup/mocks';
|
import { TriggerPanelCtrl } from '../triggers_panel_ctrl';
|
||||||
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
|
import { PANEL_DEFAULTS, DEFAULT_TARGET } from '../triggers_panel_ctrl';
|
||||||
import {PANEL_DEFAULTS, DEFAULT_TARGET} from '../triggers_panel_ctrl';
|
|
||||||
// import { create } from 'domain';
|
let datasourceSrvMock, zabbixDSMock;
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => {
|
||||||
|
return {
|
||||||
|
getDataSourceSrv: () => datasourceSrvMock,
|
||||||
|
};
|
||||||
|
}, {virtual: true});
|
||||||
|
|
||||||
describe('TriggerPanelCtrl', () => {
|
describe('TriggerPanelCtrl', () => {
|
||||||
let ctx: any = {};
|
let ctx: any = {};
|
||||||
let datasourceSrvMock, zabbixDSMock;
|
let createPanelCtrl: () => any;
|
||||||
const timeoutMock = () => {};
|
|
||||||
let createPanelCtrl;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx = {scope: {panel: PANEL_DEFAULTS}};
|
ctx = { scope: {
|
||||||
|
panel: {
|
||||||
|
...PANEL_DEFAULTS,
|
||||||
|
sortProblems: 'lastchange',
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
ctx.scope.panel.targets = [{
|
||||||
|
...DEFAULT_TARGET,
|
||||||
|
datasource: 'zabbix_default',
|
||||||
|
}];
|
||||||
|
|
||||||
zabbixDSMock = {
|
zabbixDSMock = {
|
||||||
replaceTemplateVars: () => {},
|
|
||||||
zabbix: {
|
zabbix: {
|
||||||
getTriggers: jest.fn().mockReturnValue([generateTrigger("1"), generateTrigger("1")]),
|
|
||||||
getExtendedEventData: jest.fn().mockResolvedValue([]),
|
getExtendedEventData: jest.fn().mockResolvedValue([]),
|
||||||
getEventAlerts: jest.fn().mockResolvedValue([]),
|
getEventAlerts: jest.fn().mockResolvedValue([]),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
datasourceSrvMock = {
|
datasourceSrvMock = {
|
||||||
getMetricSources: () => {
|
|
||||||
return [
|
|
||||||
{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' },
|
|
||||||
{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix' },
|
|
||||||
{ meta: {id: 'graphite'}, value: {}, name: 'graphite' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
get: () => Promise.resolve(zabbixDSMock)
|
get: () => Promise.resolve(zabbixDSMock)
|
||||||
};
|
};
|
||||||
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock);
|
|
||||||
|
|
||||||
const getTriggersResp = [
|
const timeoutMock = (fn: () => any) => Promise.resolve(fn());
|
||||||
[
|
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock);
|
||||||
createTrigger({
|
|
||||||
triggerid: "1", lastchange: "1510000010", priority: 5, lastEvent: {eventid: "11"}, hosts: [{maintenance_status: '1'}]
|
|
||||||
}),
|
|
||||||
createTrigger({
|
|
||||||
triggerid: "2", lastchange: "1510000040", priority: 3, lastEvent: {eventid: "12"}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
createTrigger({triggerid: "3", lastchange: "1510000020", priority: 4, lastEvent: {eventid: "13"}}),
|
|
||||||
createTrigger({triggerid: "4", lastchange: "1510000030", priority: 2, lastEvent: {eventid: "14"}}),
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Simulate 2 data sources
|
|
||||||
zabbixDSMock.zabbix.getTriggers = jest.fn()
|
|
||||||
.mockReturnValueOnce(getTriggersResp[0])
|
|
||||||
.mockReturnValueOnce(getTriggersResp[1]);
|
|
||||||
zabbixDSMock.zabbix.getExtendedEventData = jest.fn()
|
|
||||||
.mockReturnValue(Promise.resolve([defaultEvent]));
|
|
||||||
|
|
||||||
ctx.panelCtrl = createPanelCtrl();
|
ctx.panelCtrl = createPanelCtrl();
|
||||||
});
|
|
||||||
|
|
||||||
describe('When adding new panel', () => {
|
ctx.dataFramesReceived = generateDataFramesResponse([
|
||||||
it('should suggest all zabbix data sources', () => {
|
{id: "1", lastchange: "1510000010", priority: 5},
|
||||||
ctx.scope.panel = {};
|
{id: "2", lastchange: "1510000040", priority: 3},
|
||||||
const panelCtrl = createPanelCtrl();
|
{id: "3", lastchange: "1510000020", priority: 4},
|
||||||
expect(panelCtrl.available_datasources).toEqual([
|
{id: "4", lastchange: "1510000030", priority: 2},
|
||||||
'zabbix_default', 'zabbix'
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load first zabbix data source as default', () => {
|
|
||||||
ctx.scope.panel = {};
|
|
||||||
const panelCtrl = createPanelCtrl();
|
|
||||||
expect(panelCtrl.panel.targets[0].datasource).toEqual('zabbix_default');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should rewrite default empty target', () => {
|
|
||||||
ctx.scope.panel = {
|
|
||||||
targets: [{
|
|
||||||
"target": "",
|
|
||||||
"refId": "A"
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
const panelCtrl = createPanelCtrl();
|
|
||||||
expect(panelCtrl.available_datasources).toEqual([
|
|
||||||
'zabbix_default', 'zabbix'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When refreshing panel', () => {
|
describe('When refreshing panel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
|
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
|
||||||
@@ -104,8 +67,8 @@ describe('TriggerPanelCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should format triggers', (done) => {
|
it('should format triggers', (done) => {
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||||
const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
|
const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, {triggerid: "1"});
|
||||||
expect(formattedTrigger.host).toBe('backend01');
|
expect(formattedTrigger.host).toBe('backend01');
|
||||||
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
|
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
|
||||||
expect(formattedTrigger.datasource).toBe('zabbix_default');
|
expect(formattedTrigger.datasource).toBe('zabbix_default');
|
||||||
@@ -116,8 +79,8 @@ describe('TriggerPanelCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sort triggers by time by default', (done) => {
|
it('should sort triggers by time by default', (done) => {
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||||
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
|
||||||
expect(trigger_ids).toEqual([
|
expect(trigger_ids).toEqual([
|
||||||
'2', '4', '3', '1'
|
'2', '4', '3', '1'
|
||||||
]);
|
]);
|
||||||
@@ -126,175 +89,119 @@ describe('TriggerPanelCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sort triggers by severity', (done) => {
|
it('should sort triggers by severity', (done) => {
|
||||||
ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' };
|
ctx.panelCtrl.panel.sortProblems = 'priority';
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||||
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
|
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
|
||||||
expect(trigger_ids).toEqual([
|
expect(trigger_ids).toEqual([
|
||||||
'1', '3', '2', '4'
|
'1', '3', '2', '4'
|
||||||
]);
|
]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add acknowledges to trigger', (done) => {
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
expect(trigger.acknowledges).toHaveLength(1);
|
|
||||||
expect(trigger.acknowledges[0].message).toBe("event ack");
|
|
||||||
|
|
||||||
expect(getTriggerById(2, ctx).acknowledges).toBe(undefined);
|
|
||||||
expect(getTriggerById(3, ctx).acknowledges).toBe(undefined);
|
|
||||||
expect(getTriggerById(4, ctx).acknowledges).toBe(undefined);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When formatting triggers', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx.panelCtrl = createPanelCtrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle new lines in trigger description', () => {
|
|
||||||
ctx.panelCtrl.setTriggerSeverity = jest.fn((trigger) => trigger);
|
|
||||||
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(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe('backend01');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format host name to display (tech name)', (done) => {
|
|
||||||
ctx.panelCtrl.panel.hostField = false;
|
|
||||||
ctx.panelCtrl.panel.hostTechNameField = true;
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe('backend01_tech');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format host name to display (both tech and visible)', (done) => {
|
|
||||||
ctx.panelCtrl.panel.hostField = true;
|
|
||||||
ctx.panelCtrl.panel.hostTechNameField = true;
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe('backend01 (backend01_tech)');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should hide hostname if both visible and tech name checkboxes unset', (done) => {
|
|
||||||
ctx.panelCtrl.panel.hostField = false;
|
|
||||||
ctx.panelCtrl.panel.hostTechNameField = false;
|
|
||||||
ctx.panelCtrl.onRefresh().then(() => {
|
|
||||||
const trigger = getTriggerById(1, ctx);
|
|
||||||
const hostname = ctx.panelCtrl.formatHostName(trigger);
|
|
||||||
expect(hostname).toBe("");
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When formatting acknowledges', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx.panelCtrl = createPanelCtrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should build proper user name', () => {
|
|
||||||
const ack = {
|
|
||||||
alias: 'alias', name: 'name', surname: 'surname'
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatted = ctx.panelCtrl.formatAcknowledge(ack);
|
|
||||||
expect(formatted.user).toBe('alias (name surname)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty name if it is not defined', () => {
|
|
||||||
const formatted = ctx.panelCtrl.formatAcknowledge({});
|
|
||||||
expect(formatted.user).toBe('');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultTrigger: any = {
|
const defaultProblem: any = {
|
||||||
"triggerid": "13565",
|
"acknowledges": [],
|
||||||
"value": "1",
|
|
||||||
"groups": [{"groupid": "1", "name": "Backend"}] ,
|
|
||||||
"hosts": [{"host": "backend01_tech", "hostid": "10001","maintenance_status": "0", "name": "backend01"}] ,
|
|
||||||
"lastEvent": {
|
|
||||||
"eventid": "11",
|
|
||||||
"clock": "1507229064",
|
|
||||||
"ns": "556202037",
|
|
||||||
"acknowledged": "1",
|
|
||||||
"value": "1",
|
|
||||||
"object": "0",
|
|
||||||
"source": "0",
|
|
||||||
"objectid": "13565",
|
|
||||||
},
|
|
||||||
"tags": [] ,
|
|
||||||
"lastchange": "1440259530",
|
|
||||||
"priority": "2",
|
|
||||||
"description": "Lack of free swap space on server",
|
|
||||||
"comments": "It probably means that the systems requires\nmore physical memory.",
|
"comments": "It probably means that the systems requires\nmore physical memory.",
|
||||||
"url": "https://host.local/path",
|
"correlation_mode": "0",
|
||||||
"templateid": "0", "expression": "{13174}<50", "manual_close": "0", "correlation_mode": "0",
|
"correlation_tag": "",
|
||||||
"correlation_tag": "", "recovery_mode": "0", "recovery_expression": "", "state": "0", "status": "0",
|
"datasource": "zabbix_default",
|
||||||
"flags": "0", "type": "0", "items": [] , "error": ""
|
"description": "Lack of free swap space on server",
|
||||||
};
|
"error": "",
|
||||||
|
"expression": "{13297}>20",
|
||||||
const defaultEvent: any = {
|
"flags": "0",
|
||||||
"eventid": "11",
|
"groups": [
|
||||||
"acknowledges": [
|
|
||||||
{
|
{
|
||||||
"acknowledgeid": "185",
|
"groupid": "2",
|
||||||
"action": "0",
|
"name": "Linux servers"
|
||||||
"alias": "api",
|
},
|
||||||
"clock": "1512382246",
|
{
|
||||||
"eventid": "11",
|
"groupid": "9",
|
||||||
"message": "event ack",
|
"name": "Backend"
|
||||||
"name": "api",
|
|
||||||
"surname": "user",
|
|
||||||
"userid": "3"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"clock": "1507229064",
|
"hosts": [
|
||||||
"ns": "556202037",
|
{
|
||||||
"acknowledged": "1",
|
"host": "backend01_tech",
|
||||||
"value": "1",
|
"hostid": "10111",
|
||||||
|
"maintenance_status": "1",
|
||||||
|
"name": "backend01",
|
||||||
|
"proxy_hostid": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemid": "23979",
|
||||||
|
"key_": "system.cpu.util[,iowait]",
|
||||||
|
"lastvalue": "25.2091",
|
||||||
|
"name": "CPU $2 time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastEvent": {
|
||||||
|
"acknowledged": "0",
|
||||||
|
"clock": "1589297010",
|
||||||
|
"eventid": "4399289",
|
||||||
|
"name": "Disk I/O is overloaded on backend01",
|
||||||
|
"ns": "224779201",
|
||||||
"object": "0",
|
"object": "0",
|
||||||
|
"objectid": "13682",
|
||||||
|
"severity": "2",
|
||||||
"source": "0",
|
"source": "0",
|
||||||
"objectid": "1",
|
"value": "1"
|
||||||
|
},
|
||||||
|
"lastchange": "1440259530",
|
||||||
|
"maintenance": true,
|
||||||
|
"manual_close": "0",
|
||||||
|
"priority": "2",
|
||||||
|
"recovery_expression": "",
|
||||||
|
"recovery_mode": "0",
|
||||||
|
"showAckButton": true,
|
||||||
|
"state": "0",
|
||||||
|
"status": "0",
|
||||||
|
"tags": [],
|
||||||
|
"templateid": "13671",
|
||||||
|
"triggerid": "13682",
|
||||||
|
"type": "0",
|
||||||
|
"url": "",
|
||||||
|
"value": "1"
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateTrigger(id, timestamp?, severity?): any {
|
function generateDataFramesResponse(problemDescs: any[] = [{id: 1}]): any {
|
||||||
const trigger = _.cloneDeep(defaultTrigger);
|
const problems = problemDescs.map(problem => generateProblem(problem.id, problem.lastchange, problem.priority));
|
||||||
trigger.triggerid = id.toString();
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"name": "Problems",
|
||||||
|
"state": {
|
||||||
|
"scopedVars": {},
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"type": "other",
|
||||||
|
"values": problems,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"length": 16,
|
||||||
|
"name": "problems"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateProblem(id, timestamp?, severity?): any {
|
||||||
|
const problem = _.cloneDeep(defaultProblem);
|
||||||
|
problem.triggerid = id.toString();
|
||||||
if (severity) {
|
if (severity) {
|
||||||
trigger.priority = severity.toString();
|
problem.priority = severity.toString();
|
||||||
}
|
}
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
trigger.lastchange = timestamp;
|
problem.lastchange = timestamp;
|
||||||
}
|
}
|
||||||
return trigger;
|
return problem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrigger(props): any {
|
function getProblemById(id, ctx): any {
|
||||||
let trigger = _.cloneDeep(defaultTrigger);
|
return _.find(ctx.panelCtrl.renderData, {triggerid: id.toString()});
|
||||||
trigger = _.merge(trigger, props);
|
|
||||||
trigger.lastEvent.objectid = trigger.triggerid;
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTriggerById(id, ctx): any {
|
|
||||||
return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,725 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
|
||||||
import * as utils from '../datasource-zabbix/utils';
|
|
||||||
import { PanelCtrl } from 'grafana/app/plugins/sdk';
|
|
||||||
import { triggerPanelOptionsTab } from './options_tab';
|
|
||||||
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;
|
|
||||||
|
|
||||||
export const DEFAULT_TARGET = {
|
|
||||||
group: {filter: ""},
|
|
||||||
host: {filter: ""},
|
|
||||||
application: {filter: ""},
|
|
||||||
trigger: {filter: ""},
|
|
||||||
tags: {filter: ""},
|
|
||||||
proxy: {filter: ""},
|
|
||||||
};
|
|
||||||
|
|
||||||
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},
|
|
||||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true},
|
|
||||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true},
|
|
||||||
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true},
|
|
||||||
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true},
|
|
||||||
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
|
||||||
|
|
||||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
|
||||||
|
|
||||||
export const PANEL_DEFAULTS = {
|
|
||||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
||||||
targets: [getDefaultTarget([])],
|
|
||||||
// Fields
|
|
||||||
hostField: true,
|
|
||||||
hostTechNameField: false,
|
|
||||||
hostGroups: false,
|
|
||||||
hostProxy: false,
|
|
||||||
showTags: true,
|
|
||||||
statusField: true,
|
|
||||||
statusIcon: false,
|
|
||||||
severityField: true,
|
|
||||||
ageField: false,
|
|
||||||
descriptionField: true,
|
|
||||||
descriptionAtNewLine: false,
|
|
||||||
// Options
|
|
||||||
hostsInMaintenance: true,
|
|
||||||
showTriggers: 'all triggers',
|
|
||||||
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
|
||||||
showEvents: { text: 'Problems', value: 1 },
|
|
||||||
limit: 100,
|
|
||||||
// View options
|
|
||||||
layout: 'table',
|
|
||||||
fontSize: '100%',
|
|
||||||
pageSize: 10,
|
|
||||||
problemTimeline: true,
|
|
||||||
highlightBackground: false,
|
|
||||||
highlightNewEvents: false,
|
|
||||||
highlightNewerThan: '1h',
|
|
||||||
customLastChangeFormat: false,
|
|
||||||
lastChangeFormat: "",
|
|
||||||
resizedColumns: [],
|
|
||||||
// Triggers severity and colors
|
|
||||||
triggerSeverity: getDefaultSeverity(),
|
|
||||||
okEventColor: 'rgb(56, 189, 113)',
|
|
||||||
ackEventColor: 'rgb(56, 219, 156)',
|
|
||||||
markAckEvents: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerStatusMap = {
|
|
||||||
'0': 'OK',
|
|
||||||
'1': 'PROBLEM'
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TriggerPanelCtrl extends PanelCtrl {
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope, $injector, $timeout, datasourceSrv, templateSrv, contextSrv, dashboardSrv, timeSrv) {
|
|
||||||
super($scope, $injector);
|
|
||||||
this.datasourceSrv = datasourceSrv;
|
|
||||||
this.templateSrv = templateSrv;
|
|
||||||
this.contextSrv = contextSrv;
|
|
||||||
this.dashboardSrv = dashboardSrv;
|
|
||||||
this.timeSrv = timeSrv;
|
|
||||||
this.scope = $scope;
|
|
||||||
this.$timeout = $timeout;
|
|
||||||
|
|
||||||
this.editorTabIndex = 1;
|
|
||||||
this.triggerStatusMap = triggerStatusMap;
|
|
||||||
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
|
||||||
this.pageIndex = 0;
|
|
||||||
this.triggerList = [];
|
|
||||||
this.datasources = {};
|
|
||||||
this.range = {};
|
|
||||||
|
|
||||||
this.panel = migratePanelSchema(this.panel);
|
|
||||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
|
||||||
|
|
||||||
this.available_datasources = _.map(this.getZabbixDataSources(), 'name');
|
|
||||||
if (this.panel.targets && !this.panel.targets[0].datasource) {
|
|
||||||
this.panel.targets[0].datasource = this.available_datasources[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initDatasources();
|
|
||||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
|
||||||
this.events.on('refresh', this.onRefresh.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanelError(err, defaultError) {
|
|
||||||
const defaultErrorMessage = defaultError || "Request Error";
|
|
||||||
this.inspector = { error: err };
|
|
||||||
this.error = err.message || defaultErrorMessage;
|
|
||||||
if (err.data) {
|
|
||||||
if (err.data.message) {
|
|
||||||
this.error = err.data.message;
|
|
||||||
}
|
|
||||||
if (err.data.error) {
|
|
||||||
this.error = err.data.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.events.emit('data-error', err);
|
|
||||||
console.log('Panel data error:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
initDatasources() {
|
|
||||||
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 => {
|
|
||||||
this.datasources[ds] = datasource;
|
|
||||||
return datasource;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
getZabbixDataSources() {
|
|
||||||
return _.filter(this.datasourceSrv.getMetricSources(), datasource => {
|
|
||||||
return datasource.meta.id === ZABBIX_DS_ID && datasource.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmptyTargets() {
|
|
||||||
const emptyTargets = _.isEmpty(this.panel.targets);
|
|
||||||
const emptyTarget = (this.panel.targets.length === 1 && (
|
|
||||||
_.isEmpty(this.panel.targets[0]) ||
|
|
||||||
this.panel.targets[0].target === ""
|
|
||||||
));
|
|
||||||
return emptyTargets || emptyTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
onInitEditMode() {
|
|
||||||
this.addEditorTab('Triggers', triggerPanelTriggersTab, 1);
|
|
||||||
this.addEditorTab('Options', triggerPanelOptionsTab, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeQueryStart() {
|
|
||||||
this.timing.queryStart = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeQueryEnd() {
|
|
||||||
this.timing.queryEnd = (new Date()).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRefresh() {
|
|
||||||
// ignore fetching data if another panel is in fullscreen
|
|
||||||
if (this.otherPanelInFullscreenMode()) { return; }
|
|
||||||
|
|
||||||
this.range = this.timeSrv.timeRange();
|
|
||||||
|
|
||||||
// clear loading/error state
|
|
||||||
delete this.error;
|
|
||||||
this.loading = true;
|
|
||||||
this.setTimeQueryStart();
|
|
||||||
this.pageIndex = 0;
|
|
||||||
|
|
||||||
return this.getTriggers()
|
|
||||||
.then(triggers => {
|
|
||||||
// Notify panel that request is finished
|
|
||||||
this.loading = false;
|
|
||||||
this.setTimeQueryEnd();
|
|
||||||
return this.renderTriggers(triggers);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$timeout(() => {
|
|
||||||
this.renderingCompleted();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
if (err.cancelled) {
|
|
||||||
console.log('Panel request cancelled', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setPanelError(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTriggers(zabbixTriggers) {
|
|
||||||
let triggers = _.cloneDeep(zabbixTriggers || this.triggerListUnfiltered);
|
|
||||||
this.triggerListUnfiltered = _.cloneDeep(triggers);
|
|
||||||
|
|
||||||
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
|
||||||
triggers = this.filterTriggersPost(triggers);
|
|
||||||
triggers = this.sortTriggers(triggers);
|
|
||||||
|
|
||||||
// Limit triggers number
|
|
||||||
triggers = triggers.slice(0, this.panel.limit || PANEL_DEFAULTS.limit);
|
|
||||||
|
|
||||||
this.triggerList = triggers;
|
|
||||||
|
|
||||||
return this.$timeout(() => {
|
|
||||||
return super.render(this.triggerList);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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 = 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);
|
|
||||||
const hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter);
|
|
||||||
const appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter);
|
|
||||||
const proxyFilter = datasource.replaceTemplateVars(triggerFilter.proxy.filter);
|
|
||||||
|
|
||||||
let triggersOptions = {
|
|
||||||
showTriggers: showEvents
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showEvents !== 1) {
|
|
||||||
triggersOptions.timeFrom = timeFrom;
|
|
||||||
triggersOptions.timeTo = timeTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions, proxyFilter),
|
|
||||||
getProxiesPromise
|
|
||||||
]);
|
|
||||||
}).then(([triggers, sourceProxies]) => {
|
|
||||||
proxies = _.keyBy(sourceProxies, 'proxyid');
|
|
||||||
const eventids = _.compact(triggers.map(trigger => {
|
|
||||||
return trigger.lastEvent.eventid;
|
|
||||||
}));
|
|
||||||
return Promise.all([
|
|
||||||
this.datasources[ds].zabbix.getExtendedEventData(eventids),
|
|
||||||
Promise.resolve(triggers)
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
.then(([events, triggers]) => {
|
|
||||||
this.addEventTags(events, triggers);
|
|
||||||
this.addAcknowledges(events, triggers);
|
|
||||||
return triggers;
|
|
||||||
})
|
|
||||||
.then(triggers => this.setMaintenanceStatus(triggers))
|
|
||||||
.then(triggers => this.setAckButtonStatus(triggers, showAckButton))
|
|
||||||
.then(triggers => this.filterTriggersPre(triggers, target))
|
|
||||||
.then(triggers => this.addTriggerDataSource(triggers, target))
|
|
||||||
.then(triggers => this.addTriggerHostProxy(triggers, proxies));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(promises)
|
|
||||||
.then(results => _.flatten(results));
|
|
||||||
}
|
|
||||||
|
|
||||||
addAcknowledges(events, triggers) {
|
|
||||||
// Map events to triggers
|
|
||||||
_.each(triggers, trigger => {
|
|
||||||
var event = _.find(events, event => {
|
|
||||||
return event.eventid === trigger.lastEvent.eventid;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
trigger.acknowledges = _.map(event.acknowledges, this.formatAcknowledge.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!trigger.lastEvent.eventid) {
|
|
||||||
trigger.lastEvent = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatAcknowledge(ack) {
|
|
||||||
let timestamp = moment.unix(ack.clock);
|
|
||||||
if (this.panel.customLastChangeFormat) {
|
|
||||||
ack.time = timestamp.format(this.panel.lastChangeFormat);
|
|
||||||
} else {
|
|
||||||
ack.time = timestamp.format(this.defaultTimeFormat);
|
|
||||||
}
|
|
||||||
ack.user = ack.alias || '';
|
|
||||||
if (ack.name || ack.surname) {
|
|
||||||
const fullName = `${ack.name || ''} ${ack.surname || ''}`;
|
|
||||||
ack.user += ` (${fullName})`;
|
|
||||||
}
|
|
||||||
return ack;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventTags(events, triggers) {
|
|
||||||
_.each(triggers, trigger => {
|
|
||||||
var event = _.find(events, event => {
|
|
||||||
return event.eventid === trigger.lastEvent.eventid;
|
|
||||||
});
|
|
||||||
if (event && event.tags && event.tags.length) {
|
|
||||||
trigger.tags = event.tags;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterTriggersPre(triggerList, target) {
|
|
||||||
// Filter triggers by description
|
|
||||||
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];
|
|
||||||
if (target.tags.filter) {
|
|
||||||
let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter);
|
|
||||||
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
|
||||||
tagsFilter = tagsFilter.replace('/^', '').replace('$/', '');
|
|
||||||
const tags = this.parseTags(tagsFilter);
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return _.every(tags, (tag) => {
|
|
||||||
return _.find(trigger.tags, {tag: tag.tag, value: tag.value});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return triggerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterTriggersPost(triggers) {
|
|
||||||
let triggerList = _.cloneDeep(triggers);
|
|
||||||
|
|
||||||
// Filter acknowledged triggers
|
|
||||||
if (this.panel.showTriggers === 'unacknowledged') {
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return !(trigger.acknowledges && trigger.acknowledges.length);
|
|
||||||
});
|
|
||||||
} else if (this.panel.showTriggers === 'acknowledged') {
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return trigger.acknowledges && trigger.acknowledges.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by maintenance status
|
|
||||||
if (!this.panel.hostsInMaintenance) {
|
|
||||||
triggerList = _.filter(triggerList, (trigger) => trigger.maintenance === false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter triggers by severity
|
|
||||||
triggerList = _.filter(triggerList, trigger => {
|
|
||||||
return this.panel.triggerSeverity[trigger.priority].show;
|
|
||||||
});
|
|
||||||
|
|
||||||
return triggerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaintenanceStatus(triggers) {
|
|
||||||
_.each(triggers, (trigger) => {
|
|
||||||
let maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1');
|
|
||||||
trigger.maintenance = maintenance_status;
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAckButtonStatus(triggers, showAckButton) {
|
|
||||||
_.each(triggers, (trigger) => {
|
|
||||||
trigger.showAckButton = showAckButton;
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
addTriggerDataSource(triggers, target) {
|
|
||||||
_.each(triggers, (trigger) => {
|
|
||||||
trigger.datasource = target.datasource;
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
addTriggerHostProxy(triggers, proxies) {
|
|
||||||
triggers.forEach(trigger => {
|
|
||||||
if (trigger.hosts && trigger.hosts.length) {
|
|
||||||
let host = trigger.hosts[0];
|
|
||||||
if (host.proxy_hostid !== '0') {
|
|
||||||
const hostProxy = proxies[host.proxy_hostid];
|
|
||||||
host.proxy = hostProxy ? hostProxy.host : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
|
|
||||||
sortTriggers(triggerList) {
|
|
||||||
if (this.panel.sortTriggersBy.value === 'priority') {
|
|
||||||
triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']);
|
|
||||||
} else {
|
|
||||||
triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']);
|
|
||||||
}
|
|
||||||
return triggerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTrigger(zabbixTrigger) {
|
|
||||||
let trigger = _.cloneDeep(zabbixTrigger);
|
|
||||||
|
|
||||||
// Set host and proxy that the trigger belongs
|
|
||||||
if (trigger.hosts && trigger.hosts.length) {
|
|
||||||
const host = trigger.hosts[0];
|
|
||||||
trigger.host = host.name;
|
|
||||||
trigger.hostTechName = host.host;
|
|
||||||
if (host.proxy) {
|
|
||||||
trigger.proxy = host.proxy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set tags if present
|
|
||||||
if (trigger.tags && trigger.tags.length === 0) {
|
|
||||||
trigger.tags = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle multi-line description
|
|
||||||
if (trigger.comments) {
|
|
||||||
trigger.comments = trigger.comments.replace('\n', '<br>');
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger.lastchangeUnix = Number(trigger.lastchange);
|
|
||||||
return trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseTags(tagStr) {
|
|
||||||
if (!tagStr) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
|
||||||
tags = _.map(tags, (tag) => {
|
|
||||||
const tagParts = tag.split(':');
|
|
||||||
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
|
||||||
});
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
tagsToString(tags) {
|
|
||||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
target.tags.filter = newFilter;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
target.tags.filter = newFilter;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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) {
|
|
||||||
host = `${trigger.host} (${trigger.hostTechName})`;
|
|
||||||
} else if (this.panel.hostField || this.panel.hostTechNameField) {
|
|
||||||
host = this.panel.hostField ? trigger.host : trigger.hostTechName;
|
|
||||||
}
|
|
||||||
if (this.panel.hostProxy && trigger.proxy) {
|
|
||||||
host = `${trigger.proxy}: ${host}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatHostGroups(trigger) {
|
|
||||||
let groupNames = "";
|
|
||||||
if (this.panel.hostGroups) {
|
|
||||||
let groups = _.map(trigger.groups, 'name').join(', ');
|
|
||||||
groupNames += `[ ${groups} ]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNewTrigger(trigger) {
|
|
||||||
try {
|
|
||||||
const highlightIntervalMs = utils.parseInterval(this.panel.highlightNewerThan || PANEL_DEFAULTS.highlightNewerThan);
|
|
||||||
const durationSec = (Date.now() - trigger.lastchangeUnix * 1000);
|
|
||||||
return durationSec < highlightIntervalMs;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAlertIconClass(trigger) {
|
|
||||||
let iconClass = '';
|
|
||||||
if (trigger.value === '1' && trigger.priority >= 2) {
|
|
||||||
iconClass = 'icon-gf-critical';
|
|
||||||
} else {
|
|
||||||
iconClass = 'icon-gf-online';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) {
|
|
||||||
iconClass += ' zabbix-trigger--blinked';
|
|
||||||
}
|
|
||||||
return iconClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAlertIconClassBySeverity(triggerSeverity) {
|
|
||||||
let iconClass = 'icon-gf-online';
|
|
||||||
if (triggerSeverity.priority >= 2) {
|
|
||||||
iconClass = 'icon-gf-critical';
|
|
||||||
}
|
|
||||||
return iconClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAlertStateClass(trigger) {
|
|
||||||
let statusClass = '';
|
|
||||||
|
|
||||||
if (trigger.value === '1') {
|
|
||||||
statusClass = 'alert-state-critical';
|
|
||||||
} else {
|
|
||||||
statusClass = 'alert-state-ok';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) {
|
|
||||||
statusClass += ' zabbix-trigger--blinked';
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetResizedColumns() {
|
|
||||||
this.panel.resizedColumns = [];
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
acknowledgeTrigger(trigger, message) {
|
|
||||||
let eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null;
|
|
||||||
let grafana_user = this.contextSrv.user.name;
|
|
||||||
let ack_message = grafana_user + ' (Grafana): ' + message;
|
|
||||||
return this.datasourceSrv.get(trigger.datasource)
|
|
||||||
.then(datasource => {
|
|
||||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
|
||||||
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
|
||||||
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
|
||||||
}
|
|
||||||
if (eventid) {
|
|
||||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message);
|
|
||||||
} else {
|
|
||||||
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(this.onRefresh.bind(this))
|
|
||||||
.catch((err) => {
|
|
||||||
this.setPanelError(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePageSizeChange(pageSize, pageIndex) {
|
|
||||||
this.panel.pageSize = pageSize;
|
|
||||||
this.pageIndex = pageIndex;
|
|
||||||
this.scope.$apply(() => {
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleColumnResize(newResized) {
|
|
||||||
this.panel.resizedColumns = newResized;
|
|
||||||
this.scope.$apply(() => {
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
link(scope, elem, attrs, ctrl) {
|
|
||||||
let panel = ctrl.panel;
|
|
||||||
let triggerList = ctrl.triggerList;
|
|
||||||
|
|
||||||
scope.$watchGroup(['ctrl.triggerList'], renderPanel);
|
|
||||||
ctrl.events.on('render', (renderData) => {
|
|
||||||
triggerList = renderData || triggerList;
|
|
||||||
renderPanel();
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderPanel() {
|
|
||||||
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
|
||||||
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
|
||||||
|
|
||||||
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1));
|
|
||||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
|
||||||
|
|
||||||
const pageSize = panel.pageSize || 10;
|
|
||||||
const loading = ctrl.loading && (!ctrl.triggerList || !ctrl.triggerList.length);
|
|
||||||
|
|
||||||
let panelOptions = {};
|
|
||||||
for (let prop in PANEL_DEFAULTS) {
|
|
||||||
panelOptions[prop] = ctrl.panel[prop];
|
|
||||||
}
|
|
||||||
const problemsListProps = {
|
|
||||||
problems: ctrl.triggerList,
|
|
||||||
panelOptions,
|
|
||||||
timeRange: { timeFrom, timeTo },
|
|
||||||
loading,
|
|
||||||
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) => {
|
|
||||||
const message = data.message;
|
|
||||||
return ctrl.acknowledgeTrigger(trigger, message);
|
|
||||||
},
|
|
||||||
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
|
||||||
if (ctrlKey || shiftKey) {
|
|
||||||
ctrl.removeTagFilter(tag, datasource);
|
|
||||||
} else {
|
|
||||||
ctrl.addTagFilter(tag, datasource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let problemsReactElem;
|
|
||||||
if (panel.layout === 'list') {
|
|
||||||
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
|
||||||
} else {
|
|
||||||
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
|
||||||
}
|
|
||||||
ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
|
||||||
|
|
||||||
function filterTriggers(triggers, triggerFilter) {
|
|
||||||
if (utils.isRegex(triggerFilter)) {
|
|
||||||
return _.filter(triggers, function(trigger) {
|
|
||||||
return utils.buildRegex(triggerFilter).test(trigger.description);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return _.filter(triggers, function(trigger) {
|
|
||||||
return trigger.description === triggerFilter;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
434
src/panel-triggers/triggers_panel_ctrl.ts
Normal file
434
src/panel-triggers/triggers_panel_ctrl.ts
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { PanelEvents } from '@grafana/data';
|
||||||
|
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||||
|
import * as utils from '../datasource-zabbix/utils';
|
||||||
|
import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';
|
||||||
|
import { triggerPanelOptionsTab } from './options_tab';
|
||||||
|
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||||
|
import ProblemList from './components/Problems/Problems';
|
||||||
|
import AlertList from './components/AlertList/AlertList';
|
||||||
|
import { ProblemDTO } from 'datasource-zabbix/types';
|
||||||
|
|
||||||
|
const PROBLEM_EVENTS_LIMIT = 100;
|
||||||
|
|
||||||
|
export const DEFAULT_TARGET = {
|
||||||
|
group: {filter: ""},
|
||||||
|
host: {filter: ""},
|
||||||
|
application: {filter: ""},
|
||||||
|
trigger: {filter: ""},
|
||||||
|
tags: {filter: ""},
|
||||||
|
proxy: {filter: ""},
|
||||||
|
showProblems: 'problems',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_SEVERITY = [
|
||||||
|
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
|
||||||
|
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true},
|
||||||
|
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true},
|
||||||
|
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true},
|
||||||
|
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true},
|
||||||
|
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
||||||
|
|
||||||
|
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||||
|
|
||||||
|
export const PANEL_DEFAULTS = {
|
||||||
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||||
|
// Fields
|
||||||
|
hostField: true,
|
||||||
|
hostTechNameField: false,
|
||||||
|
hostProxy: false,
|
||||||
|
hostGroups: false,
|
||||||
|
showTags: true,
|
||||||
|
statusField: true,
|
||||||
|
statusIcon: false,
|
||||||
|
severityField: true,
|
||||||
|
ackField: true,
|
||||||
|
ageField: false,
|
||||||
|
descriptionField: true,
|
||||||
|
descriptionAtNewLine: false,
|
||||||
|
// Options
|
||||||
|
sortProblems: 'lastchange',
|
||||||
|
limit: null,
|
||||||
|
// View options
|
||||||
|
layout: 'table',
|
||||||
|
fontSize: '100%',
|
||||||
|
pageSize: 10,
|
||||||
|
problemTimeline: true,
|
||||||
|
highlightBackground: false,
|
||||||
|
highlightNewEvents: false,
|
||||||
|
highlightNewerThan: '1h',
|
||||||
|
customLastChangeFormat: false,
|
||||||
|
lastChangeFormat: "",
|
||||||
|
resizedColumns: [],
|
||||||
|
// Triggers severity and colors
|
||||||
|
triggerSeverity: getDefaultSeverity(),
|
||||||
|
okEventColor: 'rgb(56, 189, 113)',
|
||||||
|
ackEventColor: 'rgb(56, 219, 156)',
|
||||||
|
markAckEvents: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerStatusMap = {
|
||||||
|
'0': 'OK',
|
||||||
|
'1': 'PROBLEM'
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||||
|
scope: any;
|
||||||
|
useDataFrames: boolean;
|
||||||
|
triggerStatusMap: any;
|
||||||
|
defaultTimeFormat: string;
|
||||||
|
pageIndex: number;
|
||||||
|
renderData: any[];
|
||||||
|
problems: any[];
|
||||||
|
contextSrv: any;
|
||||||
|
static templateUrl: string;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope, $injector, $timeout) {
|
||||||
|
super($scope, $injector);
|
||||||
|
this.scope = $scope;
|
||||||
|
this.$timeout = $timeout;
|
||||||
|
|
||||||
|
// Tell Grafana do not convert data frames to table or series
|
||||||
|
this.useDataFrames = true;
|
||||||
|
|
||||||
|
this.triggerStatusMap = triggerStatusMap;
|
||||||
|
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
||||||
|
this.pageIndex = 0;
|
||||||
|
this.range = {};
|
||||||
|
this.renderData = [];
|
||||||
|
|
||||||
|
this.panel = migratePanelSchema(this.panel);
|
||||||
|
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||||
|
|
||||||
|
// this.events.on(PanelEvents.render, this.onRender.bind(this));
|
||||||
|
this.events.on('data-frames-received', this.onDataFramesReceived.bind(this));
|
||||||
|
this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this));
|
||||||
|
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitEditMode() {
|
||||||
|
// Update schema version to prevent migration on up-to-date targets
|
||||||
|
this.panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||||
|
this.addEditorTab('Options', triggerPanelOptionsTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataFramesReceived(data: any): Promise<any> {
|
||||||
|
this.range = this.timeSrv.timeRange();
|
||||||
|
let problems = [];
|
||||||
|
|
||||||
|
if (data && data.length) {
|
||||||
|
for (const dataFrame of data) {
|
||||||
|
try {
|
||||||
|
const values = dataFrame.fields[0].values;
|
||||||
|
if (values.toArray) {
|
||||||
|
problems.push(values.toArray());
|
||||||
|
} else if (values.length > 0) {
|
||||||
|
// On snapshot mode values is a plain Array, not ArrayVector
|
||||||
|
problems.push(values);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
problems = _.flatten(problems);
|
||||||
|
this.problems = problems;
|
||||||
|
return this.renderProblems(problems);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataSnapshotLoad(snapshotData) {
|
||||||
|
return this.onDataFramesReceived(snapshotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
reRenderProblems() {
|
||||||
|
if (this.problems) {
|
||||||
|
this.renderProblems(this.problems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanelError(err, defaultError = "Request Error") {
|
||||||
|
this.inspector = { error: err };
|
||||||
|
this.error = err.message || defaultError;
|
||||||
|
if (err.data) {
|
||||||
|
if (err.data.message) {
|
||||||
|
this.error = err.data.message;
|
||||||
|
}
|
||||||
|
if (err.data.error) {
|
||||||
|
this.error = err.data.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.events.emit(PanelEvents.dataError, err);
|
||||||
|
console.log('Panel data error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProblems(problems) {
|
||||||
|
let triggers = _.cloneDeep(problems);
|
||||||
|
|
||||||
|
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
||||||
|
triggers = this.filterProblems(triggers);
|
||||||
|
triggers = this.sortTriggers(triggers);
|
||||||
|
|
||||||
|
this.renderData = triggers;
|
||||||
|
|
||||||
|
return this.$timeout(() => {
|
||||||
|
return super.render(triggers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterProblems(problems) {
|
||||||
|
let problemsList = _.cloneDeep(problems);
|
||||||
|
|
||||||
|
// Filter acknowledged triggers
|
||||||
|
if (this.panel.showTriggers === 'unacknowledged') {
|
||||||
|
problemsList = _.filter(problemsList, trigger => {
|
||||||
|
return !(trigger.acknowledges && trigger.acknowledges.length);
|
||||||
|
});
|
||||||
|
} else if (this.panel.showTriggers === 'acknowledged') {
|
||||||
|
problemsList = _.filter(problemsList, trigger => {
|
||||||
|
return trigger.acknowledges && trigger.acknowledges.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter triggers by severity
|
||||||
|
problemsList = _.filter(problemsList, problem => {
|
||||||
|
if (problem.severity) {
|
||||||
|
return this.panel.triggerSeverity[problem.severity].show;
|
||||||
|
} else {
|
||||||
|
return this.panel.triggerSeverity[problem.priority].show;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return problemsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortTriggers(triggerList) {
|
||||||
|
if (this.panel.sortProblems === 'priority') {
|
||||||
|
triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||||
|
} else if (this.panel.sortProblems === 'lastchange') {
|
||||||
|
triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||||
|
}
|
||||||
|
return triggerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTrigger(zabbixTrigger) {
|
||||||
|
const trigger = _.cloneDeep(zabbixTrigger);
|
||||||
|
|
||||||
|
// Set host and proxy that the trigger belongs
|
||||||
|
if (trigger.hosts && trigger.hosts.length) {
|
||||||
|
const host = trigger.hosts[0];
|
||||||
|
trigger.host = host.name;
|
||||||
|
trigger.hostTechName = host.host;
|
||||||
|
if (host.proxy) {
|
||||||
|
trigger.proxy = host.proxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set tags if present
|
||||||
|
if (trigger.tags && trigger.tags.length === 0) {
|
||||||
|
trigger.tags = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multi-line description
|
||||||
|
if (trigger.comments) {
|
||||||
|
trigger.comments = trigger.comments.replace('\n', '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.lastchangeUnix = Number(trigger.lastchange);
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTags(tagStr) {
|
||||||
|
if (!tagStr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||||
|
tags = _.map(tags, (tag) => {
|
||||||
|
const tagParts = tag.split(':');
|
||||||
|
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
||||||
|
});
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsToString(tags) {
|
||||||
|
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
addTagFilter(tag, datasource) {
|
||||||
|
for (const target of this.panel.targets) {
|
||||||
|
if (target.datasource === datasource || this.panel.datasource === datasource) {
|
||||||
|
const tagFilter = target.tags.filter;
|
||||||
|
let targetTags = this.parseTags(tagFilter);
|
||||||
|
const newTag = {tag: tag.tag, value: tag.value};
|
||||||
|
targetTags.push(newTag);
|
||||||
|
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||||
|
const newFilter = this.tagsToString(targetTags);
|
||||||
|
target.tags.filter = newFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTagFilter(tag, datasource) {
|
||||||
|
const matchTag = t => t.tag === tag.tag && t.value === tag.value;
|
||||||
|
for (const target of this.panel.targets) {
|
||||||
|
if (target.datasource === datasource || this.panel.datasource === datasource) {
|
||||||
|
const tagFilter = target.tags.filter;
|
||||||
|
let targetTags = this.parseTags(tagFilter);
|
||||||
|
_.remove(targetTags, matchTag);
|
||||||
|
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||||
|
const newFilter = this.tagsToString(targetTags);
|
||||||
|
target.tags.filter = newFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getDataSourceSrv().get(problem.datasource)
|
||||||
|
.then((datasource: any) => {
|
||||||
|
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProblemAlerts(problem: ProblemDTO) {
|
||||||
|
if (!problem.eventid) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
const eventids = [problem.eventid];
|
||||||
|
return getDataSourceSrv().get(problem.datasource)
|
||||||
|
.then((datasource: any) => {
|
||||||
|
return datasource.zabbix.getEventAlerts(eventids);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlertIconClassBySeverity(triggerSeverity) {
|
||||||
|
let iconClass = 'icon-gf-online';
|
||||||
|
if (triggerSeverity.priority >= 2) {
|
||||||
|
iconClass = 'icon-gf-critical';
|
||||||
|
}
|
||||||
|
return iconClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetResizedColumns() {
|
||||||
|
this.panel.resizedColumns = [];
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
acknowledgeProblem(problem: ProblemDTO, message, action, severity) {
|
||||||
|
const eventid = problem.eventid;
|
||||||
|
const grafana_user = this.contextSrv.user.name;
|
||||||
|
const ack_message = grafana_user + ' (Grafana): ' + message;
|
||||||
|
return getDataSourceSrv().get(problem.datasource)
|
||||||
|
.then((datasource: any) => {
|
||||||
|
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||||
|
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
||||||
|
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
||||||
|
}
|
||||||
|
if (eventid) {
|
||||||
|
return datasource.zabbix.acknowledgeEvent(eventid, ack_message, action, severity);
|
||||||
|
} else {
|
||||||
|
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(this.refresh.bind(this))
|
||||||
|
.catch((err) => {
|
||||||
|
this.setPanelError(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePageSizeChange(pageSize, pageIndex) {
|
||||||
|
this.panel.pageSize = pageSize;
|
||||||
|
this.pageIndex = pageIndex;
|
||||||
|
this.scope.$apply(() => {
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColumnResize(newResized) {
|
||||||
|
this.panel.resizedColumns = newResized;
|
||||||
|
this.scope.$apply(() => {
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
link(scope, elem, attrs, ctrl) {
|
||||||
|
const panel = ctrl.panel;
|
||||||
|
|
||||||
|
ctrl.events.on(PanelEvents.render, (renderData) => {
|
||||||
|
renderData = renderData || this.renderData;
|
||||||
|
renderPanel(renderData);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderPanel(problems) {
|
||||||
|
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
||||||
|
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
||||||
|
|
||||||
|
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1), 10);
|
||||||
|
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||||
|
|
||||||
|
const pageSize = panel.pageSize || 10;
|
||||||
|
const loading = ctrl.loading && (!problems || !problems.length);
|
||||||
|
|
||||||
|
const panelOptions = {};
|
||||||
|
for (const prop in PANEL_DEFAULTS) {
|
||||||
|
panelOptions[prop] = ctrl.panel[prop];
|
||||||
|
}
|
||||||
|
const problemsListProps = {
|
||||||
|
problems,
|
||||||
|
panelOptions,
|
||||||
|
timeRange: { timeFrom, timeTo },
|
||||||
|
loading,
|
||||||
|
pageSize,
|
||||||
|
fontSize: fontSizeProp,
|
||||||
|
panelId: ctrl.panel.id,
|
||||||
|
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||||
|
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
||||||
|
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||||
|
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||||
|
onProblemAck: (trigger, data) => {
|
||||||
|
const { message, action, severity } = data;
|
||||||
|
return ctrl.acknowledgeProblem(trigger, message, action, severity);
|
||||||
|
},
|
||||||
|
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
||||||
|
if (ctrlKey || shiftKey) {
|
||||||
|
ctrl.removeTagFilter(tag, datasource);
|
||||||
|
} else {
|
||||||
|
ctrl.addTagFilter(tag, datasource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let problemsReactElem;
|
||||||
|
if (panel.layout === 'list') {
|
||||||
|
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
||||||
|
} else {
|
||||||
|
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelContainerElem = elem.find('.panel-content');
|
||||||
|
if (panelContainerElem && panelContainerElem.length) {
|
||||||
|
ReactDOM.render(problemsReactElem, panelContainerElem[0]);
|
||||||
|
} else {
|
||||||
|
ReactDOM.render(problemsReactElem, elem[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import * as utils from '../datasource-zabbix/utils';
|
|
||||||
import { getDefaultTarget } from './triggers_panel_ctrl';
|
|
||||||
|
|
||||||
class TriggersTabCtrl {
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope, $rootScope, uiSegmentSrv, templateSrv) {
|
|
||||||
$scope.editor = this;
|
|
||||||
this.panelCtrl = $scope.ctrl;
|
|
||||||
this.panel = this.panelCtrl.panel;
|
|
||||||
this.templateSrv = templateSrv;
|
|
||||||
this.datasources = {};
|
|
||||||
|
|
||||||
// Load scope defaults
|
|
||||||
var scopeDefaults = {
|
|
||||||
getGroupNames: {},
|
|
||||||
getHostNames: {},
|
|
||||||
getApplicationNames: {},
|
|
||||||
getProxyNames: {},
|
|
||||||
oldTarget: _.cloneDeep(this.panel.targets)
|
|
||||||
};
|
|
||||||
_.defaultsDeep(this, scopeDefaults);
|
|
||||||
this.selectedDatasources = this.getSelectedDatasources();
|
|
||||||
|
|
||||||
this.initDatasources();
|
|
||||||
this.panelCtrl.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
initDatasources() {
|
|
||||||
return this.panelCtrl.initDatasources()
|
|
||||||
.then((datasources) => {
|
|
||||||
_.each(datasources, (datasource) => {
|
|
||||||
this.datasources[datasource.name] = datasource;
|
|
||||||
this.bindSuggestionFunctions(datasource);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSuggestionFunctions(datasource) {
|
|
||||||
// Map functions for bs-typeahead
|
|
||||||
let ds = datasource.name;
|
|
||||||
this.getGroupNames[ds] = _.bind(this.suggestGroups, this, datasource);
|
|
||||||
this.getHostNames[ds] = _.bind(this.suggestHosts, this, datasource);
|
|
||||||
this.getApplicationNames[ds] = _.bind(this.suggestApps, this, datasource);
|
|
||||||
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 => {
|
|
||||||
return _.map(groups, 'name');
|
|
||||||
})
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestHosts(datasource, query, callback) {
|
|
||||||
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');
|
|
||||||
})
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestApps(datasource, query, callback) {
|
|
||||||
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');
|
|
||||||
})
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestProxies(datasource, query, callback) {
|
|
||||||
return datasource.zabbix.getProxies()
|
|
||||||
.then(proxies => _.map(proxies, 'host'))
|
|
||||||
.then(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
datasourcesChanged() {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
parseTarget() {
|
|
||||||
this.initDatasources()
|
|
||||||
.then(() => {
|
|
||||||
var newTarget = _.cloneDeep(this.panel.targets);
|
|
||||||
if (!_.isEqual(this.oldTarget, newTarget)) {
|
|
||||||
this.oldTarget = newTarget;
|
|
||||||
this.panelCtrl.refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isRegex(str) {
|
|
||||||
return utils.isRegex(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
isVariable(str) {
|
|
||||||
return utils.isTemplateVariable(str, this.templateSrv.variables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function triggerPanelTriggersTab() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
scope: true,
|
|
||||||
templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/triggers_tab.html',
|
|
||||||
controller: TriggersTabCtrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ export interface ProblemsPanelOptions {
|
|||||||
statusField?: boolean;
|
statusField?: boolean;
|
||||||
statusIcon?: boolean;
|
statusIcon?: boolean;
|
||||||
severityField?: boolean;
|
severityField?: boolean;
|
||||||
|
ackField?: boolean;
|
||||||
ageField?: boolean;
|
ageField?: boolean;
|
||||||
descriptionField?: boolean;
|
descriptionField?: boolean;
|
||||||
descriptionAtNewLine?: boolean;
|
descriptionAtNewLine?: boolean;
|
||||||
@@ -140,6 +141,7 @@ export interface ZBXEvent {
|
|||||||
object?: string;
|
object?: string;
|
||||||
objectid?: string;
|
objectid?: string;
|
||||||
acknowledged?: string;
|
acknowledged?: string;
|
||||||
|
severity?: string;
|
||||||
hosts?: ZBXHost[];
|
hosts?: ZBXHost[];
|
||||||
acknowledges?: ZBXAcknowledge[];
|
acknowledges?: ZBXAcknowledge[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import _ from 'lodash';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { DataQuery } from '@grafana/data';
|
import { DataQuery } from '@grafana/data';
|
||||||
import * as utils from '../datasource-zabbix/utils';
|
import * as utils from '../datasource-zabbix/utils';
|
||||||
import { ZBXTrigger } from './types';
|
import { ProblemDTO } from 'datasource-zabbix/types';
|
||||||
|
|
||||||
export function isNewProblem(problem: ZBXTrigger, highlightNewerThan: string): boolean {
|
export function isNewProblem(problem: ProblemDTO, highlightNewerThan: string): boolean {
|
||||||
try {
|
try {
|
||||||
const highlightIntervalMs = utils.parseInterval(highlightNewerThan);
|
const highlightIntervalMs = utils.parseInterval(highlightNewerThan);
|
||||||
const durationSec = (Date.now() - problem.lastchangeUnix * 1000);
|
const durationSec = (Date.now() - problem.timestamp * 1000);
|
||||||
return durationSec < highlightIntervalMs;
|
return durationSec < highlightIntervalMs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
@@ -32,3 +32,74 @@ export const getNextRefIdChar = (queries: DataQuery[]): string => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UrlQueryMap = Record<string, any>;
|
||||||
|
|
||||||
|
export function renderUrl(path: string, query: UrlQueryMap | undefined): string {
|
||||||
|
if (query && Object.keys(query).length > 0) {
|
||||||
|
path += '?' + toUrlParams(query);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) {
|
||||||
|
return encodeURIComponent(val)
|
||||||
|
.replace(/%25/gi, '%2525') // Double-encode % symbol to make it properly decoded in Explore
|
||||||
|
.replace(/%40/gi, '@')
|
||||||
|
.replace(/%3A/gi, ':')
|
||||||
|
.replace(/%24/g, '$')
|
||||||
|
.replace(/%2C/gi, ',')
|
||||||
|
.replace(/%3B/gi, ';')
|
||||||
|
.replace(/%20/g, pctEncodeSpaces ? '%20' : '+');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUrlParams(a: any) {
|
||||||
|
const s: any[] = [];
|
||||||
|
const rbracket = /\[\]$/;
|
||||||
|
|
||||||
|
const isArray = (obj: any) => {
|
||||||
|
return Object.prototype.toString.call(obj) === '[object Array]';
|
||||||
|
};
|
||||||
|
|
||||||
|
const add = (k: string, v: any) => {
|
||||||
|
v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v;
|
||||||
|
if (typeof v !== 'boolean') {
|
||||||
|
s[s.length] = encodeURIComponentAsAngularJS(k, true) + '=' + encodeURIComponentAsAngularJS(v, true);
|
||||||
|
} else {
|
||||||
|
s[s.length] = encodeURIComponentAsAngularJS(k, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildParams = (prefix: string, obj: any) => {
|
||||||
|
let i, len, key;
|
||||||
|
|
||||||
|
if (prefix) {
|
||||||
|
if (isArray(obj)) {
|
||||||
|
for (i = 0, len = obj.length; i < len; i++) {
|
||||||
|
if (rbracket.test(prefix)) {
|
||||||
|
add(prefix, obj[i]);
|
||||||
|
} else {
|
||||||
|
buildParams(prefix, obj[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj && String(obj) === '[object Object]') {
|
||||||
|
for (key in obj) {
|
||||||
|
buildParams(prefix + '[' + key + ']', obj[key]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add(prefix, obj);
|
||||||
|
}
|
||||||
|
} else if (isArray(obj)) {
|
||||||
|
for (i = 0, len = obj.length; i < len; i++) {
|
||||||
|
add(obj[i].name, obj[i].value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (key in obj) {
|
||||||
|
buildParams(key, obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
return buildParams('', a).join('&');
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
},
|
},
|
||||||
"keywords": ["zabbix"],
|
"keywords": ["zabbix"],
|
||||||
"logos": {
|
"logos": {
|
||||||
"small": "img/zabbix_app_logo.svg",
|
"small": "img/icn-zabbix-app.svg",
|
||||||
"large": "img/zabbix_app_logo.svg"
|
"large": "img/icn-zabbix-app.svg"
|
||||||
},
|
},
|
||||||
"links": [
|
"links": [
|
||||||
{"name": "GitHub", "url": "https://github.com/alexanderzobnin/grafana-zabbix"},
|
{"name": "GitHub", "url": "https://github.com/alexanderzobnin/grafana-zabbix"},
|
||||||
@@ -27,17 +27,17 @@
|
|||||||
{"name": "Triggers", "path": "img/screenshot-triggers.png"}
|
{"name": "Triggers", "path": "img/screenshot-triggers.png"}
|
||||||
],
|
],
|
||||||
"version": "4.0.0-alpha",
|
"version": "4.0.0-alpha",
|
||||||
"updated": "2020-01-14"
|
"updated": "2020-05-28"
|
||||||
},
|
},
|
||||||
|
|
||||||
"includes": [
|
"includes": [
|
||||||
{
|
{
|
||||||
"type": "datasource",
|
"type": "datasource",
|
||||||
"name": "Zabbix Datasource"
|
"name": "Zabbix data source"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "panel",
|
"type": "panel",
|
||||||
"name": "Triggers Panel"
|
"name": "Problems panel"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
i {
|
i {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.fired {
|
||||||
|
color: $problem-statusbar-fired;
|
||||||
|
text-shadow: 0px 0px 10px rgba($problem-statusbar-fired, 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// <ReactTable /> styles
|
// <ReactTable /> styles
|
||||||
@@ -246,11 +251,31 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.problem-description-row {
|
||||||
|
|
||||||
|
.problem-description {
|
||||||
|
position: relative;
|
||||||
|
height: 4.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
text-align: right;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 70%;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: linear-gradient(to right, rgba($problem-details-background, 0), rgba($problem-details-background, 1) 50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.description-label {
|
.description-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.problem-age {
|
.problem-age {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -314,24 +339,8 @@
|
|||||||
margin-left: 1.6rem;
|
margin-left: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-action-button {
|
.problem-actions-left {
|
||||||
&.btn {
|
margin-right: 1.6rem;
|
||||||
width: 3rem;
|
|
||||||
height: 2rem;
|
|
||||||
|
|
||||||
background-image: none;
|
|
||||||
background-color: $action-button-color;
|
|
||||||
border: 1px solid darken($action-button-color, 6%);
|
|
||||||
border-radius: 1px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: $action-button-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: darken($action-button-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-details-middle {
|
.problem-details-middle {
|
||||||
@@ -546,9 +555,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.zbx-ack-modal {
|
.zbx-ack-modal {
|
||||||
.gf-form-input.zbx-ack-error {
|
.zbx-ack-error {
|
||||||
border-color: $btn-danger-bg;
|
border-color: $btn-danger-bg;
|
||||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px $btn-danger-bg;
|
// box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px $btn-danger-bg;
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 2px #141619, 0 0 0px 4px $btn-danger-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ack-request-error {
|
||||||
|
padding-top: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ack-error-message {
|
||||||
|
color: $error-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gf-form .gf-form-hint {
|
.gf-form .gf-form-hint {
|
||||||
@@ -558,7 +577,6 @@
|
|||||||
|
|
||||||
&.ack-error-message {
|
&.ack-error-message {
|
||||||
float: left;
|
float: left;
|
||||||
color: $error-text-color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// DEPENDENCIES
|
// DEPENDENCIES
|
||||||
@import '../../node_modules/react-table/react-table.css';
|
@import '../../node_modules/react-table-6/react-table.css';
|
||||||
|
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
@import 'panel-triggers';
|
@import 'panel-triggers';
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
/* globals global: false */
|
/* globals global: false */
|
||||||
|
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import { PanelCtrl } from './panelStub';
|
import { PanelCtrl, MetricsPanelCtrl } from './panelStub';
|
||||||
|
|
||||||
|
console.log = () => {};
|
||||||
|
|
||||||
// Mock Grafana modules that are not available outside of the core project
|
// Mock Grafana modules that are not available outside of the core project
|
||||||
// Required for loading module.js
|
// Required for loading module.js
|
||||||
@@ -26,17 +28,34 @@ jest.mock('grafana/app/features/dashboard/dashboard_srv', () => {
|
|||||||
return {};
|
return {};
|
||||||
}, {virtual: true});
|
}, {virtual: true});
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => {
|
||||||
|
return {
|
||||||
|
getBackendSrv: () => ({
|
||||||
|
datasourceRequest: jest.fn().mockResolvedValue(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, {virtual: true});
|
||||||
|
|
||||||
jest.mock('grafana/app/core/core_module', () => {
|
jest.mock('grafana/app/core/core_module', () => {
|
||||||
return {
|
return {
|
||||||
directive: function() {},
|
directive: function() {},
|
||||||
};
|
};
|
||||||
}, {virtual: true});
|
}, {virtual: true});
|
||||||
|
|
||||||
let mockPanelCtrl = PanelCtrl;
|
jest.mock('grafana/app/core/core', () => ({
|
||||||
|
contextSrv: {},
|
||||||
|
}), {virtual: true});
|
||||||
|
|
||||||
|
const mockPanelCtrl = PanelCtrl;
|
||||||
|
const mockMetricsPanelCtrl = MetricsPanelCtrl;
|
||||||
|
|
||||||
jest.mock('grafana/app/plugins/sdk', () => {
|
jest.mock('grafana/app/plugins/sdk', () => {
|
||||||
return {
|
return {
|
||||||
QueryCtrl: null,
|
QueryCtrl: null,
|
||||||
PanelCtrl: mockPanelCtrl
|
PanelCtrl: mockPanelCtrl,
|
||||||
|
loadPluginCss: () => {},
|
||||||
|
PanelCtrl: mockPanelCtrl,
|
||||||
|
MetricsPanelCtrl: mockMetricsPanelCtrl,
|
||||||
};
|
};
|
||||||
}, {virtual: true});
|
}, {virtual: true});
|
||||||
|
|
||||||
@@ -92,3 +111,7 @@ let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
|
|||||||
global.window = dom.window;
|
global.window = dom.window;
|
||||||
global.document = global.window.document;
|
global.document = global.window.document;
|
||||||
global.Node = window.Node;
|
global.Node = window.Node;
|
||||||
|
|
||||||
|
// Mock Canvas.getContext(), fixes
|
||||||
|
// Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
|
||||||
|
window.HTMLCanvasElement.prototype.getContext = () => {};
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
// JSHint options
|
|
||||||
/* jshint ignore:start */
|
|
||||||
|
|
||||||
export class PanelCtrl {
|
|
||||||
constructor($scope, $injector) {
|
|
||||||
this.$injector = $injector;
|
|
||||||
this.$scope = $scope;
|
|
||||||
this.panel = $scope.panel;
|
|
||||||
this.timing = {};
|
|
||||||
this.events = {
|
|
||||||
on: () => {},
|
|
||||||
emit: () => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingCompleted() {
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
}
|
|
||||||
|
|
||||||
publishAppEvent(evtName, evt) {
|
|
||||||
}
|
|
||||||
|
|
||||||
changeView(fullscreen, edit) {
|
|
||||||
}
|
|
||||||
|
|
||||||
viewPanel() {
|
|
||||||
this.changeView(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
editPanel() {
|
|
||||||
this.changeView(true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
exitFullscreen() {
|
|
||||||
this.changeView(false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
initEditMode() {
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTab(newIndex) {
|
|
||||||
}
|
|
||||||
|
|
||||||
addEditorTab(title, directiveFn, index) {
|
|
||||||
}
|
|
||||||
|
|
||||||
getMenu() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtendedMenu() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
otherPanelInFullscreenMode() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculatePanelHeight() {
|
|
||||||
}
|
|
||||||
|
|
||||||
render(payload) {
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleEditorHelp(index) {
|
|
||||||
}
|
|
||||||
|
|
||||||
duplicate() {
|
|
||||||
}
|
|
||||||
|
|
||||||
updateColumnSpan(span) {
|
|
||||||
}
|
|
||||||
|
|
||||||
removePanel() {
|
|
||||||
}
|
|
||||||
|
|
||||||
editPanelJson() {
|
|
||||||
}
|
|
||||||
|
|
||||||
replacePanel(newPanel, oldPanel) {
|
|
||||||
}
|
|
||||||
|
|
||||||
sharePanel() {
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfoMode() {
|
|
||||||
}
|
|
||||||
|
|
||||||
getInfoContent(options) {
|
|
||||||
}
|
|
||||||
|
|
||||||
openInspector() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
162
src/test-setup/panelStub.ts
Normal file
162
src/test-setup/panelStub.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { PanelEvents } from '@grafana/data';
|
||||||
|
|
||||||
|
export class PanelCtrl {
|
||||||
|
panel: any;
|
||||||
|
error: any;
|
||||||
|
dashboard: any;
|
||||||
|
pluginName: string;
|
||||||
|
pluginId: string;
|
||||||
|
editorTabs: any;
|
||||||
|
$scope: any;
|
||||||
|
$injector: any;
|
||||||
|
$location: any;
|
||||||
|
$timeout: any;
|
||||||
|
editModeInitiated: boolean;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
containerHeight: any;
|
||||||
|
events: any;
|
||||||
|
loading: boolean;
|
||||||
|
timing: any;
|
||||||
|
|
||||||
|
constructor($scope, $injector) {
|
||||||
|
this.$injector = $injector;
|
||||||
|
this.$scope = $scope;
|
||||||
|
this.panel = $scope.panel;
|
||||||
|
this.timing = {};
|
||||||
|
this.events = {
|
||||||
|
on: () => {},
|
||||||
|
emit: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
renderingCompleted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
}
|
||||||
|
|
||||||
|
publishAppEvent(evtName, evt) {
|
||||||
|
}
|
||||||
|
|
||||||
|
changeView(fullscreen, edit) {
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPanel() {
|
||||||
|
this.changeView(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
editPanel() {
|
||||||
|
this.changeView(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitFullscreen() {
|
||||||
|
this.changeView(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initEditMode() {
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTab(newIndex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
addEditorTab(title, directiveFn, index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenu() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtendedMenu() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
otherPanelInFullscreenMode() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatePanelHeight() {
|
||||||
|
}
|
||||||
|
|
||||||
|
render(payload) {
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEditorHelp(index) {
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnSpan(span) {
|
||||||
|
}
|
||||||
|
|
||||||
|
removePanel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
editPanelJson() {
|
||||||
|
}
|
||||||
|
|
||||||
|
replacePanel(newPanel, oldPanel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
sharePanel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoMode() {
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoContent(options) {
|
||||||
|
}
|
||||||
|
|
||||||
|
openInspector() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetricsPanelCtrl extends PanelCtrl {
|
||||||
|
scope: any;
|
||||||
|
datasource: any;
|
||||||
|
$timeout: any;
|
||||||
|
contextSrv: any;
|
||||||
|
datasourceSrv: any;
|
||||||
|
timeSrv: any;
|
||||||
|
templateSrv: any;
|
||||||
|
range: any;
|
||||||
|
interval: any;
|
||||||
|
intervalMs: any;
|
||||||
|
resolution: any;
|
||||||
|
timeInfo?: string;
|
||||||
|
skipDataOnInit: boolean;
|
||||||
|
dataList: any[];
|
||||||
|
querySubscription?: any;
|
||||||
|
useDataFrames = false;
|
||||||
|
|
||||||
|
constructor($scope, $injector) {
|
||||||
|
super($scope, $injector);
|
||||||
|
|
||||||
|
this.events.on(PanelEvents.refresh, this.onMetricsPanelRefresh.bind(this));
|
||||||
|
|
||||||
|
this.timeSrv = {
|
||||||
|
timeRange: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitMetricsPanelEditMode() {}
|
||||||
|
onMetricsPanelRefresh() {}
|
||||||
|
setTimeQueryStart() {}
|
||||||
|
setTimeQueryEnd() {}
|
||||||
|
updateTimeRange() {}
|
||||||
|
calculateInterval() {}
|
||||||
|
applyPanelTimeOverrides() {}
|
||||||
|
issueQueries(datasource) {}
|
||||||
|
handleQueryResult(result) {}
|
||||||
|
handleDataStream(stream) {}
|
||||||
|
setDatasource(datasource) {}
|
||||||
|
getAdditionalMenuItems() {}
|
||||||
|
explore() {}
|
||||||
|
addQuery(target) {}
|
||||||
|
removeQuery(target) {}
|
||||||
|
moveQuery(target, direction) {}
|
||||||
|
}
|
||||||
@@ -2,10 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": [ "es6", "dom", "es2017" ],
|
||||||
"es6",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
@@ -21,6 +18,7 @@
|
|||||||
"noImplicitUseStrict": false,
|
"noImplicitUseStrict": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"baseUrl": "./src"
|
"baseUrl": "./src",
|
||||||
|
"strictFunctionTypes": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ module.exports = {
|
|||||||
target: 'node',
|
target: 'node',
|
||||||
context: resolve('src'),
|
context: resolve('src'),
|
||||||
entry: {
|
entry: {
|
||||||
'./module': './module.js',
|
'module': './module.js',
|
||||||
'components/config': './components/config.js',
|
'app_config_ctrl/config': './app_config_ctrl/config.js',
|
||||||
'datasource-zabbix/module': './datasource-zabbix/module.ts',
|
'datasource-zabbix/module': './datasource-zabbix/module.ts',
|
||||||
'panel-triggers/module': './panel-triggers/module.js',
|
'panel-triggers/module': './panel-triggers/module.js',
|
||||||
},
|
},
|
||||||
@@ -42,6 +42,7 @@ module.exports = {
|
|||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{ from: '**/plugin.json' },
|
{ from: '**/plugin.json' },
|
||||||
{ from: '**/*.html' },
|
{ from: '**/*.html' },
|
||||||
|
{ from: '**/*.md' },
|
||||||
{ from: 'dashboards/*' },
|
{ from: 'dashboards/*' },
|
||||||
{ from: '../README.md' },
|
{ from: '../README.md' },
|
||||||
{ from: '**/img/*' },
|
{ from: '**/img/*' },
|
||||||
|
|||||||
Reference in New Issue
Block a user