From fd775aa9d7bd9f0e6232455f6de45dd87d640339 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 27 May 2020 11:09:37 +0300 Subject: [PATCH 1/3] getScripts method --- .../zabbix/connectors/zabbix_api/types.ts | 17 ++++++++++++++++- .../connectors/zabbix_api/zabbixAPIConnector.ts | 11 ++++++++++- src/datasource-zabbix/zabbix/zabbix.ts | 4 ++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts index 007d592..677ad71 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts @@ -37,6 +37,21 @@ export interface ZabbixRequestResponse { data?: JSONRPCResponse; } -export type ZabbixAPIResponse = T; +export type ZabbixAPIResponse = Promise; export type APILoginResponse = string; + +export interface ZBXScript { + scriptid: string; + name?: string; + command?: string; + host_access?: string; + usrgrpid?: string; + groupid?: string; + description?: string; + confirmation?: string; + type?: string; + execute_on?: string; +} + +export type APIScriptGetResponse = ZabbixAPIResponse; diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts index b30cb5c..6fa3482 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts @@ -5,7 +5,7 @@ import * as utils from '../../../utils'; import { ZabbixAPICore } from './zabbixAPICore'; import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; import { ShowProblemTypes, ZBXProblem } from '../../../types'; -import { JSONRPCRequestParams } from './types'; +import { JSONRPCRequestParams, APIScriptGetResponse } from './types'; const DEFAULT_ZABBIX_VERSION = '3.0.0'; @@ -664,6 +664,15 @@ export class ZabbixAPIConnector { return this.request('proxy.get', params); } + + getScripts(hostids: string[], options?: any): APIScriptGetResponse { + const params: any = { + output: 'extend', + hostids, + }; + + return this.request('script.get', params); + } } function filterTriggersByAcknowledge(triggers, acknowledged) { diff --git a/src/datasource-zabbix/zabbix/zabbix.ts b/src/datasource-zabbix/zabbix/zabbix.ts index 9328f34..f4aa4aa 100644 --- a/src/datasource-zabbix/zabbix/zabbix.ts +++ b/src/datasource-zabbix/zabbix/zabbix.ts @@ -20,7 +20,7 @@ interface AppsResponse extends Array { const REQUESTS_TO_PROXYFY = [ 'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies', - 'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds' + 'getEventAlerts', 'getExtendedEventData', 'getProblems', 'getEventsHistory', 'getTriggersByIds', 'getScripts' ]; const REQUESTS_TO_CACHE = [ @@ -30,7 +30,7 @@ const REQUESTS_TO_CACHE = [ const REQUESTS_TO_BIND = [ 'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', - 'getExtendedEventData' + 'getExtendedEventData', 'getScripts' ]; export class Zabbix implements ZabbixConnector { From 092acec2954b52d093eab2e73c6b4347325ac58d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 27 May 2020 12:19:32 +0300 Subject: [PATCH 2/3] Exec script dialog --- .../ExecScriptButton/ExecScriptButton.tsx | 13 + src/components/index.ts | 1 + .../components/ExecScriptModal.tsx | 233 ++++++++++++++++++ .../components/Problems/ProblemDetails.tsx | 24 +- .../components/Problems/Problems.tsx | 3 + src/panel-triggers/triggers_panel_ctrl.ts | 10 + 6 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/components/ExecScriptButton/ExecScriptButton.tsx create mode 100644 src/panel-triggers/components/ExecScriptModal.tsx diff --git a/src/components/ExecScriptButton/ExecScriptButton.tsx b/src/components/ExecScriptButton/ExecScriptButton.tsx new file mode 100644 index 0000000..9a54f76 --- /dev/null +++ b/src/components/ExecScriptButton/ExecScriptButton.tsx @@ -0,0 +1,13 @@ +import React, { FC } from 'react'; +import { ActionButton } from '../ActionButton/ActionButton'; + +interface Props { + className?: string; + onClick(): void; +} + +export const ExecScriptButton: FC = ({ className, onClick }) => { + return ( + + ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 767e5b0..c342b30 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,5 +2,6 @@ export { GFHeartIcon } from './GFHeartIcon/GFHeartIcon'; export { FAIcon } from './FAIcon/FAIcon'; export { AckButton } from './AckButton/AckButton'; export { ExploreButton } from './ExploreButton/ExploreButton'; +export { ExecScriptButton } from './ExecScriptButton/ExecScriptButton'; export { Tooltip } from './Tooltip/Tooltip'; export { ModalController } from './Modal/ModalController'; diff --git a/src/panel-triggers/components/ExecScriptModal.tsx b/src/panel-triggers/components/ExecScriptModal.tsx new file mode 100644 index 0000000..fa1752c --- /dev/null +++ b/src/panel-triggers/components/ExecScriptModal.tsx @@ -0,0 +1,233 @@ +import React, { PureComponent } from 'react'; +import { cx, css } from 'emotion'; +import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants'; +import { APIScriptGetResponse, ZBXScript } from '../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; +import { Button, VerticalGroup, Spinner, Modal, Select, Forms, stylesFactory, withTheme, Themeable } from '@grafana/ui'; +import { FAIcon } from '../../components'; + +import * as grafanaUi from '@grafana/ui'; +import { GrafanaTheme, SelectableValue } from '@grafana/data'; +const Checkbox: any = Forms?.Checkbox || (grafanaUi as any).Checkbox; +const RadioButtonGroup: any = Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup; + +const KEYBOARD_ENTER_KEY = 13; +const KEYBOARD_ESCAPE_KEY = 27; + +interface Props extends Themeable { + getScripts(): Promise; + onSubmit(data?: AckProblemData): Promise | any; + onDismiss?(): void; +} + +interface State { + selectedScript: SelectableValue; + scriptOptions: Array>; + script: ZBXScript; + error: boolean; + errorMessage: string; + selectError: string; + result: string; + loading: boolean; +} + +export interface AckProblemData { + message: string; + closeProblem?: boolean; + action?: number; + severity?: number; +} + +export class ExecScriptModalUnthemed extends PureComponent { + scripts: ZBXScript[]; + + constructor(props) { + super(props); + this.state = { + error: false, + errorMessage: '', + selectError: '', + selectedScript: null, + result: '', + loading: false, + scriptOptions: [], + script: null, + }; + } + + async componentDidMount() { + const scripts = await this.props.getScripts(); + this.scripts = scripts; + const scriptOptions: Array> = scripts.map(s => { + return { + value: s.scriptid, + label: s.name, + description: s.description || s.command, + }; + }); + + const selectedScript = scriptOptions?.length ? scriptOptions[0] : null; + const script = scripts.find(s => selectedScript.value === s.scriptid); + + this.setState({ scriptOptions, selectedScript, script }); + } + + handleKeyUp = (event: React.KeyboardEvent) => { + if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') { + this.submit(); + } else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') { + this.dismiss(); + } + } + + handleBackdropClick = () => { + this.dismiss(); + } + + onChangeSelectedScript = (v: SelectableValue) => { + const script = this.scripts.find(s => v.value === s.scriptid); + this.setState({ selectedScript: v, script }); + }; + + dismiss = () => { + this.setState({ selectedScript: null, error: false, errorMessage: '', selectError: '', loading: false }); + this.props.onDismiss(); + } + + submit = () => { + // const { acknowledge, changeSeverity, closeProblem } = this.state; + + // const actionSelected = acknowledge || changeSeverity || closeProblem; + // if (!this.state.value && !actionSelected) { + // return this.setState({ + // error: true, + // errorMessage: 'Enter message text or select an action' + // }); + // } + + // this.setState({ ackError: '', loading: true }); + + // const ackData: AckProblemData = { + // message: this.state.value, + // }; + + // let action = ZBX_ACK_ACTION_ADD_MESSAGE; + // if (this.state.acknowledge) { + // action += ZBX_ACK_ACTION_ACK; + // } + // if (this.state.changeSeverity) { + // action += ZBX_ACK_ACTION_CHANGE_SEVERITY; + // ackData.severity = this.state.selectedSeverity; + // } + // if (this.state.closeProblem) { + // action += ZBX_ACK_ACTION_CLOSE; + // } + // ackData.action = action; + + // this.props.onSubmit(ackData).then(() => { + // this.dismiss(); + // }).catch(err => { + // this.setState({ + // ackError: err.message || err.data, + // loading: false, + // }); + // }); + } + + render() { + const { theme } = this.props; + const { scriptOptions, selectedScript, script, selectError, errorMessage } = this.state; + + const styles = getStyles(theme); + const modalClass = cx(styles.modal); + const modalTitleClass = cx(styles.modalHeaderTitle); + const inputGroupClass = cx('gf-form', styles.inputGroup); + const inputHintClass = cx('gf-form-hint-text', styles.inputHint); + const inputErrorClass = cx('gf-form-hint-text', styles.inputError); + const scriptCommandClass = cx('gf-form-hint-text', styles.scriptCommand); + + return ( + + {this.state.loading ? : } + Execute script + + } + > +
+
-
+
{script && {script.command}}
- {this.state.error && -
- {errorMessage} -
+
+ {result && + {result} } + {error && + {errorMessage} + } +
@@ -193,7 +175,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { const red = theme.colors.red || (theme as any).palette.red; return { modal: css` - width: 500px; + width: 600px; `, modalHeaderTitle: css` font-size: ${theme.typography.heading.h3}; @@ -201,16 +183,16 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { margin: 0 ${theme.spacing.md}; display: flex; `, - inputGroup: css` - `, input: css` border-color: ${red}; border-radius: 2px; outline-offset: 2px; box-shadow: 0 0 0 2px ${theme.colors.pageBg || (theme as any).colors.bg1}, 0 0 0px 4px ${red}; `, + scriptCommandContainer: css` + margin-bottom: ${theme.spacing.md}; + `, scriptCommand: css` - float: left; color: ${theme.colors.textWeak}; text-align: left; font-family: ${theme.typography.fontFamily.monospace}; @@ -224,6 +206,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { float: left; color: ${red}; `, + resultContainer: css` + min-height: 50px; + font-family: ${theme.typography.fontFamily.monospace}; + font-size: ${theme.typography.size.sm}; + p { + font-size: ${theme.typography.size.sm}; + margin-bottom: 0px; + } + `, + execResult: css` + `, execError: css` color: ${red}; `, diff --git a/src/panel-triggers/components/Problems/ProblemDetails.tsx b/src/panel-triggers/components/Problems/ProblemDetails.tsx index a35bcd4..ea966b5 100644 --- a/src/panel-triggers/components/Problems/ProblemDetails.tsx +++ b/src/panel-triggers/components/Problems/ProblemDetails.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import moment from 'moment'; import * as utils from '../../../datasource-zabbix/utils'; import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types'; -import { ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; +import { ZBXScript, APIExecuteScriptResponse } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { ZBXItem, GFTimeRange, RTRow } from '../../types'; import { AckModal, AckProblemData } from '../AckModal'; import EventTag from '../EventTag'; @@ -10,7 +10,7 @@ import ProblemStatusBar from './ProblemStatusBar'; import AcknowledgesList from './AcknowledgesList'; import ProblemTimeline from './ProblemTimeline'; import { FAIcon, ExploreButton, AckButton, Tooltip, ModalController, ExecScriptButton } from '../../../components'; -import { ExecScriptModal } from '../ExecScriptModal'; +import { ExecScriptModal, ExecScriptData } from '../ExecScriptModal'; interface ProblemDetailsProps extends RTRow { rootWidth: number; @@ -20,6 +20,7 @@ interface ProblemDetailsProps extends RTRow { getProblemEvents: (problem: ProblemDTO) => Promise; getProblemAlerts: (problem: ProblemDTO) => Promise; getScripts: (problem: ProblemDTO) => Promise; + onExecuteScript(problem: ProblemDTO, scriptid: string): Promise; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise | any; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; } @@ -82,6 +83,11 @@ export class ProblemDetails extends PureComponent { + const problem = this.props.original as ProblemDTO; + return this.props.onExecuteScript(problem, data.scriptid); + } + render() { const problem = this.props.original as ProblemDTO; const alerts = this.state.alerts; @@ -117,7 +123,7 @@ export class ProblemDetails extends PureComponent { showModal(ExecScriptModal, { getScripts: this.getScripts, - onSubmit: this.ackProblem, + onSubmit: this.onExecuteScript, onDismiss: hideModal, }); }} diff --git a/src/panel-triggers/components/Problems/Problems.tsx b/src/panel-triggers/components/Problems/Problems.tsx index 8e4f3d1..ceaab12 100644 --- a/src/panel-triggers/components/Problems/Problems.tsx +++ b/src/panel-triggers/components/Problems/Problems.tsx @@ -10,7 +10,7 @@ import { AckProblemData } from '../AckModal'; import { GFHeartIcon, FAIcon } from '../../../components'; import { ProblemsPanelOptions, GFTimeRange, RTCell, TriggerSeverity, RTResized } from '../../types'; import { ProblemDTO, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types'; -import { ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; +import { ZBXScript, APIExecuteScriptResponse } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { AckCell } from './AckCell'; export interface ProblemListProps { @@ -24,6 +24,7 @@ export interface ProblemListProps { getProblemEvents: (problem: ProblemDTO) => Promise; getProblemAlerts: (problem: ProblemDTO) => Promise; getScripts: (problem: ProblemDTO) => Promise; + onExecuteScript: (problem: ProblemDTO, scriptid: string) => Promise; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void; @@ -55,6 +56,9 @@ export default class ProblemList extends PureComponent { + } + handlePageSizeChange = (pageSize, pageIndex) => { if (this.props.onPageSizeChange) { this.props.onPageSizeChange(pageSize, pageIndex); @@ -174,6 +178,7 @@ export default class ProblemList extends PureComponent diff --git a/src/panel-triggers/triggers_panel_ctrl.ts b/src/panel-triggers/triggers_panel_ctrl.ts index a891955..fbe76d0 100644 --- a/src/panel-triggers/triggers_panel_ctrl.ts +++ b/src/panel-triggers/triggers_panel_ctrl.ts @@ -317,11 +317,11 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl { } getProblemScripts(problem: ProblemDTO) { - const hostIds = problem.hosts?.map(h => h.hostid); + const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null; return getDataSourceSrv().get(problem.datasource) .then((datasource: any) => { - return datasource.zabbix.getScripts(hostIds); + return datasource.zabbix.getScripts([hostid]); }); } @@ -361,6 +361,15 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl { }); } + 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; @@ -415,6 +424,7 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl { 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);