diff --git a/src/panel-triggers/components/AlertList/AlertCard.tsx b/src/panel-triggers/components/AlertList/AlertCard.tsx new file mode 100644 index 0000000..9c24554 --- /dev/null +++ b/src/panel-triggers/components/AlertList/AlertCard.tsx @@ -0,0 +1,339 @@ +import React, { PureComponent, CSSProperties } from 'react'; +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 EventTag from '../EventTag'; +import Tooltip from '.././Tooltip/Tooltip'; + +interface AlertCardProps { + problem: ZBXTrigger; + panelOptions: ProblemsPanelOptions; + onTagClick?: (tag: ZBXTag, datasource: string) => void; + onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise | any; +} + +interface AlertCardState { + showAckDialog: boolean; +} + +export default class AlertCard extends PureComponent { + constructor(props) { + super(props); + this.state = { showAckDialog: false }; + } + + handleTagClick = (tag: ZBXTag) => { + if (this.props.onTagClick) { + this.props.onTagClick(tag, this.props.problem.datasource); + } + } + + ackProblem = (data: AckProblemData) => { + const problem = this.props.problem; + 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, panelOptions } = this.props; + const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground }); + 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); + + let newProblem = false; + if (panelOptions.highlightNewerThan) { + newProblem = isNewProblem(problem, panelOptions.highlightNewerThan); + } + const blink = panelOptions.highlightNewEvents && newProblem; + + const cardStyle: CSSProperties = {}; + if (panelOptions.highlightBackground) { + cardStyle.background = severityDesc.color; + } + + return ( +
  • + + +
    +
    +

    + {problem.description} + {(panelOptions.hostField || panelOptions.hostTechNameField) && ( + + )} + {panelOptions.hostGroups && } + + {panelOptions.showTags && ( + + {problem.tags && problem.tags.map(tag => + + )} + + )} +

    + +
    + {panelOptions.statusField && } + {panelOptions.severityField && ( + + )} + + {panelOptions.ageField && "for " + age} + + {panelOptions.descriptionField && !panelOptions.descriptionAtNewLine && ( + + )} +
    + + {panelOptions.descriptionField && panelOptions.descriptionAtNewLine && ( +
    + +
    + )} + +
    +
    + + {panelOptions.datasources.length > 1 && ( +
    + + + {problem.datasource} + +
    + )} + +
    + {lastchange || "last change unknown"} +
    + {problem.url && } + {problem.state === '1' && ( + + + + )} + {problem.lastEvent && ( + + )} +
    +
    + +
  • + ); + } +} + +interface AlertIconProps { + problem: ZBXTrigger; + color: string; + blink?: boolean; + highlightBackground?: boolean; +} + +function AlertIcon(props: AlertIconProps) { + const { problem, color, blink, highlightBackground } = props; + const priority = Number(problem.priority); + let iconClass = ''; + if (problem.value === '1') { + if (priority >= 3) { + iconClass = 'icon-gf-critical'; + } else { + iconClass = 'icon-gf-warning'; + } + } else { + iconClass = 'icon-gf-online'; + } + + const className = classNames('icon-gf', iconClass, { 'zabbix-trigger--blinked': blink }); + const style: CSSProperties = {}; + if (!highlightBackground) { + style.color = color; + } + + return ( +
    + +
    + ); +} + +interface AlertHostProps { + problem: ZBXTrigger; + panelOptions: ProblemsPanelOptions; +} + +function AlertHost(props: AlertHostProps) { + const problem = props.problem; + const panel = props.panelOptions; + let host = ""; + if (panel.hostField && panel.hostTechNameField) { + host = `${problem.host} (${problem.hostTechName})`; + } else if (panel.hostField || panel.hostTechNameField) { + host = panel.hostField ? problem.host : problem.hostTechName; + } + if (panel.hostProxy && problem.proxy) { + host = `${problem.proxy}: ${host}`; + } + + return ( + + {problem.maintenance && } + {host} + + ); +} + +interface AlertGroupProps { + problem: ZBXTrigger; + panelOptions: ProblemsPanelOptions; +} + +function AlertGroup(props: AlertGroupProps) { + const problem = props.problem; + const panel = props.panelOptions; + let groupNames = ""; + if (panel.hostGroups) { + const groups = _.map(problem.groups, 'name').join(', '); + groupNames += `[ ${groups} ]`; + } + + return ( + {groupNames} + ); +} + +const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; +const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; + +function AlertStatus(props) { + const { problem, okColor, problemColor, blink } = props; + const status = problem.value === '0' ? 'RESOLVED' : 'PROBLEM'; + const color = problem.value === '0' ? okColor || DEFAULT_OK_COLOR : problemColor || DEFAULT_PROBLEM_COLOR; + const className = classNames( + 'zbx-trigger-state', + { 'alert-state-critical': problem.value === '1' }, + { 'alert-state-ok': problem.value === '0' }, + { 'zabbix-trigger--blinked': blink } + ); + return ( + + {status} + + ); +} + +function AlertSeverity(props) { + const { severityDesc, highlightBackground, blink } = props; + const className = classNames('zbx-trigger-severity', { 'zabbix-trigger--blinked': blink }); + const style: CSSProperties = {}; + if (!highlightBackground) { + style.color = severityDesc.color; + } + return ( + + {severityDesc.severity} + + ); +} + +interface AlertAcknowledgesButtonProps { + problem: ZBXTrigger; + onClick: (event?) => void; +} + +class AlertAcknowledgesButton extends PureComponent { + handleClick = (event) => { + this.props.onClick(event); + } + + renderTooltipContent = () => { + return ; + } + + render() { + const { problem } = this.props; + return ( + problem.acknowledges && problem.acknowledges.length ? + + + : + + + + ); + } +} + +interface AlertAcknowledgesProps { + problem: ZBXTrigger; + onClick: (event?) => void; +} + +class AlertAcknowledges extends PureComponent { + handleClick = (event) => { + this.props.onClick(event); + } + + render() { + const { problem } = this.props; + const ackRows = problem.acknowledges && problem.acknowledges.map(ack => { + return ( + + {ack.time} + {ack.user} + {ack.message} + + ); + }); + return ( +
    +
    + +
    + + + + + + + + + + {ackRows} + +
    TimeUserComments
    +
    + ); + } +} diff --git a/src/panel-triggers/components/AlertList/AlertList.tsx b/src/panel-triggers/components/AlertList/AlertList.tsx new file mode 100644 index 0000000..ba4cf06 --- /dev/null +++ b/src/panel-triggers/components/AlertList/AlertList.tsx @@ -0,0 +1,134 @@ +import React, { PureComponent, CSSProperties } from 'react'; +import classNames from 'classnames'; +import { ProblemsPanelOptions, ZBXTrigger, GFTimeRange, ZBXTag } from '../../types'; +import { AckProblemData } from '.././Modal'; +import AlertCard from './AlertCard'; + +export interface AlertListProps { + problems: ZBXTrigger[]; + panelOptions: ProblemsPanelOptions; + loading?: boolean; + timeRange?: GFTimeRange; + pageSize?: number; + fontSize?: number; + onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; + onTagClick?: (tag: ZBXTag, datasource: string) => void; +} + +interface AlertListState { + page: number; + currentProblems: ZBXTrigger[]; +} + +export default class AlertList extends PureComponent { + constructor(props) { + super(props); + this.state = { + page: 0, + currentProblems: this.getCurrentProblems(0), + }; + } + + getCurrentProblems(page: number) { + const { pageSize, problems } = this.props; + const start = pageSize * page; + const end = Math.min(pageSize * (page + 1), problems.length); + return this.props.problems.slice(start, end); + } + + handlePageChange = (newPage: number) => { + const items = this.getCurrentProblems(newPage); + this.setState({ + page: newPage, + currentProblems: items, + }); + } + + + handleTagClick = (tag: ZBXTag, datasource: string) => { + if (this.props.onTagClick) { + this.props.onTagClick(tag, datasource); + } + } + + handleProblemAck = (problem: ZBXTrigger, data: AckProblemData) => { + return this.props.onProblemAck(problem, data); + } + + render() { + const { problems, panelOptions } = this.props; + const currentProblems = this.getCurrentProblems(this.state.page); + + return [ +
    +
    +
    +
      + {currentProblems.map(problem => + + )} +
    +
    +
    +
    , +
    + +
    + ]; + } +} + +interface PaginationControlProps { + itemsLength: number; + pageIndex: number; + pageSize: number; + onPageChange: (index: number) => void; +} + +class PaginationControl extends PureComponent { + + handlePageChange = (index: number) => () => { + this.props.onPageChange(index); + } + + render() { + const { itemsLength, pageIndex, pageSize } = this.props; + const pageCount = Math.ceil(itemsLength / pageSize); + if (pageCount === 1) { + return
      ; + } + + const startPage = Math.max(pageIndex - 3, 0); + const endPage = Math.min(pageCount, startPage + 9); + + + const pageLinks = []; + for (let i = startPage; i < endPage; i++) { + const pageLinkClass = classNames('triggers-panel-page-link', 'pointer', { 'active': i === pageIndex }); + const value = i + 1; + const pageLinkElem = ( +
    • + {value} +
    • + ); + pageLinks.push(pageLinkElem); + } + + return ( +
        + {pageLinks} +
      + ); + } +} diff --git a/src/panel-triggers/components/ProblemDetails.tsx b/src/panel-triggers/components/ProblemDetails.tsx index cfd150b..e32357b 100644 --- a/src/panel-triggers/components/ProblemDetails.tsx +++ b/src/panel-triggers/components/ProblemDetails.tsx @@ -1,18 +1,18 @@ import React, { PureComponent } from 'react'; import moment from 'moment'; import * as utils from '../../datasource-zabbix/utils'; -import { Trigger, ZBXItem, ZBXAcknowledge, ZBXHost, ZBXGroup, ZBXEvent, GFTimeRange, RTRow, ZBXTag } from '../types'; +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 ProblemTimeline from './ProblemTimeline'; import FAIcon from './FAIcon'; -interface ProblemDetailsProps extends RTRow { +interface ProblemDetailsProps extends RTRow { rootWidth: number; timeRange: GFTimeRange; - getProblemEvents: (problem: Trigger) => Promise; - onProblemAck?: (problem: Trigger, data: AckProblemData) => Promise | any; + getProblemEvents: (problem: ZBXTrigger) => Promise; + onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => Promise | any; onTagClick?: (tag: ZBXTag, datasource: string) => void; } @@ -55,7 +55,7 @@ export default class ProblemDetails extends PureComponent { - const problem = this.props.original as Trigger; + 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(); @@ -74,7 +74,7 @@ export default class ProblemDetails extends PureComponent 1200; @@ -246,7 +246,7 @@ class ProblemHosts extends PureComponent { } interface ProblemStatusBarProps { - problem: Trigger; + problem: ZBXTrigger; className?: string; } diff --git a/src/panel-triggers/components/Problems.tsx b/src/panel-triggers/components/Problems.tsx index 83d66cc..a53e01c 100644 --- a/src/panel-triggers/components/Problems.tsx +++ b/src/panel-triggers/components/Problems.tsx @@ -4,21 +4,22 @@ import classNames from 'classnames'; import _ from 'lodash'; import moment from 'moment'; import * as utils from '../../datasource-zabbix/utils'; -import { ProblemsPanelOptions, Trigger, ZBXEvent, GFTimeRange, RTCell, ZBXTag, TriggerSeverity, RTResized } from '../types'; +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: Trigger[]; + problems: ZBXTrigger[]; panelOptions: ProblemsPanelOptions; loading?: boolean; timeRange?: GFTimeRange; pageSize?: number; fontSize?: number; getProblemEvents: (ids: string[]) => ZBXEvent[]; - onProblemAck?: (problem: Trigger, data: AckProblemData) => void; + onProblemAck?: (problem: ZBXTrigger, data: AckProblemData) => void; onTagClick?: (tag: ZBXTag, datasource: string) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void; onColumnResize?: (newResized: RTResized) => void; @@ -45,7 +46,7 @@ export class ProblemList extends PureComponent { + handleProblemAck = (problem: ZBXTrigger, data: AckProblemData) => { return this.props.onProblemAck(problem, data); } @@ -172,7 +173,7 @@ export class ProblemList extends PureComponent, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) { +function SeverityCell(props: RTCell, problemSeverityDesc: TriggerSeverity[], markAckEvents?: boolean, ackEventColor?: string) { const problem = props.original; let color: string; const severityDesc = _.find(problemSeverityDesc, s => s.priority === Number(props.original.priority)); @@ -193,7 +194,7 @@ function SeverityCell(props: RTCell, problemSeverityDesc: TriggerSeveri const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; -function StatusCell(props: RTCell, okColor = DEFAULT_OK_COLOR, problemColor = DEFAULT_PROBLEM_COLOR, highlightNewerThan?: string) { +function StatusCell(props: RTCell, 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; @@ -205,7 +206,7 @@ function StatusCell(props: RTCell, okColor = DEFAULT_OK_COLOR, problemC ); } -function StatusIconCell(props: RTCell, highlightNewerThan?: string) { +function StatusIconCell(props: RTCell, highlightNewerThan?: string) { const status = props.value === '0' ? 'ok' : 'problem'; let newProblem = false; if (highlightNewerThan) { @@ -219,7 +220,7 @@ function StatusIconCell(props: RTCell, highlightNewerThan?: string) { return ; } -function GroupCell(props: RTCell) { +function GroupCell(props: RTCell) { let groups = ""; if (props.value && props.value.length) { groups = props.value.map(g => g.name).join(', '); @@ -229,7 +230,7 @@ function GroupCell(props: RTCell) { ); } -function ProblemCell(props: RTCell) { +function ProblemCell(props: RTCell) { const comments = props.original.comments; return (
      @@ -239,14 +240,14 @@ function ProblemCell(props: RTCell) { ); } -function AgeCell(props: RTCell) { +function AgeCell(props: RTCell) { const problem = props.original; const timestamp = moment.unix(problem.lastchangeUnix); const age = timestamp.fromNow(true); return {age}; } -function LastChangeCell(props: RTCell, customFormat?: string) { +function LastChangeCell(props: RTCell, customFormat?: string) { const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; const problem = props.original; const timestamp = moment.unix(problem.lastchangeUnix); @@ -255,7 +256,7 @@ function LastChangeCell(props: RTCell, customFormat?: string) { return {lastchange}; } -interface TagCellProps extends RTCell { +interface TagCellProps extends RTCell { onTagClick: (tag: ZBXTag, datasource: string) => void; } @@ -281,13 +282,3 @@ function CustomExpander(props: RTCell) { ); } - -function isNewProblem(problem: Trigger, highlightNewerThan: string): boolean { - try { - const highlightIntervalMs = utils.parseInterval(highlightNewerThan); - const durationSec = (Date.now() - problem.lastchangeUnix * 1000); - return durationSec < highlightIntervalMs; - } catch (e) { - return false; - } -} diff --git a/src/panel-triggers/triggers_panel_ctrl.js b/src/panel-triggers/triggers_panel_ctrl.js index 68f3a2b..0a57945 100644 --- a/src/panel-triggers/triggers_panel_ctrl.js +++ b/src/panel-triggers/triggers_panel_ctrl.js @@ -5,11 +5,12 @@ import $ from 'jquery'; 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 { 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'; +import AlertList from './components/AlertList/AlertList'; const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource'; const PROBLEM_EVENTS_LIMIT = 100; @@ -636,7 +637,8 @@ export class TriggerPanelCtrl extends PanelCtrl { ctrl.addTagFilter(tag, datasource); } }; - const problemsReactElem = React.createElement(ProblemList, problemsListProps); + // const problemsReactElem = React.createElement(ProblemList, problemsListProps); + const problemsReactElem = React.createElement(AlertList, problemsListProps); ReactDOM.render(problemsReactElem, elem.find('.panel-content')[0]); } } diff --git a/src/panel-triggers/types.ts b/src/panel-triggers/types.ts index aebac58..17bb5b5 100644 --- a/src/panel-triggers/types.ts +++ b/src/panel-triggers/types.ts @@ -72,7 +72,7 @@ export interface TriggerSeverity { export type TriggerColor = string; -export interface Trigger { +export interface ZBXTrigger { acknowledges?: ZBXAcknowledge[]; alerts?: ZBXAlert[]; age?: string; @@ -104,6 +104,7 @@ export interface Trigger { status?: string; tags?: ZBXTag[]; templateid?: string; + triggerid?: string; /** Whether the trigger can generate multiple problem events. */ type?: string; url?: string; diff --git a/src/panel-triggers/utils.ts b/src/panel-triggers/utils.ts new file mode 100644 index 0000000..d88661f --- /dev/null +++ b/src/panel-triggers/utils.ts @@ -0,0 +1,22 @@ +import moment from 'moment'; +import * as utils from '../datasource-zabbix/utils'; +import { ZBXTrigger } from './types'; + +export function isNewProblem(problem: ZBXTrigger, highlightNewerThan: string): boolean { + try { + const highlightIntervalMs = utils.parseInterval(highlightNewerThan); + const durationSec = (Date.now() - problem.lastchangeUnix * 1000); + return durationSec < highlightIntervalMs; + } catch (e) { + return false; + } +} + +const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; + +export function formatLastChange(lastchangeUnix: number, customFormat?: string) { + const timestamp = moment.unix(lastchangeUnix); + const format = customFormat || DEFAULT_TIME_FORMAT; + const lastchange = timestamp.format(format); + return lastchange; +}