Add request flyouts to JSON watch form and Threshold Watch edit form. (#43232) (#46634)

* Refactor watch serialization logic into common serialization functions.
  - Refactor build helpers to accept specific arguments instead of the entire watch object, to make dependencies more obvious.
  - Move action models into common directory.
* Remove boom error reporting from action models because this is an unnecessary level of defensiveness since we control the UI that consumes this API.
* Convert tests from Mocha to Jest.
  - Remove mocks and fix assertions that depended upon mocked dependencies. These assertions were low-value because they tested implementation details.
  - Remove other assertions based upon implementation details.
* Remove serializeMonitoringWatch logic, since Monitoring doesn't use the create endpoint.
This commit is contained in:
CJ Cenizal 2019-09-25 15:10:57 -07:00 committed by GitHub
parent b312d9f140
commit 2914360a0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1484 additions and 1432 deletions

View file

@ -6,8 +6,6 @@
export const WATCH_TYPES: { [key: string]: string } = {
JSON: 'json',
THRESHOLD: 'threshold',
MONITORING: 'monitoring',
};

View file

@ -0,0 +1,9 @@
/*
* 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 declare function serializeJsonWatch(name: string, json: any): any;
export declare function serializeThresholdWatch(config: any): any;
export declare function buildInput(config: any): any;

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { singleLineScript } from './single_line_script';
export { serializeJsonWatch } from './serialize_json_watch';
export { serializeThresholdWatch } from './serialize_threshold_watch';
export { buildInput } from './serialization_helpers';

View file

@ -5,15 +5,17 @@
*/
import { forEach } from 'lodash';
import { Action } from '../../../models/action';
/*
watch.actions
*/
export function buildActions({ actions }) {
export function buildActions(actions) {
const result = {};
forEach(actions, (action) => {
Object.assign(result, action.upstreamJson);
const actionModel = Action.fromDownstreamJson(action);
Object.assign(result, actionModel.upstreamJson);
});
return result;

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { singleLineScript } from '../lib/single_line_script';
import { singleLineScript } from './single_line_script';
import { COMPARATORS } from '../../../../common/constants';
const { BETWEEN } = COMPARATORS;
/*
watch.condition.script.inline
*/
function buildInline({ aggType, thresholdComparator, hasTermsAgg }) {
function buildInline(aggType, thresholdComparator, hasTermsAgg) {
let script = '';
if (aggType === 'count' && !hasTermsAgg) {
@ -113,7 +113,7 @@ function buildInline({ aggType, thresholdComparator, hasTermsAgg }) {
/*
watch.condition.script.params
*/
function buildParams({ threshold }) {
function buildParams(threshold) {
return {
threshold
};
@ -122,11 +122,11 @@ function buildParams({ threshold }) {
/*
watch.condition
*/
export function buildCondition(watch) {
export function buildCondition({ aggType, thresholdComparator, hasTermsAgg, threshold }) {
return {
script: {
source: buildInline(watch),
params: buildParams(watch)
source: buildInline(aggType, thresholdComparator, hasTermsAgg),
params: buildParams(threshold)
}
};
}

View file

@ -9,7 +9,7 @@ import { set } from 'lodash';
/*
watch.input.search.request.indices
*/
function buildIndices({ index }) {
function buildIndices(index) {
if (Array.isArray(index)) {
return index;
}
@ -22,7 +22,7 @@ function buildIndices({ index }) {
/*
watch.input.search.request.body.query.bool.filter.range
*/
function buildRange({ timeWindowSize, timeWindowUnit, timeField }) {
function buildRange(timeWindowSize, timeWindowUnit, timeField) {
return {
[timeField]: {
gte: `{{ctx.trigger.scheduled_time}}||-${timeWindowSize}${timeWindowUnit}`,
@ -35,12 +35,12 @@ function buildRange({ timeWindowSize, timeWindowUnit, timeField }) {
/*
watch.input.search.request.body.query
*/
function buildQuery(watch) {
function buildQuery(timeWindowSize, timeWindowUnit, timeField) {
//TODO: This is where a saved search would be applied
return {
bool: {
filter: {
range: buildRange(watch)
range: buildRange(timeWindowSize, timeWindowUnit, timeField)
}
}
};
@ -49,7 +49,7 @@ function buildQuery(watch) {
/*
watch.input.search.request.body.aggs
*/
function buildAggs({ aggType, aggField, termField, termSize, termOrder }) {
function buildAggs(aggType, aggField, termField, termSize, termOrder) {
if (aggType === 'count' && !termField) {
return null;
}
@ -107,13 +107,13 @@ function buildAggs({ aggType, aggField, termField, termSize, termOrder }) {
/*
watch.input.search.request.body
*/
function buildBody(watch) {
function buildBody(timeWindowSize, timeWindowUnit, timeField, aggType, aggField, termField, termSize, termOrder) {
const result = {
size: 0,
query: buildQuery(watch)
query: buildQuery(timeWindowSize, timeWindowUnit, timeField)
};
const aggs = buildAggs(watch);
const aggs = buildAggs(aggType, aggField, termField, termSize, termOrder);
if (Boolean(aggs)) {
result.aggs = aggs;
}
@ -124,12 +124,12 @@ function buildBody(watch) {
/*
watch.input
*/
export function buildInput(watch) {
export function buildInput({ index, timeWindowSize, timeWindowUnit, timeField, aggType, aggField, termField, termSize, termOrder }) {
return {
search: {
request: {
body: buildBody(watch),
indices: buildIndices(watch)
body: buildBody(timeWindowSize, timeWindowUnit, timeField, aggType, aggField, termField, termSize, termOrder),
indices: buildIndices(index)
}
}
};

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
/*
watch.metadata
*/
export function buildMetadata({
index,
timeField,
triggerIntervalSize,
triggerIntervalUnit,
aggType,
aggField,
termSize,
termField,
thresholdComparator,
timeWindowSize,
timeWindowUnit,
threshold,
}) {
return {
watcherui: {
index,
time_field: timeField,
trigger_interval_size: triggerIntervalSize,
trigger_interval_unit: triggerIntervalUnit,
agg_type: aggType,
agg_field: aggField,
term_size: termSize,
term_field: termField,
threshold_comparator: thresholdComparator,
time_window_size: timeWindowSize,
time_window_unit: timeWindowUnit,
threshold,
}
};
}

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { singleLineScript } from '../lib/single_line_script';
import { singleLineScript } from './single_line_script';
import { COMPARATORS } from '../../../../common/constants';
const { BETWEEN } = COMPARATORS;
/*
watch.transform.script.inline
*/
function buildInline({ aggType, hasTermsAgg, thresholdComparator }) {
function buildInline(aggType, thresholdComparator, hasTermsAgg) {
let script = '';
if (aggType === 'count' && !hasTermsAgg) {
@ -119,7 +119,7 @@ function buildInline({ aggType, hasTermsAgg, thresholdComparator }) {
/*
watch.transform.script.params
*/
function buildParams({ threshold }) {
function buildParams(threshold) {
return {
threshold
};
@ -128,11 +128,11 @@ function buildParams({ threshold }) {
/*
watch.transform
*/
export function buildTransform(watch) {
export function buildTransform({ aggType, thresholdComparator, hasTermsAgg, threshold }) {
return {
script: {
source: buildInline(watch),
params: buildParams(watch)
source: buildInline(aggType, thresholdComparator, hasTermsAgg),
params: buildParams(threshold)
}
};
}

View file

@ -7,14 +7,10 @@
/*
watch.trigger.schedule
*/
function buildSchedule({ triggerIntervalSize, triggerIntervalUnit }) {
export function buildTrigger(triggerIntervalSize, triggerIntervalUnit) {
return {
interval: `${triggerIntervalSize}${triggerIntervalUnit}`
};
}
export function buildTrigger(watch) {
return {
schedule: buildSchedule(watch)
schedule: {
interval: `${triggerIntervalSize}${triggerIntervalUnit}`
},
};
}

View file

@ -0,0 +1,12 @@
/*
* 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 { buildActions } from './build_actions';
export { buildCondition } from './build_condition';
export { buildInput } from './build_input';
export { buildMetadata } from './build_metadata';
export { buildTransform } from './build_transform';
export { buildTrigger } from './build_trigger';

View file

@ -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.
*/
import { set } from 'lodash';
import { WATCH_TYPES } from '../../constants';
export function serializeJsonWatch(name, json) {
// We don't want to overwrite any metadata provided by the consumer.
const { metadata = {} } = json;
set(metadata, 'xpack.type', WATCH_TYPES.JSON);
const serializedWatch = {
...json,
metadata,
};
if (name) {
serializedWatch.metadata.name = name;
}
return serializedWatch;
}

View file

@ -0,0 +1,43 @@
/*
* 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 { serializeJsonWatch } from './serialize_json_watch';
describe('serializeJsonWatch', () => {
it('serializes with name', () => {
expect(serializeJsonWatch('test', { foo: 'bar' })).toEqual({
foo: 'bar',
metadata: {
name: 'test',
xpack: {
type: 'json',
},
},
});
});
it('serializes without name', () => {
expect(serializeJsonWatch(undefined, { foo: 'bar' })).toEqual({
foo: 'bar',
metadata: {
xpack: {
type: 'json',
},
},
});
});
it('respects provided metadata', () => {
expect(serializeJsonWatch(undefined, { metadata: { foo: 'bar' } })).toEqual({
metadata: {
foo: 'bar',
xpack: {
type: 'json',
},
},
});
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 { WATCH_TYPES } from '../../constants';
import {
buildActions,
buildCondition,
buildInput,
buildMetadata,
buildTransform,
buildTrigger,
} from './serialization_helpers';
export function serializeThresholdWatch({
name,
triggerIntervalSize,
triggerIntervalUnit,
index,
timeWindowSize,
timeWindowUnit,
timeField,
aggType,
aggField,
termField,
termSize,
termOrder,
thresholdComparator,
hasTermsAgg,
threshold,
actions,
includeMetadata = true,
}) {
const serializedWatch = {
trigger: buildTrigger(triggerIntervalSize, triggerIntervalUnit),
input: buildInput({ index, timeWindowSize, timeWindowUnit, timeField, aggType, aggField, termField, termSize, termOrder }),
condition: buildCondition({ aggType, thresholdComparator, hasTermsAgg, threshold }),
transform: buildTransform({ aggType, thresholdComparator, hasTermsAgg, threshold }),
actions: buildActions(actions),
};
if (includeMetadata) {
serializedWatch.metadata = {
xpack: {
type: WATCH_TYPES.THRESHOLD,
},
...buildMetadata({
index,
timeField,
triggerIntervalSize,
triggerIntervalUnit,
aggType,
aggField,
termSize,
termField,
thresholdComparator,
timeWindowSize,
timeWindowUnit,
threshold,
}),
};
if (name) {
serializedWatch.metadata.name = name;
}
}
return serializedWatch;
}

View file

@ -0,0 +1,314 @@
/*
* 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 { serializeThresholdWatch } from './serialize_threshold_watch';
describe('serializeThresholdWatch', () => {
it('serializes with name', () => {
expect(serializeThresholdWatch({
name: 'test',
triggerIntervalSize: 10,
triggerIntervalUnit: 's',
index: 'myIndex',
timeWindowSize: 20,
timeWindowUnit: 's',
timeField: 'myTimeField',
aggType: 'myAggType',
aggField: 'myAggField',
termField: 'myTermField',
termSize: 30,
termOrder: 40,
thresholdComparator: 'between',
hasTermsAgg: true,
threshold: 50,
actions: [],
})).toEqual({
trigger: {
schedule: {
interval: '10s',
},
},
input: {
search: {
request: {
body: {
size: 0,
query: {
bool: {
filter: {
range: {
myTimeField: {
gte: '{{ctx.trigger.scheduled_time}}||-20s',
lte: '{{ctx.trigger.scheduled_time}}',
format: 'strict_date_optional_time||epoch_millis',
},
},
},
},
},
aggs: {
bucketAgg: {
terms: {
field: 'myTermField',
size: 30,
order: {
metricAgg: 40,
},
},
aggs: {
metricAgg: {
myAggType: {
field: 'myAggField',
},
},
},
},
},
},
indices: [
'myIndex',
],
},
},
},
condition: {
script: {
// eslint-disable-next-line max-len
source: 'ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; for (int i = 0; i < arr.length; i++) { if (arr[i][\'metricAgg\'].value >= params.threshold[0] && arr[i][\'metricAgg\'].value <= params.threshold[1]) { return true; } } return false;',
params: {
threshold: 50,
},
},
},
transform: {
script: {
// eslint-disable-next-line max-len
source: 'HashMap result = new HashMap(); ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; ArrayList filteredHits = new ArrayList(); for (int i = 0; i < arr.length; i++) { HashMap filteredHit = new HashMap(); filteredHit.key = arr[i].key; filteredHit.value = arr[i][\'metricAgg\'].value; if (filteredHit.value >= params.threshold[0] && filteredHit.value <= params.threshold[1]) { filteredHits.add(filteredHit); } } result.results = filteredHits; return result;',
params: {
threshold: 50,
},
},
},
actions: {},
metadata: {
xpack: {
type: 'threshold',
},
watcherui: {
index: 'myIndex',
time_field: 'myTimeField',
trigger_interval_size: 10,
trigger_interval_unit: 's',
agg_type: 'myAggType',
agg_field: 'myAggField',
term_size: 30,
term_field: 'myTermField',
threshold_comparator: 'between',
time_window_size: 20,
time_window_unit: 's',
threshold: 50,
},
name: 'test',
},
});
});
it('serializes without name', () => {
expect(serializeThresholdWatch({
triggerIntervalSize: 10,
triggerIntervalUnit: 's',
index: 'myIndex',
timeWindowSize: 20,
timeWindowUnit: 's',
timeField: 'myTimeField',
aggType: 'myAggType',
aggField: 'myAggField',
termField: 'myTermField',
termSize: 30,
termOrder: 40,
thresholdComparator: 'between',
hasTermsAgg: true,
threshold: 50,
actions: [],
})).toEqual({
trigger: {
schedule: {
interval: '10s',
},
},
input: {
search: {
request: {
body: {
size: 0,
query: {
bool: {
filter: {
range: {
myTimeField: {
gte: '{{ctx.trigger.scheduled_time}}||-20s',
lte: '{{ctx.trigger.scheduled_time}}',
format: 'strict_date_optional_time||epoch_millis',
},
},
},
},
},
aggs: {
bucketAgg: {
terms: {
field: 'myTermField',
size: 30,
order: {
metricAgg: 40,
},
},
aggs: {
metricAgg: {
myAggType: {
field: 'myAggField',
},
},
},
},
},
},
indices: [
'myIndex',
],
},
},
},
condition: {
script: {
// eslint-disable-next-line max-len
source: 'ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; for (int i = 0; i < arr.length; i++) { if (arr[i][\'metricAgg\'].value >= params.threshold[0] && arr[i][\'metricAgg\'].value <= params.threshold[1]) { return true; } } return false;',
params: {
threshold: 50,
},
},
},
transform: {
script: {
// eslint-disable-next-line max-len
source: 'HashMap result = new HashMap(); ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; ArrayList filteredHits = new ArrayList(); for (int i = 0; i < arr.length; i++) { HashMap filteredHit = new HashMap(); filteredHit.key = arr[i].key; filteredHit.value = arr[i][\'metricAgg\'].value; if (filteredHit.value >= params.threshold[0] && filteredHit.value <= params.threshold[1]) { filteredHits.add(filteredHit); } } result.results = filteredHits; return result;',
params: {
threshold: 50,
},
},
},
actions: {},
metadata: {
xpack: {
type: 'threshold',
},
watcherui: {
index: 'myIndex',
time_field: 'myTimeField',
trigger_interval_size: 10,
trigger_interval_unit: 's',
agg_type: 'myAggType',
agg_field: 'myAggField',
term_size: 30,
term_field: 'myTermField',
threshold_comparator: 'between',
time_window_size: 20,
time_window_unit: 's',
threshold: 50,
},
},
});
});
it('excludes metadata when includeMetadata is false', () => {
expect(serializeThresholdWatch({
triggerIntervalSize: 10,
triggerIntervalUnit: 's',
index: 'myIndex',
timeWindowSize: 20,
timeWindowUnit: 's',
timeField: 'myTimeField',
aggType: 'myAggType',
aggField: 'myAggField',
termField: 'myTermField',
termSize: 30,
termOrder: 40,
thresholdComparator: 'between',
hasTermsAgg: true,
threshold: 50,
actions: [],
includeMetadata: false,
})).toEqual({
trigger: {
schedule: {
interval: '10s',
},
},
input: {
search: {
request: {
body: {
size: 0,
query: {
bool: {
filter: {
range: {
myTimeField: {
gte: '{{ctx.trigger.scheduled_time}}||-20s',
lte: '{{ctx.trigger.scheduled_time}}',
format: 'strict_date_optional_time||epoch_millis',
},
},
},
},
},
aggs: {
bucketAgg: {
terms: {
field: 'myTermField',
size: 30,
order: {
metricAgg: 40,
},
},
aggs: {
metricAgg: {
myAggType: {
field: 'myAggField',
},
},
},
},
},
},
indices: [
'myIndex',
],
},
},
},
condition: {
script: {
// eslint-disable-next-line max-len
source: 'ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; for (int i = 0; i < arr.length; i++) { if (arr[i][\'metricAgg\'].value >= params.threshold[0] && arr[i][\'metricAgg\'].value <= params.threshold[1]) { return true; } } return false;',
params: {
threshold: 50,
},
},
},
transform: {
script: {
// eslint-disable-next-line max-len
source: 'HashMap result = new HashMap(); ArrayList arr = ctx.payload.aggregations.bucketAgg.buckets; ArrayList filteredHits = new ArrayList(); for (int i = 0; i < arr.length; i++) { HashMap filteredHit = new HashMap(); filteredHit.key = arr[i].key; filteredHit.value = arr[i][\'metricAgg\'].value; if (filteredHit.value >= params.threshold[0] && filteredHit.value <= params.threshold[1]) { filteredHits.add(filteredHit); } } result.results = filteredHits; return result;',
params: {
threshold: 50,
},
},
},
actions: {},
});
});
});

View file

@ -5,7 +5,6 @@
*/
import { set } from 'lodash';
import { badRequest } from 'boom';
import { getActionType } from '../../../common/lib/get_action_type';
import { ACTION_TYPES } from '../../../common/constants';
import { LoggingAction } from './logging_action';
@ -16,7 +15,6 @@ import { WebhookAction } from './webhook_action';
import { PagerDutyAction } from './pagerduty_action';
import { JiraAction } from './jira_action';
import { UnknownAction } from './unknown_action';
import { i18n } from '@kbn/i18n';
const ActionTypes = {};
set(ActionTypes, ACTION_TYPES.LOGGING, LoggingAction);
@ -34,63 +32,17 @@ export class Action {
}
// From Elasticsearch
static fromUpstreamJson(json, options = { throwExceptions: {} }) {
if (!json.id) {
throw badRequest(
i18n.translate('xpack.watcher.models.actionStatus.idPropertyMissingBadRequestMessage', {
defaultMessage: 'JSON argument must contain an {id} property',
values: {
id: 'id'
}
}),
);
}
if (!json.actionJson) {
throw badRequest(
i18n.translate('xpack.watcher.models.action.actionJsonPropertyMissingBadRequestMessage', {
defaultMessage: 'JSON argument must contain an {actionJson} property',
values: {
actionJson: 'actionJson'
}
}),
);
}
static fromUpstreamJson(json) {
const type = getActionType(json.actionJson);
const ActionType = ActionTypes[type] || UnknownAction;
const { action, errors } = ActionType.fromUpstreamJson(json, options);
const doThrowException = options.throwExceptions.Action !== false;
if (errors && doThrowException) {
this.throwErrors(errors);
}
const { action } = ActionType.fromUpstreamJson(json);
return action;
}
// From Kibana
static fromDownstreamJson(json, options = { throwExceptions: {} }) {
static fromDownstreamJson(json) {
const ActionType = ActionTypes[json.type] || UnknownAction;
const { action, errors } = ActionType.fromDownstreamJson(json);
const doThrowException = options.throwExceptions.Action !== false;
if (errors && doThrowException) {
this.throwErrors(errors);
}
const { action } = ActionType.fromDownstreamJson(json);
return action;
}
static throwErrors(errors) {
const allMessages = errors.reduce((message, error) => {
if (message) {
return `${message}, ${error.message}`;
}
return error.message;
}, '');
throw badRequest(allMessages);
}
}

View file

@ -5,7 +5,6 @@
*/
import { Action } from './action';
import { LoggingAction } from './logging_action';
import { ACTION_TYPES } from '../../../common/constants';
jest.mock('./logging_action', () => ({
@ -18,11 +17,8 @@ jest.mock('./logging_action', () => ({
}));
describe('action', () => {
describe('Action', () => {
describe('fromUpstreamJson factory method', () => {
let upstreamJson;
beforeEach(() => {
upstreamJson = {
@ -35,43 +31,13 @@ describe('action', () => {
};
});
it(`throws an error if no 'id' property in json`, () => {
delete upstreamJson.id;
expect(() => {
Action.fromUpstreamJson(upstreamJson);
}).toThrowError('JSON argument must contain an id property');
});
it(`throws an error if no 'actionJson' property in json`, () => {
delete upstreamJson.actionJson;
expect(() => {
Action.fromUpstreamJson(upstreamJson);
}).toThrowError('JSON argument must contain an actionJson property');
});
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 } };
@ -81,7 +47,7 @@ describe('action', () => {
const upstreamEmailJson = { id: 'action2', actionJson: { email: {} } };
const emailAction = Action.fromUpstreamJson(upstreamEmailJson, options);
const upstreamSlackJson = { id: 'action3', actionJson: { slack: {} } };
const upstreamSlackJson = { id: 'action3', actionJson: { slack: { message: {} } } };
const slackAction = Action.fromUpstreamJson(upstreamSlackJson, options);
expect(loggingAction.type).toBe(ACTION_TYPES.LOGGING);
@ -102,11 +68,9 @@ describe('action', () => {
expect(action.type).toBe(ACTION_TYPES.UNKNOWN);
});
});
describe('downstreamJson getter method', () => {
let upstreamJson;
beforeEach(() => {
upstreamJson = {
@ -120,17 +84,12 @@ describe('action', () => {
});
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);
});
});
});
});

View file

@ -4,9 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { badRequest } from 'boom';
import { i18n } from '@kbn/i18n';
export class BaseAction {
constructor(props, errors) {
this.id = props.id;
@ -35,17 +32,6 @@ export class BaseAction {
}
static getPropsFromUpstreamJson(json) {
if (!json.id) {
throw badRequest(
i18n.translate('xpack.watcher.models.baseAction.idPropertyMissingBadRequestMessage', {
defaultMessage: 'JSON argument must contain an {id} property',
values: {
id: 'id'
}
}),
);
}
return {
id: json.id
};

View file

@ -108,11 +108,9 @@ export class EmailAction extends BaseAction {
code: ERROR_CODES.ERR_PROP_MISSING,
message
});
json.email = {};
}
if (!json.email.to) {
if (json.email && !json.email.to) {
const message = i18n.translate('xpack.watcher.models.emailAction.actionJsonEmailToPropertyMissingBadRequestMessage', {
defaultMessage: 'JSON argument must contain an {actionJsonEmailTo} property',
values: {

View file

@ -75,11 +75,9 @@ export class IndexAction extends BaseAction {
}
}),
});
json.index = {};
}
if (!json.index.index) {
if (json.index && !json.index.index) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.loggingAction.actionJsonIndexNamePropertyMissingBadRequestMessage', {

View file

@ -95,8 +95,6 @@ export class JiraAction extends BaseAction {
}
}),
});
json.jira = {};
}
if (!get(json, 'jira.fields.project.key')) {
@ -123,7 +121,7 @@ export class JiraAction extends BaseAction {
});
}
if (!json.jira.fields.summary) {
if (!get(json, 'jira.fields.summary')) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.jiraAction.actionJsonJiraSummaryPropertyMissingBadRequestMessage', {

View file

@ -78,11 +78,9 @@ export class LoggingAction extends BaseAction {
}
}),
});
json.logging = {};
}
if (!json.logging.text) {
if (json.logging && !json.logging.text) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.loggingAction.actionJsonLoggingTextPropertyMissingBadRequestMessage', {

View file

@ -78,11 +78,9 @@ export class PagerDutyAction extends BaseAction {
}
}),
});
json.pagerduty = {};
}
if (!json.pagerduty.description) {
if (json.pagerduty && !json.pagerduty.description) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.pagerDutyAction.actionJsonPagerDutyDescriptionPropertyMissingBadRequestMessage', {

View file

@ -87,11 +87,9 @@ export class SlackAction extends BaseAction {
}
})
});
json.slack = {};
}
if (!json.slack.message) {
if (json.slack && !json.slack.message) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.slackAction.actionJsonSlackMessagePropertyMissingBadRequestMessage', {
@ -101,8 +99,6 @@ export class SlackAction extends BaseAction {
}
}),
});
json.slack.message = {};
}
return { errors: errors.length ? errors : null };

View file

@ -145,7 +145,7 @@ export class WebhookAction extends BaseAction {
static validateJson(json) {
const errors = [];
if (!json.webhook.host) {
if (json.webhook && !json.webhook.host) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.loggingAction.actionJsonWebhookHostPropertyMissingBadRequestMessage', {
@ -155,10 +155,9 @@ export class WebhookAction extends BaseAction {
}
}),
});
json.webhook = {};
}
if (!json.webhook.port) {
if (json.webhook && !json.webhook.port) {
errors.push({
code: ERROR_CODES.ERR_PROP_MISSING,
message: i18n.translate('xpack.watcher.models.loggingAction.actionJsonWebhookPortPropertyMissingBadRequestMessage', {

View file

@ -20,11 +20,13 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { serializeJsonWatch } from '../../../../../common/lib/serialization';
import { ErrableFormRow, SectionError } from '../../../../components';
import { putWatchApiUrl } from '../../../../lib/documentation_links';
import { onWatchSave } from '../../watch_edit_actions';
import { WatchContext } from '../../watch_context';
import { goToWatchList } from '../../../../lib/navigation';
import { RequestFlyout } from '../request_flyout';
export const JsonWatchEditForm = () => {
const { watch, setWatchProperty } = useContext(WatchContext);
@ -33,6 +35,7 @@ export const JsonWatchEditForm = () => {
const hasErrors = !!Object.keys(errors).find(errorKey => errors[errorKey].length >= 1);
const [validationError, setValidationError] = useState<string | null>(null);
const [isRequestVisible, setIsRequestVisible] = useState<boolean>(false);
const [serverError, setServerError] = useState<{
data: { nessage: string; error: string };
@ -130,7 +133,7 @@ export const JsonWatchEditForm = () => {
/>
</ErrableFormRow>
<EuiSpacer size="s" />
<EuiSpacer size="m" />
<ErrableFormRow
id="watchJson"
@ -176,55 +179,80 @@ export const JsonWatchEditForm = () => {
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="saveWatchButton"
fill
color="secondary"
type="submit"
iconType="check"
isLoading={isSaving}
isDisabled={hasErrors}
onClick={async () => {
setIsSaving(true);
const savedWatch = await onWatchSave(watch);
if (savedWatch && savedWatch.error) {
const { data } = savedWatch.error;
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="saveWatchButton"
fill
color="secondary"
type="submit"
iconType="check"
isLoading={isSaving}
isDisabled={hasErrors}
onClick={async () => {
setIsSaving(true);
const savedWatch = await onWatchSave(watch);
if (savedWatch && savedWatch.error) {
const { data } = savedWatch.error;
setIsSaving(false);
setIsSaving(false);
if (data && data.error === 'validation') {
return setValidationError(data.message);
}
if (data && data.error === 'validation') {
return setValidationError(data.message);
return setServerError(savedWatch.error);
}
}}
>
{watch.isNew ? (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.json.createButtonLabel"
defaultMessage="Create watch"
/>
) : (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.json.saveButtonLabel"
defaultMessage="Save watch"
/>
)}
</EuiButton>
</EuiFlexItem>
return setServerError(savedWatch.error);
}
}}
>
{watch.isNew ? (
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="btnCancelWatch" onClick={() => goToWatchList()}>
{i18n.translate('xpack.watcher.sections.watchEdit.json.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => setIsRequestVisible(!isRequestVisible)}>
{isRequestVisible ? (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.json.createButtonLabel"
defaultMessage="Create watch"
id="xpack.watcher.sections.watchEdit.json.hideRequestButtonLabel"
defaultMessage="Hide request"
/>
) : (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.json.saveButtonLabel"
defaultMessage="Save watch"
id="xpack.watcher.sections.watchEdit.json.showRequestButtonLabel"
defaultMessage="Show request"
/>
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="btnCancelWatch" onClick={() => goToWatchList()}>
{i18n.translate('xpack.watcher.sections.watchEdit.json.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
{isRequestVisible ? (
<RequestFlyout
id={watch.id}
payload={serializeJsonWatch(watch.name, watch.watch)}
close={() => setIsRequestVisible(false)}
/>
) : null}
</Fragment>
);
};

View file

@ -0,0 +1,87 @@
/*
* 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 React, { PureComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonEmpty,
EuiCodeBlock,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
interface Props {
id: any;
close: any;
payload: any;
}
export class RequestFlyout extends PureComponent<Props> {
getEsJson(payload: any): string {
return JSON.stringify(payload, null, 2);
}
render() {
const { id, payload, close } = this.props;
const endpoint = `PUT _watcher/watch/${id || '<watchId>'}`;
const request = `${endpoint}\n${this.getEsJson(payload)}`;
return (
<EuiFlyout maxWidth={480} onClose={close}>
<EuiFlyoutHeader>
<EuiTitle>
<h2>
{id ? (
<FormattedMessage
id="xpack.watcher.requestFlyout.namedTitle"
defaultMessage="Request for '{id}'"
values={{ id }}
/>
) : (
<FormattedMessage
id="xpack.watcher.requestFlyout.unnamedTitle"
defaultMessage="Request"
/>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>
<FormattedMessage
id="xpack.watcher.requestFlyout.descriptionText"
defaultMessage="This Elasticsearch request will create or update this watch."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCodeBlock language="json" isCopyable>
{request}
</EuiCodeBlock>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty iconType="cross" onClick={close} flush="left">
<FormattedMessage
id="xpack.watcher.requestFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
}

View file

@ -27,6 +27,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { TIME_UNITS } from '../../../../../common/constants';
import { serializeThresholdWatch } from '../../../../../common/lib/serialization';
import { ErrableFormRow, SectionError } from '../../../../components';
import { fetchFields, getMatchingIndices, loadIndexPatterns } from '../../../../lib/api';
import { aggTypes } from '../../../../models/watch/agg_types';
@ -38,6 +39,7 @@ import { WatchVisualization } from './watch_visualization';
import { WatchActionsPanel } from './threshold_watch_action_panel';
import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label';
import { goToWatchList } from '../../../../lib/navigation';
import { RequestFlyout } from '../request_flyout';
const expressionFieldsWithValidation = [
'aggField',
@ -168,6 +170,7 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => {
} | null>(null);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [isIndiciesLoading, setIsIndiciesLoading] = useState<boolean>(false);
const [isRequestVisible, setIsRequestVisible] = useState<boolean>(false);
const { watch, setWatchProperty } = useContext(WatchContext);
@ -218,6 +221,14 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => {
defaultMessage: 'AND',
});
// Users might edit the request for use outside of the Watcher app. If they do make changes to it,
// we have no guarantee it will still be compatible with the threshold alert form, so we strip
// the metadata to avoid potential conflicts.
const requestPreviewWatchData = {
...watch.upstreamJson,
includeMetadata: false,
};
return (
<EuiPageContent>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
@ -870,47 +881,77 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => {
<EuiSpacer />
</Fragment>
) : null}
<EuiFlexGroup>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
fill
color="secondary"
data-test-subj="saveWatchButton"
type="submit"
iconType="check"
isDisabled={hasErrors || hasActionErrors}
isLoading={isSaving}
onClick={async () => {
setIsSaving(true);
const savedWatch = await onWatchSave(watch);
if (savedWatch && savedWatch.error) {
setIsSaving(false);
return setServerError(savedWatch.error);
}
}}
>
{watch.isNew ? (
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
fill
color="secondary"
data-test-subj="saveWatchButton"
type="submit"
iconType="check"
isDisabled={hasErrors || hasActionErrors}
isLoading={isSaving}
onClick={async () => {
setIsSaving(true);
const savedWatch = await onWatchSave(watch);
if (savedWatch && savedWatch.error) {
setIsSaving(false);
return setServerError(savedWatch.error);
}
}}
>
{watch.isNew ? (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.threshold.createButtonLabel"
defaultMessage="Create alert"
/>
) : (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.threshold.saveButtonLabel"
defaultMessage="Save alert"
/>
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => goToWatchList()}>
{i18n.translate('xpack.watcher.sections.watchEdit.threshold.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => setIsRequestVisible(!isRequestVisible)}>
{isRequestVisible ? (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.threshold.createButtonLabel"
defaultMessage="Create alert"
id="xpack.watcher.sections.watchEdit.json.hideRequestButtonLabel"
defaultMessage="Hide request"
/>
) : (
<FormattedMessage
id="xpack.watcher.sections.watchEdit.threshold.saveButtonLabel"
defaultMessage="Save alert"
id="xpack.watcher.sections.watchEdit.json.showRequestButtonLabel"
defaultMessage="Show request"
/>
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => goToWatchList()}>
{i18n.translate('xpack.watcher.sections.watchEdit.threshold.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
{isRequestVisible ? (
<RequestFlyout
id={watch.id}
payload={serializeThresholdWatch(requestPreviewWatchData)}
close={() => setIsRequestVisible(false)}
/>
) : null}
</EuiPageContent>
);
};

View file

@ -47,21 +47,18 @@ function getPropsFromAction(type: string, action: { [key: string]: any }) {
/**
* 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.
* when we add them in the JSON watch editor. This method takes takes care of it.
*/
function createActionsForWatch(watchInstance: BaseWatch) {
watchInstance.resetActions();
let action;
let type;
let actionProps;
Object.keys(watchInstance.watch.actions).forEach(k => {
action = watchInstance.watch.actions[k];
type = getTypeFromAction(action);
actionProps = { ...getPropsFromAction(type, action), ignoreDefaults: true };
const action = watchInstance.watch.actions[k];
const type = getTypeFromAction(action);
const actionProps = { ...getPropsFromAction(type, action), ignoreDefaults: true };
watchInstance.createAction(type, actionProps);
});
return watchInstance;
}

View file

@ -1,223 +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 { pick } from 'lodash';
import expect from '@kbn/expect';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
const constructorMock = sinon.stub();
const upstreamJsonMock = sinon.stub();
const downstreamJsonMock = sinon.stub();
const getPropsFromUpstreamJsonMock = sinon.stub();
const getPropsFromDownstreamJsonMock = sinon.stub();
class BaseWatchStub {
constructor(props) {
constructorMock(props);
}
get upstreamJson() {
upstreamJsonMock();
return {
baseCalled: true
};
}
get downstreamJson() {
downstreamJsonMock();
return {
baseCalled: true
};
}
static getPropsFromUpstreamJson(json) {
getPropsFromUpstreamJsonMock();
return pick(json, 'watchJson');
}
static getPropsFromDownstreamJson(json) {
getPropsFromDownstreamJsonMock();
return pick(json, 'watchJson');
}
}
const { JsonWatch } = proxyquire('../json_watch', {
'./base_watch': { BaseWatch: BaseWatchStub }
});
describe('JsonWatch', () => {
describe('Constructor', () => {
let props;
beforeEach(() => {
constructorMock.resetHistory();
props = {
watch: 'foo'
};
});
it('should call the BaseWatch constructor', () => {
new JsonWatch(props);
expect(constructorMock.called).to.be(true);
});
it('should populate all expected fields', () => {
const actual = new JsonWatch(props);
const expected = {
watch: 'foo'
};
expect(actual).to.eql(expected);
});
});
describe('watchJson getter method', () => {
let props;
beforeEach(() => {
props = {
watch: { foo: 'bar' }
};
});
it('should return the correct result', () => {
const watch = new JsonWatch(props);
expect(watch.watchJson).to.eql(props.watch);
});
});
describe('upstreamJson getter method', () => {
beforeEach(() => {
upstreamJsonMock.resetHistory();
});
it('should call the getter from WatchBase and return the correct result', () => {
const watch = new JsonWatch({ watch: 'foo' });
const actual = watch.upstreamJson;
const expected = {
baseCalled: true
};
expect(upstreamJsonMock.called).to.be(true);
expect(actual).to.eql(expected);
});
});
describe('downstreamJson getter method', () => {
let props;
beforeEach(() => {
downstreamJsonMock.resetHistory();
props = {
watch: 'foo',
watchJson: 'bar'
};
});
it('should call the getter from WatchBase and return the correct result', () => {
const watch = new JsonWatch(props);
const actual = watch.downstreamJson;
const expected = {
baseCalled: true,
watch: 'foo'
};
expect(downstreamJsonMock.called).to.be(true);
expect(actual).to.eql(expected);
});
});
describe('fromUpstreamJson factory method', () => {
let upstreamJson;
beforeEach(() => {
getPropsFromUpstreamJsonMock.resetHistory();
upstreamJson = {
watchJson: { foo: { bar: 'baz' } }
};
});
it('should call the getPropsFromUpstreamJson method of BaseWatch', () => {
JsonWatch.fromUpstreamJson(upstreamJson);
expect(getPropsFromUpstreamJsonMock.called).to.be(true);
});
it('should clone the watchJson property into a watch property', () => {
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch).to.eql(upstreamJson.watchJson);
expect(jsonWatch.watch).to.not.be(upstreamJson.watchJson);
});
it('should remove the metadata.name property from the watch property', () => {
upstreamJson.watchJson.metadata = { name: 'foobar', foo: 'bar' };
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch.metadata.name).to.be(undefined);
});
it('should remove the metadata.xpack property from the watch property', () => {
upstreamJson.watchJson.metadata = {
name: 'foobar',
xpack: { prop: 'val' },
foo: 'bar'
};
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch.metadata.xpack).to.be(undefined);
});
it('should remove an empty metadata property from the watch property', () => {
upstreamJson.watchJson.metadata = { name: 'foobar' };
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch.metadata).to.be(undefined);
});
});
describe('fromDownstreamJson factory method', () => {
let downstreamJson;
beforeEach(() => {
getPropsFromDownstreamJsonMock.resetHistory();
downstreamJson = {
watch: { foo: { bar: 'baz' } }
};
});
it('should call the getPropsFromDownstreamJson method of BaseWatch', () => {
JsonWatch.fromDownstreamJson(downstreamJson);
expect(getPropsFromDownstreamJsonMock.called).to.be(true);
});
it('should copy the watch property', () => {
const jsonWatch = JsonWatch.fromDownstreamJson(downstreamJson);
expect(jsonWatch.watch).to.eql(downstreamJson.watch);
});
});
});

View file

@ -1,159 +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 { pick } from 'lodash';
import expect from '@kbn/expect';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
const constructorMock = sinon.stub();
const downstreamJsonMock = sinon.stub();
const getPropsFromUpstreamJsonMock = sinon.stub();
class BaseWatchStub {
constructor(props) {
constructorMock(props);
}
get downstreamJson() {
downstreamJsonMock();
return {
baseCalled: true
};
}
static getPropsFromUpstreamJson(json) {
getPropsFromUpstreamJsonMock();
return pick(json, 'watchJson');
}
}
const { MonitoringWatch } = proxyquire('../monitoring_watch', {
'./base_watch': { BaseWatch: BaseWatchStub }
});
describe('MonitoringWatch', () => {
describe('Constructor', () => {
let props;
beforeEach(() => {
constructorMock.resetHistory();
props = {};
});
it('should call the BaseWatch constructor', () => {
new MonitoringWatch(props);
expect(constructorMock.called).to.be(true);
});
it('should populate all expected fields', () => {
const actual = new MonitoringWatch(props);
const expected = {
isSystemWatch: true
};
expect(actual).to.eql(expected);
});
});
describe('watchJson getter method', () => {
it('should return an empty object', () => {
const watch = new MonitoringWatch({});
const actual = watch.watchJson;
const expected = {};
expect(actual).to.eql(expected);
});
});
describe('getVisualizeQuery method', () => {
it(`throws an error`, () => {
const watch = new MonitoringWatch({});
expect(watch.getVisualizeQuery).to.throwError(/getVisualizeQuery called for monitoring watch/i);
});
});
describe('formatVisualizeData method', () => {
it(`throws an error`, () => {
const watch = new MonitoringWatch({});
expect(watch.formatVisualizeData).to.throwError(/formatVisualizeData called for monitoring watch/i);
});
});
describe('upstreamJson getter method', () => {
it(`throws an error`, () => {
const watch = new MonitoringWatch({});
expect(() => watch.upstreamJson).to.throwError(/upstreamJson called for monitoring watch/i);
});
});
describe('downstreamJson getter method', () => {
let props;
beforeEach(() => {
downstreamJsonMock.resetHistory();
props = {};
});
it('should call the getter from WatchBase and return the correct result', () => {
const watch = new MonitoringWatch(props);
const actual = watch.downstreamJson;
const expected = {
baseCalled: true
};
expect(downstreamJsonMock.called).to.be(true);
expect(actual).to.eql(expected);
});
});
describe('fromUpstreamJson factory method', () => {
beforeEach(() => {
getPropsFromUpstreamJsonMock.resetHistory();
});
it('should call the getPropsFromUpstreamJson method of BaseWatch', () => {
MonitoringWatch.fromUpstreamJson({});
expect(getPropsFromUpstreamJsonMock.called).to.be(true);
});
it('should generate a valid MonitoringWatch object', () => {
const actual = MonitoringWatch.fromUpstreamJson({});
const expected = { isSystemWatch: true };
expect(actual).to.eql(expected);
});
});
describe('fromDownstreamJson factory method', () => {
it(`throws an error`, () => {
expect(MonitoringWatch.fromDownstreamJson).withArgs({})
.to.throwError(/fromDownstreamJson called for monitoring watch/i);
});
});
});

View file

@ -1,448 +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 { pick } from 'lodash';
import expect from '@kbn/expect';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
import { COMPARATORS, SORT_ORDERS } from '../../../../common/constants';
const constructorMock = sinon.stub();
const upstreamJsonMock = sinon.stub();
const downstreamJsonMock = sinon.stub();
const getPropsFromUpstreamJsonMock = sinon.stub();
const getPropsFromDownstreamJsonMock = sinon.stub();
const buildTriggerMock = sinon.stub();
const buildInputMock = sinon.stub();
const buildConditionMock = sinon.stub();
const buildTransformMock = sinon.stub();
const buildActionsMock = sinon.stub();
const buildMetadataMock = sinon.stub();
const buildVisualizeQueryMock = sinon.stub();
const formatVisualizeDataMock = sinon.stub();
class BaseWatchStub {
constructor(props) {
constructorMock(props);
}
get upstreamJson() {
upstreamJsonMock();
return {
baseCalled: true
};
}
get downstreamJson() {
downstreamJsonMock();
return {
baseCalled: true
};
}
static getPropsFromUpstreamJson(json) {
getPropsFromUpstreamJsonMock();
return pick(json, 'watchJson');
}
static getPropsFromDownstreamJson(json) {
getPropsFromDownstreamJsonMock();
return pick(json, 'watchJson');
}
}
const { ThresholdWatch } = proxyquire('../threshold_watch/threshold_watch', {
'../base_watch': { BaseWatch: BaseWatchStub },
'./build_actions': {
buildActions: (...args) => {
buildActionsMock(...args);
return 'buildActionsResult';
}
},
'./build_condition': {
buildCondition: (...args) => {
buildConditionMock(...args);
return 'buildConditionResult';
}
},
'./build_input': {
buildInput: (...args) => {
buildInputMock(...args);
return 'buildInputResult';
}
},
'./build_metadata': {
buildMetadata: (...args) => {
buildMetadataMock(...args);
return 'buildMetadataResult';
}
},
'./build_transform': {
buildTransform: (...args) => {
buildTransformMock(...args);
return 'buildTransformResult';
}
},
'./build_trigger': {
buildTrigger: (...args) => {
buildTriggerMock(...args);
return 'buildTriggerResult';
}
},
'./build_visualize_query': {
buildVisualizeQuery: (...args) => {
buildVisualizeQueryMock(...args);
}
},
'./format_visualize_data': {
formatVisualizeData: (...args) => {
formatVisualizeDataMock(...args);
}
}
});
describe('ThresholdWatch', () => {
describe('Constructor', () => {
let props;
beforeEach(() => {
constructorMock.resetHistory();
props = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
});
it('should call the BaseWatch constructor', () => {
new ThresholdWatch(props);
expect(constructorMock.called).to.be(true);
});
it('should populate all expected fields', () => {
const actual = new ThresholdWatch(props);
const expected = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
expect(actual).to.eql(expected);
});
});
describe('hasTermAgg getter method', () => {
it('should return true if termField is defined', () => {
const downstreamJson = { termField: 'foobar' };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.hasTermsAgg).to.be(true);
});
it('should return false if termField is undefined', () => {
const downstreamJson = { termField: undefined };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.hasTermsAgg).to.be(false);
});
});
describe('termOrder getter method', () => {
it('should return SORT_ORDERS.DESCENDING if thresholdComparator is COMPARATORS.GREATER_THAN', () => {
const downstreamJson = { thresholdComparator: COMPARATORS.GREATER_THAN };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.termOrder).to.be(SORT_ORDERS.DESCENDING);
});
it('should return SORT_ORDERS.ASCENDING if thresholdComparator is not COMPARATORS.GREATER_THAN', () => {
const downstreamJson = { thresholdComparator: 'foo' };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.termOrder).to.be(SORT_ORDERS.ASCENDING);
});
});
describe('watchJson getter method', () => {
beforeEach(() => {
buildActionsMock.resetHistory();
buildConditionMock.resetHistory();
buildInputMock.resetHistory();
buildMetadataMock.resetHistory();
buildTransformMock.resetHistory();
buildTriggerMock.resetHistory();
});
it('should return the correct result', () => {
const watch = new ThresholdWatch({});
const actual = watch.watchJson;
const expected = {
trigger: 'buildTriggerResult',
input: 'buildInputResult',
condition: 'buildConditionResult',
transform: 'buildTransformResult',
actions: 'buildActionsResult',
metadata: 'buildMetadataResult'
};
expect(actual).to.eql(expected);
expect(buildActionsMock.calledWith(watch)).to.be(true);
expect(buildConditionMock.calledWith(watch)).to.be(true);
expect(buildInputMock.calledWith(watch)).to.be(true);
expect(buildMetadataMock.calledWith(watch)).to.be(true);
expect(buildTransformMock.calledWith(watch)).to.be(true);
expect(buildTriggerMock.calledWith(watch)).to.be(true);
});
});
describe('getVisualizeQuery method', () => {
beforeEach(() => {
buildVisualizeQueryMock.resetHistory();
});
it('should call the external buildVisualizeQuery method', () => {
const watch = new ThresholdWatch({});
const options = { foo: 'bar' };
watch.getVisualizeQuery(options);
expect(buildVisualizeQueryMock.calledWith(watch, options)).to.be(true);
});
});
describe('formatVisualizeData method', () => {
beforeEach(() => {
formatVisualizeDataMock.resetHistory();
});
it('should call the external formatVisualizeData method', () => {
const watch = new ThresholdWatch({});
const results = { foo: 'bar' };
watch.formatVisualizeData(results);
expect(formatVisualizeDataMock.calledWith(watch, results)).to.be(true);
});
});
describe('upstreamJson getter method', () => {
let props;
beforeEach(() => {
upstreamJsonMock.resetHistory();
props = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
});
it('should call the getter from WatchBase and return the correct result', () => {
const watch = new ThresholdWatch(props);
const actual = watch.upstreamJson;
const expected = { baseCalled: true };
expect(upstreamJsonMock.called).to.be(true);
expect(actual).to.eql(expected);
});
});
describe('downstreamJson getter method', () => {
let props;
beforeEach(() => {
downstreamJsonMock.resetHistory();
props = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
});
it('should call the getter from WatchBase and return the correct result', () => {
const watch = new ThresholdWatch(props);
const actual = watch.downstreamJson;
const expected = {
baseCalled: true,
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
expect(downstreamJsonMock.called).to.be(true);
expect(actual).to.eql(expected);
});
});
describe('fromUpstreamJson factory method', () => {
let upstreamJson;
beforeEach(() => {
getPropsFromUpstreamJsonMock.resetHistory();
upstreamJson = {
watchJson: {
foo: { bar: 'baz' },
metadata: {
watcherui: {
index: 'index',
time_field: 'timeField',
trigger_interval_size: 'triggerIntervalSize',
trigger_interval_unit: 'triggerIntervalUnit',
agg_type: 'aggType',
agg_field: 'aggField',
term_size: 'termSize',
term_field: 'termField',
threshold_comparator: 'thresholdComparator',
time_window_size: 'timeWindowSize',
time_window_unit: 'timeWindowUnit',
threshold: 'threshold'
}
}
}
};
});
it('should call the getPropsFromUpstreamJson method of BaseWatch', () => {
ThresholdWatch.fromUpstreamJson(upstreamJson);
expect(getPropsFromUpstreamJsonMock.called).to.be(true);
});
it('should generate a valid ThresholdWatch object', () => {
const actual = ThresholdWatch.fromUpstreamJson(upstreamJson);
const expected = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: ['threshold']
};
expect(actual).to.eql(expected);
});
});
describe('fromDownstreamJson factory method', () => {
let downstreamJson;
beforeEach(() => {
getPropsFromDownstreamJsonMock.resetHistory();
downstreamJson = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
});
it('should call the getPropsFromDownstreamJson method of BaseWatch', () => {
ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(getPropsFromDownstreamJsonMock.called).to.be(true);
});
it('should generate a valid ThresholdWatch object', () => {
const actual = ThresholdWatch.fromDownstreamJson(downstreamJson);
const expected = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
expect(actual).to.eql(expected);
});
});
});

View file

@ -1,130 +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 '@kbn/expect';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
import { WATCH_TYPES } from '../../../../common/constants';
const watchTypeMocks = {};
function buildMock(watchType) {
const fromDownstreamJsonMock = sinon.stub();
const fromUpstreamJsonMock = sinon.stub();
watchTypeMocks[watchType] = {
fromDownstreamJsonMock,
fromUpstreamJsonMock,
Class: class WatchStub {
static fromDownstreamJson(...args) {
fromDownstreamJsonMock(...args);
}
static fromUpstreamJson(...args) {
fromUpstreamJsonMock(...args);
}
}
};
}
buildMock(WATCH_TYPES.JSON);
buildMock(WATCH_TYPES.THRESHOLD);
buildMock(WATCH_TYPES.MONITORING);
const { Watch } = proxyquire('../watch', {
'./json_watch': { JsonWatch: watchTypeMocks[WATCH_TYPES.JSON].Class },
'./monitoring_watch': { MonitoringWatch: watchTypeMocks[WATCH_TYPES.MONITORING].Class },
'./threshold_watch': { ThresholdWatch: watchTypeMocks[WATCH_TYPES.THRESHOLD].Class }
});
describe('Watch', () => {
describe('getWatchTypes factory method', () => {
it(`There should be a property for each watch type`, () => {
// NOTE: If this test is failing because a new watch type was added
// make sure you add a 'returns an instance of' test for the new type
// as well.
const watchTypes = Watch.getWatchTypes();
const expected = Object.values(WATCH_TYPES).sort();
const actual = Object.keys(watchTypes).sort();
expect(actual).to.eql(expected);
});
});
describe('fromDownstreamJson factory method', () => {
beforeEach(() => {
Object.keys(watchTypeMocks).forEach(key => {
watchTypeMocks[key].fromDownstreamJsonMock.resetHistory();
});
});
it(`throws an error if no 'type' property in json`, () => {
expect(Watch.fromDownstreamJson).withArgs({})
.to.throwError(/must contain an type property/i);
});
it(`throws an error if the type does not correspond to a WATCH_TYPES value`, () => {
expect(Watch.fromDownstreamJson).withArgs({ type: 'foo' })
.to.throwError(/Attempted to load unknown type foo/i);
});
it('fromDownstreamJson of JsonWatch to be called when type is WATCH_TYPES.JSON', () => {
Watch.fromDownstreamJson({ type: WATCH_TYPES.JSON });
expect(watchTypeMocks[WATCH_TYPES.JSON].fromDownstreamJsonMock.called).to.be(true);
});
it('fromDownstreamJson of ThresholdWatch to be called when type is WATCH_TYPES.THRESHOLD', () => {
Watch.fromDownstreamJson({ type: WATCH_TYPES.THRESHOLD });
expect(watchTypeMocks[WATCH_TYPES.THRESHOLD].fromDownstreamJsonMock.called).to.be(true);
});
it('fromDownstreamJson of MonitoringWatch to be called when type is WATCH_TYPES.MONITORING', () => {
Watch.fromDownstreamJson({ type: WATCH_TYPES.MONITORING });
expect(watchTypeMocks[WATCH_TYPES.MONITORING].fromDownstreamJsonMock.called).to.be(true);
});
});
describe('fromUpstreamJson factory method', () => {
beforeEach(() => {
Object.keys(watchTypeMocks).forEach(key => {
watchTypeMocks[key].fromUpstreamJsonMock.resetHistory();
});
});
it(`throws an error if no 'watchJson' property in json`, () => {
expect(Watch.fromUpstreamJson).withArgs({})
.to.throwError(/must contain a watchJson property/i);
});
it('fromUpstreamJson of JsonWatch to be called when type is WATCH_TYPES.JSON', () => {
Watch.fromUpstreamJson({
watchJson: { metadata: { xpack: { type: WATCH_TYPES.JSON } } }
});
expect(watchTypeMocks[WATCH_TYPES.JSON].fromUpstreamJsonMock.called).to.be(true);
});
it('fromUpstreamJson of ThresholdWatch to be called when type is WATCH_TYPES.THRESHOLD', () => {
Watch.fromUpstreamJson({
watchJson: { metadata: { xpack: { type: WATCH_TYPES.THRESHOLD } } }
});
expect(watchTypeMocks[WATCH_TYPES.THRESHOLD].fromUpstreamJsonMock.called).to.be(true);
});
it('fromUpstreamJson of MonitoringWatch to be called when type is WATCH_TYPES.MONITORING', () => {
Watch.fromUpstreamJson({
watchJson: { metadata: { xpack: { type: WATCH_TYPES.MONITORING } } }
});
expect(watchTypeMocks[WATCH_TYPES.MONITORING].fromUpstreamJsonMock.called).to.be(true);
});
});
});

View file

@ -6,7 +6,7 @@
import { get, map, pick } from 'lodash';
import { badRequest } from 'boom';
import { Action } from '../action';
import { Action } from '../../../common/models/action';
import { WatchStatus } from '../watch_status';
import { i18n } from '@kbn/i18n';
import { WatchErrors } from '../watch_errors';

View file

@ -4,49 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import proxyquire from 'proxyquire';
import expect from '@kbn/expect';
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);
return { foo: 'bar' };
}
static fromDownstreamJson(...args) {
actionFromDownstreamJSONMock(...args);
return { foo: 'bar' };
}
}
class WatchStatusStub {
static fromUpstreamJson(...args) {
watchStatusFromUpstreamJSONMock(...args);
return { foo: 'bar' };
}
}
class WatchErrorsStub {
static fromUpstreamJson(...args) {
watchErrorsFromUpstreamJSONMock(...args);
return { foo: 'bar' };
}
}
const { BaseWatch } = proxyquire('../base_watch', {
'../action': { Action: ActionStub },
'../watch_status': { WatchStatus: WatchStatusStub },
'../watch_errors': { WatchErrors: WatchErrorsStub },
});
import { BaseWatch } from './base_watch';
describe('BaseWatch', () => {
describe('Constructor', () => {
let props;
@ -71,13 +31,13 @@ describe('BaseWatch', () => {
'actions'
];
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
it('should default isSystemWatch to false', () => {
const watch = new BaseWatch(props);
expect(watch.isSystemWatch).to.be(false);
expect(watch.isSystemWatch).toBe(false);
});
it('populates all expected fields', () => {
@ -96,7 +56,7 @@ describe('BaseWatch', () => {
actions: 'baz'
};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
});
@ -124,7 +84,7 @@ describe('BaseWatch', () => {
}
};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
it('should only populate the name metadata if a name is defined', () => {
@ -139,7 +99,7 @@ describe('BaseWatch', () => {
}
};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
});
@ -151,7 +111,7 @@ describe('BaseWatch', () => {
const actual = watch.getVisualizeQuery();
const expected = {};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
});
@ -163,7 +123,7 @@ describe('BaseWatch', () => {
const actual = watch.formatVisualizeData();
const expected = [];
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
});
@ -211,7 +171,7 @@ describe('BaseWatch', () => {
actions: props.actions.map(a => a.downstreamJson)
};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
it('should respect an undefined watchStatus & watchErrors prop', () => {
@ -231,7 +191,7 @@ describe('BaseWatch', () => {
actions: props.actions.map(a => a.downstreamJson)
};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
});
@ -275,7 +235,7 @@ describe('BaseWatch', () => {
}
};
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
});
@ -284,8 +244,6 @@ describe('BaseWatch', () => {
let downstreamJson;
beforeEach(() => {
actionFromDownstreamJSONMock.resetHistory();
downstreamJson = {
id: 'my-watch',
name: 'foo',
@ -302,45 +260,27 @@ describe('BaseWatch', () => {
'actions'
];
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
it('should properly map id and name', () => {
const props = BaseWatch.getPropsFromDownstreamJson(downstreamJson);
expect(props.id).to.be('my-watch');
expect(props.name).to.be('foo');
expect(props.id).toBe('my-watch');
expect(props.name).toBe('foo');
});
it('should return an actions property that is an array', () => {
const props = BaseWatch.getPropsFromDownstreamJson(downstreamJson);
expect(Array.isArray(props.actions)).to.be(true);
expect(props.actions.length).to.be(0);
expect(Array.isArray(props.actions)).toBe(true);
expect(props.actions.length).toBe(0);
});
it('should call Action.fromUDownstreamJSON for each action', () => {
const action0 = { type: 'email', id: 'email1' };
const action1 = { type: 'logging', id: 'logging1' };
downstreamJson.actions.push(action0);
downstreamJson.actions.push(action1);
const props = BaseWatch.getPropsFromDownstreamJson(downstreamJson);
expect(props.actions.length).to.be(2);
expect(actionFromDownstreamJSONMock.calledWith(action0)).to.be(true);
expect(actionFromDownstreamJSONMock.calledWith(action1)).to.be(true);
});
});
describe('getPropsFromUpstreamJson method', () => {
let upstreamJson;
beforeEach(() => {
actionFromUpstreamJSONMock.resetHistory();
watchStatusFromUpstreamJSONMock.resetHistory();
upstreamJson = {
id: 'my-watch',
type: 'json',
@ -363,22 +303,25 @@ describe('BaseWatch', () => {
it(`throws an error if no 'id' property in json`, () => {
delete upstreamJson.id;
expect(BaseWatch.getPropsFromUpstreamJson).withArgs(upstreamJson)
.to.throwError(/must contain an id property/i);
expect(() => {
BaseWatch.getPropsFromUpstreamJson(upstreamJson);
}).toThrow(/must contain an id property/i);
});
it(`throws an error if no 'watchJson' property in json`, () => {
delete upstreamJson.watchJson;
expect(BaseWatch.getPropsFromUpstreamJson).withArgs(upstreamJson)
.to.throwError(/must contain a watchJson property/i);
expect(() => {
BaseWatch.getPropsFromUpstreamJson(upstreamJson);
}).toThrow(/must contain a watchJson property/i);
});
it(`throws an error if no 'watchStatusJson' property in json`, () => {
delete upstreamJson.watchStatusJson;
expect(BaseWatch.getPropsFromUpstreamJson).withArgs(upstreamJson)
.to.throwError(/must contain a watchStatusJson property/i);
expect(() => {
BaseWatch.getPropsFromUpstreamJson(upstreamJson);
}).toThrow(/must contain a watchStatusJson property/i);
});
it(`should ignore unknown watchJson properties`, () => {
@ -408,7 +351,7 @@ describe('BaseWatch', () => {
'throttle_period_in_millis'
];
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
it('should return a valid props object', () => {
@ -423,69 +366,20 @@ describe('BaseWatch', () => {
'actions'
];
expect(actual).to.eql(expected);
expect(actual).toEqual(expected);
});
it('should pull name out of metadata', () => {
const props = BaseWatch.getPropsFromUpstreamJson(upstreamJson);
expect(props.name).to.be('foo');
expect(props.name).toBe('foo');
});
it('should return an actions property that is an array', () => {
const props = BaseWatch.getPropsFromUpstreamJson(upstreamJson);
expect(Array.isArray(props.actions)).to.be(true);
expect(props.actions.length).to.be(0);
expect(Array.isArray(props.actions)).toBe(true);
expect(props.actions.length).toBe(0);
});
it('should call Action.fromUpstreamJson for each action', () => {
upstreamJson.watchJson.actions = {
'my-logging-action': {
'logging': {
'text': 'foo'
}
},
'my-unknown-action': {
'foobar': {}
}
};
const props = BaseWatch.getPropsFromUpstreamJson(upstreamJson);
expect(props.actions.length).to.be(2);
expect(actionFromUpstreamJSONMock.calledWith({
id: 'my-logging-action',
actionJson: {
'logging': {
'text': 'foo'
}
}
})).to.be(true);
expect(actionFromUpstreamJSONMock.calledWith({
id: 'my-unknown-action',
actionJson: {
'foobar': {}
}
})).to.be(true);
});
it('should call WatchStatus.fromUpstreamJson for the watch status', () => {
BaseWatch.getPropsFromUpstreamJson(upstreamJson);
expect(watchStatusFromUpstreamJSONMock.calledWith({
id: 'my-watch',
watchStatusJson: {
state: {
active: true
}
},
watchErrors: {
foo: 'bar'
}
})).to.be(true);
});
});
});

View file

@ -7,6 +7,7 @@
import { isEmpty, cloneDeep, has, merge } from 'lodash';
import { BaseWatch } from './base_watch';
import { WATCH_TYPES } from '../../../common/constants';
import { serializeJsonWatch } from '../../../common/lib/serialization';
export class JsonWatch extends BaseWatch {
// This constructor should not be used directly.
@ -19,13 +20,7 @@ export class JsonWatch extends BaseWatch {
}
get watchJson() {
const result = merge(
{},
super.watchJson,
this.watch
);
return result;
return serializeJsonWatch(this.name, this.watch);
}
// To Elasticsearch

View file

@ -0,0 +1,166 @@
/*
* 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 { JsonWatch } from './json_watch';
describe('JsonWatch', () => {
describe('Constructor', () => {
let props;
beforeEach(() => {
props = {
watch: 'foo'
};
});
it('should populate all expected fields', () => {
const actual = new JsonWatch(props);
const expected = {
watch: 'foo'
};
expect(actual).toMatchObject(expected);
});
});
describe('watchJson getter method', () => {
let props;
beforeEach(() => {
props = {
watch: { foo: 'bar' },
metadata: {
xpack: {
type: 'json',
},
},
};
});
it('should return the correct result', () => {
const watch = new JsonWatch(props);
const expected = {
foo: 'bar',
metadata: {
xpack: {
type: 'json',
},
},
};
expect(watch.watchJson).toEqual(expected);
});
});
describe('upstreamJson getter method', () => {
it('should return the correct result', () => {
const watch = new JsonWatch({ watch: { foo: 'bar' } });
const actual = watch.upstreamJson;
const expected = {
id: undefined,
watch: {
foo: 'bar',
metadata: {
xpack: {
type: 'json',
},
},
},
};
expect(actual).toEqual(expected);
});
});
describe('downstreamJson getter method', () => {
let props;
beforeEach(() => {
props = {
watch: 'foo',
watchJson: 'bar'
};
});
it('should return the correct result', () => {
const watch = new JsonWatch(props);
const actual = watch.downstreamJson;
const expected = {
watch: 'foo',
isSystemWatch: false,
actions: [],
};
expect(actual).toEqual(expected);
});
});
describe('fromUpstreamJson factory method', () => {
let upstreamJson;
beforeEach(() => {
upstreamJson = {
id: 'id',
watchStatusJson: {},
watchJson: {
trigger: 'trigger',
input: 'input',
condition: 'condition',
actions: 'actions',
metadata: 'metadata',
transform: 'transform',
throttle_period: 'throttle_period',
throttle_period_in_millis: 'throttle_period_in_millis',
}
};
});
it('should clone the watchJson property into a watch property', () => {
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch).toEqual(upstreamJson.watchJson);
expect(jsonWatch.watch).not.toBe(upstreamJson.watchJson);
});
it('should remove the metadata.name property from the watch property', () => {
upstreamJson.watchJson.metadata = { name: 'foobar', foo: 'bar' };
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch.metadata.name).toBe(undefined);
});
it('should remove the metadata.xpack property from the watch property', () => {
upstreamJson.watchJson.metadata = {
name: 'foobar',
xpack: { prop: 'val' },
foo: 'bar'
};
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch.metadata.xpack).toBe(undefined);
});
it('should remove an empty metadata property from the watch property', () => {
upstreamJson.watchJson.metadata = { name: 'foobar' };
const jsonWatch = JsonWatch.fromUpstreamJson(upstreamJson);
expect(jsonWatch.watch.metadata).toBe(undefined);
});
});
describe('fromDownstreamJson factory method', () => {
let downstreamJson;
beforeEach(() => {
downstreamJson = {
watch: { foo: { bar: 'baz' } }
};
});
it('should copy the watch property', () => {
const jsonWatch = JsonWatch.fromDownstreamJson(downstreamJson);
expect(jsonWatch.watch).toEqual(downstreamJson.watch);
});
});
});

View file

@ -0,0 +1,107 @@
/*
* 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 { MonitoringWatch } from './monitoring_watch';
describe('MonitoringWatch', () => {
describe('Constructor', () => {
let props;
beforeEach(() => {
props = {};
});
it('should populate all expected fields', () => {
const actual = new MonitoringWatch(props);
const expected = {
isSystemWatch: true
};
expect(actual).toEqual(expected);
});
});
describe('watchJson getter method', () => {
it('should return an empty object', () => {
const watch = new MonitoringWatch({});
const actual = watch.watchJson;
const expected = {
metadata: {
xpack: {},
},
};
expect(actual).toEqual(expected);
});
});
describe('getVisualizeQuery method', () => {
it(`throws an error`, () => {
const watch = new MonitoringWatch({});
expect(() => watch.getVisualizeQuery()).toThrow(/getVisualizeQuery called for monitoring watch/i);
});
});
describe('formatVisualizeData method', () => {
it(`throws an error`, () => {
const watch = new MonitoringWatch({});
expect(() => watch.formatVisualizeData()).toThrow(/formatVisualizeData called for monitoring watch/i);
});
});
describe('upstreamJson getter method', () => {
it(`throws an error`, () => {
const watch = new MonitoringWatch({});
expect(() => watch.upstreamJson).toThrow(/upstreamJson called for monitoring watch/i);
});
});
describe('downstreamJson getter method', () => {
let props;
beforeEach(() => {
props = {};
});
it('should return the correct result', () => {
const watch = new MonitoringWatch(props);
const actual = watch.downstreamJson;
const expected = {
actions: [],
isSystemWatch: true,
};
expect(actual).toEqual(expected);
});
});
describe('fromUpstreamJson factory method', () => {
it('should generate a valid MonitoringWatch object', () => {
const actual = MonitoringWatch.fromUpstreamJson({
id: 'id',
watchJson: {},
watchStatusJson: {},
});
const expected = {
id: 'id',
isSystemWatch: true,
actions: [],
type: 'monitoring',
};
expect(actual).toMatchObject(expected);
});
});
describe('fromDownstreamJson factory method', () => {
it(`throws an error`, () => {
expect(() => MonitoringWatch.fromDownstreamJson({}))
.toThrow(/fromDownstreamJson called for monitoring watch/i);
});
});
});

View file

@ -1,28 +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.
*/
/*
watch.metadata
*/
export function buildMetadata(watch) {
return {
watcherui: {
index: watch.index,
time_field: watch.timeField,
trigger_interval_size: watch.triggerIntervalSize,
trigger_interval_unit: watch.triggerIntervalUnit,
agg_type: watch.aggType,
agg_field: watch.aggField,
term_size: watch.termSize,
term_field: watch.termField,
threshold_comparator: watch.thresholdComparator,
time_window_size: watch.timeWindowSize,
time_window_unit: watch.timeWindowUnit,
threshold: watch.threshold
}
};
}

View file

@ -5,7 +5,7 @@
*/
import { cloneDeep } from 'lodash';
import { buildInput } from './build_input';
import { buildInput } from '../../../../common/lib/serialization';
import { AGG_TYPES } from '../../../../common/constants';
/*
@ -93,7 +93,8 @@ function buildAggs(body, { aggType, termField }, dateAgg) {
}
export function buildVisualizeQuery(watch, visualizeOptions) {
const watchInput = buildInput(watch);
const { index, timeWindowSize, timeWindowUnit, timeField, aggType, aggField, termField, termSize, termOrder } = watch;
const watchInput = buildInput({ index, timeWindowSize, timeWindowUnit, timeField, aggType, aggField, termField, termSize, termOrder });
const body = watchInput.search.request.body;
const dateAgg = buildDateAgg({
field: watch.timeField,

View file

@ -7,12 +7,8 @@
import { merge } from 'lodash';
import { BaseWatch } from '../base_watch';
import { WATCH_TYPES, COMPARATORS, SORT_ORDERS } from '../../../../common/constants';
import { buildActions } from './build_actions';
import { buildCondition } from './build_condition';
import { buildInput } from './build_input';
import { buildMetadata } from './build_metadata';
import { buildTransform } from './build_transform';
import { buildTrigger } from './build_trigger';
import { serializeThresholdWatch } from '../../../../common/lib/serialization';
import { buildVisualizeQuery } from './build_visualize_query';
import { formatVisualizeData } from './format_visualize_data';
@ -46,20 +42,7 @@ export class ThresholdWatch extends BaseWatch {
}
get watchJson() {
const result = merge(
{},
super.watchJson,
{
trigger: buildTrigger(this),
input: buildInput(this),
condition: buildCondition(this),
transform: buildTransform(this),
actions: buildActions(this),
metadata: buildMetadata(this)
}
);
return result;
return serializeThresholdWatch(this);
}
getVisualizeQuery(visualizeOptions) {
@ -128,26 +111,41 @@ export class ThresholdWatch extends BaseWatch {
}
// from Kibana
static fromDownstreamJson(json) {
const props = merge(
{},
super.getPropsFromDownstreamJson(json),
{
type: WATCH_TYPES.THRESHOLD,
index: json.index,
timeField: json.timeField,
triggerIntervalSize: json.triggerIntervalSize,
triggerIntervalUnit: json.triggerIntervalUnit,
aggType: json.aggType,
aggField: json.aggField,
termSize: json.termSize,
termField: json.termField,
thresholdComparator: json.thresholdComparator,
timeWindowSize: json.timeWindowSize,
timeWindowUnit: json.timeWindowUnit,
threshold: json.threshold
}
);
static fromDownstreamJson({
id,
name,
actions,
index,
timeField,
triggerIntervalSize,
triggerIntervalUnit,
aggType,
aggField,
termSize,
termField,
thresholdComparator,
timeWindowSize,
timeWindowUnit,
threshold,
}) {
const props = {
type: WATCH_TYPES.THRESHOLD,
id,
name,
actions,
index,
timeField,
triggerIntervalSize,
triggerIntervalUnit,
aggType,
aggField,
termSize,
termField,
thresholdComparator,
timeWindowSize,
timeWindowUnit,
threshold,
};
return new ThresholdWatch(props);
}

View file

@ -0,0 +1,241 @@
/*
* 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 { COMPARATORS, SORT_ORDERS } from '../../../../common/constants';
import { WatchErrors } from '../../watch_errors';
import { ThresholdWatch } from './threshold_watch';
describe('ThresholdWatch', () => {
describe('Constructor', () => {
let props;
beforeEach(() => {
props = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
});
it('should populate all expected fields', () => {
const actual = new ThresholdWatch(props);
const expected = {
id: undefined,
name: undefined,
type: undefined,
isSystemWatch: false,
watchStatus: undefined,
watchErrors: undefined,
actions: undefined,
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
expect(actual).toEqual(expected);
});
});
describe('hasTermAgg getter method', () => {
it('should return true if termField is defined', () => {
const downstreamJson = { termField: 'foobar' };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.hasTermsAgg).toBe(true);
});
it('should return false if termField is undefined', () => {
const downstreamJson = { termField: undefined };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.hasTermsAgg).toBe(false);
});
});
describe('termOrder getter method', () => {
it('should return SORT_ORDERS.DESCENDING if thresholdComparator is COMPARATORS.GREATER_THAN', () => {
const downstreamJson = { thresholdComparator: COMPARATORS.GREATER_THAN };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.termOrder).toBe(SORT_ORDERS.DESCENDING);
});
it('should return SORT_ORDERS.ASCENDING if thresholdComparator is not COMPARATORS.GREATER_THAN', () => {
const downstreamJson = { thresholdComparator: 'foo' };
const thresholdWatch = ThresholdWatch.fromDownstreamJson(downstreamJson);
expect(thresholdWatch.termOrder).toBe(SORT_ORDERS.ASCENDING);
});
});
describe('downstreamJson getter method', () => {
let props;
beforeEach(() => {
props = {
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
});
it('should return the correct result', () => {
const watch = new ThresholdWatch(props);
const actual = watch.downstreamJson;
const expected = {
actions: [],
isSystemWatch: false,
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold'
};
expect(actual).toEqual(expected);
});
});
describe('fromUpstreamJson factory method', () => {
let upstreamJson;
beforeEach(() => {
upstreamJson = {
id: 'id',
watchStatusJson: {},
watchJson: {
foo: { bar: 'baz' },
metadata: {
name: 'name',
watcherui: {
index: 'index',
time_field: 'timeField',
trigger_interval_size: 'triggerIntervalSize',
trigger_interval_unit: 'triggerIntervalUnit',
agg_type: 'aggType',
agg_field: 'aggField',
term_size: 'termSize',
term_field: 'termField',
threshold_comparator: 'thresholdComparator',
time_window_size: 'timeWindowSize',
time_window_unit: 'timeWindowUnit',
threshold: 'threshold'
}
}
}
};
});
it('should generate a valid ThresholdWatch object', () => {
const actual = ThresholdWatch.fromUpstreamJson(upstreamJson);
const expected = {
id: 'id',
name: 'name',
isSystemWatch: false,
type: 'threshold',
actions: [],
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: ['threshold'],
watchErrors: new WatchErrors(),
};
expect(actual).toMatchObject(expected);
});
});
describe('fromDownstreamJson factory method', () => {
let downstreamJson;
beforeEach(() => {
downstreamJson = {
id: 'id',
name: 'name',
index: 'index',
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold',
};
});
it('should generate a valid ThresholdWatch object', () => {
const actual = ThresholdWatch.fromDownstreamJson(downstreamJson);
const expected = {
id: 'id',
name: 'name',
isSystemWatch: false,
type: 'threshold',
index: 'index',
actions: undefined,
timeField: 'timeField',
triggerIntervalSize: 'triggerIntervalSize',
triggerIntervalUnit: 'triggerIntervalUnit',
aggType: 'aggType',
aggField: 'aggField',
termSize: 'termSize',
termField: 'termField',
thresholdComparator: 'thresholdComparator',
timeWindowSize: 'timeWindowSize',
timeWindowUnit: 'timeWindowUnit',
threshold: 'threshold',
watchErrors: undefined,
watchStatus: undefined,
};
expect(actual).toEqual(expected);
});
});
});

View file

@ -0,0 +1,84 @@
/*
* 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 { WATCH_TYPES } from '../../../common/constants';
import { Watch } from './watch';
import { JsonWatch } from './json_watch';
import { MonitoringWatch } from './monitoring_watch';
import { ThresholdWatch } from './threshold_watch';
describe('Watch', () => {
describe('getWatchTypes factory method', () => {
it(`There should be a property for each watch type`, () => {
const watchTypes = Watch.getWatchTypes();
const expected = Object.values(WATCH_TYPES).sort();
const actual = Object.keys(watchTypes).sort();
expect(actual).toEqual(expected);
});
});
describe('fromDownstreamJson factory method', () => {
it(`throws an error if no 'type' property in json`, () => {
expect(() => Watch.fromDownstreamJson({}))
.toThrow(/must contain an type property/i);
});
it(`throws an error if the type does not correspond to a WATCH_TYPES value`, () => {
expect(() => Watch.fromDownstreamJson({ type: 'foo' }))
.toThrow(/Attempted to load unknown type foo/i);
});
it('JsonWatch to be used when type is WATCH_TYPES.JSON', () => {
const config = { type: WATCH_TYPES.JSON };
expect(Watch.fromDownstreamJson(config)).toEqual(JsonWatch.fromDownstreamJson(config));
});
it('ThresholdWatch to be used when type is WATCH_TYPES.THRESHOLD', () => {
const config = { type: WATCH_TYPES.THRESHOLD };
expect(Watch.fromDownstreamJson(config)).toEqual(ThresholdWatch.fromDownstreamJson(config));
});
it('MonitoringWatch to be used when type is WATCH_TYPES.MONITORING', () => {
const config = { type: WATCH_TYPES.MONITORING };
expect(() => Watch.fromDownstreamJson(config)).toThrowError();
});
});
describe('fromUpstreamJson factory method', () => {
it(`throws an error if no 'watchJson' property in json`, () => {
expect(() => Watch.fromUpstreamJson({}))
.toThrow(/must contain a watchJson property/i);
});
it('JsonWatch to be used when type is WATCH_TYPES.JSON', () => {
const config = {
id: 'id',
watchStatusJson: {},
watchJson: { metadata: { xpack: { type: WATCH_TYPES.JSON } } }
};
expect(Watch.fromUpstreamJson(config)).toEqual(JsonWatch.fromUpstreamJson(config));
});
it('ThresholdWatch to be used when type is WATCH_TYPES.THRESHOLD', () => {
const config = {
id: 'id',
watchStatusJson: {},
watchJson: { metadata: { watcherui: {}, xpack: { type: WATCH_TYPES.THRESHOLD } } }
};
expect(Watch.fromUpstreamJson(config)).toEqual(ThresholdWatch.fromUpstreamJson(config));
});
it('MonitoringWatch to be used when type is WATCH_TYPES.MONITORING', () => {
const config = {
id: 'id',
watchStatusJson: {},
watchJson: { metadata: { xpack: { type: WATCH_TYPES.MONITORING } } }
};
expect(Watch.fromUpstreamJson(config)).toEqual(MonitoringWatch.fromUpstreamJson(config));
});
});
});

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { WATCH_TYPES } from '../../../../common/constants';
import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization';
import { callWithRequestFactory } from '../../../lib/call_with_request_factory';
import { Watch } from '../../../models/watch';
import { isEsErrorFactory } from '../../../lib/is_es_error_factory';
import { wrapEsError, wrapUnknownError, wrapCustomError } from '../../../lib/error_wrappers';
import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory';
@ -17,15 +18,14 @@ function fetchWatch(callWithRequest, watchId) {
});
}
function saveWatch(callWithRequest, watch) {
function saveWatch(callWithRequest, id, body) {
return callWithRequest('watcher.putWatch', {
id: watch.id,
body: watch.watch
id,
body,
});
}
export function registerSaveRoute(server) {
const isEsError = isEsErrorFactory(server);
const licensePreRouting = licensePreRoutingFactory(server);
@ -34,22 +34,22 @@ export function registerSaveRoute(server) {
method: 'PUT',
handler: async (request) => {
const callWithRequest = callWithRequestFactory(server, request);
const watchPayload = request.payload;
const { id, type, isNew, ...watchConfig } = request.payload;
// For new watches, verify watch with the same ID doesn't already exist
if (watchPayload.isNew) {
if (isNew) {
const conflictError = wrapCustomError(
new Error(i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', {
defaultMessage: 'There is already a watch with ID \'{watchId}\'.',
values: {
watchId: watchPayload.id,
watchId: id,
}
})),
409
);
try {
const existingWatch = await fetchWatch(callWithRequest, watchPayload.id);
const existingWatch = await fetchWatch(callWithRequest, id);
if (existingWatch.found) {
throw conflictError;
@ -62,10 +62,21 @@ export function registerSaveRoute(server) {
}
}
const watchFromDownstream = Watch.fromDownstreamJson(watchPayload);
let serializedWatch;
switch (type) {
case WATCH_TYPES.JSON:
const { name, watch } = watchConfig;
serializedWatch = serializeJsonWatch(name, watch);
break;
case WATCH_TYPES.THRESHOLD:
serializedWatch = serializeThresholdWatch(watchConfig);
break;
}
// Create new watch
return saveWatch(callWithRequest, watchFromDownstream.upstreamJson)
return saveWatch(callWithRequest, id, serializedWatch)
.catch(err => {
// Case: Error from Elasticsearch JS client
if (isEsError(err)) {

View file

@ -11409,10 +11409,7 @@
"xpack.watcher.constants.watchStates.errorStateText": "エラー",
"xpack.watcher.constants.watchStates.firingStateText": "実行中",
"xpack.watcher.constants.watchStates.okStateText": "OK",
"xpack.watcher.models.action.actionJsonPropertyMissingBadRequestMessage": "json 引数には {actionJson} プロパティが含まれている必要があります",
"xpack.watcher.models.actionStatus.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります",
"xpack.watcher.models.actionStatus.notDetermineActionStatusBadImplementationMessage": "アクションステータスを把握できませんでした; action = {actionStatusJson}",
"xpack.watcher.models.baseAction.idPropertyMissingBadRequestMessage": "json 引数には {id} プロパティが含まれている必要があります",
"xpack.watcher.models.baseAction.selectMessageText": "アクションを実行します。",
"xpack.watcher.models.baseAction.simulateButtonLabel": "今すぐこのアクションをシミュレート",
"xpack.watcher.models.baseAction.simulateMessage": "アクション {id} のシミュレーションが完了しました",

View file

@ -11411,10 +11411,7 @@
"xpack.watcher.constants.watchStates.errorStateText": "错误!",
"xpack.watcher.constants.watchStates.firingStateText": "正在发送",
"xpack.watcher.constants.watchStates.okStateText": "确定",
"xpack.watcher.models.action.actionJsonPropertyMissingBadRequestMessage": "json 参数必须包含 {actionJson} 属性",
"xpack.watcher.models.actionStatus.idPropertyMissingBadRequestMessage": "json 参数必须包含 {id} 属性",
"xpack.watcher.models.actionStatus.notDetermineActionStatusBadImplementationMessage": "无法确定操作状态;操作 = {actionStatusJson}",
"xpack.watcher.models.baseAction.idPropertyMissingBadRequestMessage": "json 参数必须包含 {id} 属性",
"xpack.watcher.models.baseAction.selectMessageText": "执行操作。",
"xpack.watcher.models.baseAction.simulateButtonLabel": "立即模拟此操作",
"xpack.watcher.models.baseAction.simulateMessage": "已成功模拟操作 {id}",