[7.x] [Watcher] New Platform (NP) Migration (#50908) (#52721)

* [Watcher] New Platform (NP) Migration (#50908)

* First iteration of watch public -> new platform
Still need to switch to np ready version of use_request

* - Switched to using np ready request
- Some updates after API changes

* First attempt at server shim

* Rename file and re-enable react hooks linting

* Fix some public types and react hooks lint rules

* Fix types

* More ES lint react hooks fixes

* Migrated server lib -> ts. Part way done with migrating routes to NP router and TS

* Big subset of routes to TS and NP router - almost there

* Delete legacy error wrappers and moved last set of routes to TS and NP router

* Remove @ts-ignore's and update route registration to use shim with http router

* Added routes validations, fixes for hooks and fixes for types

* Fix more types and finish testing API routes

* Fix usage of feature catalogue and fix time buckets types

* Fix error message shape [skip ci]

* Split legacy from new platform dependencies server-side

* Refactor: Seperate client legacy and NP dependencies

* Add file: added types file

* Fix UISettings client type import

* Update license pre-routing factory spec

* Update variable names, use of I18nContext (use NP) and docs

* Use NP elasticsearchclient

* Simplify is_es_error_factory

* Fix types

* Improve code legibility and remove second use of `useAppContext`

* Use @kbn/config-schema (not validate: false) on routes!

* Fix watch create JSON spec

* Create threshold test working

* Unskip watch_edit.test.ts

* Unskip watch_list.test.ts

* Done re-enabling component integration tests

* TimeBuckets typo + remove unnecessary // @ts-ignore

* Backport for "xPack:defaultAdminEmail" 7.x specific behaviour
This commit is contained in:
Jean-Louis Leysens 2019-12-11 16:56:51 +01:00 committed by GitHub
parent 9ed3fe0ccf
commit 85f55bc946
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
256 changed files with 2326 additions and 2223 deletions

View file

@ -209,13 +209,6 @@ module.exports = {
'react-hooks/rules-of-hooks': 'off',
},
},
{
files: ['x-pack/legacy/plugins/watcher/**/*.{js,ts,tsx}'],
rules: {
'react-hooks/rules-of-hooks': 'off',
'react-hooks/exhaustive-deps': 'off',
},
},
/**
* Prettier

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
declare module 'ui/time_buckets' {
export const TimeBuckets: any;
}

View file

@ -19,11 +19,12 @@
import { useEffect, useState, useRef } from 'react';
import { HttpServiceBase } from '../../../../../src/core/public';
import { HttpServiceBase, HttpFetchQuery } from '../../../../../src/core/public';
export interface SendRequestConfig {
path: string;
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
query?: HttpFetchQuery;
body?: any;
}
@ -48,10 +49,10 @@ export interface UseRequestResponse {
export const sendRequest = async (
httpClient: HttpServiceBase,
{ path, method, body }: SendRequestConfig
{ path, method, body, query }: SendRequestConfig
): Promise<SendRequestResponse> => {
try {
const response = await httpClient[method](path, { body });
const response = await httpClient[method](path, { body, query });
return {
data: response.data ? response.data : response,
@ -70,6 +71,7 @@ export const useRequest = (
{
path,
method,
query,
body,
pollIntervalMs,
initialData,
@ -112,6 +114,7 @@ export const useRequest = (
const requestBody = {
path,
method,
query,
body,
};

View file

@ -20,7 +20,8 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) {
'uiExports/(.*)': fileMockPath,
'^src/core/(.*)': `${kibanaDirectory}/src/core/$1`,
'^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`,
'^plugins/watcher/models/(.*)': `${xPackKibanaDirectory}/legacy/plugins/watcher/public/models/$1`,
'^plugins/watcher/np_ready/application/models/(.*)':
`${xPackKibanaDirectory}/legacy/plugins/watcher/public/np_ready/application/models/$1`,
'^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`,
'^plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`,
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath,

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { ComponentType } from 'enzyme';
import {
chromeServiceMock,
docLinksServiceMock,
uiSettingsServiceMock,
notificationServiceMock,
httpServiceMock,
} from '../../../../../../../src/core/public/mocks';
import { AppContextProvider } from '../../../public/np_ready/application/app_context';
export const mockContextValue = {
docLinks: docLinksServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
legacy: {
TimeBuckets: class MockTimeBuckets {
setBounds(_domain: any) {
return {};
}
getInterval() {
return {
expression: {},
};
}
},
MANAGEMENT_BREADCRUMB: { text: 'test' },
licenseStatus: {},
},
uiSettings: uiSettingsServiceMock.createSetupContract(),
toasts: notificationServiceMock.createSetupContract().toasts,
euiUtils: {
useChartsTheme: jest.fn(),
},
// For our test harness, we don't use this mocked out http service
http: httpServiceMock.createSetupContract(),
};
export const withAppContext = (Component: ComponentType<any>) => (props: any) => {
return (
<AppContextProvider value={mockContextValue}>
<Component {...props} />
</AppContextProvider>
);
};

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { registerFieldsRoutes } from './register_fields_routes';
export const wrapBodyResponse = (obj: object) => JSON.stringify({ body: JSON.stringify(obj) });
export const unwrapBodyResponse = (string: string) => JSON.parse(JSON.parse(string).body);

View file

@ -34,7 +34,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const defaultResponse = { watchHistoryItems: [] };
server.respondWith(
'GET',
`${API_ROOT}/watch/:id/history?startTime=*`,
`${API_ROOT}/watch/:id/history`,
mockResponse(defaultResponse, response)
);
};

View file

@ -11,7 +11,7 @@ import { setup as watchCreateThresholdSetup } from './watch_create_threshold.hel
import { setup as watchEditSetup } from './watch_edit.helpers';
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils';
export { wrapBodyResponse, unwrapBodyResponse } from './body_response';
export { setupEnvironment } from './setup_environment';
export const pageHelpers = {

View file

@ -7,9 +7,17 @@
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { init as initHttpRequests } from './http_requests';
import { setHttpClient, setSavedObjectsClient } from '../../../public/lib/api';
import { setHttpClient, setSavedObjectsClient } from '../../../public/np_ready/application/lib/api';
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
mockHttpClient.interceptors.response.use(
res => {
return res.data;
},
rej => {
return Promise.reject(rej);
}
);
const mockSavedObjectsClient = () => {
return {
@ -23,7 +31,7 @@ export const setupEnvironment = () => {
// @ts-ignore
setHttpClient(mockHttpClient);
setSavedObjectsClient(mockSavedObjectsClient());
setSavedObjectsClient(mockSavedObjectsClient() as any);
return {
server,

View file

@ -3,10 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { withAppContext } from './app_context.mock';
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils';
import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit';
import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit';
import { ROUTES, WATCH_TYPES } from '../../../common/constants';
import { registerRouter } from '../../../public/lib/navigation';
import { registerRouter } from '../../../public/np_ready/application/lib/navigation';
const testBedConfig: TestBedConfig = {
memoryRouter: {
@ -17,7 +18,7 @@ const testBedConfig: TestBedConfig = {
doMountAsync: true,
};
const initTestBed = registerTestBed(WatchEdit, testBedConfig);
const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig);
export interface WatchCreateJsonTestBed extends TestBed<WatchCreateJsonTestSubjects> {
actions: {

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils';
import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit';
import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit';
import { ROUTES, WATCH_TYPES } from '../../../common/constants';
import { registerRouter } from '../../../public/lib/navigation';
import { registerRouter } from '../../../public/np_ready/application/lib/navigation';
import { withAppContext } from './app_context.mock';
const testBedConfig: TestBedConfig = {
memoryRouter: {
@ -17,7 +18,7 @@ const testBedConfig: TestBedConfig = {
doMountAsync: true,
};
const initTestBed = registerTestBed(WatchEdit, testBedConfig);
const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig);
export interface WatchCreateThresholdTestBed extends TestBed<WatchCreateThresholdTestSubjects> {
actions: {

View file

@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils';
import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit';
import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit';
import { ROUTES } from '../../../common/constants';
import { registerRouter } from '../../../public/lib/navigation';
import { registerRouter } from '../../../public/np_ready/application/lib/navigation';
import { WATCH_ID } from './constants';
import { withAppContext } from './app_context.mock';
const testBedConfig: TestBedConfig = {
memoryRouter: {
@ -18,7 +19,7 @@ const testBedConfig: TestBedConfig = {
doMountAsync: true,
};
const initTestBed = registerTestBed(WatchEdit, testBedConfig);
const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig);
export interface WatchEditTestBed extends TestBed<WatchEditSubjects> {
actions: {

View file

@ -13,8 +13,9 @@ import {
TestBedConfig,
nextTick,
} from '../../../../../../test_utils';
import { WatchList } from '../../../public/sections/watch_list/components/watch_list';
import { WatchList } from '../../../public/np_ready/application/sections/watch_list/components/watch_list';
import { ROUTES } from '../../../common/constants';
import { withAppContext } from './app_context.mock';
const testBedConfig: TestBedConfig = {
memoryRouter: {
@ -23,7 +24,7 @@ const testBedConfig: TestBedConfig = {
doMountAsync: true,
};
const initTestBed = registerTestBed(WatchList, testBedConfig);
const initTestBed = registerTestBed(withAppContext(WatchList), testBedConfig);
export interface WatchListTestBed extends TestBed<WatchListTestSubjects> {
actions: {

View file

@ -13,9 +13,10 @@ import {
TestBedConfig,
nextTick,
} from '../../../../../../test_utils';
import { WatchStatus } from '../../../public/sections/watch_status/components/watch_status';
import { WatchStatus } from '../../../public/np_ready/application/sections/watch_status/components/watch_status';
import { ROUTES } from '../../../common/constants';
import { WATCH_ID } from './constants';
import { withAppContext } from './app_context.mock';
const testBedConfig: TestBedConfig = {
memoryRouter: {
@ -25,7 +26,7 @@ const testBedConfig: TestBedConfig = {
doMountAsync: true,
};
const initTestBed = registerTestBed(WatchStatus, testBedConfig);
const initTestBed = registerTestBed(withAppContext(WatchStatus), testBedConfig);
export interface WatchStatusTestBed extends TestBed<WatchStatusTestSubjects> {
actions: {

View file

@ -4,22 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { act } from 'react-dom/test-utils';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers';
import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers';
import { WATCH } from './helpers/constants';
import defaultWatchJson from '../../public/models/watch/default_watch.json';
import defaultWatchJson from '../../public/np_ready/application/models/watch/default_watch.json';
import { getExecuteDetails } from '../../test/fixtures';
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/watcher',
}));
jest.mock('ui/time_buckets', () => {});
const { setup } = pageHelpers.watchCreateJson;
describe.skip('<JsonWatchEdit /> create route', () => {
describe('<JsonWatchEdit /> create route', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: WatchCreateJsonTestBed;
@ -107,7 +100,7 @@ describe.skip('<JsonWatchEdit /> create route', () => {
'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.';
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
id: watch.id,
name: watch.name,
type: watch.type,
@ -194,7 +187,7 @@ describe.skip('<JsonWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes,
}),
@ -258,7 +251,7 @@ describe.skip('<JsonWatchEdit /> create route', () => {
const scheduledTime = `now+${SCHEDULED_TIME}s`;
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
triggerData: {
triggeredTime,

View file

@ -7,7 +7,13 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import axios from 'axios';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import {
setupEnvironment,
pageHelpers,
nextTick,
wrapBodyResponse,
unwrapBodyResponse,
} from './helpers';
import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers';
import { getExecuteDetails } from '../../test/fixtures';
import { WATCH_TYPES } from '../../common/constants';
@ -42,31 +48,8 @@ const WATCH_VISUALIZE_DATA = {
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/watcher',
getUiSettingsClient: () => ({
get: () => {},
isDefault: () => true,
}),
}));
jest.mock('ui/time_buckets', () => {
class MockTimeBuckets {
setBounds(_domain: any) {
return {};
}
getInterval() {
return {
expression: {},
};
}
}
return { TimeBuckets: MockTimeBuckets };
});
jest.mock('../../public/lib/api', () => ({
...jest.requireActual('../../public/lib/api'),
jest.mock('../../public/np_ready/application/lib/api', () => ({
...jest.requireActual('../../public/np_ready/application/lib/api'),
loadIndexPatterns: async () => {
const INDEX_PATTERNS = [
{ attributes: { title: 'index1' } },
@ -85,7 +68,7 @@ jest.mock('@elastic/eui', () => ({
EuiComboBox: (props: any) => (
<input
data-test-subj="mockComboBox"
onChange={async (syntheticEvent: any) => {
onChange={(syntheticEvent: any) => {
props.onChange([syntheticEvent['0']]);
}}
/>
@ -94,7 +77,7 @@ jest.mock('@elastic/eui', () => ({
const { setup } = pageHelpers.watchCreateThreshold;
describe.skip('<ThresholdWatchEdit /> create route', () => {
describe('<ThresholdWatchEdit /> create route', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: WatchCreateThresholdTestBed;
@ -105,12 +88,9 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
describe('on component mount', () => {
beforeEach(async () => {
testBed = await setup();
await act(async () => {
const { component } = testBed;
await nextTick();
component.update();
});
const { component } = testBed;
await nextTick();
component.update();
});
test('should set the correct page title', () => {
@ -125,13 +105,6 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
httpRequestsMockHelpers.setLoadEsFieldsResponse({ fields: ES_FIELDS });
httpRequestsMockHelpers.setLoadSettingsResponse(SETTINGS);
httpRequestsMockHelpers.setLoadWatchVisualizeResponse(WATCH_VISUALIZE_DATA);
testBed = await setup();
await act(async () => {
await nextTick();
testBed.component.update();
});
});
describe('form validation', () => {
@ -173,7 +146,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
expect(find('saveWatchButton').props().disabled).toEqual(true);
});
test('it should enable the Create button and render additonal content with valid fields', async () => {
test('it should enable the Create button and render additional content with valid fields', async () => {
const { form, find, component, exists } = testBed;
form.setInputValue('nameInput', 'my_test_watch');
@ -192,39 +165,30 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
expect(exists('watchActionsPanel')).toBe(true);
});
describe('watch conditions', () => {
beforeEach(async () => {
const { form, find, component } = testBed;
// Looks like there is an issue with using 'mockComboBox'.
describe.skip('watch conditions', () => {
beforeEach(() => {
const { form, find } = testBed;
// Name, index and time fields are required before the watch condition expression renders
form.setInputValue('nameInput', 'my_test_watch');
find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox
form.setInputValue('watchTimeFieldSelect', '@timestamp');
await act(async () => {
await nextTick();
component.update();
act(() => {
find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox
});
form.setInputValue('watchTimeFieldSelect', '@timestamp');
});
test('should require a threshold value', async () => {
const { form, find, component } = testBed;
test('should require a threshold value', () => {
const { form, find } = testBed;
find('watchThresholdButton').simulate('click');
// Provide invalid value
form.setInputValue('watchThresholdInput', '');
expect(form.getErrorsMessages()).toContain('A value is required.');
// Provide valid value
form.setInputValue('watchThresholdInput', '0');
await act(async () => {
await nextTick();
component.update();
act(() => {
find('watchThresholdButton').simulate('click');
// Provide invalid value
form.setInputValue('watchThresholdInput', '');
// Provide valid value
form.setInputValue('watchThresholdInput', '0');
});
expect(form.getErrorsMessages()).toContain('A value is required.');
expect(form.getErrorsMessages().length).toEqual(0);
});
});
@ -273,7 +237,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -300,7 +264,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
logging_1: 'force_execute',
@ -341,7 +305,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -367,7 +331,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
index_1: 'force_execute',
@ -401,7 +365,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -430,7 +394,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
slack_1: 'force_execute',
@ -471,7 +435,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -504,7 +468,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
email_1: 'force_execute',
@ -559,7 +523,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -594,7 +558,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
webhook_1: 'force_execute',
@ -645,7 +609,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -682,7 +646,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
jira_1: 'force_execute',
@ -723,7 +687,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -750,7 +714,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
};
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
executeDetails: getExecuteDetails({
actionModes: {
pagerduty_1: 'force_execute',
@ -784,7 +748,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
const latestRequest = server.requests[server.requests.length - 1];
const thresholdWatch = {
id: JSON.parse(latestRequest.requestBody).id, // watch ID is created dynamically
id: unwrapBodyResponse(latestRequest.requestBody).id, // watch ID is created dynamically
name: WATCH_NAME,
type: WATCH_TYPES.THRESHOLD,
isNew: true,
@ -801,7 +765,7 @@ describe.skip('<ThresholdWatchEdit /> create route', () => {
threshold: 1000,
};
expect(latestRequest.requestBody).toEqual(JSON.stringify(thresholdWatch));
expect(latestRequest.requestBody).toEqual(wrapBodyResponse(thresholdWatch));
});
});
});

View file

@ -6,36 +6,17 @@
import { act } from 'react-dom/test-utils';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import axios from 'axios';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers';
import { WatchEditTestBed } from './helpers/watch_edit.helpers';
import { WATCH } from './helpers/constants';
import defaultWatchJson from '../../public/models/watch/default_watch.json';
import defaultWatchJson from '../../public/np_ready/application/models/watch/default_watch.json';
import { getWatch } from '../../test/fixtures';
import { getRandomString } from '../../../../../test_utils';
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/watcher',
}));
jest.mock('ui/time_buckets', () => {
class MockTimeBuckets {
setBounds(_domain: any) {
return {};
}
getInterval() {
return {
expression: {},
};
}
}
return { TimeBuckets: MockTimeBuckets };
});
jest.mock('../../public/lib/api', () => ({
...jest.requireActual('../../public/lib/api'),
jest.mock('../../public/np_ready/application/lib/api', () => ({
...jest.requireActual('../../public/np_ready/application/lib/api'),
loadIndexPatterns: async () => {
const INDEX_PATTERNS = [
{ attributes: { title: 'index1' } },
@ -49,7 +30,7 @@ jest.mock('../../public/lib/api', () => ({
const { setup } = pageHelpers.watchEdit;
describe.skip('<WatchEdit />', () => {
describe('<WatchEdit />', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: WatchEditTestBed;
@ -110,7 +91,7 @@ describe.skip('<WatchEdit />', () => {
'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.';
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
id: watch.id,
name: EDITED_WATCH_NAME,
type: watch.type,
@ -202,7 +183,7 @@ describe.skip('<WatchEdit />', () => {
} = watch;
expect(latestRequest.requestBody).toEqual(
JSON.stringify({
wrapBodyResponse({
id,
name: EDITED_WATCH_NAME,
type,

View file

@ -18,16 +18,9 @@ import { ROUTES } from '../../common/constants';
const { API_ROOT } = ROUTES;
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/watcher',
}));
jest.mock('ui/time_buckets', () => {});
const { setup } = pageHelpers.watchList;
describe.skip('<WatchList />', () => {
describe('<WatchList />', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: WatchListTestBed;

View file

@ -14,13 +14,6 @@ import { WATCH_STATES, ACTION_STATES } from '../../common/constants';
const { API_ROOT } = ROUTES;
jest.mock('ui/chrome', () => ({
breadcrumbs: { set: () => {} },
addBasePath: (path: string) => path || '/api/watcher',
}));
jest.mock('ui/time_buckets', () => {});
const { setup } = pageHelpers.watchStatus;
const watchHistory1 = getWatchHistory({ startTime: '2019-06-04T01:11:11.294' });
@ -45,7 +38,7 @@ const watch = {
},
};
describe.skip('<WatchStatus />', () => {
describe('<WatchStatus />', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: WatchStatusTestBed;

View file

@ -0,0 +1,9 @@
{
"id": "watcher",
"version": "kibana",
"requiredPlugins": [
"home"
],
"server": true,
"ui": true
}

View file

@ -1,46 +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 { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { registerFieldsRoutes } from './server/routes/api/fields';
import { registerSettingsRoutes } from './server/routes/api/settings';
import { registerHistoryRoutes } from './server/routes/api/history';
import { registerIndicesRoutes } from './server/routes/api/indices';
import { registerLicenseRoutes } from './server/routes/api/license';
import { registerWatchesRoutes } from './server/routes/api/watches';
import { registerWatchRoutes } from './server/routes/api/watch';
import { registerLicenseChecker } from '../../server/lib/register_license_checker';
import { PLUGIN } from './common/constants';
export const pluginDefinition = {
id: PLUGIN.ID,
configPrefix: 'xpack.watcher',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
managementSections: ['plugins/watcher'],
home: ['plugins/watcher/register_feature'],
},
init: function (server) {
// Register license checker
registerLicenseChecker(
server,
PLUGIN.ID,
PLUGIN.getI18nName(i18n),
PLUGIN.MINIMUM_LICENSE_REQUIRED
);
registerFieldsRoutes(server);
registerHistoryRoutes(server);
registerIndicesRoutes(server);
registerLicenseRoutes(server);
registerSettingsRoutes(server);
registerWatchesRoutes(server);
registerWatchRoutes(server);
},
};

View file

@ -0,0 +1,32 @@
/*
* 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 { resolve } from 'path';
import { plugin } from './server/np_ready';
import { PLUGIN } from './common/constants';
export const pluginDefinition = {
id: PLUGIN.ID,
configPrefix: 'xpack.watcher',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'),
managementSections: ['plugins/watcher/legacy'],
home: ['plugins/watcher/register_feature'],
},
init(server: any) {
plugin({} as any).setup(server.newPlatform.setup.core, {
__LEGACY: {
route: server.route.bind(server),
plugins: {
watcher: server.plugins[PLUGIN.ID],
xpack_main: server.plugins.xpack_main,
},
},
});
},
};

View file

@ -1,3 +0,0 @@
<kbn-management-app section="elasticsearch/watcher">
<div id="watchReactRoot"></div>
</kbn-management-app>

View file

@ -0,0 +1,146 @@
/*
* 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 { CoreSetup, App, AppUnmount } from 'src/core/public';
import { i18n } from '@kbn/i18n';
/* Legacy UI imports */
import { npSetup, npStart } from 'ui/new_platform';
import routes from 'ui/routes';
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { TimeBuckets } from 'ui/time_buckets';
// @ts-ignore
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
/* Legacy UI imports */
import { plugin } from './np_ready';
import { PLUGIN } from '../common/constants';
import { LICENSE_STATUS_INVALID, LICENSE_STATUS_UNAVAILABLE } from '../../../common/constants';
import { manageAngularLifecycle } from './manage_angular_lifecycle';
const template = `<kbn-management-app section="elasticsearch/watcher">
<div id="watchReactRoot"></div>
</kbn-management-app>`;
let elem: HTMLElement;
let mountApp: () => AppUnmount | Promise<AppUnmount>;
let unmountApp: AppUnmount | Promise<AppUnmount>;
routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param4?', {
template,
controller: class WatcherController {
constructor($injector: any, $scope: any) {
const $route = $injector.get('$route');
const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`);
const shimCore: CoreSetup = {
...npSetup.core,
application: {
...npSetup.core.application,
register(app: App): void {
mountApp = () =>
app.mount(npStart as any, {
element: elem,
appBasePath: '/management/elasticsearch/watcher/',
});
},
},
};
// clean up previously rendered React app if one exists
// this happens because of React Router redirects
if (elem) {
((unmountApp as unknown) as AppUnmount)();
}
$scope.$$postDigest(() => {
elem = document.getElementById('watchReactRoot')!;
const instance = plugin();
instance.setup(shimCore, {
...(npSetup.plugins as typeof npSetup.plugins & { eui_utils: any }),
__LEGACY: {
MANAGEMENT_BREADCRUMB,
TimeBuckets,
licenseStatus,
},
});
instance.start(npStart.core, npStart.plugins);
(mountApp() as Promise<AppUnmount>).then(fn => (unmountApp = fn));
manageAngularLifecycle($scope, $route, elem);
});
}
} as any,
// @ts-ignore
controllerAs: 'watchRoute',
});
routes.defaults(/\/management/, {
resolve: {
watcherManagementSection: () => {
const watchesSection = management.getSection('elasticsearch/watcher');
const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`);
const { status } = licenseStatus;
if (status === LICENSE_STATUS_INVALID || status === LICENSE_STATUS_UNAVAILABLE) {
return watchesSection.hide();
}
watchesSection.show();
},
},
});
management.getSection('elasticsearch').register('watcher', {
display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watcherDisplayName', {
defaultMessage: 'Watcher',
}),
order: 6,
url: '#/management/elasticsearch/watcher/',
} as any);
management.getSection('elasticsearch/watcher').register('watches', {
display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watchesDisplayName', {
defaultMessage: 'Watches',
}),
order: 1,
} as any);
management.getSection('elasticsearch/watcher').register('watch', {
visible: false,
} as any);
management.getSection('elasticsearch/watcher/watch').register('status', {
display: i18n.translate('xpack.watcher.sections.watchList.managementSection.statusDisplayName', {
defaultMessage: 'Status',
}),
order: 1,
visible: false,
} as any);
management.getSection('elasticsearch/watcher/watch').register('edit', {
display: i18n.translate('xpack.watcher.sections.watchList.managementSection.editDisplayName', {
defaultMessage: 'Edit',
}),
order: 2,
visible: false,
} as any);
management.getSection('elasticsearch/watcher/watch').register('new', {
display: i18n.translate(
'xpack.watcher.sections.watchList.managementSection.newWatchDisplayName',
{
defaultMessage: 'New Watch',
}
),
order: 1,
visible: false,
} as any);
management.getSection('elasticsearch/watcher/watch').register('history-item', {
order: 1,
visible: false,
} as any);

View file

@ -1,24 +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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
import { ACTION_TYPES } from '../../../common/constants';
const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`;
const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`;
const esStackBase = `${elasticDocLinkBase}elastic-stack-overview/${DOC_LINK_VERSION}`;
const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`;
export const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`;
export const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`;
export const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`;
export const watchActionsConfigurationMap = {
[ACTION_TYPES.SLACK]: `${esStackBase}/actions-slack.html#configuring-slack`,
[ACTION_TYPES.PAGERDUTY]: `${esStackBase}/actions-pagerduty.html#configuring-pagerduty`,
[ACTION_TYPES.JIRA]: `${esStackBase}/actions-jira.html#configuring-jira`,
};

View file

@ -6,7 +6,7 @@
import { unmountComponentAtNode } from 'react-dom';
export const manageAngularLifecycle = ($scope, $route, elem) => {
export const manageAngularLifecycle = ($scope: any, $route: any, elem: HTMLElement) => {
const lastRoute = $route.current;
const deregister = $scope.$on('$locationChangeSuccess', () => {
@ -17,7 +17,12 @@ export const manageAngularLifecycle = ($scope, $route, elem) => {
});
$scope.$on('$destroy', () => {
deregister && deregister();
elem && unmountComponentAtNode(elem);
if (deregister) {
deregister();
}
if (elem) {
unmountComponentAtNode(elem);
}
});
};

View file

@ -1,42 +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.
*/
declare module 'plugins/watcher/models/visualize_options' {
export const VisualizeOptions: any;
}
declare module 'plugins/watcher/models/watch' {
export const Watch: any;
}
declare module 'plugins/watcher/models/watch/threshold_watch' {
export const ThresholdWatch: any;
}
declare module 'plugins/watcher/models/watch/json_watch' {
export const JsonWatch: any;
}
declare module 'plugins/watcher/models/execute_details/execute_details' {
export const ExecuteDetails: any;
}
declare module 'plugins/watcher/models/watch_history_item' {
export const WatchHistoryItem: any;
}
declare module 'plugins/watcher/models/watch_status' {
export const WatchStatus: any;
}
declare module 'plugins/watcher/models/settings' {
export const Settings: any;
}
declare module 'plugins/watcher/models/action' {
export const Action: any;
}
declare module 'ui/time_buckets' {
export const TimeBuckets: any;
}

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { registerLoadRoute } from './register_load_route';
let defaultEmailTo: string;
export function registerHistoryRoutes(server) {
registerLoadRoute(server);
}
export const setDefaultEmailTo = (emailTo: string) => {
defaultEmailTo = emailTo;
};
export const getDefaultEmailTo = () => {
return defaultEmailTo!;
};

View file

@ -4,54 +4,61 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
import React from 'react';
import {
ChromeStart,
DocLinksStart,
HttpSetup,
ToastsSetup,
IUiSettingsClient,
} from 'src/core/public';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import {
HashRouter,
Switch,
Route,
Redirect,
withRouter,
RouteComponentProps,
} from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { WatchStatus } from './sections/watch_status/components/watch_status';
import { WatchEdit } from './sections/watch_edit/components/watch_edit';
import { WatchList } from './sections/watch_list/components/watch_list';
import { registerRouter } from './lib/navigation';
import { BASE_PATH } from './constants';
import { LICENSE_STATUS_VALID } from '../../../common/constants';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { LICENSE_STATUS_VALID } from '../../../../../common/constants';
import { AppContextProvider } from './app_context';
import { LegacyDependencies } from '../types';
class ShareRouter extends Component {
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
createHref: PropTypes.func.isRequired
}).isRequired
}).isRequired
}
constructor(...args) {
super(...args);
this.registerRouter();
}
const ShareRouter = withRouter(({ children, history }: RouteComponentProps & { children: any }) => {
registerRouter({ history });
return children;
});
registerRouter() {
// Share the router with the app without requiring React or context.
const { router } = this.context;
registerRouter(router);
}
render() {
return this.props.children;
}
export interface AppDeps {
chrome: ChromeStart;
docLinks: DocLinksStart;
toasts: ToastsSetup;
http: HttpSetup;
uiSettings: IUiSettingsClient;
legacy: LegacyDependencies;
euiUtils: any;
}
export const App = ({ licenseStatus }) => {
const { status, message } = licenseStatus;
export const App = (deps: AppDeps) => {
const { status, message } = deps.legacy.licenseStatus;
if (status !== LICENSE_STATUS_VALID) {
return (
<EuiCallOut
title={(
title={
<FormattedMessage
id="xpack.watcher.app.licenseErrorTitle"
defaultMessage="License error"
/>
)}
}
color="warning"
iconType="help"
>
@ -69,7 +76,9 @@ export const App = ({ licenseStatus }) => {
return (
<HashRouter>
<ShareRouter>
<AppWithoutRouter />
<AppContextProvider value={deps}>
<AppWithoutRouter />
</AppContextProvider>
</ShareRouter>
</HashRouter>
);
@ -81,7 +90,11 @@ export const AppWithoutRouter = () => (
<Route exact path={`${BASE_PATH}watches`} component={WatchList} />
<Route exact path={`${BASE_PATH}watches/watch/:id/status`} component={WatchStatus} />
<Route exact path={`${BASE_PATH}watches/watch/:id/edit`} component={WatchEdit} />
<Route exact path={`${BASE_PATH}watches/new-watch/:type(json|threshold)`} component={WatchEdit} />
<Route
exact
path={`${BASE_PATH}watches/new-watch/:type(json|threshold)`}
component={WatchEdit}
/>
<Redirect from={BASE_PATH} to={`${BASE_PATH}watches`} />
</Switch>
);

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { createContext, useContext } from 'react';
import { DocLinksStart } from 'src/core/public';
import { ACTION_TYPES } from '../../../common/constants';
import { AppDeps } from './app';
interface ContextValue extends Omit<AppDeps, 'docLinks'> {
links: ReturnType<typeof generateDocLinks>;
}
const AppContext = createContext<ContextValue>(null as any);
const generateDocLinks = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocLinksStart) => {
const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`;
const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`;
const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`;
const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`;
const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`;
const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`;
const watchActionsConfigurationMap = {
[ACTION_TYPES.SLACK]: `${esBase}/actions-slack.html#configuring-slack`,
[ACTION_TYPES.PAGERDUTY]: `${esBase}/actions-pagerduty.html#configuring-pagerduty`,
[ACTION_TYPES.JIRA]: `${esBase}/actions-jira.html#configuring-jira`,
};
return {
putWatchApiUrl,
executeWatchApiUrl,
watcherGettingStartedUrl,
watchActionsConfigurationMap,
};
};
export const AppContextProvider = ({
children,
value,
}: {
value: AppDeps;
children: React.ReactNode;
}) => {
const { docLinks, ...rest } = value;
return (
<AppContext.Provider
value={Object.freeze({
...rest,
links: generateDocLinks(docLinks),
})}
>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => {
const ctx = useContext(AppContext);
if (!ctx) {
throw new Error('"useAppContext" can only be called inside of AppContext.Provider!');
}
return ctx;
};

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { SavedObjectsClientContract } from 'src/core/public';
import { App, AppDeps } from './app';
import { setHttpClient, setSavedObjectsClient } from './lib/api';
import { LegacyDependencies } from '../types';
import { setDefaultEmailTo } from './7_x_only';
interface BootDeps extends AppDeps {
element: HTMLElement;
savedObjects: SavedObjectsClientContract;
I18nContext: any;
legacy: LegacyDependencies;
}
export const boot = (bootDeps: BootDeps) => {
const { I18nContext, element, legacy, savedObjects, ...appDeps } = bootDeps;
setDefaultEmailTo(bootDeps.uiSettings.get('xPack:defaultAdminEmail'));
setHttpClient(appDeps.http);
setSavedObjectsClient(savedObjects);
render(
<I18nContext>
<App {...appDeps} legacy={legacy} />
</I18nContext>,
element
);
return () => unmountComponentAtNode(element);
};

View file

@ -6,8 +6,8 @@
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { toastNotifications } from 'ui/notify';
import { deleteWatches } from '../lib/api';
import { useAppContext } from '../app_context';
export const DeleteWatchesModal = ({
watchesToDelete,
@ -16,6 +16,7 @@ export const DeleteWatchesModal = ({
watchesToDelete: string[];
callback: (deleted?: string[]) => void;
}) => {
const { toasts } = useAppContext();
const numWatchesToDelete = watchesToDelete.length;
if (!numWatchesToDelete) {
return null;
@ -54,7 +55,7 @@ export const DeleteWatchesModal = ({
const numErrors = errors.length;
callback(successes);
if (numSuccesses > 0) {
toastNotifications.addSuccess(
toasts.addSuccess(
i18n.translate(
'xpack.watcher.sections.watchList.deleteSelectedWatchesSuccessNotification.descriptionText',
{
@ -67,7 +68,7 @@ export const DeleteWatchesModal = ({
}
if (numErrors > 0) {
toastNotifications.addDanger(
toasts.addDanger(
i18n.translate(
'xpack.watcher.sections.watchList.deleteSelectedWatchesErrorNotification.descriptionText',
{

View file

@ -8,6 +8,18 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import React, { Fragment } from 'react';
export interface Error {
error: string;
/**
* wrapEsError() on the server adds a "cause" array
*/
cause?: string[];
message?: string;
/**
* @deprecated
*/
data: {
error: string;
cause?: string[];
@ -21,11 +33,9 @@ interface Props {
}
export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...rest }) => {
const {
error: errorString,
cause, // wrapEsError() on the server adds a "cause" array
message,
} = error.data;
const data = error.data || error;
const { error: errorString, cause, message } = data;
return (
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>

View file

@ -6,7 +6,7 @@
import React from 'react';
import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { ACTION_STATES, WATCH_STATES } from '../../common/constants';
import { ACTION_STATES, WATCH_STATES } from '../../../../common/constants';
function StatusIcon({ status }: { status: string }) {
switch (status) {

View file

@ -3,20 +3,20 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Settings } from 'plugins/watcher/models/settings';
import { Watch } from 'plugins/watcher/models/watch';
import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item';
import { WatchStatus } from 'plugins/watcher/models/watch_status';
import { HttpSetup, SavedObjectsClientContract } from 'src/core/public';
import { Settings } from 'plugins/watcher/np_ready/application/models/settings';
import { Watch } from 'plugins/watcher/np_ready/application/models/watch';
import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item';
import { WatchStatus } from 'plugins/watcher/np_ready/application/models/watch_status';
import { __await } from 'tslib';
import chrome from 'ui/chrome';
import { ROUTES } from '../../common/constants';
import { BaseWatch, ExecutedWatchDetails } from '../../common/types/watch_types';
import { BaseWatch, ExecutedWatchDetails } from '../../../../common/types/watch_types';
import { useRequest, sendRequest } from './use_request';
let httpClient: ng.IHttpService;
import { ROUTES } from '../../../../common/constants';
export const setHttpClient = (anHttpClient: ng.IHttpService) => {
let httpClient: HttpSetup;
export const setHttpClient = (anHttpClient: HttpSetup) => {
httpClient = anHttpClient;
};
@ -24,19 +24,17 @@ export const getHttpClient = () => {
return httpClient;
};
let savedObjectsClient: any;
let savedObjectsClient: SavedObjectsClientContract;
export const setSavedObjectsClient = (aSavedObjectsClient: any) => {
export const setSavedObjectsClient = (aSavedObjectsClient: SavedObjectsClientContract) => {
savedObjectsClient = aSavedObjectsClient;
};
export const getSavedObjectsClient = () => {
return savedObjectsClient;
};
export const getSavedObjectsClient = () => savedObjectsClient;
const basePath = chrome.addBasePath(ROUTES.API_ROOT);
const basePath = ROUTES.API_ROOT;
export const loadWatches = (pollIntervalMs: number) => {
export const useLoadWatches = (pollIntervalMs: number) => {
return useRequest({
path: `${basePath}/watches`,
method: 'get',
@ -47,7 +45,7 @@ export const loadWatches = (pollIntervalMs: number) => {
});
};
export const loadWatchDetail = (id: string) => {
export const useLoadWatchDetail = (id: string) => {
return useRequest({
path: `${basePath}/watch/${id}`,
method: 'get',
@ -55,15 +53,10 @@ export const loadWatchDetail = (id: string) => {
});
};
export const loadWatchHistory = (id: string, startTime: string) => {
let path = `${basePath}/watch/${id}/history`;
if (startTime) {
path += `?startTime=${startTime}`;
}
export const useLoadWatchHistory = (id: string, startTime: string) => {
return useRequest({
path,
query: startTime ? { startTime } : undefined,
path: `${basePath}/watch/${id}/history`,
method: 'get',
deserializer: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => {
return watchHistoryItems.map((historyItem: any) =>
@ -73,7 +66,7 @@ export const loadWatchHistory = (id: string, startTime: string) => {
});
};
export const loadWatchHistoryDetail = (id: string | undefined) => {
export const useLoadWatchHistoryDetail = (id: string | undefined) => {
return useRequest({
path: !id ? '' : `${basePath}/history/${id}`,
method: 'get',
@ -83,12 +76,10 @@ export const loadWatchHistoryDetail = (id: string | undefined) => {
};
export const deleteWatches = async (watchIds: string[]) => {
const body = {
const body = JSON.stringify({
watchIds,
};
const {
data: { results },
} = await getHttpClient().post(`${basePath}/watches/delete`, body);
});
const { results } = await getHttpClient().post(`${basePath}/watches/delete`, { body });
return results;
};
@ -107,8 +98,8 @@ export const activateWatch = async (id: string) => {
};
export const loadWatch = async (id: string) => {
const { data: watch } = await getHttpClient().get(`${basePath}/watch/${id}`);
return Watch.fromUpstreamJson(watch.watch);
const { watch } = await getHttpClient().get(`${basePath}/watch/${id}`);
return Watch.fromUpstreamJson(watch);
};
export const getMatchingIndices = async (pattern: string) => {
@ -118,32 +109,32 @@ export const getMatchingIndices = async (pattern: string) => {
if (!pattern.endsWith('*')) {
pattern = `${pattern}*`;
}
const {
data: { indices },
} = await getHttpClient().post(`${basePath}/indices`, { pattern });
const body = JSON.stringify({ pattern });
const { indices } = await getHttpClient().post(`${basePath}/indices`, { body });
return indices;
};
export const fetchFields = async (indexes: string[]) => {
const {
data: { fields },
} = await getHttpClient().post(`${basePath}/fields`, { indexes });
const { fields } = await getHttpClient().post(`${basePath}/fields`, {
body: JSON.stringify({ indexes }),
});
return fields;
};
export const createWatch = async (watch: BaseWatch) => {
const { data } = await getHttpClient().put(`${basePath}/watch/${watch.id}`, watch.upstreamJson);
return data;
return await getHttpClient().put(`${basePath}/watch/${watch.id}`, {
body: JSON.stringify(watch.upstreamJson),
});
};
export const executeWatch = async (executeWatchDetails: ExecutedWatchDetails, watch: BaseWatch) => {
return sendRequest({
path: `${basePath}/watch/execute`,
method: 'put',
body: {
body: JSON.stringify({
executeDetails: executeWatchDetails.upstreamJson,
watch: watch.upstreamJson,
},
}),
});
};
@ -156,19 +147,19 @@ export const loadIndexPatterns = async () => {
return savedObjects;
};
export const getWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => {
export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => {
return useRequest({
path: `${basePath}/watch/visualize`,
method: 'post',
body: {
body: JSON.stringify({
watch: watchModel.upstreamJson,
options: visualizeOptions.upstreamJson,
},
}),
deserializer: ({ visualizeData }: { visualizeData: any }) => visualizeData,
});
};
export const loadSettings = () => {
export const useLoadSettings = () => {
return useRequest({
path: `${basePath}/settings`,
method: 'get',
@ -183,11 +174,8 @@ export const loadSettings = () => {
};
export const ackWatchAction = async (watchId: string, actionId: string) => {
const {
data: { watchStatus },
} = await getHttpClient().put(
`${basePath}/watch/${watchId}/action/${actionId}/acknowledge`,
null
const { watchStatus } = await getHttpClient().put(
`${basePath}/watch/${watchId}/action/${actionId}/acknowledge`
);
return WatchStatus.fromUpstreamJson(watchStatus);
};

View file

@ -5,7 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import { TIME_UNITS } from '../../common/constants';
import { TIME_UNITS } from '../../../../common/constants';
export function getTimeUnitLabel(timeUnit = TIME_UNITS.SECOND, timeValue = '0') {
switch (timeUnit) {

View file

@ -11,6 +11,7 @@ import {
sendRequest as _sendRequest,
useRequest as _useRequest,
} from '../shared_imports';
import { getHttpClient } from './api';
export const sendRequest = (config: SendRequestConfig): Promise<SendRequestResponse> => {

View file

@ -5,7 +5,7 @@
*/
import { get, set } from 'lodash';
import { ACTION_TYPES } from '../../../common/constants';
import { ACTION_TYPES } from '../../../../../common/constants';
import { EmailAction } from './email_action';
import { LoggingAction } from './logging_action';
import { SlackAction } from './slack_action';

View file

@ -7,23 +7,21 @@
import { get, isArray } from 'lodash';
import { BaseAction } from './base_action';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
import { getDefaultEmailTo } from '../../7_x_only';
export class EmailAction extends BaseAction {
constructor(props = {}) {
super(props);
const uiSettings = chrome.getUiSettingsClient();
const defaultToEmail = uiSettings.get('xPack:defaultAdminEmail');
const toArray = get(props, 'to', defaultToEmail);
const toArray = get(props, 'to', getDefaultEmailTo());
this.to = isArray(toArray) ? toArray : toArray && [ toArray ];
this.to = isArray(toArray) ? toArray : toArray && [toArray];
const defaultSubject = i18n.translate('xpack.watcher.models.emailAction.defaultSubjectText', {
defaultMessage: 'Watch [{context}] has exceeded the threshold',
values: {
context: '{{ctx.metadata.name}}',
}
},
});
this.subject = get(props, 'subject', props.ignoreDefaults ? null : defaultSubject);
@ -38,9 +36,12 @@ export class EmailAction extends BaseAction {
if (!this.to || !this.to.length) {
errors.to.push(
i18n.translate('xpack.watcher.watchActions.email.emailRecipientIsRequiredValidationMessage', {
defaultMessage: '"To" email address is required.',
})
i18n.translate(
'xpack.watcher.watchActions.email.emailRecipientIsRequiredValidationMessage',
{
defaultMessage: '"To" email address is required.',
}
)
);
}
@ -60,7 +61,7 @@ export class EmailAction extends BaseAction {
body: {
text: this.body,
},
}
},
});
return result;
@ -71,8 +72,8 @@ export class EmailAction extends BaseAction {
return i18n.translate('xpack.watcher.models.emailAction.simulateMessage', {
defaultMessage: 'Sample email sent to {toList}',
values: {
toList
}
toList,
},
});
}
@ -81,8 +82,8 @@ export class EmailAction extends BaseAction {
return i18n.translate('xpack.watcher.models.emailAction.simulateFailMessage', {
defaultMessage: 'Failed to send email to {toList}.',
values: {
toList
}
toList,
},
});
}
@ -98,6 +99,6 @@ export class EmailAction extends BaseAction {
defaultMessage: 'Send an email from your server.',
});
static simulatePrompt = i18n.translate('xpack.watcher.models.emailAction.simulateButtonLabel', {
defaultMessage: 'Send test email'
defaultMessage: 'Send test email',
});
}

View file

@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
import { getMoment } from '../../../common/lib/get_moment';
import { getMoment } from '../../../../../common/lib/get_moment';
export class ActionStatus {
constructor(props = {}) {

View file

@ -0,0 +1,39 @@
/*
* 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.
*/
declare module 'plugins/watcher/np_ready/application/models/visualize_options' {
export const VisualizeOptions: any;
}
declare module 'plugins/watcher/np_ready/application/models/watch' {
export const Watch: any;
}
declare module 'plugins/watcher/np_ready/application/models/watch/threshold_watch' {
export const ThresholdWatch: any;
}
declare module 'plugins/watcher/np_ready/application/models/watch/json_watch' {
export const JsonWatch: any;
}
declare module 'plugins/watcher/np_ready/application/models/execute_details/execute_details' {
export const ExecuteDetails: any;
}
declare module 'plugins/watcher/np_ready/application/models/watch_history_item' {
export const WatchHistoryItem: any;
}
declare module 'plugins/watcher/np_ready/application/models/watch_status' {
export const WatchStatus: any;
}
declare module 'plugins/watcher/np_ready/application/models/settings' {
export const Settings: any;
}
declare module 'plugins/watcher/np_ready/application/models/action' {
export const Action: any;
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AGG_TYPES } from '../../../common/constants';
import { AGG_TYPES } from '../../../../../common/constants';
export interface AggType {
text: string;

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { COMPARATORS } from '../../../common/constants';
import { COMPARATORS } from '../../../../../common/constants';
export interface Comparator {
text: string;

View file

@ -7,7 +7,7 @@
import uuid from 'uuid';
import { get } from 'lodash';
import { BaseWatch } from './base_watch';
import { ACTION_TYPES, WATCH_TYPES } from '../../../common/constants';
import { ACTION_TYPES, WATCH_TYPES } from '../../../../../common/constants';
import defaultWatchJson from './default_watch.json';
import { i18n } from '@kbn/i18n';

View file

@ -5,7 +5,7 @@
*/
import { BaseWatch } from './base_watch';
import { WATCH_TYPES } from '../../../common/constants';
import { WATCH_TYPES } from '../../../../../common/constants';
/**
* {@code MonitoringWatch} system defined watches created by the Monitoring plugin.

View file

@ -6,7 +6,7 @@
import { BaseWatch } from './base_watch';
import uuid from 'uuid';
import { WATCH_TYPES, SORT_ORDERS, COMPARATORS } from '../../../common/constants';
import { WATCH_TYPES, SORT_ORDERS, COMPARATORS } from '../../../../../common/constants';
import { getTimeUnitLabel } from '../../lib/get_time_unit_label';
import { i18n } from '@kbn/i18n';
import { aggTypes } from './agg_types';

View file

@ -5,7 +5,7 @@
*/
import { get, set } from 'lodash';
import { WATCH_TYPES } from '../../../common/constants';
import { WATCH_TYPES } from '../../../../../common/constants';
import { JsonWatch } from './json_watch';
import { ThresholdWatch } from './threshold_watch';
import { MonitoringWatch } from './monitoring_watch';

View file

@ -6,7 +6,7 @@
import 'moment-duration-format';
import { get } from 'lodash';
import { getMoment } from '../../../common/lib/get_moment';
import { getMoment } from '../../../../../common/lib/get_moment';
import { WatchStatus } from '../watch_status';
export class WatchHistoryItem {

View file

@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
import { getMoment } from '../../../common/lib/get_moment';
import { getMoment } from '../../../../../common/lib/get_moment';
import { ActionStatus } from '../action_status';
export class WatchStatus {

View file

@ -16,10 +16,10 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details';
import { getActionType } from '../../../../../common/lib/get_action_type';
import { BaseWatch, ExecutedWatchDetails } from '../../../../../common/types/watch_types';
import { ACTION_MODES, TIME_UNITS } from '../../../../../common/constants';
import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details';
import { getActionType } from '../../../../../../../common/lib/get_action_type';
import { BaseWatch, ExecutedWatchDetails } from '../../../../../../../common/types/watch_types';
import { ACTION_MODES, TIME_UNITS } from '../../../../../../../common/constants';
import { JsonWatchEditForm } from './json_watch_edit_form';
import { JsonWatchEditSimulate } from './json_watch_edit_simulate';
import { WatchContext } from '../../watch_context';

View file

@ -20,15 +20,20 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { serializeJsonWatch } from '../../../../../common/lib/serialization';
import { ErrableFormRow, SectionError } from '../../../../components';
import { putWatchApiUrl } from '../../../../lib/documentation_links';
import { serializeJsonWatch } from '../../../../../../../common/lib/serialization';
import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components';
import { onWatchSave } from '../../watch_edit_actions';
import { WatchContext } from '../../watch_context';
import { goToWatchList } from '../../../../lib/navigation';
import { RequestFlyout } from '../request_flyout';
import { useAppContext } from '../../../../app_context';
export const JsonWatchEditForm = () => {
const {
links: { putWatchApiUrl },
toasts,
} = useAppContext();
const { watch, setWatchProperty } = useContext(WatchContext);
const { errors } = watch.validate();
@ -37,9 +42,7 @@ export const JsonWatchEditForm = () => {
const [validationError, setValidationError] = useState<string | null>(null);
const [isRequestVisible, setIsRequestVisible] = useState<boolean>(false);
const [serverError, setServerError] = useState<{
data: { nessage: string; error: string };
} | null>(null);
const [serverError, setServerError] = useState<ServerError | null>(null);
const [isSaving, setIsSaving] = useState<boolean>(false);
@ -192,7 +195,7 @@ export const JsonWatchEditForm = () => {
isDisabled={hasErrors}
onClick={async () => {
setIsSaving(true);
const savedWatch = await onWatchSave(watch);
const savedWatch = await onWatchSave(watch, toasts);
if (savedWatch && savedWatch.error) {
const { data } = savedWatch.error;
setIsSaving(false);

View file

@ -24,19 +24,19 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details';
import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item';
import { ACTION_MODES, TIME_UNITS } from '../../../../../common/constants';
import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details';
import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item';
import { ACTION_MODES, TIME_UNITS } from '../../../../../../../common/constants';
import {
ExecutedWatchDetails,
ExecutedWatchResults,
} from '../../../../../common/types/watch_types';
} from '../../../../../../../common/types/watch_types';
import { ErrableFormRow } from '../../../../components/form_errors';
import { executeWatch } from '../../../../lib/api';
import { executeWatchApiUrl } from '../../../../lib/documentation_links';
import { WatchContext } from '../../watch_context';
import { JsonWatchEditSimulateResults } from './json_watch_edit_simulate_results';
import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label';
import { useAppContext } from '../../../../app_context';
const actionModeOptions = Object.keys(ACTION_MODES).map(mode => ({
text: ACTION_MODES[mode],
@ -70,6 +70,9 @@ export const JsonWatchEditSimulate = ({
type: string;
}>;
}) => {
const {
links: { executeWatchApiUrl },
} = useAppContext();
const { watch } = useContext(WatchContext);
// hooks

View file

@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import {
ExecutedWatchDetails,
ExecutedWatchResults,
} from '../../../../../common/types/watch_types';
} from '../../../../../../../common/types/watch_types';
import { getTypeFromAction } from '../../watch_edit_actions';
import { WatchContext } from '../../watch_context';
import { WatchStatus, SectionError } from '../../../../components';

Some files were not shown because too many files have changed in this diff Show more