diff --git a/package.json b/package.json index 137be94..771a586 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "ts-jest": "24.1.0", "ts-loader": "4.4.1", "tslint": "^6.1.3", - "typescript": "4.4.4", + "typescript": "4.8.2", "webpack": "4.41.5", "webpack-cli": "3.3.10" }, diff --git a/src/app_config_ctrl/config.html b/src/app_config_ctrl/config.html deleted file mode 100644 index 4c4b34c..0000000 --- a/src/app_config_ctrl/config.html +++ /dev/null @@ -1 +0,0 @@ -

Zabbix Plugin Config

diff --git a/src/app_config_ctrl/config.js b/src/app_config_ctrl/config.js deleted file mode 100644 index b23554a..0000000 --- a/src/app_config_ctrl/config.js +++ /dev/null @@ -1,4 +0,0 @@ -export class ZabbixAppConfigCtrl { - constructor() { } -} -ZabbixAppConfigCtrl.templateUrl = 'app_config_ctrl/config.html'; diff --git a/src/components/ExploreButton/ExploreButton.tsx b/src/components/ExploreButton/ExploreButton.tsx index f76b0a7..9d14f53 100644 --- a/src/components/ExploreButton/ExploreButton.tsx +++ b/src/components/ExploreButton/ExploreButton.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import { locationService } from '@grafana/runtime'; -import { ExploreUrlState, TimeRange, urlUtil } from "@grafana/data"; +import { ExploreUrlState, TimeRange, urlUtil } from '@grafana/data'; import { MODE_ITEMID, MODE_METRICS } from '../../datasource-zabbix/constants'; import { ActionButton } from '../ActionButton/ActionButton'; import { expandItemName } from '../../datasource-zabbix/utils'; @@ -35,7 +35,7 @@ const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) = item: { filter: itemName }, }; } else { - const itemids = problem.items?.map(p => p.itemid).join(','); + const itemids = problem.items?.map((p) => p.itemid).join(','); query = { queryType: MODE_ITEMID, itemids: itemids, @@ -54,4 +54,3 @@ const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) = const url = urlUtil.renderUrl('/explore', { left: exploreState }); locationService.push(url); }; - diff --git a/src/datasource-zabbix/types.ts b/src/datasource-zabbix/types.ts index 71a55d6..fd73f0d 100644 --- a/src/datasource-zabbix/types.ts +++ b/src/datasource-zabbix/types.ts @@ -1,4 +1,4 @@ -import { DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data'; +import { BusEventWithPayload, DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data'; export interface ZabbixDSOptions extends DataSourceJsonData { username: string; @@ -207,6 +207,8 @@ export interface ProblemDTO { triggerid?: string; eventid?: string; timestamp: number; + lastchange?: string; + lastchangeUnix?: number; /** Name of the trigger. */ name?: string; @@ -223,6 +225,7 @@ export interface ProblemDTO { hostTechName?: string; proxy?: string; severity?: string; + priority?: string; acknowledged?: '1' | '0'; acknowledges?: ZBXAcknowledge[]; @@ -245,6 +248,7 @@ export interface ProblemDTO { error?: string; showAckButton?: boolean; + type?: string; } export interface ZBXProblem { @@ -323,6 +327,7 @@ export interface ZBXHost { host: string; maintenance_status?: string; proxy_hostid?: string; + proxy?: any; } export interface ZBXItem { @@ -380,3 +385,7 @@ export interface ZBXAlert { message: string; error: string; } + +export class ZBXQueryUpdatedEvent extends BusEventWithPayload { + static type = 'zbx-query-updated'; +} diff --git a/src/module.js b/src/module.ts similarity index 58% rename from src/module.js rename to src/module.ts index 0854e5c..bc8ae5e 100644 --- a/src/module.js +++ b/src/module.ts @@ -1,14 +1,12 @@ +import { AppPlugin } from '@grafana/data'; +import { loadPluginCss } from 'grafana/app/plugins/sdk'; + import './sass/grafana-zabbix.dark.scss'; import './sass/grafana-zabbix.light.scss'; -import {ZabbixAppConfigCtrl} from './app_config_ctrl/config'; -import {loadPluginCss} from 'grafana/app/plugins/sdk'; - loadPluginCss({ dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', - light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css' + light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css', }); -export { - ZabbixAppConfigCtrl as ConfigCtrl -}; +export const plugin = new AppPlugin<{}>(); diff --git a/src/panel-triggers/ProblemsPanel.tsx b/src/panel-triggers/ProblemsPanel.tsx new file mode 100644 index 0000000..0b29541 --- /dev/null +++ b/src/panel-triggers/ProblemsPanel.tsx @@ -0,0 +1,277 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import _ from 'lodash'; +import { BusEventBase, BusEventWithPayload, dateMath, PanelProps } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { useTheme2 } from '@grafana/ui'; +import { contextSrv } from 'grafana/app/core/core'; +import { ProblemsPanelOptions } from './types'; +import { ProblemDTO, ZabbixMetricsQuery, ZBXQueryUpdatedEvent, ZBXTag } from '../datasource-zabbix/types'; +import { APIExecuteScriptResponse } from '../datasource-zabbix/zabbix/connectors/zabbix_api/types'; +import ProblemList from './components/Problems/Problems'; +import { AckProblemData } from './components/AckModal'; +import AlertList from './components/AlertList/AlertList'; + +const PROBLEM_EVENTS_LIMIT = 100; + +interface ProblemsPanelProps extends PanelProps {} + +export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => { + const { data, options, timeRange, onOptionsChange } = props; + const { layout, showTriggers, triggerSeverity, sortProblems } = options; + const theme = useTheme2(); + + const prepareProblems = () => { + const problems: ProblemDTO[] = []; + if (!data?.series) { + return []; + } + + for (const dataFrame of data.series) { + try { + const values = dataFrame.fields[0].values; + if (values.toArray) { + problems.push(...values.toArray()); + } + } catch (error) { + console.log(error); + return []; + } + } + + let triggers = _.cloneDeep(problems); + + triggers = triggers.map((t) => formatTrigger(t)); + triggers = filterProblems(triggers); + triggers = sortTriggers(triggers); + + return triggers; + }; + + const filterProblems = (problems: ProblemDTO[]) => { + let problemsList = _.cloneDeep(problems); + + // Filter acknowledged triggers + if (showTriggers === 'unacknowledged') { + problemsList = problemsList.filter((trigger) => { + return !(trigger.acknowledges && trigger.acknowledges.length); + }); + } else if (showTriggers === 'acknowledged') { + problemsList = problemsList.filter((trigger) => { + return trigger.acknowledges && trigger.acknowledges.length; + }); + } + + // Filter triggers by severity + problemsList = problemsList.filter((problem) => { + if (problem.severity) { + return triggerSeverity[problem.severity].show; + } else { + return triggerSeverity[problem.priority].show; + } + }); + + return problemsList; + }; + + const sortTriggers = (problems: ProblemDTO[]) => { + if (sortProblems === 'priority') { + problems = _.orderBy(problems, ['severity', 'timestamp', 'eventid'], ['desc', 'desc', 'desc']); + } else if (sortProblems === 'lastchange') { + problems = _.orderBy(problems, ['timestamp', 'severity', 'eventid'], ['desc', 'desc', 'desc']); + } + return problems; + }; + + const formatTrigger = (zabbixTrigger: ProblemDTO) => { + const trigger = _.cloneDeep(zabbixTrigger); + + // Set host and proxy that the trigger belongs + if (trigger.hosts && trigger.hosts.length) { + const host = trigger.hosts[0]; + trigger.host = host.name; + trigger.hostTechName = host.host; + if (host.proxy) { + trigger.proxy = host.proxy; + } + } + + // Set tags if present + if (trigger.tags && trigger.tags.length === 0) { + trigger.tags = null; + } + + // Handle multi-line description + if (trigger.comments) { + trigger.comments = trigger.comments.replace('\n', '
'); + } + + trigger.lastchangeUnix = Number(trigger.lastchange); + return trigger; + }; + + const parseTags = (tagStr: string) => { + if (!tagStr) { + return []; + } + + const tagStrings = _.map(tagStr.split(','), (tag) => tag.trim()); + const tags = _.map(tagStrings, (tag) => { + const tagParts = tag.split(':'); + return { tag: tagParts[0].trim(), value: tagParts[1].trim() }; + }); + return tags; + }; + + const tagsToString = (tags: ZBXTag[]) => { + return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', '); + }; + + const addTagFilter = (tag, datasource) => { + const targets = data.request.targets; + let updated = false; + for (const target of targets) { + if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) { + const tagFilter = (target as ZabbixMetricsQuery).tags.filter; + let targetTags = parseTags(tagFilter); + const newTag = { tag: tag.tag, value: tag.value }; + targetTags.push(newTag); + targetTags = _.uniqWith(targetTags, _.isEqual); + const newFilter = tagsToString(targetTags); + (target as ZabbixMetricsQuery).tags.filter = newFilter; + updated = true; + } + } + if (updated) { + // TODO: investigate is it possible to handle this event + const event = new ZBXQueryUpdatedEvent(targets); + props.eventBus.publish(event); + } + }; + + const removeTagFilter = (tag, datasource) => { + const matchTag = (t) => t.tag === tag.tag && t.value === tag.value; + const targets = data.request.targets; + let updated = false; + for (const target of targets) { + if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) { + const tagFilter = (target as ZabbixMetricsQuery).tags.filter; + let targetTags = parseTags(tagFilter); + _.remove(targetTags, matchTag); + targetTags = _.uniqWith(targetTags, _.isEqual); + const newFilter = tagsToString(targetTags); + (target as ZabbixMetricsQuery).tags.filter = newFilter; + updated = true; + } + } + if (updated) { + // TODO: investigate is it possible to handle this event + const event = new ZBXQueryUpdatedEvent(targets); + props.eventBus.publish(event); + } + }; + + const getProblemEvents = async (problem: ProblemDTO) => { + const triggerids = [problem.triggerid]; + const timeFrom = Math.ceil(dateMath.parse(timeRange.from).unix()); + const timeTo = Math.ceil(dateMath.parse(timeRange.to).unix()); + const ds: any = await getDataSourceSrv().get(problem.datasource); + return ds.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT); + }; + + const getProblemAlerts = async (problem: ProblemDTO) => { + if (!problem.eventid) { + return Promise.resolve([]); + } + const eventids = [problem.eventid]; + const ds: any = await getDataSourceSrv().get(problem.datasource); + return ds.zabbix.getEventAlerts(eventids); + }; + + const getScripts = async (problem: ProblemDTO) => { + const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null; + const ds: any = await getDataSourceSrv().get(problem.datasource); + return ds.zabbix.getScripts([hostid]); + }; + + const onExecuteScript = async (problem: ProblemDTO, scriptid: string): Promise => { + const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null; + const ds: any = await getDataSourceSrv().get(problem.datasource); + return ds.zabbix.executeScript(hostid, scriptid); + }; + + const onProblemAck = async (problem: ProblemDTO, data: AckProblemData) => { + const { message, action, severity } = data; + const eventid = problem.eventid; + const grafana_user = (contextSrv.user as any).name; + const ack_message = grafana_user + ' (Grafana): ' + message; + const ds: any = await getDataSourceSrv().get(problem.datasource); + const userIsEditor = contextSrv.isEditor || contextSrv.isGrafanaAdmin; + if (ds.disableReadOnlyUsersAck && !userIsEditor) { + return { message: 'You have no permissions to acknowledge events.' }; + } + if (eventid) { + return ds.zabbix.acknowledgeEvent(eventid, ack_message, action, severity); + } else { + return { message: 'Trigger has no events. Nothing to acknowledge.' }; + } + }; + + const onColumnResize = (newResized) => { + onOptionsChange({ ...options, resizedColumns: newResized }); + }; + + const onTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => { + if (ctrlKey || shiftKey) { + removeTagFilter(tag, datasource); + } else { + addTagFilter(tag, datasource); + } + }; + + const renderList = () => { + const problems = prepareProblems(); + const fontSize = parseInt(options.fontSize.slice(0, options.fontSize.length - 1), 10); + const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null; + + return ( + + ); + }; + + const renderTable = () => { + const problems = prepareProblems(); + const fontSize = parseInt(options.fontSize.slice(0, options.fontSize.length - 1), 10); + const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null; + + return ( + + ); + }; + + return ( + <> + {layout === 'list' && renderList()} + {layout === 'table' && renderTable()} + + ); +}; diff --git a/src/panel-triggers/components/AlertList/AlertAcknowledges.tsx b/src/panel-triggers/components/AlertList/AlertAcknowledges.tsx index 8ddf791..10e7547 100644 --- a/src/panel-triggers/components/AlertList/AlertAcknowledges.tsx +++ b/src/panel-triggers/components/AlertList/AlertAcknowledges.tsx @@ -1,27 +1,29 @@ import React, { PureComponent } from 'react'; -import { ZBXTrigger } from '../../types'; +import { ProblemDTO } from '../../../datasource-zabbix/types'; interface AlertAcknowledgesProps { - problem: ZBXTrigger; + problem: ProblemDTO; onClick: (event?) => void; } export default 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} - - ); - }); + const ackRows = + problem.acknowledges && + problem.acknowledges.map((ack) => { + return ( + + {ack.time} + {ack.user} + {ack.message} + + ); + }); return (
@@ -32,17 +34,19 @@ export default class AlertAcknowledges extends PureComponentComments - - {ackRows} - + {ackRows}
- {problem.showAckButton && + {problem.showAckButton && (
-
- } + )}
); } diff --git a/src/panel-triggers/components/AlertList/AlertCard.tsx b/src/panel-triggers/components/AlertList/AlertCard.tsx index 8d976f2..07f25a7 100644 --- a/src/panel-triggers/components/AlertList/AlertCard.tsx +++ b/src/panel-triggers/components/AlertList/AlertCard.tsx @@ -31,25 +31,32 @@ export default class AlertCard extends PureComponent { ackProblem = (data: AckProblemData) => { const problem = this.props.problem; return this.props.onProblemAck(problem, data); - } + }; render() { const { problem, panelOptions } = this.props; const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1; - const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground }); - const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine }); + const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { + 'zbx-trigger-highlighted': panelOptions.highlightBackground, + }); + const descriptionClass = classNames('alert-rule-item__text', { + 'zbx-description--newline': panelOptions.descriptionAtNewLine, + }); const problemSeverity = Number(problem.severity); let severityDesc: TriggerSeverity; - severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); + severityDesc = _.find(panelOptions.triggerSeverity, (s) => s.priority === problemSeverity); if (problem.severity) { - severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); + severityDesc = _.find(panelOptions.triggerSeverity, (s) => s.priority === problemSeverity); } - const lastchange = formatLastChange(problem.timestamp, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat); + const lastchange = formatLastChange( + problem.timestamp, + panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat + ); const age = moment.unix(problem.timestamp).fromNow(true); - let dsName: string = (problem.datasource as string); + let dsName: string = problem.datasource as string; if ((problem.datasource as DataSourceRef)?.uid) { const dsInstance = getDataSourceSrv().getInstanceSettings((problem.datasource as DataSourceRef).uid); dsName = dsInstance.name; @@ -64,7 +71,7 @@ export default class AlertCard extends PureComponent { let problemColor: string; if (problem.value === '0') { problemColor = panelOptions.okEventColor; - } else if (panelOptions.markAckEvents && problem.acknowledged === "1") { + } else if (panelOptions.markAckEvents && problem.acknowledged === '1') { problemColor = panelOptions.ackEventColor; } else { problemColor = severityDesc.color; @@ -77,7 +84,12 @@ export default class AlertCard extends PureComponent { return (
  • - +
    @@ -90,15 +102,16 @@ export default class AlertCard extends PureComponent { {panelOptions.showTags && ( - {problem.tags && problem.tags.map(tag => - - )} + {problem.tags && + problem.tags.map((tag) => ( + + ))} )}
    @@ -106,25 +119,26 @@ export default class AlertCard extends PureComponent {
    {panelOptions.statusField && } {panelOptions.severityField && ( - + )} - - {panelOptions.ageField && "for " + age} - + {panelOptions.ageField && 'for ' + age} {panelOptions.descriptionField && !panelOptions.descriptionAtNewLine && ( )}
    {panelOptions.descriptionField && panelOptions.descriptionAtNewLine && ( -
    +
    )} -
    @@ -138,30 +152,36 @@ export default class AlertCard extends PureComponent { )}
    - {lastchange || "last change unknown"} + {lastchange || 'last change unknown'}
    - {problem.url && } + {problem.url && ( + + + + )} {problem.state === '1' && ( - + + + )} {problem.eventid && ( - {({ showModal, hideModal }) => ( - { - showModal(AckModal, { - canClose: problem.manual_close === '1', - severity: problemSeverity, - onSubmit: this.ackProblem, - onDismiss: hideModal, - }); - }} - /> - )} - + {({ showModal, hideModal }) => ( + { + showModal(AckModal, { + canClose: problem.manual_close === '1', + severity: problemSeverity, + onSubmit: this.ackProblem, + onDismiss: hideModal, + }); + }} + /> + )} + )}
    @@ -178,7 +198,7 @@ interface AlertHostProps { function AlertHost(props: AlertHostProps) { const problem = props.problem; const panel = props.panelOptions; - let host = ""; + let host = ''; if (panel.hostField && panel.hostTechNameField) { host = `${problem.host} (${problem.hostTechName})`; } else if (panel.hostField || panel.hostTechNameField) { @@ -204,15 +224,13 @@ interface AlertGroupProps { function AlertGroup(props: AlertGroupProps) { const problem = props.problem; const panel = props.panelOptions; - let groupNames = ""; + let groupNames = ''; if (panel.hostGroups) { const groups = _.map(problem.groups, 'name').join(', '); groupNames += `[ ${groups} ]`; } - return ( - {groupNames} - ); + return {groupNames}; } const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; @@ -228,11 +246,7 @@ function AlertStatus(props) { { 'alert-state-ok': problem.value === '0' }, { 'zabbix-trigger--blinked': blink } ); - return ( - - {status} - - ); + return {status}; } function AlertSeverity(props) { @@ -257,25 +271,29 @@ interface AlertAcknowledgesButtonProps { class AlertAcknowledgesButton extends PureComponent { handleClick = (event) => { this.props.onClick(event); - } + }; renderTooltipContent = () => { return ; - } + }; render() { const { problem } = this.props; let content = null; if (problem.acknowledges && problem.acknowledges.length) { content = ( - - + + + + ); } else if (problem.showAckButton) { content = ( - + + + ); } diff --git a/src/panel-triggers/components/AlertList/AlertIcon.tsx b/src/panel-triggers/components/AlertList/AlertIcon.tsx index a446e2c..9138821 100644 --- a/src/panel-triggers/components/AlertList/AlertIcon.tsx +++ b/src/panel-triggers/components/AlertList/AlertIcon.tsx @@ -14,14 +14,14 @@ export const AlertIcon: FC = ({ problem, color, blink, highlightBackgroun const severity = Number(problem.severity); const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online'; - const iconClass = cx( - 'icon-gf', - blink && 'zabbix-trigger--blinked', - ); + const iconClass = cx('icon-gf', blink && 'zabbix-trigger--blinked'); const wrapperClass = cx( 'alert-rule-item__icon', - !highlightBackground && css`color: ${color}` + !highlightBackground && + css` + color: ${color}; + ` ); return ( diff --git a/src/panel-triggers/components/AlertList/AlertList.tsx b/src/panel-triggers/components/AlertList/AlertList.tsx index f835da1..2692eba 100644 --- a/src/panel-triggers/components/AlertList/AlertList.tsx +++ b/src/panel-triggers/components/AlertList/AlertList.tsx @@ -1,6 +1,6 @@ import React, { PureComponent, CSSProperties } from 'react'; import classNames from 'classnames'; -import { ProblemsPanelOptions, GFTimeRange } from '../../types'; +import { ProblemsPanelOptions } from '../../types'; import { AckProblemData } from '../AckModal'; import AlertCard from './AlertCard'; import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types'; @@ -10,7 +10,6 @@ export interface AlertListProps { problems: ProblemDTO[]; panelOptions: ProblemsPanelOptions; loading?: boolean; - timeRange?: GFTimeRange; pageSize?: number; fontSize?: number; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; @@ -44,18 +43,17 @@ export default class AlertList extends PureComponent { if (this.props.onTagClick) { this.props.onTagClick(tag, datasource, ctrlKey, shiftKey); } - } + }; handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => { return this.props.onProblemAck(problem, data); - } + }; render() { const { problems, panelOptions } = this.props; @@ -68,15 +66,17 @@ export default class AlertList extends PureComponent
      - {currentProblems.map((problem, index) => + {currentProblems.map((problem, index) => ( - )} + ))}
    @@ -101,10 +101,9 @@ interface PaginationControlProps { } class PaginationControl extends PureComponent { - handlePageChange = (index: number) => () => { this.props.onPageChange(index); - } + }; render() { const { itemsLength, pageIndex, pageSize } = this.props; @@ -116,23 +115,20 @@ class PaginationControl extends PureComponent { 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 pageLinkClass = classNames('triggers-panel-page-link', 'pointer', { active: i === pageIndex }); const value = i + 1; const pageLinkElem = (
  • - {value} + + {value} +
  • ); pageLinks.push(pageLinkElem); } - return ( -
      - {pageLinks} -
    - ); + return
      {pageLinks}
    ; } } diff --git a/src/panel-triggers/components/EventTag.tsx b/src/panel-triggers/components/EventTag.tsx index 6e3dce3..b4c24df 100644 --- a/src/panel-triggers/components/EventTag.tsx +++ b/src/panel-triggers/components/EventTag.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; -import { ZBXTag } from '../types'; import { DataSourceRef } from '@grafana/data'; import { Tooltip } from '@grafana/ui'; +import { ZBXTag } from '../../datasource-zabbix/types'; const TAG_COLORS = [ '#E24D42', @@ -96,10 +96,10 @@ interface EventTagProps { export default class EventTag extends PureComponent { handleClick = (event) => { if (this.props.onClick) { - const { tag, datasource} = this.props; + const { tag, datasource } = this.props; this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey); } - } + }; render() { const { tag, highlight } = this.props; @@ -110,13 +110,12 @@ export default class EventTag extends PureComponent { }; return ( - - {tag.value ? - `${tag.tag}: ${tag.value}` : - `${tag.tag}` - } + onClick={this.handleClick} + > + {tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`} ); diff --git a/src/panel-triggers/components/ExecScriptModal.tsx b/src/panel-triggers/components/ExecScriptModal.tsx index bbc3f98..0f53d6f 100644 --- a/src/panel-triggers/components/ExecScriptModal.tsx +++ b/src/panel-triggers/components/ExecScriptModal.tsx @@ -46,7 +46,7 @@ export class ExecScriptModalUnthemed extends PureComponent { async componentDidMount() { const scripts = await this.props.getScripts(); this.scripts = scripts; - const scriptOptions: Array> = scripts.map(s => { + const scriptOptions: Array> = scripts.map((s) => { return { value: s.scriptid, label: s.name, @@ -55,27 +55,27 @@ export class ExecScriptModalUnthemed extends PureComponent { }); const selectedScript = scriptOptions?.length ? scriptOptions[0] : null; - const script = scripts.find(s => selectedScript.value === s.scriptid); + const script = scripts.find((s) => selectedScript.value === s.scriptid); this.setState({ scriptOptions, selectedScript, script }); } onChangeSelectedScript = (v: SelectableValue) => { - const script = this.scripts.find(s => v.value === s.scriptid); + const script = this.scripts.find((s) => v.value === s.scriptid); this.setState({ selectedScript: v, script, errorMessage: '', loading: false, result: '' }); }; dismiss = () => { this.setState({ selectedScript: null, error: false, errorMessage: '', selectError: '', loading: false }); this.props.onDismiss(); - } + }; submit = () => { const { selectedScript } = this.state; if (!selectedScript) { return this.setState({ - selectError: 'Select a script to execute.' + selectError: 'Select a script to execute.', }); } @@ -85,30 +85,33 @@ export class ExecScriptModalUnthemed extends PureComponent { scriptid: selectedScript.value, }; - this.props.onSubmit(data).then((result: APIExecuteScriptResponse) => { - const message = this.formatResult(result?.value || ''); - if (result?.response === 'success') { - this.setState({ result: message, loading: false }); - } else { - this.setState({ error: true, errorMessage: message, loading: false }); - } - }).catch(err => { - let errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || ''; - errorMessage = this.formatResult(errorMessage); - this.setState({ - error: true, - loading: false, - errorMessage, + this.props + .onSubmit(data) + .then((result: APIExecuteScriptResponse) => { + const message = this.formatResult(result?.value || ''); + if (result?.response === 'success') { + this.setState({ result: message, loading: false }); + } else { + this.setState({ error: true, errorMessage: message, loading: false }); + } + }) + .catch((err) => { + let errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || ''; + errorMessage = this.formatResult(errorMessage); + this.setState({ + error: true, + loading: false, + errorMessage, + }); }); - }); - } + }; formatResult = (result: string) => { const formatted = result.split('\n').map((p, i) => { return

    {p}

    ; }); return <>{formatted}; - } + }; render() { const { theme } = this.props; @@ -135,14 +138,8 @@ export class ExecScriptModalUnthemed extends PureComponent { >
    @@ -150,17 +147,17 @@ export class ExecScriptModalUnthemed extends PureComponent {
    - {result && - {result} - } - {error && - {errorMessage} - } + {result && {result}} + {error && {errorMessage}}
    - - + +
    ); @@ -211,8 +208,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { margin-bottom: 0px; } `, - execResult: css` - `, + execResult: css``, execError: css` color: ${red}; `, diff --git a/src/panel-triggers/components/ProblemColorEditor.tsx b/src/panel-triggers/components/ProblemColorEditor.tsx new file mode 100644 index 0000000..5fe830c --- /dev/null +++ b/src/panel-triggers/components/ProblemColorEditor.tsx @@ -0,0 +1,69 @@ +import React, { FormEvent } from 'react'; +import { + Button, + ColorPicker, + HorizontalGroup, + InlineField, + InlineFieldRow, + InlineLabel, + InlineSwitch, + Input, + VerticalGroup, +} from '@grafana/ui'; +import { StandardEditorProps } from '@grafana/data'; +import { GFHeartIcon } from '../../components'; +import { TriggerSeverity } from '../types'; + +type Props = StandardEditorProps; +export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => { + const onSeverityItemChange = (severity: TriggerSeverity) => { + value.forEach((v, i) => { + if (v.priority === severity.priority) { + value[i] = severity; + } + }); + onChange(value); + }; + + return ( + <> + {value.map((severity, index) => ( + onSeverityItemChange(value)} + /> + ))} + + ); +}; + +interface ProblemColorEditorRowProps { + value: TriggerSeverity; + onChange: (value?: TriggerSeverity) => void; +} + +export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps): JSX.Element => { + const onSeverityNameChange = (v: FormEvent) => { + const newValue = v?.currentTarget?.value; + if (newValue !== null) { + onChange({ ...value, severity: newValue }); + } + }; + + return ( + + + + + + + onChange({ ...value, color })} /> + + + onChange({ ...value, show: !value.show })} /> + + + + ); +}; diff --git a/src/panel-triggers/components/Problems/AckCell.tsx b/src/panel-triggers/components/Problems/AckCell.tsx index 710cf44..156a9ae 100644 --- a/src/panel-triggers/components/Problems/AckCell.tsx +++ b/src/panel-triggers/components/Problems/AckCell.tsx @@ -21,12 +21,12 @@ export const AckCell: React.FC> = (props: RTCell) return (
    - {problem.acknowledges?.length > 0 && + {problem.acknowledges?.length > 0 && ( <> ({problem.acknowledges?.length}) - } + )}
    ); }; diff --git a/src/panel-triggers/components/Problems/AcknowledgesList.tsx b/src/panel-triggers/components/Problems/AcknowledgesList.tsx index 4171360..a0ee965 100644 --- a/src/panel-triggers/components/Problems/AcknowledgesList.tsx +++ b/src/panel-triggers/components/Problems/AcknowledgesList.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ZBXAcknowledge } from '../../types'; +import { ZBXAcknowledge } from '../../../datasource-zabbix/types'; interface AcknowledgesListProps { acknowledges: ZBXAcknowledge[]; @@ -10,13 +10,25 @@ export default function AcknowledgesList(props: AcknowledgesListProps) { return (
    - {acknowledges.map(ack => {ack.time})} + {acknowledges.map((ack) => ( + + {ack.time} + + ))}
    - {acknowledges.map(ack => {ack.user})} + {acknowledges.map((ack) => ( + + {ack.user} + + ))}
    - {acknowledges.map(ack => {ack.message})} + {acknowledges.map((ack) => ( + + {ack.message} + + ))}
    ); diff --git a/src/panel-triggers/components/Problems/ProblemDetails.tsx b/src/panel-triggers/components/Problems/ProblemDetails.tsx index 3898852..7e6ce4a 100644 --- a/src/panel-triggers/components/Problems/ProblemDetails.tsx +++ b/src/panel-triggers/components/Problems/ProblemDetails.tsx @@ -1,24 +1,24 @@ import React, { FC, PureComponent } from 'react'; import moment from 'moment'; -import { TimeRange, DataSourceRef } from "@grafana/data"; +import { TimeRange, DataSourceRef } from '@grafana/data'; import { Tooltip } from '@grafana/ui'; import { getDataSourceSrv } from '@grafana/runtime'; import * as utils from '../../../datasource-zabbix/utils'; import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag } from '../../../datasource-zabbix/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; -import { GFTimeRange, RTRow, ZBXItem } from '../../types'; import { AckModal, AckProblemData } from '../AckModal'; import EventTag from '../EventTag'; import AcknowledgesList from './AcknowledgesList'; import ProblemTimeline from './ProblemTimeline'; import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components'; import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal'; -import ProblemStatusBar from "./ProblemStatusBar"; +import ProblemStatusBar from './ProblemStatusBar'; +import { ZBXItem } from '../../../datasource-zabbix/types'; +import { RTRow } from '../../types'; interface ProblemDetailsProps extends RTRow { rootWidth: number; - timeRange: GFTimeRange; - range: TimeRange; + timeRange: TimeRange; showTimeline?: boolean; panelId?: number; getProblemEvents: (problem: ProblemDTO) => Promise; @@ -65,16 +65,14 @@ export class ProblemDetails extends PureComponent { + this.props.getProblemEvents(problem).then((events) => { this.setState({ events }); }); } fetchProblemAlerts() { const problem = this.props.original; - this.props.getProblemAlerts(problem) - .then(alerts => { + this.props.getProblemAlerts(problem).then((alerts) => { this.setState({ alerts }); }); } @@ -97,15 +95,15 @@ export class ProblemDetails extends PureComponent 1200; - const compactStatusBar = rootWidth < 800 || problem.acknowledges && wideLayout && rootWidth < 1400; + const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400); const age = moment.unix(problem.timestamp).fromNow(true); const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0; const problemSeverity = Number(problem.severity); - let dsName: string = (this.props.original.datasource as string); + let dsName: string = this.props.original.datasource as string; if ((this.props.original.datasource as DataSourceRef)?.uid) { const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid); dsName = dsInstance.name; @@ -117,106 +115,111 @@ export class ProblemDetails extends PureComponent
    - +
    - {problem.showAckButton && -
    - - {({ showModal, hideModal }) => ( - { - showModal(ExecScriptModal, { - getScripts: this.getScripts, - onSubmit: this.onExecuteScript, - onDismiss: hideModal, - }); - }} - /> - )} - - - {({ showModal, hideModal }) => ( - { - showModal(AckModal, { - canClose: problem.manual_close === '1', - severity: problemSeverity, - onSubmit: this.ackProblem, - onDismiss: hideModal, - }); - }} - /> - )} - -
    - } - + {problem.showAckButton && ( +
    + + {({ showModal, hideModal }) => ( + { + showModal(ExecScriptModal, { + getScripts: this.getScripts, + onSubmit: this.onExecuteScript, + onDismiss: hideModal, + }); + }} + /> + )} + + + {({ showModal, hideModal }) => ( + { + showModal(AckModal, { + canClose: problem.manual_close === '1', + severity: problemSeverity, + onSubmit: this.ackProblem, + onDismiss: hideModal, + }); + }} + /> + )} + +
    + )} +
    - + {age}
    - {problem.items && } + {problem.items && }
    - {problem.comments && -
    -
    - - Description:  - - {problem.comments} + {problem.comments && ( +
    +
    + + Description:  + + {problem.comments} +
    +
    + )} + {problem.tags && problem.tags.length > 0 && ( +
    + {problem.tags && + problem.tags.map((tag) => ( + + ))} +
    + )} + {this.props.showTimeline && this.state.events.length > 0 && ( + + )} + {showAcknowledges && !wideLayout && ( +
    +
    + Acknowledges +
    + +
    + )} +
    + {showAcknowledges && wideLayout && ( +
    +
    +
    + Acknowledges +
    +
    - } - {problem.tags && problem.tags.length > 0 && -
    - {problem.tags && problem.tags.map(tag => - ) - } -
    - } - {this.props.showTimeline && this.state.events.length > 0 && - - } - {showAcknowledges && !wideLayout && -
    -
    Acknowledges
    - -
    - } -
    - {showAcknowledges && wideLayout && -
    -
    -
    Acknowledges
    - -
    -
    - } + )}
    - + {dsName}
    - {problem.proxy && -
    - - {problem.proxy} -
    - } - {problem.groups && } - {problem.hosts && } + {problem.proxy && ( +
    + + {problem.proxy} +
    + )} + {problem.groups && } + {problem.hosts && }
    @@ -232,11 +235,17 @@ interface ProblemItemProps { function ProblemItem(props: ProblemItemProps) { const { item, showName } = props; const itemName = utils.expandItemName(item.name, item.key_); - const tooltipContent = () => <>{itemName}
    {item.lastvalue}; + const tooltipContent = () => ( + <> + {itemName} +
    + {item.lastvalue} + + ); return (
    - + {showName && {item.name}: } {item.lastvalue} @@ -252,10 +261,11 @@ interface ProblemItemsProps { const ProblemItems: FC = ({ items }) => { return (
    - {items.length > 1 ? - items.map(item => ) : - - } + {items.length > 1 ? ( + items.map((item) => ) + ) : ( + + )}
    ); }; @@ -267,9 +277,9 @@ interface ProblemGroupsProps { class ProblemGroups extends PureComponent { render() { - return this.props.groups.map(g => ( + return this.props.groups.map((g) => (
    - + {g.name}
    )); @@ -283,9 +293,9 @@ interface ProblemHostsProps { class ProblemHosts extends PureComponent { render() { - return this.props.hosts.map(h => ( + return this.props.hosts.map((h) => (
    - + {h.name}
    )); diff --git a/src/panel-triggers/components/Problems/ProblemStatusBar.tsx b/src/panel-triggers/components/Problems/ProblemStatusBar.tsx index a58fbb7..2703cec 100644 --- a/src/panel-triggers/components/Problems/ProblemStatusBar.tsx +++ b/src/panel-triggers/components/Problems/ProblemStatusBar.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Tooltip } from '@grafana/ui'; import FAIcon from '../../../components/FAIcon/FAIcon'; -import { ZBXTrigger, ZBXAlert } from '../../types'; +import { ZBXAlert, ProblemDTO } from '../../../datasource-zabbix/types'; export interface ProblemStatusBarProps { - problem: ZBXTrigger; + problem: ProblemDTO; alerts?: ZBXAlert[]; className?: string; } @@ -26,7 +26,11 @@ export default function ProblemStatusBar(props: ProblemStatusBarProps) { - + @@ -56,5 +60,11 @@ function ProblemStatusBarItem(props: ProblemStatusBarItemProps) {
    ); } - return link ? {item} : item; + return link ? ( + + {item} + + ) : ( + item + ); } diff --git a/src/panel-triggers/components/Problems/ProblemTimeline.tsx b/src/panel-triggers/components/Problems/ProblemTimeline.tsx index 3270e1a..a71a4ff 100644 --- a/src/panel-triggers/components/Problems/ProblemTimeline.tsx +++ b/src/panel-triggers/components/Problems/ProblemTimeline.tsx @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; import moment from 'moment'; -import { GFTimeRange, ZBXEvent, ZBXAcknowledge } from '../../types'; +import { ZBXEvent, ZBXAcknowledge } from '../../../datasource-zabbix/types'; +import { TimeRange } from '@grafana/data'; const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; @@ -12,7 +13,7 @@ const EVENT_REGION_HEIGHT = Math.round(EVENT_POINT_SIZE * 0.6); export interface ProblemTimelineProps { events: ZBXEvent[]; - timeRange: GFTimeRange; + timeRange: TimeRange; okColor?: string; problemColor?: string; eventRegionHeight?: number; @@ -51,7 +52,7 @@ export default class ProblemTimeline extends PureComponent { + setRootRef = (ref) => { this.rootRef = ref; - const width = ref && ref.clientWidth || 0; + const width = (ref && ref.clientWidth) || 0; this.setState({ width }); - } + }; handlePointHighlight = (index: number, secondIndex?: number) => { const event: ZBXEvent = this.sortedEvents[index]; @@ -80,15 +81,15 @@ export default class ProblemTimeline extends PureComponent { this.setState({ showEventInfo: false, highlightedRegion: null }); - } + }; handleAckHighlight = (ack: ZBXAcknowledge, index: number) => { this.setState({ @@ -96,34 +97,34 @@ export default class ProblemTimeline extends PureComponent { 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)); + const events = _.sortBy(this.props.events, (e) => Number(e.clock)); this.sortedEvents = events; return events; } @@ -137,7 +138,7 @@ export default class ProblemTimeline extends PureComponent Number(ack.clock)); + return _.sortBy(acks, (ack) => Number(ack.clock)); } render() { @@ -156,13 +157,14 @@ export default class ProblemTimeline extends PureComponent - + + /> @@ -265,7 +267,7 @@ class TimelineInfoContainer extends PureComponent { Time:  {tsFormatted} - + , ]; } if (eventInfo && eventInfo.duration) { @@ -285,7 +287,7 @@ class TimelineInfoContainer extends PureComponent { Time:  {tsFormatted} - + , ]; } @@ -296,20 +298,16 @@ class TimelineInfoContainer extends PureComponent { return (
    -
    - {infoItems} -
    -
    - {durationItem} -
    - {eventInfo && eventInfo.message && +
    {infoItems}
    +
    {durationItem}
    + {eventInfo && eventInfo.message && (
    Message:  {eventInfo.message}
    - } + )}
    ); } @@ -317,7 +315,7 @@ class TimelineInfoContainer extends PureComponent { interface TimelineRegionsProps { events: ZBXEvent[]; - timeRange: GFTimeRange; + timeRange: TimeRange; width: number; height: number; okColor?: string; @@ -332,7 +330,8 @@ class TimelineRegions extends PureComponent { render() { const { events, timeRange, width, height, highlightedRegion } = this.props; - const { timeFrom, timeTo } = timeRange; + const timeFrom = timeRange.from.unix(); + const timeTo = timeRange.to.unix(); const range = timeTo - timeFrom; let firstItem: React.ReactNode; @@ -349,9 +348,7 @@ class TimelineRegions extends PureComponent { width: regionWidth, height: height, }; - firstItem = ( - - ); + firstItem = ; } const eventsIntervalItems = events.map((event, index) => { @@ -359,7 +356,7 @@ class TimelineRegions extends PureComponent { 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 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' : ''}`; @@ -370,21 +367,16 @@ class TimelineRegions extends PureComponent { height: height, }; - return ( - - ); + return ; }); - return [ - firstItem, - eventsIntervalItems - ]; + return [firstItem, eventsIntervalItems]; } } interface TimelinePointsProps { events: ZBXEvent[]; - timeRange: GFTimeRange; + timeRange: TimeRange; width: number; pointSize: number; okColor?: string; @@ -415,7 +407,7 @@ class TimelinePoints extends PureComponent () => { let pointsToHighlight = [index]; @@ -429,12 +421,12 @@ class TimelinePoints extends PureComponent () => { + 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 timeFrom = timeRange.from.unix(); + const timeTo = timeRange.to.unix(); 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 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; @@ -491,7 +484,7 @@ class TimelinePoints extends PureComponent eventsItems[i]); + return this.state.order.map((i) => eventsItems[i]); } return eventsItems; } @@ -528,13 +521,13 @@ class TimelinePoint extends PureComponent { if (this.props.onPointUnHighlight) { this.props.onPointUnHighlight(); } - } + }; render() { const { x } = this.props; @@ -543,11 +536,13 @@ class TimelinePoint extends PureComponent + onMouseLeave={this.handleMouseLeave} + > @@ -557,7 +552,7 @@ class TimelinePoint extends PureComponent void; @@ -581,7 +576,7 @@ class TimelineAcks extends PureComponent { this.props.onHighlight(ack, index); } this.bringToFront(index, true); - } + }; handleUnHighlight = () => { if (this.props.onUnHighlight) { @@ -589,7 +584,7 @@ class TimelineAcks extends PureComponent { } const order = this.props.acknowledges.map((v, i) => i); this.setState({ order, highlighted: null }); - } + }; bringToFront = (index: number, highlight = false) => { const { acknowledges } = this.props; @@ -597,16 +592,17 @@ class TimelineAcks extends PureComponent { 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 timeFrom = timeRange.from.unix(); + const timeTo = timeRange.to.unix(); 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 posLeft = Math.round(((ts - timeFrom) / range) * width - pointR); const highlighted = this.state.highlighted === i; return ( @@ -621,7 +617,7 @@ class TimelineAcks extends PureComponent { ); }); if (this.state.order.length) { - return this.state.order.map(i => eventsItems[i]); + return this.state.order.map((i) => eventsItems[i]); } return eventsItems; } @@ -656,13 +652,13 @@ class TimelineAck extends PureComponent { if (this.props.onHighlight) { this.props.onHighlight(); } - } + }; handleUnHighlight = () => { if (this.props.onUnHighlight) { this.props.onUnHighlight(); } - } + }; render() { const { x } = this.props; @@ -671,11 +667,13 @@ class TimelineAck extends PureComponent { const rInner = Math.round(r * INNER_POINT_SIZE); const className = `problem-event-ack ${this.state.highlighted ? 'highlighted' : ''}`; return ( - + onMouseLeave={this.handleUnHighlight} + > diff --git a/src/panel-triggers/components/Problems/Problems.tsx b/src/panel-triggers/components/Problems/Problems.tsx index 1151664..09f8889 100644 --- a/src/panel-triggers/components/Problems/Problems.tsx +++ b/src/panel-triggers/components/Problems/Problems.tsx @@ -8,17 +8,17 @@ import EventTag from '../EventTag'; import { ProblemDetails } from './ProblemDetails'; import { AckProblemData } from '../AckModal'; import { FAIcon, GFHeartIcon } from '../../../components'; -import { GFTimeRange, ProblemsPanelOptions, RTCell, RTResized, TriggerSeverity } from '../../types'; +import { ProblemsPanelOptions, RTCell, RTResized, TriggerSeverity } from '../../types'; import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource-zabbix/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { AckCell } from './AckCell'; -import { DataSourceRef, TimeRange } from "@grafana/data"; +import { DataSourceRef, TimeRange } from '@grafana/data'; export interface ProblemListProps { problems: ProblemDTO[]; panelOptions: ProblemsPanelOptions; loading?: boolean; - timeRange?: GFTimeRange; + timeRange?: TimeRange; range?: TimeRange; pageSize?: number; fontSize?: number; @@ -52,7 +52,7 @@ export default class ProblemList extends PureComponent { + setRootRef = (ref) => { this.rootRef = ref; }; @@ -60,8 +60,7 @@ export default class ProblemList extends PureComponent { - }; + onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => {}; handlePageSizeChange = (pageSize, pageIndex) => { if (this.props.onPageSizeChange) { @@ -132,10 +131,12 @@ export default class ProblemList extends PureComponent StatusCell(props, highlightNewerThan); - const statusIconCell = props => StatusIconCell(props, highlightNewerThan); - const hostNameCell = props => ; - const hostTechNameCell = props => ; + const statusCell = (props) => StatusCell(props, highlightNewerThan); + const statusIconCell = (props) => StatusIconCell(props, highlightNewerThan); + const hostNameCell = (props) => ; + const hostTechNameCell = (props) => ( + + ); const columns = [ { Header: 'Host', id: 'host', show: options.hostField, Cell: hostNameCell }, @@ -143,35 +144,62 @@ export default class ProblemList extends PureComponent problem.priority, + 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, options.okEventColor), + Cell: (props) => + SeverityCell( + props, + options.triggerSeverity, + options.markAckEvents, + options.ackEventColor, + options.okEventColor + ), }, { - Header: '', id: 'statusIcon', show: options.statusIcon, className: 'problem-status-icon', width: 50, + 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: 'Ack', id: 'ack', show: options.ackField, width: 70, - Cell: props => + Header: 'Ack', + id: 'ack', + show: options.ackField, + width: 70, + Cell: (props) => , }, { - Header: 'Tags', accessor: 'tags', show: options.showTags, className: 'problem-tags', - Cell: props => + Header: 'Tags', + accessor: 'tags', + show: options.showTags, + className: 'problem-tags', + Cell: (props) => , }, { - Header: 'Age', className: 'problem-age', width: 100, show: options.ageField, accessor: 'timestamp', + Header: 'Age', + className: 'problem-age', + width: 100, + show: options.ageField, + accessor: 'timestamp', id: 'age', Cell: AgeCell, }, { - Header: 'Time', className: 'last-change', width: 150, accessor: 'timestamp', + Header: 'Time', + className: 'last-change', + width: 150, + accessor: 'timestamp', id: 'lastchange', - Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat), + Cell: (props) => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat), }, { Header: '', className: 'custom-expander', width: 60, expander: true, Expander: CustomExpander }, ]; @@ -207,25 +235,25 @@ export default class ProblemList extends PureComponent - ( + - } + )} expanded={this.getExpandedPage(this.state.page)} onExpandedChange={this.handleExpandedChange} - onPageChange={page => this.setState({ page })} + onPageChange={(page) => this.setState({ page })} onPageSizeChange={this.handlePageSizeChange} onResizedChange={this.handleResizedChange} /> @@ -243,7 +271,7 @@ const HostCell: React.FC = ({ name, maintenance }) => { return (
    {name} - {maintenance && } + {maintenance && }
    ); }; @@ -260,15 +288,15 @@ function SeverityCell( let severityDesc: TriggerSeverity; const severity = Number(problem.severity); - severityDesc = _.find(problemSeverityDesc, s => s.priority === severity); + severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severity); if (problem.severity && problem.value === '1') { - severityDesc = _.find(problemSeverityDesc, s => s.priority === severity); + severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severity); } color = problem.value === '0' ? okColor : severityDesc.color; // Mark acknowledged triggers with different color - if (markAckEvents && problem.acknowledged === "1") { + if (markAckEvents && problem.acknowledged === '1') { color = ackEventColor; } @@ -290,7 +318,9 @@ function StatusCell(props: RTCell, highlightNewerThan?: string) { newProblem = isNewProblem(props.original, highlightNewerThan); } return ( - {status} + + {status} + ); } @@ -300,22 +330,21 @@ function StatusIconCell(props: RTCell, highlightNewerThan?: string) if (highlightNewerThan) { newProblem = isNewProblem(props.original, highlightNewerThan); } - const className = classNames('zbx-problem-status-icon', + const className = classNames( + 'zbx-problem-status-icon', { 'problem-status--new': newProblem }, { 'zbx-problem': props.value === '1' }, - { 'zbx-ok': props.value === '0' }, + { 'zbx-ok': props.value === '0' } ); - return ; + return ; } function GroupCell(props: RTCell) { - let groups = ""; + let groups = ''; if (props.value && props.value.length) { - groups = props.value.map(g => g.name).join(', '); + groups = props.value.map((g) => g.name).join(', '); } - return ( - {groups} - ); + return {groups}; } function ProblemCell(props: RTCell) { @@ -336,7 +365,7 @@ function AgeCell(props: RTCell) { } function LastChangeCell(props: RTCell, customFormat?: string) { - const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; + const DEFAULT_TIME_FORMAT = 'DD MMM YYYY HH:mm:ss'; const problem = props.original; const timestamp = moment.unix(problem.timestamp); const format = customFormat || DEFAULT_TIME_FORMAT; @@ -358,14 +387,21 @@ class TagCell extends PureComponent { render() { const tags = this.props.value || []; return [ - tags.map(tag => ) + tags.map((tag) => ( + + )), ]; } } function CustomExpander(props: RTCell) { return ( - + ); diff --git a/src/panel-triggers/components/ResetColumnsEditor.tsx b/src/panel-triggers/components/ResetColumnsEditor.tsx new file mode 100644 index 0000000..e4534c0 --- /dev/null +++ b/src/panel-triggers/components/ResetColumnsEditor.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Button } from '@grafana/ui'; +import { StandardEditorProps } from '@grafana/data'; + +type Props = StandardEditorProps; +export const ResetColumnsEditor = ({ onChange }: Props): JSX.Element => { + return ( + + ); +}; diff --git a/src/panel-triggers/migrations.ts b/src/panel-triggers/migrations.ts index 054919a..fc052de 100644 --- a/src/panel-triggers/migrations.ts +++ b/src/panel-triggers/migrations.ts @@ -1,18 +1,20 @@ import _ from 'lodash'; import { getNextRefIdChar } from './utils'; import { ShowProblemTypes } from '../datasource-zabbix/types'; +import { ProblemsPanelOptions } from './types'; +import { PanelModel } from '@grafana/data'; // Actual schema version export const CURRENT_SCHEMA_VERSION = 8; export const getDefaultTarget = (targets?) => { return { - group: {filter: ""}, - host: {filter: ""}, - application: {filter: ""}, - trigger: {filter: ""}, - tags: {filter: ""}, - proxy: {filter: ""}, + group: { filter: '' }, + host: { filter: '' }, + application: { filter: '' }, + trigger: { filter: '' }, + tags: { filter: '' }, + proxy: { filter: '' }, refId: getNextRefIdChar(targets), }; }; @@ -105,7 +107,7 @@ export function migratePanelSchema(panel) { target.options = migrateOptions(panel); _.defaults(target.options, getDefaultTargetOptions()); - _.defaults(target, { tags: { filter: "" } }); + _.defaults(target, { tags: { filter: '' } }); } panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange'; @@ -161,7 +163,7 @@ function isEmptyPanel(panel) { } function isEmptyTargets(targets) { - return !targets || (_.isArray(targets) && (targets.length === 0 || targets.length === 1 && _.isEmpty(targets[0]))); + return !targets || (_.isArray(targets) && (targets.length === 0 || (targets.length === 1 && _.isEmpty(targets[0])))); } function isDefaultPanel(panel) { @@ -169,7 +171,13 @@ function isDefaultPanel(panel) { } function isDefaultTarget(target) { - return !target.group?.filter && !target.host?.filter && !target.application?.filter && !target.trigger?.filter && !target.queryType; + return ( + !target.group?.filter && + !target.host?.filter && + !target.application?.filter && + !target.trigger?.filter && + !target.queryType + ); } function isEmptyTarget(target) { @@ -179,3 +187,53 @@ function isEmptyTarget(target) { function isInvalidTarget(target, targetKey) { return target && target.refId === 'A' && targetKey === '0'; } + +// This is called when the panel changes from another panel +export const problemsPanelMigrationHandler = (panel: PanelModel> | any) => { + let options = (panel.options ?? {}) as ProblemsPanelOptions; + const legacyOptions: Partial = { + layout: panel.layout, + hostField: panel.hostField, + hostTechNameField: panel.hostTechNameField, + hostGroups: panel.hostGroups, + hostProxy: panel.hostProxy, + showTags: panel.showTags, + statusField: panel.statusField, + statusIcon: panel.statusIcon, + severityField: panel.severityField, + ackField: panel.ackField, + ageField: panel.ageField, + descriptionField: panel.descriptionField, + descriptionAtNewLine: panel.descriptionAtNewLine, + hostsInMaintenance: panel.hostsInMaintenance, + showTriggers: panel.showTriggers, + sortProblems: panel.sortProblems, + limit: panel.limit, + fontSize: panel.fontSize, + pageSize: panel.pageSize, + problemTimeline: panel.problemTimeline, + highlightBackground: panel.highlightBackground, + highlightNewEvents: panel.highlightNewEvents, + highlightNewerThan: panel.highlightNewerThan, + customLastChangeFormat: panel.customLastChangeFormat, + lastChangeFormat: panel.lastChangeFormat, + resizedColumns: panel.resizedColumns, + triggerSeverity: panel.triggerSeverity, + okEventColor: panel.okEventColor, + ackEventColor: panel.ackEventColor, + markAckEvents: panel.markAckEvents, + showEvents: panel.showEvents?.value ?? panel.showEvents, + }; + + return { ...legacyOptions, ...options }; +}; + +// This is called when the panel changes from another panel +export const problemsPanelChangedHandler = ( + panel: PanelModel> | any, + prevPluginId: string, + prevOptions: any +) => { + let options = (panel.options ?? {}) as ProblemsPanelOptions; + return options; +}; diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js deleted file mode 100644 index a6f7f15..0000000 --- a/src/panel-triggers/module.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Grafana-Zabbix - * Zabbix plugin for Grafana. - * http://github.com/alexanderzobnin/grafana-zabbix - * - * Trigger panel. - * This feature sponsored by CORE IT - * http://www.coreit.fr - * - * Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com - * Licensed under the Apache License, Version 2.0 - */ - -import { TriggerPanelCtrl } from './triggers_panel_ctrl'; -import { loadPluginCss } from 'grafana/app/plugins/sdk'; - -loadPluginCss({ - dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', - light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css' -}); - -export { - TriggerPanelCtrl as PanelCtrl -}; diff --git a/src/panel-triggers/module.tsx b/src/panel-triggers/module.tsx new file mode 100644 index 0000000..6b01fa6 --- /dev/null +++ b/src/panel-triggers/module.tsx @@ -0,0 +1,214 @@ +import { PanelPlugin, StandardEditorProps } from '@grafana/data'; +import { problemsPanelChangedHandler, problemsPanelMigrationHandler } from './migrations'; +import { ProblemsPanel } from './ProblemsPanel'; +import { defaultPanelOptions, ProblemsPanelOptions } from './types'; +import { ResetColumnsEditor } from './components/ResetColumnsEditor'; +import { ProblemColorEditor } from './components/ProblemColorEditor'; + +export const plugin = new PanelPlugin(ProblemsPanel) + .setPanelChangeHandler(problemsPanelChangedHandler) + .setMigrationHandler(problemsPanelMigrationHandler) + .setPanelOptions((builder) => { + builder + .addSelect({ + path: 'layout', + name: 'Layout', + defaultValue: defaultPanelOptions.layout, + settings: { + options: [ + { label: 'Table', value: 'table' }, + { label: 'List', value: 'list' }, + ], + }, + }) + .addSelect({ + path: 'sortProblems', + name: 'Sort by', + defaultValue: defaultPanelOptions.sortProblems, + settings: { + options: [ + { label: 'Default', value: 'default' }, + { label: 'Last change', value: 'lastchange' }, + { label: 'Severity', value: 'priority' }, + ], + }, + }) + .addSelect({ + path: 'fontSize', + name: 'Font size', + defaultValue: defaultPanelOptions.fontSize, + settings: { + options: fontSizeOptions, + }, + }) + .addNumberInput({ + path: 'pageSize', + name: 'Page size', + defaultValue: defaultPanelOptions.pageSize, + }) + .addBooleanSwitch({ + path: 'problemTimeline', + name: 'Problem timeline', + defaultValue: defaultPanelOptions.problemTimeline, + showIf: (options) => options.layout === 'table', + }) + .addBooleanSwitch({ + path: 'highlightBackground', + name: 'Highlight background', + defaultValue: defaultPanelOptions.highlightBackground, + showIf: (options) => options.layout === 'list', + }) + .addBooleanSwitch({ + path: 'highlightNewEvents', + name: 'Highlight new events', + defaultValue: defaultPanelOptions.highlightNewEvents, + }) + .addTextInput({ + path: 'highlightNewerThan', + name: 'Newer than', + defaultValue: defaultPanelOptions.highlightNewerThan, + showIf: (options) => options.highlightNewEvents, + }) + .addBooleanSwitch({ + path: 'customLastChangeFormat', + name: 'Custom last change format', + defaultValue: defaultPanelOptions.customLastChangeFormat, + }) + .addTextInput({ + path: 'lastChangeFormat', + name: 'Last change format', + defaultValue: defaultPanelOptions.lastChangeFormat, + description: 'See moment.js dosc for time format http://momentjs.com/docs/#/displaying/format/', + settings: { + placeholder: 'dddd, MMMM Do YYYY, h:mm:ss a', + }, + showIf: (options) => options.customLastChangeFormat, + }) + .addCustomEditor({ + id: 'resetColumns', + path: 'resizedColumns', + name: 'Reset resized columns', + editor: ResetColumnsEditor, + showIf: (options) => options.layout === 'table', + }) + .addCustomEditor({ + id: 'triggerColors', + path: 'triggerSeverity', + name: 'Problem colors', + editor: ProblemColorEditor, + category: ['Colors'], + }) + .addBooleanSwitch({ + path: 'markAckEvents', + name: 'Mark acknowledged events', + defaultValue: defaultPanelOptions.markAckEvents, + category: ['Colors'], + }) + .addColorPicker({ + path: 'ackEventColor', + name: 'Acknowledged color', + defaultValue: defaultPanelOptions.ackEventColor, + showIf: (options) => options.markAckEvents, + // enableNamedColors does not work now + settings: [{ enableNamedColors: false }], + category: ['Colors'], + }) + .addColorPicker({ + path: 'okEventColor', + name: 'OK event color', + defaultValue: defaultPanelOptions.okEventColor, + settings: [{ enableNamedColors: false }], + category: ['Colors'], + }) + + // Show/hide fields + .addBooleanSwitch({ + path: 'hostField', + name: 'Host name', + defaultValue: defaultPanelOptions.hostField, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'hostTechNameField', + name: 'Technical name', + defaultValue: defaultPanelOptions.hostTechNameField, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'hostGroups', + name: 'Host groups', + defaultValue: defaultPanelOptions.hostGroups, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'hostProxy', + name: 'Host proxy', + defaultValue: defaultPanelOptions.hostProxy, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'showTags', + name: 'Tags', + defaultValue: defaultPanelOptions.showTags, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'statusField', + name: 'Status', + defaultValue: defaultPanelOptions.statusField, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'statusIcon', + name: 'Status icon', + defaultValue: defaultPanelOptions.statusIcon, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'severityField', + name: 'Severity', + defaultValue: defaultPanelOptions.severityField, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'ackField', + name: 'Ack', + defaultValue: defaultPanelOptions.ackField, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'ageField', + name: 'Age', + defaultValue: defaultPanelOptions.ageField, + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'descriptionField', + name: 'Description', + defaultValue: defaultPanelOptions.descriptionField, + showIf: (options) => options.layout === 'list', + category: ['Fields'], + }) + .addBooleanSwitch({ + path: 'descriptionAtNewLine', + name: 'At the new line', + defaultValue: defaultPanelOptions.descriptionAtNewLine, + showIf: (options) => options.layout === 'list', + category: ['Fields'], + }); + }); + +const fontSizeOptions = [ + { label: '80%', value: '80%' }, + { label: '90%', value: '90%' }, + { label: '100%', value: '100%' }, + { label: '110%', value: '110%' }, + { label: '120%', value: '120%' }, + { label: '130%', value: '130%' }, + { label: '150%', value: '150%' }, + { label: '160%', value: '160%' }, + { label: '180%', value: '180%' }, + { label: '200%', value: '200%' }, + { label: '220%', value: '220%' }, + { label: '250%', value: '250%' }, +]; diff --git a/src/panel-triggers/options_tab.js b/src/panel-triggers/options_tab.js deleted file mode 100644 index c67521f..0000000 --- a/src/panel-triggers/options_tab.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Grafana-Zabbix - * Zabbix plugin for Grafana. - * http://github.com/alexanderzobnin/grafana-zabbix - * - * Trigger panel. - * This feature sponsored by CORE IT - * http://www.coreit.fr - * - * Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com - * Licensed under the Apache License, Version 2.0 - */ - -class TriggerPanelOptionsCtrl { - - /** @ngInject */ - constructor($scope) { - $scope.editor = this; - this.panelCtrl = $scope.ctrl; - this.panel = this.panelCtrl.panel; - - this.layouts = [ - { text: 'Table', value: 'table' }, - { text: 'List', value: 'list' } - ]; - this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%']; - this.ackFilters = [ - 'all triggers', - 'unacknowledged', - 'acknowledged' - ]; - - this.sortingOptions = [ - { text: 'Default', value: 'default' }, - { text: 'Last change', value: 'lastchange' }, - { text: 'Severity', value: 'priority' }, - ]; - - this.showEventsFields = [ - { text: 'All', value: [0,1] }, - { text: 'OK', value: [0] }, - { text: 'Problems', value: 1 } - ]; - } -} - -export function triggerPanelOptionsTab() { - return { - restrict: 'E', - scope: true, - templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/options_tab.html', - controller: TriggerPanelOptionsCtrl, - }; -} diff --git a/src/panel-triggers/partials/module.html b/src/panel-triggers/partials/module.html deleted file mode 100644 index 0a4718e..0000000 --- a/src/panel-triggers/partials/module.html +++ /dev/null @@ -1 +0,0 @@ -
    diff --git a/src/panel-triggers/partials/options_tab.html b/src/panel-triggers/partials/options_tab.html deleted file mode 100644 index 378bb08..0000000 --- a/src/panel-triggers/partials/options_tab.html +++ /dev/null @@ -1,237 +0,0 @@ -
    -
    -
    Fields
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    -
    View options
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - - -
    - - - - - - -
    - - -
    - - - -
    -
    - -
    -
    -
    - -
    -
    Problems severity and colors
    -
    -
    - - - - - - -
    - - -
    - -
    -
    - - - - - - -
    - - -
    -
    -
    - - - - - - -
    -
    -
    -
    diff --git a/src/panel-triggers/specs/matchMedia.mock b/src/panel-triggers/specs/matchMedia.mock deleted file mode 100644 index 0580f98..0000000 --- a/src/panel-triggers/specs/matchMedia.mock +++ /dev/null @@ -1,13 +0,0 @@ -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // deprecated - removeListener: jest.fn(), // deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}); diff --git a/src/panel-triggers/specs/migrations.spec.ts b/src/panel-triggers/specs/migrations.spec.ts deleted file mode 100644 index 9e5d42f..0000000 --- a/src/panel-triggers/specs/migrations.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import _ from 'lodash'; -import './matchMedia.mock'; -import { DEFAULT_SEVERITY, DEFAULT_TARGET, PANEL_DEFAULTS, TriggerPanelCtrl } from '../triggers_panel_ctrl'; -import { CURRENT_SCHEMA_VERSION } from '../migrations'; - -jest.mock('@grafana/runtime', () => { - return { - getDataSourceSrv: () => ({ - getMetricSources: () => { - return [{ meta: { id: 'alexanderzobnin-zabbix-datasource' }, value: {}, name: 'zabbix_default' }]; - }, - get: () => Promise.resolve({}) - }), - }; -}, { virtual: true }); - -describe('Triggers Panel schema migration', () => { - let ctx: any = {}; - let updatePanelCtrl; - - const timeoutMock = () => { - }; - - beforeEach(() => { - ctx = { - scope: { - panel: { - datasource: 'zabbix', - triggers: DEFAULT_TARGET, - hostField: true, - statusField: false, - severityField: false, - lastChangeField: true, - ageField: true, - infoField: true, - limit: 10, - showTriggers: 'unacknowledged', - hideHostsInMaintenance: false, - hostsInMaintenance: false, - sortTriggersBy: { text: 'last change', value: 'lastchange' }, - showEvents: { text: 'Problems', value: '1' }, - triggerSeverity: DEFAULT_SEVERITY, - okEventColor: 'rgba(0, 245, 153, 0.45)', - ackEventColor: 'rgba(0, 0, 0, 0)', - scroll: true, - pageSize: 10, - fontSize: '100%', - } - } - }; - - updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock); - }); - - it('should update old panel schema', () => { - const updatedPanelCtrl = updatePanelCtrl(ctx.scope); - - const expected = _.defaultsDeep({ - schemaVersion: CURRENT_SCHEMA_VERSION, - datasource: 'zabbix', - targets: [ - { - ...DEFAULT_TARGET, - queryType: 5, - showProblems: 'problems', - options: { - hostsInMaintenance: false, - acknowledged: 0, - sortProblems: 'default', - minSeverity: 0, - limit: 10, - }, - } - ], - sortProblems: 'lastchange', - ageField: true, - statusField: false, - severityField: false, - limit: 10, - okEventColor: 'rgba(0, 245, 153, 0.45)', - ackEventColor: 'rgba(0, 0, 0, 0)' - }, PANEL_DEFAULTS); - - expect(updatedPanelCtrl.panel).toEqual(expected); - }); - - it('should create new panel with default schema', () => { - ctx.scope.panel = {}; - const updatedPanelCtrl = updatePanelCtrl(ctx.scope); - - const expected = _.defaultsDeep({ - schemaVersion: CURRENT_SCHEMA_VERSION, - }, PANEL_DEFAULTS); - expect(updatedPanelCtrl.panel).toEqual(expected); - }); -}); diff --git a/src/panel-triggers/specs/panel_ctrl.spec.ts b/src/panel-triggers/specs/panel_ctrl.spec.ts deleted file mode 100644 index 8a67eaa..0000000 --- a/src/panel-triggers/specs/panel_ctrl.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import _ from 'lodash'; -import './matchMedia.mock'; -import { DEFAULT_TARGET, PANEL_DEFAULTS, TriggerPanelCtrl } from '../triggers_panel_ctrl'; - -let datasourceSrvMock, zabbixDSMock; - -jest.mock('@grafana/runtime', () => { - return { - getDataSourceSrv: () => datasourceSrvMock, - }; -}, { virtual: true }); - -describe('TriggerPanelCtrl', () => { - let ctx: any = {}; - let createPanelCtrl: () => any; - - beforeEach(() => { - ctx = { - scope: { - panel: { - ...PANEL_DEFAULTS, - sortProblems: 'lastchange', - } - } - }; - ctx.scope.panel.targets = [{ - ...DEFAULT_TARGET, - datasource: 'zabbix_default', - }]; - - zabbixDSMock = { - zabbix: { - getExtendedEventData: jest.fn().mockResolvedValue([]), - getEventAlerts: jest.fn().mockResolvedValue([]), - } - }; - - datasourceSrvMock = { - get: () => Promise.resolve(zabbixDSMock) - }; - - const timeoutMock = (fn: () => any) => Promise.resolve(fn()); - createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock); - - ctx.panelCtrl = createPanelCtrl(); - - ctx.dataFramesReceived = generateDataFramesResponse([ - { id: "1", timestamp: "1510000010", severity: 5 }, - { id: "2", timestamp: "1510000040", severity: 3 }, - { id: "3", timestamp: "1510000020", severity: 4 }, - { id: "4", timestamp: "1510000030", severity: 2 }, - ]); - }); - - describe('When refreshing panel', () => { - beforeEach(() => { - ctx.scope.panel.datasources = ['zabbix_default', 'zabbix']; - ctx.scope.panel.targets = [ - { - ...DEFAULT_TARGET, - datasource: 'zabbix_default' - }, - { - ...DEFAULT_TARGET, - datasource: 'zabbix' - }, - ]; - ctx.panelCtrl = createPanelCtrl(); - }); - - it('should format triggers', (done) => { - ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { - const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, { triggerid: "1" }); - expect(formattedTrigger.host).toBe('backend01'); - expect(formattedTrigger.hostTechName).toBe('backend01_tech'); - expect(formattedTrigger.datasource).toBe('zabbix_default'); - expect(formattedTrigger.maintenance).toBe(true); - expect(formattedTrigger.lastchange).toBeTruthy(); - done(); - }); - }); - - it('should sort triggers by time by default', (done) => { - ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { - const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid'); - expect(trigger_ids).toEqual([ - '2', '4', '3', '1' - ]); - done(); - }); - }); - - it('should sort triggers by severity', (done) => { - ctx.panelCtrl.panel.sortProblems = 'priority'; - ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => { - const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid'); - expect(trigger_ids).toEqual([ - '1', '3', '2', '4' - ]); - done(); - }); - }); - }); -}); - -const defaultProblem: any = { - "acknowledges": [], - "comments": "It probably means that the systems requires\nmore physical memory.", - "correlation_mode": "0", - "correlation_tag": "", - "datasource": "zabbix_default", - "description": "Lack of free swap space on server", - "error": "", - "expression": "{13297}>20", - "flags": "0", - "groups": [ - { - "groupid": "2", - "name": "Linux servers" - }, - { - "groupid": "9", - "name": "Backend" - } - ], - "hosts": [ - { - "host": "backend01_tech", - "hostid": "10111", - "maintenance_status": "1", - "name": "backend01", - "proxy_hostid": "0" - } - ], - "items": [ - { - "itemid": "23979", - "key_": "system.cpu.util[,iowait]", - "lastvalue": "25.2091", - "name": "CPU $2 time" - } - ], - "lastEvent": { - "acknowledged": "0", - "clock": "1589297010", - "eventid": "4399289", - "name": "Disk I/O is overloaded on backend01", - "ns": "224779201", - "object": "0", - "objectid": "13682", - "severity": "2", - "source": "0", - "value": "1" - }, - "lastchange": "1440259530", - "maintenance": true, - "manual_close": "0", - "severity": "2", - "recovery_expression": "", - "recovery_mode": "0", - "showAckButton": true, - "state": "0", - "status": "0", - "tags": [], - "templateid": "13671", - "triggerid": "13682", - "type": "0", - "url": "", - "value": "1" -}; - -function generateDataFramesResponse(problemDescs: any[] = [{ id: 1 }]): any { - const problems = problemDescs.map(problem => generateProblem(problem.id, problem.timestamp, problem.severity)); - - return [ - { - "fields": [ - { - "config": {}, - "name": "Problems", - "state": { - "scopedVars": {}, - "title": null - }, - "type": "other", - "values": problems, - } - ], - "length": 16, - "name": "problems" - } - ]; -} - -function generateProblem(id, timestamp?, severity?): any { - const problem = _.cloneDeep(defaultProblem); - problem.triggerid = id.toString(); - problem.eventid = id.toString(); - if (severity) { - problem.severity = severity.toString(); - } - if (timestamp) { - problem.lastchange = timestamp; - problem.timestamp = timestamp; - } - return problem; -} - -function getProblemById(id, ctx): any { - return _.find(ctx.panelCtrl.renderData, { triggerid: id.toString() }); -} diff --git a/src/panel-triggers/triggers_panel_ctrl.ts b/src/panel-triggers/triggers_panel_ctrl.ts deleted file mode 100644 index 3a325a2..0000000 --- a/src/panel-triggers/triggers_panel_ctrl.ts +++ /dev/null @@ -1,456 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import _ from 'lodash'; -import { getDataSourceSrv } from '@grafana/runtime'; -import { PanelEvents } from '@grafana/data'; -import * as dateMath from 'grafana/app/core/utils/datemath'; -import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; -import { triggerPanelOptionsTab } from './options_tab'; -import { CURRENT_SCHEMA_VERSION, migratePanelSchema } from './migrations'; -import ProblemList from './components/Problems/Problems'; -import AlertList from './components/AlertList/AlertList'; -import { ProblemDTO } from 'datasource-zabbix/types'; - -const PROBLEM_EVENTS_LIMIT = 100; - -export const DEFAULT_TARGET = { - group: { filter: "" }, - host: { filter: "" }, - application: { filter: "" }, - trigger: { filter: "" }, - tags: { filter: "" }, - proxy: { filter: "" }, - showProblems: 'problems', -}; - -export const DEFAULT_SEVERITY = [ - { priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true }, - { priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true }, - { priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true }, - { priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true }, - { priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true }, - { priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true }, -]; - -export const getDefaultSeverity = () => DEFAULT_SEVERITY; - -const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss"; - -export const PANEL_DEFAULTS = { - schemaVersion: CURRENT_SCHEMA_VERSION, - targets: [{}], - // Fields - hostField: true, - hostTechNameField: false, - hostProxy: false, - hostGroups: false, - showTags: true, - statusField: true, - statusIcon: false, - severityField: true, - ackField: true, - ageField: false, - descriptionField: true, - descriptionAtNewLine: false, - // Options - sortProblems: 'lastchange', - limit: null, - // View options - layout: 'table', - fontSize: '100%', - pageSize: 10, - problemTimeline: true, - highlightBackground: false, - highlightNewEvents: false, - highlightNewerThan: '1h', - customLastChangeFormat: false, - lastChangeFormat: "", - resizedColumns: [], - // Triggers severity and colors - triggerSeverity: getDefaultSeverity(), - okEventColor: 'rgb(56, 189, 113)', - ackEventColor: 'rgb(56, 219, 156)', - markAckEvents: false, -}; - -const triggerStatusMap = { - '0': 'OK', - '1': 'PROBLEM' -}; - -export class TriggerPanelCtrl extends MetricsPanelCtrl { - scope: any; - useDataFrames: boolean; - triggerStatusMap: any; - defaultTimeFormat: string; - pageIndex: number; - renderData: any[]; - problems: any[]; - contextSrv: any; - static templateUrl: string; - - /** @ngInject */ - constructor($scope, $injector, $timeout) { - super($scope, $injector); - this.scope = $scope; - this.$timeout = $timeout; - - // Tell Grafana do not convert data frames to table or series - this.useDataFrames = true; - - this.triggerStatusMap = triggerStatusMap; - this.defaultTimeFormat = DEFAULT_TIME_FORMAT; - this.pageIndex = 0; - this.range = {}; - this.renderData = []; - - this.panel = migratePanelSchema(this.panel); - _.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS)); - - // this.events.on(PanelEvents.render, this.onRender.bind(this)); - this.events.on('data-frames-received', this.onDataFramesReceived.bind(this)); - this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this)); - this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this)); - } - - onInitEditMode() { - // Update schema version to prevent migration on up-to-date targets - this.panel.schemaVersion = CURRENT_SCHEMA_VERSION; - this.addEditorTab('Options', triggerPanelOptionsTab); - } - - onDataFramesReceived(data: any): Promise { - this.range = this.timeSrv.timeRange(); - let problems = []; - - if (data && data.length) { - for (const dataFrame of data) { - try { - const values = dataFrame.fields[0].values; - if (values.toArray) { - problems.push(values.toArray()); - } else if (values.length > 0) { - // On snapshot mode values is a plain Array, not ArrayVector - problems.push(values); - } - } catch (error) { - console.log(error); - return Promise.reject(error); - } - } - } - - this.loading = false; - problems = _.flatten(problems); - this.problems = problems; - return this.renderProblems(problems); - } - - onDataSnapshotLoad(snapshotData) { - return this.onDataFramesReceived(snapshotData); - } - - reRenderProblems() { - if (this.problems) { - this.renderProblems(this.problems); - } - } - - setPanelError(err, defaultError = "Request Error") { - this.inspector = { error: err }; - this.error = err.message || defaultError; - if (err.data) { - if (err.data.message) { - this.error = err.data.message; - } - if (err.data.error) { - this.error = err.data.error; - } - } - - // this.events.emit(PanelEvents.dataError, err); - console.log('Panel data error:', err); - } - - renderProblems(problems) { - let triggers = _.cloneDeep(problems); - - triggers = _.map(triggers, this.formatTrigger.bind(this)); - triggers = this.filterProblems(triggers); - triggers = this.sortTriggers(triggers); - - this.renderData = triggers; - - return this.$timeout(() => { - return super.render(triggers); - }); - } - - filterProblems(problems) { - let problemsList = _.cloneDeep(problems); - - // Filter acknowledged triggers - if (this.panel.showTriggers === 'unacknowledged') { - problemsList = _.filter(problemsList, trigger => { - return !(trigger.acknowledges && trigger.acknowledges.length); - }); - } else if (this.panel.showTriggers === 'acknowledged') { - problemsList = _.filter(problemsList, trigger => { - return trigger.acknowledges && trigger.acknowledges.length; - }); - } - - // Filter triggers by severity - problemsList = _.filter(problemsList, problem => { - if (problem.severity) { - return this.panel.triggerSeverity[problem.severity].show; - } else { - return this.panel.triggerSeverity[problem.priority].show; - } - }); - - return problemsList; - } - - sortTriggers(problems: ProblemDTO[]) { - if (this.panel.sortProblems === 'priority') { - problems = _.orderBy(problems, ['severity', 'timestamp', 'eventid'], ['desc', 'desc', 'desc']); - } else if (this.panel.sortProblems === 'lastchange') { - problems = _.orderBy(problems, ['timestamp', 'severity', 'eventid'], ['desc', 'desc', 'desc']); - } - return problems; - } - - formatTrigger(zabbixTrigger) { - const trigger = _.cloneDeep(zabbixTrigger); - - // Set host and proxy that the trigger belongs - if (trigger.hosts && trigger.hosts.length) { - const host = trigger.hosts[0]; - trigger.host = host.name; - trigger.hostTechName = host.host; - if (host.proxy) { - trigger.proxy = host.proxy; - } - } - - // Set tags if present - if (trigger.tags && trigger.tags.length === 0) { - trigger.tags = null; - } - - // Handle multi-line description - if (trigger.comments) { - trigger.comments = trigger.comments.replace('\n', '
    '); - } - - trigger.lastchangeUnix = Number(trigger.lastchange); - return trigger; - } - - parseTags(tagStr) { - if (!tagStr) { - return []; - } - - let tags = _.map(tagStr.split(','), (tag) => tag.trim()); - tags = _.map(tags, (tag) => { - const tagParts = tag.split(':'); - return { tag: tagParts[0].trim(), value: tagParts[1].trim() }; - }); - return tags; - } - - tagsToString(tags) { - return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', '); - } - - addTagFilter(tag, datasource) { - for (const target of this.panel.targets) { - if (target.datasource?.uid === datasource?.uid || target.datasource === datasource || this.panel.datasource === datasource) { - const tagFilter = target.tags.filter; - let targetTags = this.parseTags(tagFilter); - const newTag = { tag: tag.tag, value: tag.value }; - targetTags.push(newTag); - targetTags = _.uniqWith(targetTags, _.isEqual); - const newFilter = this.tagsToString(targetTags); - target.tags.filter = newFilter; - } - } - this.refresh(); - } - - removeTagFilter(tag, datasource) { - const matchTag = t => t.tag === tag.tag && t.value === tag.value; - for (const target of this.panel.targets) { - if (target.datasource?.uid === datasource?.uid || target.datasource === datasource || this.panel.datasource === datasource) { - const tagFilter = target.tags.filter; - let targetTags = this.parseTags(tagFilter); - _.remove(targetTags, matchTag); - targetTags = _.uniqWith(targetTags, _.isEqual); - const newFilter = this.tagsToString(targetTags); - target.tags.filter = newFilter; - } - } - this.refresh(); - } - - getProblemEvents(problem) { - const triggerids = [problem.triggerid]; - const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000); - const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000); - return getDataSourceSrv().get(problem.datasource) - .then((datasource: any) => { - return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT); - }); - } - - getProblemAlerts(problem: ProblemDTO) { - if (!problem.eventid) { - return Promise.resolve([]); - } - const eventids = [problem.eventid]; - return getDataSourceSrv().get(problem.datasource) - .then((datasource: any) => { - return datasource.zabbix.getEventAlerts(eventids); - }); - } - - getProblemScripts(problem: ProblemDTO) { - const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null; - - return getDataSourceSrv().get(problem.datasource) - .then((datasource: any) => { - return datasource.zabbix.getScripts([hostid]); - }); - } - - getAlertIconClassBySeverity(triggerSeverity) { - let iconClass = 'icon-gf-online'; - if (triggerSeverity.priority >= 2) { - iconClass = 'icon-gf-critical'; - } - return iconClass; - } - - resetResizedColumns() { - this.panel.resizedColumns = []; - this.render(); - } - - acknowledgeProblem(problem: ProblemDTO, message, action, severity) { - const eventid = problem.eventid; - const grafana_user = this.contextSrv.user.name; - const ack_message = grafana_user + ' (Grafana): ' + message; - return getDataSourceSrv().get(problem.datasource) - .then((datasource: any) => { - const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin; - if (datasource.disableReadOnlyUsersAck && !userIsEditor) { - return Promise.reject({ message: 'You have no permissions to acknowledge events.' }); - } - if (eventid) { - return datasource.zabbix.acknowledgeEvent(eventid, ack_message, action, severity); - } else { - return Promise.reject({ message: 'Trigger has no events. Nothing to acknowledge.' }); - } - }) - .then(this.refresh.bind(this)) - .catch((err) => { - this.setPanelError(err); - return Promise.reject(err); - }); - } - - executeScript(problem: ProblemDTO, scriptid: string) { - const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null; - - return getDataSourceSrv().get(problem.datasource) - .then((datasource: any) => { - return datasource.zabbix.executeScript(hostid, scriptid); - }); - } - - handlePageSizeChange(pageSize, pageIndex) { - this.panel.pageSize = pageSize; - this.pageIndex = pageIndex; - this.scope.$apply(() => { - this.render(); - }); - } - - handleColumnResize(newResized) { - this.panel.resizedColumns = newResized; - this.scope.$apply(() => { - this.render(); - }); - } - - link(scope, elem, attrs, ctrl) { - const panel = ctrl.panel; - - ctrl.events.on(PanelEvents.render, (renderData) => { - renderData = renderData || this.renderData; - renderPanel(renderData); - ctrl.renderingCompleted(); - }); - - function renderPanel(problems) { - const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000); - const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000); - - const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1), 10); - const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null; - - const pageSize = panel.pageSize || 10; - const loading = ctrl.loading && (!problems || !problems.length); - - const panelOptions = {}; - for (const prop in PANEL_DEFAULTS) { - panelOptions[prop] = ctrl.panel[prop]; - } - const problemsListProps = { - problems, - panelOptions, - timeRange: { timeFrom, timeTo }, - range: ctrl.range, - loading, - pageSize, - fontSize: fontSizeProp, - panelId: ctrl.panel.id, - getProblemEvents: ctrl.getProblemEvents.bind(ctrl), - getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl), - getScripts: ctrl.getProblemScripts.bind(ctrl), - onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl), - onColumnResize: ctrl.handleColumnResize.bind(ctrl), - onProblemAck: (trigger, data) => { - const { message, action, severity } = data; - return ctrl.acknowledgeProblem(trigger, message, action, severity); - }, - onExecuteScript: ctrl.executeScript.bind(ctrl), - onTagClick: (tag, datasource, ctrlKey, shiftKey) => { - if (ctrlKey || shiftKey) { - ctrl.removeTagFilter(tag, datasource); - } else { - ctrl.addTagFilter(tag, datasource); - } - } - }; - - let problemsReactElem; - if (panel.layout === 'list') { - problemsReactElem = React.createElement(AlertList, problemsListProps); - } else { - problemsReactElem = React.createElement(ProblemList, problemsListProps); - } - - const panelContainerElem = elem.find('.panel-content'); - if (panelContainerElem && panelContainerElem.length) { - ReactDOM.render(problemsReactElem, panelContainerElem[0]); - } else { - ReactDOM.render(problemsReactElem, elem[0]); - } - } - } -} - -TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html'; diff --git a/src/panel-triggers/types.ts b/src/panel-triggers/types.ts index 36b8445..f199e60 100644 --- a/src/panel-triggers/types.ts +++ b/src/panel-triggers/types.ts @@ -1,9 +1,11 @@ -import { DataSourceRef } from "@grafana/data"; +import { DataSourceRef } from '@grafana/data'; +import { CURRENT_SCHEMA_VERSION } from './migrations'; export interface ProblemsPanelOptions { schemaVersion: number; datasources: any[]; targets: ProblemsPanelTarget[]; + layout: 'table' | 'list'; // Fields hostField?: boolean; hostTechNameField?: boolean; @@ -19,15 +21,9 @@ export interface ProblemsPanelOptions { descriptionAtNewLine?: boolean; // Options hostsInMaintenance?: boolean; - showTriggers?: 'all triggers' | 'unacknowledged' | 'acknowledges'; - sortTriggersBy?: { - text: string; - value: 'lastchange' | 'priority'; - }; - showEvents?: { - text: 'All' | 'OK' | 'Problems'; - value: 1 | Array<0 | 1>; - }; + showTriggers?: 'all triggers' | 'unacknowledged' | 'acknowledged'; + sortProblems?: 'default' | 'lastchange' | 'priority'; + showEvents?: Number[]; limit?: number; // View options fontSize?: string; @@ -46,24 +42,71 @@ export interface ProblemsPanelOptions { markAckEvents?: boolean; } +export const DEFAULT_SEVERITY = [ + { priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true }, + { priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true }, + { priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true }, + { priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true }, + { priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true }, + { priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true }, +]; + +export const getDefaultSeverity = () => DEFAULT_SEVERITY; + +export const defaultPanelOptions: Partial = { + schemaVersion: CURRENT_SCHEMA_VERSION, + // Fields + hostField: true, + hostTechNameField: false, + hostProxy: false, + hostGroups: false, + showTags: true, + statusField: true, + statusIcon: false, + severityField: true, + ackField: true, + ageField: false, + descriptionField: true, + descriptionAtNewLine: false, + // Options + sortProblems: 'lastchange', + limit: null, + // View options + layout: 'table', + fontSize: '100%', + pageSize: 10, + problemTimeline: true, + highlightBackground: false, + highlightNewEvents: false, + highlightNewerThan: '1h', + customLastChangeFormat: false, + lastChangeFormat: '', + resizedColumns: [], + // Triggers severity and colors + triggerSeverity: getDefaultSeverity(), + okEventColor: 'rgb(56, 189, 113)', + ackEventColor: 'rgb(56, 219, 156)', + markAckEvents: false, +}; + export interface ProblemsPanelTarget { group: { - filter: string + filter: string; }; host: { - filter: string + filter: string; }; application: { - filter: string + filter: string; }; trigger: { - filter: string + filter: string; }; tags: { - filter: string + filter: string; }; proxy: { - filter: string + filter: string; }; datasource: string; } @@ -77,108 +120,6 @@ export interface TriggerSeverity { export type TriggerColor = string; -export interface ZBXTrigger { - acknowledges?: ZBXAcknowledge[]; - showAckButton?: boolean; - alerts?: ZBXAlert[]; - age?: string; - color?: TriggerColor; - comments?: string; - correlation_mode?: string; - correlation_tag?: string; - datasource?: DataSourceRef | string; - description?: string; - error?: string; - expression?: string; - flags?: string; - groups?: ZBXGroup[]; - host?: string; - hostTechName?: string; - hosts?: ZBXHost[]; - items?: ZBXItem[]; - lastEvent?: ZBXEvent; - lastchange?: string; - lastchangeUnix?: number; - maintenance?: boolean; - manual_close?: string; - priority?: string; - proxy?: string; - recovery_expression?: string; - recovery_mode?: string; - severity?: string; - state?: string; - status?: string; - tags?: ZBXTag[]; - templateid?: string; - triggerid?: string; - /** Whether the trigger can generate multiple problem events. */ - type?: string; - url?: string; - value?: string; -} - -export interface ZBXGroup { - groupid: string; - name: string; -} - -export interface ZBXHost { - hostid: string; - name: string; -} - -export interface ZBXItem { - itemid: string; - name: string; - key_: string; - lastvalue?: string; -} - -export interface ZBXEvent { - eventid: string; - clock: string; - ns?: string; - value?: string; - source?: string; - object?: string; - objectid?: string; - acknowledged?: string; - severity?: string; - hosts?: ZBXHost[]; - acknowledges?: ZBXAcknowledge[]; -} - -export interface ZBXTag { - tag: string; - value?: string; -} - -export interface ZBXAcknowledge { - acknowledgeid: string; - eventid: string; - userid: string; - action: string; - clock: string; - time: string; - message?: string; - user: string; - alias: string; - name: string; - surname: string; -} - -export interface ZBXAlert { - eventid: string; - clock: string; - message: string; - error: string; -} - -export interface GFTimeRange { - timeFrom: number; - timeTo: number; -} - export interface RTRow { /** the materialized row of data */ row: any; diff --git a/tsconfig.json b/tsconfig.json index 6081e9f..d1b4a5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "moduleResolution": "node", - "target": "es5", + "target": "ES6", "lib": [ "es6", "dom", "es2017" ], "rootDir": "./src", "jsx": "react", diff --git a/webpack/webpack.base.conf.js b/webpack/webpack.base.conf.js index f8a3b76..e8ff171 100644 --- a/webpack/webpack.base.conf.js +++ b/webpack/webpack.base.conf.js @@ -15,10 +15,9 @@ module.exports = { target: 'node', context: resolve('src'), entry: { - 'module': './module.js', - 'app_config_ctrl/config': './app_config_ctrl/config.js', + 'module': './module.ts', 'datasource-zabbix/module': './datasource-zabbix/module.ts', - 'panel-triggers/module': './panel-triggers/module.js', + 'panel-triggers/module': './panel-triggers/module.tsx', }, output: { filename: "[name].js", diff --git a/yarn.lock b/yarn.lock index 89530a8..e54fdac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16484,11 +16484,6 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.4.4: - version "4.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" - integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== - typescript@4.6.4: version "4.6.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" @@ -16499,6 +16494,11 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +typescript@4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" + integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== + ua-parser-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"