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:
- checkout
- 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:
version: 2

View File

@@ -1,11 +1,54 @@
# 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/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [3.12.1] - 2020-05-25
### 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
### 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/
repo_url: https://github.com/alexanderzobnin/grafana-zabbix/
edit_uri: blob/docs/docs/sources/
copyright: Copyright © 2015-2019, Alexander Zobnin
copyright: Copyright © 2015-2020, Alexander Zobnin
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
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.
- **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
```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
**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
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(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. |
|$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:
```

View File

@@ -1,7 +1,7 @@
{
"name": "grafana-zabbix",
"private": false,
"version": "3.10.5",
"version": "3.11.0",
"description": "Zabbix plugin for Grafana",
"homepage": "http://grafana-zabbix.org",
"scripts": {
@@ -30,10 +30,11 @@
"@babel/preset-env": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@emotion/core": "^10.0.27",
"@grafana/data": "canary",
"@grafana/runtime": "canary",
"@grafana/toolkit": "canary",
"@grafana/ui": "canary",
"@grafana/data": "^6.7.3",
"@grafana/runtime": "^6.7.3",
"@grafana/ui": "^6.7.3",
"@grafana/toolkit": "^6.7.3",
"@popperjs/core": "^2.4.0",
"@types/classnames": "^2.2.9",
"@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "^23.1.1",
@@ -69,24 +70,26 @@
"jshint-stylish": "^2.1.0",
"load-grunt-tasks": "~3.2.0",
"lodash": "~4.17.13",
"memoize-one": "^5.1.1",
"moment": "~2.21.0",
"ng-annotate-webpack-plugin": "^0.3.0",
"node-sass": "^4.13.0",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-popper": "^1.3.2",
"react-table": "^6.8.6",
"react": "16.12.0",
"react-dom": "16.12.0",
"react-popper": "^2.2.3",
"react-table-6": "^6.8.6",
"react-test-renderer": "^16.7.0",
"react-transition-group": "^2.5.2",
"rst2html": "github:thoward/rst2html#990cb89",
"sass-loader": "^8.0.0",
"semver": "^7.3.2",
"style-loader": "^0.23.1",
"tether-drop": "^1.4.2",
"ts-jest": "^24.2.0",
"ts-loader": "^6.2.0",
"tslint": "5.20.1",
"typescript": "3.7.2",
"typescript": "^3.9.2",
"webpack": "4.29.6",
"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> {
node: HTMLElement = document.createElement('div');
node: HTMLElement;
portalRoot: HTMLElement;
constructor(props) {
@@ -17,6 +17,7 @@ export default class BodyPortal extends PureComponent<Props> {
root = document.body
} = this.props;
this.node = document.createElement('div');
if (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 {
placement: string;
show: boolean;
}
export default function withPopper(WrappedComponent) {
export const withPopper = (WrappedComponent) => {
return class extends React.Component<Props, State> {
static defaultProps: Partial<Props> = {
placement: 'auto',
};
constructor(props) {
super(props);
this.setState = this.setState.bind(this);
this.state = {
placement: this.props.placement || 'auto',
show: false,
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.placement && nextProps.placement !== this.state.placement) {
this.setState(prevState => {
return {
...prevState,
placement: nextProps.placement,
};
});
}
}
showPopper = () => {
this.setState(prevState => ({
...prevState,
show: true,
}));
this.setState({ show: true });
};
hidePopper = () => {
this.setState(prevState => ({
...prevState,
show: false,
}));
this.setState({ show: false });
};
renderContent(content) {
@@ -71,8 +55,8 @@ export default function withPopper(WrappedComponent) {
}
render() {
const { show, placement } = this.state;
const className = this.props.className || '';
const { show } = this.state;
const { placement, className } = this.props;
return (
<WrappedComponent
@@ -80,11 +64,13 @@ export default function withPopper(WrappedComponent) {
showPopper={this.showPopper}
hidePopper={this.hidePopper}
renderContent={this.renderContent}
show={show}
placement={placement}
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' }}>
<h4 style={{ color: 'white' }}> {name} </h4>
<div>{description}</div>
/>
</div>
);
}

View File

@@ -1,16 +1,22 @@
import React, { PureComponent } from 'react';
import { parseLegacyVariableQuery } from '../utils';
import { Select, Input, AsyncSelect, FormLabel } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { VariableQuery, VariableQueryTypes, VariableQueryProps, VariableQueryData } from '../types';
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> {
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
{ value: VariableQueryTypes.Group, label: 'Group'},
{ value: VariableQueryTypes.Host, label: 'Host' },
{ value: VariableQueryTypes.Application, label: 'Application' },
{ value: VariableQueryTypes.Item, label: 'Item' },
{ value: VariableQueryTypes.ItemValues, label: 'Item values' },
];
defaults: VariableQueryData = {
@@ -119,7 +125,8 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
}
</div>
{(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 max-width-30">
<FormLabel width={10}>Application</FormLabel>
@@ -129,7 +136,8 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
onBlur={this.handleQueryChange}
/>
</div>
{selectedQueryType.value === VariableQueryTypes.Item &&
{(selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) &&
<div className="gf-form max-width-30">
<FormLabel width={10}>Item</FormLabel>
<ZabbixInput

View File

@@ -1,17 +1,20 @@
import React, { FC } from 'react';
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 { 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 getStyles = (theme: GrafanaTheme) => ({
inputRegex: css`
color: ${theme.colors.orange}
color: ${theme.colors.orange || (theme as any).palette.orange}
`,
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 {
}
const UnthemedZabbixInput: FC<Props> = ({ theme, value, ref, validationEvents, ...restProps }) => {
export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => {
const theme = useTheme();
const styles = getStyles(theme);
let inputClass;
let inputClass = styles.inputRegex;
if (variablePattern.test(value as string)) {
inputClass = styles.inputVariable;
}
if (isRegex(value)) {
} else if (isRegex(value)) {
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 { getDataSourceSrv } from '@grafana/runtime';
import { migrateDSConfig } from './migrations';
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 = {
trends: false,
dbConnectionEnable: false,
@@ -17,29 +12,24 @@ const defaultConfig = {
addThresholds: false,
alertingMinSeverity: 3,
disableReadOnlyUsersAck: false,
zabbixVersion: 3,
};
export class ZabbixDSConfigController {
/** @ngInject */
constructor($scope, $injector, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
constructor() {
this.current.jsonData = migrateDSConfig(this.current.jsonData);
_.defaults(this.current.jsonData, defaultConfig);
this.dbConnectionDatasourceId = this.current.jsonData.dbConnectionDatasourceId;
this.dbDataSources = this.getSupportedDBDataSources();
this.zabbixVersions = _.cloneDeep(zabbixVersions);
this.autoDetectZabbixVersion();
if (!this.dbConnectionDatasourceId) {
this.loadCurrentDBDatasource();
}
}
getSupportedDBDataSources() {
let datasources = this.datasourceSrv.getAll();
let datasources = getDataSourceSrv().getAll();
return _.filter(datasources, ds => {
return _.includes(SUPPORTED_SQL_DS, ds.type);
});
@@ -53,7 +43,7 @@ export class ZabbixDSConfigController {
loadCurrentDBDatasource() {
const dsName= this.current.jsonData.dbConnectionDatasourceName;
this.datasourceSrv.loadDatasource(dsName)
getDataSourceSrv().loadDatasource(dsName)
.then(ds => {
if (ds) {
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() {
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
export const DATAPOINT_VALUE = 0;
export const DATAPOINT_TS = 1;
@@ -8,6 +12,7 @@ export const MODE_ITSERVICE = 1;
export const MODE_TEXT = 2;
export const MODE_ITEMID = 3;
export const MODE_TRIGGERS = 4;
export const MODE_PROBLEMS = 5;
// Triggers severity
export const SEV_NOT_CLASSIFIED = 0;
@@ -23,8 +28,10 @@ export const SHOW_OK_EVENTS = 1;
// Acknowledge
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_ADD_MESSAGE = 4;
export const ZBX_ACK_ACTION_CHANGE_SEVERITY = 8;
export const TRIGGER_SEVERITY = [
{val: 0, text: 'Not classified'},
@@ -39,3 +46,5 @@ export const TRIGGER_SEVERITY = [
export const MIN_SLA_INTERVAL = 3600;
export const RANGE_VARIABLE_VALUE = 'range_series';
export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001;

View File

@@ -1,35 +1,37 @@
import _ from 'lodash';
// Available in 7.0
// import { getTemplateSrv } from '@grafana/runtime';
import * as utils from './utils';
import ts, { groupBy_perf as groupBy } from './timeseries';
let SUM = ts.SUM;
let COUNT = ts.COUNT;
let AVERAGE = ts.AVERAGE;
let MIN = ts.MIN;
let MAX = ts.MAX;
let MEDIAN = ts.MEDIAN;
let PERCENTILE = ts.PERCENTILE;
const SUM = ts.SUM;
const COUNT = ts.COUNT;
const AVERAGE = ts.AVERAGE;
const MIN = ts.MIN;
const MAX = ts.MAX;
const MEDIAN = ts.MEDIAN;
const PERCENTILE = ts.PERCENTILE;
let downsampleSeries = ts.downsample;
let groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
let sumSeries = ts.sumSeries;
let delta = ts.delta;
let rate = ts.rate;
let scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
let offset = (delta, datapoints) => ts.offset(datapoints, delta);
let simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
let expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
let percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
const downsampleSeries = ts.downsample;
const groupBy_exported = (interval, groupFunc, datapoints) => groupBy(datapoints, interval, groupFunc);
const sumSeries = ts.sumSeries;
const delta = ts.delta;
const rate = ts.rate;
const scale = (factor, datapoints) => ts.scale_perf(datapoints, factor);
const offset = (delta, datapoints) => ts.offset(datapoints, delta);
const simpleMovingAverage = (n, datapoints) => ts.simpleMovingAverage(datapoints, n);
const expMovingAverage = (a, datapoints) => ts.expMovingAverage(datapoints, a);
const percentile = (interval, n, datapoints) => groupBy(datapoints, interval, _.partial(PERCENTILE, n));
function limit(order, n, orderByFunc, timeseries) {
let orderByCallback = aggregationFunctions[orderByFunc];
let sortByIteratee = (ts) => {
let values = _.map(ts.datapoints, (point) => {
const orderByCallback = aggregationFunctions[orderByFunc];
const sortByIteratee = (ts) => {
const values = _.map(ts.datapoints, (point) => {
return point[0];
});
return orderByCallback(values);
};
let sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
const sortedTimeseries = _.sortBy(timeseries, sortByIteratee);
if (order === 'bottom') {
return sortedTimeseries.slice(0, n);
} else {
@@ -64,13 +66,17 @@ function transformNull(n, datapoints) {
});
}
function sortSeries(direction, timeseries) {
return _.orderBy(timeseries, [function (ts) {
function sortSeries(direction, timeseries: any[]) {
return _.orderBy(timeseries, [ts => {
return ts.target.toLowerCase();
}], direction);
}
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;
return timeseries;
}
@@ -84,6 +90,10 @@ function replaceAlias(regexp, newAlias, timeseries) {
}
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;
return timeseries;
}
@@ -94,14 +104,13 @@ function setAliasByRegex(alias, timeseries) {
}
function extractText(str, pattern) {
var extractPattern = new RegExp(pattern);
var extractedValue = extractPattern.exec(str);
extractedValue = extractedValue[0];
return extractedValue;
const extractPattern = new RegExp(pattern);
const extractedValue = extractPattern.exec(str);
return extractedValue[0];
}
function groupByWrapper(interval, groupFunc, datapoints) {
var groupByCallback = aggregationFunctions[groupFunc];
const groupByCallback = aggregationFunctions[groupFunc];
return groupBy(datapoints, interval, groupByCallback);
}
@@ -110,12 +119,12 @@ function aggregateByWrapper(interval, aggregateFunc, datapoints) {
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = aggregationFunctions[aggregateFunc];
const groupByCallback = aggregationFunctions[aggregateFunc];
return groupBy(sortedPoints, interval, groupByCallback);
}
function aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = ts.flattenDatapoints(datapoints);
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
return groupBy(sortedPoints, interval, groupByCallback);
@@ -125,19 +134,19 @@ function percentileAgg(interval, n, datapoints) {
const flattenedPoints = ts.flattenDatapoints(datapoints);
// groupBy_perf works with sorted series only
const sortedPoints = ts.sortByTime(flattenedPoints);
let groupByCallback = _.partial(PERCENTILE, n);
const groupByCallback = _.partial(PERCENTILE, n);
return groupBy(sortedPoints, interval, groupByCallback);
}
function timeShift(interval, range) {
let shift = utils.parseTimeShiftInterval(interval) / 1000;
const shift = utils.parseTimeShiftInterval(interval) / 1000;
return _.map(range, time => {
return time - shift;
});
}
function unShiftTimeSeries(interval, datapoints) {
let unshift = utils.parseTimeShiftInterval(interval);
const unshift = utils.parseTimeShiftInterval(interval);
return _.map(datapoints, dp => {
return [
dp[0],
@@ -146,7 +155,7 @@ function unShiftTimeSeries(interval, datapoints) {
});
}
let metricFunctions = {
const metricFunctions = {
groupBy: groupByWrapper,
scale: scale,
offset: offset,
@@ -177,7 +186,7 @@ let metricFunctions = {
replaceAlias: replaceAlias
};
let aggregationFunctions = {
const aggregationFunctions = {
avg: AVERAGE,
min: MIN,
max: MAX,

View File

@@ -1,5 +1,6 @@
import _ from 'lodash';
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 utils from './utils';
import * as migrations from './migrations';
@@ -7,34 +8,44 @@ import * as metricFunctions from './metricFunctions';
import * as c from './constants';
import dataProcessor from './dataProcessor';
import responseHandler from './responseHandler';
import problemsHandler from './problemsHandler';
import { Zabbix } from './zabbix/zabbix';
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore';
import {
DataSourceApi,
// DataSourceInstanceSettings,
} 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;
import { VariableQueryTypes, ShowProblemTypes } from './types';
import { getBackendSrv } from '@grafana/runtime';
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
export class ZabbixDatasource extends DataSourceApi {
name: string;
url: string;
basicAuth: any;
withCredentials: any;
/**
* @ngInject
* @param {DataSourceInstanceSettings} instanceSettings
* @param {TemplateSrv} templateSrv
* @param {BackendSrv} backendSrv
* @param {DataSourceSrv} datasourceSrv
* @param {ZabbixAlertingService} zabbixAlertingSrv
*/
constructor(instanceSettings, templateSrv, backendSrv, datasourceSrv, zabbixAlertingSrv) {
username: string;
password: string;
trends: boolean;
trendsFrom: string;
trendsRange: string;
cacheTTL: any;
alertingEnabled: boolean;
addThresholds: boolean;
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);
this.type = 'zabbix';
this.templateSrv = templateSrv;
this.backendSrv = backendSrv;
this.zabbixAlertingSrv = zabbixAlertingSrv;
this.enableDebugLog = config.buildInfo.env === 'development';
@@ -61,7 +72,7 @@ export class ZabbixDatasource extends DataSourceApi {
this.trendsRange = jsonData.trendsRange || '4d';
// Set cache update interval
var ttl = jsonData.cacheTTL || '1h';
const ttl = jsonData.cacheTTL || '1h';
this.cacheTTL = utils.parseInterval(ttl);
// Alerting options
@@ -71,7 +82,6 @@ export class ZabbixDatasource extends DataSourceApi {
// Other options
this.disableReadOnlyUsersAck = jsonData.disableReadOnlyUsersAck;
this.zabbixVersion = jsonData.zabbixVersion || DEFAULT_ZABBIX_VERSION;
// Direct DB Connection options
this.enableDirectDBConnection = jsonData.dbConnectionEnable || false;
@@ -79,21 +89,21 @@ export class ZabbixDatasource extends DataSourceApi {
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
let zabbixOptions = {
const zabbixOptions = {
url: this.url,
username: this.username,
password: this.password,
basicAuth: this.basicAuth,
withCredentials: this.withCredentials,
zabbixVersion: this.zabbixVersion,
cacheTTL: this.cacheTTL,
enableDirectDBConnection: this.enableDirectDBConnection,
dbConnectionDatasourceId: this.dbConnectionDatasourceId,
dbConnectionDatasourceName: this.dbConnectionDatasourceName,
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
let promises = _.map(options.targets, t => {
const promises = _.map(options.targets, t => {
// Don't request for hidden targets
if (t.hide) {
return [];
@@ -144,40 +154,45 @@ export class ZabbixDatasource extends DataSourceApi {
this.replaceTargetVariables(target, options);
// Apply Time-related functions (timeShift(), etc)
let timeFunctions = bindFunctionDefs(target.functions, 'Time');
const timeFunctions = bindFunctionDefs(target.functions, 'Time');
if (timeFunctions.length) {
const [time_from, time_to] = utils.sequence(timeFunctions)([timeFrom, timeTo]);
timeFrom = time_from;
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
if (!target.mode || target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT) {
// Metrics or Text query
if (!target.queryType || target.queryType === c.MODE_METRICS || target.queryType === c.MODE_TEXT) {
// Don't request undefined targets
if (!target.group || !target.host || !target.item) {
return [];
}
if (!target.mode || target.mode === c.MODE_METRICS) {
if (!target.queryType || target.queryType === c.MODE_METRICS) {
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);
} else {
return [];
}
} else if (target.mode === c.MODE_ITEMID) {
// Item ID mode
} else if (target.queryType === c.MODE_ITEMID) {
// Item ID query
if (!target.itemids) {
return [];
}
return this.queryItemIdData(target, timeRange, useTrends, options);
} else if (target.mode === c.MODE_ITSERVICE) {
// IT services mode
} else if (target.queryType === c.MODE_ITSERVICE) {
// IT services query
return this.queryITServiceData(target, timeRange, options);
} else if (target.mode === c.MODE_TRIGGERS) {
// Triggers mode
} else if (target.queryType === c.MODE_TRIGGERS) {
// Triggers query
return this.queryTriggersData(target, timeRange);
} else if (target.queryType === c.MODE_PROBLEMS) {
// Problems query
return this.queryProblems(target, timeRange, options);
} else {
return [];
}
@@ -192,7 +207,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
doTsdbRequest(options) {
const tsdbRequestData = {
const tsdbRequestData: any = {
queries: options.targets.map(target => {
target.datasourceId = this.datasourceId;
target.queryType = 'zabbixAPI';
@@ -205,7 +220,7 @@ export class ZabbixDatasource extends DataSourceApi {
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) {
let queryStart, queryEnd;
let getItemOptions = {
const getItemOptions = {
itemtype: 'num'
};
return this.zabbix.getItemsFromTarget(target, getItemOptions)
@@ -242,7 +257,7 @@ export class ZabbixDatasource extends DataSourceApi {
}).then(result => {
queryEnd = new Date().getTime();
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;
});
@@ -269,18 +284,18 @@ export class ZabbixDatasource extends DataSourceApi {
getTrendValueType(target) {
// Find trendValue() function and get specified trend value
var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
var trendValueFunc = _.find(target.functions, func => {
const trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
const trendValueFunc = _.find(target.functions, func => {
return _.includes(trendFunctions, func.def.name);
});
return trendValueFunc ? trendValueFunc.params[0] : "avg";
}
applyDataProcessingFunctions(timeseries_data, target) {
let transformFunctions = bindFunctionDefs(target.functions, 'Transform');
let aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
let filterFunctions = bindFunctionDefs(target.functions, 'Filter');
let aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
const transformFunctions = bindFunctionDefs(target.functions, 'Transform');
const aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate');
const filterFunctions = bindFunctionDefs(target.functions, 'Filter');
const aliasFunctions = bindFunctionDefs(target.functions, 'Alias');
// Apply transformation functions
timeseries_data = _.cloneDeep(_.map(timeseries_data, timeseries => {
@@ -298,8 +313,8 @@ export class ZabbixDatasource extends DataSourceApi {
let dp = _.map(timeseries_data, 'datapoints');
dp = utils.sequence(aggregationFunctions)(dp);
let aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
let lastAgg = _.findLast(target.functions, func => {
const aggFuncNames = _.map(metricFunctions.getCategories()['Aggregate'], 'name');
const lastAgg = _.findLast(target.functions, func => {
return _.includes(aggFuncNames, func.def.name);
});
@@ -310,7 +325,7 @@ export class ZabbixDatasource extends DataSourceApi {
}
// Apply alias functions
_.forEach(timeseries_data, utils.sequence(aliasFunctions));
_.forEach(timeseries_data, utils.sequence(aliasFunctions).bind(this));
// Apply Time-related functions (timeShift(), etc)
// Find timeShift() function and get specified trend value
@@ -321,11 +336,11 @@ export class ZabbixDatasource extends DataSourceApi {
applyTimeShiftFunction(timeseries_data, target) {
// Find timeShift() function and get specified interval
let timeShiftFunc = _.find(target.functions, (func) => {
const timeShiftFunc = _.find(target.functions, (func) => {
return func.def.name === 'timeShift';
});
if (timeShiftFunc) {
let shift = timeShiftFunc.params[0];
const shift = timeShiftFunc.params[0];
_.forEach(timeseries_data, (series) => {
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) {
let options = {
const options = {
itemtype: 'text'
};
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) {
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) {
// Don't show undefined and hidden targets
@@ -382,21 +397,26 @@ export class ZabbixDatasource extends DataSourceApi {
itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars);
}
options.slaInterval = target.slaInterval;
return this.zabbix.getITServices(itServiceFilter)
.then(itservices => {
if (options.isOldVersion) {
itservices = _.filter(itservices, {'serviceid': target.itservice?.serviceid});
}
return this.zabbix.getSLA(itservices, timeRange, target, options);})
.then(itservicesdp => this.applyDataProcessingFunctions(itservicesdp, target));
}
queryTriggersData(target, timeRange) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
return this.zabbix.getHostsFromTarget(target)
.then(results => {
let [hosts, apps] = results;
const [hosts, apps] = results;
if (hosts.length) {
let hostids = _.map(hosts, 'hostid');
let appids = _.map(apps, 'applicationid');
let options = {
const hostids = _.map(hosts, 'hostid');
const appids = _.map(apps, 'applicationid');
const options = {
minSeverity: target.triggers.minSeverity,
acknowledged: target.triggers.acknowledged,
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.
*/
@@ -436,30 +528,26 @@ export class ZabbixDatasource extends DataSourceApi {
title: "Success",
message: message
};
}
catch (error) {
} catch (error) {
if (error instanceof ZabbixAPIError) {
return {
status: "error",
title: error.message,
message: error.message
};
}
else if (error.data && error.data.message) {
} else if (error.data && error.data.message) {
return {
status: "error",
title: "Zabbix Client Error",
message: error.data.message
};
}
else if (typeof (error) === 'string') {
} else if (typeof (error) === 'string') {
return {
status: "error",
title: "Unknown Error",
message: error
};
}
else {
} else {
console.log(error);
return {
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 //
////////////////
@@ -495,7 +569,7 @@ export class ZabbixDatasource extends DataSourceApi {
* @return {string} Metric name - group, host, app or item or list
* of metrics in "{metric1,metcic2,...,metricN}" format.
*/
metricFindQuery(query) {
metricFindQuery(query, options) {
let resultPromise;
let queryModel = _.cloneDeep(query);
@@ -512,6 +586,8 @@ export class ZabbixDatasource extends DataSourceApi {
queryModel[prop] = this.replaceTemplateVars(queryModel[prop], {});
}
const { group, host, application, item } = queryModel;
switch (queryModel.queryType) {
case VariableQueryTypes.Group:
resultPromise = this.zabbix.getGroups(queryModel.group);
@@ -525,6 +601,10 @@ export class ZabbixDatasource extends DataSourceApi {
case VariableQueryTypes.Item:
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item);
break;
case VariableQueryTypes.ItemValues:
const range = options?.range;
resultPromise = this.zabbix.getItemValues(group, host, application, item, { range });
break;
default:
resultPromise = Promise.resolve([]);
break;
@@ -543,72 +623,64 @@ export class ZabbixDatasource extends DataSourceApi {
const timeRange = options.range || options.rangeRaw;
const timeFrom = Math.ceil(dateMath.parse(timeRange.from) / 1000);
const timeTo = Math.ceil(dateMath.parse(timeRange.to) / 1000);
var annotation = options.annotation;
var showOkEvents = annotation.showOkEvents ? c.SHOW_ALL_EVENTS : c.SHOW_OK_EVENTS;
const annotation = options.annotation;
// Show all triggers
let triggersOptions = {
showTriggers: c.SHOW_ALL_TRIGGERS,
hideHostsInMaintenance: false
const problemsOptions: any = {
value: annotation.showOkEvents ? ['0', '1'] : '1',
valueFromEvent: true,
timeFrom,
timeTo,
};
var getTriggers = this.zabbix.getTriggers(this.replaceTemplateVars(annotation.group, {}),
this.replaceTemplateVars(annotation.host, {}),
this.replaceTemplateVars(annotation.application, {}),
triggersOptions);
return getTriggers.then(triggers => {
// Filter triggers by description
let triggerName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(triggerName)) {
triggers = _.filter(triggers, trigger => {
return utils.buildRegex(triggerName).test(trigger.description);
});
} else if (triggerName) {
triggers = _.filter(triggers, trigger => {
return trigger.description === triggerName;
});
if (annotation.minseverity) {
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity));
problemsOptions.severities = severities;
}
// Remove events below the chose severity
triggers = _.filter(triggers, trigger => {
return Number(trigger.priority) >= Number(annotation.minseverity);
});
const groupFilter = this.replaceTemplateVars(annotation.group, {});
const hostFilter = this.replaceTemplateVars(annotation.host, {});
const appFilter = this.replaceTemplateVars(annotation.application, {});
const proxyFilter = undefined;
var objectids = _.map(triggers, 'triggerid');
return this.zabbix
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
.then(events => {
var indexedTriggers = _.keyBy(triggers, 'triggerid');
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
.then(problems => {
// Filter triggers by description
const problemName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(problemName)) {
problems = _.filter(problems, p => {
return utils.buildRegex(problemName).test(p.description);
});
} else if (problemName) {
problems = _.filter(problems, p => {
return p.description === problemName;
});
}
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
events = _.filter(events, event => {
return !event.acknowledges.length;
problems = _.filter(problems, p => {
return !p.acknowledges?.length;
});
}
return _.map(events, event => {
let tags;
return _.map(problems, p => {
const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges);
let annotationTags: string[] = [];
if (annotation.showHostname) {
tags = _.map(event.hosts, 'name');
annotationTags = _.map(p.hosts, 'name');
}
// Show event type (OK or Problem)
let title = Number(event.value) ? 'Problem' : 'OK';
let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges);
return {
title: p.value === '1' ? 'Problem' : 'OK',
time: p.timestamp * 1000,
annotation: annotation,
time: event.clock * 1000,
title: title,
tags: tags,
text: indexedTriggers[event.objectid].description + formatted_acknowledges
text: p.name + formattedAcknowledges,
tags: annotationTags,
};
});
});
});
}
/**
@@ -617,8 +689,8 @@ export class ZabbixDatasource extends DataSourceApi {
* or empty object if no related triggers are finded.
*/
alertQuery(options) {
let enabled_targets = filterEnabledTargets(options.targets);
let getPanelItems = _.map(enabled_targets, t => {
const enabled_targets = filterEnabledTargets(options.targets);
const getPanelItems = _.map(enabled_targets, t => {
let target = _.cloneDeep(t);
target = migrations.migrate(target);
this.replaceTargetVariables(target, options);
@@ -627,8 +699,8 @@ export class ZabbixDatasource extends DataSourceApi {
return Promise.all(getPanelItems)
.then(results => {
let items = _.flatten(results);
let itemids = _.map(items, 'itemid');
const items = _.flatten(results);
const itemids = _.map(items, 'itemid');
if (itemids.length === 0) {
return [];
@@ -646,12 +718,12 @@ export class ZabbixDatasource extends DataSourceApi {
let state = 'ok';
let firedTriggers = _.filter(triggers, {value: '1'});
const firedTriggers = _.filter(triggers, {value: '1'});
if (firedTriggers.length) {
state = 'alerting';
}
let thresholds = _.map(triggers, trigger => {
const thresholds = _.map(triggers, trigger => {
return getTriggerThreshold(trigger.expression);
});
@@ -665,7 +737,7 @@ export class ZabbixDatasource extends DataSourceApi {
// Replace template variables
replaceTargetVariables(target, options) {
let parts = ['group', 'host', 'application', 'item'];
const parts = ['group', 'host', 'application', 'item'];
_.forEach(parts, p => {
if (target[p] && target[p].filter) {
target[p].filter = this.replaceTemplateVars(target[p].filter, options.scopedVars);
@@ -685,10 +757,10 @@ export class ZabbixDatasource extends DataSourceApi {
}
isUseTrends(timeRange) {
let [timeFrom, timeTo] = timeRange;
let useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
let useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
let useTrends = this.trends && (
const [timeFrom, timeTo] = timeRange;
const useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
const useTrendsRange = Math.ceil(utils.parseInterval(this.trendsRange) / 1000);
const useTrends = this.trends && (
(timeFrom < useTrendsFrom) ||
(timeTo - timeFrom > useTrendsRange)
);
@@ -697,20 +769,20 @@ export class ZabbixDatasource extends DataSourceApi {
}
function bindFunctionDefs(functionDefs, category) {
var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
var aggFuncDefs = _.filter(functionDefs, function(func) {
const aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
const aggFuncDefs = _.filter(functionDefs, func => {
return _.includes(aggregationFunctions, func.def.name);
});
return _.map(aggFuncDefs, function(func) {
var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return _.map(aggFuncDefs, func => {
const funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(dataProcessor.metricFunctions);
});
}
function getConsolidateBy(target) {
let consolidateBy;
let funcDef = _.find(target.functions, func => {
const funcDef = _.find(target.functions, func => {
return func.def.name === 'consolidateBy';
});
if (funcDef && funcDef.params && funcDef.params.length) {
@@ -720,8 +792,8 @@ function getConsolidateBy(target) {
}
function downsampleSeries(timeseries_data, options) {
let defaultAgg = dataProcessor.aggregationFunctions['avg'];
let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
const defaultAgg = dataProcessor.aggregationFunctions['avg'];
const consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
return _.map(timeseries_data, timeseries => {
if (timeseries.datapoints.length > options.maxDataPoints) {
timeseries.datapoints = dataProcessor
@@ -753,7 +825,7 @@ export function zabbixTemplateFormat(value) {
return utils.escapeRegex(value);
}
var escapedValues = _.map(value, utils.escapeRegex);
const escapedValues = _.map(value, utils.escapeRegex);
return '(' + escapedValues.join('|') + ')';
}
@@ -773,7 +845,7 @@ function zabbixItemIdsTemplateFormat(value) {
* /$variable/ -> /a|b|c/ -> /a|b|c/
*/
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)) {
replacedTarget = '/^' + replacedTarget + '$/';
}
@@ -787,8 +859,8 @@ function filterEnabledTargets(targets) {
}
function getTriggerThreshold(expression) {
let thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
let finded_thresholds = expression.match(thresholdPattern);
const thresholdPattern = /.*[<>=]{1,2}([\d\.]+)/;
const finded_thresholds = expression.match(thresholdPattern);
if (finded_thresholds && finded_thresholds.length >= 2) {
let threshold = finded_thresholds[1];
threshold = Number(threshold);
@@ -797,7 +869,3 @@ function getTriggerThreshold(expression) {
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 'jquery';
import { isNumeric } from './utils';
var index = [];
var categories = {
const index = [];
const categories = {
Transform: [],
Aggregate: [],
Filter: [],
@@ -298,11 +298,15 @@ addFuncDef({
defaultParams: ['avg'],
});
_.each(categories, function(funcList, catName) {
_.each(categories, (funcList, catName) => {
categories[catName] = _.sortBy(funcList, 'name');
});
class FuncInstance {
def: any;
params: any;
text: string;
constructor(funcDef, params) {
this.def = funcDef;
@@ -318,13 +322,13 @@ class FuncInstance {
}
bindFunction(metricFunctions) {
var func = metricFunctions[this.def.name];
const func = metricFunctions[this.def.name];
if (func) {
// Bind function arguments
var bindedFunc = func;
var param;
for (var i = 0; i < this.params.length; i++) {
let bindedFunc = func;
let param;
for (let i = 0; i < this.params.length; i++) {
param = this.params[i];
// Convert numeric params
@@ -341,23 +345,21 @@ class FuncInstance {
}
render(metricExp) {
var str = this.def.name + '(';
var parameters = _.map(this.params, function(value, index) {
var paramType = this.def.params[index].type;
const str = this.def.name + '(';
const parameters = _.map(this.params, (value, index) => {
const paramType = this.def.params[index].type;
if (paramType === 'int' ||
paramType === 'float' ||
paramType === 'value_or_series' ||
paramType === 'boolean') {
return value;
}
else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
} else if (paramType === 'int_or_interval' && isNumeric(value)) {
return value;
}
return "'" + value + "'";
}, this);
});
if (metricExp) {
parameters.unshift(metricExp);
@@ -378,16 +380,15 @@ class FuncInstance {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this._hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal, idx) {
_.each(strValue.split(','), (partVal, idx) => {
this.updateParam(partVal.trim(), idx);
}, this);
});
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
}else {
this.params[index] = strValue;
}
@@ -400,7 +401,7 @@ class FuncInstance {
return;
}
var text = this.def.name + '(';
let text = this.def.name + '(';
text += this.params.join(', ');
text += ')';
this.text = text;

View File

@@ -1,5 +1,6 @@
import _ from 'lodash';
import { ZabbixMetricsQuery } from './types';
import * as c from './constants';
/**
* Query format migration.
@@ -28,6 +29,34 @@ export function migrateFrom2To3version(target: ZabbixMetricsQuery) {
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) {
target.resultFormat = target.resultFormat || 'time_series';
target = fixTargetGroup(target);
@@ -35,6 +64,8 @@ export function migrate(target) {
return migrateFrom2To3version(target);
}
migratePercentileAgg(target);
migrateQueryType(target);
migrateSLA(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 function migrateDSConfig(jsonData) {
if (!jsonData) {

View File

@@ -73,15 +73,6 @@
placeholder="1h">
</input>
</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 class="gf-form-group">

View File

@@ -5,14 +5,14 @@
<label class="gf-form-label width-7">Query Mode</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-change="ctrl.switchEditorMode(ctrl.target.mode)"
ng-model="ctrl.target.mode"
ng-options="m.mode as m.text for m in ctrl.editorModes">
ng-change="ctrl.switchEditorMode(ctrl.target.queryType)"
ng-model="ctrl.target.queryType"
ng-options="m.queryType as m.text for m in ctrl.editorModes">
</select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.target.mode == editorMode.TEXT">
<label class="gf-form-label query-keyword width-8">Format As</label>
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Format As</label>
<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>
</div>
@@ -23,7 +23,7 @@
</div>
<!-- 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">
<label class="gf-form-label query-keyword width-7">IT Service</label>
<input type="text"
@@ -40,7 +40,7 @@
</input>
</div>
<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">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
@@ -49,12 +49,22 @@
</select>
</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-label gf-form-label--grow"></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 -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Group</label>
@@ -71,8 +81,8 @@
}"></input>
</div>
<!-- Select Host -->
<div class="gf-form">
<label class="gf-form-label query-keyword width-8">Host</label>
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Host</label>
<input type="text"
ng-model="ctrl.target.host.filter"
bs-typeahead="ctrl.getHostNames"
@@ -86,12 +96,27 @@
}">
</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-label gf-form-label--grow"></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 -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Application</label>
@@ -109,8 +134,8 @@
</div>
<!-- Select Item -->
<div class="gf-form" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT">
<label class="gf-form-label query-keyword width-8">Item</label>
<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-7">Item</label>
<input type="text"
ng-model="ctrl.target.item.filter"
bs-typeahead="ctrl.getItemNames"
@@ -124,62 +149,93 @@
}">
</div>
<div class="gf-form max-width-23" ng-show="ctrl.target.mode == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-8">Min Severity</label>
<div class="gf-form-select-wrapper width-16">
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Problem</label>
<input type="text"
ng-model="ctrl.target.trigger.filter"
ng-blur="ctrl.onTargetBlur()"
placeholder="Problem name"
class="gf-form-input"
ng-style="ctrl.target.trigger.style"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.trigger.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.trigger.filter)
}"
empty-to-null>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Tags</label>
<input type="text" class="gf-form-input width-14"
ng-model="ctrl.target.tags.filter"
ng-blur="ctrl.onTargetBlur()"
placeholder="tag1:value1, tag2:value2">
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Show</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-model="ctrl.target.showProblems"
ng-options="v.value as v.text for v in ctrl.showProblemsOptions"
ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Min severity</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-model="ctrl.target.options.minSeverity"
ng-options="v.val as v.text for v in ctrl.severityOptions"
ng-change="ctrl.onTargetBlur()">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-7">Min Severity</label>
<div class="gf-form-select-wrapper width-14">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.minSeverity"
ng-options="s.val as s.text for s in ctrl.triggerSeverity">
ng-options="s.val as s.text for s in ctrl.severityOptions">
</select>
</div>
</div>
<div class="gf-form max-width-20" ng-show="ctrl.target.mode == editorMode.TRIGGERS">
<label class="gf-form-label query-keyword width-8">Acknowledged</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.triggers.acknowledged"
ng-options="a.value as a.text for a in ctrl.ackFilters">
</select>
</div>
</div>
<gf-form-switch class="gf-form" label="Count" ng-show="ctrl.target.mode == editorMode.TRIGGERS"
checked="ctrl.target.triggers.count" on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<label class="gf-form-label gf-form-label--grow">
<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 class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Query options -->
<div class="gf-form-group" ng-if="ctrl.showQueryOptions">
<div class="gf-form offset-width-7" ng-hide="ctrl.target.mode == editorMode.TRIGGERS">
<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>
<!-- 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>
<div class="gf-form offset-width-7" ng-show="ctrl.target.mode === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
<gf-form-switch class="gf-form" label-class="width-10"
label="Skip empty values"
checked="ctrl.target.options.skipEmptyValues"
on-change="ctrl.onQueryOptionChange()">
<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>
<!-- 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">
<label class="gf-form-label query-keyword width-7">Item IDs</label>
<input type="text"
@@ -201,7 +257,7 @@
</div>
<!-- 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">
<label class="gf-form-label query-keyword width-7">Functions</label>
</div>
@@ -215,23 +271,82 @@
</div>
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == 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 class="gf-form gf-form--grow">
<label class="gf-form-label gf-form-label--grow">
<a ng-click="ctrl.toggleQueryOptions()">
<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>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
<!-- Query options -->
<div class="gf-form-group offset-width-7" ng-if="ctrl.showQueryOptions">
<div class="gf-form" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<gf-form-switch class="gf-form" label-class="width-10"
label="Show disabled items"
checked="ctrl.target.options.showDisabledItems"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
<gf-form-switch class="gf-form" label-class="width-10"
label="Skip empty values"
checked="ctrl.target.options.skipEmptyValues"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
</div>
<div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
label-class="width-9"
label="Count"
checked="ctrl.target.triggers.count"
on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label width-9">Acknowledged</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-change="ctrl.onQueryOptionChange()"
ng-model="ctrl.target.options.acknowledged"
ng-options="a.value as a.text for a in ctrl.ackFilters">
</select>
</div>
</div>
<div ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form">
<label class="gf-form-label width-9">Sort by</label>
<div class="gf-form-select-wrapper width-12">
<select class="gf-form-input"
ng-model="ctrl.target.options.sortProblems"
ng-options="f.value as f.text for f in ctrl.sortByFields"
ng-change="ctrl.onQueryOptionChange()">
</select>
</div>
</div>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Hosts in maintenance"
checked="ctrl.target.options.hostsInMaintenance"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label-class="width-9"
label="Host proxy"
checked="ctrl.target.options.hostProxy"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-9">Limit triggers</label>
<input class="gf-form-input width-5"
type="number" placeholder="100"
ng-model="ctrl.target.options.limit"
ng-model-onblur ng-change="ctrl.onQueryOptionChange()">
</div>
</div>
</div>
</div>
</query-editor-row>

View File

@@ -38,8 +38,8 @@
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
},
"logos": {
"small": "img/zabbix_app_logo.svg",
"large": "img/zabbix_app_logo.svg"
"small": "img/icn-zabbix-datasource.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 metricFunctions from './metricFunctions';
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 {
@@ -17,11 +69,12 @@ export class ZabbixQueryController extends QueryCtrl {
this.templateSrv = templateSrv;
this.editorModes = [
{value: 'num', text: 'Metrics', mode: c.MODE_METRICS},
{value: 'text', text: 'Text', mode: c.MODE_TEXT},
{value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE},
{value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID},
{value: 'triggers', text: 'Triggers', mode: c.MODE_TRIGGERS}
{value: 'num', text: 'Metrics', queryType: c.MODE_METRICS},
{value: 'text', text: 'Text', queryType: c.MODE_TEXT},
{value: 'itservice', text: 'IT Services', queryType: c.MODE_ITSERVICE},
{value: 'itemid', text: 'Item ID', queryType: c.MODE_ITEMID},
{value: 'triggers', text: 'Triggers', queryType: c.MODE_TRIGGERS},
{value: 'problems', text: 'Problems', queryType: c.MODE_PROBLEMS},
];
this.$scope.editorMode = {
@@ -29,7 +82,8 @@ export class ZabbixQueryController extends QueryCtrl {
TEXT: c.MODE_TEXT,
ITSERVICE: c.MODE_ITSERVICE,
ITEMID: c.MODE_ITEMID,
TRIGGERS: c.MODE_TRIGGERS
TRIGGERS: c.MODE_TRIGGERS,
PROBLEMS: c.MODE_PROBLEMS,
};
this.slaPropertyList = [
@@ -40,15 +94,49 @@ export class ZabbixQueryController extends QueryCtrl {
{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 = [
{text: 'all triggers', value: 2},
{text: 'unacknowledged', value: 0},
{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.triggerSeverity = c.TRIGGER_SEVERITY;
this.severityOptions = getSeverityOptions();
// Map functions for bs-typeahead
this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList');
@@ -56,6 +144,7 @@ export class ZabbixQueryController extends QueryCtrl {
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
this.getProxyNames = _.bind(this.getMetricNames, this, 'proxyList');
this.getVariables = _.bind(this.getTemplateVariables, this);
// Update metric suggestion when template variable was changed
@@ -80,40 +169,32 @@ export class ZabbixQueryController extends QueryCtrl {
_.defaults(this, scopeDefaults);
// Load default values
var targetDefaults = {
'mode': c.MODE_METRICS,
'group': { 'filter': "" },
'host': { 'filter': "" },
'application': { 'filter': "" },
'item': { 'filter': "" },
'functions': [],
'triggers': {
'count': true,
'minSeverity': 3,
'acknowledged': 2
},
'options': {
'showDisabledItems': false,
'skipEmptyValues': false
},
'table': {
'skipEmptyValues': false
const targetDefaults = getTargetDefaults();
_.defaultsDeep(target, targetDefaults);
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
target.queryType = c.MODE_PROBLEMS;
}
};
_.defaults(target, targetDefaults);
// Create function instances from saved JSON
target.functions = _.map(target.functions, function(func) {
return metricFunctions.createFuncInstance(func.def, func.params);
});
if (target.mode === c.MODE_METRICS ||
target.mode === c.MODE_TEXT ||
target.mode === c.MODE_TRIGGERS) {
this.initFilters();
if (target.queryType === c.MODE_ITSERVICE) {
_.defaultsDeep(target, getSLATargetDefaults());
}
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();
}
};
@@ -123,14 +204,20 @@ export class ZabbixQueryController extends QueryCtrl {
}
initFilters() {
let itemtype = _.find(this.editorModes, {'mode': this.target.mode});
let itemtype = _.find(this.editorModes, {'queryType': this.target.queryType});
itemtype = itemtype ? itemtype.value : null;
return Promise.all([
const promises = [
this.suggestGroups(),
this.suggestHosts(),
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
@@ -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) {
return utils.isRegex(str);
}
@@ -302,19 +398,42 @@ export class ZabbixQueryController extends QueryCtrl {
}
renderQueryOptionsText() {
var optionsMap = {
const metricOptionsMap = {
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) => {
if (value) {
if (value && optionsMap[key]) {
if (value === true) {
// Show only option name (if enabled) for boolean options
options.push(optionsMap[key]);
} else {
// 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
*/
switchEditorMode(mode) {
this.target.mode = mode;
this.target.queryType = mode;
this.queryOptionsText = this.renderQueryOptionsText();
this.init();
this.targetChanged();
}

View File

@@ -29,3 +29,7 @@ This mode is suitable for rendering charts in grafana by passing itemids as url
##### Triggers
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
var grouped_history = _.groupBy(history, 'itemid');
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
const grouped_history = _.groupBy(history, 'itemid');
const hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
return _.map(grouped_history, function(historyPoint, itemid) {
var item = _.find(items, {'itemid': itemid});
var alias = item.name;
if (_.keys(hosts).length > 1 && addHostName) { //only when actual multi hosts selected
var host = _.find(hosts, {'hostid': item.hostid});
return _.map(grouped_history, (hist, itemid) => {
const item = _.find(items, {'itemid': itemid}) as any;
let alias = item.name;
// Add scopedVars for using in alias functions
const scopedVars: any = {
'__zbx_item': { value: item.name },
'__zbx_item_name': { value: item.name },
'__zbx_item_key': { value: item.key_ },
};
if (_.keys(hosts).length > 0) {
const host = _.find(hosts, {'hostid': item.hostid});
scopedVars['__zbx_host'] = { value: host.host };
scopedVars['__zbx_host_name'] = { value: host.name };
// Only add host when multiple hosts selected
if (_.keys(hosts).length > 1 && addHostName) {
alias = host.name + ": " + alias;
}
}
return {
target: alias,
datapoints: _.map(historyPoint, convertPointCallback)
datapoints: _.map(hist, convertPointCallback),
scopedVars,
};
});
}
@@ -53,26 +69,26 @@ function handleHistory(history, items, 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);
}
function handleText(history, items, target, addHostName = true) {
let convertTextCallback = _.partial(convertText, target);
const convertTextCallback = _.partial(convertText, target);
return convertHistory(history, items, addHostName, convertTextCallback);
}
function handleHistoryAsTable(history, items, target) {
let table = new TableModel();
const table: any = new TableModel();
table.addColumn({text: 'Host'});
table.addColumn({text: 'Item'});
table.addColumn({text: 'Key'});
table.addColumn({text: 'Last value'});
let grouped_history = _.groupBy(history, 'itemid');
const grouped_history = _.groupBy(history, 'itemid');
_.each(items, (item) => {
let itemHistory = grouped_history[item.itemid] || [];
let lastPoint = _.last(itemHistory);
const itemHistory = grouped_history[item.itemid] || [];
const lastPoint = _.last(itemHistory);
let lastValue = lastPoint ? lastPoint.value : null;
if (target.options.skipEmptyValues && (!lastValue || lastValue === '')) {
@@ -84,7 +100,7 @@ function handleHistoryAsTable(history, items, target) {
lastValue = extractText(lastValue, target.textFilter, target.useCaptureGroups);
}
let host = _.first(item.hosts);
let host: any = _.first(item.hosts);
host = host ? host.name : "";
table.rows.push([
@@ -110,22 +126,22 @@ function convertText(target, point) {
}
function extractText(str, pattern, useCaptureGroups) {
let extractPattern = new RegExp(pattern);
let extractedValue = extractPattern.exec(str);
const extractPattern = new RegExp(pattern);
const extractedValue = extractPattern.exec(str);
if (extractedValue) {
if (useCaptureGroups) {
extractedValue = extractedValue[1];
return extractedValue[1];
} else {
extractedValue = extractedValue[0];
return extractedValue[0];
}
}
return extractedValue;
return "";
}
function handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla;
const targetSLA = slaObject[itservice.serviceid].sla;
if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10);
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
@@ -134,7 +150,7 @@ function handleSLAResponse(itservice, slaProperty, slaObject) {
};
} else {
let i;
let slaArr = [];
const slaArr = [];
for (i = 0; i < targetSLA.length; i++) {
if (i === 0) {
slaArr.push([targetSLA[i][slaProperty.property], targetSLA[i].from * 1000]);
@@ -165,7 +181,7 @@ function handleTriggersResponse(triggers, groups, timeRange) {
} else {
const stats = getTriggerStats(triggers);
const groupNames = _.map(groups, 'name');
let table = new TableModel();
const table: any = new TableModel();
table.addColumn({text: 'Host group'});
_.each(_.orderBy(c.TRIGGER_SEVERITY, ['val'], ['desc']), (severity) => {
table.addColumn({text: severity.text});
@@ -182,9 +198,9 @@ function handleTriggersResponse(triggers, groups, timeRange) {
}
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 stats = {};
const stats = {};
_.each(groups, (group) => {
stats[group] = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0}; // severity:count
});
@@ -205,7 +221,7 @@ function convertHistoryPoint(point) {
}
function convertTrendPoint(valueType, point) {
var value;
let value;
switch (valueType) {
case "min":
value = point.value_min;
@@ -242,6 +258,3 @@ export default {
handleTriggersResponse,
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 { dateMath } from '@grafana/data';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
}),
}), {virtual: true});
describe('ZabbixDatasource', () => {
let ctx = {};
@@ -21,11 +27,11 @@ describe('ZabbixDatasource', () => {
};
ctx.templateSrv = mocks.templateSrvMock;
ctx.backendSrv = mocks.backendSrvMock;
// ctx.backendSrv = mocks.backendSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock;
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', () => {
@@ -119,7 +125,7 @@ describe('ZabbixDatasource', () => {
item: {filter: "System information"},
textFilter: "",
useCaptureGroups: true,
mode: 2,
queryType: 2,
resultFormat: "table",
options: {
skipEmptyValues: false

View File

@@ -1,11 +1,17 @@
import mocks from '../../test-setup/mocks';
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', () => {
let ctx = {};
const datasourceSrv = mocks.datasourceSrvMock;
datasourceSrv.loadDatasource.mockResolvedValue({ id: 42, name: 'foo', meta: {} });
datasourceSrv.getAll.mockReturnValue([{ id: 42, name: 'foo' }]);
const ctx: any = {};
describe('When init DB connector', () => {
beforeEach(() => {
@@ -13,34 +19,34 @@ describe('DBConnector', () => {
datasourceId: 42,
datasourceName: undefined
};
loadDatasourceMock.mockClear();
getAllMock.mockClear();
});
it('should try to load datasource by name first', () => {
ctx.options = {
datasourceName: 'bar'
};
const dbConnector = new DBConnector(ctx.options, datasourceSrv);
const dbConnector = new DBConnector({ datasourceName: 'bar' });
dbConnector.loadDBDataSource();
expect(datasourceSrv.getAll).not.toHaveBeenCalled();
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('bar');
expect(getAllMock).not.toHaveBeenCalled();
expect(loadDatasourceMock).toHaveBeenCalledWith('bar');
});
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();
expect(datasourceSrv.getAll).toHaveBeenCalled();
expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('foo');
expect(getAllMock).toHaveBeenCalled();
expect(loadDatasourceMock).toHaveBeenCalledWith('foo');
});
it('should throw error if no name and id specified', () => {
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');
});
it('should throw error if datasource with given id is not found', () => {
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');
});
});

View File

@@ -1,17 +1,20 @@
import { InfluxDBConnector } from '../zabbix/connectors/influxdb/influxdbConnector';
import { compactQuery } from '../utils';
jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: jest.fn(() => ({
loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
})),
}));
describe('InfluxDBConnector', () => {
let ctx = {};
beforeEach(() => {
ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
ctx.datasourceSrvMock = {
loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} }
),
};
ctx.influxDBConnector = new InfluxDBConnector(ctx.options, ctx.datasourceSrvMock);
ctx.influxDBConnector = new InfluxDBConnector(ctx.options);
ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]);
ctx.defaultQueryParams = {
itemids: ['123', '234'],

View File

@@ -20,35 +20,30 @@ const POINT_TIMESTAMP = 1;
* Downsample time series by using given function (avg, min, max).
*/
function downsample(datapoints, time_to, ms_interval, func) {
var downsampledSeries = [];
var timeWindow = {
const downsampledSeries = [];
const timeWindow = {
from: time_to * 1000 - ms_interval,
to: time_to * 1000
};
var points_sum = 0;
var points_num = 0;
var value_avg = 0;
var frame = [];
let points_sum = 0;
let points_num = 0;
let value_avg = 0;
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) {
points_sum += datapoints[i][0];
points_num++;
frame.push(datapoints[i][0]);
}
else {
} else {
value_avg = points_num ? points_sum / points_num : 0;
if (func === "max") {
downsampledSeries.push([_.max(frame), timeWindow.to]);
}
else if (func === "min") {
} else if (func === "min") {
downsampledSeries.push([_.min(frame), timeWindow.to]);
}
// avg by default
else {
} else {
downsampledSeries.push([value_avg, timeWindow.to]);
}
@@ -72,25 +67,25 @@ function downsample(datapoints, time_to, ms_interval, func) {
* datapoints: [[<value>, <unixtime>], ...]
*/
function groupBy(datapoints, interval, groupByCallback) {
var ms_interval = utils.parseInterval(interval);
const ms_interval = utils.parseInterval(interval);
// Calculate frame timestamps
var frames = _.groupBy(datapoints, function (point) {
const frames = _.groupBy(datapoints, point => {
// Calculate time for group of points
return Math.floor(point[1] / ms_interval) * ms_interval;
});
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
var grouped = _.mapValues(frames, function (frame) {
var points = _.map(frame, function (point) {
const grouped = _.mapValues(frames, frame => {
const points = _.map(frame, point => {
return point[0];
});
return groupByCallback(points);
});
// Convert points to Grafana format
return sortByTime(_.map(grouped, function (value, timestamp) {
return sortByTime(_.map(grouped, (value, timestamp) => {
return [Number(value), Number(timestamp)];
}));
}
@@ -104,8 +99,8 @@ export function groupBy_perf(datapoints, interval, groupByCallback) {
return groupByRange(datapoints, groupByCallback);
}
let ms_interval = utils.parseInterval(interval);
let grouped_series = [];
const ms_interval = utils.parseInterval(interval);
const grouped_series = [];
let frame_values = [];
let frame_value;
let frame_ts = datapoints.length ? getPointTimeFrame(datapoints[0][POINT_TIMESTAMP], ms_interval) : 0;
@@ -157,30 +152,30 @@ export function groupByRange(datapoints, groupByCallback) {
function sumSeries(timeseries) {
// 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];
}));
new_timestamps = _.sortBy(new_timestamps);
var interpolated_timeseries = _.map(timeseries, function (series) {
const interpolated_timeseries = _.map(timeseries, series => {
series = fillZeroes(series, new_timestamps);
var timestamps = _.map(series, function (point) {
const timestamps = _.map(series, point => {
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];
});
var new_series = series.concat(new_points);
const new_series = series.concat(new_points);
return sortByTime(new_series);
});
_.each(interpolated_timeseries, interpolateSeries);
var new_timeseries = [];
var sum;
for (var i = new_timestamps.length - 1; i >= 0; i--) {
const new_timeseries = [];
let sum;
for (let i = new_timestamps.length - 1; i >= 0; i--) {
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];
}
new_timeseries.push([sum, new_timestamps[i]]);
@@ -225,9 +220,9 @@ function offset(datapoints, delta) {
* @param {*} datapoints
*/
function delta(datapoints) {
let newSeries = [];
const newSeries = [];
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];
newSeries.push([deltaValue, datapoints[i][1]]);
}
@@ -239,7 +234,7 @@ function delta(datapoints) {
* @param {*} datapoints
*/
function rate(datapoints) {
let newSeries = [];
const newSeries = [];
let point, point_prev;
let valueDelta = 0;
let timeDelta = 0;
@@ -261,7 +256,7 @@ function rate(datapoints) {
}
function simpleMovingAverage(datapoints, n) {
let sma = [];
const sma = [];
let w_sum;
let w_avg = null;
let w_count = 0;
@@ -352,7 +347,7 @@ function expMovingAverage(datapoints, n) {
}
function PERCENTILE(n, values) {
var sorted = _.sortBy(values);
const sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length * n / 100)];
}
@@ -361,7 +356,7 @@ function COUNT(values) {
}
function SUM(values) {
var sum = null;
let sum = null;
for (let i = 0; i < values.length; i++) {
if (values[i] !== null) {
sum += values[i];
@@ -371,7 +366,7 @@ function SUM(values) {
}
function AVERAGE(values) {
let values_non_null = getNonNullValues(values);
const values_non_null = getNonNullValues(values);
if (values_non_null.length === 0) {
return null;
}
@@ -379,7 +374,7 @@ function AVERAGE(values) {
}
function getNonNullValues(values) {
let values_non_null = [];
const values_non_null = [];
for (let i = 0; i < values.length; i++) {
if (values[i] !== null) {
values_non_null.push(values[i]);
@@ -397,7 +392,7 @@ function MAX(values) {
}
function MEDIAN(values) {
var sorted = _.sortBy(values);
const sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length / 2)];
}
@@ -418,7 +413,7 @@ function getPointTimeFrame(timestamp, ms_interval) {
}
function sortByTime(series) {
return _.sortBy(series, function (point) {
return _.sortBy(series, point => {
return point[1];
});
}
@@ -432,8 +427,8 @@ function sortByTime(series) {
* @param {*} timestamps
*/
function fillZeroes(series, timestamps) {
let prepend = [];
let append = [];
const prepend = [];
const append = [];
let new_point;
for (let i = 0; i < timestamps.length; i++) {
if (timestamps[i] < series[0][POINT_TIMESTAMP]) {
@@ -451,10 +446,10 @@ function fillZeroes(series, timestamps) {
* Interpolate series with gaps
*/
function interpolateSeries(series) {
var left, right;
let left, right;
// Interpolate series
for (var i = series.length - 1; i >= 0; i--) {
for (let i = series.length - 1; i >= 0; i--) {
if (!series[i][0]) {
left = findNearestLeft(series, i);
right = findNearestRight(series, i);
@@ -479,7 +474,7 @@ function linearInterpolation(timestamp, left, right) {
}
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) {
return series[i];
}
@@ -488,7 +483,7 @@ function findNearestRight(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) {
return series[i];
}

View File

@@ -111,4 +111,183 @@ export enum VariableQueryTypes {
Host = 'host',
Application = 'application',
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]
* @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:
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
@@ -78,13 +78,26 @@ export function containsMacro(itemName) {
return MACRO_PATTERN.test(itemName);
}
export function replaceMacro(item, macros) {
let itemName = item.name;
export function replaceMacro(item, macros, isTriggerItem?) {
let itemName = isTriggerItem ? item.url : item.name;
const item_macros = itemName.match(MACRO_PATTERN);
_.forEach(item_macros, macro => {
const host_macros = _.filter(macros, m => {
if (m.hostid) {
if (isTriggerItem) {
// Trigger item can have multiple hosts
// Check all trigger host ids against macro host id
let hostIdFound = false;
_.forEach(item.hosts, h => {
if (h.hostid === m.hostid) {
hostIdFound = true;
}
});
return hostIdFound;
} else {
// Check app host id against macro host id
return m.hostid === item.hostid;
}
} else {
// Add global macros
return true;
@@ -222,10 +235,11 @@ export function escapeRegex(value) {
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 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) {
@@ -314,7 +328,7 @@ export function isValidVersion(version) {
return versionPattern.exec(version);
}
export function parseVersion(version) {
export function parseVersion(version: string) {
const match = versionPattern.exec(version);
if (!match) {
return null;
@@ -344,7 +358,29 @@ export function getArrayDepth(a, level = 0) {
return level + 1;
}
// Fix for backward compatibility with lodash 2.4
if (!_.includes) {
_.includes = (_ as any).contains;
/**
* Checks whether its argument represents a numeric value.
*/
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 { getDataSourceSrv } from '@grafana/runtime';
export const DEFAULT_QUERY_LIMIT = 10000;
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.
*/
export class DBConnector {
constructor(options, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
constructor(options) {
this.datasourceId = options.datasourceId;
this.datasourceName = options.datasourceName;
this.datasourceTypeId = null;
this.datasourceTypeName = null;
}
static loadDatasource(dsId, dsName, datasourceSrv) {
static loadDatasource(dsId, dsName) {
if (!dsName && dsId !== undefined) {
let ds = _.find(datasourceSrv.getAll(), {'id': dsId});
let ds = _.find(getDataSourceSrv().getAll(), {'id': dsId});
if (!ds) {
return Promise.reject(`Data Source with ID ${dsId} not found`);
}
dsName = ds.name;
}
if (dsName) {
return datasourceSrv.loadDatasource(dsName);
return getDataSourceSrv().loadDatasource(dsName);
} else {
return Promise.reject(`Data Source name should be specified`);
}
}
loadDBDataSource() {
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName, this.datasourceSrv)
return DBConnector.loadDatasource(this.datasourceId, this.datasourceName)
.then(ds => {
this.datasourceTypeId = ds.meta.id;
this.datasourceTypeName = ds.meta.name;
@@ -123,22 +123,36 @@ export class ZabbixNotImplemented {
*/
function convertGrafanaTSResponse(time_series, items, addHostName) {
//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 itemid = series.name;
var item = _.find(items, {'itemid': itemid});
var alias = item.name;
//only when actual multi hosts selected
const itemid = series.name;
const item = _.find(items, {'itemid': itemid});
let alias = item.name;
// Add scopedVars for using in alias functions
const scopedVars = {
'__zbx_item': { value: item.name },
'__zbx_item_name': { value: item.name },
'__zbx_item_key': { value: item.key_ },
};
if (_.keys(hosts).length > 0) {
const host = _.find(hosts, {'hostid': item.hostid});
scopedVars['__zbx_host'] = { value: host.host };
scopedVars['__zbx_host_name'] = { value: host.name };
// Only add host when multiple hosts selected
if (_.keys(hosts).length > 1 && addHostName) {
var host = _.find(hosts, {'hostid': item.hostid});
alias = host.name + ": " + alias;
}
}
// CachingProxy deduplicates requests and returns one time series for equal queries.
// Clone is needed to prevent changing of series object shared between all targets.
let datapoints = _.cloneDeep(series.points);
const datapoints = _.cloneDeep(series.points);
return {
target: alias,
datapoints: datapoints
datapoints,
scopedVars,
};
});

View File

@@ -11,8 +11,8 @@ const consolidateByFunc = {
};
export class InfluxDBConnector extends DBConnector {
constructor(options, datasourceSrv) {
super(options, datasourceSrv);
constructor(options) {
super(options);
this.retentionPolicy = options.retentionPolicy;
super.loadDBDataSource().then(ds => {
this.influxDS = ds;
@@ -24,7 +24,14 @@ export class InfluxDBConnector extends DBConnector {
* Try to invoke test query for one of Zabbix database tables.
*/
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) {

View File

@@ -3,26 +3,24 @@
*/
function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) {
let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
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}
WHERE itemid IN (${itemids})
AND clock > ${timeFrom} AND clock < ${timeTill}
GROUP BY ${time_expression}, metric
GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric
ORDER BY time_sec ASC
`;
return query;
}
function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) {
let time_expression = `clock DIV ${intervalSec} * ${intervalSec}`;
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}
WHERE itemid IN (${itemids})
AND clock > ${timeFrom} AND clock < ${timeTill}
GROUP BY ${time_expression}, metric
GROUP BY (clock-${timeFrom}) DIV ${intervalSec}, metric
ORDER BY time_sec ASC
`;
return query;

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import { getBackendSrv } from '@grafana/runtime';
import { compactQuery } from '../../../utils';
import mysql from './mysql';
import postgres from './postgres';
@@ -10,15 +11,14 @@ const supportedDatabases = {
};
export class SQLConnector extends DBConnector {
constructor(options, datasourceSrv) {
super(options, datasourceSrv);
constructor(options) {
super(options);
this.limit = options.limit || DEFAULT_QUERY_LIMIT;
this.sqlDialect = null;
super.loadDBDataSource()
.then(ds => {
this.backendSrv = ds.backendSrv;
.then(() => {
this.loadSQLDialect();
});
}
@@ -43,6 +43,12 @@ export class SQLConnector extends DBConnector {
let {intervalMs, consolidateBy} = options;
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';
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
@@ -66,6 +72,12 @@ export class SQLConnector extends DBConnector {
let { intervalMs, consolidateBy } = options;
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';
let aggFunction = dbConnector.consolidateByFunc[consolidateBy];
@@ -96,7 +108,7 @@ export class SQLConnector extends DBConnector {
maxDataPoints: this.limit
};
return this.backendSrv.datasourceRequest({
return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
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 semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, 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.
@@ -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.
*/
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.username = username;
this.password = password;
this.auth = '';
this.version = version;
this.requestOptions = {
basicAuth: basicAuth,
@@ -23,16 +42,17 @@ export class ZabbixAPIConnector {
};
this.datasourceId = datasourceId;
this.backendSrv = backendSrv;
this.loginPromise = null;
this.loginErrorCount = 0;
this.maxLoginAttempts = 3;
this.zabbixAPICore = new ZabbixAPICore(backendSrv);
this.zabbixAPICore = new ZabbixAPICore();
this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30;
this.initVersion();
}
//////////////////////////
@@ -59,13 +79,40 @@ export class ZabbixAPIConnector {
}],
};
return this.backendSrv.datasourceRequest({
return getBackendSrv().datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
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) {
if (!response || !response.data || !response.data.results) {
return [];
@@ -78,9 +125,8 @@ export class ZabbixAPIConnector {
* 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
* 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) {
this.loginPromise = Promise.resolve(
this.login().then(auth => {
@@ -96,7 +142,7 @@ export class ZabbixAPIConnector {
/**
* Get authentication token.
*/
login() {
login(): Promise<string> {
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);
}
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 //
////////////////////////////////
acknowledgeEvent(eventid, message) {
const action = this.version >= 4 ? ZBX_ACK_ACTION_ACK + ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE;
const params = {
acknowledgeEvent(eventid: string, message: string, action?: number, severity?: number) {
if (!action) {
action = semver.gte(this.version, '4.0.0') ? ZBX_ACK_ACTION_ADD_MESSAGE : ZBX_ACK_ACTION_NONE;
}
const params: any = {
eventids: eventid,
message: message,
action: action
};
if (severity) {
params.severity = severity;
}
return this.request('event.acknowledge', params);
}
getGroups() {
var params = {
const params = {
output: ['name'],
sortfield: 'name',
real_hosts: true
@@ -133,7 +205,7 @@ export class ZabbixAPIConnector {
}
getHosts(groupids) {
var params = {
const params: any = {
output: ['name', 'host'],
sortfield: 'name'
};
@@ -144,8 +216,8 @@ export class ZabbixAPIConnector {
return this.request('host.get', params);
}
getApps(hostids) {
var params = {
getApps(hostids): Promise<any[]> {
const params = {
output: 'extend',
hostids: hostids
};
@@ -161,7 +233,7 @@ export class ZabbixAPIConnector {
* @return {[type]} array of items
*/
getItems(hostids, appids, itemtype) {
var params = {
const params: any = {
output: [
'name', 'key_',
'value_type',
@@ -172,7 +244,7 @@ export class ZabbixAPIConnector {
sortfield: 'name',
webitems: true,
filter: {},
selectHosts: ['hostid', 'name']
selectHosts: ['hostid', 'name', 'host']
};
if (hostids) {
params.hostids = hostids;
@@ -194,7 +266,7 @@ export class ZabbixAPIConnector {
}
getItemsByIDs(itemids) {
var params = {
const params = {
itemids: itemids,
output: [
'name', 'key_',
@@ -208,11 +280,11 @@ export class ZabbixAPIConnector {
};
return this.request('item.get', params)
.then(utils.expandItems);
.then(items => utils.expandItems(items));
}
getMacros(hostids) {
var params = {
const params = {
output: 'extend',
hostids: hostids
};
@@ -221,7 +293,7 @@ export class ZabbixAPIConnector {
}
getGlobalMacros() {
var params = {
const params = {
output: 'extend',
globalmacro: true
};
@@ -230,7 +302,7 @@ export class ZabbixAPIConnector {
}
getLastValue(itemid) {
var params = {
const params = {
output: ['lastvalue'],
itemids: itemid
};
@@ -249,10 +321,10 @@ export class ZabbixAPIConnector {
getHistory(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
let itemids = _.map(items, 'itemid');
let params = {
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid');
const params: any = {
output: 'extend',
history: value_type,
itemids: itemids,
@@ -284,10 +356,10 @@ export class ZabbixAPIConnector {
getTrend_ZBXNEXT1193(items, timeFrom, timeTill) {
// Group items by value type and perform request for each value type
let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
let itemids = _.map(items, 'itemid');
let params = {
const grouped_items = _.groupBy(items, 'value_type');
const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid');
const params: any = {
output: 'extend',
trend: value_type,
itemids: itemids,
@@ -308,10 +380,10 @@ export class ZabbixAPIConnector {
}
getTrend_30(items, time_from, time_till, value_type) {
var self = this;
var itemids = _.map(items, 'itemid');
const self = this;
const itemids = _.map(items, 'itemid');
var params = {
const params: any = {
output: ["itemid",
"clock",
value_type
@@ -328,8 +400,8 @@ export class ZabbixAPIConnector {
return self.request('trend.get', params);
}
getITService(serviceids) {
var params = {
getITService(serviceids?) {
const params = {
output: 'extend',
serviceids: serviceids
};
@@ -337,18 +409,88 @@ export class ZabbixAPIConnector {
}
getSLA(serviceids, timeRange, options) {
const intervals = buildSLAIntervals(timeRange, options.intervalMs);
const params = {
const [timeFrom, timeTo] = timeRange;
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,
intervals
};
return this.request('service.getsla', params);
}
getTriggers(groupids, hostids, applicationids, options) {
let {showTriggers, maintenance, timeFrom, timeTo} = options;
getProblems(groupids, hostids, applicationids, options): Promise<ZBXProblem[]> {
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',
groupids: groupids,
hostids: hostids,
@@ -369,8 +511,10 @@ export class ZabbixAPIConnector {
selectTags: 'extend'
};
if (showTriggers) {
params.filter.value = showTriggers;
if (showTriggers === ShowProblemTypes.Problems) {
params.filter.value = 1;
} else if (showTriggers === ShowProblemTypes.Recent || showTriggers === ShowProblemTypes.History) {
params.filter.value = [0, 1];
}
if (maintenance) {
@@ -386,7 +530,7 @@ export class ZabbixAPIConnector {
}
getEvents(objectids, timeFrom, timeTo, showEvents, limit) {
var params = {
const params: any = {
output: 'extend',
time_from: timeFrom,
time_till: timeTo,
@@ -402,27 +546,47 @@ export class ZabbixAPIConnector {
params.sortorder = 'DESC';
}
return this.request('event.get', params);
return this.request('event.get', params).then(utils.mustArray);
}
getAcknowledges(eventids) {
var params = {
getEventsHistory(groupids, hostids, applicationids, options) {
const { timeFrom, timeTo, severities, limit, value } = options;
const params: any = {
output: 'extend',
eventids: eventids,
preservekeys: true,
time_from: timeFrom,
time_till: timeTo,
value: '1',
source: '0',
object: '0',
evaltype: '0',
sortfield: ['eventid'],
sortorder: 'ASC',
select_acknowledges: 'extend',
sortfield: 'clock',
sortorder: 'DESC'
selectTags: 'extend',
selectSuppressionData: ['maintenanceid', 'suppress_until'],
groupids,
hostids,
applicationids,
};
return this.request('event.get', params)
.then(events => {
return _.filter(events, (event) => event.acknowledges.length);
});
if (limit) {
params.limit = limit;
}
if (severities) {
params.severities = severities;
}
if (value) {
params.value = value;
}
return this.request('event.get', params).then(utils.mustArray);
}
getExtendedEventData(eventids) {
var params = {
const params = {
output: 'extend',
eventids: eventids,
preservekeys: true,
@@ -450,8 +614,24 @@ export class ZabbixAPIConnector {
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) {
var params = {
const params: any = {
output: 'extend',
itemids: itemids,
expandDescription: true,
@@ -475,8 +655,8 @@ export class ZabbixAPIConnector {
}
getHostAlerts(hostids, applicationids, options) {
let {minSeverity, acknowledged, count, timeFrom, timeTo} = options;
let params = {
const {minSeverity, acknowledged, count, timeFrom, timeTo} = options;
const params: any = {
output: 'extend',
hostids: hostids,
min_severity: minSeverity,
@@ -517,7 +697,7 @@ export class ZabbixAPIConnector {
}
getProxies() {
var params = {
const params = {
output: ['proxyid', 'host'],
};
@@ -535,13 +715,17 @@ function filterTriggersByAcknowledge(triggers, acknowledged) {
}
}
// function isNotAuthorized(message) {
// return (
// message === "Session terminated, re-login, please." ||
// message === "Not authorised." ||
// message === "Not authorized."
// );
// }
function isNotAuthorized(message) {
return (
message === "Session terminated, re-login, please." ||
message === "Not authorised." ||
message === "Not authorized."
);
}
function isNotInitialized(message) {
return message === "Not initialized";
}
function getSLAInterval(intervalMs) {
// 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);
}
function buildSLAIntervals(timeRange, intervalMs) {
function buildSLAIntervals(timeRange, interval) {
let [timeFrom, timeTo] = timeRange;
const slaInterval = getSLAInterval(intervalMs);
const intervals = [];
// Align time range with calculated interval
timeFrom = Math.floor(timeFrom / slaInterval) * slaInterval;
timeTo = Math.ceil(timeTo / slaInterval) * slaInterval;
timeFrom = Math.floor(timeFrom / interval) * interval;
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({
from : i,
to : (i + slaInterval)
to : (i + interval)
});
}

View File

@@ -1,20 +1,16 @@
/**
* General Zabbix API methods
*/
import { getBackendSrv } from '@grafana/runtime';
import { JSONRPCRequest, ZabbixRequestResponse, JSONRPCError, APILoginResponse, GFHTTPRequest, GFRequestOptions } from './types';
export class ZabbixAPICore {
/** @ngInject */
constructor(backendSrv) {
this.backendSrv = backendSrv;
}
/**
* Request data from Zabbix API
* @return {object} response.result
*/
request(api_url, method, params, options, auth) {
let requestData = {
request(api_url: string, method: string, params: any, options: GFRequestOptions, auth?: string) {
const requestData: JSONRPCRequest = {
jsonrpc: '2.0',
method: method,
params: params,
@@ -23,13 +19,13 @@ export class ZabbixAPICore {
if (auth === "") {
// Reject immediately if not authenticated
return Promise.reject(new ZabbixAPIError({data: "Not authorised."}));
return Promise.reject(new ZabbixAPIError({data: "Not initialized"}));
} else if (auth) {
// Set auth parameter only if it needed
requestData.auth = auth;
}
let requestOptions = {
const requestOptions: GFHTTPRequest = {
method: 'POST',
url: api_url,
data: requestData,
@@ -50,18 +46,18 @@ export class ZabbixAPICore {
}
datasourceRequest(requestOptions) {
return this.backendSrv.datasourceRequest(requestOptions)
.then((response) => {
if (!response.data) {
return getBackendSrv().datasourceRequest(requestOptions)
.then((response: ZabbixRequestResponse) => {
if (!response?.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
return Promise.reject(new ZabbixAPIError(response.data.error));
}
// Success
return response.data.result;
return response?.data.result;
});
}
@@ -69,8 +65,8 @@ export class ZabbixAPICore {
* Get authentication token.
* @return {string} auth token
*/
login(api_url, username, password, options) {
let params = {
login(api_url: string, username: string, password: string, options: GFRequestOptions): Promise<APILoginResponse> {
const params = {
user: username,
password: password
};
@@ -81,14 +77,22 @@ export class ZabbixAPICore {
* Get Zabbix API version
* Matches the version of Zabbix starting from Zabbix 2.0.4
*/
getVersion(api_url, options) {
return this.request(api_url, 'apiinfo.version', [], options);
getVersion(api_url: string, options: GFRequestOptions): Promise<string> {
return this.request(api_url, 'apiinfo.version', [], options).catch(err => {
console.error(err);
return undefined;
});
}
}
// Define zabbix API exception type
export class ZabbixAPIError {
constructor(error) {
code: number;
name: string;
data: string;
message: string;
constructor(error: JSONRPCError) {
this.code = error.code || null;
this.name = error.message || "";
this.data = error.data || "";

View File

@@ -4,6 +4,10 @@
*/
export class CachingProxy {
cacheEnabled: boolean;
ttl: number;
cache: any;
promises: any;
constructor(cacheOptions) {
this.cacheEnabled = cacheOptions.enabled;
@@ -33,13 +37,13 @@ export class CachingProxy {
}
proxyfyWithCache(func, funcName, funcScope) {
let proxyfied = this.proxyfy(func, funcName, funcScope);
const proxyfied = this.proxyfy(func, funcName, funcScope);
return this.cacheRequest(proxyfied, funcName, funcScope);
}
_isExpired(cacheObject) {
if (cacheObject) {
let object_age = Date.now() - cacheObject.timestamp;
const object_age = Date.now() - cacheObject.timestamp;
return !(cacheObject.timestamp && object_age < this.ttl);
} else {
return true;
@@ -52,8 +56,9 @@ export class CachingProxy {
* with same params when waiting for result.
*/
function callOnce(func, promiseKeeper, funcScope) {
// tslint:disable-next-line: only-arrow-functions
return function() {
var hash = getRequestHash(arguments);
const hash = getRequestHash(arguments);
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = Promise.resolve(
func.apply(funcScope, arguments)
@@ -68,22 +73,25 @@ function callOnce(func, promiseKeeper, funcScope) {
}
function cacheRequest(func, funcName, funcScope, self) {
// tslint:disable-next-line: only-arrow-functions
return function() {
if (!self.cache[funcName]) {
self.cache[funcName] = {};
}
let cacheObject = self.cache[funcName];
let hash = getRequestHash(arguments);
const cacheObject = self.cache[funcName];
const hash = getRequestHash(arguments);
if (self.cacheEnabled && !self._isExpired(cacheObject[hash])) {
return Promise.resolve(cacheObject[hash].value);
} else {
return func.apply(funcScope, arguments)
.then(result => {
if (result !== undefined) {
cacheObject[hash] = {
value: result,
timestamp: Date.now()
};
}
return result;
});
}
@@ -92,17 +100,17 @@ function cacheRequest(func, funcName, funcScope, self) {
function getRequestHash(args) {
const argsJson = JSON.stringify(args);
return argsJson.getHash();
return getHash(argsJson);
}
String.prototype.getHash = function() {
var hash = 0, i, chr, len;
if (this.length !== 0) {
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
function getHash(str: string): number {
let hash = 0, i, chr, len;
if (str.length !== 0) {
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
}
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';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue({data: {result: ''}}),
}),
}), {virtual: true});
describe('Zabbix', () => {
let ctx = {};
let zabbix;
@@ -8,14 +13,13 @@ describe('Zabbix', () => {
url: 'http://localhost',
username: 'zabbix',
password: 'zabbix',
zabbixVersion: 4,
};
beforeEach(() => {
ctx.options = options;
ctx.backendSrv = mocks.backendSrvMock;
ctx.datasourceSrv = mocks.datasourceSrvMock;
zabbix = new Zabbix(ctx.options, ctx.backendSrvMock, ctx.datasourceSrvMock);
// ctx.backendSrv = mocks.backendSrvMock;
// ctx.datasourceSrv = mocks.datasourceSrvMock;
zabbix = new Zabbix(ctx.options);
});
describe('When querying proxies', () => {

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import moment from 'moment';
import * as utils from '../utils';
import responseHandler from '../responseHandler';
import { CachingProxy } from './proxy/cachingProxy';
@@ -7,11 +8,19 @@ import { DBConnector } from './connectors/dbConnector';
import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector';
import { SQLConnector } from './connectors/sql/sqlConnector';
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 = [
'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs',
'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies',
'getEventAlerts', 'getExtendedEventData'
'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds'
];
const REQUESTS_TO_CACHE = [
@@ -24,40 +33,63 @@ const REQUESTS_TO_BIND = [
'getExtendedEventData'
];
export class Zabbix {
constructor(options, datasourceSrv, backendSrv, datasourceId) {
let {
export class Zabbix implements ZabbixConnector {
enableDirectDBConnection: boolean;
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,
username,
password,
basicAuth,
withCredentials,
zabbixVersion,
cacheTTL,
enableDirectDBConnection,
dbConnectionDatasourceId,
dbConnectionDatasourceName,
dbConnectionRetentionPolicy,
datasourceId,
} = options;
this.enableDirectDBConnection = enableDirectDBConnection;
// Initialize caching proxy for requests
let cacheOptions = {
const cacheOptions = {
enabled: true,
ttl: cacheTTL
};
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.cacheRequests();
this.bindRequests();
if (enableDirectDBConnection) {
const connectorOptions = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv, connectorOptions)
const connectorOptions: any = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, connectorOptions)
.then(() => {
this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', 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) {
return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv)
initDBConnector(datasourceId, datasourceName, options) {
return DBConnector.loadDatasource(datasourceId, datasourceName)
.then(ds => {
let connectorOptions = { datasourceId, datasourceName };
const connectorOptions: any = { datasourceId, datasourceName };
if (ds.type === 'influxdb') {
connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
this.dbConnector = new InfluxDBConnector(connectorOptions, datasourceSrv);
this.dbConnector = new InfluxDBConnector(connectorOptions);
} else {
this.dbConnector = new SQLConnector(connectorOptions, datasourceSrv);
this.dbConnector = new SQLConnector(connectorOptions);
}
return this.dbConnector;
});
}
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);
}
}
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);
}
}
bindRequests() {
for (let request of REQUESTS_TO_BIND) {
for (const request of REQUESTS_TO_BIND) {
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.
* @return {object} test result object:
* ```
{
zabbixVersion,
dbConnectorStatus: {
dsType,
dsName
}
}
```
* {
* zabbixVersion,
* dbConnectorStatus: {
* dsType,
* dsName
* }
* }
* ```
*/
// testDataSource() {
// let zabbixVersion;
@@ -143,19 +175,20 @@ export class Zabbix {
// }
getItemsFromTarget(target, options) {
let parts = ['group', 'host', 'application', 'item'];
let filters = _.map(parts, p => target[p].filter);
const parts = ['group', 'host', 'application', 'item'];
const filters = _.map(parts, p => target[p].filter);
return this.getItems(...filters, options);
}
getHostsFromTarget(target) {
let parts = ['group', 'host', 'application'];
let filters = _.map(parts, p => target[p].filter);
const parts = ['group', 'host', 'application'];
const filters = _.map(parts, p => target[p].filter);
return Promise.all([
this.getHosts(...filters),
this.getApps(...filters),
]).then((results) => {
let [hosts, apps] = results;
]).then(results => {
const hosts = results[0];
let apps: AppsResponse = results[1];
if (apps.appFilterEmpty) {
apps = [];
}
@@ -178,12 +211,12 @@ export class Zabbix {
getAllHosts(groupFilter) {
return this.getGroups(groupFilter)
.then(groups => {
let groupids = _.map(groups, 'groupid');
const groupids = _.map(groups, 'groupid');
return this.zabbixAPI.getHosts(groupids);
});
}
getHosts(groupFilter, hostFilter) {
getHosts(groupFilter?, hostFilter?) {
return this.getAllHosts(groupFilter)
.then(hosts => findByFilter(hosts, hostFilter));
}
@@ -194,34 +227,34 @@ export class Zabbix {
getAllApps(groupFilter, hostFilter) {
return this.getHosts(groupFilter, hostFilter)
.then(hosts => {
let hostids = _.map(hosts, 'hostid');
const hostids = _.map(hosts, 'hostid');
return this.zabbixAPI.getApps(hostids);
});
}
getApps(groupFilter, hostFilter, appFilter) {
getApps(groupFilter?, hostFilter?, appFilter?): Promise<AppsResponse> {
return this.getHosts(groupFilter, hostFilter)
.then(hosts => {
let hostids = _.map(hosts, 'hostid');
const hostids = _.map(hosts, 'hostid');
if (appFilter) {
return this.zabbixAPI.getApps(hostids)
.then(apps => filterByQuery(apps, appFilter));
} else {
return {
appFilterEmpty: true,
hostids: hostids
};
const appsResponse: AppsResponse = hostids;
appsResponse.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)
.then(apps => {
if (apps.appFilterEmpty) {
return this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
} else {
let appids = _.map(apps, 'applicationid');
const appids = _.map(apps, 'applicationid');
return this.zabbixAPI.getItems(undefined, appids, options.itemtype);
}
})
@@ -235,34 +268,54 @@ export class Zabbix {
.then(this.expandUserMacro.bind(this));
}
expandUserMacro(items) {
let hostids = getHostIds(items);
expandUserMacro(items, isTriggerItem) {
const hostids = getHostIds(items);
return this.getMacros(hostids)
.then(macros => {
_.forEach(items, item => {
if (utils.containsMacro(item.name)) {
if (utils.containsMacro(isTriggerItem ? item.url : item.name)) {
if (isTriggerItem) {
item.url = utils.replaceMacro(item, macros, isTriggerItem);
} else {
item.name = utils.replaceMacro(item, macros);
}
}
});
return items;
});
}
getItems(groupFilter, hostFilter, appFilter, itemFilter, options = {}) {
getItems(groupFilter?, hostFilter?, appFilter?, itemFilter?, options = {}) {
return this.getAllItems(groupFilter, hostFilter, appFilter, options)
.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) {
return this.zabbixAPI.getITService()
.then(itServices => findByFilter(itServices, itServiceFilter));
}
/**
* Build query - convert target filters to array of Zabbix items
*/
getTriggers(groupFilter, hostFilter, appFilter, options, proxyFilter) {
let promises = [
getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?) {
const promises = [
this.getGroups(groupFilter),
this.getHosts(groupFilter, hostFilter),
this.getApps(groupFilter, hostFilter, appFilter)
@@ -270,8 +323,8 @@ export class Zabbix {
return Promise.all(promises)
.then(results => {
let [filteredGroups, filteredHosts, filteredApps] = results;
let query = {};
const [filteredGroups, filteredHosts, filteredApps] = results;
const query: any = {};
if (appFilter) {
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
@@ -285,8 +338,53 @@ export class Zabbix {
return query;
})
.then(query => this.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options))
.then(triggers => this.filterTriggersByProxy(triggers, proxyFilter));
.then(query => this.zabbixAPI.getProblems(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]) => 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) {
@@ -295,14 +393,13 @@ export class Zabbix {
if (proxyFilter && proxyFilter !== '/.*/' && triggers) {
const proxy_ids = proxies.map(proxy => proxy.proxyid);
triggers = triggers.filter(trigger => {
let filtered = false;
for (let i = 0; i < trigger.hosts.length; i++) {
const host = trigger.hosts[i];
if (proxy_ids.includes(host.proxy_hostid)) {
filtered = true;
return true;
}
}
return filtered;
return false;
});
}
return triggers;
@@ -318,7 +415,7 @@ export class Zabbix {
}
getHistoryTS(items, timeRange, options) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
if (this.enableDirectDBConnection) {
return this.getHistoryDB(items, timeFrom, timeTo, options)
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
@@ -329,12 +426,12 @@ export class Zabbix {
}
getTrends(items, timeRange, options) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
if (this.enableDirectDBConnection) {
return this.getTrendsDB(items, timeFrom, timeTo, options)
.then(history => this.dbConnector.handleGrafanaTSResponse(history, items));
} else {
let valueType = options.consolidateBy || options.valueType;
const valueType = options.consolidateBy || options.valueType;
return this.zabbixAPI.getTrend(items, timeFrom, timeTo)
.then(history => responseHandler.handleTrends(history, items, valueType))
.then(responseHandler.sortTimeseries); // Sort trend data, issue #202
@@ -342,7 +439,7 @@ export class Zabbix {
}
getHistoryText(items, timeRange, target) {
let [timeFrom, timeTo] = timeRange;
const [timeFrom, timeTo] = timeRange;
if (items.length) {
return this.zabbixAPI.getHistory(items, timeFrom, timeTo)
.then(history => {
@@ -358,15 +455,11 @@ export class Zabbix {
}
getSLA(itservices, timeRange, target, options) {
let itServices = itservices;
if (options.isOldVersion) {
itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
}
let itServiceIds = _.map(itServices, 'serviceid');
const itServiceIds = _.map(itservices, 'serviceid');
return this.zabbixAPI.getSLA(itServiceIds, timeRange, options)
.then(slaResponse => {
return _.map(itServiceIds, serviceid => {
let itservice = _.find(itServices, {'serviceid': serviceid});
const itservice = _.find(itservices, {'serviceid': serviceid});
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
});
});
@@ -382,7 +475,7 @@ export class Zabbix {
* @return array with finded element or empty array
*/
function findByName(list, name) {
var finded = _.find(list, {'name': name});
const finded = _.find(list, {'name': name});
if (finded) {
return [finded];
} else {
@@ -399,7 +492,7 @@ function findByName(list, name) {
* @return {[type]} array with finded element or empty array
*/
function filterByName(list, name) {
var finded = _.filter(list, {'name': name});
const finded = _.filter(list, {'name': name});
if (finded) {
return finded;
} else {
@@ -408,8 +501,8 @@ function filterByName(list, name) {
}
function filterByRegex(list, regex) {
var filterPattern = utils.buildRegex(regex);
return _.filter(list, function (zbx_obj) {
const filterPattern = utils.buildRegex(regex);
return _.filter(list, (zbx_obj) => {
return filterPattern.test(zbx_obj.name);
});
}
@@ -431,7 +524,7 @@ function filterByQuery(list, filter) {
}
function getHostIds(items) {
let hostIds = _.map(items, item => {
const hostIds = _.map(items, item => {
return _.map(item.hosts, 'hostid');
});
return _.uniq(_.flatten(hostIds));

View File

@@ -16,8 +16,11 @@ export class ZabbixAlertingService {
}
setPanelAlertState(panelId, alertState) {
let panelIndex;
if (!alertState) {
return;
}
let panelIndex;
let panelContainers = _.filter($('.panel-container'), elem => {
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 && !panelContainers[panelIndex].className.includes('panel-container--absolute')) {
if (panelIndex >= 0) {
let alertClass = "panel-has-alert panel-alert-state--ok panel-alert-state--alerting";
$(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.light.scss';
import {ZabbixAppConfigCtrl} from './components/config';
import {ZabbixAppConfigCtrl} from './app_config_ctrl/config';
import {loadPluginCss} from 'grafana/app/plugins/sdk';
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 moment from 'moment';
import { isNewProblem, formatLastChange } from '../../utils';
import { ProblemsPanelOptions, ZBXTrigger, ZBXTag } from '../../types';
import { AckProblemData, Modal } from '.././Modal';
import { ProblemsPanelOptions, TriggerSeverity } from '../../types';
import { AckProblemData, AckModal } from '../AckModal';
import EventTag from '../EventTag';
import Tooltip from '.././Tooltip/Tooltip';
import AlertAcknowledges from './AlertAcknowledges';
import AlertIcon from './AlertIcon';
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
import { ModalController, Tooltip } from '../../../components';
interface AlertCardProps {
problem: ZBXTrigger;
problem: ProblemDTO;
panelOptions: ProblemsPanelOptions;
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 {
showAckDialog: boolean;
}
export default class AlertCard extends PureComponent<AlertCardProps, AlertCardState> {
constructor(props) {
super(props);
this.state = { showAckDialog: false };
}
export default class AlertCard extends PureComponent<AlertCardProps> {
handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) {
@@ -35,23 +28,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
ackProblem = (data: AckProblemData) => {
const problem = this.props.problem;
return this.props.onProblemAck(problem, data).then(result => {
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 });
return this.props.onProblemAck(problem, data);
}
render() {
@@ -59,9 +36,16 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground });
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine });
const severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.priority));
const lastchange = formatLastChange(problem.lastchangeUnix, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat);
const age = moment.unix(problem.lastchangeUnix).fromNow(true);
const problemSeverity = Number(problem.severity);
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;
if (panelOptions.highlightNewerThan) {
@@ -72,7 +56,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
let problemColor: string;
if (problem.value === '0') {
problemColor = panelOptions.okEventColor;
} else if (panelOptions.markAckEvents && problem.lastEvent.acknowledged === "1") {
} else if (panelOptions.markAckEvents && problem.acknowledged === "1") {
problemColor = panelOptions.ackEventColor;
} else {
problemColor = severityDesc.color;
@@ -153,22 +137,32 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
<span><i className="fa fa-question-circle"></i></span>
</Tooltip>
)}
{problem.lastEvent && (
<AlertAcknowledgesButton problem={problem} onClick={this.showAckDialog} />
{problem.eventid && (
<ModalController>
{({ showModal, hideModal }) => (
<AlertAcknowledgesButton
problem={problem}
onClick={() => {
showModal(AckModal, {
canClose: problem.manual_close === '1',
severity: problemSeverity,
onSubmit: this.ackProblem,
onDismiss: hideModal,
});
}}
/>
)}
</ModalController>
)}
</div>
</div>
<Modal withBackdrop={true}
isOpen={this.state.showAckDialog}
onSubmit={this.ackProblem}
onClose={this.closeAckDialog} />
</li>
);
}
}
interface AlertHostProps {
problem: ZBXTrigger;
problem: ProblemDTO;
panelOptions: ProblemsPanelOptions;
}
@@ -194,7 +188,7 @@ function AlertHost(props: AlertHostProps) {
}
interface AlertGroupProps {
problem: ZBXTrigger;
problem: ProblemDTO;
panelOptions: ProblemsPanelOptions;
}
@@ -247,7 +241,7 @@ function AlertSeverity(props) {
}
interface AlertAcknowledgesButtonProps {
problem: ZBXTrigger;
problem: ProblemDTO;
onClick: (event?) => void;
}

View File

@@ -1,33 +1,34 @@
import React, { CSSProperties } from 'react';
import classNames from 'classnames';
import { ZBXTrigger } from '../../types';
import React, { FC } from 'react';
import { cx, css } from 'emotion';
import { GFHeartIcon } from '../../../components';
import { ProblemDTO } from '../../../datasource-zabbix/types';
interface AlertIconProps {
problem: ZBXTrigger;
interface Props {
problem: ProblemDTO;
color: string;
blink?: boolean;
highlightBackground?: boolean;
}
export default function AlertIcon(props: AlertIconProps) {
const { problem, color, blink, highlightBackground } = props;
const priority = Number(problem.priority);
let iconClass = '';
if (problem.value === '1' && priority >= 2) {
iconClass = 'icon-gf-critical';
} else {
iconClass = 'icon-gf-online';
}
export const AlertIcon: FC<Props> = ({ problem, color, blink, highlightBackground }) => {
const severity = Number(problem.severity);
const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online';
const className = classNames('icon-gf', iconClass, { 'zabbix-trigger--blinked': blink });
const style: CSSProperties = {};
if (!highlightBackground) {
style.color = color;
}
const iconClass = cx(
'icon-gf',
blink && 'zabbix-trigger--blinked',
);
const wrapperClass = cx(
'alert-rule-item__icon',
!highlightBackground && css`color: ${color}`
);
return (
<div className="alert-rule-item__icon" style={style}>
<i className={className}></i>
<div className={wrapperClass}>
<GFHeartIcon status={status} className={iconClass} />
</div>
);
}
};
export default AlertIcon;

View File

@@ -1,23 +1,24 @@
import React, { PureComponent, CSSProperties } from 'react';
import classNames from 'classnames';
import { ProblemsPanelOptions, ZBXTrigger, GFTimeRange, ZBXTag } from '../../types';
import { AckProblemData } from '.././Modal';
import { ProblemsPanelOptions, GFTimeRange } from '../../types';
import { AckProblemData } from '../AckModal';
import AlertCard from './AlertCard';
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
export interface AlertListProps {
problems: ZBXTrigger[];
problems: ProblemDTO[];
panelOptions: ProblemsPanelOptions;
loading?: boolean;
timeRange?: GFTimeRange;
pageSize?: 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;
}
interface AlertListState {
page: number;
currentProblems: ZBXTrigger[];
currentProblems: ProblemDTO[];
}
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);
}
@@ -68,7 +69,7 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
<ol className={alertListClass}>
{currentProblems.map(problem =>
<AlertCard
key={`${problem.triggerid}-${problem.datasource}`}
key={`${problem.triggerid}-${problem.eventid}-${problem.datasource}`}
problem={problem}
panelOptions={panelOptions}
onTagClick={this.handleTagClick}

View File

@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { ZBXTag } from '../types';
import Tooltip from './Tooltip/Tooltip';
import Tooltip from '../../components/Tooltip/Tooltip';
const TAG_COLORS = [
'#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 moment from 'moment';
import * as utils from '../../../datasource-zabbix/utils';
import { ZBXTrigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag, ZBXAlert } from '../../types';
import { Modal, AckProblemData } from '../Modal';
import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
import { ZBXItem, GFTimeRange, RTRow } from '../../types';
import { AckModal, AckProblemData } from '../AckModal';
import EventTag from '../EventTag';
import Tooltip from '../Tooltip/Tooltip';
import ProblemStatusBar from './ProblemStatusBar';
import AcknowledgesList from './AcknowledgesList';
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;
timeRange: GFTimeRange;
showTimeline?: boolean;
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
panelId?: number;
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
}
@@ -24,17 +25,15 @@ interface ProblemDetailsState {
events: ZBXEvent[];
alerts: ZBXAlert[];
show: boolean;
showAckDialog: boolean;
}
export default class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> {
export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> {
constructor(props) {
super(props);
this.state = {
events: [],
alerts: [],
show: false,
showAckDialog: false,
};
}
@@ -71,32 +70,20 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
}
ackProblem = (data: AckProblemData) => {
const problem = this.props.original as ZBXTrigger;
return this.props.onProblemAck(problem, data).then(result => {
this.closeAckDialog();
}).catch(err => {
console.log(err);
this.closeAckDialog();
});
}
showAckDialog = () => {
this.setState({ showAckDialog: true });
}
closeAckDialog = () => {
this.setState({ showAckDialog: false });
const problem = this.props.original as ProblemDTO;
return this.props.onProblemAck(problem, data);
}
render() {
const problem = this.props.original as ZBXTrigger;
const problem = this.props.original as ProblemDTO;
const alerts = this.state.alerts;
const rootWidth = this.props.rootWidth;
const displayClass = this.state.show ? 'show' : '';
const wideLayout = rootWidth > 1200;
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 problemSeverity = Number(problem.severity);
return (
<div className={`problem-details-container ${displayClass}`}>
@@ -109,21 +96,37 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
</div>
{problem.items && <ProblemItems items={problem.items} />}
</div>
<div className="problem-actions-left">
<ExploreButton problem={problem} panelId={this.props.panelId} />
</div>
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
{problem.showAckButton &&
<div className="problem-actions">
<ProblemActionButton className="navbar-button navbar-button--settings"
icon="reply-all"
tooltip="Acknowledge problem"
onClick={this.showAckDialog} />
<ModalController>
{({ showModal, hideModal }) => (
<AckButton
className="navbar-button navbar-button--settings"
onClick={() => {
showModal(AckModal, {
canClose: problem.manual_close === '1',
severity: problemSeverity,
onSubmit: this.ackProblem,
onDismiss: hideModal,
});
}}
/>
)}
</ModalController>
</div>
}
</div>
{problem.comments &&
<div className="problem-description-row">
<div className="problem-description">
<span className="description-label">Description:&nbsp;</span>
<span>{problem.comments}</span>
</div>
</div>
}
{problem.tags && problem.tags.length > 0 &&
<div className="problem-tags">
@@ -169,10 +172,6 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
</div>
<Modal withBackdrop={true}
isOpen={this.state.showAckDialog}
onSubmit={this.ackProblem}
onClose={this.closeAckDialog} />
</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 FAIcon from '../FAIcon';
import Tooltip from '../Tooltip/Tooltip';
import FAIcon from '../../../components/FAIcon/FAIcon';
import Tooltip from '../../../components/Tooltip/Tooltip';
import { ZBXTrigger, ZBXAlert } from '../../types';
export interface ProblemStatusBarProps {

View File

@@ -371,7 +371,7 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
};
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 (
<TimelinePoint
key={event.eventid}
key={`${event.eventid}-${i}`}
className={className}
x={posLeft}
r={pointR}
@@ -611,7 +611,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
return (
<TimelineAck
key={ack.eventid}
key={`${ack.eventid}-${i}`}
x={posLeft}
r={pointR}
highlighted={highlighted}

View File

@@ -1,26 +1,28 @@
import React, { PureComponent } from 'react';
import ReactTable from 'react-table';
import ReactTable from 'react-table-6';
import classNames from 'classnames';
import _ from 'lodash';
import moment from 'moment';
import * as utils from '../../../datasource-zabbix/utils';
import { isNewProblem } from '../../utils';
import { ProblemsPanelOptions, ZBXTrigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized, ZBXAlert } from '../../types';
import EventTag from '../EventTag';
import ProblemDetails from './ProblemDetails';
import { AckProblemData } from '../Modal';
import GFHeartIcon from '../GFHeartIcon';
import { ProblemDetails } from './ProblemDetails';
import { AckProblemData } from '../AckModal';
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 {
problems: ZBXTrigger[];
problems: ProblemDTO[];
panelOptions: ProblemsPanelOptions;
loading?: boolean;
timeRange?: GFTimeRange;
pageSize?: number;
fontSize?: number;
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ZBXTrigger) => Promise<ZBXAlert[]>;
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
panelId?: number;
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
onColumnResize?: (newResized: RTResized) => void;
@@ -47,7 +49,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
this.rootRef = ref;
}
handleProblemAck = (problem: ZBXTrigger, data: AckProblemData) => {
handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => {
return this.props.onProblemAck(problem, data);
}
@@ -85,19 +87,21 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
const result = [];
const options = this.props.panelOptions;
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 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 = [
{ Header: 'Host', accessor: 'host', show: options.hostField },
{ Header: 'Host (Technical Name)', accessor: 'hostTechName', show: options.hostTechNameField },
{ Header: 'Host', id: 'host', show: options.hostField, Cell: hostNameCell },
{ Header: 'Host (Technical Name)', id: 'hostTechName', show: options.hostTechNameField, Cell: hostTechNameCell },
{ Header: 'Host Groups', accessor: 'groups', show: options.hostGroups, Cell: GroupCell },
{ Header: 'Proxy', accessor: 'proxy', show: options.hostProxy },
{
Header: 'Severity', show: options.severityField, className: 'problem-severity', width: 120,
accessor: problem => problem.priority,
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,
@@ -106,17 +110,21 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
},
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
{ 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',
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',
Cell: AgeCell,
},
{
Header: 'Time', className: 'last-change', width: 150, accessor: 'lastchangeUnix',
Header: 'Time', className: 'last-change', width: 150, accessor: 'timestamp',
id: 'lastchange',
Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat),
},
@@ -159,10 +167,12 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
rootWidth={this.rootWidth}
timeRange={this.props.timeRange}
showTimeline={panelOptions.problemTimeline}
panelId={this.props.panelId}
getProblemEvents={this.props.getProblemEvents}
getProblemAlerts={this.props.getProblemAlerts}
onProblemAck={this.handleProblemAck}
onTagClick={this.handleTagClick}
subRows={false}
/>
}
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;
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
if (markAckEvents && problem.lastEvent.acknowledged === "1") {
if (markAckEvents && problem.acknowledged === "1") {
color = ackEventColor;
}
@@ -197,9 +234,9 @@ function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSev
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
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 color = props.value === '0' ? okColor : problemColor;
const color = props.value === '0' ? DEFAULT_OK_COLOR : DEFAULT_PROBLEM_COLOR;
let newProblem = false;
if (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';
let newProblem = false;
if (highlightNewerThan) {
@@ -223,7 +260,7 @@ function StatusIconCell(props: RTCell<ZBXTrigger>, highlightNewerThan?: string)
return <GFHeartIcon status={status} className={className} />;
}
function GroupCell(props: RTCell<ZBXTrigger>) {
function GroupCell(props: RTCell<ProblemDTO>) {
let groups = "";
if (props.value && props.value.length) {
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;
return (
<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 timestamp = moment.unix(problem.lastchangeUnix);
const timestamp = moment.unix(problem.timestamp);
const age = timestamp.fromNow(true);
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 problem = props.original;
const timestamp = moment.unix(problem.lastchangeUnix);
const timestamp = moment.unix(problem.timestamp);
const format = customFormat || DEFAULT_TIME_FORMAT;
const lastchange = timestamp.format(format);
return <span>{lastchange}</span>;
}
interface TagCellProps extends RTCell<ZBXTrigger> {
interface TagCellProps extends RTCell<ProblemDTO> {
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 { getNextRefIdChar } from './utils';
import { getDefaultTarget } from './triggers_panel_ctrl';
import { ShowProblemTypes } from '../datasource-zabbix/types';
// 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) {
if (isEmptyPanel(panel)) {
@@ -12,7 +30,7 @@ export function migratePanelSchema(panel) {
}
const schemaVersion = getSchemaVersion(panel);
panel.schemaVersion = CURRENT_SCHEMA_VERSION;
// panel.schemaVersion = CURRENT_SCHEMA_VERSION;
if (schemaVersion < 2) {
panel.datasources = [panel.datasource];
@@ -66,9 +84,70 @@ export function migratePanelSchema(panel) {
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;
}
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) {
return panel.schemaVersion || 1;
}

View File

@@ -13,7 +13,6 @@
import { TriggerPanelCtrl } from './triggers_panel_ctrl';
import { loadPluginCss } from 'grafana/app/plugins/sdk';
import './datasource-selector.directive';
loadPluginCss({
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',

View File

@@ -29,10 +29,13 @@ class TriggerPanelOptionsCtrl {
'unacknowledged',
'acknowledged'
];
this.sortByFields = [
{ text: 'last change', value: 'lastchange' },
{ text: 'severity', value: 'priority' }
this.sortingOptions = [
{ text: 'Default', value: 'default' },
{ text: 'Last change', value: 'lastchange' },
{ text: 'Severity', value: 'priority' },
];
this.showEventsFields = [
{ text: 'All', value: [0,1] },
{ text: 'OK', value: [0] },

View File

@@ -1,6 +1,6 @@
<div class="editor-row">
<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"
label-class="width-9"
label="Host name"
@@ -23,7 +23,7 @@
label-class="width-9"
label="Host proxy"
checked="ctrl.panel.hostProxy"
on-change="ctrl.refresh()">
on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label-class="width-9"
@@ -50,6 +50,12 @@
checked="ctrl.panel.severityField"
on-change="ctrl.render()">
</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"
label-class="width-9"
label="Age"
@@ -72,53 +78,6 @@
</gf-form-switch>
</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">
<h5 class="section-heading">View options</h5>
<div class="gf-form">
@@ -130,6 +89,16 @@
ng-change="ctrl.render()"></select>
</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">
<label class="gf-form-label width-10">Font size</label>
<div class="gf-form-select-wrapper max-width-8">
@@ -202,7 +171,7 @@
</div>
<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">
<label class="gf-form-label width-3">{{ trigger.priority }}</label>
@@ -224,7 +193,7 @@
label-class="width-0"
label="Show"
checked="trigger.show"
on-change="ctrl.refresh()">
on-change="ctrl.reRenderProblems()">
</gf-form-switch>
</div>
@@ -246,7 +215,7 @@
label-class="width-0"
label="Show"
checked="ctrl.panel.markAckEvents"
on-change="ctrl.refresh()">
on-change="ctrl.reRenderProblems()">
</gf-form-switch>
</div>
<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",
"id": "alexanderzobnin-zabbix-triggers-panel",
"dataFormats": [],
"skipDataQuery": true,
"info": {
"author": {
"name": "Alexander Zobnin",
"url": "https://github.com/alexanderzobnin/grafana-zabbix"
},
"logos": {
"small": "img/icn-zabbix-problems-panel.svg",
"large": "img/icn-zabbix-problems-panel.svg"
}
}
}

View File

@@ -4,15 +4,20 @@ import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
import { DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS } from '../triggers_panel_ctrl';
import { CURRENT_SCHEMA_VERSION } from '../migrations';
describe('Triggers Panel schema migration', () => {
let ctx: any = {};
let updatePanelCtrl;
const datasourceSrvMock = {
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', () => {
let ctx: any = {};
let updatePanelCtrl;
const timeoutMock = () => {};
@@ -29,8 +34,9 @@ describe('Triggers Panel schema migration', () => {
ageField: true,
infoField: true,
limit: 10,
showTriggers: 'all triggers',
showTriggers: 'unacknowledged',
hideHostsInMaintenance: false,
hostsInMaintenance: false,
sortTriggersBy: { text: 'last change', value: 'lastchange' },
showEvents: { text: 'Problems', value: '1' },
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', () => {
@@ -51,12 +57,22 @@ describe('Triggers Panel schema migration', () => {
const expected = _.defaultsDeep({
schemaVersion: CURRENT_SCHEMA_VERSION,
datasource: 'zabbix',
targets: [
{
...DEFAULT_TARGET,
datasource: 'zabbix',
queryType: 5,
showProblems: 'problems',
options: {
hostsInMaintenance: false,
acknowledged: 0,
sortProblems: 'default',
minSeverity: 0,
limit: 10,
},
}
],
sortProblems: 'lastchange',
ageField: true,
statusField: false,
severityField: false,
@@ -74,27 +90,7 @@ describe('Triggers Panel schema migration', () => {
const expected = _.defaultsDeep({
schemaVersion: CURRENT_SCHEMA_VERSION,
targets: [{
...DEFAULT_TARGET,
datasource: 'zabbix_default'
}]
}, PANEL_DEFAULTS);
expect(updatedPanelCtrl.panel).toEqual(expected);
});
it('should set default targets for new panel with empty targets', () => {
ctx.scope.panel = {
targets: []
};
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
const expected = _.defaultsDeep({
targets: [{
...DEFAULT_TARGET,
datasource: 'zabbix_default'
}]
}, PANEL_DEFAULTS);
expect(updatedPanelCtrl.panel).toEqual(expected);
});
});

View File

@@ -1,92 +1,55 @@
import _ from 'lodash';
import mocks from '../../test-setup/mocks';
import { TriggerPanelCtrl } 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', () => {
let ctx: any = {};
let datasourceSrvMock, zabbixDSMock;
const timeoutMock = () => {};
let createPanelCtrl;
let createPanelCtrl: () => any;
beforeEach(() => {
ctx = {scope: {panel: PANEL_DEFAULTS}};
ctx = { scope: {
panel: {
...PANEL_DEFAULTS,
sortProblems: 'lastchange',
}
}};
ctx.scope.panel.targets = [{
...DEFAULT_TARGET,
datasource: 'zabbix_default',
}];
zabbixDSMock = {
replaceTemplateVars: () => {},
zabbix: {
getTriggers: jest.fn().mockReturnValue([generateTrigger("1"), generateTrigger("1")]),
getExtendedEventData: jest.fn().mockResolvedValue([]),
getEventAlerts: jest.fn().mockResolvedValue([]),
}
};
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)
};
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock, datasourceSrvMock, {}, {}, {}, mocks.timeSrvMock);
const getTriggersResp = [
[
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]));
const timeoutMock = (fn: () => any) => Promise.resolve(fn());
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock);
ctx.panelCtrl = createPanelCtrl();
});
describe('When adding new panel', () => {
it('should suggest all zabbix data sources', () => {
ctx.scope.panel = {};
const panelCtrl = createPanelCtrl();
expect(panelCtrl.available_datasources).toEqual([
'zabbix_default', 'zabbix'
ctx.dataFramesReceived = generateDataFramesResponse([
{id: "1", lastchange: "1510000010", priority: 5},
{id: "2", lastchange: "1510000040", priority: 3},
{id: "3", lastchange: "1510000020", priority: 4},
{id: "4", lastchange: "1510000030", priority: 2},
]);
});
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', () => {
beforeEach(() => {
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
@@ -104,8 +67,8 @@ describe('TriggerPanelCtrl', () => {
});
it('should format triggers', (done) => {
ctx.panelCtrl.onRefresh().then(() => {
const formattedTrigger: any = _.find(ctx.panelCtrl.triggerList, {triggerid: "1"});
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, {triggerid: "1"});
expect(formattedTrigger.host).toBe('backend01');
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
expect(formattedTrigger.datasource).toBe('zabbix_default');
@@ -116,8 +79,8 @@ describe('TriggerPanelCtrl', () => {
});
it('should sort triggers by time by default', (done) => {
ctx.panelCtrl.onRefresh().then(() => {
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
expect(trigger_ids).toEqual([
'2', '4', '3', '1'
]);
@@ -126,175 +89,119 @@ describe('TriggerPanelCtrl', () => {
});
it('should sort triggers by severity', (done) => {
ctx.panelCtrl.panel.sortTriggersBy = { text: 'severity', value: 'priority' };
ctx.panelCtrl.onRefresh().then(() => {
const trigger_ids = _.map(ctx.panelCtrl.triggerList, 'triggerid');
ctx.panelCtrl.panel.sortProblems = 'priority';
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
expect(trigger_ids).toEqual([
'1', '3', '2', '4'
]);
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 = {
"triggerid": "13565",
"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",
const defaultProblem: any = {
"acknowledges": [],
"comments": "It probably means that the systems requires\nmore physical memory.",
"url": "https://host.local/path",
"templateid": "0", "expression": "{13174}<50", "manual_close": "0", "correlation_mode": "0",
"correlation_tag": "", "recovery_mode": "0", "recovery_expression": "", "state": "0", "status": "0",
"flags": "0", "type": "0", "items": [] , "error": ""
};
const defaultEvent: any = {
"eventid": "11",
"acknowledges": [
"correlation_mode": "0",
"correlation_tag": "",
"datasource": "zabbix_default",
"description": "Lack of free swap space on server",
"error": "",
"expression": "{13297}>20",
"flags": "0",
"groups": [
{
"acknowledgeid": "185",
"action": "0",
"alias": "api",
"clock": "1512382246",
"eventid": "11",
"message": "event ack",
"name": "api",
"surname": "user",
"userid": "3"
"groupid": "2",
"name": "Linux servers"
},
{
"groupid": "9",
"name": "Backend"
}
],
"clock": "1507229064",
"ns": "556202037",
"acknowledged": "1",
"value": "1",
"hosts": [
{
"host": "backend01_tech",
"hostid": "10111",
"maintenance_status": "1",
"name": "backend01",
"proxy_hostid": "0"
}
],
"items": [
{
"itemid": "23979",
"key_": "system.cpu.util[,iowait]",
"lastvalue": "25.2091",
"name": "CPU $2 time"
}
],
"lastEvent": {
"acknowledged": "0",
"clock": "1589297010",
"eventid": "4399289",
"name": "Disk I/O is overloaded on backend01",
"ns": "224779201",
"object": "0",
"objectid": "13682",
"severity": "2",
"source": "0",
"objectid": "1",
"value": "1"
},
"lastchange": "1440259530",
"maintenance": true,
"manual_close": "0",
"priority": "2",
"recovery_expression": "",
"recovery_mode": "0",
"showAckButton": true,
"state": "0",
"status": "0",
"tags": [],
"templateid": "13671",
"triggerid": "13682",
"type": "0",
"url": "",
"value": "1"
};
function generateTrigger(id, timestamp?, severity?): any {
const trigger = _.cloneDeep(defaultTrigger);
trigger.triggerid = id.toString();
function generateDataFramesResponse(problemDescs: any[] = [{id: 1}]): any {
const problems = problemDescs.map(problem => generateProblem(problem.id, problem.lastchange, problem.priority));
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) {
trigger.priority = severity.toString();
problem.priority = severity.toString();
}
if (timestamp) {
trigger.lastchange = timestamp;
problem.lastchange = timestamp;
}
return trigger;
return problem;
}
function createTrigger(props): any {
let trigger = _.cloneDeep(defaultTrigger);
trigger = _.merge(trigger, props);
trigger.lastEvent.objectid = trigger.triggerid;
return trigger;
}
function getTriggerById(id, ctx): any {
return _.find(ctx.panelCtrl.triggerList, {triggerid: id.toString()});
function getProblemById(id, ctx): any {
return _.find(ctx.panelCtrl.renderData, {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;
statusIcon?: boolean;
severityField?: boolean;
ackField?: boolean;
ageField?: boolean;
descriptionField?: boolean;
descriptionAtNewLine?: boolean;
@@ -140,6 +141,7 @@ export interface ZBXEvent {
object?: string;
objectid?: string;
acknowledged?: string;
severity?: string;
hosts?: ZBXHost[];
acknowledges?: ZBXAcknowledge[];
}

View File

@@ -2,12 +2,12 @@ import _ from 'lodash';
import moment from 'moment';
import { DataQuery } from '@grafana/data';
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 {
const highlightIntervalMs = utils.parseInterval(highlightNewerThan);
const durationSec = (Date.now() - problem.lastchangeUnix * 1000);
const durationSec = (Date.now() - problem.timestamp * 1000);
return durationSec < highlightIntervalMs;
} catch (e) {
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"],
"logos": {
"small": "img/zabbix_app_logo.svg",
"large": "img/zabbix_app_logo.svg"
"small": "img/icn-zabbix-app.svg",
"large": "img/icn-zabbix-app.svg"
},
"links": [
{"name": "GitHub", "url": "https://github.com/alexanderzobnin/grafana-zabbix"},
@@ -27,17 +27,17 @@
{"name": "Triggers", "path": "img/screenshot-triggers.png"}
],
"version": "4.0.0-alpha",
"updated": "2020-01-14"
"updated": "2020-05-28"
},
"includes": [
{
"type": "datasource",
"name": "Zabbix Datasource"
"name": "Zabbix data source"
},
{
"type": "panel",
"name": "Triggers Panel"
"name": "Problems panel"
}
],

View File

@@ -7,6 +7,11 @@
i {
width: 1rem;
}
&.fired {
color: $problem-statusbar-fired;
text-shadow: 0px 0px 10px rgba($problem-statusbar-fired, 0.5);
}
}
// <ReactTable /> styles
@@ -246,11 +251,31 @@
flex-direction: column;
}
.problem-description-row {
.problem-description {
position: relative;
height: 4.5rem;
overflow: hidden;
&:after {
content: "";
text-align: right;
position: absolute;
bottom: 0;
right: 0;
width: 70%;
height: 1.5rem;
background: linear-gradient(to right, rgba($problem-details-background, 0), rgba($problem-details-background, 1) 50%);
}
}
.description-label {
font-weight: 500;
font-style: italic;
color: $text-muted;
}
}
.problem-age {
font-weight: 500;
@@ -314,24 +339,8 @@
margin-left: 1.6rem;
}
.problem-action-button {
&.btn {
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-actions-left {
margin-right: 1.6rem;
}
.problem-details-middle {
@@ -546,9 +555,19 @@
}
.zbx-ack-modal {
.gf-form-input.zbx-ack-error {
.zbx-ack-error {
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 {
@@ -558,7 +577,6 @@
&.ack-error-message {
float: left;
color: $error-text-color;
}
}
}

View File

@@ -1,5 +1,5 @@
// DEPENDENCIES
@import '../../node_modules/react-table/react-table.css';
@import '../../node_modules/react-table-6/react-table.css';
@import 'variables';
@import 'panel-triggers';

View File

@@ -2,7 +2,9 @@
/* globals global: false */
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
// Required for loading module.js
@@ -26,17 +28,34 @@ jest.mock('grafana/app/features/dashboard/dashboard_srv', () => {
return {};
}, {virtual: true});
jest.mock('@grafana/runtime', () => {
return {
getBackendSrv: () => ({
datasourceRequest: jest.fn().mockResolvedValue(),
}),
};
}, {virtual: true});
jest.mock('grafana/app/core/core_module', () => {
return {
directive: function() {},
};
}, {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', () => {
return {
QueryCtrl: null,
PanelCtrl: mockPanelCtrl
PanelCtrl: mockPanelCtrl,
loadPluginCss: () => {},
PanelCtrl: mockPanelCtrl,
MetricsPanelCtrl: mockMetricsPanelCtrl,
};
}, {virtual: true});
@@ -92,3 +111,7 @@ let dom = new JSDOM('<html><head><script></script></head><body></body></html>');
global.window = dom.window;
global.document = global.window.document;
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": {
"moduleResolution": "node",
"target": "es5",
"lib": [
"es6",
"dom"
],
"lib": [ "es6", "dom", "es2017" ],
"rootDir": "./src",
"jsx": "react",
"module": "esnext",
@@ -21,6 +18,7 @@
"noImplicitUseStrict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"baseUrl": "./src"
"baseUrl": "./src",
"strictFunctionTypes": false
}
}

View File

@@ -15,8 +15,8 @@ module.exports = {
target: 'node',
context: resolve('src'),
entry: {
'./module': './module.js',
'components/config': './components/config.js',
'module': './module.js',
'app_config_ctrl/config': './app_config_ctrl/config.js',
'datasource-zabbix/module': './datasource-zabbix/module.ts',
'panel-triggers/module': './panel-triggers/module.js',
},
@@ -42,6 +42,7 @@ module.exports = {
new CopyWebpackPlugin([
{ from: '**/plugin.json' },
{ from: '**/*.html' },
{ from: '**/*.md' },
{ from: 'dashboards/*' },
{ from: '../README.md' },
{ from: '**/img/*' },

1408
yarn.lock

File diff suppressed because it is too large Load Diff