Instead of immediately throwing an error if a watch Action is invalid (an email for example), there is now an `option` object that we can pass to the `fromUpstreamJson()` method and receive back any error that might exist in a Watch. The Watch has a new "watchErrors" property to display configuration error in the UI. fixes #20305 fixes #20970
This commit is contained in:
parent
4329778bdc
commit
500da50a72
|
@ -33,4 +33,9 @@ export const ACTION_STATES = {
|
|||
defaultMessage: 'Error'
|
||||
}),
|
||||
|
||||
// Action has a configuration error
|
||||
CONFIG_ERROR: i18n.translate('xpack.watcher.constants.actionStates.configErrorStateText', {
|
||||
defaultMessage: 'Config error'
|
||||
}),
|
||||
|
||||
};
|
||||
|
|
11
x-pack/plugins/watcher/common/constants/error_codes.js
Normal file
11
x-pack/plugins/watcher/common/constants/error_codes.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const ERROR_CODES = {
|
||||
|
||||
// Property missing on object
|
||||
ERR_PROP_MISSING: 'ERR_PROP_MISSING',
|
||||
};
|
|
@ -22,3 +22,4 @@ export { WATCH_STATE_COMMENTS } from './watch_state_comments';
|
|||
export { WATCH_HISTORY } from './watch_history';
|
||||
export { WATCH_STATES } from './watch_states';
|
||||
export { WATCH_TYPES } from './watch_types';
|
||||
export { ERROR_CODES } from './error_codes';
|
||||
|
|
|
@ -24,4 +24,8 @@ export const WATCH_STATES = {
|
|||
defaultMessage: 'Error!'
|
||||
}),
|
||||
|
||||
CONFIG_ERROR: i18n.translate('xpack.watcher.constants.watchStates.configErrorStateText', {
|
||||
defaultMessage: 'Config error'
|
||||
}),
|
||||
|
||||
};
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
></div>
|
||||
<div
|
||||
class="kuiIcon kuiIcon--error fa-exclamation-triangle"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.ERROR"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.ERROR || actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.CONFIG_ERROR"
|
||||
></div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<div class="kuiModal">
|
||||
<div class="kuiModalHeader">
|
||||
<h1 class="kuiModalHeader__title" id="watcher__error-display-modal-title">{{ vm.title }}</h1>
|
||||
</div>
|
||||
<div class="kuiModalBody">
|
||||
<ul>
|
||||
<li class="kuiVerticalRhythm" ng-repeat="error in vm.errors">{{ error.message }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="kuiModalFooter">
|
||||
<button
|
||||
ng-click="vm.close()"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="close">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.controller('WatcherErrorsDisplayController', function WatcherErrorsDisplayController($scope, $modalInstance, params) {
|
||||
this.title = params.title;
|
||||
this.errors = params.errors;
|
||||
|
||||
this.close = function close() {
|
||||
$modalInstance.close();
|
||||
};
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 './errors_display_modal';
|
|
@ -13,6 +13,6 @@
|
|||
></div>
|
||||
<div
|
||||
class="kuiIcon kuiIcon--error fa-exclamation-triangle"
|
||||
ng-if="watchStateIcon.watchStatus.state === watchStateIcon.WATCH_STATES.ERROR"
|
||||
ng-if="watchStateIcon.watchStatus.state === watchStateIcon.WATCH_STATES.ERROR || watchStateIcon.watchStatus.state === watchStateIcon.WATCH_STATES.CONFIG_ERROR"
|
||||
></div>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,10 @@ export class EmailAction extends BaseAction {
|
|||
Object.assign(result, {
|
||||
to: this.to,
|
||||
subject: this.subject,
|
||||
body: this.body
|
||||
body: this.body,
|
||||
email: {
|
||||
to: this.to.length ? this.to : undefined,
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
|
|
|
@ -17,9 +17,13 @@ export class LoggingAction extends BaseAction {
|
|||
|
||||
get upstreamJson() {
|
||||
const result = super.upstreamJson;
|
||||
const text = !!this.text.trim() ? this.text : undefined;
|
||||
|
||||
Object.assign(result, {
|
||||
text: this.text
|
||||
text,
|
||||
logging: {
|
||||
text,
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
|
|
|
@ -17,12 +17,35 @@ export class SlackAction extends BaseAction {
|
|||
this.text = props.text;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const errors = [];
|
||||
|
||||
if (!this.to.length) {
|
||||
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.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return { errors: errors.length ? errors : null };
|
||||
}
|
||||
|
||||
get upstreamJson() {
|
||||
const result = super.upstreamJson;
|
||||
|
||||
const message = this.text || this.to.length
|
||||
? {
|
||||
text: this.text,
|
||||
to: this.to.length ? this.to : undefined
|
||||
}
|
||||
: undefined;
|
||||
Object.assign(result, {
|
||||
to: this.to,
|
||||
text: this.text
|
||||
text: this.text,
|
||||
slack: {
|
||||
message
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { getSearchValue } from 'plugins/watcher/lib/get_search_value';
|
|||
import { get, isEqual, remove, map, merge } from 'lodash';
|
||||
import { Action } from '../action';
|
||||
import { WatchStatus } from '../watch_status';
|
||||
import { WatchErrors } from '../watch_errors';
|
||||
import { createActionId } from './lib/create_action_id';
|
||||
import { checkActionIdCollision } from './lib/check_action_id_collision';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -31,6 +32,7 @@ export class BaseWatch {
|
|||
this.name = get(props, 'name', '');
|
||||
this.isSystemWatch = Boolean(get(props, 'isSystemWatch'));
|
||||
this.watchStatus = WatchStatus.fromUpstreamJson(get(props, 'watchStatus'));
|
||||
this.watchErrors = WatchErrors.fromUpstreamJson(get(props, 'watchErrors'));
|
||||
|
||||
const actions = get(props, 'actions', []);
|
||||
this.actions = actions.map(Action.fromUpstreamJson);
|
||||
|
@ -76,6 +78,10 @@ export class BaseWatch {
|
|||
remove(this.actions, action);
|
||||
}
|
||||
|
||||
resetActions = () => {
|
||||
this.actions = [];
|
||||
};
|
||||
|
||||
get displayName() {
|
||||
if (this.isNew) {
|
||||
return i18n.translate('xpack.watcher.models.baseWatch.displayName', {
|
||||
|
@ -130,6 +136,44 @@ export class BaseWatch {
|
|||
return isEqual(cleanWatch, cleanOtherWatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client validation of the Watch.
|
||||
* Currently we are *only* validating the Watch "Actions"
|
||||
*/
|
||||
validate() {
|
||||
|
||||
// Get the errors from each watch action
|
||||
const actionsErrors = this.actions.reduce((actionsErrors, action) => {
|
||||
if (action.validate) {
|
||||
const { errors } = action.validate();
|
||||
if (!errors) {
|
||||
return actionsErrors;
|
||||
}
|
||||
return [...actionsErrors, ...errors];
|
||||
}
|
||||
return actionsErrors;
|
||||
}, []);
|
||||
|
||||
if (!actionsErrors.length) {
|
||||
return { warning: null };
|
||||
}
|
||||
|
||||
// Concatenate their message
|
||||
const warningMessage = actionsErrors.reduce((message, error) => (
|
||||
!!message
|
||||
? `${message}, ${error.message}`
|
||||
: error.message
|
||||
), '');
|
||||
|
||||
// We are not doing any *blocking* validation in the client,
|
||||
// so we return the errors as a _warning_
|
||||
return {
|
||||
warning: {
|
||||
message: warningMessage,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static typeName = i18n.translate('xpack.watcher.models.baseWatch.typeName', {
|
||||
defaultMessage: 'Watch',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { WatchErrors } from './watch_errors';
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash';
|
||||
|
||||
export class WatchErrors {
|
||||
constructor(props = {}) {
|
||||
this.actionErrors = get(props, 'actions');
|
||||
}
|
||||
|
||||
static fromUpstreamJson(upstreamWatchErrors) {
|
||||
return new WatchErrors(upstreamWatchErrors);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,9 @@
|
|||
{{ 'xpack.watcher.sections.watchDetail.actionStatusTable.stateColumnLabel' | i18n: { defaultMessage: 'State' } }}
|
||||
</sortable-column>
|
||||
</th>
|
||||
<th scope="col" class="kuiTableHeaderCell" ng-if="actionStatusTable.actionErrors">
|
||||
<span class="kuiTableHeaderCell__liner">Errors</span>
|
||||
</th>
|
||||
<th scope="col" class="kuiTableHeaderCell">
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -39,6 +42,14 @@
|
|||
{{ actionStatus.state }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="kuiTableRowCell" ng-if="actionStatusTable.actionErrors">
|
||||
<div class="kuiTableRowCell__liner">
|
||||
<a
|
||||
role="button"
|
||||
ng-if="actionStatusTable.actionErrors[actionStatus.id]"
|
||||
ng-click="actionStatusTable.showErrors(actionStatus.id, actionStatusTable.actionErrors[actionStatus.id])">{{actionStatusTable.getLabelErrors(actionStatus.id)}}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="kuiTableRowCell">
|
||||
<div class="kuiTableRowCell__liner">
|
||||
<div class="kuiMenuButtonGroup kuiMenuButtonGroup--alignRight">
|
||||
|
|
|
@ -9,20 +9,36 @@ import template from './action_status_table.html';
|
|||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('actionStatusTable', function () {
|
||||
app.directive('actionStatusTable', function ($injector, i18n) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template: template,
|
||||
scope: {
|
||||
actionStatuses: '=',
|
||||
actionErrors: '=',
|
||||
sortField: '=',
|
||||
sortReverse: '=',
|
||||
onSortChange: '=',
|
||||
onActionAcknowledge: '=',
|
||||
showErrors: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controllerAs: 'actionStatusTable',
|
||||
controller: class ActionStatusTableController {}
|
||||
controller: class ActionStatusTableController {
|
||||
getLabelErrors(actionId) {
|
||||
const errors = this.actionErrors[actionId];
|
||||
const total = errors.length;
|
||||
|
||||
const label = i18n('xpack.watcher.sections.watchDetail.actionStatusTotalErrors', {
|
||||
defaultMessage: '{total, number} {total, plural, one {error} other {errors}}',
|
||||
values: {
|
||||
total,
|
||||
}
|
||||
});
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -53,10 +53,12 @@
|
|||
<!-- Action status table -->
|
||||
<action-status-table
|
||||
action-statuses="watchDetail.sortedActionStatuses"
|
||||
action-errors="watchDetail.actionErrors"
|
||||
on-action-acknowledge="watchDetail.onActionAcknowledge"
|
||||
sort-field="watchDetail.actionStatusTableSortField"
|
||||
sort-reverse="watchDetail.actionStatusTableSortReverse"
|
||||
on-sort-change="watchDetail.onActionSortChange"
|
||||
show-errors="watchDetail.showErrors"
|
||||
></action-status-table>
|
||||
<table-info ng-show="!watchDetail.hasActionStatusTableActions">
|
||||
{{ 'xpack.watcher.sections.watchDetail.noActionsFoundText' | i18n: { defaultMessage: 'No actions found.' } }}
|
||||
|
|
|
@ -14,8 +14,10 @@ import 'ui/table_info';
|
|||
import 'plugins/watcher/components/tool_bar_selected_count';
|
||||
import 'plugins/watcher/services/watch';
|
||||
import 'plugins/watcher/services/license';
|
||||
import 'plugins/watcher/components/errors_display_modal';
|
||||
|
||||
import template from './watch_detail.html';
|
||||
import errorsDisplayTemplate from 'plugins/watcher/components/errors_display_modal/errors_display_modal.html';
|
||||
import '../watch_history';
|
||||
import '../action_status_table';
|
||||
import { REFRESH_INTERVALS } from 'plugins/watcher/../common/constants';
|
||||
|
@ -33,6 +35,7 @@ app.directive('watchDetail', function ($injector, i18n) {
|
|||
|
||||
const $filter = $injector.get('$filter');
|
||||
const orderBy = $filter('orderBy');
|
||||
const $modal = $injector.get('$modal');
|
||||
|
||||
moment.tz.setDefault(config.get('dateFormat:tz'));
|
||||
|
||||
|
@ -54,38 +57,39 @@ app.directive('watchDetail', function ($injector, i18n) {
|
|||
|
||||
this.actionStatusTableSortField = 'id';
|
||||
this.actionStatusTableSortReverse = false;
|
||||
this.actionErrors = (this.watch.watchErrors && this.watch.watchErrors.actionErrors) || null;
|
||||
|
||||
this.omitBreadcrumbPages = [
|
||||
'watch',
|
||||
this.watch.id
|
||||
];
|
||||
this.omitBreadcrumbPages = ['watch', this.watch.id];
|
||||
this.breadcrumb = this.watch.displayName;
|
||||
|
||||
// Reload watch history periodically
|
||||
const refreshInterval = $interval(() => this.loadWatchHistory(), REFRESH_INTERVALS.WATCH_HISTORY);
|
||||
const refreshInterval = $interval(
|
||||
() => this.loadWatchHistory(),
|
||||
REFRESH_INTERVALS.WATCH_HISTORY
|
||||
);
|
||||
$scope.$on('$destroy', () => $interval.cancel(refreshInterval));
|
||||
|
||||
// react to data and UI changes
|
||||
$scope.$watchMulti([
|
||||
'watchDetail.actionStatusTableSortField',
|
||||
'watchDetail.actionStatusTableSortReverse',
|
||||
], this.applySortToActionStatusTable);
|
||||
$scope.$watchMulti(
|
||||
['watchDetail.actionStatusTableSortField', 'watchDetail.actionStatusTableSortReverse'],
|
||||
this.applySortToActionStatusTable
|
||||
);
|
||||
}
|
||||
|
||||
loadWatchHistory = () => {
|
||||
return watchService.loadWatchHistory(this.watch.id, this.historyRange)
|
||||
return watchService
|
||||
.loadWatchHistory(this.watch.id, this.historyRange)
|
||||
.then(watchHistoryItems => {
|
||||
this.isHistoryLoading = false;
|
||||
this.watchHistoryItems = watchHistoryItems;
|
||||
})
|
||||
.catch(err => {
|
||||
return licenseService.checkValidity()
|
||||
.then(() => toastNotifications.addDanger(err));
|
||||
return licenseService.checkValidity().then(() => toastNotifications.addDanger(err));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// update the watch history items when the time range changes
|
||||
onHistoryRangeChange = (range) => {
|
||||
onHistoryRangeChange = range => {
|
||||
this.historyRange = range;
|
||||
this.isHistoryLoading = true;
|
||||
return this.loadWatchHistory();
|
||||
|
@ -124,6 +128,33 @@ app.directive('watchDetail', function ($injector, i18n) {
|
|||
});
|
||||
}
|
||||
|
||||
showErrors = (actionId, errors) => {
|
||||
const errorsModal = $modal.open({
|
||||
template: errorsDisplayTemplate,
|
||||
controller: 'WatcherErrorsDisplayController',
|
||||
controllerAs: 'vm',
|
||||
backdrop: 'static',
|
||||
keyboard: true,
|
||||
ariaLabelledBy: 'watcher__error-display-modal-title',
|
||||
resolve: {
|
||||
params: function () {
|
||||
return {
|
||||
title: i18n('xpack.watcher.sections.watchDetail.errorDisplayModalTitleText', {
|
||||
defaultMessage: 'Errors in the "{actionId}" action',
|
||||
values: { actionId } }
|
||||
),
|
||||
errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
errorsModal.result.catch(() => {
|
||||
// We need to add this empty Promise catch to avoid
|
||||
// a console error "Possibly unhandled rejection"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler methods
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@ import '../watch_edit_execute_detail';
|
|||
import '../watch_edit_actions_execute_summary';
|
||||
import '../watch_edit_watch_execute_summary';
|
||||
import 'plugins/watcher/services/license';
|
||||
import { ACTION_TYPES } from '../../../../../common/constants';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
|
@ -100,14 +101,16 @@ app.directive('jsonWatchEdit', function ($injector, i18n) {
|
|||
}
|
||||
|
||||
onWatchSave = () => {
|
||||
this.createActionsForWatch(this.watch);
|
||||
|
||||
if (!this.watch.isNew) {
|
||||
return this.saveWatch();
|
||||
return this.validateAndSaveWatch();
|
||||
}
|
||||
|
||||
return this.isExistingWatch()
|
||||
.then(existingWatch => {
|
||||
if (!existingWatch) {
|
||||
return this.saveWatch();
|
||||
return this.validateAndSaveWatch();
|
||||
}
|
||||
|
||||
const confirmModalOptions = {
|
||||
|
@ -152,6 +155,23 @@ app.directive('jsonWatchEdit', function ($injector, i18n) {
|
|||
});
|
||||
}
|
||||
|
||||
validateAndSaveWatch = () => {
|
||||
const { warning } = this.watch.validate();
|
||||
|
||||
if (warning) {
|
||||
const confirmModalOptions = {
|
||||
onConfirm: this.saveWatch,
|
||||
confirmButtonText: i18n('xpack.watcher.sections.watchEdit.json.watchErrorsWarning.confirmSaveWatch', {
|
||||
defaultMessage: 'Save watch',
|
||||
}),
|
||||
};
|
||||
|
||||
return confirmModal(warning.message, confirmModalOptions);
|
||||
}
|
||||
|
||||
return this.saveWatch();
|
||||
}
|
||||
|
||||
saveWatch = () => {
|
||||
return watchService.saveWatch(this.watch)
|
||||
.then(() => {
|
||||
|
@ -211,6 +231,59 @@ app.directive('jsonWatchEdit', function ($injector, i18n) {
|
|||
// dirtyPrompt.deregister();
|
||||
kbnUrl.change('/management/elasticsearch/watcher/watches', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions instances are not automatically added to the Watch _actions_ Array
|
||||
* when we add them in the Json editor. This method takes takes care of it.
|
||||
*
|
||||
* @param watchModel Watch instance
|
||||
* @return Watch instance
|
||||
*/
|
||||
createActionsForWatch(watchInstance) {
|
||||
watchInstance.resetActions();
|
||||
|
||||
let action;
|
||||
let type;
|
||||
let actionProps;
|
||||
|
||||
Object.keys(watchInstance.watch.actions).forEach((k) => {
|
||||
action = watchInstance.watch.actions[k];
|
||||
type = this.getTypeFromAction(action);
|
||||
actionProps = this.getPropsFromAction(type, action);
|
||||
|
||||
watchInstance.createAction(type, actionProps);
|
||||
});
|
||||
|
||||
return watchInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type from an action where a key defines its type.
|
||||
* eg: { email: { ... } } | { slack: { ... } }
|
||||
*
|
||||
* @param action A raw action object
|
||||
* @return {string} The action type
|
||||
*/
|
||||
getTypeFromAction(action) {
|
||||
const actionKeys = Object.keys(action);
|
||||
let type;
|
||||
|
||||
Object.keys(ACTION_TYPES).forEach((k) => {
|
||||
if (actionKeys.includes(ACTION_TYPES[k])) {
|
||||
type = ACTION_TYPES[k];
|
||||
}
|
||||
});
|
||||
|
||||
return type ? type : ACTION_TYPES.UNKNOWN;
|
||||
}
|
||||
|
||||
getPropsFromAction(type, action) {
|
||||
if (type === ACTION_TYPES.SLACK) {
|
||||
// Slack action has its props inside the "message" object
|
||||
return action[type].message;
|
||||
}
|
||||
return action[type];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,108 +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 expect from 'expect.js';
|
||||
import { Action } from '../action';
|
||||
import { ACTION_TYPES } from '../../../../common/constants';
|
||||
|
||||
describe('action', () => {
|
||||
|
||||
describe('Action', () => {
|
||||
|
||||
describe('fromUpstreamJson factory method', () => {
|
||||
|
||||
let upstreamJson;
|
||||
beforeEach(() => {
|
||||
upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
"logging": {
|
||||
"text": "foo"
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it(`throws an error if no 'id' property in json`, () => {
|
||||
delete upstreamJson.id;
|
||||
expect(Action.fromUpstreamJson).withArgs(upstreamJson)
|
||||
.to.throwError(/must contain an id property/i);
|
||||
});
|
||||
|
||||
it(`throws an error if no 'actionJson' property in json`, () => {
|
||||
delete upstreamJson.actionJson;
|
||||
expect(Action.fromUpstreamJson).withArgs(upstreamJson)
|
||||
.to.throwError(/must contain an actionJson property/i);
|
||||
});
|
||||
|
||||
it('returns correct Action instance', () => {
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
expect(action.id).to.be(upstreamJson.id);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('type getter method', () => {
|
||||
|
||||
it(`returns a value from ACTION_TYPES when there is a valid model class`, () => {
|
||||
const upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
logging: {
|
||||
'text': 'foo'
|
||||
}
|
||||
}
|
||||
};
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
expect(action.type).to.be(ACTION_TYPES.LOGGING);
|
||||
});
|
||||
|
||||
it(`returns ACTION_TYPES.UNKNOWN when there is no valid model class`, () => {
|
||||
const upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
unknown_action_type: {
|
||||
'foo': 'bar'
|
||||
}
|
||||
}
|
||||
};
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
expect(action.type).to.be(ACTION_TYPES.UNKNOWN);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('downstreamJson getter method', () => {
|
||||
|
||||
let upstreamJson;
|
||||
beforeEach(() => {
|
||||
upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
"logging": {
|
||||
"text": "foo"
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('returns correct JSON for client', () => {
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
const json = action.downstreamJson;
|
||||
|
||||
expect(json.id).to.be(action.id);
|
||||
expect(json.type).to.be(action.type);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -26,7 +26,18 @@ export class Action {
|
|||
}
|
||||
|
||||
// From Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
static fromUpstreamJson(json, options = { throwExceptions: {} }) {
|
||||
if (!json.id) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.actionStatus.absenceOfIdPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {id} property',
|
||||
values: {
|
||||
id: 'id'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!json.actionJson) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.action.absenceOfActionJsonPropertyBadRequestMessage', {
|
||||
|
@ -40,13 +51,38 @@ export class Action {
|
|||
|
||||
const type = getActionType(json.actionJson);
|
||||
const ActionType = ActionTypes[type] || UnknownAction;
|
||||
return ActionType.fromUpstreamJson(json);
|
||||
|
||||
const { action, errors } = ActionType.fromUpstreamJson(json, options);
|
||||
const doThrowException = options.throwExceptions.Action !== false;
|
||||
|
||||
if (errors && doThrowException) {
|
||||
this.throwErrors(errors);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
// From Kibana
|
||||
static fromDownstreamJson(json) {
|
||||
static fromDownstreamJson(json, options = { throwExceptions: {} }) {
|
||||
const ActionType = ActionTypes[json.type] || UnknownAction;
|
||||
|
||||
return ActionType.fromDownstreamJson(json);
|
||||
const { action, errors } = ActionType.fromDownstreamJson(json);
|
||||
const doThrowException = options.throwExceptions.Action !== false;
|
||||
|
||||
if (errors && doThrowException) {
|
||||
this.throwErrors(errors);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
static throwErrors(errors) {
|
||||
const allMessages = errors.reduce((message, error) => {
|
||||
if (message) {
|
||||
return `${message}, ${error.message}`;
|
||||
}
|
||||
return error.message;
|
||||
}, '');
|
||||
throw badRequest(allMessages);
|
||||
}
|
||||
}
|
||||
|
|
136
x-pack/plugins/watcher/server/models/action/action.test.js
Normal file
136
x-pack/plugins/watcher/server/models/action/action.test.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 { Action } from './action';
|
||||
import { LoggingAction } from './logging_action';
|
||||
import { ACTION_TYPES } from '../../../common/constants';
|
||||
|
||||
jest.mock('./logging_action', () => ({
|
||||
LoggingAction: {
|
||||
fromUpstreamJson: jest.fn(({ id }) => ({
|
||||
errors: null,
|
||||
action: { id, type: 'logging' },
|
||||
})),
|
||||
}
|
||||
}));
|
||||
|
||||
describe('action', () => {
|
||||
|
||||
describe('Action', () => {
|
||||
|
||||
describe('fromUpstreamJson factory method', () => {
|
||||
|
||||
let upstreamJson;
|
||||
beforeEach(() => {
|
||||
upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
"logging": {
|
||||
"text": "foo"
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it(`throws an error if no 'id' property in json`, () => {
|
||||
delete upstreamJson.id;
|
||||
expect(() => {
|
||||
Action.fromUpstreamJson(upstreamJson);
|
||||
}).toThrowError(/must contain an id property/i);
|
||||
});
|
||||
|
||||
it(`throws an error if no 'actionJson' property in json`, () => {
|
||||
delete upstreamJson.actionJson;
|
||||
expect(() => {
|
||||
Action.fromUpstreamJson(upstreamJson);
|
||||
}).toThrowError(/must contain an actionJson property/i);
|
||||
});
|
||||
|
||||
it(`throws an error if an Action is invalid`, () => {
|
||||
const message = 'Missing prop in Logging Action!';
|
||||
|
||||
LoggingAction.fromUpstreamJson.mockReturnValueOnce({
|
||||
errors: [{ message }],
|
||||
action: {},
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
Action.fromUpstreamJson(upstreamJson);
|
||||
}).toThrowError(message);
|
||||
});
|
||||
|
||||
it('returns correct Action instance', () => {
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
expect(action.id).toBe(upstreamJson.id);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('type getter method', () => {
|
||||
|
||||
it(`returns the correct known Action type`, () => {
|
||||
const options = { throwExceptions: { Action: false } };
|
||||
|
||||
const upstreamLoggingJson = { id: 'action1', actionJson: { logging: {} } };
|
||||
const loggingAction = Action.fromUpstreamJson(upstreamLoggingJson, options);
|
||||
|
||||
const upstreamEmailJson = { id: 'action2', actionJson: { email: {} } };
|
||||
const emailAction = Action.fromUpstreamJson(upstreamEmailJson, options);
|
||||
|
||||
const upstreamSlackJson = { id: 'action3', actionJson: { slack: {} } };
|
||||
const slackAction = Action.fromUpstreamJson(upstreamSlackJson, options);
|
||||
|
||||
expect(loggingAction.type).toBe(ACTION_TYPES.LOGGING);
|
||||
expect(emailAction.type).toBe(ACTION_TYPES.EMAIL);
|
||||
expect(slackAction.type).toBe(ACTION_TYPES.SLACK);
|
||||
});
|
||||
|
||||
it(`returns ACTION_TYPES.UNKNOWN when there is no valid model class`, () => {
|
||||
const upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
unknown_action_type: {
|
||||
'foo': 'bar'
|
||||
}
|
||||
}
|
||||
};
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
expect(action.type).toBe(ACTION_TYPES.UNKNOWN);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('downstreamJson getter method', () => {
|
||||
|
||||
let upstreamJson;
|
||||
beforeEach(() => {
|
||||
upstreamJson = {
|
||||
id: 'my-action',
|
||||
actionJson: {
|
||||
"email": {
|
||||
"to": "elastic@elastic.co"
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('returns correct JSON for client', () => {
|
||||
|
||||
const action = Action.fromUpstreamJson(upstreamJson);
|
||||
|
||||
const json = action.downstreamJson;
|
||||
|
||||
expect(json.id).toBe(action.id);
|
||||
expect(json.type).toBe(action.type);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -8,9 +8,10 @@ import { badRequest } from 'boom';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export class BaseAction {
|
||||
constructor(props) {
|
||||
constructor(props, errors) {
|
||||
this.id = props.id;
|
||||
this.type = props.type;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
get downstreamJson() {
|
||||
|
|
|
@ -4,15 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { badRequest } from 'boom';
|
||||
import { BaseAction } from './base_action';
|
||||
import { ACTION_TYPES } from '../../../common/constants';
|
||||
import { ACTION_TYPES, ERROR_CODES } from '../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export class EmailAction extends BaseAction {
|
||||
constructor(props) {
|
||||
constructor(props, errors) {
|
||||
props.type = ACTION_TYPES.EMAIL;
|
||||
super(props);
|
||||
super(props, errors);
|
||||
|
||||
this.to = props.to;
|
||||
this.subject = props.subject;
|
||||
|
@ -34,14 +33,16 @@ export class EmailAction extends BaseAction {
|
|||
// From Kibana
|
||||
static fromDownstreamJson(json) {
|
||||
const props = super.getPropsFromDownstreamJson(json);
|
||||
const { errors } = this.validateJson(json);
|
||||
|
||||
Object.assign(props, {
|
||||
to: json.to,
|
||||
subject: json.subject,
|
||||
body: json.body
|
||||
body: json.body,
|
||||
});
|
||||
|
||||
return new EmailAction(props);
|
||||
const action = new EmailAction(props, errors);
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
// To Elasticsearch
|
||||
|
@ -70,29 +71,10 @@ export class EmailAction extends BaseAction {
|
|||
// From Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
const props = super.getPropsFromUpstreamJson(json);
|
||||
|
||||
if (!json.actionJson.email) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.emailAction.absenceOfActionJsonEmailPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonEmail} property',
|
||||
values: {
|
||||
actionJsonEmail: 'actionJson.email'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!json.actionJson.email.to) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.emailAction.absenceOfActionJsonEmailToPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonEmailTo} property',
|
||||
values: {
|
||||
actionJsonEmailTo: 'actionJson.email.to'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { errors } = this.validateJson(json.actionJson);
|
||||
|
||||
const optionalFields = {};
|
||||
|
||||
if (json.actionJson.email.subject) {
|
||||
optionalFields.subject = json.actionJson.email.subject;
|
||||
}
|
||||
|
@ -106,7 +88,45 @@ export class EmailAction extends BaseAction {
|
|||
...optionalFields,
|
||||
});
|
||||
|
||||
return new EmailAction(props);
|
||||
const action = new EmailAction(props, errors);
|
||||
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
static validateJson(json) {
|
||||
const errors = [];
|
||||
|
||||
if (!json.email) {
|
||||
const message = i18n.translate('xpack.watcher.models.emailAction.absenceOfActionJsonEmailPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonEmail} property',
|
||||
values: {
|
||||
actionJsonEmail: 'actionJson.email'
|
||||
}
|
||||
});
|
||||
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message
|
||||
});
|
||||
|
||||
json.email = {};
|
||||
}
|
||||
|
||||
if (!json.email.to) {
|
||||
const message = i18n.translate('xpack.watcher.models.emailAction.absenceOfActionJsonEmailToPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonEmailTo} property',
|
||||
values: {
|
||||
actionJsonEmailTo: 'actionJson.email.to'
|
||||
}
|
||||
});
|
||||
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
return { errors: errors.length ? errors : null };
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -4,15 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { badRequest } from 'boom';
|
||||
import { BaseAction } from './base_action';
|
||||
import { ACTION_TYPES } from '../../../common/constants';
|
||||
import { ACTION_TYPES, ERROR_CODES } from '../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export class LoggingAction extends BaseAction {
|
||||
constructor(props) {
|
||||
constructor(props, errors) {
|
||||
props.type = ACTION_TYPES.LOGGING;
|
||||
super(props);
|
||||
super(props, errors);
|
||||
|
||||
this.text = props.text;
|
||||
}
|
||||
|
@ -30,12 +29,14 @@ export class LoggingAction extends BaseAction {
|
|||
// From Kibana
|
||||
static fromDownstreamJson(json) {
|
||||
const props = super.getPropsFromDownstreamJson(json);
|
||||
const { errors } = this.validateJson(json);
|
||||
|
||||
Object.assign(props, {
|
||||
text: json.text
|
||||
});
|
||||
|
||||
return new LoggingAction(props);
|
||||
const action = new LoggingAction(props, errors);
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
// To Elasticsearch
|
||||
|
@ -54,33 +55,46 @@ export class LoggingAction extends BaseAction {
|
|||
// From Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
const props = super.getPropsFromUpstreamJson(json);
|
||||
|
||||
if (!json.actionJson.logging) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.loggingAction.absenceOfActionJsonLoggingPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonLogging} property',
|
||||
values: {
|
||||
actionJsonLogging: 'actionJson.logging'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!json.actionJson.logging.text) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.loggingAction.absenceOfActionJsonLoggingTextPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonLoggingText} property',
|
||||
values: {
|
||||
actionJsonLoggingText: 'actionJson.logging.text'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { errors } = this.validateJson(json.actionJson);
|
||||
|
||||
Object.assign(props, {
|
||||
text: json.actionJson.logging.text
|
||||
});
|
||||
|
||||
return new LoggingAction(props);
|
||||
const action = new LoggingAction(props, errors);
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
static validateJson(json) {
|
||||
const errors = [];
|
||||
|
||||
if (!json.logging) {
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message: i18n.translate('xpack.watcher.models.loggingAction.absenceOfActionJsonLoggingPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonLogging} property',
|
||||
values: {
|
||||
actionJsonLogging: 'actionJson.logging'
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
json.logging = {};
|
||||
}
|
||||
|
||||
if (!json.logging.text) {
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message: i18n.translate('xpack.watcher.models.loggingAction.absenceOfActionJsonLoggingTextPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonLoggingText} property',
|
||||
values: {
|
||||
actionJsonLoggingText: 'actionJson.logging.text'
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return { errors: errors.length ? errors : null };
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -4,15 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { badRequest } from 'boom';
|
||||
import { BaseAction } from './base_action';
|
||||
import { ACTION_TYPES } from '../../../common/constants';
|
||||
import { ACTION_TYPES, ERROR_CODES } from '../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export class SlackAction extends BaseAction {
|
||||
constructor(props) {
|
||||
constructor(props, errors) {
|
||||
props.type = ACTION_TYPES.SLACK;
|
||||
super(props);
|
||||
super(props, errors);
|
||||
|
||||
this.to = props.to;
|
||||
this.text = props.text;
|
||||
|
@ -33,12 +32,15 @@ export class SlackAction extends BaseAction {
|
|||
static fromDownstreamJson(json) {
|
||||
const props = super.getPropsFromDownstreamJson(json);
|
||||
|
||||
const { errors } = this.validateJson(json);
|
||||
|
||||
Object.assign(props, {
|
||||
to: json.to,
|
||||
text: json.text
|
||||
});
|
||||
|
||||
return new SlackAction(props);
|
||||
const action = new SlackAction(props, errors);
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
// To Elasticsearch
|
||||
|
@ -60,45 +62,50 @@ export class SlackAction extends BaseAction {
|
|||
// From Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
const props = super.getPropsFromUpstreamJson(json);
|
||||
|
||||
if (!json.actionJson.slack) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.slackAction.absenceOfActionJsonSlackPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonSlack} property',
|
||||
values: {
|
||||
actionJsonSlack: 'actionJson.slack'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!json.actionJson.slack.message) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.slackAction.absenceOfActionJsonSlackMessagePropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonSlackMessage} property',
|
||||
values: {
|
||||
actionJsonSlackMessage: 'actionJson.slack.message'
|
||||
}
|
||||
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!json.actionJson.slack.message.to) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.slackAction.absenceOfActionJsonSlackMessageToPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonSlackMessageTo} property',
|
||||
values: {
|
||||
actionJsonSlackMessageTo: 'actionJson.slack.message.to'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { errors } = this.validateJson(json.actionJson);
|
||||
|
||||
Object.assign(props, {
|
||||
to: json.actionJson.slack.message.to,
|
||||
text: json.actionJson.slack.message.text
|
||||
});
|
||||
|
||||
return new SlackAction(props);
|
||||
const action = new SlackAction(props, errors);
|
||||
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
static validateJson(json) {
|
||||
const errors = [];
|
||||
|
||||
if (!json.slack) {
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message: i18n.translate('xpack.watcher.models.slackAction.absenceOfActionJsonSlackPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonSlack} property',
|
||||
values: {
|
||||
actionJsonSlack: 'actionJson.slack'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
json.slack = {};
|
||||
}
|
||||
|
||||
if (!json.slack.message) {
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message: i18n.translate('xpack.watcher.models.slackAction.absenceOfActionJsonSlackMessagePropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJsonSlackMessage} property',
|
||||
values: {
|
||||
actionJsonSlackMessage: 'actionJson.slack.message'
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
json.slack.message = {};
|
||||
}
|
||||
|
||||
return { errors: errors.length ? errors : null };
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -4,15 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { badRequest } from 'boom';
|
||||
import { BaseAction } from './base_action';
|
||||
import { ACTION_TYPES } from '../../../common/constants';
|
||||
import { ACTION_TYPES, ERROR_CODES } from '../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export class UnknownAction extends BaseAction {
|
||||
constructor(props) {
|
||||
constructor(props, errors) {
|
||||
props.type = ACTION_TYPES.UNKNOWN;
|
||||
super(props);
|
||||
super(props, errors);
|
||||
|
||||
this.actionJson = props.actionJson;
|
||||
}
|
||||
|
@ -51,23 +50,33 @@ export class UnknownAction extends BaseAction {
|
|||
// From Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
const props = super.getPropsFromUpstreamJson(json);
|
||||
|
||||
if (!json.actionJson) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.unknownAction.absenceOfActionJsonPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJson} property',
|
||||
values: {
|
||||
actionJson: 'actionJson'
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { errors } = this.validateJson(json);
|
||||
|
||||
Object.assign(props, {
|
||||
actionJson: json.actionJson
|
||||
});
|
||||
|
||||
return new UnknownAction(props);
|
||||
const action = new UnknownAction(props, errors);
|
||||
|
||||
return { action, errors };
|
||||
}
|
||||
|
||||
static validateJson(json) {
|
||||
const errors = [];
|
||||
|
||||
if (!json.actionJson) {
|
||||
errors.push({
|
||||
code: ERROR_CODES.ERR_PROP_MISSING,
|
||||
message: i18n.translate('xpack.watcher.models.unknownAction.absenceOfActionJsonPropertyBadRequestMessage', {
|
||||
defaultMessage: 'json argument must contain an {actionJson} property',
|
||||
values: {
|
||||
actionJson: 'actionJson'
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return { errors: errors.length ? errors : null };
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('action_status', () => {
|
|||
'timestamp': '2017-03-01T20:55:49.679Z',
|
||||
'successful': true
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('action_status', () => {
|
|||
});
|
||||
|
||||
it('returns correct ActionStatus instance', () => {
|
||||
const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson);
|
||||
const actionStatus = ActionStatus.fromUpstreamJson({ ...upstreamJson, errors: { foo: 'bar' } });
|
||||
|
||||
expect(actionStatus.id).to.be(upstreamJson.id);
|
||||
expect(actionStatus.lastAcknowledged).to
|
||||
|
@ -68,6 +68,8 @@ describe('action_status', () => {
|
|||
.eql(moment(upstreamJson.actionStatusJson.last_throttle.timestamp));
|
||||
expect(actionStatus.lastSuccessfulExecution).to
|
||||
.eql(moment(upstreamJson.actionStatusJson.last_successful_execution.timestamp));
|
||||
expect(actionStatus.errors).to
|
||||
.eql({ foo: 'bar' });
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -106,6 +108,12 @@ describe('action_status', () => {
|
|||
expect(actionStatus.state).to.be(ACTION_STATES.ERROR);
|
||||
});
|
||||
|
||||
it('correctly calculates ACTION_STATES.CONFIG_ERROR', () => {
|
||||
const actionStatus = ActionStatus.fromUpstreamJson({ ...upstreamJson, errors: { foo: 'bar' } });
|
||||
expect(actionStatus.state).to.be(ACTION_STATES.CONFIG_ERROR);
|
||||
});
|
||||
|
||||
|
||||
it(`correctly calculates ACTION_STATES.OK`, () => {
|
||||
upstreamJson.actionStatusJson.ack.state = 'awaits_successful_execution';
|
||||
const actionStatus = ActionStatus.fromUpstreamJson(upstreamJson);
|
||||
|
|
|
@ -14,6 +14,7 @@ export class ActionStatus {
|
|||
constructor(props) {
|
||||
this.id = props.id;
|
||||
this.actionStatusJson = props.actionStatusJson;
|
||||
this.errors = props.errors;
|
||||
|
||||
this.lastAcknowledged = getMoment(get(this.actionStatusJson, 'ack.timestamp'));
|
||||
this.lastExecution = getMoment(get(this.actionStatusJson, 'last_execution.timestamp'));
|
||||
|
@ -31,6 +32,10 @@ export class ActionStatus {
|
|||
return ACTION_STATES.ERROR;
|
||||
}
|
||||
|
||||
if (this.errors) {
|
||||
return ACTION_STATES.CONFIG_ERROR;
|
||||
}
|
||||
|
||||
if (ackState === 'awaits_successful_execution') {
|
||||
return ACTION_STATES.OK;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import sinon from 'sinon';
|
|||
const actionFromUpstreamJSONMock = sinon.stub();
|
||||
const actionFromDownstreamJSONMock = sinon.stub();
|
||||
const watchStatusFromUpstreamJSONMock = sinon.stub();
|
||||
const watchErrorsFromUpstreamJSONMock = sinon.stub();
|
||||
|
||||
class ActionStub {
|
||||
static fromUpstreamJson(...args) {
|
||||
actionFromUpstreamJSONMock(...args);
|
||||
|
@ -30,9 +32,17 @@ class WatchStatusStub {
|
|||
}
|
||||
}
|
||||
|
||||
class WatchErrorsStub {
|
||||
static fromUpstreamJson(...args) {
|
||||
watchErrorsFromUpstreamJSONMock(...args);
|
||||
return { foo: 'bar' };
|
||||
}
|
||||
}
|
||||
|
||||
const { BaseWatch } = proxyquire('../base_watch', {
|
||||
'../action': { Action: ActionStub },
|
||||
'../watch_status': { WatchStatus: WatchStatusStub }
|
||||
'../watch_status': { WatchStatus: WatchStatusStub },
|
||||
'../watch_errors': { WatchErrors: WatchErrorsStub },
|
||||
});
|
||||
|
||||
describe('BaseWatch', () => {
|
||||
|
@ -57,6 +67,7 @@ describe('BaseWatch', () => {
|
|||
'type',
|
||||
'isSystemWatch',
|
||||
'watchStatus',
|
||||
'watchErrors',
|
||||
'actions'
|
||||
];
|
||||
|
||||
|
@ -72,6 +83,7 @@ describe('BaseWatch', () => {
|
|||
it('populates all expected fields', () => {
|
||||
props.watchStatus = 'bar';
|
||||
props.actions = 'baz';
|
||||
props.watchErrors = { actions: 'email' };
|
||||
|
||||
const actual = new BaseWatch(props);
|
||||
const expected = {
|
||||
|
@ -80,6 +92,7 @@ describe('BaseWatch', () => {
|
|||
type: 'logging',
|
||||
isSystemWatch: false,
|
||||
watchStatus: 'bar',
|
||||
watchErrors: { actions: 'email' },
|
||||
actions: 'baz'
|
||||
};
|
||||
|
||||
|
@ -169,6 +182,12 @@ describe('BaseWatch', () => {
|
|||
prop2: 'prop2'
|
||||
}
|
||||
},
|
||||
watchErrors: {
|
||||
downstreamJson: {
|
||||
prop1: 'prop1',
|
||||
prop2: 'prop2'
|
||||
}
|
||||
},
|
||||
actions: [{
|
||||
downstreamJson: {
|
||||
prop1: 'prop3',
|
||||
|
@ -188,14 +207,16 @@ describe('BaseWatch', () => {
|
|||
type: props.type,
|
||||
isSystemWatch: false,
|
||||
watchStatus: props.watchStatus.downstreamJson,
|
||||
watchErrors: props.watchErrors.downstreamJson,
|
||||
actions: props.actions.map(a => a.downstreamJson)
|
||||
};
|
||||
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should respect an undefined watchStatus prop', () => {
|
||||
it('should respect an undefined watchStatus & watchErrors prop', () => {
|
||||
delete props.watchStatus;
|
||||
delete props.watchErrors;
|
||||
|
||||
const watch = new BaseWatch(props);
|
||||
const actual = watch.downstreamJson;
|
||||
|
@ -206,6 +227,7 @@ describe('BaseWatch', () => {
|
|||
type: props.type,
|
||||
isSystemWatch: false,
|
||||
watchStatus: undefined,
|
||||
watchErrors: undefined,
|
||||
actions: props.actions.map(a => a.downstreamJson)
|
||||
};
|
||||
|
||||
|
@ -397,6 +419,7 @@ describe('BaseWatch', () => {
|
|||
'name',
|
||||
'watchJson',
|
||||
'watchStatus',
|
||||
'watchErrors',
|
||||
'actions'
|
||||
];
|
||||
|
||||
|
@ -456,6 +479,9 @@ describe('BaseWatch', () => {
|
|||
state: {
|
||||
active: true
|
||||
}
|
||||
},
|
||||
watchErrors: {
|
||||
foo: 'bar'
|
||||
}
|
||||
})).to.be(true);
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { badRequest } from 'boom';
|
|||
import { Action } from '../action';
|
||||
import { WatchStatus } from '../watch_status';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { WatchErrors } from '../watch_errors';
|
||||
|
||||
export class BaseWatch {
|
||||
// This constructor should not be used directly.
|
||||
|
@ -22,6 +23,7 @@ export class BaseWatch {
|
|||
this.isSystemWatch = false;
|
||||
|
||||
this.watchStatus = props.watchStatus;
|
||||
this.watchErrors = props.watchErrors;
|
||||
this.actions = props.actions;
|
||||
}
|
||||
|
||||
|
@ -57,6 +59,7 @@ export class BaseWatch {
|
|||
type: this.type,
|
||||
isSystemWatch: this.isSystemWatch,
|
||||
watchStatus: this.watchStatus ? this.watchStatus.downstreamJson : undefined,
|
||||
watchErrors: this.watchErrors ? this.watchErrors.downstreamJson : undefined,
|
||||
actions: map(this.actions, (action) => action.downstreamJson)
|
||||
};
|
||||
|
||||
|
@ -87,7 +90,7 @@ export class BaseWatch {
|
|||
}
|
||||
|
||||
// from Elasticsearch
|
||||
static getPropsFromUpstreamJson(json) {
|
||||
static getPropsFromUpstreamJson(json, options) {
|
||||
if (!json.id) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.baseWatch.absenceOfIdPropertyBadRequestMessage', {
|
||||
|
@ -135,12 +138,15 @@ export class BaseWatch {
|
|||
|
||||
const actionsJson = get(watchJson, 'actions', {});
|
||||
const actions = map(actionsJson, (actionJson, actionId) => {
|
||||
return Action.fromUpstreamJson({ id: actionId, actionJson });
|
||||
return Action.fromUpstreamJson({ id: actionId, actionJson }, options);
|
||||
});
|
||||
|
||||
const watchErrors = WatchErrors.fromUpstreamJson(this.getWatchErrors(actions));
|
||||
|
||||
const watchStatus = WatchStatus.fromUpstreamJson({
|
||||
id,
|
||||
watchStatusJson
|
||||
watchStatusJson,
|
||||
watchErrors,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -148,7 +154,31 @@ export class BaseWatch {
|
|||
name,
|
||||
watchJson,
|
||||
watchStatus,
|
||||
watchErrors,
|
||||
actions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the errors in the watch
|
||||
*
|
||||
* @param {array} actions - Watch actions
|
||||
*/
|
||||
static getWatchErrors(actions) {
|
||||
const watchErrors = {};
|
||||
|
||||
// Check for errors in Actions
|
||||
const actionsErrors = actions.reduce((acc, action) => {
|
||||
if (action.errors) {
|
||||
acc[action.id] = action.errors;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(actionsErrors).length) {
|
||||
watchErrors.actions = actionsErrors;
|
||||
}
|
||||
|
||||
return watchErrors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ export class JsonWatch extends BaseWatch {
|
|||
}
|
||||
|
||||
// From Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
const baseProps = super.getPropsFromUpstreamJson(json);
|
||||
static fromUpstreamJson(json, options) {
|
||||
const baseProps = super.getPropsFromUpstreamJson(json, options);
|
||||
const watch = cloneDeep(baseProps.watchJson);
|
||||
|
||||
if (has(watch, 'metadata.name')) {
|
||||
|
|
|
@ -50,7 +50,7 @@ export class Watch {
|
|||
}
|
||||
|
||||
// from Elasticsearch
|
||||
static fromUpstreamJson(json) {
|
||||
static fromUpstreamJson(json, options) {
|
||||
if (!json.watchJson) {
|
||||
throw badRequest(
|
||||
i18n.translate('xpack.watcher.models.watch.absenceOfWatchJsonPropertyBadRequestMessage', {
|
||||
|
@ -65,6 +65,6 @@ export class Watch {
|
|||
const type = getWatchType(json.watchJson);
|
||||
const WatchType = WatchTypes[type];
|
||||
|
||||
return WatchType.fromUpstreamJson(json);
|
||||
return WatchType.fromUpstreamJson(json, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { WatchErrors } from './watch_errors';
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export class WatchErrors {
|
||||
constructor({ actions } = {}) {
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
// generate object to send to kibana
|
||||
get downstreamJson() {
|
||||
const json = {
|
||||
actions: this.actions,
|
||||
};
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
// generate object from elasticsearch response
|
||||
static fromUpstreamJson(sections) {
|
||||
return new WatchErrors(sections);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { WatchErrors } from './watch_errors';
|
||||
|
||||
describe('watch_errors', () => {
|
||||
describe('WatchErrors constructor', () => {
|
||||
it('should set "actions" error', () => {
|
||||
const watchErrors1 = new WatchErrors();
|
||||
const watchErrors2 = new WatchErrors({ actions: { foo: 'bar' } });
|
||||
|
||||
expect(watchErrors1.actions).toEqual(undefined);
|
||||
expect(watchErrors2.actions).toEqual({ foo: 'bar' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromUpstreamJson()', () => {
|
||||
it('should return WatchErrors instance', () => {
|
||||
const instance = WatchErrors.fromUpstreamJson();
|
||||
|
||||
expect(instance instanceof WatchErrors).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass errors secctions to the constructor', () => {
|
||||
const instance = WatchErrors.fromUpstreamJson({ actions: { foo: 'bar' } });
|
||||
|
||||
expect(instance.actions).toEqual({ foo: 'bar' });
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -73,7 +73,8 @@ describe('watch_history_item', () => {
|
|||
state: {
|
||||
active: upstreamJson.watchHistoryItemJson.status.state.active
|
||||
}
|
||||
}
|
||||
},
|
||||
watchErrors: {}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -252,6 +252,17 @@ describe('watch_status', () => {
|
|||
expect(watchStatus.state).to.be(WATCH_STATES.ERROR);
|
||||
});
|
||||
|
||||
it('correctly calculates WATCH_STATE.CONFIG_ERROR', () => {
|
||||
const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson);
|
||||
|
||||
watchStatus.actionStatuses = [
|
||||
{ state: ACTION_STATES.OK },
|
||||
{ state: ACTION_STATES.CONFIG_ERROR }
|
||||
];
|
||||
|
||||
expect(watchStatus.state).to.be(WATCH_STATES.CONFIG_ERROR);
|
||||
});
|
||||
|
||||
it(`correctly calculates WATCH_STATES.DISABLED when watch is inactive`, () => {
|
||||
const watchStatus = WatchStatus.fromUpstreamJson(upstreamJson);
|
||||
watchStatus.isActive = false;
|
||||
|
|
|
@ -31,6 +31,7 @@ export class WatchStatus {
|
|||
this.id = props.id;
|
||||
this.watchState = props.state;
|
||||
this.watchStatusJson = props.watchStatusJson;
|
||||
this.watchErrors = props.watchErrors || {};
|
||||
|
||||
this.isActive = Boolean(get(this.watchStatusJson, 'state.active'));
|
||||
this.lastChecked = getMoment(get(this.watchStatusJson, 'last_checked'));
|
||||
|
@ -38,7 +39,11 @@ export class WatchStatus {
|
|||
|
||||
const actionStatusesJson = get(this.watchStatusJson, 'actions', {});
|
||||
this.actionStatuses = map(actionStatusesJson, (actionStatusJson, id) => {
|
||||
const json = { id, actionStatusJson };
|
||||
const json = {
|
||||
id,
|
||||
actionStatusJson,
|
||||
errors: this.watchErrors.actions && this.watchErrors.actions[id],
|
||||
};
|
||||
return ActionStatus.fromUpstreamJson(json);
|
||||
});
|
||||
}
|
||||
|
@ -58,6 +63,10 @@ export class WatchStatus {
|
|||
return WATCH_STATES.ERROR;
|
||||
}
|
||||
|
||||
if (totals[ACTION_STATES.CONFIG_ERROR] > 0) {
|
||||
return WATCH_STATES.CONFIG_ERROR;
|
||||
}
|
||||
|
||||
const firingTotal = totals[ACTION_STATES.FIRING] + totals[ACTION_STATES.ACKNOWLEDGED] +
|
||||
totals[ACTION_STATES.THROTTLED];
|
||||
|
||||
|
|
|
@ -30,16 +30,20 @@ export function registerLoadRoute(server) {
|
|||
const id = request.params.id;
|
||||
|
||||
return fetchWatch(callWithRequest, id)
|
||||
.then((hit) => {
|
||||
.then(hit => {
|
||||
const watchJson = get(hit, 'watch');
|
||||
const watchStatusJson = get(hit, 'status');
|
||||
const json = {
|
||||
id,
|
||||
watchJson,
|
||||
watchStatusJson
|
||||
watchStatusJson,
|
||||
};
|
||||
|
||||
const watch = Watch.fromUpstreamJson(json);
|
||||
const watch = Watch.fromUpstreamJson(json, {
|
||||
throwExceptions: {
|
||||
Action: false,
|
||||
},
|
||||
});
|
||||
return {
|
||||
watch: watch.downstreamJson
|
||||
};
|
||||
|
@ -48,7 +52,7 @@ export function registerLoadRoute(server) {
|
|||
// Case: Error from Elasticsearch JS client
|
||||
if (isEsError(err)) {
|
||||
const statusCodeToMessageMap = {
|
||||
404: `Watch with id = ${id} not found`
|
||||
404: `Watch with id = ${id} not found`,
|
||||
};
|
||||
throw wrapEsError(err, statusCodeToMessageMap);
|
||||
}
|
||||
|
|
|
@ -44,11 +44,18 @@ export function registerListRoute(server) {
|
|||
const watchJson = get(hit, '_source');
|
||||
const watchStatusJson = get(hit, '_source.status');
|
||||
|
||||
return Watch.fromUpstreamJson({
|
||||
id,
|
||||
watchJson,
|
||||
watchStatusJson
|
||||
});
|
||||
return Watch.fromUpstreamJson(
|
||||
{
|
||||
id,
|
||||
watchJson,
|
||||
watchStatusJson,
|
||||
},
|
||||
{
|
||||
throwExceptions: {
|
||||
Action: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
Loading…
Reference in a new issue