diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 097a659bf633e4..5f49adc954d5a8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7965,7 +7965,6 @@ "xpack.watcher.sections.watchEdit.json.simulateTabTitle": "模拟监视", "xpack.watcher.sections.watchEdit.json.simulateWatchButtonLabel": "模拟监视", "xpack.watcher.sections.watchEdit.json.simulationOutputLabel": "模拟输出:", - "xpack.watcher.sections.watchEdit.json.warningPossibleInvalidSlackAction.description": "此监视具有不包含“to”属性的 Slack 操作。 只有在 Elasticsearch 的 Slack“message_default”中指定了“to”属性,此监视才有效。", "xpack.watcher.sections.watchEdit.json.watchErrorsWarning.confirmSaveWatch": "保存监视", "xpack.watcher.sections.watchEdit.threshold.matchingFollowingConditionTitle": "匹配以下条件", "xpack.watcher.sections.watchEdit.threshold.saveButtonLabel": "保存", @@ -8079,4 +8078,4 @@ "xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/watcher/common/constants/index.ts b/x-pack/plugins/watcher/common/constants/index.ts index d9156e132c038a..4260688c6eb451 100644 --- a/x-pack/plugins/watcher/common/constants/index.ts +++ b/x-pack/plugins/watcher/common/constants/index.ts @@ -23,4 +23,3 @@ export { WATCH_HISTORY } from './watch_history'; export { WATCH_STATES } from './watch_states'; export { WATCH_TYPES } from './watch_types'; export { ERROR_CODES } from './error_codes'; -export { WATCH_TABS, WATCH_TAB_ID_EDIT, WATCH_TAB_ID_SIMULATE } from './watch_tabs'; diff --git a/x-pack/plugins/watcher/common/constants/watch_tabs.ts b/x-pack/plugins/watcher/common/constants/watch_tabs.ts deleted file mode 100644 index 1d741b9610663c..00000000000000 --- a/x-pack/plugins/watcher/common/constants/watch_tabs.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const WATCH_TAB_ID_EDIT = 'watchEditTab'; -export const WATCH_TAB_ID_SIMULATE = 'watchSimulateTab'; - -interface WatchTab { - id: string; - name: string; -} - -export const WATCH_TABS: WatchTab[] = [ - { - id: WATCH_TAB_ID_EDIT, - name: i18n.translate('xpack.watcher.sections.watchEdit.json.editTabLabel', { - defaultMessage: 'Edit', - }), - }, - { - id: WATCH_TAB_ID_SIMULATE, - name: i18n.translate('xpack.watcher.sections.watchEdit.json.simulateTabLabel', { - defaultMessage: 'Simulate', - }), - }, -]; diff --git a/x-pack/plugins/watcher/common/types/watch_types.ts b/x-pack/plugins/watcher/common/types/watch_types.ts index 545920496bc3a6..0850911cdab6c4 100644 --- a/x-pack/plugins/watcher/common/types/watch_types.ts +++ b/x-pack/plugins/watcher/common/types/watch_types.ts @@ -11,15 +11,15 @@ export interface ExecutedWatchResults { startTime: Date; watchStatus: { state: string; - actionStatuses: Array<{ state: string; lastExecutionReason: string }>; + actionStatuses: Array<{ state: string; lastExecutionReason: string; id: string }>; }; } export interface ExecutedWatchDetails { - triggerData: { - triggeredTime: Date; - scheduledTime: Date; - }; + scheduledTimeValue: string | undefined; + scheduledTimeUnit: string; + triggeredTimeValue: string | undefined; + triggeredTimeUnit: string; ignoreCondition: boolean; alternativeInput: any; actionModes: { @@ -42,7 +42,7 @@ export interface BaseWatch { upstreamJson: any; resetActions: () => void; createAction: (type: string, actionProps: {}) => void; - validate: () => { warning: { message: string } }; + validate: () => { warning: { message: string; title?: string } }; actions: [ { id: string; diff --git a/x-pack/plugins/watcher/public/components/confirm_watches_modal.tsx b/x-pack/plugins/watcher/public/components/confirm_watches_modal.tsx index 61adccb45ebb24..2ab21d34d648e6 100644 --- a/x-pack/plugins/watcher/public/components/confirm_watches_modal.tsx +++ b/x-pack/plugins/watcher/public/components/confirm_watches_modal.tsx @@ -11,19 +11,23 @@ export const ConfirmWatchesModal = ({ modalOptions, callback, }: { - modalOptions: { message: string } | null; + modalOptions: { + title: string; + message: string; + buttonLabel?: string; + buttonType?: 'primary' | 'danger'; + } | null; callback: (isConfirmed?: boolean) => void; }) => { if (!modalOptions) { return null; } + const { title, message, buttonType, buttonLabel } = modalOptions; return ( callback()} onConfirm={() => { callback(true); @@ -32,12 +36,16 @@ export const ConfirmWatchesModal = ({ 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.cancelButtonLabel', { defaultMessage: 'Cancel' } )} - confirmButtonText={i18n.translate( - 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.saveButtonLabel', - { defaultMessage: 'Save' } - )} + confirmButtonText={ + buttonLabel + ? buttonLabel + : i18n.translate( + 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.saveButtonLabel', + { defaultMessage: 'Save watch' } + ) + } > - {modalOptions.message} + {message} ); diff --git a/x-pack/plugins/watcher/public/lib/documentation_links/documentation_links.ts b/x-pack/plugins/watcher/public/lib/documentation_links/documentation_links.ts index 1868a4f372ae0d..19ca2689e4cd1c 100644 --- a/x-pack/plugins/watcher/public/lib/documentation_links/documentation_links.ts +++ b/x-pack/plugins/watcher/public/lib/documentation_links/documentation_links.ts @@ -11,5 +11,11 @@ export const documentationLinks = { putWatchApi: makeDocumentationLink( '{baseUrl}guide/en/elasticsearch/reference/{urlVersion}/watcher-api-put-watch.html' ), + executeWatchApi: makeDocumentationLink( + '{baseUrl}guide/en/elasticsearch/reference/{urlVersion}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode' + ), + watchNotificationSettings: makeDocumentationLink( + '{baseUrl}guide/en/elasticsearch/reference/{urlVersion}/notification-settings.html#slack-notification-settings' + ), }, }; diff --git a/x-pack/plugins/watcher/public/models/action/slack_action.js b/x-pack/plugins/watcher/public/models/action/slack_action.js index f94bc2f06b19d8..2cbfa27b2ca7a0 100644 --- a/x-pack/plugins/watcher/public/models/action/slack_action.js +++ b/x-pack/plugins/watcher/public/models/action/slack_action.js @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { get, isArray } from 'lodash'; import { BaseAction } from './base_action'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode, EuiLink } from '@elastic/eui'; +import { documentationLinks } from '../../lib/documentation_links'; export class SlackAction extends BaseAction { constructor(props = {}) { @@ -21,11 +25,27 @@ export class SlackAction extends BaseAction { const errors = []; if (!this.to.length) { + const message = ( + message_defaults, + link: ( + + + + ) + }} + /> + ); errors.push({ - message: i18n.translate('xpack.watcher.sections.watchEdit.json.warningPossibleInvalidSlackAction.description', { - // eslint-disable-next-line max-len - defaultMessage: 'This watch has a Slack action without a "to" property. This watch will only be valid if you specified the "to" property in the Slack "message_default" setting in Elasticsearch.' - }) + message }); } diff --git a/x-pack/plugins/watcher/public/models/execute_details/execute_details.js b/x-pack/plugins/watcher/public/models/execute_details/execute_details.js index 855f805349f599..68cc4e45c9a87f 100644 --- a/x-pack/plugins/watcher/public/models/execute_details/execute_details.js +++ b/x-pack/plugins/watcher/public/models/execute_details/execute_details.js @@ -6,6 +6,7 @@ import { TIME_UNITS } from '../../../common/constants'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; export class ExecuteDetails { constructor(props = {}) { @@ -13,13 +14,39 @@ export class ExecuteDetails { this.triggeredTimeUnit = props.triggeredTimeUnit; this.scheduledTimeValue = props.scheduledTimeValue; this.scheduledTimeUnit = props.scheduledTimeUnit; - this.scheduledTime = props.scheduledTime; this.ignoreCondition = props.ignoreCondition; - this.alternativeInput = props.alternativeInput; + this.alternativeInput = props.alternativeInput || ''; this.actionModes = props.actionModes; this.recordExecution = props.recordExecution; } + validate() { + const errors = { + json: [], + }; + if (this.alternativeInput || this.alternativeInput !== '') { + try { + const parsedJson = JSON.parse(this.alternativeInput); + if (parsedJson && typeof parsedJson !== 'object') { + errors.json.push(i18n.translate( + 'xpack.watcher.sections.watchEdit.simulate.form.alternativeInputFieldError', + { + defaultMessage: 'Invalid JSON', + } + )); + } + } catch (e) { + errors.json.push(i18n.translate( + 'xpack.watcher.sections.watchEdit.simulate.form.alternativeInputFieldError', + { + defaultMessage: 'Invalid JSON', + } + )); + } + } + return errors; + } + formatTime(timeUnit, value) { let timeValue = moment(); switch (timeUnit) { @@ -42,18 +69,17 @@ export class ExecuteDetails { get upstreamJson() { const hasTriggerTime = this.triggeredTimeValue !== ''; const hasScheduleTime = this.scheduledTimeValue !== ''; - const formattedTriggerTime = hasTriggerTime ? this.formatTime(this.triggeredTimeUnit, this.triggeredTimeValue) : undefined; - const formattedScheduleTime = hasScheduleTime ? this.formatTime(this.scheduledTimeUnit, this.scheduledTimeValue) : undefined; - const triggerData = { - triggeredTime: formattedTriggerTime, - scheduledTime: formattedScheduleTime, - }; + const triggeredTime = hasTriggerTime ? this.formatTime(this.triggeredTimeUnit, this.triggeredTimeValue) : undefined; + const scheduledTime = hasScheduleTime ? this.formatTime(this.scheduledTimeUnit, this.scheduledTimeValue) : undefined; return { - triggerData, + triggerData: { + triggeredTime, + scheduledTime, + }, ignoreCondition: this.ignoreCondition, - alternativeInput: this.alternativeInput, + alternativeInput: this.alternativeInput !== '' ? JSON.parse(this.alternativeInput) : undefined, actionModes: this.actionModes, - recordExecution: this.recordExecution + recordExecution: this.recordExecution, }; } } diff --git a/x-pack/plugins/watcher/public/models/watch/base_watch.js b/x-pack/plugins/watcher/public/models/watch/base_watch.js index c19e20db5ea90b..94d8079387101b 100644 --- a/x-pack/plugins/watcher/public/models/watch/base_watch.js +++ b/x-pack/plugins/watcher/public/models/watch/base_watch.js @@ -25,10 +25,9 @@ export class BaseWatch { * @param {array} props.actions Action definitions */ constructor(props = {}) { - this.id = get(props, 'id', ''); + this.id = get(props, 'id'); this.type = get(props, 'type'); this.isNew = get(props, 'isNew', true); - this.name = get(props, 'name'); this.isSystemWatch = Boolean(get(props, 'isSystemWatch')); this.watchStatus = WatchStatus.fromUpstreamJson(get(props, 'watchStatus')); diff --git a/x-pack/plugins/watcher/public/models/watch/json_watch.js b/x-pack/plugins/watcher/public/models/watch/json_watch.js index 586efc5ad7f780..e051bc63412e43 100644 --- a/x-pack/plugins/watcher/public/models/watch/json_watch.js +++ b/x-pack/plugins/watcher/public/models/watch/json_watch.js @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from 'plugins/watcher/../common/constants'; +import { ACTION_TYPES, WATCH_TYPES } from 'plugins/watcher/../common/constants'; import defaultWatchJson from './default_watch.json'; import { i18n } from '@kbn/i18n'; @@ -17,8 +17,70 @@ export class JsonWatch extends BaseWatch { constructor(props = {}) { props.type = WATCH_TYPES.JSON; super(props); + const existingWatch = get(props, 'watch'); + this.watch = existingWatch ? existingWatch : defaultWatchJson; + this.watchString = get(props, 'watchString', JSON.stringify(existingWatch ? existingWatch : defaultWatchJson, null, 2)); + } - this.watch = get(props, 'watch', defaultWatchJson); + validate() { + const validationResult = super.validate(); + const idRegex = /^[A-Za-z0-9\-\_]+$/; + const errors = { + id: [], + json: [], + }; + validationResult.errors = errors; + // Watch id validation + if (!this.id) { + errors.id.push( + i18n.translate('xpack.watcher.sections.watchEdit.json.error.requiredIdText', { + defaultMessage: 'ID is required', + }) + ); + } else if (!idRegex.test(this.id)) { + errors.id.push(i18n.translate('xpack.watcher.sections.watchEdit.json.error.invalidIdText', { + defaultMessage: 'ID can only contain letters, underscores, dashes, and numbers.', + })); + } + // JSON validation + if (!this.watchString || this.watchString === '') { + errors.json.push(i18n.translate('xpack.watcher.sections.watchEdit.json.error.requiredJsonText', { + defaultMessage: 'JSON is required', + })); + } else { + try { + const parsedJson = JSON.parse(this.watchString); + if (parsedJson && typeof parsedJson === 'object') { + const { actions } = parsedJson; + if (actions) { + // Validate if the action(s) provided is one of the supported actions + const invalidActions = Object.keys(actions).find(actionKey => { + const actionKeys = Object.keys(actions[actionKey]); + let type; + Object.keys(ACTION_TYPES).forEach(actionTypeKey => { + if (actionKeys.includes(ACTION_TYPES[actionTypeKey]) && !actionKeys.includes(ACTION_TYPES.UNKNOWN)) { + type = ACTION_TYPES[actionTypeKey]; + } + }); + return !type; + }); + if (invalidActions) { + errors.json.push(i18n.translate('xpack.watcher.sections.watchEdit.json.error.invalidActionType', { + defaultMessage: 'Unknown action type provided for action "{action}".', + values: { + action: invalidActions, + }, + })); + } + } + } + } catch (e) { + errors.json.push(i18n.translate('xpack.watcher.sections.watchEdit.json.error.invalidJsonText', { + defaultMessage: 'Invalid JSON', + })); + } + } + return validationResult; } get upstreamJson() { @@ -26,7 +88,6 @@ export class JsonWatch extends BaseWatch { Object.assign(result, { watch: this.watch }); - return result; } diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_component.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_component.tsx index c43e8ecdda937b..88c486652be160 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_component.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_component.tsx @@ -15,11 +15,70 @@ import { EuiTabs, EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { injectI18n } from '@kbn/i18n/react'; -import { WATCH_TAB_ID_EDIT, WATCH_TAB_ID_SIMULATE, WATCH_TABS } from '../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; +import { getActionType } from '../../../../common/lib/get_action_type'; +import { BaseWatch, ExecutedWatchDetails } from '../../../../common/types/watch_types'; +import { ACTION_MODES, TIME_UNITS } from '../../../../common/constants'; import { JsonWatchEditForm } from './json_watch_edit_form'; import { JsonWatchEditSimulate } from './json_watch_edit_simulate'; import { WatchContext } from './watch_context'; +interface WatchAction { + actionId: string; + actionMode: string; + type: string; +} + +interface WatchTab { + id: string; + name: string; +} + +const WATCH_EDIT_TAB = 'watchEditTab'; +const WATCH_SIMULATE_TAB = 'watchSimulateTab'; + +const WATCH_TABS: WatchTab[] = [ + { + id: WATCH_EDIT_TAB, + name: i18n.translate('xpack.watcher.sections.watchEdit.json.editTabLabel', { + defaultMessage: 'Edit', + }), + }, + { + id: WATCH_SIMULATE_TAB, + name: i18n.translate('xpack.watcher.sections.watchEdit.json.simulateTabLabel', { + defaultMessage: 'Simulate', + }), + }, +]; + +const EXECUTE_DETAILS_INITIAL_STATE = { + triggeredTimeValue: 0, + triggeredTimeUnit: TIME_UNITS.MILLISECOND, + scheduledTimeValue: 0, + scheduledTimeUnit: TIME_UNITS.SECOND, + ignoreCondition: false, +}; + +function getActions(watch: BaseWatch) { + const actions = (watch.watch && watch.watch.actions) || {}; + return Object.keys(actions).map(actionKey => ({ + actionId: actionKey, + type: getActionType(actions[actionKey]), + actionMode: ACTION_MODES.SIMULATE, + })); +} + +function getActionModes(items: WatchAction[]) { + const result = items.reduce((itemsAccum: any, item) => { + if (item.actionId) { + itemsAccum[item && item.actionId] = item.actionMode; + } + return itemsAccum; + }, {}); + return result; +} const JsonWatchEditUi = ({ pageTitle, @@ -31,21 +90,18 @@ const JsonWatchEditUi = ({ licenseService: any; }) => { const { watch } = useContext(WatchContext); + const watchActions = getActions(watch); // hooks - const [selectedTab, setSelectedTab] = useState(WATCH_TAB_ID_EDIT); - const [watchErrors, setWatchErrors] = useState<{ [key: string]: string[] }>({ - watchId: [], - watchJson: [], - }); - const [isShowingWatchErrors, setIsShowingWatchErrors] = useState(false); - const [executeWatchJsonString, setExecuteWatchJsonString] = useState(''); - const [isShowingExecuteWatchErrors, setIsShowingExecuteWatchErrors] = useState(false); - const [executeWatchErrors, setExecuteWatchErrors] = useState<{ [key: string]: string[] }>({ - simulateExecutionInputOverride: [], - }); - // ace editor requires json to be in string format - const [watchJsonString, setWatchJsonString] = useState( - JSON.stringify(watch.watch, null, 2) + const [selectedTab, setSelectedTab] = useState(WATCH_EDIT_TAB); + const [executeDetails, setExecuteDetails] = useState( + new ExecuteDetails({ + ...EXECUTE_DETAILS_INITIAL_STATE, + actionModes: getActionModes(watchActions), + }) + ); + const executeWatchErrors = executeDetails.validate(); + const hasExecuteWatchErrors = !!Object.keys(executeWatchErrors).find( + errorKey => executeWatchErrors[errorKey].length >= 1 ); return ( @@ -61,6 +117,9 @@ const JsonWatchEditUi = ({ { setSelectedTab(tab.id); + setExecuteDetails( + new ExecuteDetails({ ...executeDetails, actionModes: getActionModes(watchActions) }) + ); }} isSelected={tab.id === selectedTab} key={index} @@ -70,32 +129,17 @@ const JsonWatchEditUi = ({ ))} - {selectedTab === WATCH_TAB_ID_SIMULATE && ( + {selectedTab === WATCH_SIMULATE_TAB && ( setExecuteWatchJsonString(json)} - errors={executeWatchErrors} - setErrors={(errors: { [key: string]: string[] }) => setExecuteWatchErrors(errors)} - isShowingErrors={isShowingExecuteWatchErrors} - setIsShowingErrors={(isShowingErrors: boolean) => - setIsShowingExecuteWatchErrors(isShowingErrors) - } - isDisabled={isShowingExecuteWatchErrors || isShowingWatchErrors} + executeDetails={executeDetails} + setExecuteDetails={(details: ExecutedWatchDetails) => setExecuteDetails(details)} + executeWatchErrors={executeWatchErrors} + hasExecuteWatchErrors={hasExecuteWatchErrors} + watchActions={watchActions} /> )} - {selectedTab === WATCH_TAB_ID_EDIT && ( - setWatchJsonString(json)} - errors={watchErrors} - setErrors={(errors: { [key: string]: string[] }) => setWatchErrors(errors)} - isShowingErrors={isShowingWatchErrors} - setIsShowingErrors={(isShowingErrors: boolean) => - setIsShowingWatchErrors(isShowingErrors) - } - /> + {selectedTab === WATCH_EDIT_TAB && ( + )} ); diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_form.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_form.tsx index b110f64aa365a8..9bfc0ea115e6fc 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_form.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_form.tsx @@ -24,48 +24,23 @@ import { documentationLinks } from '../../../lib/documentation_links'; import { onWatchSave, saveWatch } from '../watch_edit_actions'; import { WatchContext } from './watch_context'; -const JSON_WATCH_IDS = { - ID: 'watchId', - NAME: 'watchName', - JSON: 'watchJson', -}; - -function validateId(id: string) { - const regex = /^[A-Za-z0-9\-\_]+$/; - if (!id) { - return i18n.translate('xpack.watcher.sections.watchEdit.json.error.requiredIdText', { - defaultMessage: 'ID is required', - }); - } else if (!regex.test(id)) { - return i18n.translate('xpack.watcher.sections.watchEdit.json.error.invalidIdText', { - defaultMessage: 'ID can only contain letters, underscores, dashes, and numbers.', - }); - } - return false; -} - export const JsonWatchEditForm = ({ urlService, licenseService, - watchJsonString, - setWatchJsonString, - errors, - setErrors, - isShowingErrors, - setIsShowingErrors, }: { urlService: any; licenseService: any; - watchJsonString: string; - setWatchJsonString: (json: string) => void; - errors: { [key: string]: string[] }; - setErrors: (errors: { [key: string]: string[] }) => void; - isShowingErrors: boolean; - setIsShowingErrors: (isShowingErrors: boolean) => void; }) => { const { watch, setWatchProperty } = useContext(WatchContext); // hooks - const [modal, setModal] = useState<{ message: string } | null>(null); + const [modal, setModal] = useState<{ title: string; message: string } | null>(null); + const { errors } = watch.validate(); + const hasErrors = !!Object.keys(errors).find(errorKey => errors[errorKey].length >= 1); + + if (errors.json.length === 0) { + setWatchProperty('watch', JSON.parse(watch.watchString)); + } + return ( ) => { - const id = e.target.value; - const error = validateId(id); - const newErrors = { ...errors, [JSON_WATCH_IDS.ID]: error ? [error] : [] }; - const isInvalidForm = !!Object.keys(newErrors).find( - errorKey => newErrors[errorKey].length >= 1 - ); - setErrors(newErrors); - setIsShowingErrors(isInvalidForm); - setWatchProperty('id', id); + setWatchProperty('id', e.target.value); + }} + onBlur={() => { + if (!watch.id) { + setWatchProperty('id', ''); + } }} /> ) => { setWatchProperty('name', e.target.value); }} + onBlur={() => { + if (!watch.name) { + setWatchProperty('name', ''); + } + }} /> {i18n.translate('xpack.watcher.sections.watchEdit.json.form.watchJsonLabel', { @@ -138,8 +115,8 @@ export const JsonWatchEditForm = ({ ) } - errorKey={JSON_WATCH_IDS.JSON} - isShowingErrors={isShowingErrors} + errorKey="json" + isShowingErrors={hasErrors} fullWidth errors={errors} > @@ -154,31 +131,9 @@ export const JsonWatchEditForm = ({ defaultMessage: 'Code editor', } )} - value={watchJsonString} + value={watch.watchString} onChange={(json: string) => { - setWatchJsonString(json); - try { - const watchJson = JSON.parse(json); - if (watchJson && typeof watchJson === 'object') { - setWatchProperty('watch', watchJson); - const newErrors = { ...errors, [JSON_WATCH_IDS.JSON]: [] }; - const isInvalidForm = !!Object.keys(newErrors).find( - errorKey => newErrors[errorKey].length >= 1 - ); - setErrors(newErrors); - setIsShowingErrors(isInvalidForm); - } - } catch (e) { - setErrors({ - ...errors, - [JSON_WATCH_IDS.JSON]: [ - i18n.translate('xpack.watcher.sections.watchEdit.json.error.invalidJsonText', { - defaultMessage: 'Invalid JSON', - }), - ], - }); - setIsShowingErrors(true); - } + setWatchProperty('watchString', json); }} /> @@ -187,16 +142,11 @@ export const JsonWatchEditForm = ({ { - const error = validateId(watch.id); - setErrors({ ...errors, [JSON_WATCH_IDS.ID]: error ? [error] : [] }); - setIsShowingErrors(!!error); - if (!error) { - const savedWatch = await onWatchSave(watch, urlService, licenseService); - if (savedWatch && savedWatch.error) { - return setModal(savedWatch.error); - } + const savedWatch = await onWatchSave(watch, urlService, licenseService); + if (savedWatch && savedWatch.error) { + return setModal(savedWatch.error); } }} > diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate.tsx index 1f5e972fcaf0d1..c8d45d7ccc2909 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate.tsx @@ -16,94 +16,48 @@ import { EuiFlexItem, EuiForm, EuiFormRow, + EuiLink, EuiSelect, EuiSpacer, EuiSwitch, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { map } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; import { toastNotifications } from 'ui/notify'; import { ACTION_MODES, TIME_UNITS } from '../../../../common/constants'; -import { getActionType } from '../../../../common/lib/get_action_type'; -import { BaseWatch, ExecutedWatchResults } from '../../../../common/types/watch_types'; +import { ExecutedWatchDetails, ExecutedWatchResults } from '../../../../common/types/watch_types'; import { ErrableFormRow } from '../../../components/form_errors'; import { executeWatch } from '../../../lib/api'; +import { documentationLinks } from '../../../lib/documentation_links'; import { WatchContext } from '../../../sections/watch_edit/components/watch_context'; import { timeUnits } from '../time_units'; import { JsonWatchEditSimulateResults } from './json_watch_edit_simulate_results'; -interface TableDataRow { - actionId: string | undefined; - actionMode: string | undefined; - type: string; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface TableData extends Array {} - -const EXECUTE_DETAILS_INITIAL_STATE = { - triggeredTimeValue: 0, - triggeredTimeUnit: TIME_UNITS.MILLISECOND, - scheduledTimeValue: 0, - scheduledTimeUnit: TIME_UNITS.SECOND, - ignoreCondition: false, -}; - -const INPUT_OVERRIDE_ID = 'simulateExecutionInputOverride'; - -function getTableData(watch: BaseWatch) { - const actions = watch.watch && watch.watch.actions; - return map(actions, (action, actionId) => { - const type = getActionType(action); - return { - actionId, - type, - actionMode: ACTION_MODES.SIMULATE, - }; - }); -} - -function getActionModes(items: TableData) { - const result = items.reduce((itemsAccum: any, item) => { - if (item.actionId) { - itemsAccum[item && item.actionId] = item.actionMode; - } - return itemsAccum; - }, {}); - return result; -} - export const JsonWatchEditSimulate = ({ - executeWatchJsonString, - setExecuteWatchJsonString, - errors, - setErrors, - isShowingErrors, - setIsShowingErrors, - isDisabled, + executeWatchErrors, + hasExecuteWatchErrors, + executeDetails, + setExecuteDetails, + watchActions, }: { - executeWatchJsonString: string; - setExecuteWatchJsonString: (json: string) => void; - errors: { [key: string]: string[] }; - setErrors: (errors: { [key: string]: string[] }) => void; - isShowingErrors: boolean; - setIsShowingErrors: (isShowingErrors: boolean) => void; - isDisabled: boolean; + executeWatchErrors: { [key: string]: string[] }; + hasExecuteWatchErrors: boolean; + executeDetails: ExecutedWatchDetails; + setExecuteDetails: (details: ExecutedWatchDetails) => void; + watchActions: Array<{ + actionId: string; + actionMode: string; + type: string; + }>; }) => { const { watch } = useContext(WatchContext); - const tableData = getTableData(watch); - // hooks - const [executeDetails, setExecuteDetails] = useState( - new ExecuteDetails({ - ...EXECUTE_DETAILS_INITIAL_STATE, - actionModes: getActionModes(tableData), - }) - ); const [executeResults, setExecuteResults] = useState(null); + const { errors: watchErrors } = watch.validate(); + const hasWatchJsonError = watchErrors.json.length >= 1; const columns = [ { @@ -176,14 +130,14 @@ export const JsonWatchEditSimulate = ({ setExecuteResults(null)} /> )}

{i18n.translate('xpack.watcher.sections.watchEdit.simulate.pageDescription', { - defaultMessage: 'Modify the fields below to simulate a watch execution.', + defaultMessage: + 'Use the simulator to override the watch schedule, input results, conditions, and actions.', })}

@@ -195,15 +149,14 @@ export const JsonWatchEditSimulate = ({

{i18n.translate( 'xpack.watcher.sections.watchEdit.simulate.form.triggerOverridesTitle', - { defaultMessage: 'Trigger overrides' } + { defaultMessage: 'Trigger' } )}

} description={i18n.translate( 'xpack.watcher.sections.watchEdit.simulate.form.triggerOverridesDescription', { - defaultMessage: - 'These fields are parsed as the data of the trigger event that will be used during the watch execution.', + defaultMessage: 'Schedule the time and date for starting the watch.', } )} > @@ -317,7 +270,7 @@ export const JsonWatchEditSimulate = ({

{i18n.translate( 'xpack.watcher.sections.watchEdit.simulate.form.inputOverridesTitle', - { defaultMessage: 'Input overrides' } + { defaultMessage: 'Input' } )}

} @@ -325,12 +278,12 @@ export const JsonWatchEditSimulate = ({ 'xpack.watcher.sections.watchEdit.simulate.form.inputOverridesDescription', { defaultMessage: - 'When present, the watch uses this object as a payload instead of executing its own input.', + 'Enter JSON data to override the watch payload that comes from running the input.', } )} > { - setExecuteWatchJsonString(json); - try { - const alternativeInput = json === '' ? undefined : JSON.parse(json); - if ( - typeof alternativeInput === 'undefined' || - (alternativeInput && typeof alternativeInput === 'object') - ) { - setExecuteDetails( - new ExecuteDetails({ - ...executeDetails, - alternativeInput, - }) - ); - setIsShowingErrors(false); - setErrors({ ...errors, [INPUT_OVERRIDE_ID]: [] }); - } - } catch (e) { - setErrors({ - ...errors, - [INPUT_OVERRIDE_ID]: [ - i18n.translate( - 'xpack.watcher.sections.watchEdit.simulate.form.alternativeInputFieldError', - { - defaultMessage: 'Invalid JSON', - } - ), - ], - }); - setIsShowingErrors(true); - } + setExecuteDetails( + new ExecuteDetails({ + ...executeDetails, + alternativeInput: json, + }) + ); }} /> @@ -396,14 +325,15 @@ export const JsonWatchEditSimulate = ({

{i18n.translate( 'xpack.watcher.sections.watchEdit.simulate.form.conditionOverridesTitle', - { defaultMessage: 'Condition overrides' } + { defaultMessage: 'Condition' } )}

} description={i18n.translate( 'xpack.watcher.sections.watchEdit.simulate.form.conditionOverridesDescription', { - defaultMessage: 'When enabled, the watch execution uses the Always Condition.', + defaultMessage: + 'Execute the watch when the condition is met. Otherwise, ignore the condition and run the watch on a fixed schedule.', } )} > @@ -429,17 +359,28 @@ export const JsonWatchEditSimulate = ({

{i18n.translate( 'xpack.watcher.sections.watchEdit.simulate.form.actionOverridesTitle', - { defaultMessage: 'Action overrides' } + { defaultMessage: 'Actions' } )}

} - description={i18n.translate( - 'xpack.watcher.sections.watchEdit.simulate.form.actionOverridesDescription', - { - defaultMessage: - 'The action modes determine how to handle the watch actions as part of the watch execution.', - } - )} + description={ + + {i18n.translate( + 'xpack.watcher.sections.watchEdit.simulate.form.actionOverridesDescription.linkLabel', + { + defaultMessage: 'Learn about actions.', + } + )} + + ), + }} + /> + } > @@ -462,7 +403,7 @@ export const JsonWatchEditSimulate = ({ iconType="play" fill type="submit" - isDisabled={isDisabled} + isDisabled={hasExecuteWatchErrors || hasWatchJsonError} onClick={async () => { try { const executedWatch = await executeWatch(executeDetails, watch); diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate_results.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate_results.tsx index cc67aefe98650e..ce7c0d194bf539 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate_results.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/json_watch_edit_simulate_results.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment, useContext } from 'react'; import { EuiBasicTable, @@ -18,13 +18,10 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; import { WATCH_STATES } from '../../../../common/constants'; -import { - BaseWatch, - ExecutedWatchDetails, - ExecutedWatchResults, -} from '../../../../common/types/watch_types'; +import { ExecutedWatchDetails, ExecutedWatchResults } from '../../../../common/types/watch_types'; +import { getTypeFromAction } from '../watch_edit_actions'; +import { WatchContext } from './watch_context'; const WATCH_ICON_COLORS = { [WATCH_STATES.DISABLED]: 'subdued', @@ -35,36 +32,36 @@ const WATCH_ICON_COLORS = { }; export const JsonWatchEditSimulateResults = ({ - executeDetails, executeResults, + executeDetails, onCloseFlyout, - watch, }: { - executeDetails: ExecutedWatchDetails; executeResults: ExecutedWatchResults; + executeDetails: ExecutedWatchDetails; onCloseFlyout: () => void; - watch: BaseWatch; }) => { + const { watch } = useContext(WatchContext); + const getTableData = () => { - const actions = watch.actions; - const actionStatuses = executeResults.watchStatus.actionStatuses; + const actionStatuses = executeResults.watchStatus && executeResults.watchStatus.actionStatuses; const actionModes = executeDetails.actionModes; - const actionDetails = actions.map(action => { - const actionMode = actionModes[action.id]; - const actionStatus = find(actionStatuses, { id: action.id }); - - return { - actionId: action.id, - actionType: action.type, - actionMode, - actionState: actionStatus && actionStatus.state, - actionReason: actionStatus && actionStatus.lastExecutionReason, - }; - }); - return actionDetails; + const actions = watch.watch && watch.watch.actions; + if (actions) { + return Object.keys(actions).map(actionKey => { + const actionStatus = actionStatuses.find(status => status.id === actionKey); + return { + actionId: actionKey, + actionType: getTypeFromAction(actions[actionKey]), + actionMode: actionModes[actionKey], + actionState: actionStatus && actionStatus.state, + actionReason: actionStatus && actionStatus.lastExecutionReason, + }; + }); + } + return []; }; - const tableData = getTableData(); + const actionsTableData = getTableData(); const columns = [ { @@ -141,19 +138,23 @@ export const JsonWatchEditSimulateResults = ({ - -
- {i18n.translate( - 'xpack.watcher.sections.watchEdit.simulateResults.actionsSectionTitle', - { - defaultMessage: 'Actions', - } - )} -
-
- - - + {actionsTableData && actionsTableData.length > 0 && ( + + +
+ {i18n.translate( + 'xpack.watcher.sections.watchEdit.simulateResults.actionsSectionTitle', + { + defaultMessage: 'Actions', + } + )} +
+
+ + + +
+ )}
{i18n.translate( diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit_component.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit_component.tsx index 01048a7167b60f..839cf93f2a789d 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit_component.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit_component.tsx @@ -138,7 +138,7 @@ const ThresholdWatchEditUi = ({ const [watchThresholdPopoverOpen, setWatchThresholdPopoverOpen] = useState(false); const [watchDurationPopoverOpen, setWatchDurationPopoverOpen] = useState(false); const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); - const [modal, setModal] = useState<{ message: string } | null>(null); + const [modal, setModal] = useState<{ title: string; message: string } | null>(null); const { watch, setWatchProperty } = useContext(WatchContext); const getIndexPatterns = async () => { const { savedObjects } = await savedObjectsClient.find({ diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx index 4b537bfe7e637f..7b2f1f4cc70bb7 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx +++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx @@ -8,6 +8,7 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Watch } from 'plugins/watcher/models/watch'; import React, { useEffect, useReducer } from 'react'; +import { isEqual } from 'lodash'; import { WATCH_TYPES } from '../../../../common/constants'; import { BaseWatch } from '../../../../common/types/watch_types'; import { loadWatch } from '../../../lib/api'; @@ -21,7 +22,7 @@ const getTitle = (watch: BaseWatch) => { return i18n.translate( 'xpack.watcher.sections.watchEdit.json.titlePanel.createNewTypeOfWatchTitle', { - defaultMessage: 'Create a new {typeName}', + defaultMessage: 'Create {typeName}', values: { typeName }, } ); @@ -38,7 +39,12 @@ const watchReducer = (state: any, action: any) => { case 'setWatch': return payload; case 'setProperty': - return new (Watch.getWatchTypes())[state.type]({ ...state, ...payload }); + const { property, value } = payload; + if (isEqual(state[property], value)) { + return state; + } else { + return new (Watch.getWatchTypes())[state.type]({ ...state, [property]: value }); + } case 'addAction': const newWatch = new (Watch.getWatchTypes())[state.type](state); newWatch.addAction(payload); @@ -62,7 +68,7 @@ export const WatchEdit = ({ // hooks const [watch, dispatch] = useReducer(watchReducer, null); const setWatchProperty = (property: string, value: any) => { - dispatch({ command: 'setProperty', payload: { [property]: value } }); + dispatch({ command: 'setProperty', payload: { property, value } }); }; const addAction = (action: any) => { dispatch({ command: 'addAction', payload: action }); diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts b/x-pack/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts index e751e04ac0fed6..cd054dafc34971 100644 --- a/x-pack/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts @@ -14,7 +14,7 @@ import { createWatch, loadWatch } from '../../lib/api'; * Get the type from an action where a key defines its type. * eg: { email: { ... } } | { slack: { ... } } */ -function getTypeFromAction(action: { [key: string]: any }) { +export function getTypeFromAction(action: { [key: string]: any }) { const actionKeys = Object.keys(action); let type; Object.keys(ACTION_TYPES).forEach(k => { @@ -65,7 +65,6 @@ export async function saveWatch(watch: BaseWatch, urlService: any, licenseServic }, }) ); - // TODO: Not correctly redirecting back to /watches route urlService.change('/management/elasticsearch/watcher/watches', {}); } catch (error) { return licenseService @@ -83,6 +82,12 @@ export async function validateActionsAndSaveWatch( if (warning) { return { error: { + title: i18n.translate( + 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.errorValidationTitleText', + { + defaultMessage: 'Save watch?', + } + ), message: warning.message, }, }; @@ -106,27 +111,25 @@ export async function onWatchSave( if (existingWatch) { return { error: { + title: i18n.translate( + 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.existingWatchTitleText', + { + defaultMessage: 'A watch with this ID already exists', + } + ), message: i18n.translate( - 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.descriptionText', + 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.existingWatchDescriptionText', + { + defaultMessage: 'Saving this watch will overwrite previous content.', + } + ), + buttonLabel: i18n.translate( + 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.existingWatchButtonLabel', { - defaultMessage: - 'Watch with ID "{watchId}" {watchNameMessageFragment} already exists. Do you want to overwrite it?', - values: { - watchId: existingWatch.id, - watchNameMessageFragment: existingWatch.name - ? i18n.translate( - 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.descriptionFragmentText', - { - defaultMessage: '(name: "{existingWatchName}")', - values: { - existingWatchName: existingWatch.name, - }, - } - ) - : '', - }, + defaultMessage: 'Overwrite', } ), + buttonType: 'danger', }, }; }