mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* 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:
parent
b312d9f140
commit
2914360a0b
48 changed files with 1484 additions and 1432 deletions
|
@ -6,8 +6,6 @@
|
|||
|
||||
export const WATCH_TYPES: { [key: string]: string } = {
|
||||
JSON: 'json',
|
||||
|
||||
THRESHOLD: 'threshold',
|
||||
|
||||
MONITORING: 'monitoring',
|
||||
};
|
||||
|
|
9
x-pack/legacy/plugins/watcher/common/lib/serialization/index.d.ts
vendored
Normal file
9
x-pack/legacy/plugins/watcher/common/lib/serialization/index.d.ts
vendored
Normal 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;
|
|
@ -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';
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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}`
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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: {},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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: {
|
|
@ -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', {
|
|
@ -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', {
|
|
@ -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', {
|
|
@ -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', {
|
|
@ -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 };
|
|
@ -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', {
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)) {
|
||||
|
|
|
@ -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} のシミュレーションが完了しました",
|
||||
|
|
|
@ -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}",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue