=> {
+ const queryParams = {
dateRangeStart,
dateRangeEnd,
...(filters && { filters }),
...(statusFilter && { statusFilter }),
};
- const urlParams = new URLSearchParams(params).toString();
- const response = await fetch(`${url}?${urlParams}`);
- if (!response.ok) {
- throw new Error(response.statusText);
- }
- const responseData = await response.json();
- const decoded = SnapshotType.decode(responseData);
- ThrowReporter.report(decoded);
- if (isRight(decoded)) {
- return decoded.right;
- }
- throw new Error('`getSnapshotCount` response did not correspond to expected type');
+
+ return await apiService.get(API_URLS.SNAPSHOT_COUNT, queryParams, SnapshotType);
};
diff --git a/x-pack/legacy/plugins/uptime/public/state/api/types.ts b/x-pack/legacy/plugins/uptime/public/state/api/types.ts
index a148f1c7d7ae3..4232751cbc032 100644
--- a/x-pack/legacy/plugins/uptime/public/state/api/types.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/api/types.ts
@@ -5,7 +5,6 @@
*/
export interface BaseParams {
- basePath: string;
dateStart: string;
dateEnd: string;
filters?: string;
@@ -14,4 +13,4 @@ export interface BaseParams {
monitorId?: string;
}
-export type APIFn = (params: { basePath: string } & P) => Promise;
+export type APIFn = (params: P) => Promise;
diff --git a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts b/x-pack/legacy/plugins/uptime/public/state/api/utils.ts
new file mode 100644
index 0000000000000..e67efa8570c11
--- /dev/null
+++ b/x-pack/legacy/plugins/uptime/public/state/api/utils.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 { PathReporter } from 'io-ts/lib/PathReporter';
+import { isRight } from 'fp-ts/lib/Either';
+import { HttpFetchQuery, HttpSetup } from '../../../../../../../target/types/core/public';
+
+class ApiService {
+ private static instance: ApiService;
+ private _http!: HttpSetup;
+
+ public get http() {
+ return this._http;
+ }
+
+ public set http(httpSetup: HttpSetup) {
+ this._http = httpSetup;
+ }
+
+ private constructor() {}
+
+ static getInstance(): ApiService {
+ if (!ApiService.instance) {
+ ApiService.instance = new ApiService();
+ }
+
+ return ApiService.instance;
+ }
+
+ public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) {
+ const response = await this._http!.get(apiUrl, { query: params });
+
+ if (decodeType) {
+ const decoded = decodeType.decode(response);
+ if (isRight(decoded)) {
+ return decoded.right;
+ } else {
+ // eslint-disable-next-line no-console
+ console.error(
+ `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}`
+ );
+ }
+ }
+
+ return response;
+ }
+
+ public async post(apiUrl: string, data?: any, decodeType?: any) {
+ const response = await this._http!.post(apiUrl, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+
+ if (decodeType) {
+ const decoded = decodeType.decode(response);
+ if (isRight(decoded)) {
+ return decoded.right;
+ } else {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}`
+ );
+ }
+ }
+ return response;
+ }
+
+ public async delete(apiUrl: string) {
+ const response = await this._http!.delete(apiUrl);
+ if (response instanceof Error) {
+ throw response;
+ }
+ return response;
+ }
+}
+
+export const apiService = ApiService.getInstance();
diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts
index ea389ff0a6745..d1d7626b2eab3 100644
--- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { call, put, select } from 'redux-saga/effects';
+import { call, put } from 'redux-saga/effects';
import { Action } from 'redux-actions';
-import { getBasePath } from '../selectors';
/**
* Factory function for a fetch effect. It expects three action creators,
@@ -25,15 +24,17 @@ export function fetchEffectFactory(
fail: (error: Error) => Action
) {
return function*(action: Action) {
- try {
- const {
- payload: { ...params },
- } = action;
- const basePath = yield select(getBasePath);
- const response = yield call(fetch, { ...params, basePath });
+ const {
+ payload: { ...params },
+ } = action;
+ const response = yield call(fetch, params);
+ if (response instanceof Error) {
+ // eslint-disable-next-line no-console
+ console.error(response);
+
+ yield put(fail(response));
+ } else {
yield put(success(response));
- } catch (error) {
- yield put(fail(error));
}
};
}
diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts
index 1cac7424b4e5b..ed21f315476d4 100644
--- a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts
@@ -4,48 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { call, put, takeLatest, select } from 'redux-saga/effects';
-import { Action } from 'redux-actions';
+import { takeLatest } from 'redux-saga/effects';
import {
- FETCH_MONITOR_DETAILS,
- FETCH_MONITOR_DETAILS_SUCCESS,
- FETCH_MONITOR_DETAILS_FAIL,
- FETCH_MONITOR_LOCATIONS,
- FETCH_MONITOR_LOCATIONS_SUCCESS,
- FETCH_MONITOR_LOCATIONS_FAIL,
+ getMonitorDetailsAction,
+ getMonitorDetailsActionSuccess,
+ getMonitorDetailsActionFail,
+ getMonitorLocationsAction,
+ getMonitorLocationsActionSuccess,
+ getMonitorLocationsActionFail,
} from '../actions/monitor';
import { fetchMonitorDetails, fetchMonitorLocations } from '../api';
-import { getBasePath } from '../selectors';
-import { MonitorDetailsActionPayload } from '../actions/types';
-
-function* monitorDetailsEffect(action: Action) {
- const { monitorId, dateStart, dateEnd }: MonitorDetailsActionPayload = action.payload;
- try {
- const basePath = yield select(getBasePath);
- const response = yield call(fetchMonitorDetails, {
- monitorId,
- basePath,
- dateStart,
- dateEnd,
- });
- yield put({ type: FETCH_MONITOR_DETAILS_SUCCESS, payload: response });
- } catch (error) {
- yield put({ type: FETCH_MONITOR_DETAILS_FAIL, payload: error.message });
- }
-}
-
-function* monitorLocationsEffect(action: Action) {
- const payload = action.payload;
- try {
- const basePath = yield select(getBasePath);
- const response = yield call(fetchMonitorLocations, { basePath, ...payload });
- yield put({ type: FETCH_MONITOR_LOCATIONS_SUCCESS, payload: response });
- } catch (error) {
- yield put({ type: FETCH_MONITOR_LOCATIONS_FAIL, payload: error.message });
- }
-}
+import { fetchEffectFactory } from './fetch_effect';
export function* fetchMonitorDetailsEffect() {
- yield takeLatest(FETCH_MONITOR_DETAILS, monitorDetailsEffect);
- yield takeLatest(FETCH_MONITOR_LOCATIONS, monitorLocationsEffect);
+ yield takeLatest(
+ getMonitorDetailsAction,
+ fetchEffectFactory(
+ fetchMonitorDetails,
+ getMonitorDetailsActionSuccess,
+ getMonitorDetailsActionFail
+ )
+ );
+
+ yield takeLatest(
+ getMonitorLocationsAction,
+ fetchEffectFactory(
+ fetchMonitorLocations,
+ getMonitorLocationsActionSuccess,
+ getMonitorLocationsActionFail
+ )
+ );
}
diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts
index cab32092a14cd..1207ab20bc711 100644
--- a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts
@@ -4,50 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { call, put, takeLatest, select } from 'redux-saga/effects';
-import { Action } from 'redux-actions';
+import { takeLatest } from 'redux-saga/effects';
import {
- getSelectedMonitor,
- getSelectedMonitorSuccess,
- getSelectedMonitorFail,
- getMonitorStatus,
- getMonitorStatusSuccess,
- getMonitorStatusFail,
-} from '../actions/monitor_status';
+ getSelectedMonitorAction,
+ getSelectedMonitorActionSuccess,
+ getSelectedMonitorActionFail,
+ getMonitorStatusAction,
+ getMonitorStatusActionSuccess,
+ getMonitorStatusActionFail,
+} from '../actions';
import { fetchSelectedMonitor, fetchMonitorStatus } from '../api';
-import { getBasePath } from '../selectors';
-
-function* selectedMonitorEffect(action: Action) {
- const { monitorId } = action.payload;
- try {
- const basePath = yield select(getBasePath);
- const response = yield call(fetchSelectedMonitor, {
- monitorId,
- basePath,
- });
- yield put({ type: getSelectedMonitorSuccess, payload: response });
- } catch (error) {
- yield put({ type: getSelectedMonitorFail, payload: error.message });
- }
-}
-
-function* monitorStatusEffect(action: Action) {
- const { monitorId, dateStart, dateEnd } = action.payload;
- try {
- const basePath = yield select(getBasePath);
- const response = yield call(fetchMonitorStatus, {
- monitorId,
- basePath,
- dateStart,
- dateEnd,
- });
- yield put({ type: getMonitorStatusSuccess, payload: response });
- } catch (error) {
- yield put({ type: getMonitorStatusFail, payload: error.message });
- }
-}
+import { fetchEffectFactory } from './fetch_effect';
export function* fetchMonitorStatusEffect() {
- yield takeLatest(getMonitorStatus, monitorStatusEffect);
- yield takeLatest(getSelectedMonitor, selectedMonitorEffect);
+ yield takeLatest(
+ getMonitorStatusAction,
+ fetchEffectFactory(
+ fetchMonitorStatus,
+ getMonitorStatusActionSuccess,
+ getMonitorStatusActionFail
+ )
+ );
+
+ yield takeLatest(
+ getSelectedMonitorAction,
+ fetchEffectFactory(
+ fetchSelectedMonitor,
+ getSelectedMonitorActionSuccess,
+ getSelectedMonitorActionFail
+ )
+ );
}
diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts
index 91df43dd9e826..10010004d47a0 100644
--- a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts
@@ -6,16 +6,20 @@
import { takeLatest } from 'redux-saga/effects';
import {
- FETCH_SNAPSHOT_COUNT,
- fetchSnapshotCountFail,
- fetchSnapshotCountSuccess,
+ getSnapshotCountAction,
+ getSnapshotCountActionFail,
+ getSnapshotCountActionSuccess,
} from '../actions';
import { fetchSnapshotCount } from '../api';
import { fetchEffectFactory } from './fetch_effect';
export function* fetchSnapshotCountEffect() {
yield takeLatest(
- FETCH_SNAPSHOT_COUNT,
- fetchEffectFactory(fetchSnapshotCount, fetchSnapshotCountSuccess, fetchSnapshotCountFail)
+ getSnapshotCountAction,
+ fetchEffectFactory(
+ fetchSnapshotCount,
+ getSnapshotCountActionSuccess,
+ getSnapshotCountActionFail
+ )
);
}
diff --git a/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts b/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts
new file mode 100644
index 0000000000000..4fd2d446daa17
--- /dev/null
+++ b/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreStart } from 'kibana/public';
+import { apiService } from './api/utils';
+
+class KibanaService {
+ private static instance: KibanaService;
+ private _core!: CoreStart;
+
+ public get core() {
+ return this._core;
+ }
+
+ public set core(coreStart: CoreStart) {
+ this._core = coreStart;
+ apiService.http = this._core.http;
+ }
+
+ private constructor() {}
+
+ static getInstance(): KibanaService {
+ if (!KibanaService.instance) {
+ KibanaService.instance = new KibanaService();
+ }
+
+ return KibanaService.instance;
+ }
+}
+
+export const kibanaService = KibanaService.getInstance();
diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts
index 95c576e0fd72e..3650422571ce8 100644
--- a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts
@@ -5,19 +5,20 @@
*/
import { snapshotReducer } from '../snapshot';
-import { SnapshotActionTypes } from '../../actions';
+import {
+ getSnapshotCountAction,
+ getSnapshotCountActionSuccess,
+ getSnapshotCountActionFail,
+} from '../../actions';
describe('snapshot reducer', () => {
it('updates existing state', () => {
- const action: SnapshotActionTypes = {
- type: 'FETCH_SNAPSHOT_COUNT',
- payload: {
- dateRangeStart: 'now-15m',
- dateRangeEnd: 'now',
- filters: 'foo: bar',
- statusFilter: 'up',
- },
- };
+ const action = getSnapshotCountAction({
+ dateRangeStart: 'now-15m',
+ dateRangeEnd: 'now',
+ filters: 'foo: bar',
+ statusFilter: 'up',
+ });
expect(
snapshotReducer(
{
@@ -31,33 +32,28 @@ describe('snapshot reducer', () => {
});
it(`sets the state's status to loading during a fetch`, () => {
- const action: SnapshotActionTypes = {
- type: 'FETCH_SNAPSHOT_COUNT',
- payload: {
- dateRangeStart: 'now-15m',
- dateRangeEnd: 'now',
- },
- };
+ const action = getSnapshotCountAction({
+ dateRangeStart: 'now-15m',
+ dateRangeEnd: 'now',
+ });
expect(snapshotReducer(undefined, action)).toMatchSnapshot();
});
it('changes the count when a snapshot fetch succeeds', () => {
- const action: SnapshotActionTypes = {
- type: 'FETCH_SNAPSHOT_COUNT_SUCCESS',
- payload: {
- up: 10,
- down: 15,
- total: 25,
- },
- };
+ const action = getSnapshotCountActionSuccess({
+ up: 10,
+ down: 15,
+ total: 25,
+ });
+
expect(snapshotReducer(undefined, action)).toMatchSnapshot();
});
it('appends a current error to existing errors list', () => {
- const action: SnapshotActionTypes = {
- type: 'FETCH_SNAPSHOT_COUNT_FAIL',
- payload: new Error(`I couldn't get your data because the server denied the request`),
- };
+ const action = getSnapshotCountActionFail(
+ new Error(`I couldn't get your data because the server denied the request`)
+ );
+
expect(snapshotReducer(undefined, action)).toMatchSnapshot();
});
});
diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts
index aac8a90598d0c..632f3a270e1a1 100644
--- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts
@@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Action } from 'redux-actions';
import {
- MonitorActionTypes,
MonitorDetailsState,
- FETCH_MONITOR_DETAILS,
- FETCH_MONITOR_DETAILS_SUCCESS,
- FETCH_MONITOR_DETAILS_FAIL,
- FETCH_MONITOR_LOCATIONS,
- FETCH_MONITOR_LOCATIONS_SUCCESS,
- FETCH_MONITOR_LOCATIONS_FAIL,
+ getMonitorDetailsAction,
+ getMonitorLocationsAction,
+ getMonitorDetailsActionSuccess,
+ getMonitorDetailsActionFail,
+ getMonitorLocationsActionSuccess,
+ getMonitorLocationsActionFail,
} from '../actions/monitor';
import { MonitorLocations } from '../../../common/runtime_types';
@@ -32,14 +32,14 @@ const initialState: MonitorState = {
errors: [],
};
-export function monitorReducer(state = initialState, action: MonitorActionTypes): MonitorState {
+export function monitorReducer(state = initialState, action: Action): MonitorState {
switch (action.type) {
- case FETCH_MONITOR_DETAILS:
+ case String(getMonitorDetailsAction):
return {
...state,
loading: true,
};
- case FETCH_MONITOR_DETAILS_SUCCESS:
+ case String(getMonitorDetailsActionSuccess):
const { monitorId } = action.payload;
return {
...state,
@@ -49,17 +49,17 @@ export function monitorReducer(state = initialState, action: MonitorActionTypes)
},
loading: false,
};
- case FETCH_MONITOR_DETAILS_FAIL:
+ case String(getMonitorDetailsActionFail):
return {
...state,
errors: [...state.errors, action.payload],
};
- case FETCH_MONITOR_LOCATIONS:
+ case String(getMonitorLocationsAction):
return {
...state,
loading: true,
};
- case FETCH_MONITOR_LOCATIONS_SUCCESS:
+ case String(getMonitorLocationsActionSuccess):
const monLocations = state.monitorLocationsList;
monLocations.set(action.payload.monitorId, action.payload);
return {
@@ -67,7 +67,7 @@ export function monitorReducer(state = initialState, action: MonitorActionTypes)
monitorLocationsList: monLocations,
loading: false,
};
- case FETCH_MONITOR_LOCATIONS_FAIL:
+ case String(getMonitorLocationsActionFail):
return {
...state,
errors: [...state.errors, action.payload],
diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts
index 2688a0946dd61..c2dfbd7f90ff2 100644
--- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts
@@ -5,12 +5,12 @@
*/
import { handleActions, Action } from 'redux-actions';
import {
- getSelectedMonitor,
- getSelectedMonitorSuccess,
- getSelectedMonitorFail,
- getMonitorStatus,
- getMonitorStatusSuccess,
- getMonitorStatusFail,
+ getSelectedMonitorAction,
+ getSelectedMonitorActionSuccess,
+ getSelectedMonitorActionFail,
+ getMonitorStatusAction,
+ getMonitorStatusActionSuccess,
+ getMonitorStatusActionFail,
} from '../actions';
import { Ping } from '../../../common/graphql/types';
import { QueryParams } from '../actions/types';
@@ -31,34 +31,34 @@ type MonitorStatusPayload = QueryParams & Ping;
export const monitorStatusReducer = handleActions(
{
- [String(getSelectedMonitor)]: (state, action: Action) => ({
+ [String(getSelectedMonitorAction)]: (state, action: Action) => ({
...state,
loading: true,
}),
- [String(getSelectedMonitorSuccess)]: (state, action: Action) => ({
+ [String(getSelectedMonitorActionSuccess)]: (state, action: Action) => ({
...state,
loading: false,
monitor: { ...action.payload } as Ping,
}),
- [String(getSelectedMonitorFail)]: (state, action: Action) => ({
+ [String(getSelectedMonitorActionFail)]: (state, action: Action) => ({
...state,
loading: false,
}),
- [String(getMonitorStatus)]: (state, action: Action) => ({
+ [String(getMonitorStatusAction)]: (state, action: Action) => ({
...state,
loading: true,
}),
- [String(getMonitorStatusSuccess)]: (state, action: Action) => ({
+ [String(getMonitorStatusActionSuccess)]: (state, action: Action) => ({
...state,
loading: false,
status: { ...action.payload } as Ping,
}),
- [String(getMonitorStatusFail)]: (state, action: Action) => ({
+ [String(getMonitorStatusActionFail)]: (state, action: Action) => ({
...state,
loading: false,
}),
diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts
index b219421f4f4dc..0b67d8b0e7689 100644
--- a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts
@@ -49,6 +49,7 @@ export function overviewFiltersReducer(
return {
...state,
errors: [...state.errors, action.payload],
+ loading: false,
};
default:
return state;
diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts
index 2155d0e3a74e3..3ba1ef84d41a5 100644
--- a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Action } from 'redux-actions';
import { Snapshot } from '../../../common/runtime_types';
import {
- FETCH_SNAPSHOT_COUNT,
- FETCH_SNAPSHOT_COUNT_FAIL,
- FETCH_SNAPSHOT_COUNT_SUCCESS,
- SnapshotActionTypes,
+ getSnapshotCountAction,
+ getSnapshotCountActionSuccess,
+ getSnapshotCountActionFail,
} from '../actions';
export interface SnapshotState {
@@ -28,20 +28,20 @@ const initialState: SnapshotState = {
loading: false,
};
-export function snapshotReducer(state = initialState, action: SnapshotActionTypes): SnapshotState {
+export function snapshotReducer(state = initialState, action: Action): SnapshotState {
switch (action.type) {
- case FETCH_SNAPSHOT_COUNT:
+ case String(getSnapshotCountAction):
return {
...state,
loading: true,
};
- case FETCH_SNAPSHOT_COUNT_SUCCESS:
+ case String(getSnapshotCountActionSuccess):
return {
...state,
count: action.payload,
loading: false,
};
- case FETCH_SNAPSHOT_COUNT_FAIL:
+ case String(getSnapshotCountActionFail):
return {
...state,
errors: [...state.errors, action.payload],
diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts
index adba288b8b145..4767c25e8f52f 100644
--- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts
+++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts
@@ -13,11 +13,11 @@ export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: App
integrationsPopoverOpen;
// Monitor Selectors
-export const getMonitorDetails = (state: AppState, summary: any) => {
+export const monitorDetailsSelector = (state: AppState, summary: any) => {
return state.monitor.monitorDetailsList[summary.monitor_id];
};
-export const selectMonitorLocations = (state: AppState, monitorId: string) => {
+export const monitorLocationsSelector = (state: AppState, monitorId: string) => {
return state.monitor.monitorLocationsList?.get(monitorId);
};
diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx
index 427870797a206..09156db9ca7d2 100644
--- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx
+++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx
@@ -23,6 +23,7 @@ import { CommonlyUsedRange } from './components/functional/uptime_date_picker';
import { store } from './state';
import { setBasePath } from './state/actions';
import { PageRouter } from './routes';
+import { kibanaService } from './state/kibana_service';
export interface UptimeAppColors {
danger: string;
@@ -83,6 +84,8 @@ const Application = (props: UptimeAppProps) => {
);
}, [canSave, renderGlobalHelpControls, setBadge]);
+ kibanaService.core = core;
+
// @ts-ignore
store.dispatch(setBasePath(basePath));
diff --git a/x-pack/legacy/plugins/xpack_main/public/components/index.js b/x-pack/legacy/plugins/xpack_main/public/components/index.js
index e57bd6af189f8..871d86e642dec 100644
--- a/x-pack/legacy/plugins/xpack_main/public/components/index.js
+++ b/x-pack/legacy/plugins/xpack_main/public/components/index.js
@@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { LicenseStatus } from '../../../license_management/public/np_ready/application/sections/license_dashboard/license_status/license_status';
+export { LicenseStatus } from '../../../../../plugins/license_management/public/application/sections/license_dashboard/license_status/license_status';
-export { AddLicense } from '../../../license_management/public/np_ready/application/sections/license_dashboard/add_license/add_license';
+export { AddLicense } from '../../../../../plugins/license_management/public/application/sections/license_dashboard/add_license/add_license';
/*
* For to link to management
*/
-export { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../license_management/common/constants';
+export { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../../../plugins/license_management/common/constants';
diff --git a/x-pack/package.json b/x-pack/package.json
index 6f15b46e28f53..192ecd25b582c 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -23,7 +23,7 @@
}
},
"resolutions": {
- "**/@types/node": "10.12.27"
+ "**/@types/node": ">=10.17.17 <10.20.0"
},
"devDependencies": {
"@cypress/webpack-preprocessor": "^4.1.0",
@@ -80,7 +80,7 @@
"@types/mime": "^2.0.1",
"@types/mocha": "^5.2.7",
"@types/nock": "^10.0.3",
- "@types/node": "^10.12.27",
+ "@types/node": ">=10.17.17 <10.20.0",
"@types/node-fetch": "^2.5.0",
"@types/nodemailer": "^6.2.1",
"@types/object-hash": "^1.3.0",
diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts
index 0be1983477256..7eded9bb40964 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts
@@ -43,18 +43,46 @@ describe('actionTypeRegistry.get() works', () => {
describe('config validation', () => {
test('config validation succeeds when config is valid', () => {
- const config: Record = {};
+ const config: Record = {
+ index: 'testing-123',
+ refresh: false,
+ };
expect(validateConfig(actionType, config)).toEqual({
...config,
- index: null,
+ index: 'testing-123',
+ refresh: false,
});
- config.index = 'testing-123';
+ config.executionTimeField = 'field-123';
expect(validateConfig(actionType, config)).toEqual({
...config,
index: 'testing-123',
+ refresh: false,
+ executionTimeField: 'field-123',
});
+
+ delete config.index;
+
+ expect(() => {
+ validateConfig(actionType, { index: 666 });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"error validating action type config: [index]: expected value of type [string] but got [number]"`
+ );
+ delete config.executionTimeField;
+
+ expect(() => {
+ validateConfig(actionType, { index: 'testing-123', executionTimeField: true });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"error validating action type config: [executionTimeField]: expected value of type [string] but got [boolean]"`
+ );
+
+ delete config.refresh;
+ expect(() => {
+ validateConfig(actionType, { index: 'testing-123', refresh: 'foo' });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"error validating action type config: [refresh]: expected value of type [boolean] but got [string]"`
+ );
});
test('config validation fails when config is not valid', () => {
@@ -65,46 +93,16 @@ describe('config validation', () => {
expect(() => {
validateConfig(actionType, baseConfig);
}).toThrowErrorMatchingInlineSnapshot(
- `"error validating action type config: [indeX]: definition for this key is missing"`
+ `"error validating action type config: [index]: expected value of type [string] but got [undefined]"`
);
-
- delete baseConfig.user;
- baseConfig.index = 666;
-
- expect(() => {
- validateConfig(actionType, baseConfig);
- }).toThrowErrorMatchingInlineSnapshot(`
-"error validating action type config: [index]: types that failed validation:
-- [index.0]: expected value of type [string] but got [number]
-- [index.1]: expected value to equal [null]"
-`);
});
});
describe('params validation', () => {
test('params validation succeeds when params is valid', () => {
const params: Record = {
- index: 'testing-123',
- executionTimeField: 'field-used-for-time',
- refresh: true,
documents: [{ rando: 'thing' }],
};
- expect(validateParams(actionType, params)).toMatchInlineSnapshot(`
- Object {
- "documents": Array [
- Object {
- "rando": "thing",
- },
- ],
- "executionTimeField": "field-used-for-time",
- "index": "testing-123",
- "refresh": true,
- }
- `);
-
- delete params.index;
- delete params.refresh;
- delete params.executionTimeField;
expect(validateParams(actionType, params)).toMatchInlineSnapshot(`
Object {
"documents": Array [
@@ -129,24 +127,6 @@ describe('params validation', () => {
`"error validating action params: [documents]: expected value of type [array] but got [undefined]"`
);
- expect(() => {
- validateParams(actionType, { index: 666 });
- }).toThrowErrorMatchingInlineSnapshot(
- `"error validating action params: [index]: expected value of type [string] but got [number]"`
- );
-
- expect(() => {
- validateParams(actionType, { executionTimeField: true });
- }).toThrowErrorMatchingInlineSnapshot(
- `"error validating action params: [executionTimeField]: expected value of type [string] but got [boolean]"`
- );
-
- expect(() => {
- validateParams(actionType, { refresh: 'foo' });
- }).toThrowErrorMatchingInlineSnapshot(
- `"error validating action params: [refresh]: expected value of type [boolean] but got [string]"`
- );
-
expect(() => {
validateParams(actionType, { documents: ['should be an object'] });
}).toThrowErrorMatchingInlineSnapshot(
@@ -162,13 +142,10 @@ describe('execute()', () => {
let params: ActionParamsType;
let executorOptions: ActionTypeExecutorOptions;
- // minimal params, index via param
- config = { index: null };
+ // minimal params
+ config = { index: 'index-value', refresh: false, executionTimeField: undefined };
params = {
- index: 'index-via-param',
documents: [{ jim: 'bob' }],
- executionTimeField: undefined,
- refresh: undefined,
};
const actionId = 'some-id';
@@ -190,19 +167,17 @@ describe('execute()', () => {
"jim": "bob",
},
],
- "index": "index-via-param",
+ "index": "index-value",
+ "refresh": false,
},
],
]
`);
- // full params (except index), index via config
- config = { index: 'index-via-config' };
+ // full params
+ config = { index: 'index-value', executionTimeField: 'field_to_use_for_time', refresh: true };
params = {
- index: undefined,
documents: [{ jimbob: 'jr' }],
- executionTimeField: 'field_to_use_for_time',
- refresh: true,
};
executorOptions = { actionId, config, secrets, params, services };
@@ -226,20 +201,17 @@ describe('execute()', () => {
"jimbob": "jr",
},
],
- "index": "index-via-config",
+ "index": "index-value",
"refresh": true,
},
],
]
`);
- // minimal params, index via config and param
- config = { index: 'index-via-config' };
+ // minimal params
+ config = { index: 'index-value', executionTimeField: undefined, refresh: false };
params = {
- index: 'index-via-param',
documents: [{ jim: 'bob' }],
- executionTimeField: undefined,
- refresh: undefined,
};
executorOptions = { actionId, config, secrets, params, services };
@@ -259,19 +231,17 @@ describe('execute()', () => {
"jim": "bob",
},
],
- "index": "index-via-config",
+ "index": "index-value",
+ "refresh": false,
},
],
]
`);
// multiple documents
- config = { index: null };
+ config = { index: 'index-value', executionTimeField: undefined, refresh: false };
params = {
- index: 'index-via-param',
documents: [{ a: 1 }, { b: 2 }],
- executionTimeField: undefined,
- refresh: undefined,
};
executorOptions = { actionId, config, secrets, params, services };
@@ -297,7 +267,8 @@ describe('execute()', () => {
"b": 2,
},
],
- "index": "index-via-param",
+ "index": "index-value",
+ "refresh": false,
},
],
]
diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts
index f8217046b2ea5..b1fe5e3af2d11 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts
@@ -8,7 +8,6 @@ import { curry } from 'lodash';
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
-import { nullableType } from './lib/nullable';
import { Logger } from '../../../../../src/core/server';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
@@ -17,7 +16,9 @@ import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from
export type ActionTypeConfigType = TypeOf;
const ConfigSchema = schema.object({
- index: nullableType(schema.string()),
+ index: schema.string(),
+ refresh: schema.boolean({ defaultValue: false }),
+ executionTimeField: schema.maybe(schema.string()),
});
// params definition
@@ -28,9 +29,6 @@ export type ActionParamsType = TypeOf;
// - timeout not added here, as this seems to be a generic thing we want to do
// eventually: https://github.com/elastic/kibana/projects/26#card-24087404
const ParamsSchema = schema.object({
- index: schema.maybe(schema.string()),
- executionTimeField: schema.maybe(schema.string()),
- refresh: schema.maybe(schema.boolean()),
documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())),
});
@@ -60,27 +58,12 @@ async function executor(
const params = execOptions.params as ActionParamsType;
const services = execOptions.services;
- if (config.index == null && params.index == null) {
- const message = i18n.translate('xpack.actions.builtin.esIndex.indexParamRequiredErrorMessage', {
- defaultMessage: 'index param needs to be set because not set in config for action',
- });
- return {
- status: 'error',
- actionId,
- message,
- };
- }
-
- if (config.index != null && params.index != null) {
- logger.debug(`index passed in params overridden by index set in config for action ${actionId}`);
- }
-
- const index = config.index || params.index;
+ const index = config.index;
const bulkBody = [];
for (const document of params.documents) {
- if (params.executionTimeField != null) {
- document[params.executionTimeField] = new Date();
+ if (config.executionTimeField != null) {
+ document[config.executionTimeField] = new Date();
}
bulkBody.push({ index: {} });
@@ -92,9 +75,7 @@ async function executor(
body: bulkBody,
};
- if (params.refresh != null) {
- bulkParams.refresh = params.refresh;
- }
+ bulkParams.refresh = config.refresh;
let result;
try {
@@ -103,6 +84,7 @@ async function executor(
const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', {
defaultMessage: 'error indexing documents',
});
+ logger.error(`error indexing documents: ${err.message}`);
return {
status: 'error',
actionId,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts
index 381b44439033c..be687e33e2201 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts
@@ -4,68 +4,157 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { handleCreateIncident, handleUpdateIncident } from './action_handlers';
+import {
+ handleCreateIncident,
+ handleUpdateIncident,
+ handleIncident,
+ createComments,
+} from './action_handlers';
import { ServiceNow } from './lib';
-import { finalMapping } from './mock';
-import { Incident } from './lib/types';
+import { Mapping } from './types';
jest.mock('./lib');
const ServiceNowMock = ServiceNow as jest.Mock;
-const incident: Incident = {
- short_description: 'A title',
- description: 'A description',
-};
+const finalMapping: Mapping = new Map();
+
+finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'overwrite',
+});
-const comments = [
- {
- commentId: '456',
- version: 'WzU3LDFd',
- comment: 'A comment',
- incidentCommentId: undefined,
+finalMapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+});
+
+finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+});
+
+finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'overwrite',
+});
+
+const params = {
+ caseId: '123',
+ title: 'a title',
+ description: 'a description',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ incidentId: null,
+ incident: {
+ short_description: 'a title',
+ description: 'a description',
},
-];
+ comments: [
+ {
+ commentId: '456',
+ version: 'WzU3LDFd',
+ comment: 'first comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ],
+};
-describe('handleCreateIncident', () => {
- beforeAll(() => {
- ServiceNowMock.mockImplementation(() => {
- return {
- serviceNow: {
- getUserID: jest.fn().mockResolvedValue('1234'),
- createIncident: jest.fn().mockResolvedValue({
- incidentId: '123',
- number: 'INC01',
- pushedDate: '2020-03-10T12:24:20.000Z',
- }),
- updateIncident: jest.fn().mockResolvedValue({
- incidentId: '123',
- number: 'INC01',
- pushedDate: '2020-03-10T12:24:20.000Z',
- }),
- batchCreateComments: jest
- .fn()
- .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]),
- batchUpdateComments: jest
- .fn()
- .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]),
+beforeAll(() => {
+ ServiceNowMock.mockImplementation(() => {
+ return {
+ serviceNow: {
+ getUserID: jest.fn().mockResolvedValue('1234'),
+ getIncident: jest.fn().mockResolvedValue({
+ short_description: 'servicenow title',
+ description: 'servicenow desc',
+ }),
+ createIncident: jest.fn().mockResolvedValue({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ }),
+ updateIncident: jest.fn().mockResolvedValue({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ }),
+ batchCreateComments: jest
+ .fn()
+ .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]),
+ },
+ };
+ });
+});
+
+describe('handleIncident', () => {
+ test('create an incident', async () => {
+ const { serviceNow } = new ServiceNowMock();
+
+ const res = await handleIncident({
+ incidentId: null,
+ serviceNow,
+ params,
+ comments: params.comments,
+ mapping: finalMapping,
+ });
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ comments: [
+ {
+ commentId: '456',
+ pushedDate: '2020-03-10T12:24:20.000Z',
},
- };
+ ],
});
});
+ test('update an incident', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ const res = await handleIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: params.comments,
+ mapping: finalMapping,
+ });
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ comments: [
+ {
+ commentId: '456',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ },
+ ],
+ });
+ });
+});
+
+describe('handleCreateIncident', () => {
test('create an incident without comments', async () => {
const { serviceNow } = new ServiceNowMock();
const res = await handleCreateIncident({
serviceNow,
- params: incident,
+ params,
comments: [],
mapping: finalMapping,
});
expect(serviceNow.createIncident).toHaveBeenCalled();
- expect(serviceNow.createIncident).toHaveBeenCalledWith(incident);
+ expect(serviceNow.createIncident).toHaveBeenCalledWith({
+ short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
expect(serviceNow.createIncident).toHaveReturned();
expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
expect(res).toEqual({
@@ -80,16 +169,36 @@ describe('handleCreateIncident', () => {
const res = await handleCreateIncident({
serviceNow,
- params: incident,
- comments,
+ params,
+ comments: params.comments,
mapping: finalMapping,
});
expect(serviceNow.createIncident).toHaveBeenCalled();
- expect(serviceNow.createIncident).toHaveBeenCalledWith(incident);
+ expect(serviceNow.createIncident).toHaveBeenCalledWith({
+ description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
expect(serviceNow.createIncident).toHaveReturned();
expect(serviceNow.batchCreateComments).toHaveBeenCalled();
- expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments');
+ expect(serviceNow.batchCreateComments).toHaveBeenCalledWith(
+ '123',
+ [
+ {
+ comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
+ commentId: '456',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzU3LDFd',
+ },
+ ],
+ 'comments'
+ );
expect(res).toEqual({
incidentId: '123',
number: 'INC01',
@@ -102,22 +211,27 @@ describe('handleCreateIncident', () => {
],
});
});
+});
+describe('handleUpdateIncident', () => {
test('update an incident without comments', async () => {
const { serviceNow } = new ServiceNowMock();
const res = await handleUpdateIncident({
incidentId: '123',
serviceNow,
- params: incident,
+ params,
comments: [],
mapping: finalMapping,
});
expect(serviceNow.updateIncident).toHaveBeenCalled();
- expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident);
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
expect(serviceNow.updateIncident).toHaveReturned();
- expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
expect(res).toEqual({
incidentId: '123',
number: 'INC01',
@@ -125,23 +239,89 @@ describe('handleCreateIncident', () => {
});
});
- test('update an incident and create new comments', async () => {
+ test('update an incident with comments', async () => {
const { serviceNow } = new ServiceNowMock();
+ serviceNow.batchCreateComments.mockResolvedValue([
+ { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' },
+ { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' },
+ ]);
const res = await handleUpdateIncident({
incidentId: '123',
serviceNow,
- params: incident,
- comments,
+ params,
+ comments: [
+ {
+ comment: 'first comment',
+ commentId: '456',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzU3LDFd',
+ },
+ {
+ comment: 'second comment',
+ commentId: '789',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ version: 'WzU3LDFd',
+ },
+ ],
mapping: finalMapping,
});
expect(serviceNow.updateIncident).toHaveBeenCalled();
- expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident);
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
expect(serviceNow.updateIncident).toHaveReturned();
- expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled();
- expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments');
-
+ expect(serviceNow.batchCreateComments).toHaveBeenCalled();
+ expect(serviceNow.batchCreateComments).toHaveBeenCalledWith(
+ '123',
+ [
+ {
+ comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
+ commentId: '456',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzU3LDFd',
+ },
+ {
+ comment: 'second comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
+ commentId: '789',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ version: 'WzU3LDFd',
+ },
+ ],
+ 'comments'
+ );
expect(res).toEqual({
incidentId: '123',
number: 'INC01',
@@ -151,7 +331,487 @@ describe('handleCreateIncident', () => {
commentId: '456',
pushedDate: '2020-03-10T12:24:20.000Z',
},
+ {
+ commentId: '789',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ },
],
});
});
});
+
+describe('handleUpdateIncident: different action types', () => {
+ test('overwrite & append', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'overwrite',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description:
+ 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('nothing & append', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'nothing',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ description:
+ 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('append & append', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'append',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description:
+ 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description:
+ 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('nothing & nothing', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'nothing',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {});
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('overwrite & nothing', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'overwrite',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('overwrite & overwrite', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'overwrite',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('nothing & overwrite', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'nothing',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('append & overwrite', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'append',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description:
+ 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+ test('append & nothing', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ finalMapping.set('title', {
+ target: 'short_description',
+ actionType: 'append',
+ });
+
+ finalMapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ finalMapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ finalMapping.set('short_description', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ const res = await handleUpdateIncident({
+ incidentId: '123',
+ serviceNow,
+ params,
+ comments: [],
+ mapping: finalMapping,
+ });
+
+ expect(serviceNow.updateIncident).toHaveBeenCalled();
+ expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {
+ short_description:
+ 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ expect(serviceNow.updateIncident).toHaveReturned();
+ expect(serviceNow.batchCreateComments).not.toHaveBeenCalled();
+ expect(res).toEqual({
+ incidentId: '123',
+ number: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ });
+ });
+});
+
+describe('createComments', () => {
+ test('create comments correctly', async () => {
+ const { serviceNow } = new ServiceNowMock();
+ serviceNow.batchCreateComments.mockResolvedValue([
+ { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' },
+ { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' },
+ ]);
+
+ const comments = [
+ {
+ comment: 'first comment',
+ commentId: '456',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzU3LDFd',
+ },
+ {
+ comment: 'second comment',
+ commentId: '789',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ version: 'WzU3LDFd',
+ },
+ ];
+
+ const res = await createComments(serviceNow, '123', 'comments', comments);
+
+ expect(serviceNow.batchCreateComments).toHaveBeenCalled();
+ expect(serviceNow.batchCreateComments).toHaveBeenCalledWith(
+ '123',
+ [
+ {
+ comment: 'first comment',
+ commentId: '456',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzU3LDFd',
+ },
+ {
+ comment: 'second comment',
+ commentId: '789',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ version: 'WzU3LDFd',
+ },
+ ],
+ 'comments'
+ );
+ expect(res).toEqual([
+ {
+ commentId: '456',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ },
+ {
+ commentId: '789',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts
index 47120c5da096d..6439a68813fd5 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts
@@ -5,26 +5,27 @@
*/
import { zipWith } from 'lodash';
-import { Incident, CommentResponse } from './lib/types';
+import { CommentResponse } from './lib/types';
import {
- ActionHandlerArguments,
- UpdateParamsType,
- UpdateActionHandlerArguments,
- IncidentCreationResponse,
- CommentType,
- CommentsZipped,
+ HandlerResponse,
+ Comment,
+ SimpleComment,
+ CreateHandlerArguments,
+ UpdateHandlerArguments,
+ IncidentHandlerArguments,
} from './types';
import { ServiceNow } from './lib';
+import { transformFields, prepareFieldsForTransformation, transformComments } from './helpers';
-const createComments = async (
+export const createComments = async (
serviceNow: ServiceNow,
incidentId: string,
key: string,
- comments: CommentType[]
-): Promise => {
+ comments: Comment[]
+): Promise => {
const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key);
- return zipWith(comments, createdComments, (a: CommentType, b: CommentResponse) => ({
+ return zipWith(comments, createdComments, (a: Comment, b: CommentResponse) => ({
commentId: a.commentId,
pushedDate: b.pushedDate,
}));
@@ -35,16 +36,30 @@ export const handleCreateIncident = async ({
params,
comments,
mapping,
-}: ActionHandlerArguments): Promise => {
- const paramsAsIncident = params as Incident;
+}: CreateHandlerArguments): Promise => {
+ const fields = prepareFieldsForTransformation({
+ params,
+ mapping,
+ });
+
+ const incident = transformFields({
+ params,
+ fields,
+ });
const { incidentId, number, pushedDate } = await serviceNow.createIncident({
- ...paramsAsIncident,
+ ...incident,
});
- const res: IncidentCreationResponse = { incidentId, number, pushedDate };
+ const res: HandlerResponse = { incidentId, number, pushedDate };
- if (comments && Array.isArray(comments) && comments.length > 0) {
+ if (
+ comments &&
+ Array.isArray(comments) &&
+ comments.length > 0 &&
+ mapping.get('comments').actionType !== 'nothing'
+ ) {
+ comments = transformComments(comments, params, ['informationAdded']);
res.comments = [
...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)),
];
@@ -59,16 +74,33 @@ export const handleUpdateIncident = async ({
params,
comments,
mapping,
-}: UpdateActionHandlerArguments): Promise => {
- const paramsAsIncident = params as UpdateParamsType;
+}: UpdateHandlerArguments): Promise => {
+ const currentIncident = await serviceNow.getIncident(incidentId);
+ const fields = prepareFieldsForTransformation({
+ params,
+ mapping,
+ defaultPipes: ['informationUpdated'],
+ });
+
+ const incident = transformFields({
+ params,
+ fields,
+ currentIncident,
+ });
const { number, pushedDate } = await serviceNow.updateIncident(incidentId, {
- ...paramsAsIncident,
+ ...incident,
});
- const res: IncidentCreationResponse = { incidentId, number, pushedDate };
+ const res: HandlerResponse = { incidentId, number, pushedDate };
- if (comments && Array.isArray(comments) && comments.length > 0) {
+ if (
+ comments &&
+ Array.isArray(comments) &&
+ comments.length > 0 &&
+ mapping.get('comments').actionType !== 'nothing'
+ ) {
+ comments = transformComments(comments, params, ['informationAdded']);
res.comments = [
...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)),
];
@@ -76,3 +108,17 @@ export const handleUpdateIncident = async ({
return { ...res };
};
+
+export const handleIncident = async ({
+ incidentId,
+ serviceNow,
+ params,
+ comments,
+ mapping,
+}: IncidentHandlerArguments): Promise => {
+ if (!incidentId) {
+ return await handleCreateIncident({ serviceNow, params, comments, mapping });
+ } else {
+ return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping });
+ }
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts
index 96962b41b3c68..ce8c3542ab69f 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts
@@ -4,18 +4,62 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { normalizeMapping, buildMap, mapParams } from './helpers';
+import {
+ normalizeMapping,
+ buildMap,
+ mapParams,
+ appendField,
+ appendInformationToField,
+ prepareFieldsForTransformation,
+ transformFields,
+ transformComments,
+} from './helpers';
import { mapping, finalMapping } from './mock';
import { SUPPORTED_SOURCE_FIELDS } from './constants';
-import { MapsType } from './types';
+import { MapEntry, Params, Comment } from './types';
-const maliciousMapping: MapsType[] = [
+const maliciousMapping: MapEntry[] = [
{ source: '__proto__', target: 'short_description', actionType: 'nothing' },
{ source: 'description', target: '__proto__', actionType: 'nothing' },
{ source: 'comments', target: 'comments', actionType: 'nothing' },
{ source: 'unsupportedSource', target: 'comments', actionType: 'nothing' },
];
+const fullParams: Params = {
+ caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
+ title: 'a title',
+ description: 'a description',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ incidentId: null,
+ incident: {
+ short_description: 'a title',
+ description: 'a description',
+ },
+ comments: [
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'second comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ],
+};
+
describe('sanitizeMapping', () => {
test('remove malicious fields', () => {
const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping);
@@ -81,3 +125,251 @@ describe('mapParams', () => {
expect(fields).not.toEqual(expect.objectContaining(unexpectedFields));
});
});
+
+describe('prepareFieldsForTransformation', () => {
+ test('prepare fields with defaults', () => {
+ const res = prepareFieldsForTransformation({
+ params: fullParams,
+ mapping: finalMapping,
+ });
+ expect(res).toEqual([
+ {
+ key: 'short_description',
+ value: 'a title',
+ actionType: 'overwrite',
+ pipes: ['informationCreated'],
+ },
+ {
+ key: 'description',
+ value: 'a description',
+ actionType: 'append',
+ pipes: ['informationCreated', 'append'],
+ },
+ ]);
+ });
+
+ test('prepare fields with default pipes', () => {
+ const res = prepareFieldsForTransformation({
+ params: fullParams,
+ mapping: finalMapping,
+ defaultPipes: ['myTestPipe'],
+ });
+ expect(res).toEqual([
+ {
+ key: 'short_description',
+ value: 'a title',
+ actionType: 'overwrite',
+ pipes: ['myTestPipe'],
+ },
+ {
+ key: 'description',
+ value: 'a description',
+ actionType: 'append',
+ pipes: ['myTestPipe', 'append'],
+ },
+ ]);
+ });
+});
+
+describe('transformFields', () => {
+ test('transform fields for creation correctly', () => {
+ const fields = prepareFieldsForTransformation({
+ params: fullParams,
+ mapping: finalMapping,
+ });
+
+ const res = transformFields({
+ params: fullParams,
+ fields,
+ });
+
+ expect(res).toEqual({
+ short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ });
+
+ test('transform fields for update correctly', () => {
+ const fields = prepareFieldsForTransformation({
+ params: fullParams,
+ mapping: finalMapping,
+ defaultPipes: ['informationUpdated'],
+ });
+
+ const res = transformFields({
+ params: fullParams,
+ fields,
+ currentIncident: {
+ short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ },
+ });
+ expect(res).toEqual({
+ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ description:
+ 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ });
+ });
+
+ test('add newline character to descripton', () => {
+ const fields = prepareFieldsForTransformation({
+ params: fullParams,
+ mapping: finalMapping,
+ defaultPipes: ['informationUpdated'],
+ });
+
+ const res = transformFields({
+ params: fullParams,
+ fields,
+ currentIncident: {
+ short_description: 'first title',
+ description: 'first description',
+ },
+ });
+ expect(res.description?.includes('\r\n')).toBe(true);
+ });
+
+ test('append username if fullname is undefined', () => {
+ const fields = prepareFieldsForTransformation({
+ params: fullParams,
+ mapping: finalMapping,
+ });
+
+ const res = transformFields({
+ params: { ...fullParams, createdBy: { fullName: null, username: 'elastic' } },
+ fields,
+ });
+
+ expect(res).toEqual({
+ short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)',
+ description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)',
+ });
+ });
+});
+
+describe('appendField', () => {
+ test('prefix correctly', () => {
+ expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' }));
+ });
+
+ test('suffix correctly', () => {
+ expect('my_value my_suffix').toEqual(appendField({ value: 'my_value', suffix: 'my_suffix' }));
+ });
+
+ test('prefix and suffix correctly', () => {
+ expect('my_prefixmy_value my_suffix').toEqual(
+ appendField({ value: 'my_value', prefix: 'my_prefix', suffix: 'my_suffix' })
+ );
+ });
+});
+
+describe('appendInformationToField', () => {
+ test('creation mode', () => {
+ const res = appendInformationToField({
+ value: 'my value',
+ user: 'Elastic Test User',
+ date: '2020-03-13T08:34:53.450Z',
+ mode: 'create',
+ });
+ expect(res).toEqual('my value (created at 2020-03-13T08:34:53.450Z by Elastic Test User)');
+ });
+
+ test('update mode', () => {
+ const res = appendInformationToField({
+ value: 'my value',
+ user: 'Elastic Test User',
+ date: '2020-03-13T08:34:53.450Z',
+ mode: 'update',
+ });
+ expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)');
+ });
+
+ test('add mode', () => {
+ const res = appendInformationToField({
+ value: 'my value',
+ user: 'Elastic Test User',
+ date: '2020-03-13T08:34:53.450Z',
+ mode: 'add',
+ });
+ expect(res).toEqual('my value (added at 2020-03-13T08:34:53.450Z by Elastic Test User)');
+ });
+});
+
+describe('transformComments', () => {
+ test('transform creation comments', () => {
+ const comments: Comment[] = [
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ];
+ const res = transformComments(comments, fullParams, ['informationCreated']);
+ expect(res).toEqual([
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ]);
+ });
+
+ test('transform update comments', () => {
+ const comments: Comment[] = [
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ];
+ const res = transformComments(comments, fullParams, ['informationUpdated']);
+ expect(res).toEqual([
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ]);
+ });
+ test('transform added comments', () => {
+ const comments: Comment[] = [
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ];
+ const res = transformComments(comments, fullParams, ['informationAdded']);
+ expect(res).toEqual([
+ {
+ commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ version: 'WzU3LDFd',
+ comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts
index 99e67c1c43f35..46d4789e0bd53 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts
@@ -3,18 +3,34 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { flow } from 'lodash';
import { SUPPORTED_SOURCE_FIELDS } from './constants';
-import { MapsType, FinalMapping } from './types';
+import {
+ MapEntry,
+ Mapping,
+ AppendFieldArgs,
+ AppendInformationFieldArgs,
+ Params,
+ Comment,
+ TransformFieldsArgs,
+ PipedField,
+ PrepareFieldsForTransformArgs,
+ KeyAny,
+} from './types';
+import { Incident } from './lib/types';
-export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => {
+import * as transformers from './transformers';
+import * as i18n from './translations';
+
+export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => {
// Prevent prototype pollution and remove unsupported fields
return mapping.filter(
- m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source)
+ m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source)
);
};
-export const buildMap = (mapping: MapsType[]): FinalMapping => {
+export const buildMap = (mapping: MapEntry[]): Mapping => {
return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => {
const { source, target, actionType } = field;
fieldsMap.set(source, { target, actionType });
@@ -23,11 +39,7 @@ export const buildMap = (mapping: MapsType[]): FinalMapping => {
}, new Map());
};
-interface KeyAny {
- [key: string]: unknown;
-}
-
-export const mapParams = (params: any, mapping: FinalMapping) => {
+export const mapParams = (params: any, mapping: Mapping) => {
return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => {
const field = mapping.get(curr);
if (field) {
@@ -36,3 +48,72 @@ export const mapParams = (params: any, mapping: FinalMapping) => {
return prev;
}, {});
};
+
+export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => {
+ return `${prefix}${value} ${suffix}`;
+};
+
+const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution exists.
+
+export const prepareFieldsForTransformation = ({
+ params,
+ mapping,
+ defaultPipes = ['informationCreated'],
+}: PrepareFieldsForTransformArgs): PipedField[] => {
+ return Object.keys(params.incident)
+ .filter(p => mapping.get(p).actionType !== 'nothing')
+ .map(p => ({
+ key: p,
+ value: params.incident[p],
+ actionType: mapping.get(p).actionType,
+ pipes: [...defaultPipes],
+ }))
+ .map(p => ({
+ ...p,
+ pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes,
+ }));
+};
+
+export const transformFields = ({
+ params,
+ fields,
+ currentIncident,
+}: TransformFieldsArgs): Incident => {
+ return fields.reduce((prev: Incident, cur) => {
+ const transform = flow(...cur.pipes.map(p => t[p]));
+ prev[cur.key] = transform({
+ value: cur.value,
+ date: params.createdAt,
+ user: params.createdBy.fullName ?? params.createdBy.username,
+ previousValue: currentIncident ? currentIncident[cur.key] : '',
+ }).value;
+ return prev;
+ }, {} as Incident);
+};
+
+export const appendInformationToField = ({
+ value,
+ user,
+ date,
+ mode = 'create',
+}: AppendInformationFieldArgs): string => {
+ return appendField({
+ value,
+ suffix: i18n.FIELD_INFORMATION(mode, date, user),
+ });
+};
+
+export const transformComments = (
+ comments: Comment[],
+ params: Params,
+ pipes: string[]
+): Comment[] => {
+ return comments.map(c => ({
+ ...c,
+ comment: flow(...pipes.map(p => t[p]))({
+ value: c.comment,
+ date: params.createdAt,
+ user: params.createdBy.fullName ?? '',
+ }).value,
+ }));
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts
index a1df243b0ee7c..8ee81c5e76451 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts
@@ -14,13 +14,12 @@ import { configUtilsMock } from '../../actions_config.mock';
import { ACTION_TYPE_ID } from './constants';
import * as i18n from './translations';
-import { handleCreateIncident, handleUpdateIncident } from './action_handlers';
+import { handleIncident } from './action_handlers';
import { incidentResponse } from './mock';
jest.mock('./action_handlers');
-const handleCreateIncidentMock = handleCreateIncident as jest.Mock;
-const handleUpdateIncidentMock = handleUpdateIncident as jest.Mock;
+const handleIncidentMock = handleIncident as jest.Mock;
const services: Services = {
callCluster: async (path: string, opts: any) => {},
@@ -63,12 +62,19 @@ const mockOptions = {
incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d',
title: 'Incident title',
description: 'Incident description',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
comments: [
{
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
version: 'WzU3LDFd',
comment: 'A comment',
- incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
},
],
},
@@ -169,8 +175,7 @@ describe('validateParams()', () => {
describe('execute()', () => {
beforeEach(() => {
- handleCreateIncidentMock.mockReset();
- handleUpdateIncidentMock.mockReset();
+ handleIncidentMock.mockReset();
});
test('should create an incident', async () => {
@@ -185,7 +190,7 @@ describe('execute()', () => {
services,
};
- handleCreateIncidentMock.mockImplementation(() => incidentResponse);
+ handleIncidentMock.mockImplementation(() => incidentResponse);
const actionResponse = await actionType.executor(executorOptions);
expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse });
@@ -205,7 +210,7 @@ describe('execute()', () => {
};
const errorMessage = 'Failed to create incident';
- handleCreateIncidentMock.mockImplementation(() => {
+ handleIncidentMock.mockImplementation(() => {
throw new Error(errorMessage);
});
@@ -243,7 +248,7 @@ describe('execute()', () => {
};
const errorMessage = 'Failed to update incident';
- handleUpdateIncidentMock.mockImplementation(() => {
+ handleIncidentMock.mockImplementation(() => {
throw new Error(errorMessage);
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
index 01e566af17d08..f844bef6441ee 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
@@ -18,12 +18,12 @@ import { ServiceNow } from './lib';
import * as i18n from './translations';
import { ACTION_TYPE_ID } from './constants';
-import { ConfigType, SecretsType, ParamsType, CommentType } from './types';
+import { ConfigType, SecretsType, Comment, ExecutorParams } from './types';
import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema';
import { buildMap, mapParams } from './helpers';
-import { handleCreateIncident, handleUpdateIncident } from './action_handlers';
+import { handleIncident } from './action_handlers';
function validateConfig(
configurationUtilities: ActionsConfigurationUtilities,
@@ -77,21 +77,22 @@ async function serviceNowExecutor(
const actionId = execOptions.actionId;
const {
apiUrl,
- casesConfiguration: { mapping },
+ casesConfiguration: { mapping: configurationMapping },
} = execOptions.config as ConfigType;
const { username, password } = execOptions.secrets as SecretsType;
- const params = execOptions.params as ParamsType;
+ const params = execOptions.params as ExecutorParams;
const { comments, incidentId, ...restParams } = params;
- const finalMap = buildMap(mapping);
- const restParamsMapped = mapParams(restParams, finalMap);
+ const mapping = buildMap(configurationMapping);
+ const incident = mapParams(restParams, mapping);
const serviceNow = new ServiceNow({ url: apiUrl, username, password });
const handlerInput = {
+ incidentId,
serviceNow,
- params: restParamsMapped,
- comments: comments as CommentType[],
- mapping: finalMap,
+ params: { ...params, incident },
+ comments: comments as Comment[],
+ mapping,
};
const res: Pick &
@@ -100,13 +101,7 @@ async function serviceNowExecutor(
actionId,
};
- let data = {};
-
- if (!incidentId) {
- data = await handleCreateIncident(handlerInput);
- } else {
- data = await handleUpdateIncident({ incidentId, ...handlerInput });
- }
+ const data = await handleIncident(handlerInput);
return {
...res,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts
index 22be625611e85..17c8bce651403 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts
@@ -132,7 +132,10 @@ describe('ServiceNow lib', () => {
commentId: '456',
version: 'WzU3LDFd',
comment: 'A comment',
- incidentCommentId: undefined,
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
};
const res = await serviceNow.createComment('123', comment, 'comments');
@@ -173,13 +176,19 @@ describe('ServiceNow lib', () => {
commentId: '123',
version: 'WzU3LDFd',
comment: 'A comment',
- incidentCommentId: undefined,
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
},
{
commentId: '456',
version: 'WzU3LDFd',
comment: 'A second comment',
- incidentCommentId: undefined,
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
},
];
const res = await serviceNow.batchCreateComments('000', comments, 'comments');
@@ -210,7 +219,9 @@ describe('ServiceNow lib', () => {
try {
await serviceNow.getUserID();
} catch (error) {
- expect(error.message).toEqual('[ServiceNow]: Instance is not alive.');
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.'
+ );
}
});
@@ -226,7 +237,96 @@ describe('ServiceNow lib', () => {
try {
await serviceNow.getUserID();
} catch (error) {
- expect(error.message).toEqual('[ServiceNow]: Instance is not alive.');
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.'
+ );
+ }
+ });
+
+ test('check error when getting user', async () => {
+ expect.assertions(1);
+
+ axiosMock.mockImplementationOnce(() => {
+ throw new Error('Bad request.');
+ });
+ try {
+ await serviceNow.getUserID();
+ } catch (error) {
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to get user id. Error: Bad request.'
+ );
+ }
+ });
+
+ test('check error when getting incident', async () => {
+ expect.assertions(1);
+
+ axiosMock.mockImplementationOnce(() => {
+ throw new Error('Bad request.');
+ });
+ try {
+ await serviceNow.getIncident('123');
+ } catch (error) {
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to get incident with id 123. Error: Bad request.'
+ );
+ }
+ });
+
+ test('check error when creating incident', async () => {
+ expect.assertions(1);
+
+ axiosMock.mockImplementationOnce(() => {
+ throw new Error('Bad request.');
+ });
+ try {
+ await serviceNow.createIncident({ short_description: 'title' });
+ } catch (error) {
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to create incident. Error: Bad request.'
+ );
+ }
+ });
+
+ test('check error when updating incident', async () => {
+ expect.assertions(1);
+
+ axiosMock.mockImplementationOnce(() => {
+ throw new Error('Bad request.');
+ });
+ try {
+ await serviceNow.updateIncident('123', { short_description: 'title' });
+ } catch (error) {
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to update incident with id 123. Error: Bad request.'
+ );
+ }
+ });
+
+ test('check error when creating comment', async () => {
+ expect.assertions(1);
+
+ axiosMock.mockImplementationOnce(() => {
+ throw new Error('Bad request.');
+ });
+ try {
+ await serviceNow.createComment(
+ '123',
+ {
+ commentId: '456',
+ version: 'WzU3LDFd',
+ comment: 'A second comment',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: null,
+ updatedBy: null,
+ },
+ 'comment'
+ );
+ } catch (error) {
+ expect(error.message).toEqual(
+ '[Action][ServiceNow]: Unable to create comment at incident with id 123. Error: Bad request.'
+ );
}
});
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts
index b3d17affb14c2..2d1d8975c9efc 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts
@@ -8,7 +8,7 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios';
import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants';
import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types';
-import { CommentType } from '../types';
+import { Comment } from '../types';
const validStatusCodes = [200, 201];
@@ -68,41 +68,77 @@ class ServiceNow {
return `${date} GMT`;
}
+ private _getErrorMessage(msg: string) {
+ return `[Action][ServiceNow]: ${msg}`;
+ }
+
async getUserID(): Promise {
- const res = await this._request({ url: `${this.userUrl}${this.instance.username}` });
- return res.data.result[0].sys_id;
+ try {
+ const res = await this._request({ url: `${this.userUrl}${this.instance.username}` });
+ return res.data.result[0].sys_id;
+ } catch (error) {
+ throw new Error(this._getErrorMessage(`Unable to get user id. Error: ${error.message}`));
+ }
}
- async createIncident(incident: Incident): Promise {
- const res = await this._request({
- url: `${this.incidentUrl}`,
- method: 'post',
- data: { ...incident },
- });
+ async getIncident(incidentId: string) {
+ try {
+ const res = await this._request({
+ url: `${this.incidentUrl}/${incidentId}`,
+ });
+
+ return { ...res.data.result };
+ } catch (error) {
+ throw new Error(
+ this._getErrorMessage(
+ `Unable to get incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
+ }
- return {
- number: res.data.result.number,
- incidentId: res.data.result.sys_id,
- pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(),
- };
+ async createIncident(incident: Incident): Promise {
+ try {
+ const res = await this._request({
+ url: `${this.incidentUrl}`,
+ method: 'post',
+ data: { ...incident },
+ });
+
+ return {
+ number: res.data.result.number,
+ incidentId: res.data.result.sys_id,
+ pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`));
+ }
}
async updateIncident(incidentId: string, incident: UpdateIncident): Promise {
- const res = await this._patch({
- url: `${this.incidentUrl}/${incidentId}`,
- data: { ...incident },
- });
-
- return {
- number: res.data.result.number,
- incidentId: res.data.result.sys_id,
- pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
- };
+ try {
+ const res = await this._patch({
+ url: `${this.incidentUrl}/${incidentId}`,
+ data: { ...incident },
+ });
+
+ return {
+ number: res.data.result.number,
+ incidentId: res.data.result.sys_id,
+ pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ this._getErrorMessage(
+ `Unable to update incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
}
async batchCreateComments(
incidentId: string,
- comments: CommentType[],
+ comments: Comment[],
field: string
): Promise {
const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field)));
@@ -111,18 +147,26 @@ class ServiceNow {
async createComment(
incidentId: string,
- comment: CommentType,
+ comment: Comment,
field: string
): Promise {
- const res = await this._patch({
- url: `${this.commentUrl}/${incidentId}`,
- data: { [field]: comment.comment },
- });
-
- return {
- commentId: comment.commentId,
- pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
- };
+ try {
+ const res = await this._patch({
+ url: `${this.commentUrl}/${incidentId}`,
+ data: { [field]: comment.comment },
+ });
+
+ return {
+ commentId: comment.commentId,
+ pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ this._getErrorMessage(
+ `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
}
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts
index 4a3c5c42fcb44..3c245bf3f688f 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts
@@ -11,9 +11,10 @@ export interface Instance {
}
export interface Incident {
- short_description?: string;
+ short_description: string;
description?: string;
caller_id?: string;
+ [index: string]: string | undefined;
}
export interface IncidentResponse {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts
index 9a150bbede5f8..b9608511159b6 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts
@@ -4,40 +4,44 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { MapsType, FinalMapping, ParamsType } from './types';
+import { MapEntry, Mapping, ExecutorParams } from './types';
import { Incident } from './lib/types';
-const mapping: MapsType[] = [
- { source: 'title', target: 'short_description', actionType: 'nothing' },
- { source: 'description', target: 'description', actionType: 'nothing' },
- { source: 'comments', target: 'comments', actionType: 'nothing' },
+const mapping: MapEntry[] = [
+ { source: 'title', target: 'short_description', actionType: 'overwrite' },
+ { source: 'description', target: 'description', actionType: 'append' },
+ { source: 'comments', target: 'comments', actionType: 'append' },
];
-const finalMapping: FinalMapping = new Map();
+const finalMapping: Mapping = new Map();
finalMapping.set('title', {
target: 'short_description',
- actionType: 'nothing',
+ actionType: 'overwrite',
});
finalMapping.set('description', {
target: 'description',
- actionType: 'nothing',
+ actionType: 'append',
});
finalMapping.set('comments', {
target: 'comments',
- actionType: 'nothing',
+ actionType: 'append',
});
finalMapping.set('short_description', {
target: 'title',
- actionType: 'nothing',
+ actionType: 'overwrite',
});
-const params: ParamsType = {
+const params: ExecutorParams = {
caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
title: 'Incident title',
description: 'Incident description',
comments: [
@@ -45,13 +49,19 @@ const params: ParamsType = {
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
version: 'WzU3LDFd',
comment: 'A comment',
- incidentCommentId: '263ede42075300100e48fbbf7c1ed047',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
},
{
commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d',
version: 'WlK3LDFd',
comment: 'Another comment',
- incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0',
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-03-13T08:34:53.450Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
},
],
};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
index 0bb4f50819665..889b57c8e92e2 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
@@ -6,7 +6,7 @@
import { schema } from '@kbn/config-schema';
-export const MapsSchema = schema.object({
+export const MapEntrySchema = schema.object({
source: schema.string(),
target: schema.string(),
actionType: schema.oneOf([
@@ -17,7 +17,7 @@ export const MapsSchema = schema.object({
});
export const CasesConfigurationSchema = schema.object({
- mapping: schema.arrayOf(MapsSchema),
+ mapping: schema.arrayOf(MapEntrySchema),
});
export const ConfigSchemaProps = {
@@ -34,11 +34,25 @@ export const SecretsSchemaProps = {
export const SecretsSchema = schema.object(SecretsSchemaProps);
+export const UserSchema = schema.object({
+ fullName: schema.nullable(schema.string()),
+ username: schema.string(),
+});
+
+const EntityInformationSchemaProps = {
+ createdAt: schema.string(),
+ createdBy: UserSchema,
+ updatedAt: schema.nullable(schema.string()),
+ updatedBy: schema.nullable(UserSchema),
+};
+
+export const EntityInformationSchema = schema.object(EntityInformationSchemaProps);
+
export const CommentSchema = schema.object({
commentId: schema.string(),
comment: schema.string(),
version: schema.maybe(schema.string()),
- incidentCommentId: schema.maybe(schema.string()),
+ ...EntityInformationSchemaProps,
});
export const ExecutorAction = schema.oneOf([
@@ -48,8 +62,9 @@ export const ExecutorAction = schema.oneOf([
export const ParamsSchema = schema.object({
caseId: schema.string(),
+ title: schema.string(),
comments: schema.maybe(schema.arrayOf(CommentSchema)),
description: schema.maybe(schema.string()),
- title: schema.maybe(schema.string()),
- incidentId: schema.maybe(schema.string()),
+ incidentId: schema.nullable(schema.string()),
+ ...EntityInformationSchemaProps,
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts
new file mode 100644
index 0000000000000..dc0a03fab8c71
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts
@@ -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 { TransformerArgs } from './types';
+import * as i18n from './translations';
+
+export const informationCreated = ({
+ value,
+ date,
+ user,
+ ...rest
+}: TransformerArgs): TransformerArgs => ({
+ value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`,
+ ...rest,
+});
+
+export const informationUpdated = ({
+ value,
+ date,
+ user,
+ ...rest
+}: TransformerArgs): TransformerArgs => ({
+ value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`,
+ ...rest,
+});
+
+export const informationAdded = ({
+ value,
+ date,
+ user,
+ ...rest
+}: TransformerArgs): TransformerArgs => ({
+ value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`,
+ ...rest,
+});
+
+export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({
+ value: previousValue ? `${previousValue} \r\n${value}` : `${value}`,
+ ...rest,
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
index 8601c5ce772db..3b216a6c3260a 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
@@ -51,3 +51,32 @@ export const UNEXPECTED_STATUS = (status: number) =>
status,
},
});
+
+export const FIELD_INFORMATION = (
+ mode: string,
+ date: string | undefined,
+ user: string | undefined
+) => {
+ switch (mode) {
+ case 'create':
+ return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', {
+ values: { date, user },
+ defaultMessage: '(created at {date} by {user})',
+ });
+ case 'update':
+ return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', {
+ values: { date, user },
+ defaultMessage: '(updated at {date} by {user})',
+ });
+ case 'add':
+ return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', {
+ values: { date, user },
+ defaultMessage: '(added at {date} by {user})',
+ });
+ default:
+ return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', {
+ values: { date, user },
+ defaultMessage: '(created at {date} by {user})',
+ });
+ }
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
index 7442f14fed064..418b78add2429 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
@@ -11,11 +11,12 @@ import {
SecretsSchema,
ParamsSchema,
CasesConfigurationSchema,
- MapsSchema,
+ MapEntrySchema,
CommentSchema,
} from './schema';
import { ServiceNow } from './lib';
+import { Incident } from './lib/types';
// config definition
export type ConfigType = TypeOf;
@@ -23,34 +24,83 @@ export type ConfigType = TypeOf;
// secrets definition
export type SecretsType = TypeOf;
-export type ParamsType = TypeOf;
+export type ExecutorParams = TypeOf;
export type CasesConfigurationType = TypeOf;
-export type MapsType = TypeOf;
-export type CommentType = TypeOf;
+export type MapEntry = TypeOf;
+export type Comment = TypeOf;
-export type FinalMapping = Map;
+export type Mapping = Map;
-export interface ActionHandlerArguments {
+export interface Params extends ExecutorParams {
+ incident: Record;
+}
+export interface CreateHandlerArguments {
serviceNow: ServiceNow;
- params: any;
- comments: CommentType[];
- mapping: FinalMapping;
+ params: Params;
+ comments: Comment[];
+ mapping: Mapping;
}
-export type UpdateParamsType = Partial;
-export type UpdateActionHandlerArguments = ActionHandlerArguments & {
+export type UpdateHandlerArguments = CreateHandlerArguments & {
incidentId: string;
};
-export interface IncidentCreationResponse {
+export type IncidentHandlerArguments = CreateHandlerArguments & {
+ incidentId: string | null;
+};
+
+export interface HandlerResponse {
incidentId: string;
number: string;
- comments?: CommentsZipped[];
+ comments?: SimpleComment[];
pushedDate: string;
}
-export interface CommentsZipped {
+export interface SimpleComment {
commentId: string;
pushedDate: string;
}
+
+export interface AppendFieldArgs {
+ value: string;
+ prefix?: string;
+ suffix?: string;
+}
+
+export interface KeyAny {
+ [index: string]: string;
+}
+
+export interface AppendInformationFieldArgs {
+ value: string;
+ user: string;
+ date: string;
+ mode: string;
+}
+
+export interface TransformerArgs {
+ value: string;
+ date?: string;
+ user?: string;
+ previousValue?: string;
+}
+
+export interface PrepareFieldsForTransformArgs {
+ params: Params;
+ mapping: Mapping;
+ defaultPipes?: string[];
+}
+
+export interface PipedField {
+ key: string;
+ value: string;
+ actionType: string;
+ pipes: string[];
+}
+
+export interface TransformFieldsArgs {
+ params: Params;
+ fields: PipedField[];
+ currentIncident?: Incident;
+}
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts
deleted file mode 100644
index a9fbe0ef4f721..0000000000000
--- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_connector.ts
+++ /dev/null
@@ -1,68 +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 { schema } from '@kbn/config-schema';
-import Boom from 'boom';
-import { pipe } from 'fp-ts/lib/pipeable';
-import { fold } from 'fp-ts/lib/Either';
-import { identity } from 'fp-ts/lib/function';
-
-import { ActionResult } from '../../../../../../actions/common';
-import { CasesConnectorConfigurationRT, throwErrors } from '../../../../../common/api';
-import { RouteDeps } from '../../types';
-import { wrapError, escapeHatch } from '../../utils';
-
-export function initCaseConfigurePatchActionConnector({ caseService, router }: RouteDeps) {
- router.patch(
- {
- path: '/api/cases/configure/connectors/{connector_id}',
- validate: {
- params: schema.object({
- connector_id: schema.string(),
- }),
- body: escapeHatch,
- },
- },
- async (context, request, response) => {
- try {
- const query = pipe(
- CasesConnectorConfigurationRT.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
- const client = context.core.savedObjects.client;
- const { connector_id: connectorId } = request.params;
- const { cases_configuration: casesConfiguration } = query;
-
- const normalizedMapping = casesConfiguration.mapping.map(m => ({
- source: m.source,
- target: m.target,
- actionType: m.action_type,
- }));
-
- const action = await client.get('action', connectorId);
-
- const { config } = action.attributes;
- const res = await client.update('action', connectorId, {
- config: {
- ...config,
- casesConfiguration: { ...casesConfiguration, mapping: normalizedMapping },
- },
- });
-
- return response.ok({
- body: CasesConnectorConfigurationRT.encode({
- cases_configuration:
- res.attributes.config?.casesConfiguration ??
- action.attributes.config.casesConfiguration,
- }),
- });
- } catch (error) {
- return response.customError(wrapError(error));
- }
- }
- );
-}
diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts
index 956f410c9c10a..60ee57a0efea7 100644
--- a/x-pack/plugins/case/server/routes/api/index.ts
+++ b/x-pack/plugins/case/server/routes/api/index.ts
@@ -26,7 +26,6 @@ import { initGetTagsApi } from './cases/tags/get_tags';
import { RouteDeps } from './types';
import { initCaseConfigureGetActionConnector } from './cases/configure/get_connectors';
-import { initCaseConfigurePatchActionConnector } from './cases/configure/patch_connector';
import { initGetCaseConfigure } from './cases/configure/get_configure';
import { initPatchCaseConfigure } from './cases/configure/patch_configure';
import { initPostCaseConfigure } from './cases/configure/post_configure';
@@ -48,7 +47,6 @@ export function initCaseApi(deps: RouteDeps) {
initPostCommentApi(deps);
// Cases Configure
initCaseConfigureGetActionConnector(deps);
- initCaseConfigurePatchActionConnector(deps);
initGetCaseConfigure(deps);
initPatchCaseConfigure(deps);
initPostCaseConfigure(deps);
diff --git a/x-pack/plugins/case/server/scripts/README.md b/x-pack/plugins/case/server/scripts/README.md
new file mode 100644
index 0000000000000..2c35eb305282a
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/README.md
@@ -0,0 +1,90 @@
+README.md for developers working on the Case API on how to get started
+using the CURL scripts in the scripts folder.
+
+The scripts rely on CURL and jq:
+
+- [CURL](https://curl.haxx.se)
+- [jq](https://stedolan.github.io/jq/)
+
+Install curl and jq
+
+```sh
+brew update
+brew install curl
+brew install jq
+```
+
+Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL`
+and add these environment variables:
+
+```sh
+export ELASTICSEARCH_USERNAME=${user}
+export ELASTICSEARCH_PASSWORD=${password}
+export ELASTICSEARCH_URL=https://${ip}:9200
+export KIBANA_URL=http://localhost:5601
+export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id}
+export KIBANA_INDEX=.kibana-${your user id}
+```
+
+source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set:
+
+```sh
+source ~/.zshrc
+```
+
+Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will
+get in the way of the CURL scripts written as is.
+
+Go to the scripts folder `cd kibana/x-pack/plugins/case/server/scripts` and run:
+
+```sh
+./hard_reset.sh
+```
+
+which will:
+
+- Delete any existing cases you have
+- Delete any existing comments you have
+- Posts the sample case from `./mock/case/post_case.json`
+- Posts the sample comment from `./mock/comment/post_comment.json` to the new case
+
+Now you can run
+
+```sh
+./find_cases.sh
+```
+
+You should see the new case created like so:
+
+```sh
+{
+ "page": 1,
+ "per_page": 20,
+ "total": 1,
+ "cases": [
+ {
+ "id": "2e0afbc0-658c-11ea-85c8-1d8f792cbc08",
+ "version": "Wzc5NSwxXQ==",
+ "comments": [],
+ "comment_ids": [
+ "2ecec0f0-658c-11ea-85c8-1d8f792cbc08"
+ ],
+ "created_at": "2020-03-14T00:38:53.004Z",
+ "created_by": {
+ "full_name": "Steph Milovic",
+ "username": "smilovic"
+ },
+ "updated_at": null,
+ "updated_by": null,
+ "description": "This looks not so good",
+ "title": "Bad meanie defacing data",
+ "status": "open",
+ "tags": [
+ "defacement"
+ ]
+ }
+ ],
+ "count_open_cases": 1,
+ "count_closed_cases": 1
+}
+```
diff --git a/x-pack/plugins/case/server/scripts/check_env_variables.sh b/x-pack/plugins/case/server/scripts/check_env_variables.sh
new file mode 100755
index 0000000000000..2f7644051debb
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/check_env_variables.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Add this to the start of any scripts to detect if env variables are set
+
+set -e
+
+if [ -z "${ELASTICSEARCH_USERNAME}" ]; then
+ echo "Set ELASTICSEARCH_USERNAME in your environment"
+ exit 1
+fi
+
+if [ -z "${ELASTICSEARCH_PASSWORD}" ]; then
+ echo "Set ELASTICSEARCH_PASSWORD in your environment"
+ exit 1
+fi
+
+if [ -z "${ELASTICSEARCH_URL}" ]; then
+ echo "Set ELASTICSEARCH_URL in your environment"
+ exit 1
+fi
+
+if [ -z "${KIBANA_URL}" ]; then
+ echo "Set KIBANA_URL in your environment"
+ exit 1
+fi
+
+if [ -z "${TASK_MANAGER_INDEX}" ]; then
+ echo "Set TASK_MANAGER_INDEX in your environment"
+ exit 1
+fi
+
+if [ -z "${KIBANA_INDEX}" ]; then
+ echo "Set KIBANA_INDEX in your environment"
+ exit 1
+fi
diff --git a/x-pack/plugins/case/server/scripts/delete_cases.sh b/x-pack/plugins/case/server/scripts/delete_cases.sh
new file mode 100755
index 0000000000000..c04afed5fe679
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/delete_cases.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./delete_cases.sh
+
+# Example with CASE_ID args:
+# ./delete_cases.sh 1234-example-id 5678-example-id
+
+set -e
+./check_env_variables.sh
+
+if [ "$1" ]; then
+ ALL=("$@")
+ i=0
+
+ COUNT=${#ALL[@]}
+ IDS=""
+ for ID in "${ALL[@]}"
+ do
+ let i=i+1
+ if [ $i -eq $COUNT ]; then
+ IDS+="%22${ID}%22"
+ else
+ IDS+="%22${ID}%22,"
+ fi
+ done
+
+ curl -s -k \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/cases?ids=\[${IDS}\]" \
+ | jq .;
+ exit 1
+else
+ CASE_ID=("$(./generate_case_data.sh | jq '.id' -j)")
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/cases?ids=\[%22${CASE_ID}%22\]" \
+ | jq .;
+ exit 1
+fi
diff --git a/x-pack/plugins/case/server/scripts/delete_comment.sh b/x-pack/plugins/case/server/scripts/delete_comment.sh
new file mode 100755
index 0000000000000..a858d9cb11a57
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/delete_comment.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./delete_comment.sh
+
+# Example with CASE_ID and COMMENT_ID arg:
+# ./delete_comment.sh 1234-example-case-id 5678-example-comment-id
+
+set -e
+./check_env_variables.sh
+
+
+if [ "$1" ] && [ "$2" ]; then
+ curl -s -k \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/cases/$1/comments/$2" \
+ | jq .;
+ exit 1
+else
+ DATA="$(./generate_case_and_comment_data.sh | jq '{ caseId: .caseId, commentId: .commentId}' -j)"
+ CASE_ID=$(echo $DATA | jq ".caseId" -j)
+ COMMENT_ID=$(echo $DATA | jq ".commentId" -j)
+ curl -s -k \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments/$COMMENT_ID" \
+ | jq .;
+ exit 1
+fi
+./delete_case.sh [b6766a90-6559-11ea-9fd5-b52942ab389a]
\ No newline at end of file
diff --git a/x-pack/plugins/case/server/scripts/find_cases.sh b/x-pack/plugins/case/server/scripts/find_cases.sh
new file mode 100755
index 0000000000000..bb4232b0c6c27
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/find_cases.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Example:
+# ./find_cases.sh
+
+set -e
+./check_env_variables.sh
+
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET ${KIBANA_URL}${SPACE_URL}/api/cases/_find | jq .
diff --git a/x-pack/plugins/case/server/scripts/find_cases_by_filter.sh b/x-pack/plugins/case/server/scripts/find_cases_by_filter.sh
new file mode 100755
index 0000000000000..433311c117e98
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/find_cases_by_filter.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Example:
+# ./find_cases_by_filter.sh
+
+# Example get all open cases:
+# ./find_cases_by_filter.sh "cases.attributes.state:%20open"
+
+# Example get all the names that start with Bad*
+# ./find_cases_by_filter.sh "cases.attributes.title:%20Bad*"
+
+# Exampe get everything that has phishing
+# ./find_cases_by_filter.sh "cases.attributes.tags:phishing"
+
+set -e
+./check_env_variables.sh
+
+FILTER=${1:-'cases.attributes.state:%20closed'}
+
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET ${KIBANA_URL}${SPACE_URL}/api/cases/_find?filter=$FILTER | jq .
diff --git a/x-pack/plugins/case/server/scripts/find_cases_sort.sh b/x-pack/plugins/case/server/scripts/find_cases_sort.sh
new file mode 100755
index 0000000000000..436b475220102
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/find_cases_sort.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Example:
+# ./find_cases_sort.sh
+
+# Example with sort args:
+# ./find_cases_sort.sh createdAt desc
+
+set -e
+./check_env_variables.sh
+
+SORT=${1:-'createdAt'}
+ORDER=${2:-'asc'}
+
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/_find?sortField=$SORT&sortOrder=$ORDER" \
+ | jq .
diff --git a/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh b/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh
new file mode 100755
index 0000000000000..9b6f472d798e0
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/generate_case_and_comment_data.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# returns case/comment data as { commentId, commentVersion, caseId, caseVersion }
+# Example:
+# ./generate_case_and_comment_data.sh
+
+set -e
+./check_env_variables.sh
+
+COMMENT=(${1:-./mock/comment/post_comment.json})
+CASE_ID=$(./post_case.sh | jq ".id" -j)
+
+POSTED_COMMENT="$(curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X POST "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments" \
+ -d @${COMMENT} \
+ | jq '{ commentId: .id, commentVersion: .version }'
+)"
+POSTED_CASE=$(./get_case.sh $CASE_ID | jq '{ caseId: .id, caseVersion: .version }' -j)
+
+echo ${POSTED_COMMENT} ${POSTED_CASE} \
+ | jq -s add;
\ No newline at end of file
diff --git a/x-pack/plugins/case/server/scripts/generate_case_data.sh b/x-pack/plugins/case/server/scripts/generate_case_data.sh
new file mode 100755
index 0000000000000..f8f6142a5d733
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/generate_case_data.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# returns case data as { id, version }
+# Example:
+# ./generate_case_data.sh
+
+set -e
+./check_env_variables.sh
+./post_case.sh | jq '{ id: .id, version: .version }' -j;
+
diff --git a/x-pack/plugins/case/server/scripts/get_case.sh b/x-pack/plugins/case/server/scripts/get_case.sh
new file mode 100755
index 0000000000000..c0106993fd81e
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/get_case.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./get_case.sh
+
+# Example with CASE_ID arg:
+# ./get_case.sh 1234-example-id
+
+set -e
+./check_env_variables.sh
+
+
+if [ "$1" ]; then
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/$1" \
+ | jq .;
+ exit 1
+else
+ CASE_ID=("$(./generate_case_data.sh | jq '.id' -j)")
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID" \
+ | jq .;
+ exit 1
+fi
diff --git a/x-pack/plugins/case/server/scripts/get_case_comments.sh b/x-pack/plugins/case/server/scripts/get_case_comments.sh
new file mode 100755
index 0000000000000..65b7c43a68824
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/get_case_comments.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and comments it if no CASE_ID is specified
+
+# Example:
+# ./get_case_comments.sh
+
+# Example:
+# ./get_case_comments.sh 1234-example-id
+
+set -e
+./check_env_variables.sh
+
+
+if [ "$1" ]; then
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/$1/comments" \
+ | jq .;
+ exit 1
+else
+ CASE_ID="$(./generate_case_and_comment_data.sh | jq '.caseId' -j)"
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments" \
+ | jq .;
+ exit 1
+fi
diff --git a/x-pack/plugins/case/server/scripts/get_comment.sh b/x-pack/plugins/case/server/scripts/get_comment.sh
new file mode 100755
index 0000000000000..9b2f7d6636745
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/get_comment.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./get_comment.sh
+
+# Example with CASE_ID and COMMENT_ID arg:
+# ./get_comment.sh 1234-example-case-id 5678-example-comment-id
+
+set -e
+./check_env_variables.sh
+
+
+if [ "$1" ] && [ "$2" ]; then
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/$1/comments/$2" \
+ | jq .;
+ exit 1
+else
+ DATA="$(./generate_case_and_comment_data.sh | jq '{ caseId: .caseId, commentId: .commentId}' -j)"
+ CASE_ID=$(echo $DATA | jq ".caseId" -j)
+ COMMENT_ID=$(echo $DATA | jq ".commentId" -j)
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments/$COMMENT_ID" \
+ | jq .;
+ exit 1
+fi
diff --git a/x-pack/plugins/case/server/scripts/get_reporters.sh b/x-pack/plugins/case/server/scripts/get_reporters.sh
new file mode 100755
index 0000000000000..2c926269d31f8
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/get_reporters.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./get_tags.sh
+
+
+set -e
+./check_env_variables.sh
+
+curl -s -k \
+-H 'Content-Type: application/json' \
+-H 'kbn-xsrf: 123' \
+-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+-X GET "${KIBANA_URL}${SPACE_URL}/api/cases/reporters" \
+| jq .;
diff --git a/x-pack/plugins/case/server/scripts/get_status.sh b/x-pack/plugins/case/server/scripts/get_status.sh
new file mode 100755
index 0000000000000..b246a2267a222
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/get_status.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./get_tags.sh
+
+
+set -e
+./check_env_variables.sh
+
+curl -s -k \
+-H 'Content-Type: application/json' \
+-H 'kbn-xsrf: 123' \
+-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+-X GET "${KIBANA_URL}${SPACE_URL}/api/cases/status" \
+| jq .;
diff --git a/x-pack/plugins/case/server/scripts/get_tags.sh b/x-pack/plugins/case/server/scripts/get_tags.sh
new file mode 100755
index 0000000000000..c5fcf13405e0c
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/get_tags.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Creates a new case and then gets it if no CASE_ID is specified
+
+# Example:
+# ./get_tags.sh
+
+
+set -e
+./check_env_variables.sh
+
+curl -s -k \
+-H 'Content-Type: application/json' \
+-H 'kbn-xsrf: 123' \
+-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+-X GET "${KIBANA_URL}${SPACE_URL}/api/cases/tags" \
+| jq .;
diff --git a/x-pack/plugins/case/server/scripts/hard_reset.sh b/x-pack/plugins/case/server/scripts/hard_reset.sh
new file mode 100755
index 0000000000000..e5309e0ab7f6c
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/hard_reset.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Deletes all current cases and comments and creates one new case with a comment
+# Example:
+# ./hard_reset.sh
+
+set -e
+./check_env_variables.sh
+#
+ALL_CASES=$(curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/cases/_find?perPage=500" | jq '.cases' -j)
+
+IDS=""
+for row in $(echo "${ALL_CASES}" | jq -r '.[] | @base64'); do
+ _jq() {
+ echo ${row} | base64 --decode | jq -r ${1}
+ }
+ IDS+="$(_jq '.id') "
+done
+
+./generate_case_and_comment_data.sh
+./delete_cases.sh $IDS
\ No newline at end of file
diff --git a/x-pack/plugins/case/server/scripts/mock/case/post_case.json b/x-pack/plugins/case/server/scripts/mock/case/post_case.json
new file mode 100644
index 0000000000000..25a9780596828
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/mock/case/post_case.json
@@ -0,0 +1,8 @@
+{
+ "description": "This looks not so good",
+ "title": "Bad meanie defacing data",
+ "status": "open",
+ "tags": [
+ "defacement"
+ ]
+}
diff --git a/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json b/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json
new file mode 100644
index 0000000000000..cf066d2c8a1e8
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json
@@ -0,0 +1,8 @@
+{
+ "description": "I hope there are some good security engineers at this company...",
+ "title": "Another bad dude",
+ "status": "open",
+ "tags": [
+ "phishing"
+ ]
+}
diff --git a/x-pack/plugins/case/server/scripts/mock/comment/post_comment.json b/x-pack/plugins/case/server/scripts/mock/comment/post_comment.json
new file mode 100644
index 0000000000000..82cf3e7ce7309
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/mock/comment/post_comment.json
@@ -0,0 +1,3 @@
+{
+ "comment": "Solve this fast!"
+}
diff --git a/x-pack/plugins/case/server/scripts/mock/comment/post_comment_v2.json b/x-pack/plugins/case/server/scripts/mock/comment/post_comment_v2.json
new file mode 100644
index 0000000000000..e753231e36911
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/mock/comment/post_comment_v2.json
@@ -0,0 +1,3 @@
+{
+ "comment": "This looks bad"
+}
diff --git a/x-pack/plugins/case/server/scripts/patch_cases.sh b/x-pack/plugins/case/server/scripts/patch_cases.sh
new file mode 100755
index 0000000000000..2faa524daac7b
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/patch_cases.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# A new case will be generated and the title will be updated in the PATCH call
+# Example:
+# ./patch_cases.sh
+
+set -e
+./check_env_variables.sh
+
+PATCH_CASE="$(./generate_case_data.sh | jq '{ cases: [{ id: .id, version: .version, title: "Change the title" }] }' -j)"
+
+curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X PATCH ${KIBANA_URL}${SPACE_URL}/api/cases \
+ -d "$PATCH_CASE" \
+ | jq .;
diff --git a/x-pack/plugins/case/server/scripts/patch_comment.sh b/x-pack/plugins/case/server/scripts/patch_comment.sh
new file mode 100755
index 0000000000000..2f0bbe2883b0f
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/patch_comment.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# A new case and comment will be generated and the comment will be updated in the PATCH call
+# Example:
+# ./patch_comment.sh
+
+set -e
+./check_env_variables.sh
+
+DATA="$(./generate_case_and_comment_data.sh | jq '{ caseId: .caseId, id: .commentId, version: .commentVersion, comment: "Update the comment" }' -j)"
+CASE_ID=$(echo "${DATA}" | jq ".caseId" -j)
+PATCH_COMMENT=$(echo "${DATA}" | jq 'del(.caseId)')
+
+curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X PATCH "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments" \
+ -d "$PATCH_COMMENT" \
+ | jq .;
diff --git a/x-pack/plugins/case/server/scripts/post_case.sh b/x-pack/plugins/case/server/scripts/post_case.sh
new file mode 100755
index 0000000000000..fff449741fe17
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/post_case.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Example:
+# ./post_case.sh
+
+# Example:
+# ./post_case.sh ./mock/case/post_case.json
+
+# Example glob:
+# ./post_case.sh ./mock/case/*
+
+set -e
+./check_env_variables.sh
+
+# Uses a default if no argument is specified
+CASES=(${@:-./mock/case/post_case.json})
+
+for CASE in "${CASES[@]}"
+do {
+ [ -e "$CASE" ] || continue
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X POST ${KIBANA_URL}${SPACE_URL}/api/cases \
+ -d @${CASE} \
+ | jq .;
+} &
+done
+
+wait
diff --git a/x-pack/plugins/case/server/scripts/post_comment.sh b/x-pack/plugins/case/server/scripts/post_comment.sh
new file mode 100755
index 0000000000000..91e07f5bd110c
--- /dev/null
+++ b/x-pack/plugins/case/server/scripts/post_comment.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+# Example:
+# ./post_comment.sh
+
+# Example:
+# ./post_comment.sh 92970bf0-64a7-11ea-9979-d394b1de38af ./mock/comment/post_comment.json
+
+# Example glob:
+# ./post_comment.sh 92970bf0-64a7-11ea-9979-d394b1de38af ./mock/comment/*
+
+set -e
+./check_env_variables.sh
+
+# Uses a default if no argument is specified
+COMMENTS=(${2:-./mock/comment/post_comment.json})
+
+if [ "$1" ]; then
+ for COMMENT in "${COMMENTS[@]}"
+ do {
+ [ -e "$COMMENT" ] || continue
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X POST "${KIBANA_URL}${SPACE_URL}/api/cases/$1/comments" \
+ -d @${COMMENT} \
+ | jq .;
+ } &
+ done
+
+ wait
+ exit 1
+else
+ CASE_ID=("$(./generate_case_data.sh | jq '.id' -j)")
+ for COMMENT in "${COMMENTS[@]}"
+ do {
+ [ -e "$COMMENT" ] || continue
+ curl -s -k \
+ -H 'Content-Type: application/json' \
+ -H 'kbn-xsrf: 123' \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X POST "${KIBANA_URL}${SPACE_URL}/api/cases/$CASE_ID/comments" \
+ -d @${COMMENT} \
+ | jq .;
+ } &
+ done
+
+ wait
+ exit 1
+fi
\ No newline at end of file
diff --git a/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json b/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json
index a6ec31465392a..2195b74640c79 100644
--- a/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json
+++ b/x-pack/plugins/console_extensions/server/spec/generated/ml.estimate_memory_usage.json
@@ -1,7 +1,7 @@
{
"ml.estimate_memory_usage": {
"methods": [
- "POST"
+ "PUT"
],
"patterns": [
"_ml/data_frame/analytics/_estimate_memory_usage"
diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts
index 25c6a789cca93..c493e8ce86781 100644
--- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts
@@ -10,16 +10,17 @@ import {
TSearchStrategyProvider,
ISearchContext,
ISearch,
- SYNC_SEARCH_STRATEGY,
getEsPreference,
} from '../../../../../src/plugins/data/public';
import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common';
+import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy';
+import { IAsyncSearchOptions } from './types';
export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = (
context: ISearchContext
) => {
- const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY);
- const { search: syncSearch } = syncStrategyProvider(context);
+ const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY);
+ const { search: asyncSearch } = asyncStrategyProvider(context);
const search: ISearch = (
request: IEnhancedEsSearchRequest,
@@ -32,9 +33,12 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider;
+ const asyncOptions: IAsyncSearchOptions = { pollInterval: 0, ...options };
+
+ return asyncSearch(
+ { ...request, serverStrategy: ES_SEARCH_STRATEGY },
+ asyncOptions
+ ) as Observable;
};
return { search };
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index 69b357196dc32..11f0b9a0dc83c 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -14,10 +14,16 @@ import {
TSearchStrategyProvider,
ISearch,
ISearchOptions,
+ ISearchCancel,
getDefaultSearchParams,
} from '../../../../../src/plugins/data/server';
import { IEnhancedEsSearchRequest } from '../../common';
+export interface AsyncSearchResponse {
+ id: string;
+ response: SearchResponse;
+}
+
export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = (
context: ISearchContext,
caller: APICaller
@@ -28,28 +34,62 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider {
const config = await context.config$.pipe(first()).toPromise();
const defaultParams = getDefaultSearchParams(config);
- const params = { ...defaultParams, ...request.params };
+ const params = { ...defaultParams, trackTotalHits: true, ...request.params };
- const rawResponse = (await (request.indexType === 'rollup'
+ const response = await (request.indexType === 'rollup'
? rollupSearch(caller, { ...request, params }, options)
- : caller('search', params, options))) as SearchResponse;
+ : asyncSearch(caller, { ...request, params }, options));
+
+ const rawResponse =
+ request.indexType === 'rollup'
+ ? (response as SearchResponse)
+ : (response as AsyncSearchResponse).response;
+
+ if (typeof rawResponse.hits.total !== 'number') {
+ // @ts-ignore This should be fixed as part of https://github.com/elastic/kibana/issues/26356
+ rawResponse.hits.total = rawResponse.hits.total.value;
+ }
+ const id = (response as AsyncSearchResponse).id;
const { total, failed, successful } = rawResponse._shards;
const loaded = failed + successful;
- return { total, loaded, rawResponse };
+ return { id, total, loaded, rawResponse };
};
- return { search };
+ const cancel: ISearchCancel = async id => {
+ const method = 'DELETE';
+ const path = `_async_search/${id}`;
+ await caller('transport.request', { method, path });
+ };
+
+ return { search, cancel };
};
-function rollupSearch(
+function asyncSearch(
+ caller: APICaller,
+ request: IEnhancedEsSearchRequest,
+ options?: ISearchOptions
+) {
+ const { body = undefined, index = undefined, ...params } = request.id ? {} : request.params;
+
+ // If we have an ID, then just poll for that ID, otherwise send the entire request body
+ const method = request.id ? 'GET' : 'POST';
+ const path = request.id ? `_async_search/${request.id}` : `${index}/_async_search`;
+
+ // Wait up to 1s for the response to return
+ const query = toSnakeCase({ waitForCompletion: '1s', ...params });
+
+ return caller('transport.request', { method, path, body, query }, options);
+}
+
+async function rollupSearch(
caller: APICaller,
request: IEnhancedEsSearchRequest,
options?: ISearchOptions
) {
+ const { body, index, ...params } = request.params;
const method = 'POST';
- const path = `${request.params.index}/_rollup_search`;
- const { body, ...params } = request.params;
+ const path = `${index}/_rollup_search`;
const query = toSnakeCase(params);
return caller('transport.request', { method, path, body, query }, options);
}
diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts
index a687d7af1c590..dfb906c7af606 100644
--- a/x-pack/plugins/endpoint/common/generate_data.test.ts
+++ b/x-pack/plugins/endpoint/common/generate_data.test.ts
@@ -21,8 +21,8 @@ describe('data generator', () => {
const generator1 = new EndpointDocGenerator('seed');
const generator2 = new EndpointDocGenerator('seed');
const timestamp = new Date().getTime();
- const metadata1 = generator1.generateEndpointMetadata(timestamp);
- const metadata2 = generator2.generateEndpointMetadata(timestamp);
+ const metadata1 = generator1.generateHostMetadata(timestamp);
+ const metadata2 = generator2.generateHostMetadata(timestamp);
expect(metadata1).toEqual(metadata2);
});
@@ -30,14 +30,14 @@ describe('data generator', () => {
const generator1 = new EndpointDocGenerator('seed');
const generator2 = new EndpointDocGenerator('different seed');
const timestamp = new Date().getTime();
- const metadata1 = generator1.generateEndpointMetadata(timestamp);
- const metadata2 = generator2.generateEndpointMetadata(timestamp);
+ const metadata1 = generator1.generateHostMetadata(timestamp);
+ const metadata2 = generator2.generateHostMetadata(timestamp);
expect(metadata1).not.toEqual(metadata2);
});
- it('creates endpoint metadata documents', () => {
+ it('creates host metadata documents', () => {
const timestamp = new Date().getTime();
- const metadata = generator.generateEndpointMetadata(timestamp);
+ const metadata = generator.generateHostMetadata(timestamp);
expect(metadata['@timestamp']).toEqual(timestamp);
expect(metadata.event.created).toEqual(timestamp);
expect(metadata.endpoint).not.toBeNull();
diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts
index 36896e5af6810..2e1d6074d0c2f 100644
--- a/x-pack/plugins/endpoint/common/generate_data.ts
+++ b/x-pack/plugins/endpoint/common/generate_data.ts
@@ -6,7 +6,7 @@
import uuid from 'uuid';
import seedrandom from 'seedrandom';
-import { AlertEvent, EndpointEvent, EndpointMetadata, OSFields, HostFields } from './types';
+import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types';
export type Event = AlertEvent | EndpointEvent;
@@ -104,8 +104,8 @@ export class EndpointDocGenerator {
this.commonInfo = this.createHostData();
}
- // This function will create new values for all the host fields, so documents from a different endpoint can be created
- // This provides a convenient way to make documents from multiple endpoints that are all tied to a single seed value
+ // This function will create new values for all the host fields, so documents from a different host can be created
+ // This provides a convenient way to make documents from multiple hosts that are all tied to a single seed value
public randomizeHostData() {
this.commonInfo = this.createHostData();
}
@@ -129,7 +129,7 @@ export class EndpointDocGenerator {
};
}
- public generateEndpointMetadata(ts = new Date().getTime()): EndpointMetadata {
+ public generateHostMetadata(ts = new Date().getTime()): HostMetadata {
return {
'@timestamp': ts,
event: {
diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts
new file mode 100644
index 0000000000000..650486f3c3858
--- /dev/null
+++ b/x-pack/plugins/endpoint/common/models/event.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { EndpointEvent, LegacyEndpointEvent } from '../types';
+
+export function isLegacyEvent(
+ event: EndpointEvent | LegacyEndpointEvent
+): event is LegacyEndpointEvent {
+ return (event as LegacyEndpointEvent).endgame !== undefined;
+}
+
+export function eventTimestamp(
+ event: EndpointEvent | LegacyEndpointEvent
+): string | undefined | number {
+ if (isLegacyEvent(event)) {
+ return event.endgame.timestamp_utc;
+ } else {
+ return event['@timestamp'];
+ }
+}
+
+export function eventName(event: EndpointEvent | LegacyEndpointEvent): string {
+ if (isLegacyEvent(event)) {
+ return event.endgame.process_name ? event.endgame.process_name : '';
+ } else {
+ return event.process.name;
+ }
+}
diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts
index aa326c663965d..7e4cf3d700ec8 100644
--- a/x-pack/plugins/endpoint/common/types.ts
+++ b/x-pack/plugins/endpoint/common/types.ts
@@ -83,10 +83,10 @@ export interface AlertResultList {
prev: string | null;
}
-export interface EndpointResultList {
- /* the endpoints restricted by the page size */
- endpoints: EndpointMetadata[];
- /* the total number of unique endpoints in the index */
+export interface HostResultList {
+ /* the hosts restricted by the page size */
+ hosts: HostMetadata[];
+ /* the total number of unique hosts in the index */
total: number;
/* the page size requested */
request_page_size: number;
@@ -243,7 +243,7 @@ interface AlertMetadata {
*/
export type AlertData = AlertEvent & AlertMetadata;
-export interface EndpointMetadata {
+export type HostMetadata = Immutable<{
'@timestamp': number;
event: {
created: number;
@@ -258,7 +258,7 @@ export interface EndpointMetadata {
version: string;
};
host: HostFields;
-}
+}>;
/**
* Represents `total` response from Elasticsearch after ES 7.0.
@@ -311,8 +311,8 @@ export interface EndpointEvent {
version: string;
};
event: {
- category: string;
- type: string;
+ category: string | string[];
+ type: string | string[];
id: string;
kind: string;
};
@@ -328,6 +328,7 @@ export interface EndpointEvent {
name: string;
parent?: {
entity_id: string;
+ name?: string;
};
};
}
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx
index f7d6551f9093b..1bafcbec93f5f 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/components/header_nav.tsx
@@ -24,11 +24,11 @@ export const navTabs: NavTabs[] = [
href: '/',
},
{
- id: 'management',
- name: i18n.translate('xpack.endpoint.headerNav.management', {
- defaultMessage: 'Management',
+ id: 'hosts',
+ name: i18n.translate('xpack.endpoint.headerNav.hosts', {
+ defaultMessage: 'Hosts',
}),
- href: '/management',
+ href: '/hosts',
},
{
id: 'alerts',
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
index cec51f570f95d..997113754f95d 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
@@ -11,15 +11,17 @@ import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { Store } from 'redux';
+import { useObservable } from 'react-use';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { RouteCapture } from './view/route_capture';
import { EndpointPluginStartDependencies } from '../../plugin';
import { appStoreFactory } from './store';
import { AlertIndex } from './view/alerts';
-import { ManagementList } from './view/managing';
+import { HostList } from './view/hosts';
import { PolicyList } from './view/policy';
import { PolicyDetails } from './view/policy';
import { HeaderNavigation } from './components/header_nav';
+import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components';
/**
* This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
@@ -48,43 +50,49 @@ interface RouterProps {
}
const AppRoot: React.FunctionComponent = React.memo(
- ({ basename, store, coreStart: { http, notifications }, depsStart: { data } }) => (
-
-
-
-
-
-
-
- (
-
-
-
- )}
- />
-
-
-
-
- (
- {
+ const isDarkMode = useObservable(uiSettings.get$('theme:darkMode'));
+
+ return (
+
+
+
+
+
+
+
+
+