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

@@ -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,71 +623,63 @@ 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);
if (annotation.minseverity) {
const severities = [0, 1, 2, 3, 4, 5].filter(v => v >= Number(annotation.minseverity));
problemsOptions.severities = severities;
}
return getTriggers.then(triggers => {
const groupFilter = this.replaceTemplateVars(annotation.group, {});
const hostFilter = this.replaceTemplateVars(annotation.host, {});
const appFilter = this.replaceTemplateVars(annotation.application, {});
const proxyFilter = undefined;
return this.zabbix.getProblemsHistory(groupFilter, hostFilter, appFilter, proxyFilter, problemsOptions)
.then(problems => {
// Filter triggers by description
let triggerName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(triggerName)) {
triggers = _.filter(triggers, trigger => {
return utils.buildRegex(triggerName).test(trigger.description);
const problemName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(problemName)) {
problems = _.filter(problems, p => {
return utils.buildRegex(problemName).test(p.description);
});
} else if (triggerName) {
triggers = _.filter(triggers, trigger => {
return trigger.description === triggerName;
} else if (problemName) {
problems = _.filter(problems, p => {
return p.description === problemName;
});
}
// Remove events below the chose severity
triggers = _.filter(triggers, trigger => {
return Number(trigger.priority) >= Number(annotation.minseverity);
});
var objectids = _.map(triggers, 'triggerid');
return this.zabbix
.getEvents(objectids, timeFrom, timeTo, showOkEvents)
.then(events => {
var indexedTriggers = _.keyBy(triggers, 'triggerid');
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
events = _.filter(events, event => {
return !event.acknowledges.length;
});
}
return _.map(events, event => {
let tags;
if (annotation.showHostname) {
tags = _.map(event.hosts, 'name');
}
// Show event type (OK or Problem)
let title = Number(event.value) ? 'Problem' : 'OK';
let formatted_acknowledges = utils.formatAcknowledges(event.acknowledges);
return {
annotation: annotation,
time: event.clock * 1000,
title: title,
tags: tags,
text: indexedTriggers[event.objectid].description + formatted_acknowledges
};
});
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
problems = _.filter(problems, p => {
return !p.acknowledges?.length;
});
}
return _.map(problems, p => {
const formattedAcknowledges = utils.formatAcknowledges(p.acknowledges);
let annotationTags: string[] = [];
if (annotation.showHostname) {
annotationTags = _.map(p.hosts, 'name');
}
return {
title: p.value === '1' ? 'Problem' : 'OK',
time: p.timestamp * 1000,
annotation: annotation,
text: p.name + formattedAcknowledges,
tags: annotationTags,
};
});
});
}
@@ -617,8 +689,8 @@ export class ZabbixDatasource extends DataSourceApi {
* or empty object if no related triggers are finded.
*/
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">
<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">
</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 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>
<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 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">
<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>
<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 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>
<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.severityOptions">
</select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TEXT">
<!-- Text metric regex -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Text filter</label>
<input type="text"
class="gf-form-input"
ng-model="ctrl.target.textFilter"
spellcheck='false'
placeholder="Text filter (regex)"
ng-blur="ctrl.onTargetBlur()">
</div>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- 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>
<!-- Query options -->
<div class="gf-form-group offset-width-7" ng-if="ctrl.showQueryOptions">
<div class="gf-form" ng-hide="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<gf-form-switch class="gf-form" label-class="width-10"
label="Show disabled items"
checked="ctrl.target.options.showDisabledItems"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
</div>
<div class="gf-form" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
<gf-form-switch class="gf-form" label-class="width-10"
label="Skip empty values"
checked="ctrl.target.options.skipEmptyValues"
on-change="ctrl.onQueryOptionChange()">
</gf-form-switch>
</div>
<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 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
}
};
_.defaults(target, targetDefaults);
const targetDefaults = getTargetDefaults();
_.defaultsDeep(target, targetDefaults);
if (this.panel.type === c.ZABBIX_PROBLEMS_PANEL_ID) {
target.queryType = c.MODE_PROBLEMS;
}
// 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});
alias = host.name + ": " + alias;
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,29 +69,29 @@ 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 === '')) {
if (target.options.skipEmptyValues && (!lastValue || lastValue === '')) {
return;
}
@@ -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,11 +198,11 @@ 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
stats[group] = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0}; // severity:count
});
_.each(triggers, (trigger) => {
_.each(trigger.groups, (group) => {
@@ -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,15 +99,15 @@ 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;
let point_frame_ts = frame_ts;
let point;
for (let i=0; i < datapoints.length; i++) {
for (let i = 0; i < datapoints.length; i++) {
point = datapoints[i];
point_frame_ts = getPointTimeFrame(point[POINT_TIMESTAMP], ms_interval);
if (point_frame_ts === frame_ts) {
@@ -142,7 +137,7 @@ export function groupByRange(datapoints, groupByCallback) {
const frame_start = datapoints[0][POINT_TIMESTAMP];
const frame_end = datapoints[datapoints.length - 1][POINT_TIMESTAMP];
let point;
for (let i=0; i < datapoints.length; i++) {
for (let i = 0; i < datapoints.length; i++) {
point = datapoints[i];
frame_values.push(point[POINT_VALUE]);
}
@@ -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) {
return m.hostid === item.hostid;
if (isTriggerItem) {
// Trigger item can have multiple hosts
// Check all trigger host ids against macro host id
let hostIdFound = false;
_.forEach(item.hosts, h => {
if (h.hostid === m.hostid) {
hostIdFound = true;
}
});
return hostIdFound;
} else {
// Check app host id against macro host id
return m.hostid === item.hostid;
}
} else {
// 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
if (_.keys(hosts).length > 1 && addHostName) {
var host = _.find(hosts, {'hostid': item.hostid});
alias = host.name + ": " + alias;
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) {
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 => {
cacheObject[hash] = {
value: result,
timestamp: Date.now()
};
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)) {
item.name = utils.replaceMacro(item, macros);
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++) {
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,20 +96,36 @@ 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">
<span className="description-label">Description:&nbsp;</span>
<span>{problem.comments}</span>
<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 &&
@@ -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

@@ -11,9 +11,8 @@
* Licensed under the Apache License, Version 2.0
*/
import {TriggerPanelCtrl} from './triggers_panel_ctrl';
import {loadPluginCss} from 'grafana/app/plugins/sdk';
import './datasource-selector.directive';
import { TriggerPanelCtrl } from './triggers_panel_ctrl';
import { loadPluginCss } from 'grafana/app/plugins/sdk';
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

@@ -1,18 +1,23 @@
import _ from 'lodash';
import mocks from '../../test-setup/mocks';
import {TriggerPanelCtrl} from '../triggers_panel_ctrl';
import {DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS} from '../triggers_panel_ctrl';
import {CURRENT_SCHEMA_VERSION} from '../migrations';
import { DEFAULT_TARGET, DEFAULT_SEVERITY, PANEL_DEFAULTS } from '../triggers_panel_ctrl';
import { CURRENT_SCHEMA_VERSION } from '../migrations';
jest.mock('@grafana/runtime', () => {
return {
getDataSourceSrv: () => ({
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
}),
};
}, {virtual: true});
describe('Triggers Panel schema migration', () => {
let ctx: any = {};
let updatePanelCtrl;
const datasourceSrvMock = {
getMetricSources: () => {
return [{ meta: {id: 'alexanderzobnin-zabbix-datasource'}, value: {}, name: 'zabbix_default' }];
},
get: () => Promise.resolve({})
};
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,90 +1,53 @@
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';
import { TriggerPanelCtrl } from '../triggers_panel_ctrl';
import { PANEL_DEFAULTS, DEFAULT_TARGET } from '../triggers_panel_ctrl';
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'
]);
});
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'
]);
});
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},
]);
});
describe('When refreshing panel', () => {
@@ -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",
"object": "0",
"source": "0",
"objectid": "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",
"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,10 +251,30 @@
flex-direction: column;
}
.description-label {
font-weight: 500;
font-style: italic;
color: $text-muted;
.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 {
@@ -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) {}
}