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/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts index 007d592..57e4423 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/types.ts @@ -37,6 +37,24 @@ 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 interface APIExecuteScriptResponse { + response: 'success' | 'failed'; + value?: string; +} diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts index 718c539..22248ca 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts @@ -4,7 +4,7 @@ import kbn from 'grafana/app/core/utils/kbn'; import * as utils from '../../../utils'; import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; import { ShowProblemTypes, ZBXProblem } from '../../../types'; -import { GFHTTPRequest, JSONRPCError } from './types'; +import { GFHTTPRequest, JSONRPCError, ZBXScript, APIExecuteScriptResponse } from './types'; import { getBackendSrv } from '@grafana/runtime'; const DEFAULT_ZABBIX_VERSION = '3.0.0'; @@ -629,6 +629,24 @@ export class ZabbixAPIConnector { return this.request('proxy.get', params); } + + getScripts(hostids: string[], options?: any): Promise { + const params: any = { + output: 'extend', + hostids, + }; + + return this.request('script.get', params).then(utils.mustArray); + } + + executeScript(hostid: string, scriptid: string): Promise { + const params: any = { + hostid, + scriptid, + }; + + return this.request('script.execute', params); + } } function filterTriggersByAcknowledge(triggers, acknowledged) { diff --git a/src/datasource-zabbix/zabbix/zabbix.ts b/src/datasource-zabbix/zabbix/zabbix.ts index 76e173c..b459e69 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', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', - 'getExtendedEventData' + 'getExtendedEventData', 'getScripts', 'executeScript', ]; export class Zabbix implements ZabbixConnector { diff --git a/src/panel-triggers/components/ExecScriptModal.tsx b/src/panel-triggers/components/ExecScriptModal.tsx new file mode 100644 index 0000000..357e8e0 --- /dev/null +++ b/src/panel-triggers/components/ExecScriptModal.tsx @@ -0,0 +1,226 @@ +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 { ZBXScript, APIExecuteScriptResponse } from '../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; +import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable } from '@grafana/ui'; +import { GrafanaTheme, SelectableValue } from '@grafana/data'; +import { FAIcon } from '../../components'; + +const KEYBOARD_ENTER_KEY = 13; +const KEYBOARD_ESCAPE_KEY = 27; + +interface Props extends Themeable { + getScripts(): Promise; + onSubmit(data?: ExecScriptData): Promise | any; + onDismiss?(): void; +} + +interface State { + selectedScript: SelectableValue; + scriptOptions: Array>; + script: ZBXScript; + error: boolean; + errorMessage: string | JSX.Element; + result: string | JSX.Element; + selectError: string; + loading: boolean; +} + +export interface ExecScriptData { + scriptid: string; +} + +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 }); + } + + onChangeSelectedScript = (v: SelectableValue) => { + 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.' + }); + } + + this.setState({ errorMessage: '', loading: true, result: '' }); + + const data: ExecScriptData = { + 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.message || err.data || ''; + 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; + const { scriptOptions, selectedScript, script, result, selectError, errorMessage, error } = this.state; + + const styles = getStyles(theme); + const modalClass = cx(styles.modal); + const modalTitleClass = cx(styles.modalHeaderTitle); + const selectErrorClass = cx('gf-form-hint-text', styles.inputError); + const scriptCommandContainerClass = cx('gf-form', styles.scriptCommandContainer); + const scriptCommandClass = cx('gf-form-hint-text', styles.scriptCommand); + + return ( + + {this.state.loading ? : } + Execute script + + } + > +
+