problems: initial timeline
This commit is contained in:
105
src/panel-triggers/components/ProblemTimeline.tsx
Normal file
105
src/panel-triggers/components/ProblemTimeline.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { GFTimeRange, ZBXEvent } from 'panel-triggers/types';
|
||||||
|
|
||||||
|
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
|
||||||
|
const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)';
|
||||||
|
const EVENT_ITEM_SIZE = 16;
|
||||||
|
|
||||||
|
export interface ProblemTimelineProps {
|
||||||
|
events: ZBXEvent[];
|
||||||
|
timeRange: GFTimeRange;
|
||||||
|
okColor?: string;
|
||||||
|
problemColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProblemTimelineState {
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ProblemTimeline extends PureComponent<ProblemTimelineProps, ProblemTimelineState> {
|
||||||
|
rootWidth: number;
|
||||||
|
rootRef: any;
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
okColor: DEFAULT_OK_COLOR,
|
||||||
|
problemColor: DEFAULT_PROBLEM_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
width: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setRootRef = ref => {
|
||||||
|
this.rootRef = ref;
|
||||||
|
const width = this.rootRef && this.rootRef.clientWidth || 0;
|
||||||
|
this.setState({ width });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.rootRef) {
|
||||||
|
return <div className="event-timeline" ref={this.setRootRef} />;
|
||||||
|
}
|
||||||
|
const { events, timeRange } = this.props;
|
||||||
|
const { timeFrom, timeTo } = timeRange;
|
||||||
|
const range = timeTo - timeFrom;
|
||||||
|
const width = this.state.width;
|
||||||
|
|
||||||
|
let firstItem;
|
||||||
|
if (events.length) {
|
||||||
|
const firstTs = events.length ? Number(events[0].clock) : timeTo;
|
||||||
|
const duration = (firstTs - timeFrom) / range;
|
||||||
|
const firstEventColor = events[0].value !== '1' ? this.props.problemColor : this.props.okColor;
|
||||||
|
const firstEventStyle: React.CSSProperties = {
|
||||||
|
width: duration * width,
|
||||||
|
left: 0,
|
||||||
|
background: firstEventColor,
|
||||||
|
};
|
||||||
|
firstItem = (
|
||||||
|
<div key='0' className="problem-event-interval" style={firstEventStyle}></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 posLeft = (ts - timeFrom) / range * width;
|
||||||
|
const eventColor = event.value === '1' ? this.props.problemColor : this.props.okColor;
|
||||||
|
const styles: React.CSSProperties = {
|
||||||
|
width: duration * width,
|
||||||
|
left: posLeft,
|
||||||
|
background: eventColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={event.eventid} className="problem-event-interval" style={styles}></div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsItems = events.map(event => {
|
||||||
|
const ts = Number(event.clock);
|
||||||
|
const posLeft = (ts - timeFrom) / range * width - EVENT_ITEM_SIZE / 2;
|
||||||
|
const eventColor = event.value === '1' ? this.props.problemColor : this.props.okColor;
|
||||||
|
const styles: React.CSSProperties = {
|
||||||
|
transform: `translate(${posLeft}px, -2px)`,
|
||||||
|
// background: eventColor,
|
||||||
|
borderColor: eventColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={event.eventid} className="problem-event-item" style={styles}></div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="event-timeline" ref={this.setRootRef}>
|
||||||
|
{firstItem}
|
||||||
|
{eventsIntervalItems}
|
||||||
|
{eventsItems}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
import * as utils from '../../datasource-zabbix/utils';
|
||||||
|
import { ProblemsPanelOptions, Trigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange } from '../types';
|
||||||
|
import { Modal, AckProblemData } from './Modal';
|
||||||
import EventTag from './EventTag';
|
import EventTag from './EventTag';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import { Modal, AckProblemData } from './Modal';
|
import ProblemTimeline from './ProblemTimeline';
|
||||||
import { ProblemsPanelOptions, Trigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup } from '../types';
|
|
||||||
import * as utils from '../../datasource-zabbix/utils';
|
|
||||||
|
|
||||||
export interface ProblemListProps {
|
export interface ProblemListProps {
|
||||||
problems: Trigger[];
|
problems: Trigger[];
|
||||||
panelOptions: ProblemsPanelOptions;
|
panelOptions: ProblemsPanelOptions;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
timeRange?: GFTimeRange;
|
||||||
|
getProblemEvents: (ids: string[]) => ZBXEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProblemListState {
|
interface ProblemListState {
|
||||||
@@ -93,7 +96,13 @@ export class ProblemList extends PureComponent<ProblemListProps, ProblemListStat
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
defaultPageSize={10}
|
defaultPageSize={10}
|
||||||
loading={this.props.loading}
|
loading={this.props.loading}
|
||||||
SubComponent={props => <ProblemDetails rootWidth={this.rootWidth} {...props} />}
|
SubComponent={props =>
|
||||||
|
<ProblemDetails {...props}
|
||||||
|
rootWidth={this.rootWidth}
|
||||||
|
timeRange={this.props.timeRange}
|
||||||
|
getProblemEvents={this.props.getProblemEvents}
|
||||||
|
/>
|
||||||
|
}
|
||||||
expanded={this.getExpandedPage(this.state.page)}
|
expanded={this.getExpandedPage(this.state.page)}
|
||||||
onExpandedChange={this.handleExpandedChange}
|
onExpandedChange={this.handleExpandedChange}
|
||||||
onPageChange={page => this.setState({ page })}
|
onPageChange={page => this.setState({ page })}
|
||||||
@@ -352,6 +361,7 @@ class ProblemActionButton extends PureComponent<ProblemActionButtonProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ProblemDetailsState {
|
interface ProblemDetailsState {
|
||||||
|
events: ZBXEvent[];
|
||||||
show: boolean;
|
show: boolean;
|
||||||
showAckDialog: boolean;
|
showAckDialog: boolean;
|
||||||
}
|
}
|
||||||
@@ -360,12 +370,19 @@ class ProblemDetails extends PureComponent<any, ProblemDetailsState> {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
events: [],
|
||||||
show: false,
|
show: false,
|
||||||
showAckDialog: false,
|
showAckDialog: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const problem = this.props.original;
|
||||||
|
this.props.getProblemEvents(problem)
|
||||||
|
.then(events => {
|
||||||
|
console.log(events, this.props.timeRange);
|
||||||
|
this.setState({ events });
|
||||||
|
});
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.setState({ show: true });
|
this.setState({ show: true });
|
||||||
});
|
});
|
||||||
@@ -417,11 +434,14 @@ class ProblemDetails extends PureComponent<any, ProblemDetailsState> {
|
|||||||
<span>{problem.comments}</span>
|
<span>{problem.comments}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
{problem.tags && problem.tags.length &&
|
||||||
<div className="problem-tags">
|
<div className="problem-tags">
|
||||||
{problem.tags && problem.tags.map(tag =>
|
{problem.tags && problem.tags.map(tag =>
|
||||||
<EventTag key={tag.tag + tag.value} tag={tag} highlight={tag.tag === problem.correlation_tag} />)
|
<EventTag key={tag.tag + tag.value} tag={tag} highlight={tag.tag === problem.correlation_tag} />)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange} />
|
||||||
{problem.acknowledges && !wideLayout &&
|
{problem.acknowledges && !wideLayout &&
|
||||||
<div className="problem-ack-container">
|
<div className="problem-ack-container">
|
||||||
<h6><FAIcon icon="reply-all" /> Acknowledges</h6>
|
<h6><FAIcon icon="reply-all" /> Acknowledges</h6>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||||
import * as utils from '../datasource-zabbix/utils';
|
import * as utils from '../datasource-zabbix/utils';
|
||||||
import {PanelCtrl} from 'grafana/app/plugins/sdk';
|
import {PanelCtrl} from 'grafana/app/plugins/sdk';
|
||||||
import {triggerPanelOptionsTab} from './options_tab';
|
import {triggerPanelOptionsTab} from './options_tab';
|
||||||
@@ -74,12 +75,13 @@ const triggerStatusMap = {
|
|||||||
export class TriggerPanelCtrl extends PanelCtrl {
|
export class TriggerPanelCtrl extends PanelCtrl {
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, $injector, $timeout, datasourceSrv, templateSrv, contextSrv, dashboardSrv) {
|
constructor($scope, $injector, $timeout, datasourceSrv, templateSrv, contextSrv, dashboardSrv, timeSrv) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
this.datasourceSrv = datasourceSrv;
|
this.datasourceSrv = datasourceSrv;
|
||||||
this.templateSrv = templateSrv;
|
this.templateSrv = templateSrv;
|
||||||
this.contextSrv = contextSrv;
|
this.contextSrv = contextSrv;
|
||||||
this.dashboardSrv = dashboardSrv;
|
this.dashboardSrv = dashboardSrv;
|
||||||
|
this.timeSrv = timeSrv;
|
||||||
this.scope = $scope;
|
this.scope = $scope;
|
||||||
this.$timeout = $timeout;
|
this.$timeout = $timeout;
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
|||||||
this.triggerList = [];
|
this.triggerList = [];
|
||||||
this.currentTriggersPage = [];
|
this.currentTriggersPage = [];
|
||||||
this.datasources = {};
|
this.datasources = {};
|
||||||
|
this.range = {};
|
||||||
|
|
||||||
this.panel = migratePanelSchema(this.panel);
|
this.panel = migratePanelSchema(this.panel);
|
||||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||||
@@ -159,6 +162,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
|||||||
// ignore fetching data if another panel is in fullscreen
|
// ignore fetching data if another panel is in fullscreen
|
||||||
if (this.otherPanelInFullscreenMode()) { return; }
|
if (this.otherPanelInFullscreenMode()) { return; }
|
||||||
|
|
||||||
|
this.range = this.timeSrv.timeRange();
|
||||||
|
|
||||||
// clear loading/error state
|
// clear loading/error state
|
||||||
delete this.error;
|
delete this.error;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -518,6 +523,16 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProblemEvents(trigger) {
|
||||||
|
const triggerids = [trigger.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(trigger.datasource)
|
||||||
|
.then(datasource => {
|
||||||
|
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentTriggersPage() {
|
getCurrentTriggersPage() {
|
||||||
let pageSize = this.panel.pageSize || PANEL_DEFAULTS.pageSize;
|
let pageSize = this.panel.pageSize || PANEL_DEFAULTS.pageSize;
|
||||||
let startPos = this.pageIndex * pageSize;
|
let startPos = this.pageIndex * pageSize;
|
||||||
@@ -705,6 +720,9 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
|||||||
function renderProblems() {
|
function renderProblems() {
|
||||||
console.debug('rendering ProblemsList React component');
|
console.debug('rendering ProblemsList React component');
|
||||||
// console.log(ctrl);
|
// console.log(ctrl);
|
||||||
|
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
||||||
|
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
||||||
|
|
||||||
let panelOptions = {};
|
let panelOptions = {};
|
||||||
for (let prop in PANEL_DEFAULTS) {
|
for (let prop in PANEL_DEFAULTS) {
|
||||||
panelOptions[prop] = ctrl.panel[prop];
|
panelOptions[prop] = ctrl.panel[prop];
|
||||||
@@ -712,6 +730,8 @@ export class TriggerPanelCtrl extends PanelCtrl {
|
|||||||
const problemsListProps = {
|
const problemsListProps = {
|
||||||
problems: ctrl.triggerList,
|
problems: ctrl.triggerList,
|
||||||
panelOptions,
|
panelOptions,
|
||||||
|
timeRange: { timeFrom, timeTo },
|
||||||
|
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||||
};
|
};
|
||||||
const problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
const problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
||||||
ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]);
|
ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]);
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export interface ZBXEvent {
|
|||||||
object?: string;
|
object?: string;
|
||||||
objectid?: string;
|
objectid?: string;
|
||||||
acknowledged?: string;
|
acknowledged?: string;
|
||||||
|
hosts?: ZBXHost[];
|
||||||
|
acknowledges?: ZBXAcknowledge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZBXTag {
|
export interface ZBXTag {
|
||||||
@@ -158,3 +160,8 @@ export interface ZBXAlert {
|
|||||||
message: string;
|
message: string;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GFTimeRange {
|
||||||
|
timeFrom: number;
|
||||||
|
timeTo: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -336,6 +336,42 @@
|
|||||||
animation: blink-shadow 2s ease-out infinite;
|
animation: blink-shadow 2s ease-out infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-timeline {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
margin: 1.6rem 0;
|
||||||
|
// margin-top: auto;
|
||||||
|
|
||||||
|
.problem-event-interval {
|
||||||
|
height: 12px;
|
||||||
|
position: absolute;
|
||||||
|
// opacity: 0.7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
// opacity: 1;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
box-shadow: 0px 0px 5px rgba($blue, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-event-item {
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
// transform: translate(0px, -2px);
|
||||||
|
background: $problem-statusbar-background;
|
||||||
|
border: 4px solid $blue;
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0px 0px 6px 1px rgba($orange, 1);
|
||||||
|
background-color: $zbx-text-highlighted;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .rt-tr-group {
|
// .rt-tr-group {
|
||||||
|
|||||||
Reference in New Issue
Block a user