problems: refactor
This commit is contained in:
23
src/panel-triggers/components/Problems/AcknowledgesList.tsx
Normal file
23
src/panel-triggers/components/Problems/AcknowledgesList.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { ZBXAcknowledge } from 'panel-triggers/types';
|
||||
|
||||
interface AcknowledgesListProps {
|
||||
acknowledges: ZBXAcknowledge[];
|
||||
}
|
||||
|
||||
export default function AcknowledgesList(props: AcknowledgesListProps) {
|
||||
const { acknowledges } = props;
|
||||
return (
|
||||
<div className="problem-ack-list">
|
||||
<div className="problem-ack-col problem-ack-time">
|
||||
{acknowledges.map(ack => <span key={ack.acknowledgeid} className="problem-ack-time">{ack.time}</span>)}
|
||||
</div>
|
||||
<div className="problem-ack-col problem-ack-user">
|
||||
{acknowledges.map(ack => <span key={ack.acknowledgeid} className="problem-ack-user">{ack.user}</span>)}
|
||||
</div>
|
||||
<div className="problem-ack-col problem-ack-message">
|
||||
{acknowledges.map(ack => <span key={ack.acknowledgeid} className="problem-ack-message">{ack.message}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
257
src/panel-triggers/components/Problems/ProblemDetails.tsx
Normal file
257
src/panel-triggers/components/Problems/ProblemDetails.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
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 } from '../../types';
|
||||
import { Modal, AckProblemData } from '../Modal';
|
||||
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';
|
||||
|
||||
interface ProblemDetailsProps extends RTRow<ZBXTrigger> {
|
||||
rootWidth: number;
|
||||
timeRange: GFTimeRange;
|
||||
getProblemEvents: (problem: ZBXTrigger) => Promise<ZBXEvent[]>;
|
||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise<any> | any;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string) => void;
|
||||
}
|
||||
|
||||
interface ProblemDetailsState {
|
||||
events: ZBXEvent[];
|
||||
show: boolean;
|
||||
showAckDialog: boolean;
|
||||
}
|
||||
|
||||
export default class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDetailsState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
events: [],
|
||||
show: false,
|
||||
showAckDialog: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchProblemEvents();
|
||||
requestAnimationFrame(() => {
|
||||
this.setState({ show: true });
|
||||
});
|
||||
}
|
||||
|
||||
handleTagClick = (tag: ZBXTag) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, this.props.original.datasource);
|
||||
}
|
||||
}
|
||||
|
||||
fetchProblemEvents() {
|
||||
const problem = this.props.original;
|
||||
this.props.getProblemEvents(problem)
|
||||
.then(events => {
|
||||
console.log(events, this.props.timeRange);
|
||||
this.setState({ events });
|
||||
});
|
||||
}
|
||||
|
||||
ackProblem = (data: AckProblemData) => {
|
||||
const problem = this.props.original as ZBXTrigger;
|
||||
console.log('acknowledge: ', problem.lastEvent && problem.lastEvent.eventid, data);
|
||||
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 });
|
||||
}
|
||||
|
||||
render() {
|
||||
const problem = this.props.original as ZBXTrigger;
|
||||
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);
|
||||
|
||||
return (
|
||||
<div className={`problem-details-container ${displayClass}`}>
|
||||
<div className="problem-details">
|
||||
<div className="problem-details-row">
|
||||
<div className="problem-value-container">
|
||||
<div className="problem-age">
|
||||
<FAIcon icon="clock-o" />
|
||||
<span>{age}</span>
|
||||
</div>
|
||||
{problem.items && <ProblemItems items={problem.items} />}
|
||||
</div>
|
||||
<ProblemStatusBar problem={problem} className={compactStatusBar && 'compact'} />
|
||||
<div className="problem-actions">
|
||||
<ProblemActionButton className="navbar-button navbar-button--settings"
|
||||
icon="reply-all"
|
||||
tooltip="Acknowledge problem"
|
||||
onClick={this.showAckDialog} />
|
||||
</div>
|
||||
</div>
|
||||
{problem.comments &&
|
||||
<div className="problem-description">
|
||||
<span className="description-label">Description: </span>
|
||||
<span>{problem.comments}</span>
|
||||
</div>
|
||||
}
|
||||
{problem.tags && problem.tags.length > 0 &&
|
||||
<div className="problem-tags">
|
||||
{problem.tags && problem.tags.map(tag =>
|
||||
<EventTag
|
||||
key={tag.tag + tag.value}
|
||||
tag={tag}
|
||||
highlight={tag.tag === problem.correlation_tag}
|
||||
onClick={this.handleTagClick}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{this.state.events.length > 0 &&
|
||||
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange} />
|
||||
}
|
||||
{problem.acknowledges && !wideLayout &&
|
||||
<div className="problem-ack-container">
|
||||
<h6><FAIcon icon="reply-all" /> Acknowledges</h6>
|
||||
<AcknowledgesList acknowledges={problem.acknowledges} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{problem.acknowledges && wideLayout &&
|
||||
<div className="problem-details-middle">
|
||||
<div className="problem-ack-container">
|
||||
<h6><FAIcon icon="reply-all" /> Acknowledges</h6>
|
||||
<AcknowledgesList acknowledges={problem.acknowledges} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="problem-details-right">
|
||||
<div className="problem-details-right-item">
|
||||
<FAIcon icon="database" />
|
||||
<span>{problem.datasource}</span>
|
||||
</div>
|
||||
{problem.proxy &&
|
||||
<div className="problem-details-right-item">
|
||||
<FAIcon icon="cloud" />
|
||||
<span>{problem.proxy}</span>
|
||||
</div>
|
||||
}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ProblemItemProps {
|
||||
item: ZBXItem;
|
||||
showName?: boolean;
|
||||
}
|
||||
|
||||
function ProblemItem(props: ProblemItemProps) {
|
||||
const { item, showName } = props;
|
||||
const itemName = utils.expandItemName(item.name, item.key_);
|
||||
return (
|
||||
<Tooltip placement="bottom" content={itemName}>
|
||||
<div className="problem-item">
|
||||
<FAIcon icon="thermometer-three-quarters" />
|
||||
{showName && <span className="problem-item-name">{item.name}: </span>}
|
||||
<span className="problem-item-value">{item.lastvalue}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProblemItemsProps {
|
||||
items: ZBXItem[];
|
||||
}
|
||||
|
||||
class ProblemItems extends PureComponent<ProblemItemsProps> {
|
||||
render() {
|
||||
const { items } = this.props;
|
||||
return (items.length > 1 ?
|
||||
items.map(item => <ProblemItem item={item} key={item.itemid} showName={true} />) :
|
||||
<ProblemItem item={items[0]} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ProblemGroupsProps {
|
||||
groups: ZBXGroup[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class ProblemGroups extends PureComponent<ProblemGroupsProps> {
|
||||
render() {
|
||||
return this.props.groups.map(g => (
|
||||
<div className={this.props.className || ''} key={g.groupid}>
|
||||
<FAIcon icon="folder" />
|
||||
<span>{g.name}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
interface ProblemHostsProps {
|
||||
hosts: ZBXHost[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class ProblemHosts extends PureComponent<ProblemHostsProps> {
|
||||
render() {
|
||||
return this.props.hosts.map(h => (
|
||||
<div className={this.props.className || ''} key={h.hostid}>
|
||||
<FAIcon icon="server" />
|
||||
<span>{h.name}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
59
src/panel-triggers/components/Problems/ProblemStatusBar.tsx
Normal file
59
src/panel-triggers/components/Problems/ProblemStatusBar.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import FAIcon from '../FAIcon';
|
||||
import Tooltip from '../Tooltip/Tooltip';
|
||||
import { ZBXTrigger } from 'panel-triggers/types';
|
||||
|
||||
export interface ProblemStatusBarProps {
|
||||
problem: ZBXTrigger;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProblemStatusBar(props: ProblemStatusBarProps) {
|
||||
const { problem, className } = props;
|
||||
const multiEvent = problem.type === '1';
|
||||
const link = problem.url && problem.url !== '';
|
||||
const maintenance = problem.maintenance;
|
||||
const manualClose = problem.manual_close === '1';
|
||||
const error = problem.error && problem.error !== '';
|
||||
const stateUnknown = problem.state === '1';
|
||||
const closeByTag = problem.correlation_mode === '1';
|
||||
const actions = problem.alerts && problem.alerts.length !== 0;
|
||||
const actionMessage = problem.alerts ? problem.alerts[0].message : '';
|
||||
|
||||
return (
|
||||
<div className={`problem-statusbar ${className || ''}`}>
|
||||
<ProblemStatusBarItem icon="wrench" fired={maintenance} tooltip="Host maintenance" />
|
||||
<ProblemStatusBarItem icon="globe" fired={link} link={link && problem.url} tooltip="External link" />
|
||||
<ProblemStatusBarItem icon="bullhorn" fired={multiEvent} tooltip="Trigger generates multiple problem events" />
|
||||
<ProblemStatusBarItem icon="tag" fired={closeByTag} tooltip={`OK event closes problems matched to tag: ${problem.correlation_tag}`} />
|
||||
<ProblemStatusBarItem icon="circle-o-notch" fired={actions} tooltip={actionMessage} />
|
||||
<ProblemStatusBarItem icon="question-circle" fired={stateUnknown} tooltip="Current trigger state is unknown" />
|
||||
<ProblemStatusBarItem icon="warning" fired={error} tooltip={problem.error} />
|
||||
<ProblemStatusBarItem icon="window-close-o" fired={manualClose} tooltip="Manual close problem" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProblemStatusBarItemProps {
|
||||
icon: string;
|
||||
fired?: boolean;
|
||||
link?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
function ProblemStatusBarItem(props: ProblemStatusBarItemProps) {
|
||||
const { fired, icon, link, tooltip } = props;
|
||||
let item = (
|
||||
<div className={`problem-statusbar-item ${fired ? 'fired' : 'muted'}`}>
|
||||
<FAIcon icon={icon} />
|
||||
</div>
|
||||
);
|
||||
if (tooltip && fired) {
|
||||
item = (
|
||||
<Tooltip placement="bottom" content={tooltip}>
|
||||
{item}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return link ? <a href={link} target="_blank">{item}</a> : item;
|
||||
}
|
||||
692
src/panel-triggers/components/Problems/ProblemTimeline.tsx
Normal file
692
src/panel-triggers/components/Problems/ProblemTimeline.tsx
Normal file
@@ -0,0 +1,692 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { GFTimeRange, ZBXEvent, ZBXAcknowledge } from 'panel-triggers/types';
|
||||
|
||||
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
|
||||
const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)';
|
||||
const EVENT_POINT_SIZE = 16;
|
||||
const INNER_POINT_SIZE = 0.6;
|
||||
const HIGHLIGHTED_POINT_SIZE = 1.1;
|
||||
const EVENT_REGION_HEIGHT = Math.round(EVENT_POINT_SIZE * 0.6);
|
||||
|
||||
export interface ProblemTimelineProps {
|
||||
events: ZBXEvent[];
|
||||
timeRange: GFTimeRange;
|
||||
okColor?: string;
|
||||
problemColor?: string;
|
||||
eventRegionHeight?: number;
|
||||
eventPointSize?: number;
|
||||
}
|
||||
|
||||
interface ProblemTimelineState {
|
||||
width: number;
|
||||
highlightedEvent?: ZBXEvent | null;
|
||||
highlightedRegion?: number | null;
|
||||
showEventInfo?: boolean;
|
||||
eventInfo?: EventInfo;
|
||||
}
|
||||
|
||||
interface EventInfo {
|
||||
timestamp?: number;
|
||||
duration?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export default class ProblemTimeline extends PureComponent<ProblemTimelineProps, ProblemTimelineState> {
|
||||
rootRef: any;
|
||||
sortedEvents: ZBXEvent[];
|
||||
|
||||
static defaultProps = {
|
||||
okColor: DEFAULT_OK_COLOR,
|
||||
problemColor: DEFAULT_PROBLEM_COLOR,
|
||||
eventRegionHeight: EVENT_REGION_HEIGHT,
|
||||
eventPointSize: EVENT_POINT_SIZE,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
width: 0,
|
||||
highlightedEvent: null,
|
||||
highlightedRegion: null,
|
||||
showEventInfo: false,
|
||||
eventInfo: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.rootRef && prevState.width !== this.rootRef.clientWidth) {
|
||||
const width = this.rootRef.clientWidth;
|
||||
this.setState({ width });
|
||||
}
|
||||
}
|
||||
|
||||
setRootRef = ref => {
|
||||
this.rootRef = ref;
|
||||
const width = ref && ref.clientWidth || 0;
|
||||
this.setState({ width });
|
||||
}
|
||||
|
||||
handlePointHighlight = (index: number, secondIndex?: number) => {
|
||||
const event: ZBXEvent = this.sortedEvents[index];
|
||||
const regionToHighlight = this.getRegionToHighlight(index);
|
||||
let duration: number;
|
||||
if (secondIndex !== undefined) {
|
||||
duration = this.getEventDuration(index, secondIndex);
|
||||
}
|
||||
this.setState({
|
||||
highlightedEvent: event,
|
||||
showEventInfo: true,
|
||||
highlightedRegion: regionToHighlight,
|
||||
eventInfo: {
|
||||
duration
|
||||
}
|
||||
});
|
||||
// this.showEventInfo(event);
|
||||
}
|
||||
|
||||
handlePointUnHighlight = () => {
|
||||
this.setState({ showEventInfo: false, highlightedRegion: null });
|
||||
}
|
||||
|
||||
handleAckHighlight = (ack: ZBXAcknowledge, index: number) => {
|
||||
this.setState({
|
||||
showEventInfo: true,
|
||||
eventInfo: {
|
||||
timestamp: Number(ack.clock),
|
||||
message: ack.message,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleAckUnHighlight = () => {
|
||||
this.setState({ showEventInfo: false });
|
||||
}
|
||||
|
||||
showEventInfo = (event: ZBXEvent) => {
|
||||
this.setState({ highlightedEvent: event, showEventInfo: true });
|
||||
}
|
||||
|
||||
hideEventInfo = () => {
|
||||
this.setState({ showEventInfo: false });
|
||||
}
|
||||
|
||||
getRegionToHighlight = (index: number): number => {
|
||||
const event = this.sortedEvents[index];
|
||||
const regionToHighlight = event.value === '1' ? index + 1 : index;
|
||||
return regionToHighlight;
|
||||
}
|
||||
|
||||
getEventDuration(firstIndex: number, secondIndex: number): number {
|
||||
return Math.abs(Number(this.sortedEvents[firstIndex].clock) - Number(this.sortedEvents[secondIndex].clock)) * 1000;
|
||||
}
|
||||
|
||||
sortEvents() {
|
||||
const events = _.sortBy(this.props.events, e => Number(e.clock));
|
||||
this.sortedEvents = events;
|
||||
return events;
|
||||
}
|
||||
|
||||
getEventAcks(events: ZBXEvent[]): ZBXAcknowledge[] {
|
||||
const acks: ZBXAcknowledge[] = [];
|
||||
for (const event of events) {
|
||||
if (event.acknowledges) {
|
||||
for (const ack of event.acknowledges) {
|
||||
acks.push(ack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.sortBy(acks, ack => Number(ack.clock));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.rootRef) {
|
||||
return <div className="event-timeline" ref={this.setRootRef} />;
|
||||
}
|
||||
|
||||
const { timeRange, eventPointSize, eventRegionHeight } = this.props;
|
||||
const events = this.sortEvents();
|
||||
const acknowledges = this.getEventAcks(events);
|
||||
const boxWidth = this.state.width + eventPointSize;
|
||||
const boxHeight = Math.round(eventPointSize * 2.5);
|
||||
const width = boxWidth - eventPointSize;
|
||||
const padding = Math.round(eventPointSize / 2);
|
||||
const pointsYpos = eventRegionHeight / 2;
|
||||
const timelineYpos = Math.round(boxHeight / 2 - eventPointSize / 2);
|
||||
|
||||
return (
|
||||
<div className="event-timeline" ref={this.setRootRef} style={{ transform: `translate(${-padding}px, 0)`}}>
|
||||
<TimelineInfoContainer className="timeline-info-container"
|
||||
event={this.state.highlightedEvent}
|
||||
eventInfo={this.state.eventInfo}
|
||||
show={this.state.showEventInfo}
|
||||
left={padding}
|
||||
/>
|
||||
<svg className="event-timeline-canvas" viewBox={`0 0 ${boxWidth} ${boxHeight}`}>
|
||||
<defs>
|
||||
<TimelineSVGFilters />
|
||||
</defs>
|
||||
<g className="event-timeline-group" transform={`translate(${padding}, ${timelineYpos})`}>
|
||||
<g className="event-timeline-regions" filter="url(#boxShadow)">
|
||||
<TimelineRegions
|
||||
events={events}
|
||||
timeRange={timeRange}
|
||||
width={width}
|
||||
height={eventRegionHeight}
|
||||
highlightedRegion={this.state.highlightedRegion}
|
||||
/>
|
||||
</g>
|
||||
<g className="timeline-acknowledges" transform={`translate(0, ${pointsYpos})`}>
|
||||
<TimelineAcks
|
||||
acknowledges={acknowledges}
|
||||
timeRange={timeRange}
|
||||
width={width}
|
||||
size={eventPointSize}
|
||||
onHighlight={this.handleAckHighlight}
|
||||
onUnHighlight={this.handleAckUnHighlight}
|
||||
/>
|
||||
</g>
|
||||
<g className="timeline-points" transform={`translate(0, ${pointsYpos})`}>
|
||||
<TimelinePoints
|
||||
events={events}
|
||||
timeRange={timeRange}
|
||||
width={width}
|
||||
pointSize={eventPointSize}
|
||||
onPointHighlight={this.handlePointHighlight}
|
||||
onPointUnHighlight={this.handlePointUnHighlight}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TimelineSVGFilters() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<filter id="dropShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="2" />
|
||||
<feOffset dx="1" dy="1" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="boxShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="1" />
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.7" />
|
||||
</feComponentTransfer>
|
||||
<feOffset dx="1" dy="1" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glowShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="timelinePointBlur" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blurOut" />
|
||||
</filter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimelineInfoContainerProps {
|
||||
event?: ZBXEvent | null;
|
||||
eventInfo?: EventInfo | null;
|
||||
show?: boolean;
|
||||
left?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
|
||||
static defaultProps = {
|
||||
left: 0,
|
||||
className: '',
|
||||
show: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { event, eventInfo, show, className, left } = this.props;
|
||||
let infoItems, durationItem;
|
||||
if (event) {
|
||||
console.log(event);
|
||||
const ts = moment(Number(event.clock) * 1000);
|
||||
const tsFormatted = ts.format('HH:mm:ss');
|
||||
infoItems = [
|
||||
<span key="ts" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Time: </span>
|
||||
<span className="event-timestamp-value">{tsFormatted}</span>
|
||||
</span>
|
||||
];
|
||||
}
|
||||
if (eventInfo && eventInfo.duration) {
|
||||
const duration = eventInfo.duration;
|
||||
const durationFormatted = moment.utc(duration).format('HH:mm:ss');
|
||||
durationItem = (
|
||||
<span key="duration" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Duration: </span>
|
||||
<span className="event-timestamp-value">{durationFormatted}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (eventInfo && eventInfo.timestamp) {
|
||||
const ts = moment(eventInfo.timestamp * 1000);
|
||||
const tsFormatted = ts.format('HH:mm:ss');
|
||||
infoItems = [
|
||||
<span key="ts" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Time: </span>
|
||||
<span className="event-timestamp-value">{tsFormatted}</span>
|
||||
</span>
|
||||
];
|
||||
}
|
||||
|
||||
const containerStyle: React.CSSProperties = { left };
|
||||
if (!show) {
|
||||
containerStyle.opacity = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} style={containerStyle}>
|
||||
<div>
|
||||
{infoItems}
|
||||
</div>
|
||||
<div>
|
||||
{durationItem}
|
||||
</div>
|
||||
{eventInfo && eventInfo.message &&
|
||||
<div>
|
||||
<span key="duration" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Message: </span>
|
||||
<span className="event-timestamp-value">{eventInfo.message}</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface TimelineRegionsProps {
|
||||
events: ZBXEvent[];
|
||||
timeRange: GFTimeRange;
|
||||
width: number;
|
||||
height: number;
|
||||
okColor?: string;
|
||||
problemColor?: string;
|
||||
highlightedRegion?: number | null;
|
||||
}
|
||||
|
||||
class TimelineRegions extends PureComponent<TimelineRegionsProps> {
|
||||
static defaultProps = {
|
||||
highlightedRegion: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { events, timeRange, width, height, highlightedRegion } = this.props;
|
||||
const { timeFrom, timeTo } = timeRange;
|
||||
const range = timeTo - timeFrom;
|
||||
|
||||
let firstItem: React.ReactNode;
|
||||
if (events.length) {
|
||||
const firstTs = events.length ? Number(events[0].clock) : timeTo;
|
||||
const duration = (firstTs - timeFrom) / range;
|
||||
const regionWidth = Math.round(duration * width);
|
||||
const highlighted = highlightedRegion === 0;
|
||||
const valueClass = `problem-event--${events[0].value !== '1' ? 'problem' : 'ok'}`;
|
||||
const className = `problem-event-region ${valueClass} ${highlighted ? 'highlighted' : ''}`;
|
||||
const firstEventAttributes = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: regionWidth,
|
||||
height: height,
|
||||
};
|
||||
firstItem = (
|
||||
<rect key='0' className={className} {...firstEventAttributes}></rect>
|
||||
);
|
||||
}
|
||||
|
||||
const eventsIntervalItems = events.map((event, index) => {
|
||||
const ts = Number(event.clock);
|
||||
const nextTs = index < events.length - 1 ? Number(events[index + 1].clock) : timeTo;
|
||||
const duration = (nextTs - ts) / range;
|
||||
const regionWidth = Math.round(duration * width);
|
||||
const posLeft = Math.round((ts - timeFrom) / range * width);
|
||||
const highlighted = highlightedRegion && highlightedRegion - 1 === index;
|
||||
const valueClass = `problem-event--${event.value === '1' ? 'problem' : 'ok'}`;
|
||||
const className = `problem-event-region ${valueClass} ${highlighted ? 'highlighted' : ''}`;
|
||||
const attributes = {
|
||||
x: posLeft,
|
||||
y: 0,
|
||||
width: regionWidth,
|
||||
height: height,
|
||||
};
|
||||
|
||||
return (
|
||||
<rect key={event.eventid} className={className} {...attributes} />
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
firstItem,
|
||||
eventsIntervalItems
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
interface TimelinePointsProps {
|
||||
events: ZBXEvent[];
|
||||
timeRange: GFTimeRange;
|
||||
width: number;
|
||||
pointSize: number;
|
||||
okColor?: string;
|
||||
problemColor?: string;
|
||||
highlightRegion?: boolean;
|
||||
onPointHighlight?: (index: number, secondIndex?: number) => void;
|
||||
onPointUnHighlight?: () => void;
|
||||
}
|
||||
|
||||
interface TimelinePointsState {
|
||||
order: number[];
|
||||
highlighted: number[];
|
||||
}
|
||||
|
||||
class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsState> {
|
||||
static defaultProps = {
|
||||
highlightRegion: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { order: [], highlighted: [] };
|
||||
}
|
||||
|
||||
bringToFront = (indexes: number[], highlight = false) => {
|
||||
const { events } = this.props;
|
||||
let order = events.map((v, i) => i);
|
||||
order = moveToEnd(order, indexes);
|
||||
const highlighted = highlight ? indexes : null;
|
||||
this.setState({ order, highlighted });
|
||||
}
|
||||
|
||||
highlightPoint = (index: number) => () => {
|
||||
let pointsToHighlight = [index];
|
||||
if (this.props.onPointHighlight) {
|
||||
if (this.props.highlightRegion) {
|
||||
pointsToHighlight = this.getRegionEvents(index);
|
||||
const secondIndex = pointsToHighlight.length === 2 ? pointsToHighlight[1] : undefined;
|
||||
this.props.onPointHighlight(index, secondIndex);
|
||||
} else {
|
||||
this.props.onPointHighlight(index);
|
||||
}
|
||||
}
|
||||
this.bringToFront(pointsToHighlight, true);
|
||||
}
|
||||
|
||||
getRegionEvents(index: number) {
|
||||
const events = this.props.events;
|
||||
const event = events[index];
|
||||
if (event.value === '1' && index < events.length ) {
|
||||
// Problem event
|
||||
for (let i = index; i < events.length; i++) {
|
||||
if (events[i].value === '0') {
|
||||
const okEventIndex = i;
|
||||
return [index, okEventIndex];
|
||||
}
|
||||
}
|
||||
} else if (event.value === '0' && index > 0) {
|
||||
// OK event
|
||||
let lastProblemIndex = null;
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (events[i].value === '1') {
|
||||
lastProblemIndex = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastProblemIndex !== null) {
|
||||
return [index, lastProblemIndex];
|
||||
}
|
||||
}
|
||||
return [index];
|
||||
}
|
||||
|
||||
unHighlightPoint = index => () => {
|
||||
if (this.props.onPointUnHighlight) {
|
||||
this.props.onPointUnHighlight();
|
||||
}
|
||||
const order = this.props.events.map((v, i) => i);
|
||||
this.setState({ order, highlighted: [] });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { events, timeRange, width, pointSize } = this.props;
|
||||
const { timeFrom, timeTo } = timeRange;
|
||||
const range = timeTo - timeFrom;
|
||||
const pointR = pointSize / 2;
|
||||
const eventsItems = events.map((event, i) => {
|
||||
const ts = Number(event.clock);
|
||||
const posLeft = Math.round((ts - timeFrom) / range * width - pointR);
|
||||
const className = `problem-event-item problem-event--${event.value === '1' ? 'problem' : 'ok'}`;
|
||||
const highlighted = this.state.highlighted.indexOf(i) !== -1;
|
||||
|
||||
return (
|
||||
<TimelinePoint
|
||||
key={event.eventid}
|
||||
className={className}
|
||||
x={posLeft}
|
||||
r={pointR}
|
||||
highlighted={highlighted}
|
||||
onPointHighlight={this.highlightPoint(i)}
|
||||
onPointUnHighlight={this.unHighlightPoint(i)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
if (this.state.order.length) {
|
||||
return this.state.order.map(i => eventsItems[i]);
|
||||
}
|
||||
return eventsItems;
|
||||
}
|
||||
}
|
||||
|
||||
interface TimelinePointProps {
|
||||
x: number;
|
||||
r: number;
|
||||
color?: string;
|
||||
highlighted?: boolean;
|
||||
className?: string;
|
||||
onPointHighlight?: () => void;
|
||||
onPointUnHighlight?: () => void;
|
||||
}
|
||||
|
||||
interface TimelinePointState {
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { highlighted: false };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TimelinePointProps) {
|
||||
// Update component after reordering to make animation working
|
||||
if (prevProps.highlighted !== this.props.highlighted) {
|
||||
this.setState({ highlighted: this.props.highlighted });
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
if (this.props.onPointHighlight) {
|
||||
this.props.onPointHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
if (this.props.onPointUnHighlight) {
|
||||
this.props.onPointUnHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { x } = this.props;
|
||||
const r = this.state.highlighted ? Math.round(this.props.r * HIGHLIGHTED_POINT_SIZE) : this.props.r;
|
||||
const cx = x + this.props.r;
|
||||
const rInner = Math.round(r * INNER_POINT_SIZE);
|
||||
const className = `${this.props.className || ''} ${this.state.highlighted ? 'highlighted' : ''}`;
|
||||
return (
|
||||
<g className={className}
|
||||
transform={`translate(${cx}, 0)`}
|
||||
filter="url(#dropShadow)"
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseLeave={this.handleMouseLeave}>
|
||||
<circle cx={0} cy={0} r={r} className="point-border" />
|
||||
<circle cx={0} cy={0} r={rInner} className="point-core" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface TimelineAcksProps {
|
||||
acknowledges: ZBXAcknowledge[];
|
||||
timeRange: GFTimeRange;
|
||||
width: number;
|
||||
size: number;
|
||||
onHighlight?: (ack: ZBXAcknowledge, index: number) => void;
|
||||
onUnHighlight?: () => void;
|
||||
}
|
||||
|
||||
interface TimelineAcksState {
|
||||
order: number[];
|
||||
highlighted: number;
|
||||
}
|
||||
|
||||
class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { order: [], highlighted: null };
|
||||
}
|
||||
|
||||
handleHighlight = (index: number) => () => {
|
||||
if (this.props.onHighlight) {
|
||||
const ack = this.props.acknowledges[index];
|
||||
this.props.onHighlight(ack, index);
|
||||
}
|
||||
this.bringToFront(index, true);
|
||||
}
|
||||
|
||||
handleUnHighlight = () => {
|
||||
if (this.props.onUnHighlight) {
|
||||
this.props.onUnHighlight();
|
||||
}
|
||||
const order = this.props.acknowledges.map((v, i) => i);
|
||||
this.setState({ order, highlighted: null });
|
||||
}
|
||||
|
||||
bringToFront = (index: number, highlight = false) => {
|
||||
const { acknowledges } = this.props;
|
||||
let order = acknowledges.map((v, i) => i);
|
||||
order = moveToEnd(order, [index]);
|
||||
const highlighted = highlight ? index : null;
|
||||
this.setState({ order, highlighted });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { acknowledges, timeRange, width, size } = this.props;
|
||||
const { timeFrom, timeTo } = timeRange;
|
||||
const range = timeTo - timeFrom;
|
||||
const pointR = size / 2;
|
||||
const eventsItems = acknowledges.map((ack, i) => {
|
||||
const ts = Number(ack.clock);
|
||||
const posLeft = Math.round((ts - timeFrom) / range * width - pointR);
|
||||
const highlighted = this.state.highlighted === i;
|
||||
|
||||
return (
|
||||
<TimelineAck
|
||||
key={ack.eventid}
|
||||
x={posLeft}
|
||||
r={pointR}
|
||||
highlighted={highlighted}
|
||||
onHighlight={this.handleHighlight(i)}
|
||||
onUnHighlight={this.handleUnHighlight}
|
||||
/>
|
||||
);
|
||||
});
|
||||
if (this.state.order.length) {
|
||||
return this.state.order.map(i => eventsItems[i]);
|
||||
}
|
||||
return eventsItems;
|
||||
}
|
||||
}
|
||||
|
||||
interface TimelineAckProps {
|
||||
x: number;
|
||||
r: number;
|
||||
highlighted?: boolean;
|
||||
onHighlight?: () => void;
|
||||
onUnHighlight?: () => void;
|
||||
}
|
||||
|
||||
interface TimelineAckState {
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
class TimelineAck extends PureComponent<TimelineAckProps, TimelineAckState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { highlighted: false };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TimelineAckProps) {
|
||||
// Update component after reordering to make animation working
|
||||
if (prevProps.highlighted !== this.props.highlighted) {
|
||||
this.setState({ highlighted: this.props.highlighted });
|
||||
}
|
||||
}
|
||||
|
||||
handleHighlight = () => {
|
||||
if (this.props.onHighlight) {
|
||||
this.props.onHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
handleUnHighlight = () => {
|
||||
if (this.props.onUnHighlight) {
|
||||
this.props.onUnHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { x } = this.props;
|
||||
const r = this.state.highlighted ? Math.round(this.props.r * HIGHLIGHTED_POINT_SIZE) : this.props.r;
|
||||
const cx = x + this.props.r;
|
||||
const rInner = Math.round(r * INNER_POINT_SIZE);
|
||||
const className = `problem-event-ack ${this.state.highlighted ? 'highlighted' : ''}`;
|
||||
return (
|
||||
<g className={className}
|
||||
transform={`translate(${cx}, 0)`}
|
||||
filter="url(#dropShadow)"
|
||||
onMouseOver={this.handleHighlight}
|
||||
onMouseLeave={this.handleUnHighlight}>
|
||||
<circle cx={0} cy={0} r={r} className="point-border" />
|
||||
<circle cx={0} cy={0} r={rInner} className="point-core" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function moveToEnd<T>(array: T[], itemsToMove: number[]): T[] {
|
||||
const removed = _.pullAt(array, itemsToMove);
|
||||
removed.reverse();
|
||||
array.push(...removed);
|
||||
return array;
|
||||
}
|
||||
284
src/panel-triggers/components/Problems/Problems.tsx
Normal file
284
src/panel-triggers/components/Problems/Problems.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
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 } from '../../types';
|
||||
import EventTag from '../EventTag';
|
||||
import ProblemDetails from './ProblemDetails';
|
||||
import { AckProblemData } from '../Modal';
|
||||
import GFHeartIcon from '../GFHeartIcon';
|
||||
|
||||
export interface ProblemListProps {
|
||||
problems: ZBXTrigger[];
|
||||
panelOptions: ProblemsPanelOptions;
|
||||
loading?: boolean;
|
||||
timeRange?: GFTimeRange;
|
||||
pageSize?: number;
|
||||
fontSize?: number;
|
||||
getProblemEvents: (ids: string[]) => ZBXEvent[];
|
||||
onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void;
|
||||
onTagClick?: (tag: ZBXTag, datasource: string) => void;
|
||||
onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
|
||||
onColumnResize?: (newResized: RTResized) => void;
|
||||
}
|
||||
|
||||
interface ProblemListState {
|
||||
expanded: any;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export default class ProblemList extends PureComponent<ProblemListProps, ProblemListState> {
|
||||
rootWidth: number;
|
||||
rootRef: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expanded: {},
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
|
||||
setRootRef = ref => {
|
||||
this.rootRef = ref;
|
||||
}
|
||||
|
||||
handleProblemAck = (problem: ZBXTrigger, data: AckProblemData) => {
|
||||
return this.props.onProblemAck(problem, data);
|
||||
}
|
||||
|
||||
handlePageSizeChange = (pageSize, pageIndex) => {
|
||||
if (this.props.onPageSizeChange) {
|
||||
this.props.onPageSizeChange(pageSize, pageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleResizedChange = (newResized, event) => {
|
||||
if (this.props.onColumnResize) {
|
||||
this.props.onColumnResize(newResized);
|
||||
}
|
||||
}
|
||||
|
||||
handleExpandedChange = expanded => {
|
||||
const nextExpanded = { ...this.state.expanded };
|
||||
nextExpanded[this.state.page] = expanded;
|
||||
this.setState({
|
||||
expanded: nextExpanded
|
||||
});
|
||||
}
|
||||
|
||||
handleTagClick = (tag: ZBXTag, datasource: string) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, datasource);
|
||||
}
|
||||
}
|
||||
|
||||
getExpandedPage = (page: number) => {
|
||||
return this.state.expanded[page] || {};
|
||||
}
|
||||
|
||||
buildColumns() {
|
||||
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 statusIconCell = props => StatusIconCell(props, highlightNewerThan);
|
||||
|
||||
const columns = [
|
||||
{ Header: 'Host', accessor: 'host', show: options.hostField },
|
||||
{ Header: 'Host (Technical Name)', accessor: 'hostTechName', show: options.hostTechNameField },
|
||||
{ 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),
|
||||
},
|
||||
{
|
||||
Header: '', id: 'statusIcon', show: options.statusIcon, className: 'problem-status-icon', width: 50,
|
||||
accessor: 'value',
|
||||
Cell: statusIconCell,
|
||||
},
|
||||
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
|
||||
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell},
|
||||
{
|
||||
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',
|
||||
id: 'age',
|
||||
Cell: AgeCell,
|
||||
},
|
||||
{
|
||||
Header: 'Time', className: 'last-change', width: 150, accessor: 'lastchangeUnix',
|
||||
id: 'lastchange',
|
||||
Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat),
|
||||
},
|
||||
{ Header: '', className: 'custom-expander', width: 60, expander: true, Expander: CustomExpander },
|
||||
];
|
||||
for (const column of columns) {
|
||||
if (column.show || column.show === undefined) {
|
||||
delete column.show;
|
||||
result.push(column);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = this.buildColumns();
|
||||
this.rootWidth = this.rootRef && this.rootRef.clientWidth;
|
||||
const { pageSize, fontSize, panelOptions } = this.props;
|
||||
const panelClass = classNames('panel-problems', { [`font-size--${fontSize}`]: fontSize });
|
||||
let pageSizeOptions = [5, 10, 20, 25, 50, 100];
|
||||
if (pageSize) {
|
||||
pageSizeOptions.push(pageSize);
|
||||
pageSizeOptions = _.sortBy(pageSizeOptions);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={panelClass} ref={this.setRootRef}>
|
||||
<ReactTable
|
||||
data={this.props.problems}
|
||||
columns={columns}
|
||||
defaultPageSize={10}
|
||||
pageSize={pageSize}
|
||||
pageSizeOptions={pageSizeOptions}
|
||||
resized={panelOptions.resizedColumns}
|
||||
minRows={0}
|
||||
loading={this.props.loading}
|
||||
noDataText="No problems found"
|
||||
SubComponent={props =>
|
||||
<ProblemDetails {...props}
|
||||
rootWidth={this.rootWidth}
|
||||
timeRange={this.props.timeRange}
|
||||
getProblemEvents={this.props.getProblemEvents}
|
||||
onProblemAck={this.handleProblemAck}
|
||||
onTagClick={this.handleTagClick}
|
||||
/>
|
||||
}
|
||||
expanded={this.getExpandedPage(this.state.page)}
|
||||
onExpandedChange={this.handleExpandedChange}
|
||||
onPageChange={page => this.setState({ page })}
|
||||
onPageSizeChange={this.handlePageSizeChange}
|
||||
onResizedChange={this.handleResizedChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SeverityCell(props: RTCell<ZBXTrigger>, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) {
|
||||
const problem = props.original;
|
||||
let color: string;
|
||||
const severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(props.original.priority));
|
||||
color = severityDesc.color;
|
||||
|
||||
// Mark acknowledged triggers with different color
|
||||
if (markAckEvents && problem.acknowledges && problem.acknowledges.length) {
|
||||
color = ackEventColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='severity-cell' style={{ background: color }}>
|
||||
{severityDesc.severity}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
const status = props.value === '0' ? 'RESOLVED' : 'PROBLEM';
|
||||
const color = props.value === '0' ? okColor : problemColor;
|
||||
let newProblem = false;
|
||||
if (highlightNewerThan) {
|
||||
newProblem = isNewProblem(props.original, highlightNewerThan);
|
||||
}
|
||||
return (
|
||||
<span className={newProblem ? 'problem-status--new' : ''} style={{ color }}>{status}</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusIconCell(props: RTCell<ZBXTrigger>, highlightNewerThan?: string) {
|
||||
const status = props.value === '0' ? 'ok' : 'problem';
|
||||
let newProblem = false;
|
||||
if (highlightNewerThan) {
|
||||
newProblem = isNewProblem(props.original, highlightNewerThan);
|
||||
}
|
||||
const className = classNames('zbx-problem-status-icon',
|
||||
{ 'problem-status--new': newProblem },
|
||||
{ 'zbx-problem': props.value === '1' },
|
||||
{ 'zbx-ok': props.value === '0' },
|
||||
);
|
||||
return <GFHeartIcon status={status} className={className} />;
|
||||
}
|
||||
|
||||
function GroupCell(props: RTCell<ZBXTrigger>) {
|
||||
let groups = "";
|
||||
if (props.value && props.value.length) {
|
||||
groups = props.value.map(g => g.name).join(', ');
|
||||
}
|
||||
return (
|
||||
<span>{groups}</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ProblemCell(props: RTCell<ZBXTrigger>) {
|
||||
const comments = props.original.comments;
|
||||
return (
|
||||
<div>
|
||||
<span className="problem-description">{props.value}</span>
|
||||
{/* {comments && <FAIcon icon="file-text-o" customClass="comments-icon" />} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgeCell(props: RTCell<ZBXTrigger>) {
|
||||
const problem = props.original;
|
||||
const timestamp = moment.unix(problem.lastchangeUnix);
|
||||
const age = timestamp.fromNow(true);
|
||||
return <span>{age}</span>;
|
||||
}
|
||||
|
||||
function LastChangeCell(props: RTCell<ZBXTrigger>, customFormat?: string) {
|
||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||
const problem = props.original;
|
||||
const timestamp = moment.unix(problem.lastchangeUnix);
|
||||
const format = customFormat || DEFAULT_TIME_FORMAT;
|
||||
const lastchange = timestamp.format(format);
|
||||
return <span>{lastchange}</span>;
|
||||
}
|
||||
|
||||
interface TagCellProps extends RTCell<ZBXTrigger> {
|
||||
onTagClick: (tag: ZBXTag, datasource: string) => void;
|
||||
}
|
||||
|
||||
class TagCell extends PureComponent<TagCellProps> {
|
||||
handleTagClick = (tag: ZBXTag) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, this.props.original.datasource);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const tags = this.props.value || [];
|
||||
return [
|
||||
tags.map(tag => <EventTag key={tag.tag + tag.value} tag={tag} onClick={this.handleTagClick} /> )
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function CustomExpander(props: RTCell<any>) {
|
||||
return (
|
||||
<span className={props.isExpanded ? "expanded" : ""}>
|
||||
<i className="fa fa-info-circle"></i>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user