Merge branch 'master' into backend

This commit is contained in:
Alexander Zobnin
2020-05-28 12:02:36 +03:00
100 changed files with 4537 additions and 3689 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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:
``` ```

View File

@@ -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"
}, },

View File

@@ -0,0 +1,4 @@
export class ZabbixAppConfigCtrl {
constructor() { }
}
ZabbixAppConfigCtrl.templateUrl = 'app_config_ctrl/config.html';

View 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} />
);
};

View 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};
}
`,
};
});

View 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>);
};

View 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: {} });
};

View 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;

View 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;

View 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()}
</>
);
}
}

View 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;

View File

@@ -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);
} }

View 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;

View File

@@ -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;

View File

@@ -1,4 +0,0 @@
export class ZabbixAppConfigCtrl {
constructor() { }
}
ZabbixAppConfigCtrl.templateUrl = 'components/config.html';

6
src/components/index.ts Normal file
View 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';

View File

@@ -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>
); );
} }

View File

@@ -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

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,71 +623,63 @@ 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 => { const groupFilter = this.replaceTemplateVars(annotation.group, {});
const hostFilter = this.replaceTemplateVars(annotation.host, {});
const appFilter = this.replaceTemplateVars(annotation.application, {});
const proxyFilter = undefined;
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
.then(problems => {
// Filter triggers by description // Filter triggers by description
let triggerName = this.replaceTemplateVars(annotation.trigger, {}); const problemName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(triggerName)) { if (utils.isRegex(problemName)) {
triggers = _.filter(triggers, trigger => { problems = _.filter(problems, p => {
return utils.buildRegex(triggerName).test(trigger.description); return utils.buildRegex(problemName).test(p.description);
}); });
} else if (triggerName) { } else if (problemName) {
triggers = _.filter(triggers, trigger => { problems = _.filter(problems, p => {
return trigger.description === triggerName; return p.description === problemName;
}); });
} }
// Remove events below the chose severity // Hide acknowledged events if option enabled
triggers = _.filter(triggers, trigger => { if (annotation.hideAcknowledged) {
return Number(trigger.priority) >= Number(annotation.minseverity); problems = _.filter(problems, p => {
}); return !p.acknowledges?.length;
var objectids = _.map(triggers, 'triggerid');
return this.zabbix
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
.then(events => {
var indexedTriggers = _.keyBy(triggers, 'triggerid');
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
events = _.filter(events, event => {
return !event.acknowledges.length;
});
}
return _.map(events, event => {
let tags;
if (annotation.showHostname) {
tags = _.map(event.hosts, 'name');
}
// Show event type (OK or Problem)
let title = Number(event.value) ? 'Problem' : 'OK';
let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges);
return {
annotation: annotation,
time: event.clock * 1000,
title: title,
tags: tags,
text: indexedTriggers[event.objectid].description + formatted_acknowledges
};
});
}); });
}
return _.map(problems, p => {
const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges);
let annotationTags: string[] = [];
if (annotation.showHostname) {
annotationTags = _.map(p.hosts, 'name');
}
return {
title: p.value === '1' ? 'Problem' : 'OK',
time: p.timestamp * 1000,
annotation: annotation,
text: p.name + formattedAcknowledges,
tags: annotationTags,
};
});
}); });
} }
@@ -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;}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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"
<select class="gf-form-input" ng-model="ctrl.target.trigger.filter"
ng-change="ctrl.onTargetBlur()" ng-blur="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.minSeverity" placeholder="Problem name"
ng-options="s.val as s.text for s in ctrl.triggerSeverity"> class="gf-form-input"
</select> ng-style="ctrl.target.trigger.style"
</div> ng-class="{
</div> 'zbx-variable': ctrl.isVariable(ctrl.target.trigger.filter),
<div class="gf-form max-width-20" ng-show="ctrl.target.mode == editorMode.TRIGGERS"> 'zbx-regex': ctrl.isRegex(ctrl.target.trigger.filter)
<label class="gf-form-label query-keyword width-8">Acknowledged</label> }"
<div class="gf-form-select-wrapper width-12"> empty-to-null>
<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> </div>
<gf-form-switch class="gf-form" label="Count" ng-show="ctrl.target.mode == editorMode.TRIGGERS" <div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
checked="ctrl.target.triggers.count" on-change="ctrl.onTargetBlur()"> <label class="gf-form-label query-keyword width-7">Tags</label>
</gf-form-switch> <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 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 --> <div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form-group" ng-if="ctrl.showQueryOptions"> <div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form offset-width-7" ng-hide="ctrl.target.mode == editorMode.TRIGGERS"> <label class="gf-form-label query-keyword width-7">Show</label>
<gf-form-switch class="gf-form" label-class="width-10" <div class="gf-form-select-wrapper max-width-20">
label="Show disabled items" <select class="gf-form-input"
checked="ctrl.target.options.showDisabledItems" ng-model="ctrl.target.showProblems"
on-change="ctrl.onQueryOptionChange()"> ng-options="v.value as v.text for v in ctrl.showProblemsOptions"
</gf-form-switch> ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div> </div>
<div class="gf-form offset-width-7" ng-show="ctrl.target.mode === editorMode.TEXT && ctrl.target.resultFormat === 'table'"> <div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<gf-form-switch class="gf-form" label-class="width-10" <label class="gf-form-label query-keyword width-7">Min severity</label>
label="Skip empty values" <div class="gf-form-select-wrapper max-width-20">
checked="ctrl.target.options.skipEmptyValues" <select class="gf-form-input"
on-change="ctrl.onQueryOptionChange()"> ng-model="ctrl.target.options.minSeverity"
</gf-form-switch> 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"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.minSeverity"
ng-options="s.val as s.text for s in ctrl.severityOptions">
</select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
<!-- Text metric regex -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Text filter</label>
<input type="text"
class="gf-form-input"
ng-model="ctrl.target.textFilter"
spellcheck='false'
placeholder="Text filter (regex)"
ng-blur="ctrl.onTargetBlur()">
</div>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
</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' </div>
placeholder="Text filter (regex)"
ng-blur="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>
</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>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()"> <div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
</gf-form-switch> <gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
<div class="gf-form gf-form--grow"> label-class="width-9"
<div class="gf-form-label gf-form-label--grow"></div> 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>

View File

@@ -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"
} }
} }
} }

View 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;

View File

@@ -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();
} }

View File

@@ -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)

View File

@@ -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
alias = host.name + ": " + alias; 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;
}
} }
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;}

View File

@@ -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

View File

@@ -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');
}); });
}); });

View File

@@ -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'],

View File

@@ -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];
} }

View File

@@ -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;
} }

View File

@@ -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) {
return m.hostid === item.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;
}
} 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 || [];
} }

View File

@@ -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
if (_.keys(hosts).length > 1 && addHostName) { // Add scopedVars for using in alias functions
var host = _.find(hosts, {'hostid': item.hostid}); const scopedVars = {
alias = host.name + ": " + alias; '__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;
}
} }
// 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,
}; };
}); });

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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: {

View 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;

View File

@@ -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)
}); });
} }

View File

@@ -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 || "";

View File

@@ -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 => {
cacheObject[hash] = { if (result !== undefined) {
value: result, cacheObject[hash] = {
timestamp: Date.now() value: result,
}; 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;
}; }

View 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;
}

View File

@@ -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', () => {

View File

@@ -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)) {
item.name = utils.replaceMacro(item, macros); if (isTriggerItem) {
item.url = utils.replaceMacro(item, macros, isTriggerItem);
} else {
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));

View File

@@ -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);

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -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

View File

@@ -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({

View 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);

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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}

View File

@@ -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',

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);
}
}

View 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;

View File

@@ -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,20 +96,36 @@ 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"> <div className="problem-description-row">
<span className="description-label">Description:&nbsp;</span> <div className="problem-description">
<span>{problem.comments}</span> <span className="description-label">Description:&nbsp;</span>
<span>{problem.comments}</span>
</div>
</div> </div>
} }
{problem.tags && problem.tags.length > 0 && {problem.tags && problem.tags.length > 0 &&
@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();
});
}
}

View 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

View File

@@ -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;
} }

View File

@@ -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',

View File

@@ -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] },

View File

@@ -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">

View File

@@ -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>

View File

@@ -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"
} }
} }
} }

View File

@@ -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';
jest.mock('@grafana/runtime', () => {
return {
getDataSourceSrv: () => ({
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
}),
};
}, {virtual: true});
describe('Triggers Panel schema migration', () => { describe('Triggers Panel schema migration', () => {
let ctx: any = {}; let ctx: any = {};
let updatePanelCtrl; let updatePanelCtrl;
const datasourceSrvMock = {
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
};
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);
});
}); });

View File

@@ -1,90 +1,53 @@
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', () => {
@@ -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",
"object": "0", "maintenance_status": "1",
"source": "0", "name": "backend01",
"objectid": "1", "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",
"objectid": "13682",
"severity": "2",
"source": "0",
"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()});
} }

View File

@@ -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;
});
}
}

View 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';

View File

@@ -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,
};
}

View File

@@ -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[];
} }

View File

@@ -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('&');
}

View File

@@ -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"
} }
], ],

View File

@@ -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,10 +251,30 @@
flex-direction: column; flex-direction: column;
} }
.description-label { .problem-description-row {
font-weight: 500;
font-style: italic; .problem-description {
color: $text-muted; 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 {
font-weight: 500;
font-style: italic;
color: $text-muted;
}
} }
.problem-age { .problem-age {
@@ -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;
} }
} }
} }

View File

@@ -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';

View File

@@ -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 = () => {};

View File

@@ -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
View 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) {}
}

View File

@@ -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
} }
} }

View File

@@ -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/*' },

1408
yarn.lock

File diff suppressed because it is too large Load Diff