Merge branch 'master' into backend
This commit is contained in:
277
src/panel-triggers/components/AckModal.tsx
Normal file
277
src/panel-triggers/components/AckModal.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { cx, css } from 'emotion';
|
||||
import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants';
|
||||
import { Button, VerticalGroup, Spinner, Modal, Input, Forms, stylesFactory, withTheme, Themeable } from '@grafana/ui';
|
||||
import { FAIcon } from '../../components';
|
||||
|
||||
import * as grafanaUi from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
const Checkbox: any = Forms?.Checkbox || (grafanaUi as any).Checkbox;
|
||||
const RadioButtonGroup: any = Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup;
|
||||
|
||||
const KEYBOARD_ENTER_KEY = 13;
|
||||
const KEYBOARD_ESCAPE_KEY = 27;
|
||||
|
||||
interface Props extends Themeable {
|
||||
canAck?: boolean;
|
||||
canClose?: boolean;
|
||||
severity?: number;
|
||||
withBackdrop?: boolean;
|
||||
onSubmit: (data?: AckProblemData) => Promise<any> | any;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
value: string;
|
||||
error: boolean;
|
||||
errorMessage: string;
|
||||
ackError: string;
|
||||
acknowledge: boolean;
|
||||
closeProblem: boolean;
|
||||
changeSeverity: boolean;
|
||||
selectedSeverity: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface AckProblemData {
|
||||
message: string;
|
||||
closeProblem?: boolean;
|
||||
action?: number;
|
||||
severity?: number;
|
||||
}
|
||||
|
||||
const severityOptions = [
|
||||
{value: 0, label: 'Not classified'},
|
||||
{value: 1, label: 'Information'},
|
||||
{value: 2, label: 'Warning'},
|
||||
{value: 3, label: 'Average'},
|
||||
{value: 4, label: 'High'},
|
||||
{value: 5, label: 'Disaster'}
|
||||
];
|
||||
|
||||
export class AckModalUnthemed extends PureComponent<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
withBackdrop: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: '',
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
ackError: '',
|
||||
acknowledge: false,
|
||||
closeProblem: false,
|
||||
changeSeverity: false,
|
||||
selectedSeverity: props.severity || 0,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ value: event.target.value, error: false });
|
||||
}
|
||||
|
||||
handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') {
|
||||
this.submit();
|
||||
} else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
handleBackdropClick = () => {
|
||||
this.dismiss();
|
||||
}
|
||||
|
||||
onAcknowledgeToggle = () => {
|
||||
this.setState({ acknowledge: !this.state.acknowledge, error: false });
|
||||
}
|
||||
|
||||
onChangeSeverityToggle = () => {
|
||||
this.setState({ changeSeverity: !this.state.changeSeverity, error: false });
|
||||
}
|
||||
|
||||
onCloseProblemToggle = () => {
|
||||
this.setState({ closeProblem: !this.state.closeProblem, error: false });
|
||||
}
|
||||
|
||||
onChangeSelectedSeverity = v => {
|
||||
this.setState({ selectedSeverity: v });
|
||||
};
|
||||
|
||||
dismiss = () => {
|
||||
this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false });
|
||||
this.props.onDismiss();
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
const { acknowledge, changeSeverity, closeProblem } = this.state;
|
||||
|
||||
const actionSelected = acknowledge || changeSeverity || closeProblem;
|
||||
if (!this.state.value && !actionSelected) {
|
||||
return this.setState({
|
||||
error: true,
|
||||
errorMessage: 'Enter message text or select an action'
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ ackError: '', loading: true });
|
||||
|
||||
const ackData: AckProblemData = {
|
||||
message: this.state.value,
|
||||
};
|
||||
|
||||
let action = ZBX_ACK_ACTION_ADD_MESSAGE;
|
||||
if (this.state.acknowledge) {
|
||||
action += ZBX_ACK_ACTION_ACK;
|
||||
}
|
||||
if (this.state.changeSeverity) {
|
||||
action += ZBX_ACK_ACTION_CHANGE_SEVERITY;
|
||||
ackData.severity = this.state.selectedSeverity;
|
||||
}
|
||||
if (this.state.closeProblem) {
|
||||
action += ZBX_ACK_ACTION_CLOSE;
|
||||
}
|
||||
ackData.action = action;
|
||||
|
||||
this.props.onSubmit(ackData).then(() => {
|
||||
this.dismiss();
|
||||
}).catch(err => {
|
||||
this.setState({
|
||||
ackError: err.message || err.data,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderActions() {
|
||||
const { canClose } = this.props;
|
||||
|
||||
const actions = [
|
||||
<Checkbox key="ack" label="Acknowledge" value={this.state.acknowledge} onChange={this.onAcknowledgeToggle} />,
|
||||
<Checkbox
|
||||
key="change-severity"
|
||||
label="Change severity"
|
||||
description=""
|
||||
value={this.state.changeSeverity}
|
||||
onChange={this.onChangeSeverityToggle}
|
||||
/>,
|
||||
this.state.changeSeverity &&
|
||||
<RadioButtonGroup
|
||||
key="severity"
|
||||
size="sm"
|
||||
options={severityOptions}
|
||||
value={this.state.selectedSeverity}
|
||||
onChange={this.onChangeSelectedSeverity}
|
||||
/>,
|
||||
canClose &&
|
||||
<Checkbox key="close" label="Close problem" disabled={!canClose} value={this.state.closeProblem} onChange={this.onCloseProblemToggle} />,
|
||||
];
|
||||
|
||||
// <VerticalGroup /> doesn't handle empty elements properly, so don't return it
|
||||
return actions.filter(e => e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
const styles = getStyles(theme);
|
||||
const modalClass = cx(styles.modal);
|
||||
const modalTitleClass = cx(styles.modalHeaderTitle);
|
||||
const inputGroupClass = cx('gf-form', styles.inputGroup);
|
||||
const inputClass = cx(this.state.error && styles.input);
|
||||
const inputHintClass = cx('gf-form-hint-text', styles.inputHint);
|
||||
const inputErrorClass = cx('gf-form-hint-text', styles.inputError);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onDismiss={this.dismiss}
|
||||
className={modalClass}
|
||||
title={
|
||||
<div className={modalTitleClass}>
|
||||
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="reply-all" />}
|
||||
<span className="p-l-1">Acknowledge Problem</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={inputGroupClass}>
|
||||
<label className="gf-form-hint">
|
||||
<Input className={inputClass}
|
||||
type="text"
|
||||
name="message"
|
||||
placeholder="Message"
|
||||
maxLength={64}
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}>
|
||||
</Input>
|
||||
<small className={inputHintClass}>Press Enter to submit</small>
|
||||
{this.state.error &&
|
||||
<small className={inputErrorClass}>{this.state.errorMessage}</small>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<VerticalGroup>
|
||||
{this.renderActions()}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
|
||||
{this.state.ackError &&
|
||||
<div className="gf-form ack-request-error">
|
||||
<span className={styles.ackError}>{this.state.ackError}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="gf-form-button-row text-center">
|
||||
<Button variant="primary" onClick={this.submit}>Update</Button>
|
||||
<Button variant="secondary" onClick={this.dismiss}>Cancel</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const red = theme.colors.red || (theme as any).palette.red;
|
||||
return {
|
||||
modal: css`
|
||||
width: 500px;
|
||||
`,
|
||||
modalHeaderTitle: css`
|
||||
font-size: ${theme.typography.heading.h3};
|
||||
padding-top: ${theme.spacing.sm};
|
||||
margin: 0 ${theme.spacing.md};
|
||||
display: flex;
|
||||
`,
|
||||
inputGroup: css`
|
||||
margin-bottom: 16px;
|
||||
`,
|
||||
input: css`
|
||||
border-color: ${red};
|
||||
border-radius: 2px;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px ${theme.colors.pageBg || (theme as any).colors.bg1}, 0 0 0px 4px ${red};
|
||||
`,
|
||||
inputHint: css`
|
||||
display: inherit;
|
||||
float: right;
|
||||
color: ${theme.colors.textWeak};
|
||||
`,
|
||||
inputError: css`
|
||||
float: left;
|
||||
color: ${red};
|
||||
`,
|
||||
ackError: css`
|
||||
color: ${red};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const AckModal = withTheme(AckModalUnthemed);
|
||||
@@ -3,29 +3,22 @@ import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FAIconProps {
|
||||
icon: string;
|
||||
customClass?: string;
|
||||
}
|
||||
|
||||
export default function FAIcon(props: FAIconProps) {
|
||||
return (
|
||||
<span className={`fa-icon-container ${props.customClass || ''}`}>
|
||||
<i className={`fa fa-${props.icon}`}></i>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface GFHeartIconProps {
|
||||
status: 'critical' | 'warning' | 'online' | 'ok' | 'problem';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function GFHeartIcon(props: GFHeartIconProps) {
|
||||
const status = props.status;
|
||||
const className = classNames("icon-gf", props.className,
|
||||
{ "icon-gf-critical": status === 'critical' || status === 'problem' || status === 'warning'},
|
||||
{ "icon-gf-online": status === 'online' || status === 'ok' },
|
||||
);
|
||||
return (
|
||||
<i className={className}></i>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const KEYBOARD_ENTER_KEY = 13;
|
||||
const KEYBOARD_ESCAPE_KEY = 27;
|
||||
|
||||
interface ModalProps {
|
||||
isOpen?: boolean;
|
||||
withBackdrop?: boolean;
|
||||
onSubmit: (data?: AckProblemData) => Promise<any> | any;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface ModalState {
|
||||
value: string;
|
||||
error: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AckProblemData {
|
||||
message: string;
|
||||
closeProblem?: boolean;
|
||||
action?: number;
|
||||
}
|
||||
|
||||
export class Modal extends PureComponent<ModalProps, ModalState> {
|
||||
modalContainer: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: '',
|
||||
error: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
this.modalContainer = document.body;
|
||||
}
|
||||
|
||||
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ value: event.target.value, error: false });
|
||||
}
|
||||
|
||||
handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') {
|
||||
this.submit();
|
||||
} else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
handleBackdropClick = () => {
|
||||
this.dismiss();
|
||||
}
|
||||
|
||||
dismiss = () => {
|
||||
this.setState({ value: '', error: false, message: '' });
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
if (!this.state.value) {
|
||||
return this.setState({
|
||||
error: true,
|
||||
message: 'Enter message text'
|
||||
});
|
||||
}
|
||||
this.props.onSubmit({
|
||||
message: this.state.value
|
||||
}).then(() => {
|
||||
this.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.isOpen || !this.modalContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputClass = classNames('gf-form-input', { 'zbx-ack-error': this.state.error });
|
||||
|
||||
const modalNode = (
|
||||
<div className="modal modal--narrow zbx-ack-modal" key="modal">
|
||||
<div className="modal-body">
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-header-title">
|
||||
<i className="fa fa-reply-all"></i>
|
||||
<span className="p-l-1">Acknowledge Problem</span>
|
||||
</h2>
|
||||
|
||||
<a className="modal-header-close" onClick={this.dismiss}>
|
||||
<i className="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div className="modal-content">
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-hint">
|
||||
<input className={inputClass}
|
||||
type="text"
|
||||
name="message"
|
||||
placeholder="Message"
|
||||
maxLength={64}
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}>
|
||||
</input>
|
||||
<small className="gf-form-hint-text muted">Press Enter to submit</small>
|
||||
{this.state.error &&
|
||||
<small className="gf-form-hint-text muted ack-error-message">{this.state.message}</small>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-button-row text-center">
|
||||
<button className="btn btn-success" onClick={this.submit}>Acknowledge</button>
|
||||
<button className="btn btn-inverse" onClick={this.dismiss}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const modalNodeWithBackdrop = [
|
||||
modalNode,
|
||||
<div className="modal-backdrop in" key="modal-backdrop" onClick={this.handleBackdropClick}></div>
|
||||
];
|
||||
|
||||
const modal = this.props.withBackdrop ? modalNodeWithBackdrop : modalNode;
|
||||
return ReactDOM.createPortal(modal, this.modalContainer);
|
||||
}
|
||||
}
|
||||
34
src/panel-triggers/components/Problems/AckCell.tsx
Normal file
34
src/panel-triggers/components/Problems/AckCell.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { RTCell } from '../../types';
|
||||
import { ProblemDTO } from '../../../datasource-zabbix/types';
|
||||
import { FAIcon } from '../../../components';
|
||||
import { useTheme, stylesFactory } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
countLabel: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const AckCell: React.FC<RTCell<ProblemDTO>> = (props: RTCell<ProblemDTO>) => {
|
||||
const problem = props.original;
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{problem.acknowledges?.length > 0 &&
|
||||
<>
|
||||
<FAIcon icon="comments" />
|
||||
<span className={styles.countLabel}> ({problem.acknowledges?.length})</span>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AckCell;
|
||||
@@ -1,22 +1,23 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import 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: </span>
|
||||
<span>{problem.comments}</span>
|
||||
<div className="problem-description-row">
|
||||
<div className="problem-description">
|
||||
<span className="description-label">Description: </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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import BodyPortal from './Portal';
|
||||
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
const defaultTransitionStyles = {
|
||||
transition: 'opacity 200ms linear',
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
const transitionStyles = {
|
||||
exited: { opacity: 0 },
|
||||
entering: { opacity: 0 },
|
||||
entered: { opacity: 1 },
|
||||
exiting: { opacity: 0 },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
renderContent: (content: any) => any;
|
||||
show: boolean;
|
||||
placement?: any;
|
||||
content: string | ((props: any) => JSX.Element);
|
||||
refClassName?: string;
|
||||
popperClassName?: string;
|
||||
}
|
||||
|
||||
class Popper extends PureComponent<Props> {
|
||||
render() {
|
||||
const { children, renderContent, show, placement, refClassName } = this.props;
|
||||
const { content } = this.props;
|
||||
const popperClassName = classNames('popper', this.props.popperClassName);
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||
{transitionState => (
|
||||
<BodyPortal>
|
||||
<ReactPopper placement={placement}>
|
||||
{({ ref, style, placement, arrowProps }) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
...defaultTransitionStyles,
|
||||
...transitionStyles[transitionState],
|
||||
}}
|
||||
data-placement={placement}
|
||||
className={popperClassName}
|
||||
>
|
||||
<div className="popper__background">
|
||||
{renderContent(content)}
|
||||
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ReactPopper>
|
||||
</BodyPortal>
|
||||
)}
|
||||
</Transition>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Popper;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { PureComponent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
root?: HTMLElement;
|
||||
}
|
||||
|
||||
export default class BodyPortal extends PureComponent<Props> {
|
||||
node: HTMLElement = document.createElement('div');
|
||||
portalRoot: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const {
|
||||
className,
|
||||
root = document.body
|
||||
} = this.props;
|
||||
|
||||
if (className) {
|
||||
this.node.classList.add(className);
|
||||
}
|
||||
|
||||
this.portalRoot = root;
|
||||
this.portalRoot.appendChild(this.node);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.portalRoot.removeChild(this.node);
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(this.props.children, this.node);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Popper from './Popper';
|
||||
import withPopper, { UsingPopperProps } from './withPopper';
|
||||
|
||||
class Tooltip extends PureComponent<UsingPopperProps> {
|
||||
render() {
|
||||
const { children, hidePopper, showPopper, className, ...restProps } = this.props;
|
||||
|
||||
return (
|
||||
<div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
|
||||
<Popper {...restProps}>{children}</Popper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withPopper(Tooltip);
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface UsingPopperProps {
|
||||
showPopper: (prevState: object) => void;
|
||||
hidePopper: (prevState: object) => void;
|
||||
renderContent: (content: any) => any;
|
||||
show: boolean;
|
||||
placement?: string;
|
||||
content: string | ((props: any) => JSX.Element);
|
||||
className?: string;
|
||||
refClassName?: string;
|
||||
popperClassName?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
placement?: string;
|
||||
className?: string;
|
||||
refClassName?: string;
|
||||
popperClassName?: string;
|
||||
content: string | ((props: any) => JSX.Element);
|
||||
}
|
||||
|
||||
interface State {
|
||||
placement: string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export default function withPopper(WrappedComponent) {
|
||||
return class extends React.Component<Props, State> {
|
||||
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,
|
||||
}));
|
||||
};
|
||||
|
||||
hidePopper = () => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
show: false,
|
||||
}));
|
||||
};
|
||||
|
||||
renderContent(content) {
|
||||
if (typeof content === 'function') {
|
||||
// If it's a function we assume it's a React component
|
||||
const ReactComponent = content;
|
||||
return <ReactComponent />;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { show, placement } = this.state;
|
||||
const className = this.props.className || '';
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
showPopper={this.showPopper}
|
||||
hidePopper={this.hidePopper}
|
||||
renderContent={this.renderContent}
|
||||
show={show}
|
||||
placement={placement}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
const template = `
|
||||
<value-select-dropdown
|
||||
variable="ctrl.dsOptions"
|
||||
on-updated="ctrl.onChange(ctrl.dsOptions)"
|
||||
dashboard="ctrl.dashboard">
|
||||
</value-select-dropdown>
|
||||
`;
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('datasourceSelector', () => {
|
||||
return {
|
||||
scope: {
|
||||
datasources: "=",
|
||||
options: "=",
|
||||
onChange: "&"
|
||||
},
|
||||
controller: DatasourceSelectorCtrl,
|
||||
controllerAs: 'ctrl',
|
||||
template: template
|
||||
};
|
||||
});
|
||||
|
||||
class DatasourceSelectorCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
this.scope = $scope;
|
||||
let datasources = $scope.datasources;
|
||||
let options = $scope.options;
|
||||
this.dsOptions = {
|
||||
multi: true,
|
||||
current: {value: datasources, text: datasources.join(" + ")},
|
||||
options: _.map(options, (ds) => {
|
||||
return {text: ds, value: ds, selected: _.includes(datasources, ds)};
|
||||
})
|
||||
};
|
||||
// Fix for Grafana 6.0
|
||||
// https://github.com/grafana/grafana/blob/v6.0.0/public/app/core/directives/value_select_dropdown.ts#L291
|
||||
this.dashboard = {
|
||||
on: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
onChange(updatedOptions) {
|
||||
let newDataSources = updatedOptions.current.value;
|
||||
this.scope.datasources = newDataSources;
|
||||
|
||||
// Run after model was changed
|
||||
this.scope.$$postDigest(() => {
|
||||
this.scope.onChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
1
src/panel-triggers/img/icn-zabbix-problems-panel.svg
Normal file
1
src/panel-triggers/img/icn-zabbix-problems-panel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 336 336" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Layer_2"><g id="Layer_1-2"><path d="M71.833,181.833c-8.666,-8.666 -27.375,-5.916 -32.25,10.667c-4.875,-15.5 -26.5,-18.958 -34.666,-8c-7.34,9.842 -6.304,23.732 2.416,32.375l29.417,29.458c1.578,1.424 4.005,1.424 5.583,0l29.459,-29.458c9.494,-9.664 9.494,-25.378 0,-35.042l0.041,0Z" style="fill:#0b9d4d;fill-rule:nonzero;"/><path d="M71.833,91.958c-8.333,-8.333 -25.916,-5.958 -31.541,9.042l-2.792,6.958l12.083,2.875c1.471,0.358 2.514,1.686 2.514,3.199c0,0.478 -0.104,0.951 -0.305,1.385l-7.875,16.666c-0.519,1.161 -1.688,1.902 -2.959,1.875c-0.474,0.005 -0.943,-0.095 -1.375,-0.291c-1.507,-0.776 -2.195,-2.586 -1.583,-4.167l6.167,-13l-11.834,-2.833c-0.918,-0.216 -1.696,-0.826 -2.125,-1.667c-0.394,-0.859 -0.394,-1.849 0,-2.708l5.292,-13.5c-8.125,-9.084 -23.792,-9.959 -30.458,-0.959c-7.34,9.842 -6.304,23.732 2.416,32.375l29.417,29.459c1.578,1.424 4.005,1.424 5.583,0l29.459,-29.459c9.606,-9.709 9.568,-25.586 -0.084,-35.25Z" style="fill:#d40000;fill-rule:nonzero;"/><path d="M71.649,270.203c-8.333,-8.334 -25.917,-5.959 -31.542,9.041l-2.791,6.959l12.083,2.875c1.47,0.357 2.514,1.685 2.514,3.198c0,0.478 -0.104,0.951 -0.306,1.385l-7.875,16.667c-0.518,1.16 -1.687,1.901 -2.958,1.875c-0.474,0.004 -0.943,-0.096 -1.375,-0.292c-1.508,-0.776 -2.195,-2.585 -1.583,-4.167l6.166,-13l-11.833,-2.833c-0.918,-0.216 -1.697,-0.826 -2.125,-1.667c-0.394,-0.859 -0.394,-1.849 0,-2.708l5.292,-13.5c-8.125,-9.083 -23.792,-9.958 -30.459,-0.958c-7.34,9.841 -6.303,23.732 2.417,32.375l29.417,29.458c1.578,1.424 4.005,1.424 5.583,0l29.458,-29.458c9.607,-9.71 9.569,-25.587 -0.083,-35.25Z" style="fill:#d40000;fill-rule:nonzero;"/><path d="M71.649,4.685c-8.333,-8.334 -25.917,-5.959 -31.542,9.041l-2.791,6.959l12.083,2.875c1.47,0.357 2.514,1.685 2.514,3.198c0,0.479 -0.104,0.951 -0.306,1.385l-7.875,16.667c-0.518,1.161 -1.687,1.902 -2.958,1.875c-0.474,0.004 -0.943,-0.096 -1.375,-0.292c-1.508,-0.776 -2.195,-2.585 -1.583,-4.167l6.166,-13l-11.833,-2.833c-0.918,-0.216 -1.697,-0.826 -2.125,-1.667c-0.394,-0.859 -0.394,-1.849 0,-2.708l5.292,-13.5c-8.125,-9.083 -23.792,-9.958 -30.459,-0.958c-7.34,9.841 -6.303,23.732 2.417,32.375l29.417,29.458c1.578,1.424 4.005,1.424 5.583,0l29.458,-29.458c9.607,-9.71 9.569,-25.586 -0.083,-35.25Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="266.595" width="108.333" height="68.447" style="fill:#8e0000;"/><path d="M109.556,318.667l0,-35.834l208.055,0l0,35.834l-208.055,0m-17.889,16.666l239.361,0c2.453,0 4.472,-1.881 4.472,-4.166l0,-60.834c0,-2.285 -2.019,-4.166 -4.472,-4.166l-239.361,0l0,69.166Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="179.125" width="108.333" height="69.167" style="fill:#0b9d4d;"/><path d="M109.556,231.5l0,-35.667l208.055,0l0,35.792l-208.055,0m-17.889,16.667l239.361,0c2.453,0 4.472,-1.881 4.472,-4.167l0,-60.792c0,-2.285 -2.019,-4.166 -4.472,-4.166l-239.361,0l0,69.125Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="89.167" width="108.333" height="69.167" style="fill:#bdac03;"/><path d="M109.556,141.667l0,-35.834l208.055,0l0,35.834l-208.055,0m-17.889,16.666l239.361,0c2.453,0 4.472,-1.881 4.472,-4.166l0,-60.834c0,-2.285 -2.019,-4.166 -4.472,-4.166l-239.361,0l0,69.166Z" style="fill:#d40000;fill-rule:nonzero;"/><rect x="91.667" y="-0.072" width="108.333" height="68.405" style="fill:#8e0000;"/><path d="M109.556,52.428l0,-35.833l208.055,0l0,35.833l-208.055,0m-17.889,16.667l239.361,0c2.453,0 4.472,-1.881 4.472,-4.167l0,-60.833c0,-2.286 -2.019,-4.167 -4.472,-4.167l-239.361,0l0,69.167Z" style="fill:#d40000;fill-rule:nonzero;"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,9 +1,27 @@
|
||||
import _ from 'lodash';
|
||||
import { 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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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] },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Data sources</label>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<datasource-selector
|
||||
datasources="editor.selectedDatasources"
|
||||
options="editor.panelCtrl.available_datasources"
|
||||
on-change="editor.datasourcesChanged()">
|
||||
</datasource-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row" ng-repeat="target in ctrl.panel.targets">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">{{ target.datasource }}</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Group</label>
|
||||
<input type="text"
|
||||
ng-model="target.group.filter"
|
||||
bs-typeahead="editor.getGroupNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(target.group.filter),
|
||||
'zbx-regex': editor.isRegex(target.group.filter)
|
||||
}">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Host</label>
|
||||
<input type="text"
|
||||
ng-model="target.host.filter"
|
||||
bs-typeahead="editor.getHostNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(target.host.filter),
|
||||
'zbx-regex': editor.isRegex(target.host.filter)
|
||||
}">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Proxy</label>
|
||||
<input type="text"
|
||||
ng-model="target.proxy.filter"
|
||||
bs-typeahead="editor.getProxyNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(target.proxy.filter),
|
||||
'zbx-regex': editor.isRegex(target.proxy.filter)
|
||||
}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Application</label>
|
||||
<input type="text"
|
||||
ng-model="target.application.filter"
|
||||
bs-typeahead="editor.getApplicationNames[target.datasource]"
|
||||
ng-blur="editor.parseTarget()"
|
||||
data-min-length=0
|
||||
data-items=100
|
||||
class="gf-form-input width-14"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(target.application.filter),
|
||||
'zbx-regex': editor.isRegex(target.application.filter)
|
||||
}">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Trigger</label>
|
||||
<input type="text"
|
||||
ng-model="target.trigger.filter"
|
||||
ng-blur="editor.parseTarget()"
|
||||
placeholder="trigger name"
|
||||
class="gf-form-input width-14"
|
||||
ng-style="target.trigger.style"
|
||||
ng-class="{
|
||||
'zbx-variable': editor.isVariable(target.trigger.filter),
|
||||
'zbx-regex': editor.isRegex(target.trigger.filter)
|
||||
}"
|
||||
empty-to-null>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Tags</label>
|
||||
<input type="text" class="gf-form-input width-14"
|
||||
ng-model="target.tags.filter"
|
||||
ng-blur="editor.parseTarget()"
|
||||
placeholder="tag1:value1, tag2:value2">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,13 +3,14 @@
|
||||
"name": "Zabbix Problems",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()});
|
||||
}
|
||||
|
||||
@@ -1,725 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||
import * as utils from '../datasource-zabbix/utils';
|
||||
import { PanelCtrl } from 'grafana/app/plugins/sdk';
|
||||
import { triggerPanelOptionsTab } from './options_tab';
|
||||
import { triggerPanelTriggersTab } from './triggers_tab';
|
||||
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||
import ProblemList from './components/Problems/Problems';
|
||||
import AlertList from './components/AlertList/AlertList';
|
||||
import { getNextRefIdChar } from './utils';
|
||||
|
||||
const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource';
|
||||
const PROBLEM_EVENTS_LIMIT = 100;
|
||||
|
||||
export const DEFAULT_TARGET = {
|
||||
group: {filter: ""},
|
||||
host: {filter: ""},
|
||||
application: {filter: ""},
|
||||
trigger: {filter: ""},
|
||||
tags: {filter: ""},
|
||||
proxy: {filter: ""},
|
||||
};
|
||||
|
||||
export const getDefaultTarget = (targets) => {
|
||||
return {
|
||||
group: {filter: ""},
|
||||
host: {filter: ""},
|
||||
application: {filter: ""},
|
||||
trigger: {filter: ""},
|
||||
tags: {filter: ""},
|
||||
proxy: {filter: ""},
|
||||
refId: getNextRefIdChar(targets),
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_SEVERITY = [
|
||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
|
||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true},
|
||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true},
|
||||
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true},
|
||||
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true},
|
||||
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true},
|
||||
];
|
||||
|
||||
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
||||
|
||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||
|
||||
export const PANEL_DEFAULTS = {
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
targets: [getDefaultTarget([])],
|
||||
// Fields
|
||||
hostField: true,
|
||||
hostTechNameField: false,
|
||||
hostGroups: false,
|
||||
hostProxy: false,
|
||||
showTags: true,
|
||||
statusField: true,
|
||||
statusIcon: false,
|
||||
severityField: true,
|
||||
ageField: false,
|
||||
descriptionField: true,
|
||||
descriptionAtNewLine: false,
|
||||
// Options
|
||||
hostsInMaintenance: true,
|
||||
showTriggers: 'all triggers',
|
||||
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
||||
showEvents: { text: 'Problems', value: 1 },
|
||||
limit: 100,
|
||||
// View options
|
||||
layout: 'table',
|
||||
fontSize: '100%',
|
||||
pageSize: 10,
|
||||
problemTimeline: true,
|
||||
highlightBackground: false,
|
||||
highlightNewEvents: false,
|
||||
highlightNewerThan: '1h',
|
||||
customLastChangeFormat: false,
|
||||
lastChangeFormat: "",
|
||||
resizedColumns: [],
|
||||
// Triggers severity and colors
|
||||
triggerSeverity: getDefaultSeverity(),
|
||||
okEventColor: 'rgb(56, 189, 113)',
|
||||
ackEventColor: 'rgb(56, 219, 156)',
|
||||
markAckEvents: false,
|
||||
};
|
||||
|
||||
const triggerStatusMap = {
|
||||
'0': 'OK',
|
||||
'1': 'PROBLEM'
|
||||
};
|
||||
|
||||
export class TriggerPanelCtrl extends PanelCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, $timeout, datasourceSrv, templateSrv, contextSrv, dashboardSrv, timeSrv) {
|
||||
super($scope, $injector);
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
this.templateSrv = templateSrv;
|
||||
this.contextSrv = contextSrv;
|
||||
this.dashboardSrv = dashboardSrv;
|
||||
this.timeSrv = timeSrv;
|
||||
this.scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
|
||||
this.editorTabIndex = 1;
|
||||
this.triggerStatusMap = triggerStatusMap;
|
||||
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
||||
this.pageIndex = 0;
|
||||
this.triggerList = [];
|
||||
this.datasources = {};
|
||||
this.range = {};
|
||||
|
||||
this.panel = migratePanelSchema(this.panel);
|
||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||
|
||||
this.available_datasources = _.map(this.getZabbixDataSources(), 'name');
|
||||
if (this.panel.targets && !this.panel.targets[0].datasource) {
|
||||
this.panel.targets[0].datasource = this.available_datasources[0];
|
||||
}
|
||||
|
||||
this.initDatasources();
|
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||
this.events.on('refresh', this.onRefresh.bind(this));
|
||||
}
|
||||
|
||||
setPanelError(err, defaultError) {
|
||||
const defaultErrorMessage = defaultError || "Request Error";
|
||||
this.inspector = { error: err };
|
||||
this.error = err.message || defaultErrorMessage;
|
||||
if (err.data) {
|
||||
if (err.data.message) {
|
||||
this.error = err.data.message;
|
||||
}
|
||||
if (err.data.error) {
|
||||
this.error = err.data.error;
|
||||
}
|
||||
}
|
||||
|
||||
this.events.emit('data-error', err);
|
||||
console.log('Panel data error:', err);
|
||||
}
|
||||
|
||||
initDatasources() {
|
||||
if (!this.panel.targets) {
|
||||
return;
|
||||
}
|
||||
const targetDatasources = _.compact(this.panel.targets.map(target => target.datasource));
|
||||
let promises = targetDatasources.map(ds => {
|
||||
// Load datasource
|
||||
return this.datasourceSrv.get(ds)
|
||||
.then(datasource => {
|
||||
this.datasources[ds] = datasource;
|
||||
return datasource;
|
||||
});
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
getZabbixDataSources() {
|
||||
return _.filter(this.datasourceSrv.getMetricSources(), datasource => {
|
||||
return datasource.meta.id === ZABBIX_DS_ID && datasource.value;
|
||||
});
|
||||
}
|
||||
|
||||
isEmptyTargets() {
|
||||
const emptyTargets = _.isEmpty(this.panel.targets);
|
||||
const emptyTarget = (this.panel.targets.length === 1 && (
|
||||
_.isEmpty(this.panel.targets[0]) ||
|
||||
this.panel.targets[0].target === ""
|
||||
));
|
||||
return emptyTargets || emptyTarget;
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
this.addEditorTab('Triggers', triggerPanelTriggersTab, 1);
|
||||
this.addEditorTab('Options', triggerPanelOptionsTab, 2);
|
||||
}
|
||||
|
||||
setTimeQueryStart() {
|
||||
this.timing.queryStart = new Date().getTime();
|
||||
}
|
||||
|
||||
setTimeQueryEnd() {
|
||||
this.timing.queryEnd = (new Date()).getTime();
|
||||
}
|
||||
|
||||
onRefresh() {
|
||||
// ignore fetching data if another panel is in fullscreen
|
||||
if (this.otherPanelInFullscreenMode()) { return; }
|
||||
|
||||
this.range = this.timeSrv.timeRange();
|
||||
|
||||
// clear loading/error state
|
||||
delete this.error;
|
||||
this.loading = true;
|
||||
this.setTimeQueryStart();
|
||||
this.pageIndex = 0;
|
||||
|
||||
return this.getTriggers()
|
||||
.then(triggers => {
|
||||
// Notify panel that request is finished
|
||||
this.loading = false;
|
||||
this.setTimeQueryEnd();
|
||||
return this.renderTriggers(triggers);
|
||||
})
|
||||
.then(() => {
|
||||
this.$timeout(() => {
|
||||
this.renderingCompleted();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading = false;
|
||||
|
||||
if (err.cancelled) {
|
||||
console.log('Panel request cancelled', err);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setPanelError(err);
|
||||
});
|
||||
}
|
||||
|
||||
renderTriggers(zabbixTriggers) {
|
||||
let triggers = _.cloneDeep(zabbixTriggers || this.triggerListUnfiltered);
|
||||
this.triggerListUnfiltered = _.cloneDeep(triggers);
|
||||
|
||||
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
||||
triggers = this.filterTriggersPost(triggers);
|
||||
triggers = this.sortTriggers(triggers);
|
||||
|
||||
// Limit triggers number
|
||||
triggers = triggers.slice(0, this.panel.limit || PANEL_DEFAULTS.limit);
|
||||
|
||||
this.triggerList = triggers;
|
||||
|
||||
return this.$timeout(() => {
|
||||
return super.render(this.triggerList);
|
||||
});
|
||||
}
|
||||
|
||||
getTriggers() {
|
||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||
|
||||
let promises = _.map(this.panel.targets, (target) => {
|
||||
const ds = target.datasource;
|
||||
let proxies;
|
||||
let showAckButton = true;
|
||||
return this.datasourceSrv.get(ds)
|
||||
.then(datasource => {
|
||||
const zabbix = datasource.zabbix;
|
||||
const showEvents = this.panel.showEvents.value;
|
||||
const triggerFilter = target;
|
||||
const showProxy = this.panel.hostProxy;
|
||||
const getProxiesPromise = showProxy ? zabbix.getProxies() : () => [];
|
||||
showAckButton = !datasource.disableReadOnlyUsersAck || userIsEditor;
|
||||
|
||||
// Replace template variables
|
||||
const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter);
|
||||
const hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter);
|
||||
const appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter);
|
||||
const proxyFilter = datasource.replaceTemplateVars(triggerFilter.proxy.filter);
|
||||
|
||||
let triggersOptions = {
|
||||
showTriggers: showEvents
|
||||
};
|
||||
|
||||
if (showEvents !== 1) {
|
||||
triggersOptions.timeFrom = timeFrom;
|
||||
triggersOptions.timeTo = timeTo;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions, proxyFilter),
|
||||
getProxiesPromise
|
||||
]);
|
||||
}).then(([triggers, sourceProxies]) => {
|
||||
proxies = _.keyBy(sourceProxies, 'proxyid');
|
||||
const eventids = _.compact(triggers.map(trigger => {
|
||||
return trigger.lastEvent.eventid;
|
||||
}));
|
||||
return Promise.all([
|
||||
this.datasources[ds].zabbix.getExtendedEventData(eventids),
|
||||
Promise.resolve(triggers)
|
||||
]);
|
||||
})
|
||||
.then(([events, triggers]) => {
|
||||
this.addEventTags(events, triggers);
|
||||
this.addAcknowledges(events, triggers);
|
||||
return triggers;
|
||||
})
|
||||
.then(triggers => this.setMaintenanceStatus(triggers))
|
||||
.then(triggers => this.setAckButtonStatus(triggers, showAckButton))
|
||||
.then(triggers => this.filterTriggersPre(triggers, target))
|
||||
.then(triggers => this.addTriggerDataSource(triggers, target))
|
||||
.then(triggers => this.addTriggerHostProxy(triggers, proxies));
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(results => _.flatten(results));
|
||||
}
|
||||
|
||||
addAcknowledges(events, triggers) {
|
||||
// Map events to triggers
|
||||
_.each(triggers, trigger => {
|
||||
var event = _.find(events, event => {
|
||||
return event.eventid === trigger.lastEvent.eventid;
|
||||
});
|
||||
|
||||
if (event) {
|
||||
trigger.acknowledges = _.map(event.acknowledges, this.formatAcknowledge.bind(this));
|
||||
}
|
||||
|
||||
if (!trigger.lastEvent.eventid) {
|
||||
trigger.lastEvent = null;
|
||||
}
|
||||
});
|
||||
|
||||
return triggers;
|
||||
}
|
||||
|
||||
formatAcknowledge(ack) {
|
||||
let timestamp = moment.unix(ack.clock);
|
||||
if (this.panel.customLastChangeFormat) {
|
||||
ack.time = timestamp.format(this.panel.lastChangeFormat);
|
||||
} else {
|
||||
ack.time = timestamp.format(this.defaultTimeFormat);
|
||||
}
|
||||
ack.user = ack.alias || '';
|
||||
if (ack.name || ack.surname) {
|
||||
const fullName = `${ack.name || ''} ${ack.surname || ''}`;
|
||||
ack.user += ` (${fullName})`;
|
||||
}
|
||||
return ack;
|
||||
}
|
||||
|
||||
addEventTags(events, triggers) {
|
||||
_.each(triggers, trigger => {
|
||||
var event = _.find(events, event => {
|
||||
return event.eventid === trigger.lastEvent.eventid;
|
||||
});
|
||||
if (event && event.tags && event.tags.length) {
|
||||
trigger.tags = event.tags;
|
||||
}
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
filterTriggersPre(triggerList, target) {
|
||||
// Filter triggers by description
|
||||
const ds = target.datasource;
|
||||
let triggerFilter = target.trigger.filter;
|
||||
triggerFilter = this.datasources[ds].replaceTemplateVars(triggerFilter);
|
||||
if (triggerFilter) {
|
||||
triggerList = filterTriggers(triggerList, triggerFilter);
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
// const target = this.panel.targets[ds];
|
||||
if (target.tags.filter) {
|
||||
let tagsFilter = this.datasources[ds].replaceTemplateVars(target.tags.filter);
|
||||
// replaceTemplateVars() builds regex-like string, so we should trim it.
|
||||
tagsFilter = tagsFilter.replace('/^', '').replace('$/', '');
|
||||
const tags = this.parseTags(tagsFilter);
|
||||
triggerList = _.filter(triggerList, trigger => {
|
||||
return _.every(tags, (tag) => {
|
||||
return _.find(trigger.tags, {tag: tag.tag, value: tag.value});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return triggerList;
|
||||
}
|
||||
|
||||
filterTriggersPost(triggers) {
|
||||
let triggerList = _.cloneDeep(triggers);
|
||||
|
||||
// Filter acknowledged triggers
|
||||
if (this.panel.showTriggers === 'unacknowledged') {
|
||||
triggerList = _.filter(triggerList, trigger => {
|
||||
return !(trigger.acknowledges && trigger.acknowledges.length);
|
||||
});
|
||||
} else if (this.panel.showTriggers === 'acknowledged') {
|
||||
triggerList = _.filter(triggerList, trigger => {
|
||||
return trigger.acknowledges && trigger.acknowledges.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by maintenance status
|
||||
if (!this.panel.hostsInMaintenance) {
|
||||
triggerList = _.filter(triggerList, (trigger) => trigger.maintenance === false);
|
||||
}
|
||||
|
||||
// Filter triggers by severity
|
||||
triggerList = _.filter(triggerList, trigger => {
|
||||
return this.panel.triggerSeverity[trigger.priority].show;
|
||||
});
|
||||
|
||||
return triggerList;
|
||||
}
|
||||
|
||||
setMaintenanceStatus(triggers) {
|
||||
_.each(triggers, (trigger) => {
|
||||
let maintenance_status = _.some(trigger.hosts, (host) => host.maintenance_status === '1');
|
||||
trigger.maintenance = maintenance_status;
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
setAckButtonStatus(triggers, showAckButton) {
|
||||
_.each(triggers, (trigger) => {
|
||||
trigger.showAckButton = showAckButton;
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
addTriggerDataSource(triggers, target) {
|
||||
_.each(triggers, (trigger) => {
|
||||
trigger.datasource = target.datasource;
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
addTriggerHostProxy(triggers, proxies) {
|
||||
triggers.forEach(trigger => {
|
||||
if (trigger.hosts && trigger.hosts.length) {
|
||||
let host = trigger.hosts[0];
|
||||
if (host.proxy_hostid !== '0') {
|
||||
const hostProxy = proxies[host.proxy_hostid];
|
||||
host.proxy = hostProxy ? hostProxy.host : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
return triggers;
|
||||
}
|
||||
|
||||
sortTriggers(triggerList) {
|
||||
if (this.panel.sortTriggersBy.value === 'priority') {
|
||||
triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||
} else {
|
||||
triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||
}
|
||||
return triggerList;
|
||||
}
|
||||
|
||||
formatTrigger(zabbixTrigger) {
|
||||
let trigger = _.cloneDeep(zabbixTrigger);
|
||||
|
||||
// Set host and proxy that the trigger belongs
|
||||
if (trigger.hosts && trigger.hosts.length) {
|
||||
const host = trigger.hosts[0];
|
||||
trigger.host = host.name;
|
||||
trigger.hostTechName = host.host;
|
||||
if (host.proxy) {
|
||||
trigger.proxy = host.proxy;
|
||||
}
|
||||
}
|
||||
|
||||
// Set tags if present
|
||||
if (trigger.tags && trigger.tags.length === 0) {
|
||||
trigger.tags = null;
|
||||
}
|
||||
|
||||
// Handle multi-line description
|
||||
if (trigger.comments) {
|
||||
trigger.comments = trigger.comments.replace('\n', '<br>');
|
||||
}
|
||||
|
||||
trigger.lastchangeUnix = Number(trigger.lastchange);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
parseTags(tagStr) {
|
||||
if (!tagStr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||
tags = _.map(tags, (tag) => {
|
||||
const tagParts = tag.split(':');
|
||||
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
tagsToString(tags) {
|
||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||
}
|
||||
|
||||
addTagFilter(tag, datasource) {
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource);
|
||||
console.log(target);
|
||||
let tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
let newTag = {tag: tag.tag, value: tag.value};
|
||||
targetTags.push(newTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
let newFilter = this.tagsToString(targetTags);
|
||||
target.tags.filter = newFilter;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
removeTagFilter(tag, datasource) {
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource);
|
||||
let tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
_.remove(targetTags, t => t.tag === tag.tag && t.value === tag.value);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
let newFilter = this.tagsToString(targetTags);
|
||||
target.tags.filter = newFilter;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getProblemEvents(problem) {
|
||||
const triggerids = [problem.triggerid];
|
||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||
return this.datasourceSrv.get(problem.datasource)
|
||||
.then(datasource => {
|
||||
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||
});
|
||||
}
|
||||
|
||||
getProblemAlerts(problem) {
|
||||
if (!problem.lastEvent || problem.lastEvent.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const eventids = [problem.lastEvent.eventid];
|
||||
return this.datasourceSrv.get(problem.datasource)
|
||||
.then(datasource => {
|
||||
return datasource.zabbix.getEventAlerts(eventids);
|
||||
});
|
||||
}
|
||||
|
||||
formatHostName(trigger) {
|
||||
let host = "";
|
||||
if (this.panel.hostField && this.panel.hostTechNameField) {
|
||||
host = `${trigger.host} (${trigger.hostTechName})`;
|
||||
} else if (this.panel.hostField || this.panel.hostTechNameField) {
|
||||
host = this.panel.hostField ? trigger.host : trigger.hostTechName;
|
||||
}
|
||||
if (this.panel.hostProxy && trigger.proxy) {
|
||||
host = `${trigger.proxy}: ${host}`;
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
formatHostGroups(trigger) {
|
||||
let groupNames = "";
|
||||
if (this.panel.hostGroups) {
|
||||
let groups = _.map(trigger.groups, 'name').join(', ');
|
||||
groupNames += `[ ${groups} ]`;
|
||||
}
|
||||
|
||||
return groupNames;
|
||||
}
|
||||
|
||||
isNewTrigger(trigger) {
|
||||
try {
|
||||
const highlightIntervalMs = utils.parseInterval(this.panel.highlightNewerThan || PANEL_DEFAULTS.highlightNewerThan);
|
||||
const durationSec = (Date.now() - trigger.lastchangeUnix * 1000);
|
||||
return durationSec < highlightIntervalMs;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getAlertIconClass(trigger) {
|
||||
let iconClass = '';
|
||||
if (trigger.value === '1' && trigger.priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
} else {
|
||||
iconClass = 'icon-gf-online';
|
||||
}
|
||||
|
||||
if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) {
|
||||
iconClass += ' zabbix-trigger--blinked';
|
||||
}
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
getAlertIconClassBySeverity(triggerSeverity) {
|
||||
let iconClass = 'icon-gf-online';
|
||||
if (triggerSeverity.priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
}
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
getAlertStateClass(trigger) {
|
||||
let statusClass = '';
|
||||
|
||||
if (trigger.value === '1') {
|
||||
statusClass = 'alert-state-critical';
|
||||
} else {
|
||||
statusClass = 'alert-state-ok';
|
||||
}
|
||||
|
||||
if (this.panel.highlightNewEvents && this.isNewTrigger(trigger)) {
|
||||
statusClass += ' zabbix-trigger--blinked';
|
||||
}
|
||||
|
||||
return statusClass;
|
||||
}
|
||||
|
||||
resetResizedColumns() {
|
||||
this.panel.resizedColumns = [];
|
||||
this.render();
|
||||
}
|
||||
|
||||
acknowledgeTrigger(trigger, message) {
|
||||
let eventid = trigger.lastEvent ? trigger.lastEvent.eventid : null;
|
||||
let grafana_user = this.contextSrv.user.name;
|
||||
let ack_message = grafana_user + ' (Grafana): ' + message;
|
||||
return this.datasourceSrv.get(trigger.datasource)
|
||||
.then(datasource => {
|
||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
||||
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
||||
}
|
||||
if (eventid) {
|
||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message);
|
||||
} else {
|
||||
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
||||
}
|
||||
})
|
||||
.then(this.onRefresh.bind(this))
|
||||
.catch((err) => {
|
||||
this.setPanelError(err);
|
||||
});
|
||||
}
|
||||
|
||||
handlePageSizeChange(pageSize, pageIndex) {
|
||||
this.panel.pageSize = pageSize;
|
||||
this.pageIndex = pageIndex;
|
||||
this.scope.$apply(() => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
handleColumnResize(newResized) {
|
||||
this.panel.resizedColumns = newResized;
|
||||
this.scope.$apply(() => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
let panel = ctrl.panel;
|
||||
let triggerList = ctrl.triggerList;
|
||||
|
||||
scope.$watchGroup(['ctrl.triggerList'], renderPanel);
|
||||
ctrl.events.on('render', (renderData) => {
|
||||
triggerList = renderData || triggerList;
|
||||
renderPanel();
|
||||
});
|
||||
|
||||
function renderPanel() {
|
||||
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
||||
|
||||
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1));
|
||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||
|
||||
const pageSize = panel.pageSize || 10;
|
||||
const loading = ctrl.loading && (!ctrl.triggerList || !ctrl.triggerList.length);
|
||||
|
||||
let panelOptions = {};
|
||||
for (let prop in PANEL_DEFAULTS) {
|
||||
panelOptions[prop] = ctrl.panel[prop];
|
||||
}
|
||||
const problemsListProps = {
|
||||
problems: ctrl.triggerList,
|
||||
panelOptions,
|
||||
timeRange: { timeFrom, timeTo },
|
||||
loading,
|
||||
pageSize,
|
||||
fontSize: fontSizeProp,
|
||||
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
||||
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||
onProblemAck: (trigger, data) => {
|
||||
const message = data.message;
|
||||
return ctrl.acknowledgeTrigger(trigger, message);
|
||||
},
|
||||
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
||||
if (ctrlKey || shiftKey) {
|
||||
ctrl.removeTagFilter(tag, datasource);
|
||||
} else {
|
||||
ctrl.addTagFilter(tag, datasource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let problemsReactElem;
|
||||
if (panel.layout === 'list') {
|
||||
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
||||
} else {
|
||||
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
||||
}
|
||||
ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
||||
|
||||
function filterTriggers(triggers, triggerFilter) {
|
||||
if (utils.isRegex(triggerFilter)) {
|
||||
return _.filter(triggers, function(trigger) {
|
||||
return utils.buildRegex(triggerFilter).test(trigger.description);
|
||||
});
|
||||
} else {
|
||||
return _.filter(triggers, function(trigger) {
|
||||
return trigger.description === triggerFilter;
|
||||
});
|
||||
}
|
||||
}
|
||||
434
src/panel-triggers/triggers_panel_ctrl.ts
Normal file
434
src/panel-triggers/triggers_panel_ctrl.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { PanelEvents } from '@grafana/data';
|
||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||
import * as utils from '../datasource-zabbix/utils';
|
||||
import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';
|
||||
import { triggerPanelOptionsTab } from './options_tab';
|
||||
import { migratePanelSchema, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||
import ProblemList from './components/Problems/Problems';
|
||||
import AlertList from './components/AlertList/AlertList';
|
||||
import { ProblemDTO } from 'datasource-zabbix/types';
|
||||
|
||||
const PROBLEM_EVENTS_LIMIT = 100;
|
||||
|
||||
export const DEFAULT_TARGET = {
|
||||
group: {filter: ""},
|
||||
host: {filter: ""},
|
||||
application: {filter: ""},
|
||||
trigger: {filter: ""},
|
||||
tags: {filter: ""},
|
||||
proxy: {filter: ""},
|
||||
showProblems: 'problems',
|
||||
};
|
||||
|
||||
export const DEFAULT_SEVERITY = [
|
||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true},
|
||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true},
|
||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true},
|
||||
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true},
|
||||
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true},
|
||||
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true},
|
||||
];
|
||||
|
||||
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
||||
|
||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||
|
||||
export const PANEL_DEFAULTS = {
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
// Fields
|
||||
hostField: true,
|
||||
hostTechNameField: false,
|
||||
hostProxy: false,
|
||||
hostGroups: false,
|
||||
showTags: true,
|
||||
statusField: true,
|
||||
statusIcon: false,
|
||||
severityField: true,
|
||||
ackField: true,
|
||||
ageField: false,
|
||||
descriptionField: true,
|
||||
descriptionAtNewLine: false,
|
||||
// Options
|
||||
sortProblems: 'lastchange',
|
||||
limit: null,
|
||||
// View options
|
||||
layout: 'table',
|
||||
fontSize: '100%',
|
||||
pageSize: 10,
|
||||
problemTimeline: true,
|
||||
highlightBackground: false,
|
||||
highlightNewEvents: false,
|
||||
highlightNewerThan: '1h',
|
||||
customLastChangeFormat: false,
|
||||
lastChangeFormat: "",
|
||||
resizedColumns: [],
|
||||
// Triggers severity and colors
|
||||
triggerSeverity: getDefaultSeverity(),
|
||||
okEventColor: 'rgb(56, 189, 113)',
|
||||
ackEventColor: 'rgb(56, 219, 156)',
|
||||
markAckEvents: false,
|
||||
};
|
||||
|
||||
const triggerStatusMap = {
|
||||
'0': 'OK',
|
||||
'1': 'PROBLEM'
|
||||
};
|
||||
|
||||
export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||
scope: any;
|
||||
useDataFrames: boolean;
|
||||
triggerStatusMap: any;
|
||||
defaultTimeFormat: string;
|
||||
pageIndex: number;
|
||||
renderData: any[];
|
||||
problems: any[];
|
||||
contextSrv: any;
|
||||
static templateUrl: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, $timeout) {
|
||||
super($scope, $injector);
|
||||
this.scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
|
||||
// Tell Grafana do not convert data frames to table or series
|
||||
this.useDataFrames = true;
|
||||
|
||||
this.triggerStatusMap = triggerStatusMap;
|
||||
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
||||
this.pageIndex = 0;
|
||||
this.range = {};
|
||||
this.renderData = [];
|
||||
|
||||
this.panel = migratePanelSchema(this.panel);
|
||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||
|
||||
// this.events.on(PanelEvents.render, this.onRender.bind(this));
|
||||
this.events.on('data-frames-received', this.onDataFramesReceived.bind(this));
|
||||
this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this));
|
||||
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
// Update schema version to prevent migration on up-to-date targets
|
||||
this.panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||
this.addEditorTab('Options', triggerPanelOptionsTab);
|
||||
}
|
||||
|
||||
onDataFramesReceived(data: any): Promise<any> {
|
||||
this.range = this.timeSrv.timeRange();
|
||||
let problems = [];
|
||||
|
||||
if (data && data.length) {
|
||||
for (const dataFrame of data) {
|
||||
try {
|
||||
const values = dataFrame.fields[0].values;
|
||||
if (values.toArray) {
|
||||
problems.push(values.toArray());
|
||||
} else if (values.length > 0) {
|
||||
// On snapshot mode values is a plain Array, not ArrayVector
|
||||
problems.push(values);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
problems = _.flatten(problems);
|
||||
this.problems = problems;
|
||||
return this.renderProblems(problems);
|
||||
}
|
||||
|
||||
onDataSnapshotLoad(snapshotData) {
|
||||
return this.onDataFramesReceived(snapshotData);
|
||||
}
|
||||
|
||||
reRenderProblems() {
|
||||
if (this.problems) {
|
||||
this.renderProblems(this.problems);
|
||||
}
|
||||
}
|
||||
|
||||
setPanelError(err, defaultError = "Request Error") {
|
||||
this.inspector = { error: err };
|
||||
this.error = err.message || defaultError;
|
||||
if (err.data) {
|
||||
if (err.data.message) {
|
||||
this.error = err.data.message;
|
||||
}
|
||||
if (err.data.error) {
|
||||
this.error = err.data.error;
|
||||
}
|
||||
}
|
||||
|
||||
// this.events.emit(PanelEvents.dataError, err);
|
||||
console.log('Panel data error:', err);
|
||||
}
|
||||
|
||||
renderProblems(problems) {
|
||||
let triggers = _.cloneDeep(problems);
|
||||
|
||||
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
||||
triggers = this.filterProblems(triggers);
|
||||
triggers = this.sortTriggers(triggers);
|
||||
|
||||
this.renderData = triggers;
|
||||
|
||||
return this.$timeout(() => {
|
||||
return super.render(triggers);
|
||||
});
|
||||
}
|
||||
|
||||
filterProblems(problems) {
|
||||
let problemsList = _.cloneDeep(problems);
|
||||
|
||||
// Filter acknowledged triggers
|
||||
if (this.panel.showTriggers === 'unacknowledged') {
|
||||
problemsList = _.filter(problemsList, trigger => {
|
||||
return !(trigger.acknowledges && trigger.acknowledges.length);
|
||||
});
|
||||
} else if (this.panel.showTriggers === 'acknowledged') {
|
||||
problemsList = _.filter(problemsList, trigger => {
|
||||
return trigger.acknowledges && trigger.acknowledges.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter triggers by severity
|
||||
problemsList = _.filter(problemsList, problem => {
|
||||
if (problem.severity) {
|
||||
return this.panel.triggerSeverity[problem.severity].show;
|
||||
} else {
|
||||
return this.panel.triggerSeverity[problem.priority].show;
|
||||
}
|
||||
});
|
||||
|
||||
return problemsList;
|
||||
}
|
||||
|
||||
sortTriggers(triggerList) {
|
||||
if (this.panel.sortProblems === 'priority') {
|
||||
triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||
} else if (this.panel.sortProblems === 'lastchange') {
|
||||
triggerList = _.orderBy(triggerList, ['lastchangeUnix', 'priority', 'triggerid'], ['desc', 'desc', 'desc']);
|
||||
}
|
||||
return triggerList;
|
||||
}
|
||||
|
||||
formatTrigger(zabbixTrigger) {
|
||||
const trigger = _.cloneDeep(zabbixTrigger);
|
||||
|
||||
// Set host and proxy that the trigger belongs
|
||||
if (trigger.hosts && trigger.hosts.length) {
|
||||
const host = trigger.hosts[0];
|
||||
trigger.host = host.name;
|
||||
trigger.hostTechName = host.host;
|
||||
if (host.proxy) {
|
||||
trigger.proxy = host.proxy;
|
||||
}
|
||||
}
|
||||
|
||||
// Set tags if present
|
||||
if (trigger.tags && trigger.tags.length === 0) {
|
||||
trigger.tags = null;
|
||||
}
|
||||
|
||||
// Handle multi-line description
|
||||
if (trigger.comments) {
|
||||
trigger.comments = trigger.comments.replace('\n', '<br>');
|
||||
}
|
||||
|
||||
trigger.lastchangeUnix = Number(trigger.lastchange);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
parseTags(tagStr) {
|
||||
if (!tagStr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||
tags = _.map(tags, (tag) => {
|
||||
const tagParts = tag.split(':');
|
||||
return {tag: tagParts[0].trim(), value: tagParts[1].trim()};
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
tagsToString(tags) {
|
||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||
}
|
||||
|
||||
addTagFilter(tag, datasource) {
|
||||
for (const target of this.panel.targets) {
|
||||
if (target.datasource === datasource || this.panel.datasource === datasource) {
|
||||
const tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
const newTag = {tag: tag.tag, value: tag.value};
|
||||
targetTags.push(newTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
const newFilter = this.tagsToString(targetTags);
|
||||
target.tags.filter = newFilter;
|
||||
}
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
removeTagFilter(tag, datasource) {
|
||||
const matchTag = t => t.tag === tag.tag && t.value === tag.value;
|
||||
for (const target of this.panel.targets) {
|
||||
if (target.datasource === datasource || this.panel.datasource === datasource) {
|
||||
const tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
_.remove(targetTags, matchTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
const newFilter = this.tagsToString(targetTags);
|
||||
target.tags.filter = newFilter;
|
||||
}
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getProblemEvents(problem) {
|
||||
const triggerids = [problem.triggerid];
|
||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||
});
|
||||
}
|
||||
|
||||
getProblemAlerts(problem: ProblemDTO) {
|
||||
if (!problem.eventid) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const eventids = [problem.eventid];
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
return datasource.zabbix.getEventAlerts(eventids);
|
||||
});
|
||||
}
|
||||
|
||||
getAlertIconClassBySeverity(triggerSeverity) {
|
||||
let iconClass = 'icon-gf-online';
|
||||
if (triggerSeverity.priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
}
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
resetResizedColumns() {
|
||||
this.panel.resizedColumns = [];
|
||||
this.render();
|
||||
}
|
||||
|
||||
acknowledgeProblem(problem: ProblemDTO, message, action, severity) {
|
||||
const eventid = problem.eventid;
|
||||
const grafana_user = this.contextSrv.user.name;
|
||||
const ack_message = grafana_user + ' (Grafana): ' + message;
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
||||
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
||||
}
|
||||
if (eventid) {
|
||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message, action, severity);
|
||||
} else {
|
||||
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
||||
}
|
||||
})
|
||||
.then(this.refresh.bind(this))
|
||||
.catch((err) => {
|
||||
this.setPanelError(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
handlePageSizeChange(pageSize, pageIndex) {
|
||||
this.panel.pageSize = pageSize;
|
||||
this.pageIndex = pageIndex;
|
||||
this.scope.$apply(() => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
handleColumnResize(newResized) {
|
||||
this.panel.resizedColumns = newResized;
|
||||
this.scope.$apply(() => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
const panel = ctrl.panel;
|
||||
|
||||
ctrl.events.on(PanelEvents.render, (renderData) => {
|
||||
renderData = renderData || this.renderData;
|
||||
renderPanel(renderData);
|
||||
});
|
||||
|
||||
function renderPanel(problems) {
|
||||
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
||||
|
||||
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1), 10);
|
||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||
|
||||
const pageSize = panel.pageSize || 10;
|
||||
const loading = ctrl.loading && (!problems || !problems.length);
|
||||
|
||||
const panelOptions = {};
|
||||
for (const prop in PANEL_DEFAULTS) {
|
||||
panelOptions[prop] = ctrl.panel[prop];
|
||||
}
|
||||
const problemsListProps = {
|
||||
problems,
|
||||
panelOptions,
|
||||
timeRange: { timeFrom, timeTo },
|
||||
loading,
|
||||
pageSize,
|
||||
fontSize: fontSizeProp,
|
||||
panelId: ctrl.panel.id,
|
||||
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
||||
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||
onProblemAck: (trigger, data) => {
|
||||
const { message, action, severity } = data;
|
||||
return ctrl.acknowledgeProblem(trigger, message, action, severity);
|
||||
},
|
||||
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
||||
if (ctrlKey || shiftKey) {
|
||||
ctrl.removeTagFilter(tag, datasource);
|
||||
} else {
|
||||
ctrl.addTagFilter(tag, datasource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let problemsReactElem;
|
||||
if (panel.layout === 'list') {
|
||||
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
||||
} else {
|
||||
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
||||
}
|
||||
|
||||
const panelContainerElem = elem.find('.panel-content');
|
||||
if (panelContainerElem && panelContainerElem.length) {
|
||||
ReactDOM.render(problemsReactElem, panelContainerElem[0]);
|
||||
} else {
|
||||
ReactDOM.render(problemsReactElem, elem[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
||||
@@ -1,131 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import * as utils from '../datasource-zabbix/utils';
|
||||
import { getDefaultTarget } from './triggers_panel_ctrl';
|
||||
|
||||
class TriggersTabCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $rootScope, uiSegmentSrv, templateSrv) {
|
||||
$scope.editor = this;
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
this.templateSrv = templateSrv;
|
||||
this.datasources = {};
|
||||
|
||||
// Load scope defaults
|
||||
var scopeDefaults = {
|
||||
getGroupNames: {},
|
||||
getHostNames: {},
|
||||
getApplicationNames: {},
|
||||
getProxyNames: {},
|
||||
oldTarget: _.cloneDeep(this.panel.targets)
|
||||
};
|
||||
_.defaultsDeep(this, scopeDefaults);
|
||||
this.selectedDatasources = this.getSelectedDatasources();
|
||||
|
||||
this.initDatasources();
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
|
||||
initDatasources() {
|
||||
return this.panelCtrl.initDatasources()
|
||||
.then((datasources) => {
|
||||
_.each(datasources, (datasource) => {
|
||||
this.datasources[datasource.name] = datasource;
|
||||
this.bindSuggestionFunctions(datasource);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bindSuggestionFunctions(datasource) {
|
||||
// Map functions for bs-typeahead
|
||||
let ds = datasource.name;
|
||||
this.getGroupNames[ds] = _.bind(this.suggestGroups, this, datasource);
|
||||
this.getHostNames[ds] = _.bind(this.suggestHosts, this, datasource);
|
||||
this.getApplicationNames[ds] = _.bind(this.suggestApps, this, datasource);
|
||||
this.getProxyNames[ds] = _.bind(this.suggestProxies, this, datasource);
|
||||
}
|
||||
|
||||
getSelectedDatasources() {
|
||||
return _.compact(this.panel.targets.map(target => target.datasource));
|
||||
}
|
||||
|
||||
suggestGroups(datasource, query, callback) {
|
||||
return datasource.zabbix.getAllGroups()
|
||||
.then(groups => {
|
||||
return _.map(groups, 'name');
|
||||
})
|
||||
.then(callback);
|
||||
}
|
||||
|
||||
suggestHosts(datasource, query, callback) {
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource.name);
|
||||
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
|
||||
return datasource.zabbix.getAllHosts(groupFilter)
|
||||
.then(hosts => {
|
||||
return _.map(hosts, 'name');
|
||||
})
|
||||
.then(callback);
|
||||
}
|
||||
|
||||
suggestApps(datasource, query, callback) {
|
||||
const target = this.panel.targets.find(t => t.datasource === datasource.name);
|
||||
let groupFilter = datasource.replaceTemplateVars(target.group.filter);
|
||||
let hostFilter = datasource.replaceTemplateVars(target.host.filter);
|
||||
return datasource.zabbix.getAllApps(groupFilter, hostFilter)
|
||||
.then(apps => {
|
||||
return _.map(apps, 'name');
|
||||
})
|
||||
.then(callback);
|
||||
}
|
||||
|
||||
suggestProxies(datasource, query, callback) {
|
||||
return datasource.zabbix.getProxies()
|
||||
.then(proxies => _.map(proxies, 'host'))
|
||||
.then(callback);
|
||||
}
|
||||
|
||||
datasourcesChanged() {
|
||||
const newTargets = [];
|
||||
_.each(this.selectedDatasources, (ds) => {
|
||||
const dsTarget = this.panel.targets.find((target => target.datasource === ds));
|
||||
if (dsTarget) {
|
||||
newTargets.push(dsTarget);
|
||||
} else {
|
||||
const newTarget = getDefaultTarget(this.panel.targets);
|
||||
newTarget.datasource = ds;
|
||||
newTargets.push(newTarget);
|
||||
}
|
||||
this.panel.targets = newTargets;
|
||||
});
|
||||
this.parseTarget();
|
||||
}
|
||||
|
||||
parseTarget() {
|
||||
this.initDatasources()
|
||||
.then(() => {
|
||||
var newTarget = _.cloneDeep(this.panel.targets);
|
||||
if (!_.isEqual(this.oldTarget, newTarget)) {
|
||||
this.oldTarget = newTarget;
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isRegex(str) {
|
||||
return utils.isRegex(str);
|
||||
}
|
||||
|
||||
isVariable(str) {
|
||||
return utils.isTemplateVariable(str, this.templateSrv.variables);
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerPanelTriggersTab() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/triggers_tab.html',
|
||||
controller: TriggersTabCtrl,
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export interface ProblemsPanelOptions {
|
||||
statusField?: boolean;
|
||||
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[];
|
||||
}
|
||||
|
||||
@@ -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('&');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user