mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
parent
e87bbe0307
commit
41ac633007
451 changed files with 10869 additions and 10886 deletions
|
@ -24,6 +24,7 @@ export function createJestConfig({
|
|||
'^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`,
|
||||
'uiExports/(.*)': `${kibanaDirectory}/src/dev/jest/mocks/file_mocks.js`,
|
||||
'^src/core/(.*)': `${kibanaDirectory}/src/core/$1`,
|
||||
'^plugins/watcher/models/(.*)': `${xPackKibanaDirectory}/legacy/plugins/watcher/public/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)$':
|
||||
|
|
|
@ -4,32 +4,19 @@ This plugins adopts some conventions in addition to or in place of conventions i
|
|||
|
||||
## Folder structure
|
||||
```
|
||||
common/
|
||||
constants/ // constants used across client and server
|
||||
lib/ // helpers used across client and server
|
||||
types/ // TS definitions
|
||||
public/
|
||||
directives/ (This is the same as *A, but is for directives that are used cross-view)
|
||||
services/
|
||||
watch/
|
||||
index.js (no code here; only `export from watch.js`)
|
||||
watch.js
|
||||
notifications/
|
||||
index.js (no code here; only `export from notifications.js`)
|
||||
notifications.js
|
||||
...
|
||||
views/
|
||||
edit/
|
||||
...
|
||||
list/
|
||||
directives/ (*A)
|
||||
my_directive_name/
|
||||
directives/ (Subcomponents of my_directive_name are defined here, and this follows the same structure as *A)
|
||||
index.js (no code here; only `export from my_directive_name.js`)
|
||||
my_directive_name.js
|
||||
my_directive_name.html
|
||||
index.js (imports the directives in this folder, i.e.,my_directive_name)
|
||||
routes/
|
||||
index.js (no code here; only imports routes.js)
|
||||
routes.js
|
||||
index.js
|
||||
|
||||
components/ // common React components
|
||||
constants/ // constants used on the client
|
||||
lib/ // helpers used on the client
|
||||
models/ // client models
|
||||
sections/ // Sections of the app with corresponding React components
|
||||
watch_edit
|
||||
watch_list
|
||||
watch_status
|
||||
server/
|
||||
lib/
|
||||
screenshots/
|
||||
|
@ -44,90 +31,6 @@ server/
|
|||
say_hello.js
|
||||
```
|
||||
|
||||
## Data Services
|
||||
|
||||
api calls:
|
||||
- GET /watch/{id}
|
||||
- PUT /watch/{id}
|
||||
|
||||
using the service
|
||||
|
||||
```js
|
||||
import watch from './services/watch'
|
||||
|
||||
watch.get(...)
|
||||
watch.put(...)
|
||||
```
|
||||
|
||||
## Services / Lib
|
||||
- Shared code that requires state should be made into a service. For example, see `pageService`.
|
||||
- Shared code that doesn't require state (e.g. a simple helper function) should be made a lib.
|
||||
For example, see `clamp`.
|
||||
|
||||
## Controller classes
|
||||
- All functions in controller classes should be defined as arrow function constants. This is to ensure the `this` context is consistent, regardless of where it is being called.
|
||||
|
||||
GOOD
|
||||
```
|
||||
controller: class WatchListController {
|
||||
onQueryChanged = (query) => {...};
|
||||
}
|
||||
```
|
||||
|
||||
BAD
|
||||
```
|
||||
controller: class WatchListController {
|
||||
onQueryChanged(query) {...};
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
controller: class WatchListController {
|
||||
constructor() {
|
||||
this.onQueryChanged = (query) => {...};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Constructors should be used to initialize state and define $scope.$watch(es)
|
||||
|
||||
GOOD
|
||||
```
|
||||
controllerAs: 'watchList',
|
||||
bindToController: true,
|
||||
scope: { foo: '=' },
|
||||
controller: class WatchListController {
|
||||
constructor() {
|
||||
this.foo = this.foo || 'default';
|
||||
|
||||
$scope.$watch('watchList.foo', () => {
|
||||
console.log('foo changed, fool');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
## Event handlers
|
||||
|
||||
Event handler functions should be named with the following pattern:
|
||||
|
||||
> `on<Verb>`, in present tense
|
||||
|
||||
In case there is ambiguity about _what_ the verb is acting upon a noun should be included like so:
|
||||
|
||||
> `on<Noun><Verb>`, in present tense
|
||||
|
||||
GOOD
|
||||
```
|
||||
onDelete
|
||||
onWatchDelete
|
||||
```
|
||||
|
||||
BAD
|
||||
```
|
||||
onDeleted
|
||||
onWatchDeleted
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
We have a layered architecture in the Watcher UI codebase, with each layer performing a specific function to the data as it flows through it.
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getWatch } from '../../../test/fixtures';
|
||||
|
||||
export const WATCH_ID = 'my-test-watch';
|
||||
|
||||
export const WATCH = { watch: getWatch({ id: WATCH_ID }) };
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 sinon, { SinonFakeServer } from 'sinon';
|
||||
import { ROUTES } from '../../../common/constants';
|
||||
|
||||
const { API_ROOT } = ROUTES;
|
||||
|
||||
type HttpResponse = Record<string, any> | any[];
|
||||
|
||||
const mockResponse = (defaultResponse: HttpResponse, response: HttpResponse) => [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ ...defaultResponse, ...response }),
|
||||
];
|
||||
|
||||
// Register helpers to mock HTTP Requests
|
||||
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
||||
const setLoadWatchesResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watches: [] };
|
||||
|
||||
server.respondWith('GET', `${API_ROOT}/watches`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setLoadWatchResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watch: {} };
|
||||
server.respondWith('GET', `${API_ROOT}/watch/:id`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setLoadWatchHistoryResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watchHistoryItems: [] };
|
||||
server.respondWith(
|
||||
'GET',
|
||||
`${API_ROOT}/watch/:id/history?startTime=*`,
|
||||
mockResponse(defaultResponse, response)
|
||||
);
|
||||
};
|
||||
|
||||
const setLoadWatchHistoryItemResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watchHistoryItem: {} };
|
||||
server.respondWith('GET', `${API_ROOT}/history/:id`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setDeleteWatchResponse = (response?: HttpResponse, error?: any) => {
|
||||
const status = error ? error.status || 400 : 200;
|
||||
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
|
||||
|
||||
server.respondWith('POST', `${API_ROOT}/watches/delete`, [
|
||||
status,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
body,
|
||||
]);
|
||||
};
|
||||
|
||||
const setSaveWatchResponse = (id: string, response?: HttpResponse, error?: any) => {
|
||||
const status = error ? error.status || 400 : 200;
|
||||
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
|
||||
|
||||
server.respondWith('PUT', `${API_ROOT}/watch/${id}`, [
|
||||
status,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
body,
|
||||
]);
|
||||
};
|
||||
|
||||
const setLoadExecutionResultResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watchHistoryItem: {} };
|
||||
server.respondWith('PUT', `${API_ROOT}/watch/execute`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setLoadMatchingIndicesResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { indices: [] };
|
||||
server.respondWith('POST', `${API_ROOT}/indices`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setLoadEsFieldsResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { fields: [] };
|
||||
server.respondWith('POST', `${API_ROOT}/fields`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setLoadSettingsResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { action_types: {} };
|
||||
server.respondWith('GET', `${API_ROOT}/settings`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setLoadWatchVisualizeResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { visualizeData: {} };
|
||||
server.respondWith(
|
||||
'POST',
|
||||
`${API_ROOT}/watch/visualize`,
|
||||
mockResponse(defaultResponse, response)
|
||||
);
|
||||
};
|
||||
|
||||
const setDeactivateWatchResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watchStatus: {} };
|
||||
server.respondWith(
|
||||
'PUT',
|
||||
`${API_ROOT}/watch/:id/deactivate`,
|
||||
mockResponse(defaultResponse, response)
|
||||
);
|
||||
};
|
||||
|
||||
const setActivateWatchResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watchStatus: {} };
|
||||
server.respondWith(
|
||||
'PUT',
|
||||
`${API_ROOT}/watch/:id/activate`,
|
||||
mockResponse(defaultResponse, response)
|
||||
);
|
||||
};
|
||||
|
||||
const setAcknowledgeWatchResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { watchStatus: {} };
|
||||
server.respondWith(
|
||||
'PUT',
|
||||
`${API_ROOT}/watch/:id/action/:actionId/acknowledge`,
|
||||
mockResponse(defaultResponse, response)
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
setLoadWatchesResponse,
|
||||
setLoadWatchResponse,
|
||||
setLoadWatchHistoryResponse,
|
||||
setLoadWatchHistoryItemResponse,
|
||||
setDeleteWatchResponse,
|
||||
setSaveWatchResponse,
|
||||
setLoadExecutionResultResponse,
|
||||
setLoadMatchingIndicesResponse,
|
||||
setLoadEsFieldsResponse,
|
||||
setLoadSettingsResponse,
|
||||
setLoadWatchVisualizeResponse,
|
||||
setDeactivateWatchResponse,
|
||||
setActivateWatchResponse,
|
||||
setAcknowledgeWatchResponse,
|
||||
};
|
||||
};
|
||||
|
||||
export const init = () => {
|
||||
const server = sinon.fakeServer.create();
|
||||
server.respondImmediately = true;
|
||||
|
||||
// Define default response for unhandled requests.
|
||||
// We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
|
||||
// and we can mock them all with a 200 instead of mocking each one individually.
|
||||
server.respondWith([200, {}, 'DefaultResponse']);
|
||||
|
||||
const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
|
||||
|
||||
return {
|
||||
server,
|
||||
httpRequestsMockHelpers,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { setup as watchListSetup } from './watch_list.helpers';
|
||||
import { setup as watchStatusSetup } from './watch_status.helpers';
|
||||
import { setup as watchCreateJsonSetup } from './watch_create_json.helpers';
|
||||
import { setup as watchCreateThresholdSetup } from './watch_create_threshold.helpers';
|
||||
import { setup as watchEditSetup } from './watch_edit.helpers';
|
||||
|
||||
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils';
|
||||
|
||||
export { setupEnvironment } from './setup_environment';
|
||||
|
||||
export const pageHelpers = {
|
||||
watchList: { setup: watchListSetup },
|
||||
watchStatus: { setup: watchStatusSetup },
|
||||
watchCreateJson: { setup: watchCreateJsonSetup },
|
||||
watchCreateThreshold: { setup: watchCreateThresholdSetup },
|
||||
watchEdit: { setup: watchEditSetup },
|
||||
};
|
|
@ -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 axios from 'axios';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
import { init as initHttpRequests } from './http_requests';
|
||||
import { setHttpClient, setSavedObjectsClient } from '../../../public/lib/api';
|
||||
|
||||
const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
|
||||
|
||||
const mockSavedObjectsClient = () => {
|
||||
return {
|
||||
find: (_params?: any) => {},
|
||||
};
|
||||
};
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
||||
|
||||
// @ts-ignore
|
||||
setHttpClient(mockHttpClient);
|
||||
|
||||
setSavedObjectsClient(mockSavedObjectsClient());
|
||||
|
||||
return {
|
||||
server,
|
||||
httpRequestsMockHelpers,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils';
|
||||
import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit';
|
||||
import { ROUTES, WATCH_TYPES } from '../../../common/constants';
|
||||
import { registerRouter } from '../../../public/lib/navigation';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
onRouter: router => registerRouter(router),
|
||||
initialEntries: [`${ROUTES.API_ROOT}/watches/new-watch/${WATCH_TYPES.JSON}`],
|
||||
componentRoutePath: `${ROUTES.API_ROOT}/watches/new-watch/:type`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WatchEdit, testBedConfig);
|
||||
|
||||
export interface WatchCreateJsonTestBed extends TestBed<WatchCreateJsonTestSubjects> {
|
||||
actions: {
|
||||
selectTab: (tab: 'edit' | 'simulate') => void;
|
||||
clickSubmitButton: () => void;
|
||||
clickSimulateButton: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<WatchCreateJsonTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const selectTab = (tab: 'edit' | 'simulate') => {
|
||||
const tabs = ['edit', 'simulate'];
|
||||
|
||||
testBed
|
||||
.find('tab')
|
||||
.at(tabs.indexOf(tab))
|
||||
.simulate('click');
|
||||
};
|
||||
|
||||
const clickSubmitButton = () => {
|
||||
testBed.find('saveWatchButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickSimulateButton = () => {
|
||||
testBed.find('simulateWatchButton').simulate('click');
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
selectTab,
|
||||
clickSubmitButton,
|
||||
clickSimulateButton,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WatchCreateJsonTestSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'actionModesSelect'
|
||||
| 'idInput'
|
||||
| 'ignoreConditionSwitch'
|
||||
| 'jsonEditor'
|
||||
| 'jsonWatchForm'
|
||||
| 'jsonWatchSimulateForm'
|
||||
| 'nameInput'
|
||||
| 'pageTitle'
|
||||
| 'saveWatchButton'
|
||||
| 'scheduledTimeInput'
|
||||
| 'sectionError'
|
||||
| 'sectionLoading'
|
||||
| 'simulateResultsFlyout'
|
||||
| 'simulateResultsFlyoutTitle'
|
||||
| 'simulateWatchButton'
|
||||
| 'tab'
|
||||
| 'triggeredTimeInput';
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils';
|
||||
import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit';
|
||||
import { ROUTES, WATCH_TYPES } from '../../../common/constants';
|
||||
import { registerRouter } from '../../../public/lib/navigation';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
onRouter: router => registerRouter(router),
|
||||
initialEntries: [`${ROUTES.API_ROOT}/watches/new-watch/${WATCH_TYPES.THRESHOLD}`],
|
||||
componentRoutePath: `${ROUTES.API_ROOT}/watches/new-watch/:type`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WatchEdit, testBedConfig);
|
||||
|
||||
export interface WatchCreateThresholdTestBed extends TestBed<WatchCreateThresholdTestSubjects> {
|
||||
actions: {
|
||||
clickSubmitButton: () => void;
|
||||
clickAddActionButton: () => void;
|
||||
clickActionLink: (
|
||||
actionType: 'logging' | 'email' | 'webhook' | 'index' | 'slack' | 'jira' | 'pagerduty'
|
||||
) => void;
|
||||
clickSimulateButton: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<WatchCreateThresholdTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const clickSubmitButton = () => {
|
||||
testBed.find('saveWatchButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickAddActionButton = () => {
|
||||
testBed.find('addWatchActionButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickSimulateButton = () => {
|
||||
testBed.find('simulateActionButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickActionLink = (
|
||||
actionType: 'logging' | 'email' | 'webhook' | 'index' | 'slack' | 'jira' | 'pagerduty'
|
||||
) => {
|
||||
testBed.find(`${actionType}ActionButton`).simulate('click');
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
clickSubmitButton,
|
||||
clickAddActionButton,
|
||||
clickActionLink,
|
||||
clickSimulateButton,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WatchCreateThresholdTestSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'addWatchActionButton'
|
||||
| 'emailBodyInput'
|
||||
| 'emailSubjectInput'
|
||||
| 'indexInput'
|
||||
| 'indicesComboBox'
|
||||
| 'jiraIssueTypeInput'
|
||||
| 'jiraProjectKeyInput'
|
||||
| 'jiraSummaryInput'
|
||||
| 'loggingTextInput'
|
||||
| 'mockComboBox'
|
||||
| 'nameInput'
|
||||
| 'pagerdutyDescriptionInput'
|
||||
| 'pageTitle'
|
||||
| 'saveWatchButton'
|
||||
| 'sectionLoading'
|
||||
| 'simulateActionButton'
|
||||
| 'slackMessageTextarea'
|
||||
| 'slackRecipientComboBox'
|
||||
| 'toEmailAddressInput'
|
||||
| 'triggerIntervalSizeInput'
|
||||
| 'watchActionAccordion'
|
||||
| 'watchActionAccordion.mockComboBox'
|
||||
| 'watchActionsPanel'
|
||||
| 'watchConditionTitle'
|
||||
| 'watchTimeFieldSelect'
|
||||
| 'watchVisualizationChart'
|
||||
| 'webhookBodyEditor'
|
||||
| 'webhookHostInput'
|
||||
| 'webhookPasswordInput'
|
||||
| 'webhookPathInput'
|
||||
| 'webhookPortInput'
|
||||
| 'webhookMethodSelect'
|
||||
| 'webhookUsernameInput';
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils';
|
||||
import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit';
|
||||
import { ROUTES } from '../../../common/constants';
|
||||
import { registerRouter } from '../../../public/lib/navigation';
|
||||
import { WATCH_ID } from './constants';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
onRouter: router => registerRouter(router),
|
||||
initialEntries: [`${ROUTES.API_ROOT}/watches/watch/${WATCH_ID}/edit`],
|
||||
componentRoutePath: `${ROUTES.API_ROOT}/watches/watch/:id/edit`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WatchEdit, testBedConfig);
|
||||
|
||||
export interface WatchEditTestBed extends TestBed<WatchEditSubjects> {
|
||||
actions: {
|
||||
clickSubmitButton: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<WatchEditTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const clickSubmitButton = () => {
|
||||
testBed.find('saveWatchButton').simulate('click');
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
clickSubmitButton,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WatchEditSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'idInput'
|
||||
| 'jsonWatchForm'
|
||||
| 'nameInput'
|
||||
| 'pageTitle'
|
||||
| 'thresholdWatchForm'
|
||||
| 'watchTimeFieldSelect';
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
registerTestBed,
|
||||
findTestSubject,
|
||||
TestBed,
|
||||
TestBedConfig,
|
||||
nextTick,
|
||||
} from '../../../../../../test_utils';
|
||||
import { WatchList } from '../../../public/sections/watch_list/components/watch_list';
|
||||
import { ROUTES } from '../../../common/constants';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${ROUTES.API_ROOT}/watches`],
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WatchList, testBedConfig);
|
||||
|
||||
export interface WatchListTestBed extends TestBed<WatchListTestSubjects> {
|
||||
actions: {
|
||||
selectWatchAt: (index: number) => void;
|
||||
clickWatchAt: (index: number) => void;
|
||||
clickWatchActionAt: (index: number, action: 'delete' | 'edit') => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<WatchListTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const selectWatchAt = (index: number) => {
|
||||
const { rows } = testBed.table.getMetaData('watchesTable');
|
||||
const row = rows[index];
|
||||
const checkBox = row.reactWrapper.find('input').hostNodes();
|
||||
checkBox.simulate('change', { target: { checked: true } });
|
||||
};
|
||||
|
||||
const clickWatchAt = async (index: number) => {
|
||||
const { rows } = testBed.table.getMetaData('watchesTable');
|
||||
const watchesLink = findTestSubject(rows[index].reactWrapper, 'watchesLink');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { href } = watchesLink.props();
|
||||
testBed.router.navigateTo(href!);
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const clickWatchActionAt = async (index: number, action: 'delete' | 'edit') => {
|
||||
const { component, table } = testBed;
|
||||
const { rows } = table.getMetaData('watchesTable');
|
||||
const currentRow = rows[index];
|
||||
const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper;
|
||||
const button = findTestSubject(lastColumn, `${action}WatchButton`);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
selectWatchAt,
|
||||
clickWatchAt,
|
||||
clickWatchActionAt,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WatchListTestSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'appTitle'
|
||||
| 'documentationLink'
|
||||
| 'watchesTable'
|
||||
| 'cell'
|
||||
| 'row'
|
||||
| 'deleteWatchButton'
|
||||
| 'createWatchButton'
|
||||
| 'emptyPrompt'
|
||||
| 'emptyPrompt.createWatchButton'
|
||||
| 'editWatchButton';
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
registerTestBed,
|
||||
findTestSubject,
|
||||
TestBed,
|
||||
TestBedConfig,
|
||||
nextTick,
|
||||
} from '../../../../../../test_utils';
|
||||
import { WatchStatus } from '../../../public/sections/watch_status/components/watch_status';
|
||||
import { ROUTES } from '../../../common/constants';
|
||||
import { WATCH_ID } from './constants';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${ROUTES.API_ROOT}/watches/watch/${WATCH_ID}/status`],
|
||||
componentRoutePath: `${ROUTES.API_ROOT}/watches/watch/:id/status`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WatchStatus, testBedConfig);
|
||||
|
||||
export interface WatchStatusTestBed extends TestBed<WatchStatusTestSubjects> {
|
||||
actions: {
|
||||
selectTab: (tab: 'execution history' | 'action statuses') => void;
|
||||
clickToggleActivationButton: () => void;
|
||||
clickAcknowledgeButton: (index: number) => void;
|
||||
clickDeleteWatchButton: () => void;
|
||||
clickWatchExecutionAt: (index: number, tableCellText: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<WatchStatusTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
|
||||
const selectTab = (tab: 'execution history' | 'action statuses') => {
|
||||
const tabs = ['execution history', 'action statuses'];
|
||||
|
||||
testBed
|
||||
.find('tab')
|
||||
.at(tabs.indexOf(tab))
|
||||
.simulate('click');
|
||||
};
|
||||
|
||||
const clickToggleActivationButton = async () => {
|
||||
const { component } = testBed;
|
||||
const button = testBed.find('toggleWatchActivationButton');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const clickAcknowledgeButton = async (index: number) => {
|
||||
const { component, table } = testBed;
|
||||
const { rows } = table.getMetaData('watchActionStatusTable');
|
||||
const currentRow = rows[index];
|
||||
const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper;
|
||||
const button = findTestSubject(lastColumn, 'acknowledgeWatchButton');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const clickDeleteWatchButton = async () => {
|
||||
const { component } = testBed;
|
||||
const button = testBed.find('deleteWatchButton');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const clickWatchExecutionAt = async (index: number, tableCellText: string) => {
|
||||
const { component, table } = testBed;
|
||||
const { rows } = table.getMetaData('watchHistoryTable');
|
||||
const currentRow = rows[index];
|
||||
const firstColumn = currentRow.columns[0].reactWrapper;
|
||||
|
||||
const button = findTestSubject(firstColumn, `watchStartTimeColumn-${tableCellText}`);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
await nextTick(100);
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
selectTab,
|
||||
clickToggleActivationButton,
|
||||
clickAcknowledgeButton,
|
||||
clickDeleteWatchButton,
|
||||
clickWatchExecutionAt,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WatchStatusTestSubjects = TestSubjects;
|
||||
|
||||
export type TestSubjects =
|
||||
| 'acknowledgeWatchButton'
|
||||
| 'actionErrorsButton'
|
||||
| 'actionErrorsFlyout'
|
||||
| 'actionErrorsFlyout.errorMessage'
|
||||
| 'actionErrorsFlyout.title'
|
||||
| 'deleteWatchButton'
|
||||
| 'pageTitle'
|
||||
| 'tab'
|
||||
| 'toggleWatchActivationButton'
|
||||
| 'watchActionStatusTable'
|
||||
| 'watchActionsTable'
|
||||
| 'watchDetailSection'
|
||||
| 'watchHistoryDetailFlyout'
|
||||
| 'watchHistoryDetailFlyout.title'
|
||||
| 'watchHistorySection'
|
||||
| 'watchHistoryErrorDetailFlyout'
|
||||
| 'watchHistoryErrorDetailFlyout.errorMessage'
|
||||
| 'watchHistoryErrorDetailFlyout.title'
|
||||
| 'watchHistoryTable';
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import { setupEnvironment, pageHelpers, nextTick } 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 { 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', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: WatchCreateJsonTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the correct page title', () => {
|
||||
const { find } = testBed;
|
||||
expect(find('pageTitle').text()).toBe('Create advanced watch');
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
test('should have 2 tabs', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('tab').length).toBe(2);
|
||||
expect(find('tab').map(t => t.text())).toEqual(['Edit', 'Simulate']);
|
||||
});
|
||||
|
||||
test('should navigate to the "Simulate" tab', () => {
|
||||
const { exists, actions } = testBed;
|
||||
|
||||
expect(exists('jsonWatchForm')).toBe(true);
|
||||
expect(exists('jsonWatchSimulateForm')).toBe(false);
|
||||
|
||||
actions.selectTab('simulate');
|
||||
|
||||
expect(exists('jsonWatchForm')).toBe(false);
|
||||
expect(exists('jsonWatchSimulateForm')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
describe('form validation', () => {
|
||||
test('should not allow empty ID field', () => {
|
||||
const { form, actions } = testBed;
|
||||
form.setInputValue('idInput', '');
|
||||
|
||||
actions.clickSubmitButton();
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('ID is required');
|
||||
});
|
||||
test('should not allow invalid characters for ID field', () => {
|
||||
const { form, actions } = testBed;
|
||||
form.setInputValue('idInput', 'invalid$id*field/');
|
||||
|
||||
actions.clickSubmitButton();
|
||||
|
||||
expect(form.getErrorsMessages()).toContain(
|
||||
'ID can only contain letters, underscores, dashes, and numbers.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form payload & API errors', () => {
|
||||
test('should send the correct payload', async () => {
|
||||
const { form, actions } = testBed;
|
||||
const { watch } = WATCH;
|
||||
|
||||
form.setInputValue('nameInput', watch.name);
|
||||
form.setInputValue('idInput', watch.id);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const DEFAULT_LOGGING_ACTION_ID = 'logging_1';
|
||||
const DEFAULT_LOGGING_ACTION_TYPE = 'logging';
|
||||
const DEFAULT_LOGGING_ACTION_TEXT =
|
||||
'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.';
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
id: watch.id,
|
||||
name: watch.name,
|
||||
type: watch.type,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: DEFAULT_LOGGING_ACTION_ID,
|
||||
type: DEFAULT_LOGGING_ACTION_TYPE,
|
||||
text: DEFAULT_LOGGING_ACTION_TEXT,
|
||||
[DEFAULT_LOGGING_ACTION_TYPE]: {
|
||||
text: DEFAULT_LOGGING_ACTION_TEXT,
|
||||
},
|
||||
},
|
||||
],
|
||||
watch: defaultWatchJson,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should surface the API errors from the "save" HTTP request', async () => {
|
||||
const { form, actions, component, exists, find } = testBed;
|
||||
const { watch } = WATCH;
|
||||
|
||||
form.setInputValue('nameInput', watch.name);
|
||||
form.setInputValue('idInput', watch.id);
|
||||
|
||||
const error = {
|
||||
status: 400,
|
||||
error: 'Bad request',
|
||||
message: 'Watch payload is invalid',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setSaveWatchResponse(watch.id, undefined, { body: error });
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('sectionError')).toBe(true);
|
||||
expect(find('sectionError').text()).toContain(error.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simulate', () => {
|
||||
beforeEach(() => {
|
||||
const { actions, form } = testBed;
|
||||
|
||||
// Set watch id (required field) and switch to simulate tab
|
||||
form.setInputValue('idInput', WATCH.watch.id);
|
||||
actions.selectTab('simulate');
|
||||
});
|
||||
|
||||
describe('form payload & API errors', () => {
|
||||
test('should execute a watch with no input', async () => {
|
||||
const { actions } = testBed;
|
||||
const {
|
||||
watch: { id, type },
|
||||
} = WATCH;
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const actionModes = Object.keys(defaultWatchJson.actions).reduce(
|
||||
(actionAccum: any, action) => {
|
||||
actionAccum[action] = 'simulate';
|
||||
return actionAccum;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const executedWatch = {
|
||||
id,
|
||||
type,
|
||||
isNew: true,
|
||||
actions: [],
|
||||
watch: defaultWatchJson,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes,
|
||||
}),
|
||||
watch: executedWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should execute a watch with a valid payload', async () => {
|
||||
const { actions, form, find, exists, component } = testBed;
|
||||
const {
|
||||
watch: { id, type },
|
||||
} = WATCH;
|
||||
|
||||
const SCHEDULED_TIME = '5';
|
||||
const TRIGGERED_TIME = '5';
|
||||
const IGNORE_CONDITION = true;
|
||||
const ACTION_MODE = 'force_execute';
|
||||
|
||||
form.setInputValue('scheduledTimeInput', SCHEDULED_TIME);
|
||||
form.setInputValue('triggeredTimeInput', TRIGGERED_TIME);
|
||||
form.toggleEuiSwitch('ignoreConditionSwitch');
|
||||
form.setInputValue('actionModesSelect', ACTION_MODE);
|
||||
|
||||
expect(exists('simulateResultsFlyout')).toBe(false);
|
||||
|
||||
httpRequestsMockHelpers.setLoadExecutionResultResponse({
|
||||
watchHistoryItem: {
|
||||
details: {},
|
||||
watchStatus: {
|
||||
actionStatuses: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const actionModes = Object.keys(defaultWatchJson.actions).reduce(
|
||||
(actionAccum: any, action) => {
|
||||
actionAccum[action] = ACTION_MODE;
|
||||
return actionAccum;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const executedWatch = {
|
||||
id,
|
||||
type,
|
||||
isNew: true,
|
||||
actions: [],
|
||||
watch: defaultWatchJson,
|
||||
};
|
||||
|
||||
const triggeredTime = `now+${TRIGGERED_TIME}s`;
|
||||
const scheduledTime = `now+${SCHEDULED_TIME}s`;
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
triggerData: {
|
||||
triggeredTime,
|
||||
scheduledTime,
|
||||
},
|
||||
ignoreCondition: IGNORE_CONDITION,
|
||||
actionModes,
|
||||
}),
|
||||
watch: executedWatch,
|
||||
})
|
||||
);
|
||||
expect(exists('simulateResultsFlyout')).toBe(true);
|
||||
expect(find('simulateResultsFlyoutTitle').text()).toEqual('Simulation results');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,786 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
import axios from 'axios';
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers';
|
||||
import { getExecuteDetails } from '../../test/fixtures';
|
||||
import { WATCH_TYPES } from '../../common/constants';
|
||||
|
||||
const WATCH_NAME = 'my_test_watch';
|
||||
|
||||
const WATCH_TIME_FIELD = '@timestamp';
|
||||
|
||||
const MATCH_INDICES = ['index1'];
|
||||
|
||||
const ES_FIELDS = [{ name: '@timestamp', type: 'date' }];
|
||||
|
||||
const SETTINGS = {
|
||||
action_types: {
|
||||
email: { enabled: true },
|
||||
index: { enabled: true },
|
||||
jira: { enabled: true },
|
||||
logging: { enabled: true },
|
||||
pagerduty: { enabled: true },
|
||||
slack: { enabled: true },
|
||||
webhook: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
const WATCH_VISUALIZE_DATA = {
|
||||
count: [[1559404800000, 14], [1559448000000, 196], [1559491200000, 44]],
|
||||
};
|
||||
|
||||
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'),
|
||||
loadIndexPatterns: async () => {
|
||||
const INDEX_PATTERNS = [
|
||||
{ attributes: { title: 'index1' } },
|
||||
{ attributes: { title: 'index2' } },
|
||||
{ attributes: { title: 'index3' } },
|
||||
];
|
||||
return await INDEX_PATTERNS;
|
||||
},
|
||||
getHttpClient: () => mockHttpClient,
|
||||
}));
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
// Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
|
||||
// which does not produce a valid component wrapper
|
||||
EuiComboBox: (props: any) => (
|
||||
<input
|
||||
data-test-subj="mockComboBox"
|
||||
onChange={async (syntheticEvent: any) => {
|
||||
props.onChange([syntheticEvent['0']]);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
const { setup } = pageHelpers.watchCreateThreshold;
|
||||
|
||||
describe.skip('<ThresholdWatchEdit /> create route', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: WatchCreateThresholdTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the correct page title', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('pageTitle').text()).toBe('Create threshold alert');
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadMatchingIndicesResponse({ indices: MATCH_INDICES });
|
||||
httpRequestsMockHelpers.setLoadEsFieldsResponse({ fields: ES_FIELDS });
|
||||
httpRequestsMockHelpers.setLoadSettingsResponse(SETTINGS);
|
||||
httpRequestsMockHelpers.setLoadWatchVisualizeResponse(WATCH_VISUALIZE_DATA);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
describe('form validation', () => {
|
||||
test('should not allow empty name field', () => {
|
||||
const { form } = testBed;
|
||||
|
||||
form.setInputValue('nameInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Name is required.');
|
||||
});
|
||||
|
||||
test('should not allow empty time field', () => {
|
||||
const { form } = testBed;
|
||||
|
||||
form.setInputValue('watchTimeFieldSelect', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('A time field is required.');
|
||||
});
|
||||
|
||||
test('should not allow empty interval size field', () => {
|
||||
const { form } = testBed;
|
||||
|
||||
form.setInputValue('triggerIntervalSizeInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Interval size is required.');
|
||||
});
|
||||
|
||||
test('should not allow negative interval size field', () => {
|
||||
const { form } = testBed;
|
||||
|
||||
form.setInputValue('triggerIntervalSizeInput', '-1');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Interval size cannot be a negative number.');
|
||||
});
|
||||
|
||||
test('should disable the Create button with invalid fields', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('saveWatchButton').props().disabled).toEqual(true);
|
||||
});
|
||||
|
||||
test('it should enable the Create button and render additonal content with valid fields', async () => {
|
||||
const { form, find, component, exists } = testBed;
|
||||
|
||||
form.setInputValue('nameInput', 'my_test_watch');
|
||||
find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox
|
||||
form.setInputValue('watchTimeFieldSelect', '@timestamp');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(find('saveWatchButton').props().disabled).toEqual(false);
|
||||
|
||||
expect(find('watchConditionTitle').text()).toBe('Match the following condition');
|
||||
expect(exists('watchVisualizationChart')).toBe(true);
|
||||
expect(exists('watchActionsPanel')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
beforeEach(async () => {
|
||||
const { form, find, component } = testBed;
|
||||
|
||||
// Set up valid fields needed for actions component to render
|
||||
form.setInputValue('nameInput', WATCH_NAME);
|
||||
find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox
|
||||
form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should simulate a logging action', async () => {
|
||||
const { form, find, actions, exists } = testBed;
|
||||
|
||||
const LOGGING_MESSAGE = 'test log message';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('logging');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
// First, provide invalid field and verify
|
||||
form.setInputValue('loggingTextInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Log text is required.');
|
||||
expect(find('simulateActionButton').props().disabled).toEqual(true);
|
||||
|
||||
// Next, provide valid field and verify
|
||||
form.setInputValue('loggingTextInput', LOGGING_MESSAGE);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'logging_1',
|
||||
type: 'logging',
|
||||
text: LOGGING_MESSAGE,
|
||||
logging: {
|
||||
text: LOGGING_MESSAGE,
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
logging_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should simulate an index action', async () => {
|
||||
const { form, find, actions, exists } = testBed;
|
||||
|
||||
const INDEX = 'my_index';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('index');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
// First, provide invalid field and verify
|
||||
form.setInputValue('indexInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Index name is required.');
|
||||
expect(find('simulateActionButton').props().disabled).toEqual(true);
|
||||
|
||||
// Next, provide valid field and verify
|
||||
form.setInputValue('indexInput', INDEX);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'index_1',
|
||||
type: 'index',
|
||||
index: {
|
||||
index: INDEX,
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
index_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should simulate a Slack action', async () => {
|
||||
const { form, actions, exists } = testBed;
|
||||
|
||||
const SLACK_MESSAGE = 'test slack message';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('slack');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
form.setInputValue('slackMessageTextarea', SLACK_MESSAGE);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'slack_1',
|
||||
type: 'slack',
|
||||
text: SLACK_MESSAGE,
|
||||
slack: {
|
||||
message: {
|
||||
text: SLACK_MESSAGE,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
slack_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should simulate an email action', async () => {
|
||||
const { form, find, actions, exists } = testBed;
|
||||
|
||||
const EMAIL_RECIPIENT = 'test@test.com';
|
||||
const EMAIL_SUBJECT = 'test email subject';
|
||||
const EMAIL_BODY = 'this is a test email body';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('email');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
// First, provide invalid fields and verify
|
||||
form.setInputValue('emailBodyInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Email body is required.');
|
||||
expect(find('simulateActionButton').props().disabled).toEqual(true);
|
||||
|
||||
// Next, provide valid field and verify
|
||||
find('watchActionAccordion.mockComboBox').simulate('change', [
|
||||
{ label: EMAIL_RECIPIENT, value: EMAIL_RECIPIENT },
|
||||
]); // Using mocked EuiComboBox
|
||||
form.setInputValue('emailSubjectInput', EMAIL_SUBJECT);
|
||||
form.setInputValue('emailBodyInput', EMAIL_BODY);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'email_1',
|
||||
type: 'email',
|
||||
to: [EMAIL_RECIPIENT],
|
||||
subject: EMAIL_SUBJECT,
|
||||
body: EMAIL_BODY,
|
||||
email: {
|
||||
to: [EMAIL_RECIPIENT],
|
||||
subject: EMAIL_SUBJECT,
|
||||
body: {
|
||||
text: EMAIL_BODY,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
email_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should simulate a webhook action', async () => {
|
||||
const { form, find, actions, exists } = testBed;
|
||||
|
||||
const METHOD = 'put';
|
||||
const HOST = 'localhost';
|
||||
const PORT = '9200';
|
||||
const PATH = '/test';
|
||||
const USERNAME = 'test_user';
|
||||
const PASSWORD = 'test_password';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('webhook');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
// First, provide invalid fields and verify
|
||||
form.setInputValue('webhookHostInput', '');
|
||||
form.setInputValue('webhookPortInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toEqual([
|
||||
'Webhook host is required.',
|
||||
'Webhook port is required.',
|
||||
]);
|
||||
expect(find('simulateActionButton').props().disabled).toEqual(true);
|
||||
|
||||
// Next, provide valid fields and verify
|
||||
form.setInputValue('webhookMethodSelect', METHOD);
|
||||
form.setInputValue('webhookHostInput', HOST);
|
||||
form.setInputValue('webhookPortInput', PORT);
|
||||
form.setInputValue('webhookPathInput', PATH);
|
||||
form.setInputValue('webhookUsernameInput', USERNAME);
|
||||
form.setInputValue('webhookPasswordInput', PASSWORD);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'webhook_1',
|
||||
type: 'webhook',
|
||||
method: METHOD,
|
||||
host: HOST,
|
||||
port: Number(PORT),
|
||||
path: PATH,
|
||||
body:
|
||||
'{\n "message": "Watch [{{ctx.metadata.name}}] has exceeded the threshold"\n}', // Default
|
||||
username: USERNAME,
|
||||
password: PASSWORD,
|
||||
webhook: {
|
||||
host: HOST,
|
||||
port: Number(PORT),
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
webhook_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should simulate a Jira action', async () => {
|
||||
const { form, find, actions, exists } = testBed;
|
||||
|
||||
const PROJECT_KEY = 'TEST_PROJECT_KEY';
|
||||
const ISSUE_TYPE = 'Bug';
|
||||
const SUMMARY = 'test Jira summary';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('jira');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
// First, provide invalid fields and verify
|
||||
form.setInputValue('jiraProjectKeyInput', '');
|
||||
form.setInputValue('jiraIssueTypeInput', '');
|
||||
form.setInputValue('jiraSummaryInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toEqual([
|
||||
'Jira project key is required.',
|
||||
'Jira issue type is required.',
|
||||
'Jira summary is required.',
|
||||
]);
|
||||
expect(find('simulateActionButton').props().disabled).toEqual(true);
|
||||
|
||||
// Next, provide valid fields and verify
|
||||
form.setInputValue('jiraProjectKeyInput', PROJECT_KEY);
|
||||
form.setInputValue('jiraIssueTypeInput', ISSUE_TYPE);
|
||||
form.setInputValue('jiraSummaryInput', SUMMARY);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'jira_1',
|
||||
type: 'jira',
|
||||
projectKey: PROJECT_KEY,
|
||||
issueType: ISSUE_TYPE,
|
||||
summary: SUMMARY,
|
||||
jira: {
|
||||
fields: {
|
||||
project: {
|
||||
key: PROJECT_KEY,
|
||||
},
|
||||
issuetype: {
|
||||
name: ISSUE_TYPE,
|
||||
},
|
||||
summary: SUMMARY,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
jira_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should simulate a PagerDuty action', async () => {
|
||||
const { form, actions, exists, find } = testBed;
|
||||
|
||||
const DESCRIPTION = 'test pagerduty description';
|
||||
|
||||
actions.clickAddActionButton();
|
||||
actions.clickActionLink('pagerduty');
|
||||
|
||||
expect(exists('watchActionAccordion')).toBe(true);
|
||||
|
||||
// First, provide invalid fields and verify
|
||||
form.setInputValue('pagerdutyDescriptionInput', '');
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('PagerDuty description is required.');
|
||||
expect(find('simulateActionButton').props().disabled).toEqual(true);
|
||||
|
||||
// Next, provide valid fields and verify
|
||||
form.setInputValue('pagerdutyDescriptionInput', DESCRIPTION);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSimulateButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [
|
||||
{
|
||||
id: 'pagerduty_1',
|
||||
type: 'pagerduty',
|
||||
description: DESCRIPTION,
|
||||
pagerduty: {
|
||||
description: DESCRIPTION,
|
||||
},
|
||||
},
|
||||
],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
executeDetails: getExecuteDetails({
|
||||
actionModes: {
|
||||
pagerduty_1: 'force_execute',
|
||||
},
|
||||
ignoreCondition: true,
|
||||
recordExecution: false,
|
||||
}),
|
||||
watch: thresholdWatch,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form payload', () => {
|
||||
test('should send the correct payload', async () => {
|
||||
const { form, find, component, actions } = testBed;
|
||||
|
||||
// Set up required fields
|
||||
form.setInputValue('nameInput', WATCH_NAME);
|
||||
find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox
|
||||
form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
// Verify request
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const thresholdWatch = {
|
||||
id: JSON.parse(latestRequest.requestBody).id, // watch ID is created dynamically
|
||||
name: WATCH_NAME,
|
||||
type: WATCH_TYPES.THRESHOLD,
|
||||
isNew: true,
|
||||
actions: [],
|
||||
index: MATCH_INDICES,
|
||||
timeField: WATCH_TIME_FIELD,
|
||||
triggerIntervalSize: 1,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: 1000,
|
||||
};
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(JSON.stringify(thresholdWatch));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
import axios from 'axios';
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { WatchEditTestBed } from './helpers/watch_edit.helpers';
|
||||
import { WATCH } from './helpers/constants';
|
||||
import defaultWatchJson from '../../public/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'),
|
||||
loadIndexPatterns: async () => {
|
||||
const INDEX_PATTERNS = [
|
||||
{ attributes: { title: 'index1' } },
|
||||
{ attributes: { title: 'index2' } },
|
||||
{ attributes: { title: 'index3' } },
|
||||
];
|
||||
return await INDEX_PATTERNS;
|
||||
},
|
||||
getHttpClient: () => mockHttpClient,
|
||||
}));
|
||||
|
||||
const { setup } = pageHelpers.watchEdit;
|
||||
|
||||
describe.skip('<WatchEdit />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: WatchEditTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('Advanced watch', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadWatchResponse(WATCH);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
test('should set the correct page title', () => {
|
||||
const { find } = testBed;
|
||||
expect(find('pageTitle').text()).toBe(`Edit ${WATCH.watch.name}`);
|
||||
});
|
||||
|
||||
test('should populate the correct values', () => {
|
||||
const { find, exists, component } = testBed;
|
||||
const { watch } = WATCH;
|
||||
const codeEditor = component.find('EuiCodeEditor');
|
||||
|
||||
expect(exists('jsonWatchForm')).toBe(true);
|
||||
expect(find('nameInput').props().value).toBe(watch.name);
|
||||
expect(find('idInput').props().value).toBe(watch.id);
|
||||
expect(JSON.parse(codeEditor.props().value as string)).toEqual(defaultWatchJson);
|
||||
|
||||
// ID should not be editable
|
||||
expect(find('idInput').props().readOnly).toEqual(true);
|
||||
});
|
||||
|
||||
test('save a watch with new values', async () => {
|
||||
const { form, actions } = testBed;
|
||||
const { watch } = WATCH;
|
||||
|
||||
const EDITED_WATCH_NAME = 'new_watch_name';
|
||||
|
||||
form.setInputValue('nameInput', EDITED_WATCH_NAME);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
const DEFAULT_LOGGING_ACTION_ID = 'logging_1';
|
||||
const DEFAULT_LOGGING_ACTION_TYPE = 'logging';
|
||||
const DEFAULT_LOGGING_ACTION_TEXT =
|
||||
'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.';
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
id: watch.id,
|
||||
name: EDITED_WATCH_NAME,
|
||||
type: watch.type,
|
||||
isNew: false,
|
||||
actions: [
|
||||
{
|
||||
id: DEFAULT_LOGGING_ACTION_ID,
|
||||
type: DEFAULT_LOGGING_ACTION_TYPE,
|
||||
text: DEFAULT_LOGGING_ACTION_TEXT,
|
||||
[DEFAULT_LOGGING_ACTION_TYPE]: {
|
||||
text: DEFAULT_LOGGING_ACTION_TEXT,
|
||||
},
|
||||
},
|
||||
],
|
||||
watch: defaultWatchJson,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Threshold watch', () => {
|
||||
const watch = getWatch({
|
||||
id: getRandomString(),
|
||||
type: 'threshold',
|
||||
name: 'my_threshold_watch',
|
||||
timeField: '@timestamp',
|
||||
triggerIntervalSize: 10,
|
||||
triggerIntervalUnit: 'm',
|
||||
aggType: 'count',
|
||||
termSize: 10,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 10,
|
||||
timeWindowUnit: 'm',
|
||||
threshold: [1000],
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadWatchResponse({ watch });
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { component } = testBed;
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
test('should set the correct page title', () => {
|
||||
const { find } = testBed;
|
||||
expect(find('pageTitle').text()).toBe(`Edit ${watch.name}`);
|
||||
});
|
||||
|
||||
test('should populate the correct values', () => {
|
||||
const { find, exists } = testBed;
|
||||
|
||||
expect(exists('thresholdWatchForm')).toBe(true);
|
||||
expect(find('nameInput').props().value).toBe(watch.name);
|
||||
expect(find('watchTimeFieldSelect').props().value).toBe(watch.timeField);
|
||||
});
|
||||
|
||||
test('should save the watch with new values', async () => {
|
||||
const { form, actions } = testBed;
|
||||
|
||||
const EDITED_WATCH_NAME = 'new_threshold_watch_name';
|
||||
|
||||
form.setInputValue('nameInput', EDITED_WATCH_NAME);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
timeField,
|
||||
triggerIntervalSize,
|
||||
triggerIntervalUnit,
|
||||
aggType,
|
||||
termSize,
|
||||
thresholdComparator,
|
||||
timeWindowSize,
|
||||
timeWindowUnit,
|
||||
threshold,
|
||||
} = watch;
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
id,
|
||||
name: EDITED_WATCH_NAME,
|
||||
type,
|
||||
isNew: false,
|
||||
actions: [],
|
||||
timeField,
|
||||
triggerIntervalSize,
|
||||
triggerIntervalUnit,
|
||||
aggType,
|
||||
termSize,
|
||||
thresholdComparator,
|
||||
timeWindowSize,
|
||||
timeWindowUnit,
|
||||
threshold: threshold && threshold[0],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import * as fixtures from '../../test/fixtures';
|
||||
import {
|
||||
setupEnvironment,
|
||||
pageHelpers,
|
||||
nextTick,
|
||||
getRandomString,
|
||||
findTestSubject,
|
||||
} from './helpers';
|
||||
import { WatchListTestBed } from './helpers/watch_list.helpers';
|
||||
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 />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: WatchListTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
describe('watches', () => {
|
||||
describe('when there are no watches', () => {
|
||||
beforeEach(() => {
|
||||
httpRequestsMockHelpers.setLoadWatchesResponse({ watches: [] });
|
||||
});
|
||||
|
||||
test('should display an empty prompt', async () => {
|
||||
const { component, exists } = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('emptyPrompt')).toBe(true);
|
||||
expect(exists('emptyPrompt.createWatchButton')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// create a threshold and advanced watch type and monitoring
|
||||
describe('when there are watches', () => {
|
||||
const watch1 = fixtures.getWatch({
|
||||
name: `watchA-${getRandomString()}`,
|
||||
id: `a-${getRandomString()}`,
|
||||
type: 'threshold',
|
||||
});
|
||||
const watch2 = fixtures.getWatch({
|
||||
name: `watchB-${getRandomString()}`,
|
||||
id: `b-${getRandomString()}`,
|
||||
type: 'json',
|
||||
});
|
||||
const watch3 = fixtures.getWatch({
|
||||
name: `watchC-${getRandomString()}`,
|
||||
id: `c-${getRandomString()}`,
|
||||
type: 'monitoring',
|
||||
isSystemWatch: true,
|
||||
});
|
||||
|
||||
const watches = [watch1, watch2, watch3];
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadWatchesResponse({ watches });
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the correct app title', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('appTitle')).toBe(true);
|
||||
expect(find('appTitle').text()).toEqual('Watcher');
|
||||
});
|
||||
|
||||
test('should have a link to the documentation', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('documentationLink')).toBe(true);
|
||||
expect(find('documentationLink').text()).toBe('Watcher docs');
|
||||
});
|
||||
|
||||
test('should list them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
const { tableCellsValues } = table.getMetaData('watchesTable');
|
||||
|
||||
const getExpectedValue = (value: any) => (typeof value === 'undefined' ? '' : value);
|
||||
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const watch = watches[i];
|
||||
const { name, id, watchStatus } = watch;
|
||||
|
||||
expect(row).toEqual([
|
||||
'',
|
||||
id, // required value
|
||||
getExpectedValue(name),
|
||||
watchStatus.state, // required value
|
||||
getExpectedValue(watchStatus.comment),
|
||||
getExpectedValue(watchStatus.lastMetCondition),
|
||||
getExpectedValue(watchStatus.lastChecked),
|
||||
'',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should have a button to create a watch', () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('createWatchButton')).toBe(true);
|
||||
});
|
||||
|
||||
test('should have a link to view watch details', () => {
|
||||
const { table } = testBed;
|
||||
const { rows } = table.getMetaData('watchesTable');
|
||||
const idColumn = rows[0].columns[1].reactWrapper;
|
||||
|
||||
expect(findTestSubject(idColumn, `watchIdColumn-${watch1.id}`).length).toBe(1);
|
||||
expect(findTestSubject(idColumn, `watchIdColumn-${watch1.id}`).props().href).toEqual(
|
||||
`#/management/elasticsearch/watcher/watches/watch/${watch1.id}/status`
|
||||
);
|
||||
});
|
||||
|
||||
test('should have action buttons on each row to edit and delete a watch', () => {
|
||||
const { table } = testBed;
|
||||
const { rows } = table.getMetaData('watchesTable');
|
||||
const lastColumn = rows[0].columns[rows[0].columns.length - 1].reactWrapper;
|
||||
|
||||
expect(findTestSubject(lastColumn, 'editWatchButton').length).toBe(1);
|
||||
expect(findTestSubject(lastColumn, 'deleteWatchButton').length).toBe(1);
|
||||
});
|
||||
|
||||
describe('system watch', () => {
|
||||
test('should disable edit and delete actions', async () => {
|
||||
const { table } = testBed;
|
||||
const { rows } = table.getMetaData('watchesTable');
|
||||
const systemWatch = rows[2];
|
||||
const firstColumn = systemWatch.columns[0].reactWrapper;
|
||||
const lastColumn = systemWatch.columns[rows[0].columns.length - 1].reactWrapper;
|
||||
|
||||
expect(
|
||||
findTestSubject(firstColumn, `checkboxSelectRow-${watch3.id}`)
|
||||
.getDOMNode()
|
||||
.getAttribute('disabled')
|
||||
).toEqual('');
|
||||
expect(
|
||||
findTestSubject(lastColumn, 'editWatchButton')
|
||||
.getDOMNode()
|
||||
.getAttribute('disabled')
|
||||
).toEqual('');
|
||||
expect(
|
||||
findTestSubject(lastColumn, 'deleteWatchButton')
|
||||
.getDOMNode()
|
||||
.getAttribute('disabled')
|
||||
).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete watch', () => {
|
||||
test('should show a confirmation when clicking the delete watch button', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await actions.clickWatchActionAt(0, 'delete');
|
||||
|
||||
// We need to read the document "body" as the modal is added there and not inside
|
||||
// the component DOM tree.
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteWatchesConfirmation"]')
|
||||
).not.toBe(null);
|
||||
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteWatchesConfirmation"]')!
|
||||
.textContent
|
||||
).toContain('Delete watch');
|
||||
});
|
||||
|
||||
test('should send the correct HTTP request to delete watch', async () => {
|
||||
const { component, actions, table } = testBed;
|
||||
const { rows } = table.getMetaData('watchesTable');
|
||||
|
||||
const watchId = rows[0].columns[2].value;
|
||||
|
||||
await actions.clickWatchActionAt(0, 'delete');
|
||||
|
||||
const modal = document.body.querySelector(
|
||||
'[data-test-subj="deleteWatchesConfirmation"]'
|
||||
);
|
||||
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||
'[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
|
||||
httpRequestsMockHelpers.setDeleteWatchResponse({
|
||||
results: {
|
||||
successes: [watchId],
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('POST');
|
||||
expect(latestRequest.url).toBe(`${API_ROOT}/watches/delete`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { WatchStatusTestBed } from './helpers/watch_status.helpers';
|
||||
import { WATCH } from './helpers/constants';
|
||||
import { getWatchHistory } from '../../test/fixtures';
|
||||
import moment from 'moment';
|
||||
import { ROUTES } from '../../common/constants';
|
||||
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' });
|
||||
const watchHistory2 = getWatchHistory({ startTime: '2019-06-04T01:10:10.987Z' });
|
||||
|
||||
const watchHistoryItems = { watchHistoryItems: [watchHistory1, watchHistory2] };
|
||||
|
||||
const ACTION_ID = 'my_logging_action_1';
|
||||
|
||||
const watch = {
|
||||
...WATCH.watch,
|
||||
watchStatus: {
|
||||
state: WATCH_STATES.FIRING,
|
||||
isActive: true,
|
||||
actionStatuses: [
|
||||
{
|
||||
id: ACTION_ID,
|
||||
state: ACTION_STATES.FIRING,
|
||||
isAckable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe.skip('<WatchStatus />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: WatchStatusTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadWatchResponse({ watch });
|
||||
httpRequestsMockHelpers.setLoadWatchHistoryResponse(watchHistoryItems);
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the correct page title', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('pageTitle').text()).toBe(`Current status for '${watch.name}'`);
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
test('should have 2 tabs', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('tab').length).toBe(2);
|
||||
expect(find('tab').map(t => t.text())).toEqual(['Execution history', 'Action statuses']);
|
||||
});
|
||||
|
||||
test('should navigate to the "Action statuses" tab', () => {
|
||||
const { exists, actions } = testBed;
|
||||
|
||||
expect(exists('watchHistorySection')).toBe(true);
|
||||
expect(exists('watchDetailSection')).toBe(false);
|
||||
|
||||
actions.selectTab('action statuses');
|
||||
|
||||
expect(exists('watchHistorySection')).toBe(false);
|
||||
expect(exists('watchDetailSection')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution history', () => {
|
||||
test('should list history items in the table', () => {
|
||||
const { table } = testBed;
|
||||
const { tableCellsValues } = table.getMetaData('watchHistoryTable');
|
||||
|
||||
const getExpectedValue = (value: any) => (typeof value === 'undefined' ? '' : value);
|
||||
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const historyItem = watchHistoryItems.watchHistoryItems[i];
|
||||
const { startTime, watchStatus } = historyItem;
|
||||
|
||||
expect(row).toEqual([
|
||||
getExpectedValue(moment(startTime).format()),
|
||||
getExpectedValue(watchStatus.state),
|
||||
getExpectedValue(watchStatus.comment),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show execution history details on click', async () => {
|
||||
const { actions, exists } = testBed;
|
||||
|
||||
const watchHistoryItem = {
|
||||
...watchHistory1,
|
||||
watchId: watch.id,
|
||||
watchStatus: {
|
||||
state: WATCH_STATES.FIRING,
|
||||
actionStatuses: [
|
||||
{
|
||||
id: 'my_logging_action_1',
|
||||
state: ACTION_STATES.FIRING,
|
||||
isAckable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const formattedStartTime = moment(watchHistoryItem.startTime).format();
|
||||
|
||||
httpRequestsMockHelpers.setLoadWatchHistoryItemResponse({ watchHistoryItem });
|
||||
|
||||
await actions.clickWatchExecutionAt(0, formattedStartTime);
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('GET');
|
||||
expect(latestRequest.url).toBe(`${API_ROOT}/history/${watchHistoryItem.id}`);
|
||||
|
||||
expect(exists('watchHistoryDetailFlyout')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete watch', () => {
|
||||
test('should show a confirmation when clicking the delete button', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await actions.clickDeleteWatchButton();
|
||||
|
||||
// We need to read the document "body" as the modal is added there and not inside
|
||||
// the component DOM tree.
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteWatchesConfirmation"]')
|
||||
).not.toBe(null);
|
||||
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteWatchesConfirmation"]')!.textContent
|
||||
).toContain('Delete watch');
|
||||
});
|
||||
|
||||
test('should send the correct HTTP request to delete watch', async () => {
|
||||
const { component, actions } = testBed;
|
||||
|
||||
await actions.clickDeleteWatchButton();
|
||||
|
||||
const modal = document.body.querySelector('[data-test-subj="deleteWatchesConfirmation"]');
|
||||
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||
'[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
|
||||
httpRequestsMockHelpers.setDeleteWatchResponse({
|
||||
results: {
|
||||
successes: [watch.id],
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('POST');
|
||||
expect(latestRequest.url).toBe(`${API_ROOT}/watches/delete`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activate & deactive watch', () => {
|
||||
test('should send the correct HTTP request to deactivate and activate a watch', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setDeactivateWatchResponse({
|
||||
watchStatus: {
|
||||
state: WATCH_STATES.DISABLED,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
await actions.clickToggleActivationButton();
|
||||
|
||||
const deactivateRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(deactivateRequest.method).toBe('PUT');
|
||||
expect(deactivateRequest.url).toBe(`${API_ROOT}/watch/${watch.id}/deactivate`);
|
||||
|
||||
httpRequestsMockHelpers.setActivateWatchResponse({
|
||||
watchStatus: {
|
||||
state: WATCH_STATES.FIRING,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
await actions.clickToggleActivationButton();
|
||||
|
||||
const activateRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(activateRequest.method).toBe('PUT');
|
||||
expect(activateRequest.url).toBe(`${API_ROOT}/watch/${watch.id}/activate`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action statuses', () => {
|
||||
beforeEach(() => {
|
||||
const { actions } = testBed;
|
||||
|
||||
actions.selectTab('action statuses');
|
||||
});
|
||||
|
||||
test('should list the watch actions in a table', () => {
|
||||
const { table } = testBed;
|
||||
const { tableCellsValues } = table.getMetaData('watchActionStatusTable');
|
||||
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const action = watch.watchStatus.actionStatuses[i];
|
||||
const { id, state, isAckable } = action;
|
||||
|
||||
expect(row).toEqual([id, state, isAckable ? 'Acknowledge' : '']);
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow an action to be acknowledged', async () => {
|
||||
const { actions, table } = testBed;
|
||||
|
||||
httpRequestsMockHelpers.setAcknowledgeWatchResponse({
|
||||
watchStatus: {
|
||||
state: WATCH_STATES.FIRING,
|
||||
isActive: true,
|
||||
comment: 'Acked',
|
||||
actionStatuses: [
|
||||
{
|
||||
id: ACTION_ID,
|
||||
state: ACTION_STATES.ACKNOWLEDGED,
|
||||
isAckable: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await actions.clickAcknowledgeButton(0);
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('PUT');
|
||||
expect(latestRequest.url).toBe(
|
||||
`${API_ROOT}/watch/${watch.id}/action/${ACTION_ID}/acknowledge`
|
||||
);
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('watchActionStatusTable');
|
||||
|
||||
tableCellsValues.forEach(row => {
|
||||
expect(row).toEqual([ACTION_ID, ACTION_STATES.ACKNOWLEDGED, '']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { pluginDefinition } from '../plugin_definition';
|
||||
|
||||
describe ('pluginDefinition', () => {
|
||||
it('defines the configPrefix correctly', () => {
|
||||
expect(pluginDefinition.configPrefix).to.be('xpack.watcher');
|
||||
});
|
||||
});
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ACTION_MODES = {
|
||||
|
||||
export const ACTION_MODES: { [key: string]: string } = {
|
||||
// The action execution will be simulated. For example, The email action will create the email that would have been sent but will not actually send it. In this mode, the action may be throttled if the current state of the watch indicates it should be.
|
||||
SIMULATE: 'simulate',
|
||||
|
||||
|
@ -19,6 +18,5 @@ export const ACTION_MODES = {
|
|||
FORCE_EXECUTE: 'force_execute',
|
||||
|
||||
// The action will be skipped and won’t be executed nor simulated. Effectively forcing the action to be throttled.
|
||||
SKIP: 'skip'
|
||||
|
||||
SKIP: 'skip',
|
||||
};
|
|
@ -6,36 +6,34 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ACTION_STATES = {
|
||||
|
||||
export const ACTION_STATES: { [key: string]: string } = {
|
||||
// Action is not being executed because conditions haven't been met
|
||||
OK: i18n.translate('xpack.watcher.constants.actionStates.okStateText', {
|
||||
defaultMessage: 'OK'
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
|
||||
// Action has been acknowledged by user
|
||||
ACKNOWLEDGED: i18n.translate('xpack.watcher.constants.actionStates.acknowledgedStateText', {
|
||||
defaultMessage: 'Acked'
|
||||
defaultMessage: 'Acked',
|
||||
}),
|
||||
|
||||
// Action has been throttled (time-based) by the system
|
||||
THROTTLED: i18n.translate('xpack.watcher.constants.actionStates.throttledStateText', {
|
||||
defaultMessage: 'Throttled'
|
||||
defaultMessage: 'Throttled',
|
||||
}),
|
||||
|
||||
// Action has been completed
|
||||
FIRING: i18n.translate('xpack.watcher.constants.actionStates.firingStateText', {
|
||||
defaultMessage: 'Firing'
|
||||
defaultMessage: 'Firing',
|
||||
}),
|
||||
|
||||
// Action has failed
|
||||
ERROR: i18n.translate('xpack.watcher.constants.actionStates.errorStateText', {
|
||||
defaultMessage: 'Error'
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
|
||||
// Action has a configuration error
|
||||
CONFIG_ERROR: i18n.translate('xpack.watcher.constants.actionStates.configErrorStateText', {
|
||||
defaultMessage: 'Config error'
|
||||
defaultMessage: 'Config error',
|
||||
}),
|
||||
|
||||
};
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
|
||||
export const ACTION_TYPES: { [key: string]: string } = {
|
||||
EMAIL: 'email',
|
||||
|
||||
WEBHOOK: 'webhook',
|
||||
|
@ -14,14 +13,11 @@ export const ACTION_TYPES = {
|
|||
|
||||
LOGGING: 'logging',
|
||||
|
||||
HIPCHAT: 'hipchat',
|
||||
|
||||
SLACK: 'slack',
|
||||
|
||||
JIRA: 'jira',
|
||||
|
||||
PAGERDUTY: 'pagerduty',
|
||||
|
||||
UNKNOWN: 'unknown/invalid'
|
||||
|
||||
UNKNOWN: 'unknown/invalid',
|
||||
};
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const AGG_TYPES = {
|
||||
|
||||
export const AGG_TYPES: { [key: string]: string } = {
|
||||
COUNT: 'count',
|
||||
|
||||
AVERAGE: 'avg',
|
||||
|
@ -14,6 +13,5 @@ export const AGG_TYPES = {
|
|||
|
||||
MIN: 'min',
|
||||
|
||||
MAX: 'max'
|
||||
|
||||
MAX: 'max',
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const COMPARATORS: { [key: string]: string } = {
|
||||
GREATER_THAN: '>',
|
||||
GREATER_THAN_OR_EQUALS: '>=',
|
||||
BETWEEN: 'between',
|
||||
LESS_THAN: '<',
|
||||
LESS_THAN_OR_EQUALS: '<=',
|
||||
};
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ERROR_CODES = {
|
||||
|
||||
export const ERROR_CODES: { [key: string]: string } = {
|
||||
// Property missing on object
|
||||
ERR_PROP_MISSING: 'ERR_PROP_MISSING',
|
||||
};
|
|
@ -4,11 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ES_SCROLL_SETTINGS = {
|
||||
|
||||
export const ES_SCROLL_SETTINGS: {
|
||||
KEEPALIVE: string;
|
||||
PAGE_SIZE: number;
|
||||
} = {
|
||||
// How long to keep a scroll alive
|
||||
KEEPALIVE: '30s',
|
||||
|
||||
// How many results to return per scroll response
|
||||
PAGE_SIZE: 100
|
||||
PAGE_SIZE: 100,
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const INDEX_NAMES = {
|
||||
export const INDEX_NAMES: { [key: string]: string } = {
|
||||
WATCHES: '.watches',
|
||||
WATCHER_HISTORY: '.watcher-history-*',
|
||||
};
|
|
@ -5,6 +5,6 @@
|
|||
*/
|
||||
|
||||
// Durations are in ms
|
||||
export const LISTS = {
|
||||
NEW_ITEMS_HIGHLIGHT_DURATION: 1 * 1000
|
||||
export const LISTS: { [key: string]: number } = {
|
||||
NEW_ITEMS_HIGHLIGHT_DURATION: 1 * 1000,
|
||||
};
|
10
x-pack/legacy/plugins/watcher/common/constants/pagination.ts
Normal file
10
x-pack/legacy/plugins/watcher/common/constants/pagination.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const PAGINATION: { initialPageSize: number; pageSizeOptions: number[] } = {
|
||||
initialPageSize: 10,
|
||||
pageSizeOptions: [10, 50, 100],
|
||||
};
|
17
x-pack/legacy/plugins/watcher/common/constants/plugin.ts
Normal file
17
x-pack/legacy/plugins/watcher/common/constants/plugin.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LICENSE_TYPE_GOLD, LicenseType } from '../../../../common/constants';
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: 'watcher',
|
||||
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_GOLD as LicenseType,
|
||||
getI18nName: (i18n: any): string => {
|
||||
return i18n.translate('xpack.watcher.appName', {
|
||||
defaultMessage: 'Watcher',
|
||||
});
|
||||
},
|
||||
};
|
|
@ -7,8 +7,8 @@
|
|||
// In milliseconds
|
||||
const SIXTY_SECONDS = 60 * 1000;
|
||||
|
||||
export const REFRESH_INTERVALS = {
|
||||
export const REFRESH_INTERVALS: { [key: string]: number } = {
|
||||
WATCH_LIST: SIXTY_SECONDS,
|
||||
WATCH_HISTORY: SIXTY_SECONDS,
|
||||
WATCH_VISUALIZATION: SIXTY_SECONDS
|
||||
WATCH_VISUALIZATION: SIXTY_SECONDS,
|
||||
};
|
|
@ -4,6 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ROUTES = {
|
||||
export const ROUTES: { [key: string]: string } = {
|
||||
API_ROOT: '/api/watcher',
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const SORT_ORDERS = {
|
||||
export const SORT_ORDERS: { [key: string]: string } = {
|
||||
ASCENDING: 'asc',
|
||||
DESCENDING: 'desc'
|
||||
DESCENDING: 'desc',
|
||||
};
|
|
@ -4,9 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const TIME_UNITS = {
|
||||
export const TIME_UNITS: { [key: string]: string } = {
|
||||
SECOND: 's',
|
||||
MINUTE: 'm',
|
||||
HOUR: 'h',
|
||||
DAY: 'd'
|
||||
DAY: 'd',
|
||||
};
|
|
@ -4,10 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const COMPARATORS = {
|
||||
|
||||
GREATER_THAN: '>',
|
||||
|
||||
LESS_THAN: '<'
|
||||
|
||||
export const WATCH_HISTORY: { [key: string]: string } = {
|
||||
INITIAL_RANGE: 'now-1h',
|
||||
};
|
|
@ -1,33 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const WATCH_STATE_COMMENTS = {
|
||||
|
||||
OK: '',
|
||||
|
||||
PARTIALLY_THROTTLED: i18n.translate('xpack.watcher.constants.watchStateComments.partiallyThrottledStateCommentText', {
|
||||
defaultMessage: 'Partially Throttled'
|
||||
}),
|
||||
|
||||
THROTTLED: i18n.translate('xpack.watcher.constants.watchStateComments.throttledStateCommentText', {
|
||||
defaultMessage: 'Throttled'
|
||||
}),
|
||||
|
||||
PARTIALLY_ACKNOWLEDGED: i18n.translate('xpack.watcher.constants.watchStateComments.partiallyAcknowledgedStateCommentText', {
|
||||
defaultMessage: 'Partially Acked'
|
||||
}),
|
||||
|
||||
ACKNOWLEDGED: i18n.translate('xpack.watcher.constants.watchStateComments.acknowledgedStateCommentText', {
|
||||
defaultMessage: 'Acked'
|
||||
}),
|
||||
|
||||
FAILING: i18n.translate('xpack.watcher.constants.watchStateComments.executionFailingStateCommentText', {
|
||||
defaultMessage: 'Execution Failing'
|
||||
}),
|
||||
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const WATCH_STATE_COMMENTS: { [key: string]: string } = {
|
||||
OK: '',
|
||||
|
||||
PARTIALLY_THROTTLED: i18n.translate(
|
||||
'xpack.watcher.constants.watchStateComments.partiallyThrottledStateCommentText',
|
||||
{
|
||||
defaultMessage: 'Partially throttled',
|
||||
}
|
||||
),
|
||||
|
||||
THROTTLED: i18n.translate(
|
||||
'xpack.watcher.constants.watchStateComments.throttledStateCommentText',
|
||||
{
|
||||
defaultMessage: 'Throttled',
|
||||
}
|
||||
),
|
||||
|
||||
PARTIALLY_ACKNOWLEDGED: i18n.translate(
|
||||
'xpack.watcher.constants.watchStateComments.partiallyAcknowledgedStateCommentText',
|
||||
{
|
||||
defaultMessage: 'Partially acked',
|
||||
}
|
||||
),
|
||||
|
||||
ACKNOWLEDGED: i18n.translate(
|
||||
'xpack.watcher.constants.watchStateComments.acknowledgedStateCommentText',
|
||||
{
|
||||
defaultMessage: 'Acked',
|
||||
}
|
||||
),
|
||||
|
||||
FAILING: i18n.translate(
|
||||
'xpack.watcher.constants.watchStateComments.executionFailingStateCommentText',
|
||||
{
|
||||
defaultMessage: 'Execution failing',
|
||||
}
|
||||
),
|
||||
};
|
|
@ -6,26 +6,24 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const WATCH_STATES = {
|
||||
|
||||
export const WATCH_STATES: { [key: string]: string } = {
|
||||
DISABLED: i18n.translate('xpack.watcher.constants.watchStates.disabledStateText', {
|
||||
defaultMessage: 'Disabled'
|
||||
defaultMessage: 'Disabled',
|
||||
}),
|
||||
|
||||
OK: i18n.translate('xpack.watcher.constants.watchStates.okStateText', {
|
||||
defaultMessage: 'OK'
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
|
||||
FIRING: i18n.translate('xpack.watcher.constants.watchStates.firingStateText', {
|
||||
defaultMessage: 'Firing'
|
||||
defaultMessage: 'Firing',
|
||||
}),
|
||||
|
||||
ERROR: i18n.translate('xpack.watcher.constants.watchStates.errorStateText', {
|
||||
defaultMessage: 'Error!'
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
|
||||
CONFIG_ERROR: i18n.translate('xpack.watcher.constants.watchStates.configErrorStateText', {
|
||||
defaultMessage: 'Config error'
|
||||
defaultMessage: 'Config error',
|
||||
}),
|
||||
|
||||
};
|
|
@ -4,12 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const WATCH_TYPES = {
|
||||
|
||||
export const WATCH_TYPES: { [key: string]: string } = {
|
||||
JSON: 'json',
|
||||
|
||||
THRESHOLD: 'threshold',
|
||||
|
||||
MONITORING: 'monitoring'
|
||||
|
||||
MONITORING: 'monitoring',
|
||||
};
|
|
@ -56,17 +56,6 @@ describe('get_action_type', () => {
|
|||
expect(type).to.be(ACTION_TYPES.LOGGING);
|
||||
});
|
||||
|
||||
it(`correctly calculates ACTION_TYPES.HIPCHAT`, () => {
|
||||
const actionJson = {
|
||||
hipchat: {
|
||||
'foo': 'bar'
|
||||
}
|
||||
};
|
||||
const type = getActionType(actionJson);
|
||||
|
||||
expect(type).to.be(ACTION_TYPES.HIPCHAT);
|
||||
});
|
||||
|
||||
it(`correctly calculates ACTION_TYPES.SLACK`, () => {
|
||||
const actionJson = {
|
||||
slack: {
|
||||
|
|
|
@ -4,14 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { keys, values, intersection } from 'lodash';
|
||||
import { intersection, keys, values } from 'lodash';
|
||||
import { ACTION_TYPES } from '../../constants';
|
||||
|
||||
export function getActionType(action) {
|
||||
const type = intersection(
|
||||
keys(action),
|
||||
values(ACTION_TYPES)
|
||||
)[0] || ACTION_TYPES.UNKNOWN;
|
||||
export function getActionType(action: { [key: string]: { [key: string]: any } }) {
|
||||
const type = intersection(keys(action), values(ACTION_TYPES))[0] || ACTION_TYPES.UNKNOWN;
|
||||
|
||||
return type;
|
||||
}
|
86
x-pack/legacy/plugins/watcher/common/types/action_types.ts
Normal file
86
x-pack/legacy/plugins/watcher/common/types/action_types.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
type EmailActionType = 'email';
|
||||
type LoggingActionType = 'logging';
|
||||
type WebhookActionType = 'webhook';
|
||||
type IndexActionType = 'index';
|
||||
type SlackActionType = 'slack';
|
||||
type JiraActionType = 'jira';
|
||||
type PagerDutyActionType = 'pagerduty';
|
||||
|
||||
export interface BaseAction {
|
||||
id: string;
|
||||
typeName: string;
|
||||
isNew: boolean;
|
||||
simulateMessage: string;
|
||||
simulateFailMessage: string;
|
||||
simulatePrompt: string;
|
||||
selectMessage: string;
|
||||
validate: () => { [key: string]: string[] };
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface EmailAction extends BaseAction {
|
||||
type: EmailActionType;
|
||||
iconClass: 'email';
|
||||
to: string[];
|
||||
subject?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface LoggingAction extends BaseAction {
|
||||
type: LoggingActionType;
|
||||
iconClass: 'loggingApp';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface IndexAction extends BaseAction {
|
||||
type: IndexActionType;
|
||||
iconClass: 'indexOpen';
|
||||
index: string;
|
||||
}
|
||||
|
||||
export interface PagerDutyAction extends BaseAction {
|
||||
type: PagerDutyActionType;
|
||||
iconClass: 'apps';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface WebhookAction extends BaseAction {
|
||||
type: WebhookActionType;
|
||||
iconClass: 'logoWebhook';
|
||||
method?: 'head' | 'get' | 'post' | 'put' | 'delete';
|
||||
host: string;
|
||||
port: number;
|
||||
path?: string;
|
||||
body?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface SlackAction extends BaseAction {
|
||||
type: SlackActionType;
|
||||
iconClass: 'logoSlack';
|
||||
text?: string;
|
||||
to?: string[];
|
||||
}
|
||||
|
||||
export interface JiraAction extends BaseAction {
|
||||
type: JiraActionType;
|
||||
iconClass: 'apps';
|
||||
projectKey: string;
|
||||
issueType: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export type ActionType =
|
||||
| EmailAction
|
||||
| LoggingAction
|
||||
| IndexAction
|
||||
| SlackAction
|
||||
| JiraAction
|
||||
| PagerDutyAction;
|
57
x-pack/legacy/plugins/watcher/common/types/watch_types.ts
Normal file
57
x-pack/legacy/plugins/watcher/common/types/watch_types.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface ExecutedWatchResults {
|
||||
id: string;
|
||||
watchId: string;
|
||||
details: any;
|
||||
startTime: Date;
|
||||
watchStatus: {
|
||||
state: string;
|
||||
actionStatuses: Array<{ state: string; lastExecutionReason: string; id: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExecutedWatchDetails {
|
||||
scheduledTimeValue: string | undefined;
|
||||
scheduledTimeUnit: string;
|
||||
triggeredTimeValue: string | undefined;
|
||||
triggeredTimeUnit: string;
|
||||
ignoreCondition: boolean;
|
||||
alternativeInput: any;
|
||||
actionModes: {
|
||||
[key: string]: string;
|
||||
};
|
||||
recordExecution: boolean;
|
||||
upstreamJson: any;
|
||||
}
|
||||
|
||||
export interface BaseWatch {
|
||||
id: string;
|
||||
type: string;
|
||||
isNew: boolean;
|
||||
name: string;
|
||||
isSystemWatch: boolean;
|
||||
watchStatus: any;
|
||||
watchErrors: any;
|
||||
typeName: string;
|
||||
displayName: string;
|
||||
upstreamJson: any;
|
||||
resetActions: () => void;
|
||||
createAction: (type: string, actionProps: {}) => void;
|
||||
validate: () => { warning: { message: string; title?: string } };
|
||||
actions: [
|
||||
{
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
];
|
||||
watch: {
|
||||
actions: {
|
||||
[key: string]: { [key: string]: any };
|
||||
};
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
|
@ -12,7 +13,7 @@ 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 { registerLicenseChecker } from '../../server/lib/register_license_checker';
|
||||
import { PLUGIN } from './common/constants';
|
||||
|
||||
export const pluginDefinition = {
|
||||
|
@ -21,17 +22,18 @@ export const pluginDefinition = {
|
|||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
uiExports: {
|
||||
managementSections: [
|
||||
'plugins/watcher/sections/watch_detail',
|
||||
'plugins/watcher/sections/watch_edit',
|
||||
'plugins/watcher/sections/watch_list',
|
||||
'plugins/watcher/sections/watch_history_item',
|
||||
],
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
home: ['plugins/watcher/register_feature']
|
||||
managementSections: ['plugins/watcher'],
|
||||
home: ['plugins/watcher/register_feature'],
|
||||
},
|
||||
init: function (server) {
|
||||
registerLicenseChecker(server);
|
||||
// Register license checker
|
||||
registerLicenseChecker(
|
||||
server,
|
||||
PLUGIN.ID,
|
||||
PLUGIN.getI18nName(i18n),
|
||||
PLUGIN.MINIMUM_LICENSE_REQUIRED
|
||||
);
|
||||
|
||||
registerFieldsRoutes(server);
|
||||
registerHistoryRoutes(server);
|
||||
|
@ -40,5 +42,5 @@ export const pluginDefinition = {
|
|||
registerSettingsRoutes(server);
|
||||
registerWatchesRoutes(server);
|
||||
registerWatchRoutes(server);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
.mgtWatcher__list {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
3
x-pack/legacy/plugins/watcher/public/app.html
Normal file
3
x-pack/legacy/plugins/watcher/public/app.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<kbn-management-app section="elasticsearch/watcher">
|
||||
<div id="watchReactRoot"></div>
|
||||
</kbn-management-app>
|
87
x-pack/legacy/plugins/watcher/public/app.js
Normal file
87
x-pack/legacy/plugins/watcher/public/app.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HashRouter, Switch, Route, Redirect } 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';
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
registerRouter() {
|
||||
// Share the router with the app without requiring React or context.
|
||||
const { router } = this.context;
|
||||
registerRouter(router);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
export const App = ({ licenseStatus }) => {
|
||||
const { status, message } = licenseStatus;
|
||||
|
||||
if (status !== LICENSE_STATUS_VALID) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id="xpack.watcher.app.licenseErrorTitle"
|
||||
defaultMessage="License error"
|
||||
/>
|
||||
)}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
{message}{' '}
|
||||
<EuiLink href="#/management/elasticsearch/license_management/home">
|
||||
<FormattedMessage
|
||||
id="xpack.watcher.app.licenseErrorLinkText"
|
||||
defaultMessage="Manage your license."
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<ShareRouter>
|
||||
<AppWithoutRouter />
|
||||
</ShareRouter>
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
||||
// Export this so we can test it with a different router.
|
||||
export const AppWithoutRouter = () => (
|
||||
<Switch>
|
||||
<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} />
|
||||
<Redirect from={BASE_PATH} to={`${BASE_PATH}watches`} />
|
||||
</Switch>
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
<div class="kuiIcon">
|
||||
<div
|
||||
class="kuiIcon kuiIcon--success fa-check"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.OK"
|
||||
></div>
|
||||
<div
|
||||
class="kuiIcon kuiIcon--warning fa-thumbs-up"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.ACKNOWLEDGED"
|
||||
></div>
|
||||
<div
|
||||
class="kuiIcon kuiIcon--warning fa-clock-o"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.THROTTLED"
|
||||
></div>
|
||||
<div
|
||||
class="kuiIcon kuiIcon--warning fa-bullhorn"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.FIRING"
|
||||
></div>
|
||||
<div
|
||||
class="kuiIcon kuiIcon--error fa-exclamation-triangle"
|
||||
ng-if="actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.ERROR || actionStateIcon.actionStatus.state === actionStateIcon.ACTION_STATES.CONFIG_ERROR"
|
||||
></div>
|
||||
</div>
|
|
@ -1,29 +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 { uiModules } from 'ui/modules';
|
||||
import template from './action_state_icon.html';
|
||||
import { ACTION_STATES } from 'plugins/watcher/../common/constants';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('actionStateIcon', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template: template,
|
||||
scope: {
|
||||
actionStatus: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controllerAs: 'actionStateIcon',
|
||||
controller: class ActionStateIconController {
|
||||
constructor() {
|
||||
this.ACTION_STATES = ACTION_STATES;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
.actionTypeChoice {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.actionTypeChoice--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.actionTypeIcon {
|
||||
flex: 1 0 auto;
|
||||
margin-right: $euiSizeM;
|
||||
font-size: $euiFontSizeL;
|
||||
}
|
||||
|
||||
.actionTypeDescription {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.watchActionsActionType {
|
||||
flex: 1 1 auto;
|
||||
width: 450px;
|
||||
|
||||
.ui-select-bootstrap > .ui-select-choices {
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
// Bootstrap ui select tweaks
|
||||
.ui-select-bootstrap .ui-select-choices-row>span {
|
||||
padding: $euiSizeM $euiSizeL;
|
||||
}
|
||||
|
||||
.ui-select-match .btn {
|
||||
border-color: $euiColorPrimary;
|
||||
color: $euiColorPrimary;
|
||||
}
|
||||
|
||||
.ui-select-placeholder {
|
||||
color: $euiColorPrimary !important;
|
||||
padding-right: $euiSizeM;
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'action_type_select';
|
|
@ -1,34 +0,0 @@
|
|||
<ui-select
|
||||
ng-model="actionTypeSelect.selectedItem.value"
|
||||
on-select="actionTypeSelect.onSelect($select.selected)"
|
||||
>
|
||||
<ui-select-match placeholder="{{ 'xpack.watcher.actionTypeSelect.addNewActionPlaceholder' | i18n: { defaultMessage: 'Add new action' } }}">
|
||||
{{$select.selected.typeName}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="actionType in actionTypeSelect.actionTypes | filter:$select.search">
|
||||
<div
|
||||
class="actionTypeChoice"
|
||||
ng-class="{
|
||||
'actionTypeChoice--disabled': !actionType.isEnabled
|
||||
}"
|
||||
>
|
||||
<div class="actionTypeIcon">
|
||||
<span ng-class="actionType.iconClass"></span>
|
||||
</div>
|
||||
<div class="actionTypeDescription">
|
||||
<div
|
||||
ng-bind-html="actionType.typeName | highlight: $select.search"
|
||||
></div>
|
||||
<div class="action-type-description">
|
||||
<span ng-if="actionType.isEnabled" ng-bind-html="actionType.selectMessage | highlight: $select.search"></span>
|
||||
<span
|
||||
ng-if="!actionType.isEnabled"
|
||||
i18n-id="xpack.watcher.actionTypeSelect.actionDisabledTextMessage"
|
||||
i18n-default-message="Disabled. Configure {elasticsearchYmlText}."
|
||||
i18n-values="{ elasticsearchYmlText: 'elasticsearch.yml' }"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
|
@ -1,72 +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 { map } from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './action_type_select.html';
|
||||
import 'ui/angular_ui_select';
|
||||
import { Action } from 'plugins/watcher/models/action';
|
||||
import 'plugins/watcher/services/settings';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('actionTypeSelect', function ($injector) {
|
||||
const watcherSettingsService = $injector.get('xpackWatcherSettingsService');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
onChange: '='
|
||||
},
|
||||
controllerAs: 'actionTypeSelect',
|
||||
bindToController: true,
|
||||
controller: class ActionTypeSelectController {
|
||||
constructor() {
|
||||
this.selectedItem = { value: null };
|
||||
|
||||
this.loadActionTypes()
|
||||
.then(actionTypes => {
|
||||
this.actionTypes = actionTypes.filter((actionType) => {
|
||||
// 'Action' is the default action type. If an action has the default then it's
|
||||
// not fully implemented and shouldn't be presented to the user.
|
||||
return actionType.typeName !== 'Action';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadActionTypes() {
|
||||
const allActionTypes = Action.getActionTypes();
|
||||
|
||||
// load the configuration settings to determine which actions are enabled
|
||||
return watcherSettingsService.getSettings()
|
||||
.then(settings => {
|
||||
const result = map(allActionTypes, ({ typeName, iconClass, selectMessage }, type) => {
|
||||
const isEnabled = settings.actionTypes[type].enabled;
|
||||
|
||||
return {
|
||||
type,
|
||||
typeName,
|
||||
iconClass,
|
||||
selectMessage,
|
||||
isEnabled
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
onSelect(actionType) {
|
||||
this.selectedItem = { value: null };
|
||||
|
||||
if (actionType.isEnabled) {
|
||||
this.onChange(actionType.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
.chartTooltip {
|
||||
// The following rules adapted from ui/vislib/styles/_tooltip.less
|
||||
line-height: 1.1;
|
||||
font-size: $euiFontSize;
|
||||
font-weight: normal;
|
||||
background-color: $euiColorDarkestShade;
|
||||
color: $euiColorEmptyShade;
|
||||
border-radius: $euiBorderRadius;
|
||||
position: fixed;
|
||||
z-index: 120;
|
||||
word-wrap: break-word;
|
||||
max-width: 40%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
margin: $euiSizeS;
|
||||
padding: $euiSizeXS;
|
||||
}
|
||||
|
||||
.chartTooltip__label {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'chart_tooltip';
|
|
@ -1,5 +0,0 @@
|
|||
<div
|
||||
class="chartTooltip"
|
||||
ng-style="chartTooltip.style"
|
||||
ng-transclude
|
||||
></div>
|
|
@ -1,70 +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 { get } from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './chart_tooltip.html';
|
||||
|
||||
function calculateTooltipPosition(pointPosition, plotPosition, tooltipWidth, tooltipHeight) {
|
||||
|
||||
const tooltipPosition = {
|
||||
top: get(pointPosition, 'top'),
|
||||
left: get(pointPosition, 'left')
|
||||
};
|
||||
|
||||
const tooltipPositionBottom = pointPosition.top + tooltipHeight;
|
||||
const isTooltipBeyondPlotBottom = tooltipPositionBottom > get(plotPosition, 'bottom');
|
||||
if (isTooltipBeyondPlotBottom) {
|
||||
tooltipPosition.top -= tooltipHeight;
|
||||
}
|
||||
|
||||
const tooltipPositionRight = pointPosition.left + tooltipWidth;
|
||||
const isTooltipBeyondPlotRight = tooltipPositionRight > get(plotPosition, 'right');
|
||||
if (isTooltipBeyondPlotRight) {
|
||||
tooltipPosition.left -= tooltipWidth;
|
||||
}
|
||||
|
||||
return tooltipPosition;
|
||||
}
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('chartTooltip', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template: template,
|
||||
scope: {
|
||||
pointPosition: '=',
|
||||
plotPosition: '='
|
||||
},
|
||||
controllerAs: 'chartTooltip',
|
||||
bindToController: true,
|
||||
link: ($scope, element) => {
|
||||
$scope.$watchGroup([
|
||||
'chartTooltip.pointPosition',
|
||||
'chartTooltip.plotPosition'
|
||||
], () => {
|
||||
// Calculate tooltip position. This is especially necessary to make sure the tooltip
|
||||
// stays within the bounds of the chart.
|
||||
const pointPosition = $scope.chartTooltip.pointPosition;
|
||||
const plotPosition = $scope.chartTooltip.plotPosition;
|
||||
|
||||
const tooltipMargin = parseInt(element.css('margin')); // assumption: value is in px
|
||||
const tooltipWidth = element[0].scrollWidth + (2 * tooltipMargin);
|
||||
const tooltipHeight = element[0].scrollHeight + (2 * tooltipMargin);
|
||||
|
||||
const tooltipPosition = calculateTooltipPosition(pointPosition, plotPosition, tooltipWidth, tooltipHeight);
|
||||
$scope.chartTooltip.style = tooltipPosition;
|
||||
});
|
||||
},
|
||||
controller: class ChartTooltipController {
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export const ConfirmWatchesModal = ({
|
||||
modalOptions,
|
||||
callback,
|
||||
}: {
|
||||
modalOptions: {
|
||||
title: string;
|
||||
message: string;
|
||||
buttonLabel?: string;
|
||||
buttonType?: 'primary' | 'danger';
|
||||
} | null;
|
||||
callback: (isConfirmed?: boolean) => void;
|
||||
}) => {
|
||||
if (!modalOptions) {
|
||||
return null;
|
||||
}
|
||||
const { title, message, buttonType, buttonLabel } = modalOptions;
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
buttonColor={buttonType ? buttonType : 'primary'}
|
||||
title={title}
|
||||
onCancel={() => callback()}
|
||||
onConfirm={() => {
|
||||
callback(true);
|
||||
}}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.watcher.sections.watchEdit.json.saveConfirmModal.cancelButtonLabel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
confirmButtonText={
|
||||
buttonLabel
|
||||
? buttonLabel
|
||||
: i18n.translate(
|
||||
'xpack.watcher.sections.watchEdit.json.saveConfirmModal.saveButtonLabel',
|
||||
{ defaultMessage: 'Save watch' }
|
||||
)
|
||||
}
|
||||
>
|
||||
{message}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { deleteWatches } from '../lib/api';
|
||||
|
||||
export const DeleteWatchesModal = ({
|
||||
watchesToDelete,
|
||||
callback,
|
||||
}: {
|
||||
watchesToDelete: string[];
|
||||
callback: (deleted?: string[]) => void;
|
||||
}) => {
|
||||
const numWatchesToDelete = watchesToDelete.length;
|
||||
if (!numWatchesToDelete) {
|
||||
return null;
|
||||
}
|
||||
const confirmModalText = i18n.translate(
|
||||
'xpack.watcher.deleteSelectedWatchesConfirmModal.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
"You can't recover {numWatchesToDelete, plural, one {a deleted watch} other {deleted watches}}.",
|
||||
values: { numWatchesToDelete },
|
||||
}
|
||||
);
|
||||
const confirmButtonText = i18n.translate(
|
||||
'xpack.watcher.deleteSelectedWatchesConfirmModal.deleteButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete {numWatchesToDelete, plural, one {watch} other {# watches}} ',
|
||||
values: { numWatchesToDelete },
|
||||
}
|
||||
);
|
||||
const cancelButtonText = i18n.translate(
|
||||
'xpack.watcher.deleteSelectedWatchesConfirmModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
data-test-subj="deleteWatchesConfirmation"
|
||||
title={confirmButtonText}
|
||||
onCancel={() => callback()}
|
||||
onConfirm={async () => {
|
||||
const { successes, errors } = await deleteWatches(watchesToDelete);
|
||||
const numSuccesses = successes.length;
|
||||
const numErrors = errors.length;
|
||||
callback(successes);
|
||||
if (numSuccesses > 0) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.watcher.sections.watchList.deleteSelectedWatchesSuccessNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Deleted {numSuccesses, number} {numSuccesses, plural, one {watch} other {watches}}',
|
||||
values: { numSuccesses },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (numErrors > 0) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.watcher.sections.watchList.deleteSelectedWatchesErrorNotification.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Failed to delete {numErrors, number} {numErrors, plural, one {watch} other {watches}}',
|
||||
values: { numErrors },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
cancelButtonText={cancelButtonText}
|
||||
confirmButtonText={confirmButtonText}
|
||||
>
|
||||
{confirmModalText}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
.durationSelect {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.durationSelectSize {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.durationSelectUnit {
|
||||
display: inline-block;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'duration_select';
|
|
@ -1,24 +0,0 @@
|
|||
<div class="durationSelect">
|
||||
<input
|
||||
id="{{durationSelect.makeId('size')}}"
|
||||
name="{{durationSelect.makeId('size')}}"
|
||||
type="number"
|
||||
class="durationSelectSize kuiTextInput watcherNumberInput"
|
||||
min="0"
|
||||
required
|
||||
ng-model="durationSelect.size"
|
||||
aria-label="{{ 'xpack.watcher.durationSelect.durationAmountAriaLabel' | i18n: { defaultMessage: 'Duration amount' } }}"
|
||||
aria-describedby="{{durationSelect.describedBy}}"
|
||||
>
|
||||
|
||||
<select
|
||||
id="{{durationSelect.makeId('unit')}}"
|
||||
name="{{durationSelect.makeId('unit')}}"
|
||||
class="durationSelectUnit kuiSelect"
|
||||
required
|
||||
ng-model="durationSelect.unit"
|
||||
ng-options="commonTimeUnit as timeUnit.labelPlural for (commonTimeUnit, timeUnit) in durationSelect.timeUnits"
|
||||
aria-label="{{ 'xpack.watcher.durationSelect.durationTimeUnitAriaLabel' | i18n: { defaultMessage: 'Duration time unit' } }}"
|
||||
aria-describedby="{{durationSelect.describedBy}}"
|
||||
></select>
|
||||
<div>
|
|
@ -1,76 +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 'ui/fancy_forms';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { InitAfterBindingsWorkaround } from 'ui/compat';
|
||||
import { TIME_UNITS } from 'plugins/watcher/constants';
|
||||
import template from './duration_select.html';
|
||||
import moment from 'moment';
|
||||
import 'plugins/watcher/services/html_id_generator';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('durationSelect', function ($injector) {
|
||||
const htmlIdGeneratorFactory = $injector.get('xpackWatcherHtmlIdGeneratorFactory');
|
||||
|
||||
return {
|
||||
require: '^form',
|
||||
scope: {
|
||||
durationId: '@',
|
||||
minimumUnit: '=',
|
||||
minimumSize: '=',
|
||||
unit: '=',
|
||||
size: '=',
|
||||
describedBy: '@',
|
||||
},
|
||||
template,
|
||||
replace: true,
|
||||
controllerAs: 'durationSelect',
|
||||
bindToController: true,
|
||||
link: function ($scope, $element, $attrs, $ctrl) {
|
||||
$scope.durationSelect.form = $ctrl;
|
||||
},
|
||||
controller: class DurationSelectController extends InitAfterBindingsWorkaround {
|
||||
initAfterBindings($scope) {
|
||||
this.timeUnits = TIME_UNITS;
|
||||
this.makeId = htmlIdGeneratorFactory.create(['durationSelect', this.durationId]);
|
||||
|
||||
$scope.$watchMulti([
|
||||
'durationSelect.minimumSize',
|
||||
'durationSelect.minimumUnit'
|
||||
], ([minimumSize, minimumUnit]) => {
|
||||
this.minimumDuration = moment.duration(Number(minimumSize), minimumUnit).asMilliseconds();
|
||||
this.checkValidity();
|
||||
});
|
||||
|
||||
$scope.$watchMulti([
|
||||
`durationSelect.size`,
|
||||
`durationSelect.unit`
|
||||
], ([size, unit]) => {
|
||||
this.duration = moment.duration(Number(size), unit).asMilliseconds();
|
||||
this.checkValidity();
|
||||
});
|
||||
}
|
||||
|
||||
checkValidity = () => {
|
||||
const isValid = this.duration >= this.minimumDuration;
|
||||
const sizeName = this.makeId('size');
|
||||
const unitName = this.makeId('unit');
|
||||
|
||||
if (this.form[sizeName]) {
|
||||
this.form[sizeName].$setTouched(true);
|
||||
this.form[sizeName].$setValidity('minimumDuration', isValid);
|
||||
}
|
||||
|
||||
if (this.form[unitName]) {
|
||||
this.form[unitName].$setTouched(true);
|
||||
this.form[unitName].$setValidity('minimumDuration', isValid);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './duration_select';
|
|
@ -1,18 +0,0 @@
|
|||
<div class="kuiModal">
|
||||
<div class="kuiModalHeader">
|
||||
<h1 class="kuiModalHeader__title" id="watcher__error-display-modal-title">{{ vm.title }}</h1>
|
||||
</div>
|
||||
<div class="kuiModalBody">
|
||||
<ul>
|
||||
<li class="kuiVerticalRhythm" ng-repeat="error in vm.errors">{{ error.message }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="kuiModalFooter">
|
||||
<button
|
||||
ng-click="vm.close()"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="close">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,18 +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 { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.controller('WatcherErrorsDisplayController', function WatcherErrorsDisplayController($modalInstance, params) {
|
||||
this.title = params.title;
|
||||
this.errors = params.errors;
|
||||
|
||||
this.close = function close() {
|
||||
$modalInstance.close();
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './errors_display_modal';
|
|
@ -1 +0,0 @@
|
|||
@import 'components/expression_popover/expression_popover';
|
|
@ -1,26 +0,0 @@
|
|||
<div
|
||||
class="kuiPopover kuiPopover--withTitle"
|
||||
ng-class="{ 'kuiPopover-isOpen': expressionItem.isOpen }"
|
||||
ng-show="expressionItem.isVisible"
|
||||
>
|
||||
<button
|
||||
data-id="expressionItemButton"
|
||||
class="kuiExpressionButton"
|
||||
ng-class="{
|
||||
'kuiExpressionButton-isActive': expressionItem.isOpen,
|
||||
'kuiExpressionButton-isError': !expressionItem.isValid() && expressionItem.isDirty()
|
||||
}">
|
||||
<span class="kuiExpressionButton__description">
|
||||
{{ expressionItem.description | uppercase }}
|
||||
</span>
|
||||
<span class="kuiExpressionButton__value">
|
||||
{{ expressionItem.value | lowercase }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<expression-popover
|
||||
popover-title="expressionItem.description"
|
||||
>
|
||||
<div ng-transclude></div>
|
||||
</expression-popover>
|
||||
</div>
|
|
@ -1,98 +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 'ui/fancy_forms';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { keyMap } from 'ui/utils/key_map';
|
||||
import template from './expression_item.html';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('expressionItem', function ($injector) {
|
||||
const $document = $injector.get('$document');
|
||||
const $timeout = $injector.get('$timeout');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
require: '^expressionBuilder',
|
||||
transclude: true,
|
||||
template: template,
|
||||
scope: {
|
||||
itemId: '@',
|
||||
description: '=',
|
||||
value: '=',
|
||||
isOpen: '=',
|
||||
isVisible: '=',
|
||||
onOpen: '=',
|
||||
onClose: '=',
|
||||
isValid: '=',
|
||||
isDirty: '=',
|
||||
form: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controllerAs: 'expressionItem',
|
||||
link: function ($scope, $el) {
|
||||
$scope.expressionItem.$firstInput = $el.find('[data-id="expressionItemPopoverContent"]').find(':input:first');
|
||||
const $button = $el.find('[data-id="expressionItemButton"]');
|
||||
|
||||
const buttonFocusOrClick = () => {
|
||||
$scope.$apply(() => {
|
||||
$scope.expressionItem.onPopoverOpen();
|
||||
});
|
||||
};
|
||||
|
||||
const documentClick = (event) => {
|
||||
if ($el.find(event.originalEvent.target).length === 0) {
|
||||
$scope.$apply(() => {
|
||||
$scope.expressionItem.onPopoverClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const documentKeydown = (event) => {
|
||||
if ($scope.expressionItem.isOpen && keyMap[event.keyCode] === 'escape') {
|
||||
$scope.$apply(() => {
|
||||
$scope.expressionItem.onPopoverClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$button.on('focus', buttonFocusOrClick);
|
||||
$button.on('click', buttonFocusOrClick);
|
||||
$document.on('click', documentClick);
|
||||
$document.on('keydown', documentKeydown);
|
||||
$scope.$on('$destroy', () => {
|
||||
$button.off('focus', buttonFocusOrClick);
|
||||
$button.off('click', buttonFocusOrClick);
|
||||
$document.off('click', documentClick);
|
||||
$document.off('keydown', documentKeydown);
|
||||
});
|
||||
},
|
||||
controller: class ExpressionItemController {
|
||||
constructor($scope) {
|
||||
$scope.$watch('expressionItem.isOpen', (isOpen, wasOpen) => {
|
||||
if (isOpen) {
|
||||
$timeout(() => {
|
||||
$scope.expressionItem.$firstInput.focus();
|
||||
});
|
||||
} else if (wasOpen) {
|
||||
this.form.$setTouched(true);
|
||||
this.form.$setDirty(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPopoverClose = () => {
|
||||
this.onClose(this.itemId);
|
||||
}
|
||||
|
||||
onPopoverOpen = () => {
|
||||
this.onOpen(this.itemId);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './expression_item';
|
|
@ -1,3 +0,0 @@
|
|||
.watcherExpressionPopover {
|
||||
z-index: 1; // Addresses conflict between the expression popover and the visualization
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<div
|
||||
class="kuiPanelSimple kuiPopover__panel"
|
||||
>
|
||||
<div class="kuiPopoverTitle">
|
||||
{{ expressionPopover.popoverTitle }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-id="expressionItemPopoverContent"
|
||||
class="kuiExpression"
|
||||
ng-transclude
|
||||
></div>
|
||||
</div>
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './expression_popover.html';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('expressionPopover', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
require: '^expressionItem',
|
||||
transclude: true,
|
||||
template: template,
|
||||
scope: {
|
||||
popoverTitle: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controllerAs: 'expressionPopover',
|
||||
controller: class ExpressionPopoverController {
|
||||
constructor() { }
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './expression_popover';
|
|
@ -1,3 +0,0 @@
|
|||
<div
|
||||
ng-transclude
|
||||
></div>
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './expression_builder.html';
|
||||
import './components/expression_item';
|
||||
import './components/expression_popover';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('expressionBuilder', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template: template,
|
||||
scope: {},
|
||||
bindToController: true,
|
||||
controllerAs: 'expressionBuilder',
|
||||
controller: class BuilderController {
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './expression_builder';
|
|
@ -1,3 +0,0 @@
|
|||
.flotChart {
|
||||
height: 100%;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'flot_chart';
|
|
@ -1,7 +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.
|
||||
*/
|
||||
|
||||
export const FLOT_EVENT_PLOT_HOVER_DEBOUNCE_MS = 20; // milliseconds
|
|
@ -1 +0,0 @@
|
|||
<div class="flotChart"></div>
|
|
@ -1,65 +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 { isFunction, debounce } from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './flot_chart.html';
|
||||
import $ from 'plugins/xpack_main/jquery_flot';
|
||||
import { FLOT_EVENT_PLOT_HOVER_DEBOUNCE_MS } from './constants';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('flotChart', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template: template,
|
||||
scope: {
|
||||
|
||||
// See https://github.com/flot/flot/blob/master/API.md#data-format
|
||||
data: '=',
|
||||
|
||||
// See https://github.com/flot/flot/blob/master/API.md#plot-options
|
||||
options: '=',
|
||||
|
||||
// Search for "plothover" in https://github.com/flot/flot/blob/master/API.md
|
||||
onPlotHover: '=',
|
||||
|
||||
},
|
||||
controllerAs: 'flotChart',
|
||||
bindToController: true,
|
||||
link: ($scope, element) => {
|
||||
$scope.flotChart.container = element;
|
||||
},
|
||||
controller: class FlotChartController {
|
||||
constructor($scope) {
|
||||
|
||||
$scope.$watchMulti([
|
||||
'flotChart.data',
|
||||
'flotChart.options'
|
||||
], ([data, options]) => {
|
||||
this.plot = $.plot(this.container, data, options);
|
||||
});
|
||||
|
||||
$scope.$watch('flotChart.onPlotHover', (onPlotHover) => {
|
||||
this.container.unbind('plothover');
|
||||
|
||||
if (isFunction(onPlotHover)) {
|
||||
this.container.bind('plothover', debounce((...params) => {
|
||||
// We use $scope.$apply to tell Angular to trigger a digest whenever
|
||||
// the supplied event handler function is called
|
||||
$scope.$apply(() => onPlotHover(...params, this.plot));
|
||||
}, FLOT_EVENT_PLOT_HOVER_DEBOUNCE_MS));
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
this.container.unbind('plothover');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './flot_chart';
|
|
@ -1,17 +0,0 @@
|
|||
<div class="kuiInfoPanel kuiInfoPanel--error">
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
|
||||
<span
|
||||
class="kuiInfoPanelHeader__title"
|
||||
ng-transclude
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="kuiInfoPanelBody">
|
||||
<div
|
||||
class="kuiInfoPanelBody__message"
|
||||
i18n-id="xpack.watcher.forbiddenMessage.contactAdministratorTextMessage"
|
||||
i18n-default-message="Please contact your administrator."
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,25 +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 { uiModules } from 'ui/modules';
|
||||
import template from './forbidden_message.html';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('forbiddenMessage', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template: template,
|
||||
transclude: true,
|
||||
scope: {},
|
||||
controllerAs: 'forbiddenMessage',
|
||||
bindToController: true,
|
||||
controller: class ForbiddenMessageController {
|
||||
constructor() { }
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './forbidden_message';
|
|
@ -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 { EuiFormRow } from '@elastic/eui';
|
||||
import React, { Children, cloneElement, Fragment, ReactElement } from 'react';
|
||||
export const ErrableFormRow = ({
|
||||
errorKey,
|
||||
isShowingErrors,
|
||||
errors,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
errorKey: string;
|
||||
isShowingErrors: boolean;
|
||||
errors: { [key: string]: string[] };
|
||||
children: ReactElement;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFormRow
|
||||
isInvalid={isShowingErrors && errors[errorKey].length > 0}
|
||||
error={errors[errorKey]}
|
||||
{...rest}
|
||||
>
|
||||
<Fragment>{Children.map(children, child => cloneElement(child))}</Fragment>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
13
x-pack/legacy/plugins/watcher/public/components/index.ts
Normal file
13
x-pack/legacy/plugins/watcher/public/components/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { getPageErrorCode, PageError } from './page_error';
|
||||
export { ConfirmWatchesModal } from './confirm_watches_modal';
|
||||
export { DeleteWatchesModal } from './delete_watches_modal';
|
||||
export { ErrableFormRow } from './form_errors';
|
||||
export { WatchStatus } from './watch_status';
|
||||
export { SectionLoading } from './section_loading';
|
||||
export { SectionError } from './section_error';
|
|
@ -1 +0,0 @@
|
|||
@import 'index_select';
|
|
@ -1,55 +0,0 @@
|
|||
@mixin indexSelectSubText() {
|
||||
color: $euiColorDarkShade;
|
||||
font-size: $euiFontSizeXS;
|
||||
padding: $euiSizeXS $euiSizeL;
|
||||
line-height: $euiLineHeight;
|
||||
}
|
||||
|
||||
.indexSelectNoChoice {
|
||||
@include indexSelectSubText;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. To ensure a smooth UX, prevent the dropdown from hiding while we async search for more results
|
||||
* 2. ui-select likes to clear the result set out of the UI when you enter
|
||||
* a new search query which can cause a jarring UX. This helps mitigate
|
||||
* that by at least providing something in the box so it's not empty
|
||||
*/
|
||||
.indexSelect--activeSearch {
|
||||
.ui-select-dropdown.ng-hide {
|
||||
display: block !important; /* 1 */
|
||||
|
||||
&:after {
|
||||
content: 'Choose...'; /* 2 */
|
||||
@include indexSelectSubText; /* 2 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. By default, hide this until the user searches or there are results
|
||||
*/
|
||||
.indexSelect--noSearch {
|
||||
.ui-select-no-choice {
|
||||
display: none !important; /* 1 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Provide a loading indicator to the user
|
||||
*/
|
||||
.indexSelect--fetchingWithNoIndices {
|
||||
.ui-select-choices:before {
|
||||
content: 'Loading...'; /* 1 */
|
||||
margin-left: $euiSize; /* 1 */
|
||||
font-style: italic; /* 1 */
|
||||
color: $euiColorDarkShade; /* 1 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Ensure that an invalid ui-select shows a red border
|
||||
*/
|
||||
.ui-select-multiple.ui-select-bootstrap.ng-invalid {
|
||||
border-color: $euiColorDanger !important; /* 1 */
|
||||
}
|
|
@ -1,7 +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 './index_select';
|
|
@ -1,46 +0,0 @@
|
|||
<div
|
||||
class="indexSelect"
|
||||
ng-class="{
|
||||
'indexSelect--activeSearch': indexSelect.hasIndexPattern,
|
||||
'indexSelect--noSearch': !indexSelect.hasIndexPattern,
|
||||
'indexSelect--fetchingWithNoIndices': indexSelect.fetchingWithNoIndices
|
||||
}"
|
||||
>
|
||||
<ui-select
|
||||
ng-model="indexSelect.selectedIndices"
|
||||
on-select="indexSelect.onIndicesChanged()"
|
||||
on-remove="indexSelect.onIndicesChanged()"
|
||||
uis-open-close="indexSelect.onDropdownToggled(isOpen)"
|
||||
multiple
|
||||
>
|
||||
<ui-select-match
|
||||
placeholder="{{ 'xpack.watcher.indexSelect.startTypingPlaceholder' | i18n: { defaultMessage: 'Start typing…' } }}"
|
||||
>
|
||||
{{$item.indexName}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices
|
||||
repeat="index in indexSelect.fetchedIndices | filter: {indexName: $select.search}"
|
||||
refresh="indexSelect.fetchIndices($select.search)"
|
||||
refresh-delay="300"
|
||||
group-by="indexSelect.groupIndices"
|
||||
group-filter="indexSelect.sortGroupedIndices"
|
||||
>
|
||||
<div class="actionTypeChoice">
|
||||
<div class="actionTypeDescription">
|
||||
<div
|
||||
class="indexSelectionName"
|
||||
ng-bind-html="index.indexName | highlight: $select.search"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-select-choices>
|
||||
<ui-select-no-choice>
|
||||
<p
|
||||
class="indexSelectNoChoice"
|
||||
i18n-id="xpack.watcher.indexSelect.buildingListTextMessage"
|
||||
i18n-default-message="Building list… please wait"
|
||||
></p>
|
||||
</ui-select-no-choice>
|
||||
</ui-select>
|
||||
</div>
|
|
@ -1,207 +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 { sortBy, pluck, map, startsWith, endsWith } from 'lodash';
|
||||
import 'plugins/watcher/services/indices';
|
||||
import { InitAfterBindingsWorkaround } from 'ui/compat';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './index_select.html';
|
||||
|
||||
function mapIndex(indexName, isFromIndexPattern = false, isUserEntered = false) {
|
||||
return { indexName, isFromIndexPattern, isUserEntered };
|
||||
}
|
||||
|
||||
function collapseIndices(allIndices, allIndexPatterns) {
|
||||
const indices = map(allIndices, indexName => mapIndex(indexName, false));
|
||||
const indexPatterns = map(allIndexPatterns, indexName => mapIndex(indexName, true));
|
||||
indices.push(...indexPatterns);
|
||||
return indices;
|
||||
}
|
||||
|
||||
const INDICES_FROM_INDEX_PATTERNS_HEADER_COPY = 'Based on your index patterns';
|
||||
const INDICES_FOR_CREATION_HEADER_COPY = 'Choose...';
|
||||
const INDICES_FROM_INDICES_HEADER_COPY = 'Based on your indices and aliases';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('indexSelect', ($injector) => {
|
||||
const indicesService = $injector.get('xpackWatcherIndicesService');
|
||||
const indexPatternsService = $injector.get('indexPatterns');
|
||||
const $timeout = $injector.get('$timeout');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template,
|
||||
scope: {
|
||||
index: '=',
|
||||
onChange: '=',
|
||||
onTouched: '='
|
||||
},
|
||||
controllerAs: 'indexSelect',
|
||||
bindToController: true,
|
||||
link: ($scope, $ele) => {
|
||||
const $searchBox = $ele.find('input[type="search"]');
|
||||
$scope.indexSelect.$searchBox = $searchBox;
|
||||
|
||||
$searchBox.attr('id', 'indexSelectSearchBox');
|
||||
},
|
||||
controller: class IndexSelectController extends InitAfterBindingsWorkaround {
|
||||
initAfterBindings($scope) {
|
||||
this.$scope = $scope;
|
||||
this.indexPattern = undefined;
|
||||
this.fetchingWithNoIndices = true;
|
||||
this.selectedIndices = [];
|
||||
this.fetchedIndices = [];
|
||||
|
||||
if (Boolean(this.index)) {
|
||||
if (Array.isArray(this.index)) {
|
||||
this.selectedIndices.push(...this.index.map(mapIndex));
|
||||
} else {
|
||||
this.selectedIndices.push(mapIndex(this.index));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.onTouched) {
|
||||
$timeout(() => {
|
||||
this.$searchBox.on('blur', () => {
|
||||
$scope.$apply(this.onTouched);
|
||||
});
|
||||
});
|
||||
$scope.$on('$destroy', () => {
|
||||
this.$searchBox.off('blur');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$watch('indexSelect.index', () => {
|
||||
$timeout(() => {
|
||||
// Hack that forces the ui-select to resize itself
|
||||
$scope.$$childHead.$select.sizeSearchInput();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
get hasIndexPattern() {
|
||||
return Boolean(this.indexPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method powers the `on-select` and `on-remove` within ui-select
|
||||
* to handle when an index is added or removed from the list
|
||||
*/
|
||||
onIndicesChanged() {
|
||||
const indexNames = pluck(this.selectedIndices, 'indexName');
|
||||
this.onChange(indexNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method powers the `tagging` within ui-select to format
|
||||
* a search query that has no results into a valid result so it
|
||||
* can be selected
|
||||
*
|
||||
* @param {object} item
|
||||
*/
|
||||
onNewItem(unmatchedIndexPattern) {
|
||||
return mapIndex(unmatchedIndexPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method powers the `group-by` within ui-select to group
|
||||
* our indices array based on the source
|
||||
*
|
||||
* @param {object} index
|
||||
*/
|
||||
groupIndices(index) {
|
||||
if (index.isFromIndexPattern) {
|
||||
return INDICES_FROM_INDEX_PATTERNS_HEADER_COPY;
|
||||
}
|
||||
|
||||
if (index.isUserEntered) {
|
||||
return INDICES_FOR_CREATION_HEADER_COPY;
|
||||
}
|
||||
|
||||
return INDICES_FROM_INDICES_HEADER_COPY;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method powers the `group-filter` within ui-select to allow
|
||||
* us to sort the grouped object so we can control which group
|
||||
* is shown first
|
||||
*
|
||||
* @param {object} grouped
|
||||
*/
|
||||
sortGroupedIndices(grouped) {
|
||||
return sortBy(grouped, group => group.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method powers the `uis-open-close` within ui-select to ensure
|
||||
* we reset the search state once the dropdown is closed. The default
|
||||
* behavior of ui-select is to clear the input field when the dropdown
|
||||
* is closed and if we fail to reset the search state at the same time
|
||||
* it will lead to a poor UX.
|
||||
*
|
||||
* @param {bool} isOpen
|
||||
*/
|
||||
onDropdownToggled(isOpen) {
|
||||
if (!isOpen) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the search state so we have no stored query or results
|
||||
*/
|
||||
reset() {
|
||||
this.fetchedIndices.length = 0;
|
||||
this.indexPattern = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This powers the `refresh` within ui-select which is called
|
||||
* as a way to async fetch results based on the typed in query
|
||||
*
|
||||
* @param {string} indexPattern
|
||||
*/
|
||||
fetchIndices(indexPattern) {
|
||||
if (!Boolean(indexPattern)) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store this so we can display it if there are no matches
|
||||
this.indexPattern = indexPattern;
|
||||
|
||||
let pattern = indexPattern;
|
||||
|
||||
// Use wildcards religiously to ensure partial matches
|
||||
if (!endsWith(pattern, '*')) {
|
||||
pattern += '*';
|
||||
}
|
||||
if (!startsWith(pattern, '*')) {
|
||||
pattern = '*' + pattern;
|
||||
}
|
||||
|
||||
const promises = [
|
||||
indicesService.getMatchingIndices(pattern),
|
||||
indexPatternsService.getTitles()
|
||||
];
|
||||
|
||||
if (this.fetchedIndices.length === 0) {
|
||||
this.fetchingWithNoIndices = true;
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(([allIndices, allIndexPatterns]) => {
|
||||
const indices = collapseIndices(allIndices, allIndexPatterns);
|
||||
this.fetchedIndices = sortBy(indices, 'indexName');
|
||||
this.fetchedIndices.push(mapIndex(this.indexPattern, false, true));
|
||||
this.fetchingWithNoIndices = false;
|
||||
this.$scope.$apply();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
@import 'json_editor';
|
|
@ -1,3 +0,0 @@
|
|||
.json-editor {
|
||||
border: 1px solid $euiColorLightShade;
|
||||
}
|
|
@ -1,7 +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 './json_editor';
|
|
@ -1,12 +0,0 @@
|
|||
<form name="jsonEditor.form" novalidate>
|
||||
<div
|
||||
class="json-editor"
|
||||
json-input
|
||||
require-keys="true"
|
||||
ui-ace="{
|
||||
mode: 'json',
|
||||
onLoad: aceLoaded
|
||||
}"
|
||||
ng-model="jsonEditor.json"
|
||||
></div>
|
||||
</form>
|
|
@ -1,48 +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 { uiModules } from 'ui/modules';
|
||||
import template from './json_editor.html';
|
||||
import 'plugins/watcher/directives/json_input';
|
||||
import 'ace';
|
||||
|
||||
const app = uiModules.get('xpack/watcher');
|
||||
|
||||
app.directive('jsonEditor', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
replace: true,
|
||||
scope: {
|
||||
json: '=',
|
||||
onChange: '=',
|
||||
onValid: '=',
|
||||
onInvalid: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controllerAs: 'jsonEditor',
|
||||
controller: class JsonEditorController {
|
||||
constructor($scope) {
|
||||
$scope.aceLoaded = (editor) => {
|
||||
this.editor = editor;
|
||||
editor.$blockScrolling = Infinity;
|
||||
};
|
||||
|
||||
$scope.$watch('jsonEditor.form.$valid', () => {
|
||||
if (this.form.$invalid) {
|
||||
this.onInvalid();
|
||||
} else {
|
||||
this.onValid();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('jsonEditor.json', () => {
|
||||
this.onChange(this.json);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,7 +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 './kbn_tab';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue