Problems: improve ack dialog (add ack options), closes #942
This commit is contained in:
236
src/panel-triggers/components/AckModal.tsx
Normal file
236
src/panel-triggers/components/AckModal.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
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, Input, VerticalGroup, Spinner } from '@grafana/ui';
|
||||
import { FAIcon } from './FAIcon';
|
||||
|
||||
import * as grafanaUi from '@grafana/ui';
|
||||
const Checkbox: any = grafanaUi.Forms?.Checkbox || (grafanaUi as any).Checkbox;
|
||||
const RadioButtonGroup: any = grafanaUi.Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup;
|
||||
|
||||
const KEYBOARD_ENTER_KEY = 13;
|
||||
const KEYBOARD_ESCAPE_KEY = 27;
|
||||
|
||||
interface Props {
|
||||
canAck?: boolean;
|
||||
canClose?: boolean;
|
||||
isOpen?: boolean;
|
||||
withBackdrop?: boolean;
|
||||
onSubmit: (data?: AckProblemData) => Promise<any> | any;
|
||||
onClose?: () => 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 AckModal extends PureComponent<Props, State> {
|
||||
modalContainer: HTMLElement;
|
||||
|
||||
static defaultProps: Partial<Props> = {
|
||||
withBackdrop: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: '',
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
ackError: '',
|
||||
acknowledge: false,
|
||||
closeProblem: false,
|
||||
changeSeverity: false,
|
||||
selectedSeverity: 0,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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.onClose();
|
||||
}
|
||||
|
||||
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((response) => {
|
||||
this.dismiss();
|
||||
}).catch(err => {
|
||||
this.setState({
|
||||
ackError: err.message || err.data,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { canClose, canAck } = this.props;
|
||||
if (!this.props.isOpen || !this.modalContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputClass = classNames({ '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" style={{ display: 'flex' }}>
|
||||
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="reply-all" />}
|
||||
<span className="p-l-1">Acknowledge Problem</span>
|
||||
</h2>
|
||||
|
||||
<a className="modal-header-close" onClick={this.dismiss}>
|
||||
<FAIcon icon="remove" />
|
||||
</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.errorMessage}</small>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="gf-form">
|
||||
<VerticalGroup>
|
||||
<Checkbox label="Acknowledge" value={this.state.acknowledge} onChange={this.onAcknowledgeToggle} />
|
||||
<Checkbox label="Change severity" description="" value={this.state.changeSeverity} onChange={this.onChangeSeverityToggle} />
|
||||
{this.state.changeSeverity &&
|
||||
<RadioButtonGroup
|
||||
size="sm"
|
||||
options={severityOptions}
|
||||
value={this.state.selectedSeverity}
|
||||
onChange={this.onChangeSelectedSeverity}
|
||||
/>
|
||||
}
|
||||
{canClose &&
|
||||
<Checkbox label="Close problem" disabled={!canClose} value={this.state.closeProblem} onChange={this.onCloseProblemToggle} />
|
||||
}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
|
||||
{this.state.ackError &&
|
||||
<div className="gf-form ack-request-error">
|
||||
<span className="ack-error-message">{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>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { isNewProblem, formatLastChange } from '../../utils';
|
||||
import { ProblemsPanelOptions, TriggerSeverity } from '../../types';
|
||||
import { AckProblemData, Modal } from '.././Modal';
|
||||
import { AckProblemData, AckModal } from '../AckModal';
|
||||
import EventTag from '../EventTag';
|
||||
import Tooltip from '.././Tooltip/Tooltip';
|
||||
import AlertAcknowledges from './AlertAcknowledges';
|
||||
@@ -165,7 +165,7 @@ export default class AlertCard extends PureComponent<AlertCardProps, AlertCardSt
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Modal withBackdrop={true}
|
||||
<AckModal
|
||||
isOpen={this.state.showAckDialog}
|
||||
onSubmit={this.ackProblem}
|
||||
onClose={this.closeAckDialog} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ProblemsPanelOptions, GFTimeRange } from '../../types';
|
||||
import { AckProblemData } from '.././Modal';
|
||||
import { AckProblemData } from '../AckModal';
|
||||
import AlertCard from './AlertCard';
|
||||
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import * as utils from '../../../datasource-zabbix/utils';
|
||||
import { MODE_ITEMID, MODE_METRICS } from '../../../datasource-zabbix/constants';
|
||||
import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
|
||||
import { ZBXItem, ZBXAcknowledge, GFTimeRange, RTRow } from '../../types';
|
||||
import { Modal, AckProblemData } from '../Modal';
|
||||
import { AckModal, AckProblemData } from '../AckModal';
|
||||
import EventTag from '../EventTag';
|
||||
import Tooltip from '../Tooltip/Tooltip';
|
||||
import ProblemStatusBar from './ProblemStatusBar';
|
||||
@@ -79,9 +79,6 @@ export default class ProblemDetails extends PureComponent<ProblemDetailsProps, P
|
||||
const problem = this.props.original as ProblemDTO;
|
||||
return this.props.onProblemAck(problem, data).then(result => {
|
||||
this.closeAckDialog();
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
this.closeAckDialog();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,7 +208,8 @@ 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}
|
||||
<AckModal
|
||||
canClose={problem.manual_close === '1'}
|
||||
isOpen={this.state.showAckDialog}
|
||||
onSubmit={this.ackProblem}
|
||||
onClose={this.closeAckDialog} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as utils from '../../../datasource-zabbix/utils';
|
||||
import { isNewProblem } from '../../utils';
|
||||
import EventTag from '../EventTag';
|
||||
import ProblemDetails from './ProblemDetails';
|
||||
import { AckProblemData } from '../Modal';
|
||||
import { AckProblemData } from '../AckModal';
|
||||
import GFHeartIcon from '../GFHeartIcon';
|
||||
import { ProblemsPanelOptions, GFTimeRange, RTCell, TriggerSeverity, RTResized } from '../../types';
|
||||
import { ProblemDTO, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
|
||||
|
||||
@@ -329,7 +329,7 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
acknowledgeProblem(problem: ProblemDTO, message) {
|
||||
acknowledgeProblem(problem: ProblemDTO, message, action, severity) {
|
||||
const eventid = problem.eventid;
|
||||
const grafana_user = this.contextSrv.user.name;
|
||||
const ack_message = grafana_user + ' (Grafana): ' + message;
|
||||
@@ -340,7 +340,7 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||
return Promise.reject({message: 'You have no permissions to acknowledge events.'});
|
||||
}
|
||||
if (eventid) {
|
||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message);
|
||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message, action, severity);
|
||||
} else {
|
||||
return Promise.reject({message: 'Trigger has no events. Nothing to acknowledge.'});
|
||||
}
|
||||
@@ -348,6 +348,7 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||
.then(this.refresh.bind(this))
|
||||
.catch((err) => {
|
||||
this.setPanelError(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,8 +402,8 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||
onProblemAck: (trigger, data) => {
|
||||
const message = data.message;
|
||||
return ctrl.acknowledgeProblem(trigger, message);
|
||||
const { message, action, severity } = data;
|
||||
return ctrl.acknowledgeProblem(trigger, message, action, severity);
|
||||
},
|
||||
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
||||
if (ctrlKey || shiftKey) {
|
||||
|
||||
Reference in New Issue
Block a user