From 8522e40edde2b9b7477fc246a72563367e0fd638 Mon Sep 17 00:00:00 2001 From: Ben Skelker <54019610+benskelker@users.noreply.github.com> Date: Thu, 10 Oct 2019 10:37:27 +0300 Subject: [PATCH 01/92] [SIEM] Update SIEM ML license requirements popup text (#47445) * Update SIEM ML license requirements text * Updates ML license popup with link to cloud * Added updated translation json files --- .../upgrade_contents.test.tsx.snap | 21 ++++++++++++++++++- .../components/ml_popover/translations.ts | 8 ------- .../ml_popover/upgrade_contents.tsx | 20 ++++++++++++++++-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap index d0926dce55162..338c416b5d847 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap @@ -10,7 +10,26 @@ exports[`FilterGroup renders correctly against snapshot 1`] = ` - In order to access SIEM’s anomaly detection features, you must be subscribed to an Elastic Platinum license. With it, you’ll have the ability to run Machine Learning jobs to view anomalous events throughout SIEM. + + + , + } + } + /> { return ( {i18n.UPGRADE_TITLE} - {i18n.UPGRADE_DESCRIPTION} + + + + + ), + }} + /> + Date: Thu, 10 Oct 2019 11:23:41 +0200 Subject: [PATCH 02/92] Use correct color for circles (#47715) --- .../vislib/visualizations/point_series/line_chart.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js b/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js index 90683b13b4a8b..e8c3866a975d3 100644 --- a/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/point_series/line_chart.js @@ -92,18 +92,18 @@ export class LineChart extends PointSeries { return yScale(y0 + y); } - function cColor(d) { - return color(d.series); + function cColor() { + return color(data.label); } - function colorCircle(d) { + function colorCircle() { const parent = d3.select(this).node().parentNode; const lengthOfParent = d3.select(parent).data()[0].length; const isVisible = (lengthOfParent === 1); // If only 1 point exists, show circle if (!showCircles && !isVisible) return 'none'; - return cColor(d); + return cColor(); } function getCircleRadiusFn(modifier) { From be9969ee02e8648ca8b16693cf806655f94424c9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 10 Oct 2019 11:24:07 +0200 Subject: [PATCH 03/92] fix infinite loop in vega tooltips (#47700) --- package.json | 2 +- .../public/vega_view/vega_tooltip.js | 2 +- yarn.lock | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 516f8349e40f6..c7f8a327b8c45 100644 --- a/package.json +++ b/package.json @@ -257,7 +257,7 @@ "vega-lib": "4.3.0", "vega-lite": "^2.6.0", "vega-schema-url-parser": "1.0.0", - "vega-tooltip": "^0.9.14", + "vega-tooltip": "^0.19.1", "vision": "^5.3.3", "webpack": "4.41.0", "webpack-merge": "4.2.2", diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js index 8f8189166ce05..26c48209076b4 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js @@ -68,7 +68,7 @@ export class TooltipHandler { // Sanitized HTML is created by the tooltip library, // with a large number of tests, hence suppressing eslint here. // eslint-disable-next-line no-unsanitized/property - el.innerHTML = createTooltipContent(value, _.escape); + el.innerHTML = createTooltipContent(value, _.escape, 2); // add to DOM to calculate tooltip size document.body.appendChild(el); diff --git a/yarn.lock b/yarn.lock index 5c6fc1e7c1a31..52b9641b4ed33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29203,13 +29203,12 @@ vega-statistics@^1.2.1, vega-statistics@^1.2.2, vega-statistics@^1.2.3: dependencies: d3-array "^2.0.2" -vega-tooltip@^0.9.14: - version "0.9.14" - resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.9.14.tgz#c10bcacf69bf60a02c598ec46b905f94f28c54ac" - integrity sha512-rs/U6MxkccghrHJbtXzHMKq/OnUHqeGEpnF6Ssu6j3Hnx6aZ+Tmq6zdJwYZ0DTY+Xn1ey/SgV/a7G3/ILyUcmQ== +vega-tooltip@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.19.1.tgz#b0534b90a7df21fee9e693bf4e4556312f89296e" + integrity sha512-BNZ5T866SLOai+NZyGxg60U6hZhNINHuX313/z1TrUTeCprYLfCR1Ex4qRozY1WPY3HfxQcd5czLJMhoAFDotQ== dependencies: - json-stringify-safe "^5.0.1" - vega-util "^1.7.0" + vega-util "^1.11.1" vega-transforms@^2.3.0: version "2.3.1" @@ -29228,6 +29227,11 @@ vega-typings@*, vega-typings@^0.3.17: dependencies: vega-util "^1.7.0" +vega-util@^1.11.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.12.0.tgz#fe46198b5294a68d32bedddcc539bb2522de3cba" + integrity sha512-eN1PAQVDyEOcwild2Fk1gbkzkqgDHNujG2/akYRtBzkhtz2EttrVIDwBkWqV/Q+VvEINEksb7TI3Wv7qVQFR5g== + vega-util@^1.7.0: version "1.7.1" resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.7.1.tgz#0b95bbe6058895c332596c215247507caa68ab61" From 7604b7e81df52bd5f8bb722508709d350c943460 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 10 Oct 2019 11:25:17 +0200 Subject: [PATCH 04/92] [Graph] Do not carry over diversity field when switching data source (#47560) --- x-pack/legacy/plugins/graph/README.md | 3 +- .../state_management/advanced_settings.ts | 4 ++ .../state_management/datasource.sagas.ts | 56 +++++++++++++++++++ .../state_management/datasource.test.ts | 18 +++++- .../public/state_management/datasource.ts | 54 +++--------------- .../graph/public/state_management/index.ts | 1 + .../public/state_management/persistence.ts | 3 +- .../graph/public/state_management/store.ts | 3 +- 8 files changed, 90 insertions(+), 52 deletions(-) create mode 100644 x-pack/legacy/plugins/graph/public/state_management/datasource.sagas.ts diff --git a/x-pack/legacy/plugins/graph/README.md b/x-pack/legacy/plugins/graph/README.md index ab842ae594f8a..f402b35bba49f 100644 --- a/x-pack/legacy/plugins/graph/README.md +++ b/x-pack/legacy/plugins/graph/README.md @@ -26,7 +26,8 @@ Currently most of the state handling is done by a central angular controller. Th * `helpers/` contains side effect free helper functions that can be imported and used from components and services * `state_management/` contains reducers, action creators, selectors and sagas. It also exports the central store creator * Each file covers one functional area (e.g. handling of fields, handling of url templates...) - * There is no file separation between reducers, action creators, selectors and sagas of the same functional area + * Generally there is no file separation between reducers, action creators, selectors and sagas of the same functional area + * Sagas may contain cross-references between multiple functional areas (e.g. the loading saga sets fields and meta data). Because of this it is possible that circular imports occur. In this case the sagas are moved to a separate file `.sagas.ts`. * `types/` contains type definitions for unmigrated functions in `angular/` and business objects * `app.js` is the central entrypoint of the app. It initializes router, state management and root components. This will become `app.tsx` when the migration is complete diff --git a/x-pack/legacy/plugins/graph/public/state_management/advanced_settings.ts b/x-pack/legacy/plugins/graph/public/state_management/advanced_settings.ts index 44950f8a45f85..e6325c1e7fd68 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/advanced_settings.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/advanced_settings.ts @@ -10,6 +10,7 @@ import { takeLatest } from 'redux-saga/effects'; import { GraphState, GraphStoreDependencies } from './store'; import { AdvancedSettings } from '../types'; import { reset } from './global'; +import { setDatasource, requestDatasource } from './datasource'; const actionCreator = actionCreatorFactory('x-pack/graph/advancedSettings'); @@ -28,6 +29,9 @@ const initialSettings: AdvancedSettingsState = { export const advancedSettingsReducer = reducerWithInitialState(initialSettings) .case(reset, () => initialSettings) + .cases([requestDatasource, setDatasource], ({ sampleDiversityField, ...restSettings }) => ({ + ...restSettings, + })) .case(updateSettings, (_oldSettings, newSettings) => newSettings) .build(); diff --git a/x-pack/legacy/plugins/graph/public/state_management/datasource.sagas.ts b/x-pack/legacy/plugins/graph/public/state_management/datasource.sagas.ts new file mode 100644 index 0000000000000..7afea3d2a765a --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/state_management/datasource.sagas.ts @@ -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 { takeLatest, put, call, select } from 'redux-saga/effects'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern } from 'src/legacy/core_plugins/data/public'; +import { Action } from 'typescript-fsa'; +import { GraphStoreDependencies } from './store'; +import { loadFields } from './fields'; +import { mapFields } from '../services/persistence'; +import { settingsSelector } from './advanced_settings'; +import { + IndexpatternDatasource, + datasourceLoaded, + setDatasource, + requestDatasource, +} from './datasource'; + +/** + * Saga loading field information when the datasource is switched. This will overwrite current settings + * in fields. + * + * TODO: Carry over fields than can be carried over because they also exist in the target index pattern + */ +export const datasourceSaga = ({ + indexPatternProvider, + notifications, + createWorkspace, + notifyAngular, +}: GraphStoreDependencies) => { + function* fetchFields(action: Action) { + try { + const indexPattern: IndexPattern = yield call(indexPatternProvider.get, action.payload.id); + yield put(loadFields(mapFields(indexPattern))); + yield put(datasourceLoaded()); + const advancedSettings = settingsSelector(yield select()); + createWorkspace(indexPattern.title, advancedSettings); + notifyAngular(); + } catch (e) { + // in case of errors, reset the datasource and show notification + yield put(setDatasource({ type: 'none' })); + notifications.toasts.addDanger( + i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { + defaultMessage: 'Index pattern not found', + }) + ); + } + } + + return function*() { + yield takeLatest(requestDatasource.match, fetchFields); + }; +}; diff --git a/x-pack/legacy/plugins/graph/public/state_management/datasource.test.ts b/x-pack/legacy/plugins/graph/public/state_management/datasource.test.ts index 2cbc368de9ec4..7e51e463f9f61 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/datasource.test.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/datasource.test.ts @@ -5,8 +5,9 @@ */ import { createMockGraphStore, MockedGraphEnvironment } from './mocks'; -import { AdvancedSettings } from '../types'; -import { datasourceSelector, datasourceSaga, requestDatasource } from './datasource'; +import { AdvancedSettings, WorkspaceField } from '../types'; +import { datasourceSelector, requestDatasource } from './datasource'; +import { datasourceSaga } from './datasource.sagas'; import { fieldsSelector } from './fields'; import { updateSettings } from './advanced_settings'; import { IndexPattern } from 'src/legacy/core_plugins/data/public'; @@ -54,6 +55,19 @@ describe('datasource saga', () => { expect(env.mockedDeps.createWorkspace).toHaveBeenCalledWith('test-pattern', newSettings); }); + it('should not carry over diversity field into new workspace', async () => { + const newSettings = { + timeoutMillis: 123, + sampleDiversityField: { name: 'field1' } as WorkspaceField, + } as AdvancedSettings; + env.store.dispatch(updateSettings(newSettings)); + dispatchRequest(); + await waitForPromise(); + expect(env.mockedDeps.createWorkspace).toHaveBeenCalledWith('test-pattern', { + timeoutMillis: 123, + }); + }); + it('should error with a toast and abort if index pattern is not found', async () => { (env.mockedDeps.indexPatternProvider.get as jest.Mock).mockRejectedValueOnce(new Error()); dispatchRequest(); diff --git a/x-pack/legacy/plugins/graph/public/state_management/datasource.ts b/x-pack/legacy/plugins/graph/public/state_management/datasource.ts index 801dd315df30e..fac4f0da1edb0 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/datasource.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/datasource.ts @@ -4,17 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import actionCreatorFactory, { Action } from 'typescript-fsa'; +import actionCreatorFactory from 'typescript-fsa'; import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; -import { takeLatest, put, call, select } from 'redux-saga/effects'; -import { i18n } from '@kbn/i18n'; -import { IndexPattern } from 'src/legacy/core_plugins/data/public'; import { createSelector } from 'reselect'; -import { GraphState, GraphStoreDependencies } from './store'; +import { GraphState } from './store'; import { reset } from './global'; -import { loadFields } from './fields'; -import { mapFields } from '../services/persistence'; -import { settingsSelector } from './advanced_settings'; const actionCreator = actionCreatorFactory('x-pack/graph/datasource'); @@ -27,11 +21,6 @@ export interface IndexpatternDatasource { title: string; } -export interface DatasourceState { - current: NoDatasource | IndexpatternDatasource; - loading: boolean; -} - /** * Sets the current datasource. This will not trigger a load of fields */ @@ -48,6 +37,11 @@ export const requestDatasource = actionCreator('SET_DATA */ export const datasourceLoaded = actionCreator('SET_DATASOURCE_SUCCESS'); +export interface DatasourceState { + current: NoDatasource | IndexpatternDatasource; + loading: boolean; +} + const initialDatasource: DatasourceState = { current: { type: 'none' }, loading: false, @@ -74,37 +68,3 @@ export const hasDatasourceSelector = createSelector( datasourceSelector, datasource => datasource.current.type !== 'none' ); - -/** - * Saga loading field information when the datasource is switched. This will overwrite current settings - * in fields. - * - * TODO: Carry over fields than can be carried over because they also exist in the target index pattern - */ -export const datasourceSaga = ({ - indexPatternProvider, - notifications, - createWorkspace, -}: GraphStoreDependencies) => { - function* fetchFields(action: Action) { - try { - const indexPattern: IndexPattern = yield call(indexPatternProvider.get, action.payload.id); - yield put(loadFields(mapFields(indexPattern))); - yield put(datasourceLoaded()); - const advancedSettings = settingsSelector(yield select()); - createWorkspace(indexPattern.title, advancedSettings); - } catch (e) { - // in case of errors, reset the datasource and show notification - yield put(setDatasource({ type: 'none' })); - notifications.toasts.addDanger( - i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { - defaultMessage: 'Index pattern not found', - }) - ); - } - } - - return function*() { - yield takeLatest(requestDatasource.match, fetchFields); - }; -}; diff --git a/x-pack/legacy/plugins/graph/public/state_management/index.ts b/x-pack/legacy/plugins/graph/public/state_management/index.ts index da6ce44e9c32b..c8018056b2758 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/index.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/index.ts @@ -8,6 +8,7 @@ export * from './fields'; export * from './url_templates'; export * from './advanced_settings'; export * from './datasource'; +export * from './datasource.sagas'; export * from './meta_data'; export * from './persistence'; export * from './workspace'; diff --git a/x-pack/legacy/plugins/graph/public/state_management/persistence.ts b/x-pack/legacy/plugins/graph/public/state_management/persistence.ts index 7ad3caa32f1cd..0bc7827358b81 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/persistence.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/persistence.ts @@ -9,7 +9,8 @@ import { i18n } from '@kbn/i18n'; import { takeLatest, call, put, select, cps } from 'redux-saga/effects'; import { GraphWorkspaceSavedObject, Workspace } from '../types'; import { GraphStoreDependencies, GraphState } from '.'; -import { setDatasource, datasourceSelector, IndexpatternDatasource } from './datasource'; +import { datasourceSelector } from './datasource'; +import { setDatasource, IndexpatternDatasource } from './datasource'; import { loadFields, selectedFieldsSelector } from './fields'; import { updateSettings, settingsSelector } from './advanced_settings'; import { loadTemplates, templatesSelector } from './url_templates'; diff --git a/x-pack/legacy/plugins/graph/public/state_management/store.ts b/x-pack/legacy/plugins/graph/public/state_management/store.ts index 05b0c9015164d..752483e65d8cc 100644 --- a/x-pack/legacy/plugins/graph/public/state_management/store.ts +++ b/x-pack/legacy/plugins/graph/public/state_management/store.ts @@ -21,7 +21,8 @@ import { advancedSettingsReducer, syncSettingsSaga, } from './advanced_settings'; -import { DatasourceState, datasourceReducer, datasourceSaga } from './datasource'; +import { DatasourceState, datasourceReducer } from './datasource'; +import { datasourceSaga } from './datasource.sagas'; import { IndexPatternProvider, Workspace, From 31ad6348433c4ca6e3cf0f50eda8485c8f543114 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 10 Oct 2019 12:14:12 +0200 Subject: [PATCH 05/92] [ML] Converts Recognizer job page to React (#47429) * [ML] wip recognize job * [ML] error handling * [ML] layout enhancements * [ML] errors handling * [ML] fix badges alignment and error message * [ML] components split * [ML] load module from the component, reset support * [ML] validators * [ML] check view route and resolver * [ML] validation * [ML] remove angular code * [ML] change layout, fix patterns * [ML] job response fix * [ML] directive test * [ML] directive test * [ML] remove lookbehind regexp * [ML] fix IE11 layout issues * [ML] remove form tag, refactor to formState * [ML] aria labels, remove unused i18n * [ML] align kibana objects and reset button * [ML] column layout for jobs response * [ML] align icons and buttons * [ML] check existing kibana objects * [ML] don't display kibana objects panel if empty * [ML] result and reset button order * [ML] add EuiHorizontalRule * [ML] use time range and full index dat * [ML] no check icon for existing objects * [ML] toast for exception during a jobs setup * [ML] disable panels growth, advanced settings with described form * [ML] fix timeRange for results url * [ML] fix i18n * [ML] use EuiSwitch * [ML] PR remarks * [ML] job settings form component * [ML] i18n * [ML] onChange fix * [ML] custom hook for partial state update * [ML] jobGroups update * [ML] PR remarks * [ML] fix imports --- .../legacy/plugins/ml/common/types/modules.ts | 83 +++ .../plugins/ml/common/util/validators.ts | 35 ++ .../custom_hooks/index.ts} | 4 +- .../custom_hooks/use_partial_state.ts | 21 + .../components/json_tooltip/tooltips.js | 10 - x-pack/legacy/plugins/ml/public/jobs/index.js | 1 - .../ml/public/jobs/new_job/simple/_index.scss | 1 - .../jobs/new_job/simple/recognize/_index.scss | 1 - .../new_job/simple/recognize/check_module.js | 58 -- .../__tests__/create_job_controller.js | 45 -- .../recognize/create_job/_create_jobs.scss | 193 ------ .../simple/recognize/create_job/_index.scss | 1 - .../recognize/create_job/create_job.html | 362 ----------- .../create_job/create_job_controller.js | 595 ------------------ .../create_job/create_job_service.js | 51 -- .../common/components/index.ts} | 8 +- .../common/components/job_groups_input.tsx | 80 +++ .../components}/time_range_picker.tsx | 17 +- .../common/job_validator/job_validator.ts | 3 +- .../ml/public/jobs/new_job_new/index.ts | 2 + .../components/time_range_step/time_range.tsx | 10 +- .../recognize/__test__/directive.js | 45 ++ .../components/create_result_callout.tsx | 103 +++ .../components/job_settings_form.tsx | 323 ++++++++++ .../recognize/components/kibana_objects.tsx | 104 +++ .../recognize/components/module_jobs.tsx | 178 ++++++ .../jobs/new_job_new/recognize/directive.tsx | 69 ++ .../jobs/new_job_new/recognize/page.tsx | 314 +++++++++ .../jobs/new_job_new/recognize/resolvers.ts | 95 +++ .../jobs/new_job_new/recognize/route.ts | 36 ++ .../ml/public/services/job_service.d.ts | 2 +- .../public/services/ml_api_service/index.d.ts | 4 +- .../public/services/ml_api_service/index.js | 5 +- .../translations/translations/ja-JP.json | 70 --- .../translations/translations/zh-CN.json | 70 --- 35 files changed, 1513 insertions(+), 1486 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/common/types/modules.ts rename x-pack/legacy/plugins/ml/public/{jobs/new_job/simple/recognize/index.js => components/custom_hooks/index.ts} (81%) create mode 100644 x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js rename x-pack/legacy/plugins/ml/public/jobs/{new_job/simple/recognize/create_job/index.js => new_job_new/common/components/index.ts} (60%) create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx rename x-pack/legacy/plugins/ml/public/jobs/new_job_new/{pages/components/time_range_step => common/components}/time_range_picker.tsx (90%) create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__test__/directive.js create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/create_result_callout.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts new file mode 100644 index 0000000000000..cb012c3641f3b --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -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 { Datafeed, Job } from '../../public/jobs/new_job_new/common/job_creator/configs'; +import { SavedObjectAttributes } from '../../../../../../target/types/core/server'; + +export interface ModuleJob { + id: string; + config: Omit; +} + +export interface KibanaObjectConfig extends SavedObjectAttributes { + description: string; + title: string; + version: number; +} + +export interface KibanaObject { + id: string; + title: string; + config: KibanaObjectConfig; +} + +export interface KibanaObjects { + [objectType: string]: KibanaObject[] | undefined; +} + +/** + * Interface for get_module endpoint response. + */ +export interface Module { + id: string; + title: string; + description: string; + type: string; + logoFile: string; + defaultIndexPattern: string; + query: any; + jobs: ModuleJob[]; + datafeeds: Datafeed[]; + kibana: KibanaObjects; +} + +export interface KibanaObjectResponse { + exists?: boolean; + success?: boolean; + id: string; +} + +export interface SetupError { + body: string; + msg: string; + path: string; + query: {}; + response: string; + statusCode: number; +} + +export interface DatafeedResponse { + id: string; + success: boolean; + started: boolean; + error?: SetupError; +} + +export interface JobResponse { + id: string; + success: boolean; + error?: SetupError; +} + +export interface DataRecognizerConfigResponse { + datafeeds: DatafeedResponse[]; + jobs: JobResponse[]; + kibana: { + search: KibanaObjectResponse; + visualization: KibanaObjectResponse; + dashboard: KibanaObjectResponse; + }; +} diff --git a/x-pack/legacy/plugins/ml/common/util/validators.ts b/x-pack/legacy/plugins/ml/common/util/validators.ts index 746b9ac3de080..7e0dd624a52e0 100644 --- a/x-pack/legacy/plugins/ml/common/util/validators.ts +++ b/x-pack/legacy/plugins/ml/common/util/validators.ts @@ -21,3 +21,38 @@ export function maxLengthValidator( } : null; } + +/** + * Provides a validator function for checking against pattern. + * @param pattern + */ +export function patternValidator( + pattern: RegExp +): (value: string) => { pattern: { matchPattern: string } } | null { + return value => + pattern.test(value) + ? null + : { + pattern: { + matchPattern: pattern.toString(), + }, + }; +} + +/** + * Composes multiple validators into a single function + * @param validators + */ +export function composeValidators( + ...validators: Array<(value: string) => { [key: string]: any } | null> +): (value: string) => { [key: string]: any } | null { + return value => { + const validationResult = validators.reduce((acc, validator) => { + return { + ...acc, + ...(validator(value) || {}), + }; + }, {}); + return Object.keys(validationResult).length > 0 ? validationResult : null; + }; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/index.js b/x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts similarity index 81% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/index.js rename to x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts index 8bcbda8d15c48..ffead802bd6f9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/index.js +++ b/x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './create_job'; +export { usePartialState } from './use_partial_state'; diff --git a/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts b/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts new file mode 100644 index 0000000000000..3bb7dbf6578cc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts @@ -0,0 +1,21 @@ +/* + * 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 { useState } from 'react'; + +/** + * Custom hook for partial state update. + */ +export function usePartialState(initialValue: T): [T, (update: Partial) => void] { + const [state, setState] = useState(initialValue); + const setFormStateCallback = (update: Partial) => { + setState({ + ...state, + ...update, + }); + }; + return [state, setFormStateCallback]; +} diff --git a/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js b/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js index 0728f68bb1ba0..664676d18bb81 100644 --- a/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js +++ b/x-pack/legacy/plugins/ml/public/components/json_tooltip/tooltips.js @@ -189,16 +189,6 @@ export const getTooltips = () => { defaultMessage: 'Advanced option. Select to retrieve unfiltered _source document, instead of specified fields.' }) }, - new_job_advanced_settings: { - text: i18n.translate('xpack.ml.tooltips.newJobAdvancedSettingsTooltip', { - defaultMessage: 'Advanced options' - }) - }, - new_job_dedicated_index: { - text: i18n.translate('xpack.ml.tooltips.newJobDedicatedIndexTooltip', { - defaultMessage: 'Select to store results in a separate index for this job.' - }) - }, new_job_enable_model_plot: { text: i18n.translate('xpack.ml.tooltips.newJobEnableModelPlotTooltip', { defaultMessage: 'Select to enable model plot. Stores model information along with results. ' + diff --git a/x-pack/legacy/plugins/ml/public/jobs/index.js b/x-pack/legacy/plugins/ml/public/jobs/index.js index 2baf5c102f4ef..3e60b91aade49 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/index.js @@ -11,6 +11,5 @@ import './new_job/advanced'; import './new_job/simple/single_metric'; import './new_job/simple/multi_metric'; import './new_job/simple/population'; -import './new_job/simple/recognize'; import 'plugins/ml/components/validate_job'; import './new_job_new'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss index 6cd73f18ec608..5fb235e744007 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/_index.scss @@ -10,5 +10,4 @@ @import 'multi_metric/index'; // SASSTODO: Needs some rewriting @import 'population/index'; // SASSTODO: Needs some rewriting -@import 'recognize/index'; // SASSTODO: Needs some rewriting @import 'single_metric/index'; // SASSTODO: Needs some rewriting diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss deleted file mode 100644 index e04ae3b356559..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'create_job/index' \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js deleted file mode 100644 index 2f5302a1411db..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/check_module.js +++ /dev/null @@ -1,58 +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 chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { mlJobService } from '../../../../services/job_service'; -import { ml } from '../../../../services/ml_api_service'; -import { toastNotifications } from 'ui/notify'; - - -// Checks whether the jobs in a data recognizer module have been created. -// Redirects to the Anomaly Explorer to view the jobs if they have been created, -// or the recognizer job wizard for the module if not. -export function checkViewOrCreateJobs(Private, $route, kbnBaseUrl, kbnUrl) { - - return new Promise((resolve, reject) => { - const moduleId = $route.current.params.id; - const indexPatternId = $route.current.params.index; - - // Load the module, and check if the job(s) in the module have been created. - // If so, load the jobs in the Anomaly Explorer. - // Otherwise open the data recognizer wizard for the module. - // Always want to call reject() so as not to load original page. - ml.dataRecognizerModuleJobsExist({ moduleId }) - .then((resp) => { - const basePath = `${chrome.getBasePath()}/app/`; - - if (resp.jobsExist === true) { - const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); - window.location.href = `${basePath}${resultsPageUrl}`; - reject(); - } else { - window.location.href = `${basePath}ml#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; - reject(); - } - - }) - .catch((err) => { - console.log(`Error checking whether jobs in module ${moduleId} exists`, err); - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle', { - defaultMessage: 'Error checking module {moduleId}', - values: { moduleId } - }), - text: i18n.translate('xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription', { - defaultMessage: 'An error occurred trying to check whether the jobs in the module have been created.', - }) - }); - - - kbnUrl.redirect(`/jobs`); - reject(); - }); - }); -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js deleted file mode 100644 index 06c6b90323cf0..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/__tests__/create_job_controller.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - - -describe('ML - Recognize Wizard - Create Job Controller', () => { - beforeEach(() => { - ngMock.module('kibana'); - }); - - it('Initialize Create Job Controller', (done) => { - ngMock.inject(function ($rootScope, $controller, $route) { - // Set up the $route current props required for the tests. - $route.current = { - locals: { - indexPattern: {}, - savedSearch: {} - } - }; - - const scope = $rootScope.$new(); - - expect(() => { - $controller('MlCreateRecognizerJobs', { - $route: { - current: { - params: {} - } - }, - $scope: scope - }); - }).to.not.throwError(); - - expect(scope.ui.formValid).to.eql(true); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss deleted file mode 100644 index 0a19ef71ee215..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_create_jobs.scss +++ /dev/null @@ -1,193 +0,0 @@ -// SASSTODO: This file needs to be rewritten for proper variable usage and size calcs -.recognizer-job-container { - font-size: $euiFontSizeS; - width: 100%; - margin-right: auto; - margin-left: auto; - padding-left: $euiSize; - padding-right: $euiSize; - - - .job-state-info { - margin-bottom: $euiSize; - } - - .form-controls, .charts-container { - margin: 0px; - margin-right: -$euiSize; - - // SASSTODO: Proper selector - & > div { - border: 1px solid $euiBorderColor; - border-top: 0px; - } - - // SASSTODO: Proper selector - & > h4 { - margin-top: 0px; - margin-bottom: 0px; - padding: 10px; - background-color: $euiColorPrimary; - color: $euiColorEmptyShade; - } - - .btn-load-vis { - border-radius: $euiBorderRadius !important; - margin-top: -2px; - } - } - - // SASSTODO: Proper calc - .advanced-button { - min-width: 23px; - } - - .advanced-button-container { - // SASSTODO: Proper selector - label { - cursor: pointer; - display: inline-block; - } - } - - .advanced-group { - padding: 10px; - background-color: $euiColorLightestShade; - - // SASSTODO: Proper selector - label { - font-weight: $euiFontWeightRegular; - } - } - - .charts-container { - margin-left: -$euiSizeS; - margin-bottom: $euiSizeS; - margin-right: 0px; - line-height: 20px; - - .jobs-list, .save-objects-list { - padding: $euiSizeXS; - - .job-container { - border: 1px solid $euiBorderColor; - // padding: 5px; - margin: $euiSizeXS; - border-radius: $euiBorderRadius; - display: flex; - - .labels { - flex: auto; - margin: $euiBorderRadius; - .title { - color: $euiColorPrimary; - } - .exists { - color: $euiColorMediumShade; - font-style: italic; - - // SASSTODO: Proper selector - span { - font-size: $euiFontSizeXS; - } - } - .sub-title { - color: $euiColorDarkShade; - font-size: $euiFontSizeXS; - margin-top: 2px; - font-style: italic; - } - } - - .results { - flex-grow: 0; - flex-shrink: 0; - border-left: 1px solid $euiBorderColor; - background-color: $euiColorLightestShade; - padding-top: $euiSizeXS; - - opacity: 0; - transition: opacity 0.5s; - - .result-box { - display: inline-block; - vertical-align: middle; - text-align: center; - // SASSTODO: Proper calc - width: 50px; - - .result-box-title { - color: $euiColorMediumShade; - font-size: $euiFontSizeXS; - font-style: italic; - margin-bottom: $euiSizeXS; - } - - .result-box-inner { - width: 20px; - height: 20px; - font-size: $euiFontSizeM; - margin: 0px; - align-items: center; - justify-content: center; - display: inline-flex; - vertical-align: top; - } - } - } - } - } - .jobs-list { - .result-box { - margin-right: $euiSizeS; - } - } - } - - .form-section { - margin: 0px 0px 0px 0px; - border-bottom: 1px solid $euiBorderColor; - padding: $euiSizeS; - overflow: hidden; - - // SASSTODO: Proper selector - & > h4, & > h5 { - margin-top: 0px; - display: inline-block; - } - .form-group:last-child { - margin-bottom: 0px; - } - - // SASSTODO: Proper selector - .help-text { - border: 1px solid $euiBorderColor; - padding: 5px; - background-color: $euiColorEmptyShade; - border-radius: $euiSizeXS; - font-size: $euiFontSizeXS; - } - } - - .form-section:last-of-type { - margin: 0px; - border-bottom: 0px solid $euiBorderColor; - } - - .form-section-collapsed { - height: 46px; - } - - - div.validation-error { - color: $euiColorDanger; - font-size: $euiFontSizeXS; - } -} - - -// hide the default es loading indicator bar as it can't be switched off -// for standard es searches using the http header. -.kbnLoadingIndicator__bar { - display: none; -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss deleted file mode 100644 index ea8b32f6673b1..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'create_jobs'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html deleted file mode 100644 index 5839bf7c60d04..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job.html +++ /dev/null @@ -1,362 +0,0 @@ - - - -
-
-
-

- -
-
- - - - - - - -
-
-

-
-
-
-
- -
- -
-
-
-

-
- -
-
-

-
-
- -
-
-
- - {{ ::'xpack.ml.newJob.simple.recognize.jobIdPrefixLabel' | i18n: {defaultMessage: 'Job ID prefix'} }} - - -
{{ ui.validation.checks.jobLabel.message }}
-
-
- - {{ ::'xpack.ml.newJob.simple.recognize.jobGroupsLabel' | i18n: {defaultMessage: 'Job groups'} }} - - -
{{ ui.validation.checks.groupIds.message }}
-
-
-
- - - -
-
- -
-
- -
- -
-
- -
- - - -
- - -
-
-
-

- - -

- -

- - -

- -
- - - - - -
- -
-

- - -

-
- - -
-
-
-
-
-
-
-
-

-
-
-
-
{{formConfig.jobLabel}}{{job.id}}
-
{{job.jobConfig.description}}
-
{{error}}
-
-
-
-
-
- - - - -
-
-
-
-
- - - - -
-
-
-
-
- - - - -
-
-
-
-
-
- -
-

{{ui.kibanaLabels[key]}}

-
-
-
-
- {{obj.title}} - -
-
{{error}}
-
-
-
-
- - - - - -
-
-
-
-
-
-
-
-
-
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js deleted file mode 100644 index 5365078556e96..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_controller.js +++ /dev/null @@ -1,595 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; -import 'ui/angular_ui_select'; -import dateMath from '@elastic/datemath'; -import { isJobIdValid, prefixDatafeedId } from 'plugins/ml/../common/util/job_utils'; -import { getCreateRecognizerJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; -import { SearchItemsProvider, addNewJobToRecentlyAccessed } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; - - -import uiRoutes from 'ui/routes'; -import { checkViewOrCreateJobs } from '../check_module'; -import { checkLicenseExpired } from 'plugins/ml/license/check_license'; -import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from 'plugins/ml/util/index_utils'; -import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { CreateRecognizerJobsServiceProvider } from './create_job_service'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import template from './create_job.html'; -import { toastNotifications } from 'ui/notify'; -import { timefilter } from 'ui/timefilter'; - -uiRoutes - .when('/jobs/new_job/recognize', { - template, - k7Breadcrumbs: getCreateRecognizerJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - } - }); - -uiRoutes - .when('/modules/check_view_or_create', { - template, - resolve: { - checkViewOrCreateJobs - } - }); - - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module - .controller('MlCreateRecognizerJobs', function ($scope, $route, Private) { - - const mlCreateRecognizerJobsService = Private(CreateRecognizerJobsServiceProvider); - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - const msgs = mlMessageBarService; - - const SAVE_STATE = { - NOT_SAVED: 0, - SAVING: 1, - SAVED: 2, - FAILED: 3, - PARTIAL_FAILURE: 4 - }; - - const DATAFEED_STATE = { - NOT_STARTED: 0, - STARTING: 1, - STARTED: 2, - FINISHED: 3, - STOPPING: 4, - FAILED: 5 - }; - - $scope.addNewJobToRecentlyAccessed = addNewJobToRecentlyAccessed; - - $scope.SAVE_STATE = SAVE_STATE; - $scope.DATAFEED_STATE = DATAFEED_STATE; - - $scope.overallState = SAVE_STATE.NOT_SAVED; - - const moduleId = $route.current.params.id; - $scope.moduleId = moduleId; - - const createSearchItems = Private(SearchItemsProvider); - const { - indexPattern, - savedSearch, - combinedQuery } = createSearchItems(); - - const pageTitle = (savedSearch.id !== undefined) ? - i18n.translate('xpack.ml.newJob.simple.recognize.savedSearchPageTitle', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title } - }) : - i18n.translate('xpack.ml.newJob.simple.recognize.indexPatternPageTitle', { - defaultMessage: 'index pattern {indexPatternTitle}', - values: { indexPatternTitle: indexPattern.title } - }); - - $scope.displayQueryWarning = (savedSearch.id !== undefined); - - $scope.hideAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel', { - defaultMessage: 'Hide Advanced' - }); - $scope.showAdvancedButtonAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel', { - defaultMessage: 'Show Advanced' - }); - $scope.showAdvancedAriaLabel = i18n.translate('xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel', { - defaultMessage: 'Show advanced' - }); - - $scope.ui = { - formValid: true, - indexPattern, - pageTitle, - showJobInput: true, - numberOfJobs: 0, - kibanaLabels: { - dashboard: i18n.translate('xpack.ml.newJob.simple.recognize.dashboardsLabel', { - defaultMessage: 'Dashboards' - }), - search: i18n.translate('xpack.ml.newJob.simple.recognize.searchesLabel', { - defaultMessage: 'Searches' - }), - visualization: i18n.translate('xpack.ml.newJob.simple.recognize.visualizationsLabel', { - defaultMessage: 'Visualizations' - }), - }, - validation: { - checks: { - jobLabel: { valid: true }, - groupIds: { valid: true } - }, - }, - }; - - $scope.formConfig = { - indexPattern, - jobLabel: '', - jobGroups: [], - jobs: [], - kibanaObjects: {}, - start: 0, - end: 0, - useFullIndexData: true, - startDatafeedAfterSave: true, - useDedicatedIndex: false, - }; - - $scope.resultsUrl = ''; - - $scope.resetJob = function () { - $scope.overallState = SAVE_STATE.NOT_SAVED; - $scope.formConfig.jobs = []; - $scope.formConfig.kibanaObjects = {}; - - loadJobConfigs(); - }; - - function loadJobConfigs() { - // load the job and datafeed configs as well as the kibana saved objects - // from the recognizer endpoint - ml.getDataRecognizerModule({ moduleId }) - .then(resp => { - // populate the jobs and datafeeds - if (resp.jobs && resp.jobs.length) { - - const tempGroups = {}; - - resp.jobs.forEach((job) => { - $scope.formConfig.jobs.push({ - id: job.id, - jobConfig: job.config, - jobState: SAVE_STATE.NOT_SAVED, - datafeedId: null, - datafeedConfig: {}, - datafeedState: SAVE_STATE.NOT_SAVED, - runningState: DATAFEED_STATE.NOT_STARTED, - errors: [] - }); - $scope.ui.numberOfJobs++; - - // read the groups list from each job and create a deduplicated jobGroups list - if (job.config.groups && job.config.groups.length) { - job.config.groups.forEach((group) => { - tempGroups[group] = null; - }); - } - }); - $scope.formConfig.jobGroups = Object.keys(tempGroups); - - resp.datafeeds.forEach((datafeed) => { - const job = _.find($scope.formConfig.jobs, { id: datafeed.config.job_id }); - if (job !== undefined) { - const datafeedId = mlJobService.getDatafeedId(job.id); - job.datafeedId = datafeedId; - job.datafeedConfig = datafeed.config; - } - }); - } - // populate the kibana saved objects - if (resp.kibana) { - _.each(resp.kibana, (obj, key) => { - $scope.formConfig.kibanaObjects[key] = obj.map((o) => { - return { - id: o.id, - title: o.title, - saveState: SAVE_STATE.NOT_SAVED, - config: o.config, - exists: false, - errors: [], - }; - }); - }); - // check to see if any of the saved objects already exist. - // if they do, they are marked as such and greyed out. - checkIfKibanaObjectsExist($scope.formConfig.kibanaObjects); - } - $scope.$applyAsync(); - }); - } - - // toggle kibana's timepicker - $scope.changeUseFullIndexData = function () { - const shouldEnableTimeFilter = !$scope.formConfig.useFullIndexData; - if (shouldEnableTimeFilter) { - timefilter.enableTimeRangeSelector(); - } else { - timefilter.disableTimeRangeSelector(); - } - $scope.$applyAsync(); - }; - - $scope.changeJobLabelCase = function () { - $scope.formConfig.jobLabel = $scope.formConfig.jobLabel.toLowerCase(); - }; - - $scope.save = function () { - if (validateJobs()) { - msgs.clear(); - $scope.overallState = SAVE_STATE.SAVING; - angular.element('.results').css('opacity', 1); - // wait 500ms for the results section to fade in. - window.setTimeout(() => { - // save jobs,datafeeds and kibana savedObjects - saveDataRecognizerItems() - .then(() => { - // open jobs and save start datafeeds - if ($scope.formConfig.startDatafeedAfterSave) { - startDatafeeds() - .then(() => { - // everything saved correctly and datafeeds have started. - $scope.setOverallState(); - }).catch(() => { - $scope.setOverallState(); - }); - } else { - // datafeeds didn't need to be started so finish - $scope.setOverallState(); - } - }); - }, 500); - } - }; - - // call the the setupModuleConfigs endpoint to create the jobs, datafeeds and saved objects - function saveDataRecognizerItems() { - return new Promise((resolve) => { - // set all jobs, datafeeds and saved objects to a SAVING state - // i.e. display spinners - setAllToSaving(); - - const prefix = $scope.formConfig.jobLabel; - const indexPatternName = $scope.formConfig.indexPattern.title; - const groups = $scope.formConfig.jobGroups; - const useDedicatedIndex = $scope.formConfig.useDedicatedIndex; - const tempQuery = (savedSearch.id === undefined) ? - undefined : combinedQuery; - - ml.setupDataRecognizerConfig({ moduleId, prefix, groups, query: tempQuery, indexPatternName, useDedicatedIndex }) - .then((resp) => { - if (resp.jobs) { - $scope.formConfig.jobs.forEach((job) => { - // check results from saving the jobs - const jobId = `${prefix}${job.id}`; - const jobResult = resp.jobs.find(j => j.id === jobId); - if (jobResult !== undefined) { - if (jobResult.success) { - job.jobState = SAVE_STATE.SAVED; - } else { - job.jobState = SAVE_STATE.FAILED; - if (jobResult.error && jobResult.error.msg) { - job.errors.push(jobResult.error.msg); - } - } - } else { - job.jobState = SAVE_STATE.FAILED; - job.errors.push( - i18n.translate('xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage', { - defaultMessage: 'Could not save job {jobId}', - values: { jobId } - }) - ); - } - - // check results from saving the datafeeds - const datafeedId = prefixDatafeedId(job.datafeedId, prefix); - const datafeedResult = resp.datafeeds.find(d => d.id === datafeedId); - if (datafeedResult !== undefined) { - if (datafeedResult.success) { - job.datafeedState = SAVE_STATE.SAVED; - } else { - job.datafeedState = SAVE_STATE.FAILED; - if (datafeedResult.error && datafeedResult.error.msg) { - job.errors.push(datafeedResult.error.msg); - } - } - } else { - job.datafeedState = SAVE_STATE.FAILED; - job.errors.push( - i18n.translate('xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage', { - defaultMessage: 'Could not save datafeed {datafeedId}', - values: { datafeedId } - }) - ); - } - $scope.$applyAsync(); - }); - } - - if (resp.kibana) { - _.each($scope.formConfig.kibanaObjects, (kibanaObject, objName) => { - kibanaObject.forEach((obj) => { - // check the results from saving the saved objects - const kibanaObjectResult = resp.kibana[objName].find(o => o.id === obj.id); - if (kibanaObjectResult !== undefined) { - if (kibanaObjectResult.success || kibanaObjectResult.success === false && kibanaObjectResult.exists === true) { - obj.saveState = SAVE_STATE.SAVED; - } else { - obj.saveState = SAVE_STATE.FAILED; - if (kibanaObjectResult.error && kibanaObjectResult.error.message) { - obj.errors.push(kibanaObjectResult.error.message); - } - } - } else { - obj.saveState = SAVE_STATE.FAILED; - obj.errors.push( - i18n.translate('xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage', { - defaultMessage: 'Could not save {objName} {objId}', - values: { objName, objId: obj.id } - }) - ); - } - $scope.$applyAsync(); - }); - }); - } - resolve(); - }) - .catch((err) => { - console.log('Error setting up module', err); - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle', { - defaultMessage: 'Error setting up module {moduleId}', - values: { moduleId } - }), - text: i18n.translate('xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription', { - defaultMessage: 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', - values: { - count: $scope.formConfig.jobs.length - } - }) - }); - $scope.overallState = SAVE_STATE.FAILED; - $scope.$applyAsync(); - }); - }); - } - - // loop through all jobs, datafeeds and saved objects and set the save state to SAVING - function setAllToSaving() { - $scope.formConfig.jobs.forEach((j) => { - j.jobState = SAVE_STATE.SAVING; - j.datafeedState = SAVE_STATE.SAVING; - }); - - _.each($scope.formConfig.kibanaObjects, (kibanaObject) => { - kibanaObject.forEach((obj) => { - obj.saveState = SAVE_STATE.SAVING; - }); - }); - $scope.$applyAsync(); - } - - function startDatafeeds() { - return new Promise((resolve, reject) => { - - const jobs = $scope.formConfig.jobs; - const numberOfJobs = jobs.length; - - mlCreateRecognizerJobsService.indexTimeRange($scope.formConfig.indexPattern, $scope.formConfig) - .then((resp) => { - if ($scope.formConfig.useFullIndexData) { - $scope.formConfig.start = resp.start.epoch; - $scope.formConfig.end = resp.end.epoch; - } else { - $scope.formConfig.start = dateMath.parse(timefilter.getTime().from).valueOf(); - $scope.formConfig.end = dateMath.parse(timefilter.getTime().to).valueOf(); - } - let jobsCounter = 0; - let datafeedCounter = 0; - - open(jobs[jobsCounter]); - - function incrementAndOpen(job) { - jobsCounter++; - if (jobsCounter < numberOfJobs) { - open(jobs[jobsCounter]); - } else { - // if the last job failed, reject out of the function - // so it can be caught higher up - if (job.runningState === DATAFEED_STATE.FAILED) { - reject(); - } - } - } - - function open(job) { - if (job.jobState === SAVE_STATE.FAILED) { - // we're skipping over the datafeed, so bump the - // counter up manually so it all tallies at the end. - datafeedCounter++; - job.runningState = DATAFEED_STATE.FAILED; - incrementAndOpen(job); - return; - } - job.runningState = DATAFEED_STATE.STARTING; - const jobId = $scope.formConfig.jobLabel + job.id; - mlJobService.openJob(jobId) - .then(() => { - incrementAndOpen(job); - start(job); - }).catch((err) => { - console.log('Opening job failed', err); - start(job); - job.errors.push(err.message); - incrementAndOpen(job); - }); - } - - function start(job) { - const jobId = $scope.formConfig.jobLabel + job.id; - const datafeedId = prefixDatafeedId(job.datafeedId, $scope.formConfig.jobLabel); - mlCreateRecognizerJobsService.startDatafeed( - datafeedId, - jobId, - $scope.formConfig.start, - $scope.formConfig.end) - .then(() => { - job.runningState = DATAFEED_STATE.STARTED; - datafeedCounter++; - if (datafeedCounter === numberOfJobs) { - resolve(); - } - }) - .catch((err) => { - console.log('Starting datafeed failed', err); - job.errors.push(err.message); - job.runningState = DATAFEED_STATE.FAILED; - reject(err); - }) - .then(() => { - $scope.$applyAsync(); - }); - } - }); - }); - } - - - function checkIfKibanaObjectsExist(kibanaObjects) { - _.each(kibanaObjects, (objects, type) => { - objects.forEach((obj) => { - checkForSavedObject(type, obj) - .then((result) => { - if (result) { - obj.saveState = SAVE_STATE.SAVED; - obj.exists = true; - } - }); - }); - }); - } - - function checkForSavedObject(type, savedObject) { - return new Promise((resolve, reject) => { - let exists = false; - mlCreateRecognizerJobsService.loadExistingSavedObjects(type) - .then((resp) => { - const savedObjects = resp.savedObjects; - savedObjects.forEach((obj) => { - if (savedObject.title === obj.attributes.title) { - exists = true; - savedObject.id = obj.id; - } - }); - resolve(exists); - }).catch((resp) => { - console.log('Could not load saved objects', resp); - reject(resp); - }); - }); - } - - $scope.setOverallState = function () { - const jobIds = []; - const failedJobsCount = $scope.formConfig.jobs.reduce((count, job) => { - if (job.jobState === SAVE_STATE.FAILED || job.datafeedState === SAVE_STATE.FAILED) { - return count + 1; - } else { - jobIds.push(`${$scope.formConfig.jobLabel}${job.id}`); - return count; - } - }, 0); - - if (failedJobsCount) { - if (failedJobsCount === $scope.formConfig.jobs.length) { - $scope.overallState = SAVE_STATE.FAILED; - } else { - $scope.overallState = SAVE_STATE.PARTIAL_FAILURE; - } - } else { - $scope.overallState = SAVE_STATE.SAVED; - } - - $scope.resultsUrl = mlJobService.createResultsUrl( - jobIds, - $scope.formConfig.start, - $scope.formConfig.end, - 'explorer' - ); - - $scope.$applyAsync(); - }; - - - function validateJobs() { - let valid = true; - const checks = $scope.ui.validation.checks; - _.each(checks, (item) => { - item.valid = true; - }); - - // add an extra bit to the job label to avoid hitting the rule which states - // you can't have an id ending in a - or _ - // also to allow an empty label - const label = `${$scope.formConfig.jobLabel}extra`; - - - - if (isJobIdValid(label) === false) { - valid = false; - checks.jobLabel.valid = false; - const msg = i18n.translate('xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription', { - defaultMessage: 'Job label can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + - 'must start and end with an alphanumeric character' - }); - checks.jobLabel.message = msg; - } - $scope.formConfig.jobGroups.forEach(group => { - if (isJobIdValid(group) === false) { - valid = false; - checks.groupIds.valid = false; - const msg = i18n.translate('xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription', { - defaultMessage: 'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' + - 'must start and end with an alphanumeric character' - }); - checks.groupIds.message = msg; - } - }); - return valid; - } - - loadJobConfigs(); - - }); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js deleted file mode 100644 index e0a5c3ef95df1..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/create_job_service.js +++ /dev/null @@ -1,51 +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 { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; - -export function CreateRecognizerJobsServiceProvider(Private) { - - const savedObjectsClient = Private(SavedObjectsClientProvider); - class CreateRecognizerJobsService { - - constructor() {} - - createDatafeed(job, formConfig) { - return new Promise((resolve, reject) => { - const jobId = formConfig.jobLabel + job.id; - - mlJobService.saveNewDatafeed(job.datafeedConfig, jobId) - .then((resp) => { - resolve(resp); - }) - .catch((resp) => { - reject(resp); - }); - }); - } - - startDatafeed(datafeedId, jobId, start, end) { - return mlJobService.startDatafeed(datafeedId, jobId, start, end); - } - - loadExistingSavedObjects(type) { - return savedObjectsClient.find({ type, perPage: 1000 }); - } - - indexTimeRange(indexPattern, formConfig) { - const query = formConfig.combinedQuery; - return ml.getTimeFieldRange({ - index: indexPattern.title, - timeFieldName: indexPattern.timeFieldName, - query - }); - } - } - return new CreateRecognizerJobsService(); -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/index.ts similarity index 60% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/index.ts index 96b3791f4558c..30059abf55cb1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/recognize/create_job/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/index.ts @@ -4,9 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './create_job_controller'; -import './create_job_service'; -import 'plugins/ml/services/mapping_service'; -import 'plugins/ml/components/job_group_select'; +export { JobGroupsInput } from './job_groups_input'; +export { TimeRangePicker, TimeRange } from './time_range_picker'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx new file mode 100644 index 0000000000000..a71a264662fee --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/job_groups_input.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { Validation } from '../job_validator'; +import { tabColor } from '../../../../../common/util/group_color_utils'; +import { Description } from '../../pages/components/job_details_step/components/groups/description'; + +export interface JobGroupsInputProps { + existingGroups: string[]; + selectedGroups: string[]; + onChange: (value: string[]) => void; + validation: Validation; +} + +export const JobGroupsInput: FC = memo( + ({ existingGroups, selectedGroups, onChange, validation }) => { + const options = existingGroups.map(g => ({ + label: g, + color: tabColor(g), + })); + + const selectedOptions = selectedGroups.map(g => ({ + label: g, + color: tabColor(g), + })); + + function onChangeCallback(optionsIn: EuiComboBoxOptionProps[]) { + onChange(optionsIn.map(g => g.label)); + } + + function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionProps[]) { + const normalizedSearchValue = input.trim().toLowerCase(); + + if (!normalizedSearchValue) { + return; + } + + const newGroup: EuiComboBoxOptionProps = { + label: input, + color: tabColor(input), + }; + + if ( + flattenedOptions.findIndex( + option => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + options.push(newGroup); + } + + onChangeCallback([...selectedOptions, newGroup]); + } + + return ( + + + + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/time_range_picker.tsx similarity index 90% rename from x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/time_range_picker.tsx index 26140b9557e90..8c648696a9a7a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/components/time_range_picker.tsx @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import React, { Fragment, FC, useState, useEffect } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; +import moment, { Moment } from 'moment'; import { i18n } from '@kbn/i18n'; -import { EuiDatePickerRange, EuiDatePicker } from '@elastic/eui'; - -import { useKibanaContext } from '../../../../../contexts/kibana'; -import { TimeRange } from './time_range'; +import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; +import { useKibanaContext } from '../../../../contexts/kibana'; const WIDTH = '512px'; +export interface TimeRange { + start: number; + end: number; +} + interface Props { setTimeRange: (d: TimeRange) => void; timeRange: TimeRange; } -type Moment = moment.Moment; - export const TimeRangePicker: FC = ({ setTimeRange, timeRange }) => { const kibanaContext = useKibanaContext(); const dateFormat: string = kibanaContext.kibanaConfig.get('dateFormat'); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts index 5939da0d64abf..688a56208d92b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReactElement } from 'react'; import { basicJobValidation } from '../../../../../common/util/job_utils'; import { newJobLimits } from '../../../new_job/utils/new_job_defaults'; import { JobCreatorType } from '../job_creator'; @@ -22,7 +23,7 @@ export interface ValidationSummary { export interface Validation { valid: boolean; - message?: string; + message?: string | ReactElement; } export interface BasicValidations { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts index f0061b1b9847e..945d22967a65d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts @@ -10,3 +10,5 @@ import './pages/job_type/route'; import './pages/job_type/directive'; import './pages/index_or_search/route'; import './pages/index_or_search/directive'; +import './recognize/route'; +import './recognize/directive'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx index a1f0e1677bedb..a90ceb3ce27d5 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx @@ -4,25 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; +import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { timefilter } from 'ui/timefilter'; import moment from 'moment'; import { WizardNav } from '../wizard_nav'; -import { WIZARD_STEPS, StepProps } from '../step_types'; +import { StepProps, WIZARD_STEPS } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { useKibanaContext } from '../../../../../contexts/kibana'; import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector'; import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; -import { TimeRangePicker } from './time_range_picker'; import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service'; +import { TimeRangePicker, TimeRange } from '../../../common/components'; -export interface TimeRange { - start: number; - end: number; -} export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) => { const kibanaContext = useKibanaContext(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__test__/directive.js new file mode 100644 index 0000000000000..7cbf22bf45ec5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/__test__/directive.js @@ -0,0 +1,45 @@ +/* + * 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 ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +// Import this way to be able to stub/mock functions later on in the tests using sinon. +import * as indexUtils from 'plugins/ml/util/index_utils'; + +describe('ML - Recognize job directive', () => { + let $scope; + let $compile; + let $element; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + $scope.$destroy(); + }); + + it('Initialize Recognize job directive', done => { + sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/create_result_callout.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/create_result_callout.tsx new file mode 100644 index 0000000000000..9d0cf705aaba6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/create_result_callout.tsx @@ -0,0 +1,103 @@ +/* + * 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, { FC, memo } from 'react'; +import { EuiCallOut, EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SAVE_STATE } from '../page'; + +interface CreateResultCalloutProps { + saveState: SAVE_STATE; + resultsUrl: string; + onReset: () => {}; +} + +export const CreateResultCallout: FC = memo( + ({ saveState, resultsUrl, onReset }) => { + if (saveState === SAVE_STATE.NOT_SAVED) { + return null; + } + return ( + <> + {saveState === SAVE_STATE.SAVED && ( + + } + color="success" + iconType="checkInCircleFilled" + /> + )} + {saveState === SAVE_STATE.FAILED && ( + + } + color="danger" + iconType="alert" + /> + )} + {saveState === SAVE_STATE.PARTIAL_FAILURE && ( + + } + color="warning" + iconType="alert" + /> + )} + + + {saveState !== SAVE_STATE.SAVING && ( + + + + + + )} + {(saveState === SAVE_STATE.SAVED || saveState === SAVE_STATE.PARTIAL_FAILURE) && ( + + + + + + )} + + + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx new file mode 100644 index 0000000000000..617f6b31e7e53 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/job_settings_form.tsx @@ -0,0 +1,323 @@ +/* + * 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, { FC, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiAccordion, + EuiButton, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiTextAlign, +} from '@elastic/eui'; +import { ModuleJobUI, SAVE_STATE } from '../page'; +import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; +import { useKibanaContext } from '../../../../contexts/kibana'; +import { + composeValidators, + maxLengthValidator, + patternValidator, +} from '../../../../../common/util/validators'; +import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; +import { usePartialState } from '../../../../components/custom_hooks'; +import { JobGroupsInput, TimeRangePicker, TimeRange } from '../../common/components'; + +export interface JobSettingsFormValues { + jobPrefix: string; + startDatafeedAfterSave: boolean; + useFullIndexData: boolean; + timeRange: TimeRange; + useDedicatedIndex: boolean; + jobGroups: string[]; +} + +interface JobSettingsFormProps { + saveState: SAVE_STATE; + onSubmit: (values: JobSettingsFormValues) => any; + onChange: (values: JobSettingsFormValues) => any; + jobGroups: string[]; + existingGroupIds: string[]; + jobs: ModuleJobUI[]; +} + +export const JobSettingsForm: FC = ({ + onSubmit, + onChange, + saveState, + existingGroupIds, + jobs, + jobGroups, +}) => { + const { from, to } = getTimeFilterRange(); + const { currentIndexPattern: indexPattern } = useKibanaContext(); + + const jobPrefixValidator = composeValidators( + patternValidator(/^([a-z0-9]+[a-z0-9\-_]*)?$/), + maxLengthValidator(JOB_ID_MAX_LENGTH - Math.max(...jobs.map(({ id }) => id.length))) + ); + const groupValidator = composeValidators( + (value: string) => (isJobIdValid(value) ? null : { pattern: true }), + maxLengthValidator(JOB_ID_MAX_LENGTH) + ); + + const [formState, setFormState] = usePartialState({ + jobPrefix: '', + startDatafeedAfterSave: true, + useFullIndexData: true, + timeRange: { + start: from, + end: to, + }, + useDedicatedIndex: false, + jobGroups: [] as string[], + }); + const [validationResult, setValidationResult] = useState>({}); + + const onJobPrefixChange = (value: string) => { + setFormState({ + jobPrefix: value && value.toLowerCase(), + }); + }; + + const handleValidation = () => { + const jobPrefixValidationResult = jobPrefixValidator(formState.jobPrefix); + const jobGroupsValidationResult = formState.jobGroups + .map(group => groupValidator(group)) + .filter(result => result !== null); + + setValidationResult({ + jobPrefix: jobPrefixValidationResult, + jobGroups: jobGroupsValidationResult, + formValid: !jobPrefixValidationResult && jobGroupsValidationResult.length === 0, + }); + }; + + useEffect(() => { + handleValidation(); + }, [formState.jobPrefix, formState.jobGroups]); + + useEffect(() => { + onChange(formState); + }, [formState]); + + useEffect(() => { + setFormState({ jobGroups }); + }, [jobGroups]); + + return ( + <> + + + + + } + description={ + + } + > + + } + describedByIds={['ml_aria_label_new_job_recognizer_job_prefix']} + isInvalid={!!validationResult.jobPrefix} + error={ + <> + {validationResult.jobPrefix && validationResult.jobPrefix.maxLength ? ( +
+ +
+ ) : null} + {validationResult.jobPrefix && validationResult.jobPrefix.pattern && ( +
+ +
+ )} + + } + > + onJobPrefixChange(value)} + isInvalid={!!validationResult.jobPrefix} + /> +
+
+ { + setFormState({ + jobGroups: value, + }); + }} + validation={{ + valid: !validationResult.jobGroups || validationResult.jobGroups.length === 0, + message: ( + + ), + }} + /> + + + + } + checked={formState.startDatafeedAfterSave} + onChange={({ target: { checked } }) => { + setFormState({ + startDatafeedAfterSave: checked, + }); + }} + /> + + + + } + checked={formState.useFullIndexData} + onChange={({ target: { checked } }) => { + setFormState({ + useFullIndexData: checked, + }); + }} + /> + + {!formState.useFullIndexData && ( + <> + + { + setFormState({ + timeRange: value, + }); + }} + timeRange={formState.timeRange} + /> + + )} + + + } + paddingSize="l" + > + + + + } + description={ + + } + > + + { + setFormState({ + useDedicatedIndex: checked, + }); + }} + /> + + + + +
+ + { + onSubmit(formState); + }} + area-label={i18n.translate('xpack.ml.newJob.recognize.createJobButtonAriaLabel', { + defaultMessage: 'Create Job', + })} + > + {saveState === SAVE_STATE.NOT_SAVED && ( + + )} + {saveState === SAVE_STATE.SAVING && ( + + )} + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx new file mode 100644 index 0000000000000..4954b44bf8842 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/kibana_objects.tsx @@ -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 React, { FC, memo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, + EuiText, + EuiTitle, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { KibanaObjectUi } from '../page'; + +export interface KibanaObjectItemProps { + objectType: string; + kibanaObjects: KibanaObjectUi[]; + isSaving: boolean; +} + +export const KibanaObjects: FC = memo( + ({ objectType, kibanaObjects, isSaving }) => { + const kibanaObjectLabels: Record = { + dashboard: i18n.translate('xpack.ml.newJob.recognize.dashboardsLabel', { + defaultMessage: 'Dashboards', + }), + search: i18n.translate('xpack.ml.newJob.recognize.searchesLabel', { + defaultMessage: 'Searches', + }), + visualization: i18n.translate('xpack.ml.newJob.recognize.visualizationsLabel', { + defaultMessage: 'Visualizations', + }), + }; + + return ( + <> + +

{kibanaObjectLabels[objectType]}

+
+ +
    + {kibanaObjects.map(({ id, title, success, exists }, i) => ( +
  • + + + + + + {title} + + + {exists && ( + + + + + + )} + + + {!exists && ( + + + {isSaving ? : null} + {success !== undefined ? ( + + ) : null} + + + )} + + {(kibanaObjects.length === 1 || i < kibanaObjects.length - 1) && ( + + )} +
  • + ))} +
+ + ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx new file mode 100644 index 0000000000000..76028510172e1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/components/module_jobs.tsx @@ -0,0 +1,178 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { ModuleJobUI, SAVE_STATE } from '../page'; + +interface ModuleJobsProps { + jobs: ModuleJobUI[]; + jobPrefix: string; + saveState: SAVE_STATE; +} + +const SETUP_RESULTS_WIDTH = '200px'; + +export const ModuleJobs: FC = ({ jobs, jobPrefix, saveState }) => { + const isSaving = saveState === SAVE_STATE.SAVING; + return ( + <> + +

+ +

+
+ + + + {saveState !== SAVE_STATE.SAVING && saveState !== SAVE_STATE.NOT_SAVED && ( + + + + + + + + + + + + + + + + + + + + + + )} + +
    + {jobs.map(({ id, config: { description }, setupResult, datafeedResult }, i) => ( +
  • + + + + {jobPrefix} + {id} + + + + {description} + + + {setupResult && setupResult.error && ( + + {setupResult.error.msg} + + )} + + {datafeedResult && datafeedResult.error && ( + + {datafeedResult.error.msg} + + )} + + + {isSaving && } + {setupResult && datafeedResult && ( + + + + + + + + + + + + + + )} + + + {i < jobs.length - 1 && } +
  • + ))} +
+ + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx new file mode 100644 index 0000000000000..593db836b5ddb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx @@ -0,0 +1,69 @@ +/* + * 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 ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; +import { IndexPatterns } from 'ui/index_patterns'; + +import { I18nContext } from 'ui/i18n'; +import { IPrivate } from 'ui/private'; +import { InjectorService } from '../../../../common/types/angular'; + +import { SearchItemsProvider } from '../../new_job/utils/new_job_utils'; +import { Page } from './page'; + +import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; + +module.directive('mlRecognizePage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // remove time picker from top of page + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kibanaConfig = $injector.get('config'); + const Private = $injector.get('Private'); + const $route = $injector.get('$route'); + + const moduleId = $route.current.params.id; + const existingGroupIds: string[] = $route.current.locals.existingJobsAndGroups.groupIds; + + const createSearchItems = Private(SearchItemsProvider); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kbnBaseUrl, + kibanaConfig, + }; + + ReactDOM.render( + + + {React.createElement(Page, { moduleId, existingGroupIds })} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx new file mode 100644 index 0000000000000..2c7600dcb99b2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/page.tsx @@ -0,0 +1,314 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, Fragment, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiTitle, + EuiPageHeaderSection, + EuiPageHeader, + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiSpacer, + EuiCallOut, + EuiPanel, +} from '@elastic/eui'; +import { toastNotifications } from 'ui/notify'; +import { merge, flatten } from 'lodash'; +import { ml } from '../../../services/ml_api_service'; +import { useKibanaContext } from '../../../contexts/kibana'; +import { + DatafeedResponse, + DataRecognizerConfigResponse, + JobResponse, + KibanaObject, + KibanaObjectResponse, + Module, + ModuleJob, +} from '../../../../common/types/modules'; +import { mlJobService } from '../../../services/job_service'; +import { CreateResultCallout } from './components/create_result_callout'; +import { KibanaObjects } from './components/kibana_objects'; +import { ModuleJobs } from './components/module_jobs'; +import { checkForSavedObjects } from './resolvers'; +import { JobSettingsForm, JobSettingsFormValues } from './components/job_settings_form'; +import { TimeRange } from '../common/components'; + +export interface ModuleJobUI extends ModuleJob { + datafeedResult?: DatafeedResponse; + setupResult?: JobResponse; +} + +export type KibanaObjectUi = KibanaObject & KibanaObjectResponse; + +export interface KibanaObjects { + [objectType: string]: KibanaObjectUi[]; +} + +interface PageProps { + moduleId: string; + existingGroupIds: string[]; +} + +export enum SAVE_STATE { + NOT_SAVED, + SAVING, + SAVED, + FAILED, + PARTIAL_FAILURE, +} + +export const Page: FC = ({ moduleId, existingGroupIds }) => { + // #region State + const [jobPrefix, setJobPrefix] = useState(''); + const [jobs, setJobs] = useState([]); + const [jobGroups, setJobGroups] = useState([]); + const [kibanaObjects, setKibanaObjects] = useState({}); + const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); + const [resultsUrl, setResultsUrl] = useState(''); + // #endregion + + const { + currentSavedSearch: savedSearch, + currentIndexPattern: indexPattern, + combinedQuery, + } = useKibanaContext(); + const pageTitle = + savedSearch.id !== undefined + ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: savedSearch.title }, + }) + : i18n.translate('xpack.ml.newJob.recognize.indexPatternPageTitle', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: indexPattern.title }, + }); + const displayQueryWarning = savedSearch.id !== undefined; + const tempQuery = savedSearch.id === undefined ? undefined : combinedQuery; + + const loadModule = async () => { + try { + const response: Module = await ml.getDataRecognizerModule({ moduleId }); + setJobs(response.jobs); + + const kibanaObjectsResult = await checkForSavedObjects(response.kibana as KibanaObjects); + setKibanaObjects(kibanaObjectsResult); + + setJobGroups([ + ...new Set(flatten(response.jobs.map(({ config: { groups = [] } }) => groups))), + ]); + setSaveState(SAVE_STATE.NOT_SAVED); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }; + + const getTimeRange = async ( + useFullIndexData: boolean, + timeRange: TimeRange + ): Promise => { + if (useFullIndexData) { + const { start, end } = await ml.getTimeFieldRange({ + index: indexPattern.title, + timeFieldName: indexPattern.timeFieldName, + query: combinedQuery, + }); + return { + start: start.epoch, + end: end.epoch, + }; + } else { + return Promise.resolve(timeRange); + } + }; + + useEffect(() => { + loadModule(); + }, []); + + const save = async (formValues: JobSettingsFormValues) => { + setSaveState(SAVE_STATE.SAVING); + const { + jobPrefix: resultJobPrefix, + jobGroups: resultJobGroups, + startDatafeedAfterSave, + useDedicatedIndex, + useFullIndexData, + timeRange, + } = formValues; + + const resultTimeRange = await getTimeRange(useFullIndexData, timeRange); + + try { + const response: DataRecognizerConfigResponse = await ml.setupDataRecognizerConfig({ + moduleId, + prefix: resultJobPrefix, + groups: resultJobGroups, + query: tempQuery, + indexPatternName: indexPattern.title, + useDedicatedIndex, + startDatafeed: startDatafeedAfterSave, + ...resultTimeRange, + }); + const { datafeeds: datafeedsResponse, jobs: jobsResponse, kibana: kibanaResponse } = response; + + setJobs( + jobs.map(job => { + return { + ...job, + datafeedResult: datafeedsResponse.find(({ id }) => id.endsWith(job.id)), + setupResult: jobsResponse.find(({ id }) => id === resultJobPrefix + job.id), + }; + }) + ); + setKibanaObjects(merge(kibanaObjects, kibanaResponse)); + setResultsUrl( + mlJobService.createResultsUrl( + jobsResponse.filter(({ success }) => success).map(({ id }) => id), + resultTimeRange.start, + resultTimeRange.end, + 'explorer' + ) + ); + const failedJobsCount = jobsResponse.reduce((count, { success }) => { + return success ? count : count + 1; + }, 0); + setSaveState( + failedJobsCount === 0 + ? SAVE_STATE.SAVED + : failedJobsCount === jobs.length + ? SAVE_STATE.FAILED + : SAVE_STATE.PARTIAL_FAILURE + ); + } catch (e) { + setSaveState(SAVE_STATE.FAILED); + // eslint-disable-next-line no-console + console.error('Error setting up module', e); + toastNotifications.addDanger({ + title: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningTitle', { + defaultMessage: 'Error setting up module {moduleId}', + values: { moduleId }, + }), + text: i18n.translate('xpack.ml.newJob.recognize.moduleSetupFailedWarningDescription', { + defaultMessage: + 'An error occurred trying to create the {count, plural, one {job} other {jobs}} in the module.', + values: { + count: jobs.length, + }, + }), + }); + } + }; + + const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); + + return ( + + + + + +

+ +

+
+
+
+ + {displayQueryWarning && ( + <> + + } + color="warning" + iconType="alert" + > + + + + + + + )} + + + + + +

+ +

+
+ + + + {isFormVisible && ( + { + setJobPrefix(formValues.jobPrefix); + }} + existingGroupIds={existingGroupIds} + saveState={saveState} + jobs={jobs} + jobGroups={jobGroups} + /> + )} + +
+
+ + + + + {Object.keys(kibanaObjects).length > 0 && ( + <> + + + {Object.keys(kibanaObjects).map((objectType, i) => ( + + + {i < Object.keys(kibanaObjects).length - 1 && } + + ))} + + + )} + +
+ +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts new file mode 100644 index 0000000000000..d92ec7152adf8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/resolvers.ts @@ -0,0 +1,95 @@ +/* + * 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 chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; +import { toastNotifications } from 'ui/notify'; +import { IPrivate } from 'ui/private'; +import { mlJobService } from '../../../services/job_service'; +import { ml } from '../../../services/ml_api_service'; +import { KibanaObjects } from './page'; + +/** + * Checks whether the jobs in a data recognizer module have been created. + * Redirects to the Anomaly Explorer to view the jobs if they have been created, + * or the recognizer job wizard for the module if not. + */ +export function checkViewOrCreateJobs( + Private: IPrivate, + $route: any, + kbnBaseUrl: string, + kbnUrl: any +) { + return new Promise((resolve, reject) => { + const moduleId = $route.current.params.id; + const indexPatternId = $route.current.params.index; + + // Load the module, and check if the job(s) in the module have been created. + // If so, load the jobs in the Anomaly Explorer. + // Otherwise open the data recognizer wizard for the module. + // Always want to call reject() so as not to load original page. + ml.dataRecognizerModuleJobsExist({ moduleId }) + .then((resp: any) => { + const basePath = `${chrome.getBasePath()}/app/`; + + if (resp.jobsExist === true) { + const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); + window.location.href = `${basePath}${resultsPageUrl}`; + reject(); + } else { + window.location.href = `${basePath}ml#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; + reject(); + } + }) + .catch((err: Error) => { + // eslint-disable-next-line no-console + console.error(`Error checking whether jobs in module ${moduleId} exists`, err); + toastNotifications.addWarning({ + title: i18n.translate('xpack.ml.newJob.recognize.moduleCheckJobsExistWarningTitle', { + defaultMessage: 'Error checking module {moduleId}', + values: { moduleId }, + }), + text: i18n.translate('xpack.ml.newJob.recognize.moduleCheckJobsExistWarningDescription', { + defaultMessage: + 'An error occurred trying to check whether the jobs in the module have been created.', + }), + }); + + kbnUrl.redirect(`/jobs`); + reject(); + }); + }); +} + +/** + * Gets kibana objects with an existence check. + */ +export const checkForSavedObjects = async (objects: KibanaObjects): Promise => { + const savedObjectsClient = chrome.getSavedObjectsClient(); + try { + return await Object.keys(objects).reduce(async (prevPromise, type) => { + const acc = await prevPromise; + const { savedObjects } = await savedObjectsClient.find({ + type, + perPage: 1000, + }); + + acc[type] = objects[type].map(obj => { + const find = savedObjects.find(savedObject => savedObject.attributes.title === obj.title); + return { + ...obj, + exists: !!find, + id: (!!find && find.id) || obj.id, + }; + }); + return Promise.resolve(acc); + }, Promise.resolve({} as KibanaObjects)); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Could not load saved objects', e); + } + return Promise.resolve(objects); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts new file mode 100644 index 0000000000000..3d18d39d93734 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/route.ts @@ -0,0 +1,36 @@ +/* + * 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 uiRoutes from 'ui/routes'; +// @ts-ignore +import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +// @ts-ignore +import { checkLicenseExpired } from 'plugins/ml/license/check_license'; +import { getCreateRecognizerJobBreadcrumbs } from '../../breadcrumbs'; +import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; +import { mlJobService } from '../../../services/job_service'; +import { checkViewOrCreateJobs } from './resolvers'; + +uiRoutes.when('/jobs/new_job/recognize', { + template: '', + k7Breadcrumbs: getCreateRecognizerJobBreadcrumbs, + resolve: { + CheckLicense: checkLicenseExpired, + privileges: checkCreateJobsPrivilege, + indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, + checkMlNodesAvailable, + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + }, +}); + +uiRoutes.when('/modules/check_view_or_create', { + template: '', + resolve: { + checkViewOrCreateJobs, + }, +}); diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts index 3b60a7af505d7..c2d3882580be1 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts @@ -11,7 +11,7 @@ export interface ExistingJobsAndGroups { declare interface JobService { currentJob: any; - createResultsUrlForJobs: () => string; + createResultsUrlForJobs: (jobs: any[], target: string) => string; tempJobCloningObjects: { job: any; skipTimeRangeStep: boolean; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index f8b3fd468d598..58255f60edac2 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -52,7 +52,9 @@ declare interface Ml { getDatafeedStats(obj: object): Promise; esSearch(obj: object): any; getIndices(): Promise; - + dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; + getDataRecognizerModule(obj: { moduleId: string }): Promise; + setupDataRecognizerConfig(obj: object): Promise; getTimeFieldRange(obj: object): Promise; calculateModelMemoryLimit(obj: object): Promise<{ modelMemoryLimit: string }>; calendars(): Promise< diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js index 577499fd9eb60..c6a60a9eff7da 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js @@ -289,7 +289,10 @@ export const ml = { 'groups', 'indexPatternName', 'query', - 'useDedicatedIndex' + 'useDedicatedIndex', + 'startDatafeed', + 'start', + 'end' ]); return http({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b37e7747c22f6..434597c1e857a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6876,75 +6876,6 @@ "xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage": "データフィードを開始できませんでした", "xpack.ml.newJob.simple.postSaveOptions.createWatchButtonAriaLabel": "ウォッチを作成", "xpack.ml.newJob.simple.postSaveOptions.createWatchForRealTimeJobLabel": "リアルタイムジョブのウォッチを作成", - "xpack.ml.newJob.simple.recognize.advancedLabel": "高度な設定", - "xpack.ml.newJob.simple.recognize.advancedSettingsAriaLabel": "高度な設定", - "xpack.ml.newJob.simple.recognize.alreadyExistsLabel": "(既に存在します)", - "xpack.ml.newJob.simple.recognize.analysisRunningAriaLabel": "分析を実行中", - "xpack.ml.newJob.simple.recognize.analysisRunningLabel": "分析を実行中", - "xpack.ml.newJob.simple.recognize.createJobButtonAriaLabel": "ジョブの作成", - "xpack.ml.newJob.simple.recognize.createJobButtonLabel": "{numberOfJobs, plural, zero {Job} one {Job} other {Jobs}} を作成", - "xpack.ml.newJob.simple.recognize.dashboardsLabel": "ダッシュボード", - "xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage": "データフィード {datafeedId} を保存できませんでした", - "xpack.ml.newJob.simple.recognize.datafeed.notSavedAriaLabel": "保存されていません", - "xpack.ml.newJob.simple.recognize.datafeed.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.datafeed.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.datafeed.savingAriaLabel": "保存中", - "xpack.ml.newJob.simple.recognize.datafeedLabel": "データフィード", - "xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel": "高度な設定を非表示", - "xpack.ml.newJob.simple.recognize.indexPatternPageTitle": "インデックスパターン {indexPatternTitle}", - "xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage": "ジョブ {jobId} を保存できませんでした", - "xpack.ml.newJob.simple.recognize.job.notSavedAriaLabel": "保存されていません", - "xpack.ml.newJob.simple.recognize.job.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.job.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.job.savingAriaLabel": "保存中", - "xpack.ml.newJob.simple.recognize.jobDetailsTitle": "ジョブの詳細", - "xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription": "ジョブグループ名にはアルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーラインが使用でき、最初と最後を英数字にする必要があります", - "xpack.ml.newJob.simple.recognize.jobGroupsLabel": "ジョブグループ", - "xpack.ml.newJob.simple.recognize.jobIdPrefixLabel": "ジョブ ID の接頭辞", - "xpack.ml.newJob.simple.recognize.jobIdPrefixPlaceholder": "ジョブ ID の接頭辞", - "xpack.ml.newJob.simple.recognize.jobLabel": "ジョブ", - "xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription": "ジョブラベルにはアルファベットの小文字 (a-z と 0-9)、ハイフンまたはアンダーラインが使用でき、最初と最後を英数字にする必要があります", - "xpack.ml.newJob.simple.recognize.jobsCreatedTitle": "ジョブが作成されました", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonAriaLabel": "リセット", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonLabel": "ジョブの作成に失敗", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.jobsCreationFailedTitle": "ジョブの作成に失敗", - "xpack.ml.newJob.simple.recognize.jobSettingsTitle": "ジョブ設定", - "xpack.ml.newJob.simple.recognize.jobsTitle": "ジョブ", - "xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage": "{objName} {objId} を保存できませんでした", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription": "モジュールのジョブがクラッシュしたか確認する際にエラーが発生しました。", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle": "モジュール {moduleId} の確認中にエラーが発生", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription": "モジュールでの{count, plural, one {ジョブ} other {件のジョブ}}の作成中にエラーが発生しました。", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle": "モジュール {moduleId} のセットアップ中にエラーが発生", - "xpack.ml.newJob.simple.recognize.newJobFromTitle": "{pageTitle} からの新規ジョブ", - "xpack.ml.newJob.simple.recognize.results.alreadySavedAriaLabel": "既に保存済み", - "xpack.ml.newJob.simple.recognize.results.notSavedAriaLabel": "保存されていません", - "xpack.ml.newJob.simple.recognize.results.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.results.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.results.savingAriaLabel": "保存中", - "xpack.ml.newJob.simple.recognize.running.notStartedAriaLabel": "未開始", - "xpack.ml.newJob.simple.recognize.running.startedAriaLabel": "開始済み", - "xpack.ml.newJob.simple.recognize.running.startFailedAriaLabel": "開始に失敗", - "xpack.ml.newJob.simple.recognize.running.startingAriaLabel": "開始中", - "xpack.ml.newJob.simple.recognize.runningLabel": "実行中", - "xpack.ml.newJob.simple.recognize.savedAriaLabel": "保存されました", - "xpack.ml.newJob.simple.recognize.savedSearchPageTitle": "保存された検索 {savedSearchTitle}", - "xpack.ml.newJob.simple.recognize.searchesLabel": "検索", - "xpack.ml.newJob.simple.recognize.searchWillBeOverwrittenLabel": "検索は上書きされます", - "xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel": "高度な設定を表示", - "xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel": "高度な設定を表示", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonAriaLabel": "リセット", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonLabel": "リセット", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.saveFailedAriaLabel": "保存に失敗", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailedTitle": "一部のジョブの作成に失敗しました", - "xpack.ml.newJob.simple.recognize.startDatafeedAfterSaveLabel": "保存後データフィードを開始", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexAriaLabel": "専用インデックスを使用", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexLabel": "専用インデックスを使用", - "xpack.ml.newJob.simple.recognize.useFullDataLabel": "完全な {indexPatternTitle} データを使用", - "xpack.ml.newJob.simple.recognize.usingSavedSearchDescription": "保存された検索を使用すると、データフィードで使用されるクエリが、{moduleId} モジュールでデフォルトで提供されるものと異なるものになります。", - "xpack.ml.newJob.simple.recognize.viewResultsAriaLabel": "結果を表示", - "xpack.ml.newJob.simple.recognize.viewResultsLinkText": "結果を表示", - "xpack.ml.newJob.simple.recognize.visualizationsLabel": "ビジュアライゼーション", "xpack.ml.newJob.simple.singleMetric.advancedConfigurationLinkText": "高度なジョブの構成に移動", "xpack.ml.newJob.simple.singleMetric.advancedLabel": "高度な設定", "xpack.ml.newJob.simple.singleMetric.aggregationLabel": "集約", @@ -7308,7 +7239,6 @@ "xpack.ml.tooltips.newCustomUrlValueTooltip": "ドリルスルーリンクの URL です。分析されたフィールドの文字列置換をサポートします (例: {hostnameParam})。", "xpack.ml.tooltips.newFilterRuleActionTooltip": "ルールアクションを指定する文字列です。初めは有効なオプションが「filter_results」だけですが、プロビジョニングにより「disable_modeling」のようなアクションに拡張されます。", "xpack.ml.tooltips.newFilterTargetFieldNameTooltip": "フィールド名を入力する文字列です。フィルターは ruleConditions が適用される targetFieldName 値のすべての結果に適用されます。未入力の場合、フィルターは ruleConditions が適用される結果にのみ適用されます。", - "xpack.ml.tooltips.newJobAdvancedSettingsTooltip": "高度なオプション", "xpack.ml.tooltips.newJobBucketSpanTooltip": "時系列分析の間隔です。", "xpack.ml.tooltips.newJobCategorizationFieldNameTooltip": "オプション。非構造化ログデータの分析用。テキストデータタイプの使用をお勧めします。", "xpack.ml.tooltips.newJobCategorizationFiltersTooltip": "オプション。カテゴリー分けフィールドに正規表現を適用します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5ed0b97077298..971eabfdd83fb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6879,75 +6879,6 @@ "xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage": "无法开始数据馈送:", "xpack.ml.newJob.simple.postSaveOptions.createWatchButtonAriaLabel": "创建监视", "xpack.ml.newJob.simple.postSaveOptions.createWatchForRealTimeJobLabel": "为实时作业创建监视", - "xpack.ml.newJob.simple.recognize.advancedLabel": "高级", - "xpack.ml.newJob.simple.recognize.advancedSettingsAriaLabel": "高级设置", - "xpack.ml.newJob.simple.recognize.alreadyExistsLabel": "(已存在)", - "xpack.ml.newJob.simple.recognize.analysisRunningAriaLabel": "分析正在运行", - "xpack.ml.newJob.simple.recognize.analysisRunningLabel": "分析正在运行", - "xpack.ml.newJob.simple.recognize.createJobButtonAriaLabel": "创建作业", - "xpack.ml.newJob.simple.recognize.createJobButtonLabel": "创建 {numberOfJobs, plural, zero { 个作业} one {Job} other {Jobs}}", - "xpack.ml.newJob.simple.recognize.dashboardsLabel": "仪表板", - "xpack.ml.newJob.simple.recognize.datafeed.couldNotSaveDatafeedErrorMessage": "无法保存数据馈送 {datafeedId}", - "xpack.ml.newJob.simple.recognize.datafeed.notSavedAriaLabel": "未保存", - "xpack.ml.newJob.simple.recognize.datafeed.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.datafeed.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.datafeed.savingAriaLabel": "正在保存", - "xpack.ml.newJob.simple.recognize.datafeedLabel": "数据馈送", - "xpack.ml.newJob.simple.recognize.hideAdvancedButtonAriaLabel": "隐藏“高级”", - "xpack.ml.newJob.simple.recognize.indexPatternPageTitle": "索引模式 {indexPatternTitle}", - "xpack.ml.newJob.simple.recognize.job.couldNotSaveJobErrorMessage": "无法保存作业 {jobId}", - "xpack.ml.newJob.simple.recognize.job.notSavedAriaLabel": "未保存", - "xpack.ml.newJob.simple.recognize.job.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.job.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.job.savingAriaLabel": "正在保存", - "xpack.ml.newJob.simple.recognize.jobDetailsTitle": "作业详情", - "xpack.ml.newJob.simple.recognize.jobGroupAllowedCharactersDescription": "作业组名称可以包含小写字母数字(a-z 和 0-9)、连字符或下划线;必须以字母数字字符开头和结尾", - "xpack.ml.newJob.simple.recognize.jobGroupsLabel": "作业组", - "xpack.ml.newJob.simple.recognize.jobIdPrefixLabel": "作业 ID 前缀", - "xpack.ml.newJob.simple.recognize.jobIdPrefixPlaceholder": "作业 ID 前缀", - "xpack.ml.newJob.simple.recognize.jobLabel": "作业", - "xpack.ml.newJob.simple.recognize.jobLabelAllowedCharactersDescription": "作业标签可以包含小写字母数字(a-z 和 0-9)、连字符或下划线;必须以字母数字字符开头和结尾", - "xpack.ml.newJob.simple.recognize.jobsCreatedTitle": "已创建作业", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonAriaLabel": "重置", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.resetButtonLabel": "作业创建失败", - "xpack.ml.newJob.simple.recognize.jobsCreationFailed.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.jobsCreationFailedTitle": "作业创建失败", - "xpack.ml.newJob.simple.recognize.jobSettingsTitle": "作业设置", - "xpack.ml.newJob.simple.recognize.jobsTitle": "作业", - "xpack.ml.newJob.simple.recognize.kibanaObject.couldNotSaveErrorMessage": "无法保存 {objName} {objId}", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningDescription": "尝试检查模块中的作业是否已创建时出错。", - "xpack.ml.newJob.simple.recognize.moduleCheckJobsExistWarningTitle": "检查模式 {moduleId} 时出错", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningDescription": "尝试在模块中创建{count, plural, one {该作业} other {这些作业}}时出错。", - "xpack.ml.newJob.simple.recognize.moduleSetupFailedWarningTitle": "设置模块 {moduleId} 时出错", - "xpack.ml.newJob.simple.recognize.newJobFromTitle": "来自 {pageTitle} 的新作业", - "xpack.ml.newJob.simple.recognize.results.alreadySavedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.results.notSavedAriaLabel": "未保存", - "xpack.ml.newJob.simple.recognize.results.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.results.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.results.savingAriaLabel": "正在保存", - "xpack.ml.newJob.simple.recognize.running.notStartedAriaLabel": "未开始", - "xpack.ml.newJob.simple.recognize.running.startedAriaLabel": "已开始", - "xpack.ml.newJob.simple.recognize.running.startFailedAriaLabel": "启动失败", - "xpack.ml.newJob.simple.recognize.running.startingAriaLabel": "正在启动", - "xpack.ml.newJob.simple.recognize.runningLabel": "正在运行", - "xpack.ml.newJob.simple.recognize.savedAriaLabel": "已保存", - "xpack.ml.newJob.simple.recognize.savedSearchPageTitle": "已保存搜索 {savedSearchTitle}", - "xpack.ml.newJob.simple.recognize.searchesLabel": "搜索", - "xpack.ml.newJob.simple.recognize.searchWillBeOverwrittenLabel": "搜索将被覆盖", - "xpack.ml.newJob.simple.recognize.showAdvancedAriaLabel": "显示“高级”", - "xpack.ml.newJob.simple.recognize.showAdvancedButtonAriaLabel": "显示“高级”", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonAriaLabel": "重置", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.resetButtonLabel": "重置", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailed.saveFailedAriaLabel": "保存失败", - "xpack.ml.newJob.simple.recognize.someJobsCreationFailedTitle": "部分作业未能创建", - "xpack.ml.newJob.simple.recognize.startDatafeedAfterSaveLabel": "保存后开始数据馈送", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexAriaLabel": "使用专用索引", - "xpack.ml.newJob.simple.recognize.useDedicatedIndexLabel": "使用专用索引", - "xpack.ml.newJob.simple.recognize.useFullDataLabel": "使用完整的 {indexPatternTitle} 数据", - "xpack.ml.newJob.simple.recognize.usingSavedSearchDescription": "使用保存的搜索意味着在数据馈送中使用的查询会与我们在 {moduleId} 模块中提供的默认查询不同。", - "xpack.ml.newJob.simple.recognize.viewResultsAriaLabel": "查看结果", - "xpack.ml.newJob.simple.recognize.viewResultsLinkText": "查看结果", - "xpack.ml.newJob.simple.recognize.visualizationsLabel": "可视化", "xpack.ml.newJob.simple.singleMetric.advancedConfigurationLinkText": "转到高级作业配置", "xpack.ml.newJob.simple.singleMetric.advancedLabel": "高级", "xpack.ml.newJob.simple.singleMetric.aggregationLabel": "聚合", @@ -7311,7 +7242,6 @@ "xpack.ml.tooltips.newCustomUrlValueTooltip": "钻取链接的 URL。支持已分析字段(例如 {hostnameParam})的字符串替代。", "xpack.ml.tooltips.newFilterRuleActionTooltip": "指定规则操作的字符串。初始,唯一有效选项是“filter_results”,但其通过配置可扩展操作,例如“disable_modeling”。", "xpack.ml.tooltips.newFilterTargetFieldNameTooltip": "应为字段名称的字符串。筛选将应用于 ruleConditions 应用的 targetFieldName 值的所有结果。为空时,筛选将应用于 ruleConditions 适用的结果。", - "xpack.ml.tooltips.newJobAdvancedSettingsTooltip": "高级选项", "xpack.ml.tooltips.newJobBucketSpanTooltip": "时间序列分析的时间间隔。", "xpack.ml.tooltips.newJobCategorizationFieldNameTooltip": "(可选)用于分析非结构化日志数据。建议使用文本数据类型。", "xpack.ml.tooltips.newJobCategorizationFiltersTooltip": "(可选)将正则表达式应用于分类字段", From c933d2dde69772fd1a53f880f0beb5529ad623b9 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 10 Oct 2019 12:14:25 +0200 Subject: [PATCH 06/92] Expressions np ready (#47565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 generalize service getters and setters in expressio * feat: 🎸 create NP-ready plugin for expressions * refactor: 💡 flatten expressions plugin and remove service * chore: 🤖 re-export legacy expressions service dir for Lens * refactor: 💡 don't import renderer registry statically * test: 💍 mock expressions service renderers registry in Mocha --- .../{expressions/index.ts => expressions.ts} | 4 +- .../core_plugins/expressions/public/index.ts | 12 +-- .../core_plugins/expressions/public/legacy.ts | 11 +-- .../core_plugins/expressions/public/mocks.ts | 19 +---- .../expressions/public/np_ready/kibana.json | 9 +++ .../lib => np_ready/public}/execute.test.ts | 4 +- .../lib => np_ready/public}/execute.ts | 4 +- .../public}/expression_renderer.tsx | 15 +--- .../public/np_ready/public/index.ts | 29 +++++++ .../services.ts => np_ready/public/legacy.ts} | 30 +++---- .../lib => np_ready/public}/loader.test.ts | 34 ++++---- .../lib => np_ready/public}/loader.ts | 4 +- .../public/np_ready/public/mocks.ts | 80 +++++++++++++++++++ .../public/plugin.ts} | 64 +++++++++------ .../lib => np_ready/public}/render.test.ts | 23 +++--- .../lib => np_ready/public}/render.ts | 10 ++- .../public/np_ready/public/services.ts | 43 ++++++++++ .../_types.ts => np_ready/public/types.ts} | 0 .../core_plugins/expressions/public/plugin.ts | 54 +------------ .../new_platform/new_platform.karma_mock.js | 6 ++ 20 files changed, 263 insertions(+), 192 deletions(-) rename src/legacy/core_plugins/expressions/public/{expressions/index.ts => expressions.ts} (76%) create mode 100644 src/legacy/core_plugins/expressions/public/np_ready/kibana.json rename src/legacy/core_plugins/expressions/public/{expressions/lib => np_ready/public}/execute.test.ts (97%) rename src/legacy/core_plugins/expressions/public/{expressions/lib => np_ready/public}/execute.ts (98%) rename src/legacy/core_plugins/expressions/public/{expressions => np_ready/public}/expression_renderer.tsx (91%) create mode 100644 src/legacy/core_plugins/expressions/public/np_ready/public/index.ts rename src/legacy/core_plugins/expressions/public/{expressions/services.ts => np_ready/public/legacy.ts} (56%) rename src/legacy/core_plugins/expressions/public/{expressions/lib => np_ready/public}/loader.test.ts (86%) rename src/legacy/core_plugins/expressions/public/{expressions/lib => np_ready/public}/loader.ts (98%) create mode 100644 src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts rename src/legacy/core_plugins/expressions/public/{expressions/expressions_service.ts => np_ready/public/plugin.ts} (51%) rename src/legacy/core_plugins/expressions/public/{expressions/lib => np_ready/public}/render.test.ts (86%) rename src/legacy/core_plugins/expressions/public/{expressions/lib => np_ready/public}/render.ts (92%) create mode 100644 src/legacy/core_plugins/expressions/public/np_ready/public/services.ts rename src/legacy/core_plugins/expressions/public/{expressions/lib/_types.ts => np_ready/public/types.ts} (100%) diff --git a/src/legacy/core_plugins/expressions/public/expressions/index.ts b/src/legacy/core_plugins/expressions/public/expressions.ts similarity index 76% rename from src/legacy/core_plugins/expressions/public/expressions/index.ts rename to src/legacy/core_plugins/expressions/public/expressions.ts index 707bcd3cd883e..67ca3492f8244 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/index.ts +++ b/src/legacy/core_plugins/expressions/public/expressions.ts @@ -17,6 +17,4 @@ * under the License. */ -export { ExpressionsService, ExpressionsSetup, ExpressionsStart } from './expressions_service'; -export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; -export { IInterpreterRenderFunction } from './lib/_types'; +export * from './index'; diff --git a/src/legacy/core_plugins/expressions/public/index.ts b/src/legacy/core_plugins/expressions/public/index.ts index 6c6e72fb2aa5a..e0c4e13db6b17 100644 --- a/src/legacy/core_plugins/expressions/public/index.ts +++ b/src/legacy/core_plugins/expressions/public/index.ts @@ -17,14 +17,4 @@ * under the License. */ -// /// Define plugin function -import { ExpressionsPlugin as Plugin } from './plugin'; - -export function plugin() { - return new Plugin(); -} - -// /// Export types & static code - -/** @public types */ -export * from './expressions'; +export * from './np_ready/public/index'; diff --git a/src/legacy/core_plugins/expressions/public/legacy.ts b/src/legacy/core_plugins/expressions/public/legacy.ts index 77617fa61001f..b5089292b3ab4 100644 --- a/src/legacy/core_plugins/expressions/public/legacy.ts +++ b/src/legacy/core_plugins/expressions/public/legacy.ts @@ -17,13 +17,4 @@ * under the License. */ -import { npSetup, npStart } from 'ui/new_platform'; -import { plugin } from '.'; - -const expressionsPlugin = plugin(); - -export const setup = expressionsPlugin.setup(npSetup.core); - -export const start = expressionsPlugin.start(npStart.core, { - inspector: npStart.plugins.inspector, -}); +export * from './np_ready/public/legacy'; diff --git a/src/legacy/core_plugins/expressions/public/mocks.ts b/src/legacy/core_plugins/expressions/public/mocks.ts index 42ec5bfb5ba65..0dafcf8711165 100644 --- a/src/legacy/core_plugins/expressions/public/mocks.ts +++ b/src/legacy/core_plugins/expressions/public/mocks.ts @@ -17,21 +17,4 @@ * under the License. */ -import { ExpressionsSetup, ExpressionsStart } from '.'; - -function createExpressionsSetupMock() { - const mock: MockedKeys> = {}; - - return mock; -} - -function createExpressionsStartMock() { - const mock: MockedKeys> = {}; - - return mock; -} - -export const expressionsPluginMock = { - createSetup: createExpressionsSetupMock, - createStart: createExpressionsStartMock, -}; +export * from './np_ready/public/mocks'; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/kibana.json b/src/legacy/core_plugins/expressions/public/np_ready/kibana.json new file mode 100644 index 0000000000000..ec87b56f3745e --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "expressions", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [ + "inspector" + ] +} diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/execute.test.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.test.ts similarity index 97% rename from src/legacy/core_plugins/expressions/public/expressions/lib/execute.test.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/execute.test.ts index d283272983abd..88714f9699d88 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/lib/execute.test.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.test.ts @@ -19,9 +19,9 @@ import { execute, ExpressionDataHandler } from './execute'; import { fromExpression } from '@kbn/interpreter/common'; -import { ExpressionAST } from './_types'; +import { ExpressionAST } from './types'; -jest.mock('../services', () => ({ +jest.mock('./services', () => ({ getInterpreter: () => { return { interpretAst: async (expression: ExpressionAST) => { diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/execute.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts similarity index 98% rename from src/legacy/core_plugins/expressions/public/expressions/lib/execute.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts index 4cc1cead3e03c..b4b11588b91bf 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/lib/execute.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts @@ -19,8 +19,8 @@ import { fromExpression } from '@kbn/interpreter/target/common'; import { DataAdapter, RequestAdapter, Adapters } from '../../../../../../plugins/inspector/public'; -import { getInterpreter } from '../services'; -import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './_types'; +import { getInterpreter } from './services'; +import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './types'; /** * The search context describes a specific context (filters, time range and query) diff --git a/src/legacy/core_plugins/expressions/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx similarity index 91% rename from src/legacy/core_plugins/expressions/public/expressions/expression_renderer.tsx rename to src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx index 11921ca9cf269..87e8efc1cb81a 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/expression_renderer.tsx +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/expression_renderer.tsx @@ -19,9 +19,8 @@ import { useRef, useEffect } from 'react'; import React from 'react'; - -import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './lib/_types'; -import { IExpressionLoader, ExpressionLoader } from './lib/loader'; +import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './types'; +import { IExpressionLoader, ExpressionLoader } from './loader'; // Accept all options of the runner as props except for the // dom element which is provided by the component itself @@ -45,7 +44,6 @@ export const createRenderer = (loader: IExpressionLoader): ExpressionRenderer => ...options }: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); - const handlerRef: React.MutableRefObject = useRef(null); useEffect(() => { @@ -70,12 +68,5 @@ export const createRenderer = (loader: IExpressionLoader): ExpressionRenderer => mountpoint.current, ]); - return ( -
{ - mountpoint.current = el; - }} - /> - ); + return
; }; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts new file mode 100644 index 0000000000000..428b431d298ad --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from '../../../../../../core/public'; +import { ExpressionsPublicPlugin } from './plugin'; + +export * from './plugin'; +export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { IInterpreterRenderFunction } from './types'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ExpressionsPublicPlugin(initializerContext); +} diff --git a/src/legacy/core_plugins/expressions/public/expressions/services.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/legacy.ts similarity index 56% rename from src/legacy/core_plugins/expressions/public/expressions/services.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/legacy.ts index 73f627f8dc886..e97f8f913e63e 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/services.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/legacy.ts @@ -17,25 +17,17 @@ * under the License. */ -import { IInterpreter } from './lib/_types'; -import { Start as IInspector } from '../../../../../plugins/inspector/public'; +/* eslint-disable */ +import { npSetup, npStart } from 'ui/new_platform'; +/* eslint-enable */ +import { plugin } from '.'; -let interpreter: IInterpreter | undefined; -let inspector: IInspector; +const expressionsPlugin = plugin({} as any); -export const getInterpreter = (): IInterpreter => { - if (!interpreter) throw new Error('interpreter was not set'); - return interpreter; -}; +export const setup = expressionsPlugin.setup(npSetup.core, { + inspector: npSetup.plugins.inspector, +}); -export const setInterpreter = (inspectorInstance: IInterpreter) => { - interpreter = inspectorInstance; -}; - -export const getInspector = (): IInspector => { - return inspector; -}; - -export const setInspector = (inspectorInstance: IInspector) => { - inspector = inspectorInstance; -}; +export const start = expressionsPlugin.start(npStart.core, { + inspector: npStart.plugins.inspector, +}); diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/loader.test.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts similarity index 86% rename from src/legacy/core_plugins/expressions/public/expressions/lib/loader.test.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts index 42e71a8000fbc..6cb27827cffb6 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/lib/loader.test.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts @@ -20,35 +20,31 @@ import { first } from 'rxjs/operators'; import { loader, ExpressionLoader } from './loader'; import { fromExpression } from '@kbn/interpreter/common'; -import { IInterpreterRenderHandlers } from './_types'; +import { IInterpreterRenderHandlers } from './types'; import { Observable } from 'rxjs'; import { ExpressionAST } from '../../../../../../plugins/expressions/common'; const element: HTMLElement = null as any; -jest.mock('../services', () => ({ - getInterpreter: () => { - return { - interpretAst: async (expression: ExpressionAST) => { - return { type: 'render', as: 'test' }; +jest.mock('./services', () => { + const renderers: Record = { + test: { + render: (el: HTMLElement, value: unknown, handlers: IInterpreterRenderHandlers) => { + handlers.done(); }, - }; - }, -})); - -jest.mock('../../../../interpreter/public/registries', () => { - const _registry: Record = {}; - _registry.test = { - render: (el: HTMLElement, value: any, handlers: IInterpreterRenderHandlers) => { - handlers.done(); }, }; return { - renderersRegistry: { - get: (id: string) => { - return _registry[id]; - }, + getInterpreter: () => { + return { + interpretAst: async (expression: ExpressionAST) => { + return { type: 'render', as: 'test' }; + }, + }; }, + getRenderersRegistry: () => ({ + get: (id: string) => renderers[id], + }), }; }); diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/loader.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts similarity index 98% rename from src/legacy/core_plugins/expressions/public/expressions/lib/loader.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts index a072f06c85252..a2893ab854ceb 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/lib/loader.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts @@ -22,8 +22,8 @@ import { first, share } from 'rxjs/operators'; import { Adapters, InspectorSession } from '../../../../../../plugins/inspector/public'; import { execute, ExpressionDataHandler } from './execute'; import { ExpressionRenderHandler } from './render'; -import { RenderId, Data, IExpressionLoaderParams, ExpressionAST } from './_types'; -import { getInspector } from '../services'; +import { RenderId, Data, IExpressionLoaderParams, ExpressionAST } from './types'; +import { getInspector } from './services'; export class ExpressionLoader { data$: Observable; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts new file mode 100644 index 0000000000000..9a4251587a15b --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/mocks.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExpressionsSetup, ExpressionsStart, plugin as pluginInitializer } from '.'; +/* eslint-disable */ +import { coreMock } from '../../../../../../core/public/mocks'; +import { inspectorPluginMock } from '../../../../../../plugins/inspector/public/mocks'; +/* eslint-enable */ + +const createExpressionsSetupMock = (): ExpressionsSetup => { + return { + registerFunction: jest.fn(), + registerRenderer: jest.fn(), + registerType: jest.fn(), + __LEGACY: { + functions: { + register: () => {}, + } as any, + renderers: { + register: () => {}, + } as any, + types: { + register: () => {}, + } as any, + }, + }; +}; + +function createExpressionsStartMock(): ExpressionsStart { + return { + ExpressionRenderer: jest.fn(() => null), + execute: jest.fn(), + loader: jest.fn(), + render: jest.fn(), + }; +} + +const createPlugin = async () => { + const pluginInitializerContext = coreMock.createPluginInitializerContext(); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const plugin = pluginInitializer(pluginInitializerContext); + const setup = await plugin.setup(coreSetup, { + inspector: inspectorPluginMock.createSetupContract(), + }); + + return { + pluginInitializerContext, + coreSetup, + coreStart, + plugin, + setup, + doStart: async () => + await plugin.start(coreStart, { + inspector: inspectorPluginMock.createStartContract(), + }), + }; +}; + +export const expressionsPluginMock = { + createSetup: createExpressionsSetupMock, + createStart: createExpressionsStartMock, + createPlugin, +}; diff --git a/src/legacy/core_plugins/expressions/public/expressions/expressions_service.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts similarity index 51% rename from src/legacy/core_plugins/expressions/public/expressions/expressions_service.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts index 7442eb47bee4b..ba4fa01ff13dd 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/plugin.ts @@ -17,29 +17,49 @@ * under the License. */ +/* eslint-disable */ import { npSetup } from 'ui/new_platform'; -// @ts-ignore +import { ExpressionsSetupContract } from '../../../../../../plugins/expressions/public/expressions/expressions_service'; +/* eslint-enable */ -import { setInspector, setInterpreter } from './services'; -import { execute } from './lib/execute'; -import { loader } from './lib/loader'; -import { render } from './lib/render'; -import { IInterpreter } from './lib/_types'; +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../../../../core/public'; +import { + Start as InspectorStart, + Setup as InspectorSetup, +} from '../../../../../../plugins/inspector/public'; +import { IInterpreter } from './types'; +import { setInterpreter, setInspector, setRenderersRegistry } from './services'; import { createRenderer } from './expression_renderer'; +import { loader } from './loader'; +import { execute } from './execute'; +import { render } from './render'; -import { Start as IInspector } from '../../../../../plugins/inspector/public'; +export interface ExpressionsSetupDeps { + inspector: InspectorSetup; +} -export interface ExpressionsServiceStartDependencies { - inspector: IInspector; +export interface ExpressionsStartDeps { + inspector: InspectorStart; } -/** - * Expressions Service - * @internal - */ -export class ExpressionsService { - public setup() { + +export type ExpressionsSetup = ExpressionsSetupContract; +export type ExpressionsStart = ReturnType; + +export class ExpressionsPublicPlugin + implements + Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: ExpressionsSetupDeps): ExpressionsSetup { + setRenderersRegistry(npSetup.plugins.expressions.__LEGACY.renderers); + // eslint-disable-next-line - const { getInterpreter } = require('../../../interpreter/public/interpreter'); + const { getInterpreter } = require('../../../../interpreter/public/interpreter'); getInterpreter() .then(({ interpreter }: { interpreter: IInterpreter }) => { setInterpreter(interpreter); @@ -52,10 +72,11 @@ export class ExpressionsService { registerType: npSetup.plugins.expressions.registerType, registerFunction: npSetup.plugins.expressions.registerFunction, registerRenderer: npSetup.plugins.expressions.registerRenderer, + __LEGACY: npSetup.plugins.expressions.__LEGACY, }; } - public start({ inspector }: ExpressionsServiceStartDependencies) { + public start(core: CoreStart, { inspector }: ExpressionsStartDeps) { const ExpressionRenderer = createRenderer(loader); setInspector(inspector); @@ -63,16 +84,9 @@ export class ExpressionsService { execute, render, loader, - ExpressionRenderer, }; } - public stop() { - // nothing to do here yet - } + public stop() {} } - -/** @public */ -export type ExpressionsSetup = ReturnType; -export type ExpressionsStart = ReturnType; diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/render.test.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts similarity index 86% rename from src/legacy/core_plugins/expressions/public/expressions/lib/render.test.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts index 1bb2f8d6554b2..cf606b8fabec2 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/lib/render.test.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/render.test.ts @@ -19,23 +19,22 @@ import { render, ExpressionRenderHandler } from './render'; import { Observable } from 'rxjs'; -import { IInterpreterRenderHandlers } from './_types'; +import { IInterpreterRenderHandlers } from './types'; -const element: HTMLElement = null as any; +const element: HTMLElement = {} as HTMLElement; -jest.mock('../../../../interpreter/public/registries', () => { - const _registry: Record = {}; - _registry.test = { - render: (el: HTMLElement, value: any, handlers: IInterpreterRenderHandlers) => { - handlers.done(); +jest.mock('./services', () => { + const renderers: Record = { + test: { + render: (el: HTMLElement, value: unknown, handlers: IInterpreterRenderHandlers) => { + handlers.done(); + }, }, }; return { - renderersRegistry: { - get: (id: string) => { - return _registry[id]; - }, - }, + getRenderersRegistry: () => ({ + get: (id: string) => renderers[id], + }), }; }); diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/render.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/render.ts similarity index 92% rename from src/legacy/core_plugins/expressions/public/expressions/lib/render.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/render.ts index 250fa14c9de1d..6d60ce15cb79c 100644 --- a/src/legacy/core_plugins/expressions/public/expressions/lib/render.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/render.ts @@ -20,8 +20,8 @@ import { Observable } from 'rxjs'; import * as Rx from 'rxjs'; import { share, first } from 'rxjs/operators'; -import { renderersRegistry } from '../../../../interpreter/public/registries'; -import { event, RenderId, Data, IInterpreterRenderHandlers } from './_types'; +import { event, RenderId, Data, IInterpreterRenderHandlers } from './types'; +import { getRenderersRegistry } from './services'; export class ExpressionRenderHandler { render$: Observable; @@ -70,13 +70,15 @@ export class ExpressionRenderHandler { throw new Error('invalid data provided to expression renderer'); } - if (!renderersRegistry.get(data.as)) { + if (!getRenderersRegistry().get(data.as)) { throw new Error(`invalid renderer id '${data.as}'`); } const promise = this.render$.pipe(first()).toPromise(); - renderersRegistry.get(data.as).render(this.element, data.value, this.handlers); + getRenderersRegistry() + .get(data.as) + .render(this.element, data.value, this.handlers); return promise; }; diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/services.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/services.ts new file mode 100644 index 0000000000000..4d95a8a91d0bb --- /dev/null +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/services.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IInterpreter } from './types'; +import { Start as IInspector } from '../../../../../../plugins/inspector/public'; +import { ExpressionsSetup } from './plugin'; + +const createGetterSetter = (name: string): [() => T, (value: T) => void] => { + let value: T; + + const get = (): T => { + if (!value) throw new Error(`${name} was not set`); + return value; + }; + + const set = (newValue: T) => { + value = newValue; + }; + + return [get, set]; +}; + +export const [getInspector, setInspector] = createGetterSetter('Inspector'); +export const [getInterpreter, setInterpreter] = createGetterSetter('Interpreter'); +export const [getRenderersRegistry, setRenderersRegistry] = createGetterSetter< + ExpressionsSetup['__LEGACY']['renderers'] +>('Renderers registry'); diff --git a/src/legacy/core_plugins/expressions/public/expressions/lib/_types.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/types.ts similarity index 100% rename from src/legacy/core_plugins/expressions/public/expressions/lib/_types.ts rename to src/legacy/core_plugins/expressions/public/np_ready/public/types.ts diff --git a/src/legacy/core_plugins/expressions/public/plugin.ts b/src/legacy/core_plugins/expressions/public/plugin.ts index b954238c05e59..93577757fecee 100644 --- a/src/legacy/core_plugins/expressions/public/plugin.ts +++ b/src/legacy/core_plugins/expressions/public/plugin.ts @@ -17,56 +17,4 @@ * under the License. */ -import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { ExpressionsService, ExpressionsSetup, ExpressionsStart } from './expressions'; -import { - Start as InspectorStart, - Setup as InspectorSetup, -} from '../../../../plugins/inspector/public'; - -/** - * Interface for any dependencies on other plugins' `setup` contracts. - * - * @internal - */ -export interface ExpressionsPluginSetupDependencies { - inspector: InspectorSetup; -} - -export interface ExpressionsPluginStartDependencies { - inspector: InspectorStart; -} - -/** - * Interface for this plugin's returned `setup` contract. - * - * @public - */ - -export class ExpressionsPlugin - implements - Plugin< - ExpressionsSetup, - ExpressionsStart, - ExpressionsPluginSetupDependencies, - ExpressionsPluginStartDependencies - > { - // Exposed services, sorted alphabetically - private readonly expressions: ExpressionsService = new ExpressionsService(); - - public setup(core: CoreSetup): ExpressionsSetup { - return { - ...this.expressions.setup(), - }; - } - - public start(core: CoreStart, plugins: ExpressionsPluginStartDependencies): ExpressionsStart { - return { - ...this.expressions.start({ inspector: plugins.inspector }), - }; - } - - public stop() { - this.expressions.stop(); - } -} +export * from './np_ready/public/plugin'; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index bbfa8bd329c65..ae5c0a83bd781 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -31,6 +31,12 @@ export const npSetup = { registerFunction: sinon.fake(), registerRenderer: sinon.fake(), registerType: sinon.fake(), + __LEGACY: { + renderers: { + register: () => undefined, + get: () => null, + }, + }, }, data: { }, From 79fb7498bd201f573093cc73fc9e61caff96319c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 10 Oct 2019 12:16:20 +0200 Subject: [PATCH 07/92] [APM] Agent configuration phase 2 (#46995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Agent Config Management Phase 2 * Add status indicator * Extract TimestampTooltip component * Remove unused StickyTransactionProperties component * Fix snapshot and minor cleanup * Minor cleanup * Display settings conditionally by agent name * Fix client * Format timestamp * Minor design feedback * Clear cache when clicking refresh * Fix test * Revert t() short hand * Fix translations * Add support for “all” option * Fix API tests * Move delete button to footer * Fix snapshots * Add API tests * Fix toasts * Address feedback and ensure order when searching for configs * Fix snapshots * Remove timeout --- .../common/agent_configuration_constants.ts | 25 ++ .../transaction_max_spans_rt/index.test.ts | 28 ++ .../transaction_max_spans_rt/index.ts | 19 + .../transaction_sample_rate_rt/index.test.ts | 6 + .../transaction_sample_rate_rt/index.ts | 4 +- .../ErrorGroupDetails/DetailView/index.tsx | 4 +- .../app/Main/route_config/index.tsx | 4 +- .../Settings/AddSettings/AddSettingFlyout.tsx | 397 ------------------ .../AddSettings/AddSettingFlyoutBody.tsx | 277 ------------ .../AddEditFlyout/DeleteButton.tsx | 92 ++++ .../AddEditFlyout/ServiceSection.tsx | 160 +++++++ .../AddEditFlyout/SettingsSection.tsx | 174 ++++++++ .../AddEditFlyout/index.tsx | 256 +++++++++++ .../AddEditFlyout/saveConfig.ts | 115 +++++ .../AgentConfigurationList.tsx} | 145 ++++--- .../Settings/AgentConfigurations/index.tsx | 140 ++++++ .../public/components/app/Settings/index.tsx | 187 --------- .../StickyTransactionProperties.tsx | 191 --------- .../Waterfall/SpanFlyout/index.tsx | 4 +- .../DatePicker/__test__/DatePicker.test.tsx | 2 + .../components/shared/DatePicker/index.tsx | 2 + .../StickyProperties/StickyProperties.test.js | 21 - .../StickyProperties.test.js.snap | 41 +- .../shared/StickyProperties/index.tsx | 24 -- .../shared/Summary/TimestampSummaryItem.tsx | 26 -- .../shared/Summary/TransactionSummary.tsx | 4 +- .../components/shared/TimestampTooltip.tsx | 41 ++ .../apm/public/hooks/useFetcher.test.tsx | 20 +- .../plugins/apm/public/hooks/useFetcher.tsx | 15 +- .../public/services/__test__/callApi.test.ts | 4 +- .../apm/public/services/rest/callApi.ts | 10 +- .../__snapshots__/queries.test.ts.snap | 2 +- .../lib/services/get_service_agent_name.ts | 6 +- .../services/get_service_transaction_types.ts | 4 - .../__snapshots__/queries.test.ts.snap | 70 ++- .../configuration_types.d.ts | 20 +- .../create_agent_config_index.ts | 123 +++--- .../create_or_update_configuration.ts | 49 +++ .../get_agent_name_by_service.ts | 54 +++ .../get_environments/get_all_environments.ts | 15 +- ... get_existing_environments_for_service.ts} | 20 +- .../get_environments/index.ts | 10 +- .../agent_configuration/get_service_names.ts | 6 +- ...figuration.ts => mark_applied_by_agent.ts} | 22 +- .../agent_configuration/queries.test.ts | 4 +- .../agent_configuration/search.mocks.ts | 74 ++++ .../agent_configuration/search.test.ts | 39 ++ .../settings/agent_configuration/search.ts | 63 ++- .../update_configuration.ts | 37 -- .../avg_duration_by_country/index.ts | 5 - .../lib/transactions/breakdown/index.ts | 5 - .../lib/transactions/get_transaction/index.ts | 2 - .../server/lib/ui_filters/get_environments.ts | 4 - .../apm/server/routes/create_apm_api.ts | 19 +- .../plugins/apm/server/routes/settings.ts | 81 ++-- .../plugins/apm/server/routes/typings.ts | 1 + .../plugins/apm/typings/elasticsearch.ts | 8 +- .../translations/translations/ja-JP.json | 45 -- .../translations/translations/zh-CN.json | 46 -- .../apis/apm/agent_configuration.ts | 174 ++++++++ .../apis/apm/feature_controls.ts | 2 +- x-pack/test/api_integration/apis/apm/index.ts | 1 + 62 files changed, 1884 insertions(+), 1565 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts create mode 100644 x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts create mode 100644 x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyout.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts rename x-pack/legacy/plugins/apm/public/components/app/Settings/{SettingsList.tsx => AgentConfigurations/AgentConfigurationList.tsx} (51%) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip.tsx create mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts rename x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/{get_unavailable_environments.ts => get_existing_environments_for_service.ts} (69%) rename x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/{create_configuration.ts => mark_applied_by_agent.ts} (53%) create mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts delete mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts create mode 100644 x-pack/test/api_integration/apis/apm/agent_configuration.ts diff --git a/x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts b/x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts new file mode 100644 index 0000000000000..4ddc65c14a134 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALL_OPTION_VALUE = 'ALL_OPTION_VALUE'; + +// human-readable label for the option. The "All" option should be translated. +// Everything else should be returned verbatim +export function getOptionLabel(value: string | undefined) { + if (value === undefined || value === ALL_OPTION_VALUE) { + return i18n.translate('xpack.apm.settings.agentConf.allOptionLabel', { + defaultMessage: 'All' + }); + } + + return value; +} + +export function omitAllOption(value: string) { + return value === ALL_OPTION_VALUE ? undefined : value; +} diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts new file mode 100644 index 0000000000000..b62251b6974d9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { transactionMaxSpansRt } from './index'; +import { isRight } from 'fp-ts/lib/Either'; + +describe('transactionMaxSpans', () => { + it('does not accept empty values', () => { + expect(isRight(transactionMaxSpansRt.decode(undefined))).toBe(false); + expect(isRight(transactionMaxSpansRt.decode(null))).toBe(false); + expect(isRight(transactionMaxSpansRt.decode(''))).toBe(false); + }); + + it('accepts both strings and numbers as values', () => { + expect(isRight(transactionMaxSpansRt.decode('55'))).toBe(true); + expect(isRight(transactionMaxSpansRt.decode(55))).toBe(true); + }); + + it('checks if the number falls within 0, 32000', () => { + expect(isRight(transactionMaxSpansRt.decode(0))).toBe(true); + expect(isRight(transactionMaxSpansRt.decode(32000))).toBe(true); + expect(isRight(transactionMaxSpansRt.decode(-55))).toBe(false); + expect(isRight(transactionMaxSpansRt.decode(NaN))).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts new file mode 100644 index 0000000000000..251161c21babe --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * as t from 'io-ts'; + +export const transactionMaxSpansRt = new t.Type( + 'transactionMaxSpans', + t.number.is, + (input, context) => { + const value = parseInt(input as string, 10); + return value >= 0 && value <= 32000 + ? t.success(value) + : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts index 85551481e78b6..6930a69f0870a 100644 --- a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts @@ -7,6 +7,12 @@ import { transactionSampleRateRt } from './index'; import { isRight } from 'fp-ts/lib/Either'; describe('transactionSampleRateRt', () => { + it('does not accept empty values', () => { + expect(isRight(transactionSampleRateRt.decode(undefined))).toBe(false); + expect(isRight(transactionSampleRateRt.decode(null))).toBe(false); + expect(isRight(transactionSampleRateRt.decode(''))).toBe(false); + }); + it('accepts both strings and numbers as values', () => { expect(isRight(transactionSampleRateRt.decode('0.5'))).toBe(true); expect(isRight(transactionSampleRateRt.decode(0.5))).toBe(true); diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts index d6df87e7a5fed..90c60d16f7b59 100644 --- a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts @@ -10,8 +10,8 @@ export const transactionSampleRateRt = new t.Type( 'TransactionSampleRate', t.number.is, (input, context) => { - const value = Number(input); - return value >= 0 && value <= 1 && Number(value.toFixed(3)) === value + const value = parseFloat(input as string); + return value >= 0 && value <= 1 && parseFloat(value.toFixed(3)) === value ? t.success(value) : t.failure(input, context); }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 9b598ff469b9d..3f009a990afa5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -36,7 +36,7 @@ import { logStacktraceTab } from './ErrorTabs'; import { Summary } from '../../../shared/Summary'; -import { TimestampSummaryItem } from '../../../shared/Summary/TimestampSummaryItem'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; @@ -114,7 +114,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) { , + , errorUrl && method ? ( void; - onSubmit: () => void; - isOpen: boolean; - selectedConfig: Config | null; -} - -export function AddSettingsFlyout({ - onClose, - isOpen, - onSubmit, - selectedConfig -}: Props) { - const { - notifications: { toasts } - } = useKibanaCore(); - const [environment, setEnvironment] = useState( - selectedConfig - ? selectedConfig.service.environment || ENVIRONMENT_NOT_DEFINED - : undefined - ); - const [serviceName, setServiceName] = useState( - selectedConfig ? selectedConfig.service.name : undefined - ); - const [sampleRate, setSampleRate] = useState( - selectedConfig - ? selectedConfig.settings.transaction_sample_rate.toString() - : '' - ); - const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( - () => - callApmApi({ - pathname: '/api/apm/settings/agent-configuration/services' - }), - [], - { preservePreviousData: false } - ); - const { data: environments = [], status: environmentStatus } = useFetcher( - () => { - if (serviceName) { - return callApmApi({ - pathname: - '/api/apm/settings/agent-configuration/services/{serviceName}/environments', - params: { - path: { serviceName } - } - }); - } - }, - [serviceName], - { preservePreviousData: false } - ); - - const isSampleRateValid = isRight(transactionSampleRateRt.decode(sampleRate)); - - const isSelectedEnvironmentValid = environments.some( - env => - env.name === environment && (Boolean(selectedConfig) || env.available) - ); - - if (!isOpen) { - return null; - } - - return ( - - - - - {selectedConfig ? ( -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.editConfigTitle', - { - defaultMessage: 'Edit configuration' - } - )} -

- ) : ( -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.createConfigTitle', - { - defaultMessage: 'Create configuration' - } - )} -

- )} -
-
- - - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.betaCallOutText', - { - defaultMessage: - 'Please note only sample rate configuration is supported in this first version. We will extend support for agent configuration in future releases. Please be aware of bugs.' - } - )} - - - { - if (selectedConfig) { - await deleteConfig(selectedConfig, toasts); - } - onSubmit(); - }} - environment={environment} - setEnvironment={setEnvironment} - serviceName={serviceName} - setServiceName={setServiceName} - sampleRate={sampleRate} - setSampleRate={setSampleRate} - serviceNames={serviceNames} - serviceNamesStatus={serviceNamesStatus} - environments={environments} - environmentStatus={environmentStatus} - isSampleRateValid={isSampleRateValid} - isSelectedEnvironmentValid={isSelectedEnvironmentValid} - /> - - - - - - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.cancelButtonLabel', - { - defaultMessage: 'Cancel' - } - )} - - - - ) => { - event.preventDefault(); - await saveConfig({ - environment, - serviceName, - sampleRate: parseFloat(sampleRate), - configurationId: selectedConfig - ? selectedConfig.id - : undefined, - toasts - }); - onSubmit(); - }} - > - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel', - { - defaultMessage: 'Save configuration' - } - )} - - - - -
-
- ); -} - -async function deleteConfig( - selectedConfig: Config, - toasts: NotificationsStart['toasts'] -) { - try { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', - method: 'DELETE', - params: { - path: { configurationId: selectedConfig.id } - } - }); - toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.deleteConfigSucceededTitle', - { - defaultMessage: 'Configuration was deleted' - } - ), - text: ( - - ) - }); - } catch (error) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.deleteConfigFailedTitle', - { - defaultMessage: 'Configuration could not be deleted' - } - ), - text: ( - - ) - }); - } -} - -async function saveConfig({ - sampleRate, - serviceName, - environment, - configurationId, - toasts -}: { - sampleRate: number; - serviceName: string | undefined; - environment: string | undefined; - configurationId?: string; - toasts: NotificationsStart['toasts']; -}) { - trackEvent({ app: 'apm', name: 'save_agent_configuration' }); - - try { - if (isNaN(sampleRate) || !serviceName) { - throw new Error('Missing arguments'); - } - - const configuration = { - settings: { - transaction_sample_rate: sampleRate - }, - service: { - name: serviceName, - environment: - environment === ENVIRONMENT_NOT_DEFINED ? undefined : environment - } - }; - - if (configurationId) { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', - method: 'PUT', - params: { - path: { configurationId }, - body: configuration - } - }); - - toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.editConfigSucceededTitle', - { - defaultMessage: 'Configuration edited' - } - ), - text: ( - - ) - }); - } else { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/new', - method: 'POST', - params: { - body: configuration - } - }); - toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.createConfigSucceededTitle', - { - defaultMessage: 'Configuration created!' - } - ), - text: ( - - ) - }); - } - } catch (error) { - if (configurationId) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.editConfigFailedTitle', - { - defaultMessage: 'Configuration could not be edited' - } - ), - text: ( - - ) - }); - } else { - toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.createConfigFailedTitle', - { - defaultMessage: 'Configuration could not be created' - } - ), - text: ( - - ) - }); - } - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx deleted file mode 100644 index 090f3fe0d5f91..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx +++ /dev/null @@ -1,277 +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 React from 'react'; -import { - EuiForm, - EuiFormRow, - EuiButton, - EuiFieldText, - EuiTitle, - EuiSpacer, - EuiHorizontalRule, - EuiText -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; -import { SelectWithPlaceholder } from '../../../shared/SelectWithPlaceholder'; -import { Config } from '..'; - -const selectPlaceholderLabel = `- ${i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.selectPlaceholder', - { - defaultMessage: 'Select' - } -)} -`; - -export function AddSettingFlyoutBody({ - selectedConfig, - onDelete, - environment, - setEnvironment, - serviceName, - setServiceName, - sampleRate, - setSampleRate, - serviceNames, - serviceNamesStatus, - environments, - environmentStatus, - isSampleRateValid, - isSelectedEnvironmentValid -}: { - selectedConfig: Config | null; - onDelete: () => void; - environment?: string; - setEnvironment: (env: string | undefined) => void; - serviceName?: string; - setServiceName: (env: string | undefined) => void; - sampleRate: string; - setSampleRate: (env: string) => void; - serviceNames: string[]; - serviceNamesStatus?: FETCH_STATUS; - environments: Array<{ - name: string; - available: boolean; - }>; - environmentStatus?: FETCH_STATUS; - isSampleRateValid: boolean; - isSelectedEnvironmentValid: boolean; -}) { - const environmentOptions = environments.map(({ name, available }) => ({ - disabled: !available, - text: - name === ENVIRONMENT_NOT_DEFINED - ? i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel', - { - defaultMessage: 'Not set' - } - ) - : name, - value: name - })); - - return ( - -
- -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSectionTitle', - { - defaultMessage: 'Service' - } - )} -

-
- - - - - ({ text }))} - value={serviceName} - disabled={Boolean(selectedConfig)} - onChange={e => { - e.preventDefault(); - setServiceName(e.target.value); - setEnvironment(undefined); - }} - /> - - - - { - e.preventDefault(); - setEnvironment(e.target.value); - }} - /> - - - - - -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.configurationSectionTitle', - { - defaultMessage: 'Configuration' - } - )} -

-
- - - - - { - e.preventDefault(); - setSampleRate(e.target.value); - }} - disabled={!(serviceName && environment) && !selectedConfig} - /> - - - {selectedConfig ? ( - <> - - -

- - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle', - { - defaultMessage: 'Delete configuration' - } - )} - -

-
- - - - -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText', - { - defaultMessage: - 'If you wish to delete this configuration, please be aware that the agents will continue to use the existing configuration until they sync with the APM Server.' - } - )} -

-
- - - - - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel', - { - defaultMessage: 'Delete' - } - )} - - - - - ) : null} - -
- ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx new file mode 100644 index 0000000000000..05d33915a6b86 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx @@ -0,0 +1,92 @@ +/* + * 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, { useState } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { NotificationsStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { Config } from '../index'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; +import { getOptionLabel } from '../../../../../../common/agent_configuration_constants'; +import { useKibanaCore } from '../../../../../../../observability/public'; + +interface Props { + onDeleted: () => void; + selectedConfig: Config; +} + +export function DeleteButton({ onDeleted, selectedConfig }: Props) { + const [isDeleting, setIsDeleting] = useState(false); + const { + notifications: { toasts } + } = useKibanaCore(); + + return ( + { + setIsDeleting(true); + await deleteConfig(selectedConfig, toasts); + setIsDeleting(false); + onDeleted(); + }} + > + {i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.buttonLabel', + { defaultMessage: 'Delete' } + )} + + ); +} + +async function deleteConfig( + selectedConfig: Config, + toasts: NotificationsStart['toasts'] +) { + try { + await callApmApi({ + pathname: '/api/apm/settings/agent-configuration/{configurationId}', + method: 'DELETE', + params: { + path: { configurationId: selectedConfig.id } + } + }); + toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle', + { defaultMessage: 'Configuration was deleted' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText', + { + defaultMessage: + 'You have successfully deleted a configuration for "{serviceName}". It will take some time to propagate to the agents.', + values: { serviceName: getOptionLabel(selectedConfig.service.name) } + } + ) + }); + } catch (error) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle', + { defaultMessage: 'Configuration could not be deleted' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedText', + { + defaultMessage: + 'Something went wrong when deleting a configuration for "{serviceName}". Error: "{errorMessage}"', + values: { + serviceName: getOptionLabel(selectedConfig.service.name), + errorMessage: error.message + } + } + ) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx new file mode 100644 index 0000000000000..7c114977d1d72 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx @@ -0,0 +1,160 @@ +/* + * 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 { EuiTitle, EuiSpacer, EuiFormRow, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; +import { + getOptionLabel, + omitAllOption +} from '../../../../../../common/agent_configuration_constants'; + +const SELECT_PLACEHOLDER_LABEL = `- ${i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder', + { defaultMessage: 'Select' } +)} -`; + +interface Props { + isReadOnly: boolean; + serviceName: string; + setServiceName: (env: string) => void; + environment: string; + setEnvironment: (env: string) => void; +} + +export function ServiceSection({ + isReadOnly, + serviceName, + setServiceName, + environment, + setEnvironment +}: Props) { + const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( + () => { + if (!isReadOnly) { + return callApmApi({ + pathname: '/api/apm/settings/agent-configuration/services', + forceCache: true + }); + } + }, + [isReadOnly], + { preservePreviousData: false } + ); + const { data: environments = [], status: environmentStatus } = useFetcher( + () => { + if (!isReadOnly && serviceName) { + return callApmApi({ + pathname: '/api/apm/settings/agent-configuration/environments', + params: { query: { serviceName: omitAllOption(serviceName) } } + }); + } + }, + [isReadOnly, serviceName], + { preservePreviousData: false } + ); + + const ALREADY_CONFIGURED_TRANSLATED = i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption', + { defaultMessage: 'already configured' } + ); + + const serviceNameOptions = serviceNames.map(name => ({ + text: getOptionLabel(name), + value: name + })); + const environmentOptions = environments.map( + ({ name, alreadyConfigured }) => ({ + disabled: alreadyConfigured, + text: `${getOptionLabel(name)} ${ + alreadyConfigured ? `(${ALREADY_CONFIGURED_TRANSLATED})` : '' + }`, + value: name + }) + ); + + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.serviceSection.title', + { defaultMessage: 'Service' } + )} +

+
+ + + + + {isReadOnly ? ( + {getOptionLabel(serviceName)} + ) : ( + { + e.preventDefault(); + setServiceName(e.target.value); + setEnvironment(''); + }} + /> + )} + + + + {isReadOnly ? ( + {getOptionLabel(environment)} + ) : ( + { + e.preventDefault(); + setEnvironment(e.target.value); + }} + /> + )} + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx new file mode 100644 index 0000000000000..24c8222d4cd99 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx @@ -0,0 +1,174 @@ +/* + * 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 { + EuiFormRow, + EuiFieldText, + EuiTitle, + EuiSpacer, + EuiFieldNumber +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; + +interface Props { + isRumService: boolean; + + // sampleRate + sampleRate: string; + setSampleRate: (value: string) => void; + isSampleRateValid?: boolean; + + // captureBody + captureBody: string; + setCaptureBody: (value: string) => void; + + // transactionMaxSpans + transactionMaxSpans: string; + setTransactionMaxSpans: (value: string) => void; + isTransactionMaxSpansValid?: boolean; +} + +export function SettingsSection({ + isRumService, + + // sampleRate + sampleRate, + setSampleRate, + isSampleRateValid, + + // captureBody + captureBody, + setCaptureBody, + + // transactionMaxSpans + transactionMaxSpans, + setTransactionMaxSpans, + isTransactionMaxSpansValid +}: Props) { + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.settingsSection.title', + { defaultMessage: 'Options' } + )} +

+
+ + + + + { + e.preventDefault(); + setSampleRate(e.target.value); + }} + /> + + + + + {!isRumService && ( + + { + e.preventDefault(); + setCaptureBody(e.target.value); + }} + /> + + )} + + {!isRumService && ( + + { + e.preventDefault(); + setTransactionMaxSpans(e.target.value); + }} + /> + + )} + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx new file mode 100644 index 0000000000000..558f2161c0c94 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx @@ -0,0 +1,256 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiPortal, + EuiTitle, + EuiText, + EuiSpacer +} from '@elastic/eui'; +import { idx } from '@kbn/elastic-idx'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isRight } from 'fp-ts/lib/Either'; +import { transactionSampleRateRt } from '../../../../../../common/runtime_types/transaction_sample_rate_rt'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; +import { Config } from '../index'; +import { SettingsSection } from './SettingsSection'; +import { ServiceSection } from './ServiceSection'; +import { DeleteButton } from './DeleteButton'; +import { transactionMaxSpansRt } from '../../../../../../common/runtime_types/transaction_max_spans_rt'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { isRumAgentName } from '../../../../../../common/agent_name'; +import { ALL_OPTION_VALUE } from '../../../../../../common/agent_configuration_constants'; +import { saveConfig } from './saveConfig'; +import { useKibanaCore } from '../../../../../../../observability/public'; + +const defaultSettings = { + TRANSACTION_SAMPLE_RATE: '1.0', + CAPTURE_BODY: 'off', + TRANSACTION_MAX_SPANS: '500' +}; + +interface Props { + onClose: () => void; + onSaved: () => void; + onDeleted: () => void; + selectedConfig: Config | null; +} + +export function AddEditFlyout({ + onClose, + onSaved, + onDeleted, + selectedConfig +}: Props) { + const { + notifications: { toasts } + } = useKibanaCore(); + const [isSaving, setIsSaving] = useState(false); + + // config conditions (service) + const [serviceName, setServiceName] = useState( + selectedConfig ? selectedConfig.service.name || ALL_OPTION_VALUE : '' + ); + const [environment, setEnvironment] = useState( + selectedConfig ? selectedConfig.service.environment || ALL_OPTION_VALUE : '' + ); + + const { data: { agentName } = { agentName: undefined } } = useFetcher( + () => { + if (serviceName === ALL_OPTION_VALUE) { + return { agentName: undefined }; + } + + if (serviceName) { + return callApmApi({ + pathname: '/api/apm/settings/agent-configuration/agent_name', + params: { query: { serviceName } } + }); + } + }, + [serviceName], + { preservePreviousData: false } + ); + + // config settings + const [sampleRate, setSampleRate] = useState( + ( + idx(selectedConfig, _ => _.settings.transaction_sample_rate) || + defaultSettings.TRANSACTION_SAMPLE_RATE + ).toString() + ); + const [captureBody, setCaptureBody] = useState( + idx(selectedConfig, _ => _.settings.capture_body) || + defaultSettings.CAPTURE_BODY + ); + const [transactionMaxSpans, setTransactionMaxSpans] = useState( + ( + idx(selectedConfig, _ => _.settings.transaction_max_spans) || + defaultSettings.TRANSACTION_MAX_SPANS + ).toString() + ); + + const isRumService = isRumAgentName(agentName); + const isSampleRateValid = isRight(transactionSampleRateRt.decode(sampleRate)); + const isTransactionMaxSpansValid = isRight( + transactionMaxSpansRt.decode(transactionMaxSpans) + ); + + const isFormValid = + !!serviceName && + !!environment && + isSampleRateValid && + // captureBody and isTransactionMaxSpansValid are required except if service is RUM + (isRumService || (!!captureBody && isTransactionMaxSpansValid)) && + // agent name is required, except if serviceName is "all" + (serviceName === ALL_OPTION_VALUE || agentName !== undefined); + + const handleSubmitEvent = async ( + event: + | React.FormEvent + | React.MouseEvent + ) => { + event.preventDefault(); + setIsSaving(true); + + await saveConfig({ + serviceName, + environment, + sampleRate, + captureBody, + transactionMaxSpans, + configurationId: selectedConfig ? selectedConfig.id : undefined, + agentName, + toasts + }); + setIsSaving(false); + onSaved(); + }; + + return ( + + + + +

+ {selectedConfig + ? i18n.translate( + 'xpack.apm.settings.agentConf.editConfigTitle', + { defaultMessage: 'Edit configuration' } + ) + : i18n.translate( + 'xpack.apm.settings.agentConf.createConfigTitle', + { defaultMessage: 'Create configuration' } + )} +

+
+
+ + + This allows you to fine-tune your agent configuration directly in + Kibana. Best of all, changes are automatically propagated to your + APM agents so there’s no need to redeploy. + + + + + + {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +
{ + const didClickEnter = e.which === 13; + if (didClickEnter) { + handleSubmitEvent(e); + } + }} + > + + + + + + +
+
+ + + + {selectedConfig ? ( + + ) : null} + + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.cancelButtonLabel', + { defaultMessage: 'Cancel' } + )} + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.saveConfigurationButtonLabel', + { defaultMessage: 'Save' } + )} + + + + + + +
+
+ ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts new file mode 100644 index 0000000000000..50e59fc00cffe --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts @@ -0,0 +1,115 @@ +/* + * 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'; +import { NotificationsStart } from 'kibana/public'; +import { trackEvent } from '../../../../../../../infra/public/hooks/use_track_metric'; +import { isRumAgentName } from '../../../../../../common/agent_name'; +import { + getOptionLabel, + omitAllOption +} from '../../../../../../common/agent_configuration_constants'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; + +interface Settings { + transaction_sample_rate: number; + capture_body?: string; + transaction_max_spans?: number; +} + +export async function saveConfig({ + serviceName, + environment, + sampleRate, + captureBody, + transactionMaxSpans, + configurationId, + agentName, + toasts +}: { + serviceName: string; + environment: string; + sampleRate: string; + captureBody: string; + transactionMaxSpans: string; + configurationId?: string; + agentName?: string; + toasts: NotificationsStart['toasts']; +}) { + trackEvent({ app: 'apm', name: 'save_agent_configuration' }); + + try { + const settings: Settings = { + transaction_sample_rate: Number(sampleRate) + }; + + if (!isRumAgentName(agentName)) { + settings.capture_body = captureBody; + settings.transaction_max_spans = Number(transactionMaxSpans); + } + + const configuration = { + agent_name: agentName, + service: { + name: omitAllOption(serviceName), + environment: omitAllOption(environment) + }, + settings + }; + + if (configurationId) { + await callApmApi({ + pathname: '/api/apm/settings/agent-configuration/{configurationId}', + method: 'PUT', + params: { + path: { configurationId }, + body: configuration + } + }); + } else { + await callApmApi({ + pathname: '/api/apm/settings/agent-configuration/new', + method: 'POST', + params: { + body: configuration + } + }); + } + + toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.succeeded.title', + { defaultMessage: 'Configuration saved' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.succeeded.text', + { + defaultMessage: + 'The configuration for "{serviceName}" was saved. It will take some time to propagate to the agents.', + values: { serviceName: getOptionLabel(serviceName) } + } + ) + }); + } catch (error) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.failed.title', + { defaultMessage: 'Configuration could not be saved' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.failed.text', + { + defaultMessage: + 'Something went wrong when saving the configuration for "{serviceName}". Error: "{errorMessage}"', + values: { + serviceName: getOptionLabel(serviceName), + errorMessage: error.message + } + } + ) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/SettingsList.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx similarity index 51% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/SettingsList.tsx rename to x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx index 00fcd09cdcf30..161d371148478 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/SettingsList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx @@ -5,17 +5,26 @@ */ import React from 'react'; -import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { EuiEmptyPrompt, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiButton, + EuiButtonEmpty, + EuiHealth, + EuiToolTip +} from '@elastic/eui'; import { isEmpty } from 'lodash'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; -import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; -import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations'; import { Config } from '.'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { px, units } from '../../../../style/variables'; +import { getOptionLabel } from '../../../../../common/agent_configuration_constants'; -export function SettingsList({ +export function AgentConfigurationList({ status, data, setIsFlyoutOpen, @@ -23,21 +32,44 @@ export function SettingsList({ }: { status: FETCH_STATUS; data: AgentConfigurationListAPIResponse; - setIsFlyoutOpen: React.Dispatch>; - setSelectedConfig: React.Dispatch>; + setIsFlyoutOpen: (val: boolean) => void; + setSelectedConfig: (val: Config | null) => void; }) { const columns: Array> = [ + { + field: 'applied_by_agent', + align: 'center', + width: px(units.double), + name: '', + sortable: true, + render: (isApplied: boolean) => ( + + + + ) + }, { field: 'service.name', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel', - { - defaultMessage: 'Service name' - } + { defaultMessage: 'Service name' } ), sortable: true, render: (_, config: Config) => ( { @@ -45,7 +77,7 @@ export function SettingsList({ setIsFlyoutOpen(true); }} > - {config.service.name} + {getOptionLabel(config.service.name)} ) }, @@ -53,50 +85,64 @@ export function SettingsList({ field: 'service.environment', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.environmentColumnLabel', - { - defaultMessage: 'Service environment' - } + { defaultMessage: 'Service environment' } ), sortable: true, - render: (value: string) => value + render: (value: string) => getOptionLabel(value) }, { field: 'settings.transaction_sample_rate', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.sampleRateColumnLabel', - { - defaultMessage: 'Sample rate' - } + { defaultMessage: 'Sample rate' } + ), + dataType: 'number', + sortable: true, + render: (value: number) => value + }, + { + field: 'settings.capture_body', + name: i18n.translate( + 'xpack.apm.settings.agentConf.configTable.captureBodyColumnLabel', + { defaultMessage: 'Capture body' } ), sortable: true, render: (value: string) => value }, { + field: 'settings.transaction_max_spans', + name: i18n.translate( + 'xpack.apm.settings.agentConf.configTable.transactionMaxSpansColumnLabel', + { defaultMessage: 'Transaction max spans' } + ), + dataType: 'number', + sortable: true, + render: (value: number) => value + }, + { + align: 'right', field: '@timestamp', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.lastUpdatedColumnLabel', - { - defaultMessage: 'Last updated' - } + { defaultMessage: 'Last updated' } ), sortable: true, - render: (value: number) => (value ? moment(value).fromNow() : null) + render: (value: number) => ( + + ) }, { + width: px(units.double), name: '', actions: [ { name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.editButtonLabel', - { - defaultMessage: 'Edit' - } + { defaultMessage: 'Edit' } ), description: i18n.translate( 'xpack.apm.settings.agentConf.configTable.editButtonDescription', - { - defaultMessage: 'Edit this config' - } + { defaultMessage: 'Edit this config' } ), icon: 'pencil', color: 'primary', @@ -137,10 +183,8 @@ export function SettingsList({ actions={ setIsFlyoutOpen(true)}> {i18n.translate( - 'xpack.apm.settings.agentConf.createConfigButtonLabel', - { - defaultMessage: 'Create configuration' - } + 'xpack.apm.settings.agentConf.configTable.createConfigButtonLabel', + { defaultMessage: 'Create configuration' } )} } @@ -154,7 +198,7 @@ export function SettingsList({ <>

{i18n.translate( - 'xpack.apm.settings.agentConf.configTable.failurePromptText', + 'xpack.apm.settings.agentConf.configTable.configTable.failurePromptText', { defaultMessage: 'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.' @@ -165,26 +209,23 @@ export function SettingsList({ } /> ); - const hasConfigurations = !isEmpty(data); if (status === 'failure') { return failurePrompt; } - if (status === 'success') { - if (hasConfigurations) { - return ( - } - columns={columns} - items={data} - initialSortField="service.name" - initialSortDirection="asc" - initialPageSize={50} - /> - ); - } else { - return emptyStatePrompt; - } + + if (status === 'success' && isEmpty(data)) { + return emptyStatePrompt; } - return null; + + return ( + } + columns={columns} + items={data} + initialSortField="service.name" + initialSortDirection="asc" + initialPageSize={50} + /> + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx new file mode 100644 index 0000000000000..002c735da8eb3 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -0,0 +1,140 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiPanel, + EuiSpacer, + EuiButton +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations'; +import { callApmApi } from '../../../../services/rest/callApmApi'; +import { HomeLink } from '../../../shared/Links/apm/HomeLink'; +import { AgentConfigurationList } from './AgentConfigurationList'; +import { useTrackPageview } from '../../../../../../infra/public'; +import { AddEditFlyout } from './AddEditFlyout'; + +export type Config = AgentConfigurationListAPIResponse[0]; + +export function AgentConfigurations() { + const { data = [], status, refetch } = useFetcher( + () => callApmApi({ pathname: `/api/apm/settings/agent-configuration` }), + [], + { preservePreviousData: false } + ); + const [selectedConfig, setSelectedConfig] = useState(null); + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + + useTrackPageview({ app: 'apm', path: 'agent_configuration' }); + useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); + + const hasConfigurations = !isEmpty(data); + + const onClose = () => { + setSelectedConfig(null); + setIsFlyoutOpen(false); + }; + + return ( + <> + {isFlyoutOpen && ( + { + onClose(); + refetch(); + }} + onDeleted={() => { + onClose(); + refetch(); + }} + /> + )} + + + + +

+ {i18n.translate('xpack.apm.settings.agentConf.pageTitle', { + defaultMessage: 'Settings' + })} +

+ + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.returnToOverviewLinkLabel', + { defaultMessage: 'Return to overview' } + )} + + + + + + + + + + + +

+ {i18n.translate( + 'xpack.apm.settings.agentConf.configurationsPanelTitle', + { defaultMessage: 'Agent remote configuration' } + )} +

+
+
+ + {hasConfigurations ? ( + setIsFlyoutOpen(true)} /> + ) : null} +
+ + + + +
+ + ); +} + +function CreateConfigurationButton({ onClick }: { onClick: () => void }) { + return ( + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.createConfigButtonLabel', + { defaultMessage: 'Create configuration' } + )} + + + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx deleted file mode 100644 index b75d3cf6ff458..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx +++ /dev/null @@ -1,187 +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 React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiPanel, - EuiBetaBadge, - EuiSpacer, - EuiCallOut, - EuiButton, - EuiLink -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations'; -import { AddSettingsFlyout } from './AddSettings/AddSettingFlyout'; -import { callApmApi } from '../../../services/rest/callApmApi'; -import { HomeLink } from '../../shared/Links/apm/HomeLink'; -import { SettingsList } from './SettingsList'; -import { useTrackPageview } from '../../../../../infra/public'; - -export type Config = AgentConfigurationListAPIResponse[0]; - -export function Settings() { - const { data = [], status, refresh } = useFetcher( - () => - callApmApi({ - pathname: `/api/apm/settings/agent-configuration` - }), - [] - ); - const [selectedConfig, setSelectedConfig] = useState(null); - const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); - - useTrackPageview({ app: 'apm', path: 'agent_configuration' }); - useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); - - const RETURN_TO_OVERVIEW_LINK_LABEL = i18n.translate( - 'xpack.apm.settings.agentConf.returnToOverviewLinkLabel', - { - defaultMessage: 'Return to overview' - } - ); - - const hasConfigurations = !isEmpty(data); - - return ( - <> - { - setSelectedConfig(null); - setIsFlyoutOpen(false); - }} - onSubmit={() => { - setSelectedConfig(null); - setIsFlyoutOpen(false); - refresh(); - }} - /> - - - - -

- {i18n.translate('xpack.apm.settings.agentConf.pageTitle', { - defaultMessage: 'Settings' - })} -

-
-
- - - - {RETURN_TO_OVERVIEW_LINK_LABEL} - - - -
- - - - - - - -

- {i18n.translate( - 'xpack.apm.settings.agentConf.configurationsPanelTitle', - { - defaultMessage: 'Configurations' - } - )} -

-
-
- - - - {hasConfigurations ? ( - - - - setIsFlyoutOpen(true)} - > - {i18n.translate( - 'xpack.apm.settings.agentConf.createConfigButtonLabel', - { - defaultMessage: 'Create configuration' - } - )} - - - - - ) : null} -
- - - - -

- - {i18n.translate( - 'xpack.apm.settings.agentConf.agentConfigDocsLinkLabel', - { defaultMessage: 'Learn more in our docs.' } - )} - - ) - }} - /> -

-
- - - - -
- - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx deleted file mode 100644 index 4a8796da63156..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx +++ /dev/null @@ -1,191 +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'; -import React from 'react'; -import { idx } from '@kbn/elastic-idx'; -import styled from 'styled-components'; -import { - TRANSACTION_DURATION, - TRANSACTION_RESULT, - URL_FULL, - USER_ID, - TRANSACTION_PAGE_URL -} from '../../../../../common/elasticsearch_fieldnames'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; -import { asTime } from '../../../../utils/formatters'; -import { - IStickyProperty, - StickyProperties -} from '../../../shared/StickyProperties'; -import { ErrorCountBadge } from './ErrorCountBadge'; -import { isRumAgentName } from '../../../../../common/agent_name'; -import { fontSize } from '../../../../style/variables'; -import { PercentOfParent } from './PercentOfParent'; - -interface Props { - transaction: Transaction; - totalDuration?: number; - errorCount: number; -} - -const ErrorTitle = styled.span` - font-size: ${fontSize}; -`; - -export function StickyTransactionProperties({ - transaction, - totalDuration, - errorCount -}: Props) { - const timestamp = transaction['@timestamp']; - - const isRumAgent = isRumAgentName(transaction.agent.name); - const { urlFieldName, urlValue } = isRumAgent - ? { - urlFieldName: TRANSACTION_PAGE_URL, - urlValue: idx(transaction, _ => _.transaction.page.url) - } - : { - urlFieldName: URL_FULL, - urlValue: idx(transaction, _ => _.url.full) - }; - - const duration = transaction.transaction.duration.us; - - const noErrorsText = i18n.translate( - 'xpack.apm.transactionDetails.errorsNone', - { - defaultMessage: 'None' - } - ); - - const stickyProperties: IStickyProperty[] = [ - { - label: i18n.translate('xpack.apm.transactionDetails.timestampLabel', { - defaultMessage: 'Timestamp' - }), - fieldName: '@timestamp', - val: timestamp, - truncated: true, - width: '50%' - }, - { - fieldName: urlFieldName, - label: 'URL', - val: urlValue || NOT_AVAILABLE_LABEL, - truncated: true, - width: '50%' - }, - { - label: i18n.translate('xpack.apm.transactionDetails.durationLabel', { - defaultMessage: 'Duration' - }), - fieldName: TRANSACTION_DURATION, - val: asTime(duration), - width: '25%' - }, - { - label: i18n.translate( - 'xpack.apm.transactionDetails.percentOfTraceLabel', - { - defaultMessage: '% of trace' - } - ), - val: ( - - ), - width: '25%' - }, - { - label: i18n.translate('xpack.apm.transactionDetails.resultLabel', { - defaultMessage: 'Result' - }), - fieldName: TRANSACTION_RESULT, - val: idx(transaction, _ => _.transaction.result) || NOT_AVAILABLE_LABEL, - width: '14%' - }, - { - label: i18n.translate( - 'xpack.apm.transactionDetails.errorsOverviewLabel', - { - defaultMessage: 'Errors' - } - ), - val: errorCount ? ( - <> - {errorCount} - -   - {i18n.translate('xpack.apm.transactionDetails.errorsOverviewLink', { - values: { errorCount }, - defaultMessage: - '{errorCount, plural, one {Related error} other {Related errors}}' - })} - - - ) : ( - noErrorsText - ), - width: '18%' - }, - { - label: i18n.translate('xpack.apm.transactionDetails.userIdLabel', { - defaultMessage: 'User ID' - }), - fieldName: USER_ID, - val: idx(transaction, _ => _.user.id) || NOT_AVAILABLE_LABEL, - truncated: true, - width: '18%' - } - ]; - - const { user_agent: userAgent } = transaction; - - if (userAgent) { - const { os, device } = userAgent; - const width = '25%'; - stickyProperties.push({ - label: i18n.translate('xpack.apm.transactionDetails.userAgentLabel', { - defaultMessage: 'User agent' - }), - val: [userAgent.name, userAgent.version].filter(Boolean).join(' '), - truncated: true, - width - }); - - if (os) { - stickyProperties.push({ - label: i18n.translate('xpack.apm.transactionDetails.userAgentOsLabel', { - defaultMessage: 'User agent OS' - }), - val: os.full || os.name, - truncated: true, - width - }); - } - - if (device) { - stickyProperties.push({ - label: i18n.translate( - 'xpack.apm.transactionDetails.userAgentDeviceLabel', - { - defaultMessage: 'User agent device' - } - ), - val: device.name, - width - }); - } - } - - return ; -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 43edd3b7eb559..7e9171197251d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -24,7 +24,7 @@ import styled from 'styled-components'; import { idx } from '@kbn/elastic-idx'; import { px, units } from '../../../../../../../style/variables'; import { Summary } from '../../../../../../shared/Summary'; -import { TimestampSummaryItem } from '../../../../../../shared/Summary/TimestampSummaryItem'; +import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem'; import { Span } from '../../../../../../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction'; @@ -143,7 +143,7 @@ export function SpanFlyout({ , + , { + clearCache(); refreshTimeRange({ rangeFrom: start, rangeTo: end }); }} onRefreshChange={onRefreshChange} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js index f1d306c57ee8a..b6acb6904f865 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js @@ -15,11 +15,6 @@ describe('StickyProperties', () => { it('should render entire component', () => { const stickyProperties = [ - { - label: 'Timestamp', - fieldName: '@timestamp', - val: 1536405447640 - }, { fieldName: URL_FULL, label: 'URL', @@ -51,22 +46,6 @@ describe('StickyProperties', () => { }); describe('values', () => { - it('should render timestamp when fieldName is `@timestamp`', () => { - const stickyProperties = [ - { - label: 'My Timestamp', - fieldName: '@timestamp', - val: 1536405447640 - } - ]; - - const wrapper = shallow( - - ).find('TimestampValue'); - - expect(wrapper).toMatchSnapshot(); - }); - it('should render numbers', () => { const stickyProperties = [ { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap index 860a7a3be7ddf..020d5952c03e4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap @@ -20,35 +20,6 @@ exports[`StickyProperties should render entire component 1`] = ` "padding": "1em 1em 1em 0", } } - > - - - @timestamp - - } - delay="regular" - position="top" - > - - Timestamp - - - - - - `; - -exports[`StickyProperties values should render timestamp when fieldName is \`@timestamp\` 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx index 5354ca87a1b4f..c0c7922c42ea2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx @@ -7,10 +7,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import moment from 'moment'; import React from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { fontFamilyCode, fontSizes, @@ -42,10 +40,6 @@ const PropertyLabel = styled.div` `; PropertyLabel.displayName = 'PropertyLabel'; -const PropertyValueDimmed = styled.span` - color: ${theme.euiColorMediumShade}; -`; - const propertyValueLineHeight = 1.2; const PropertyValue = styled.div` display: inline-block; @@ -59,20 +53,6 @@ const PropertyValueTruncated = styled.span` ${truncate('100%')}; `; -function TimestampValue({ timestamp }: { timestamp: Date }) { - const time = moment(timestamp); - const timeAgo = timestamp ? time.fromNow() : NOT_AVAILABLE_LABEL; - const timestampFull = timestamp - ? time.format('MMMM Do YYYY, HH:mm:ss.SSS') - : NOT_AVAILABLE_LABEL; - - return ( - - {timeAgo} ({timestampFull}) - - ); -} - function getPropertyLabel({ fieldName, label }: Partial) { if (fieldName) { return ( @@ -92,10 +72,6 @@ function getPropertyValue({ fieldName, truncated = false }: Partial) { - if (fieldName === '@timestamp') { - return ; - } - if (truncated) { return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx deleted file mode 100644 index 8d619d94067cc..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx +++ /dev/null @@ -1,26 +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 React from 'react'; -import { EuiToolTip } from '@elastic/eui'; -import moment from 'moment-timezone'; - -interface Props { - time: number; -} - -const TimestampSummaryItem = (props: Props) => { - const time = moment.tz(props.time, moment.tz.guess()); - const relativeTimeLabel = time.fromNow(); - const absoluteTimeLabel = time.format('MMM Do YYYY HH:mm:ss.SSS zz'); - - return ( - - <>{relativeTimeLabel} - - ); -}; - -export { TimestampSummaryItem }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 60ee39875fe37..8f91b8cc5e2af 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { idx } from '@kbn/elastic-idx'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { Summary } from './'; -import { TimestampSummaryItem } from './TimestampSummaryItem'; +import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; import { ErrorCountSummaryItem } from './ErrorCountSummaryItem'; import { isRumAgentName } from '../../../../common/agent_name'; @@ -48,7 +48,7 @@ const TransactionSummary = ({ errorCount }: Props) => { const items = [ - , + , + <>{relativeTimeLabel} + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx index 4932992eca5e2..f30a818d048ff 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx @@ -52,7 +52,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -63,7 +63,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -75,7 +75,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'response from hook', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'success' }); }); @@ -96,7 +96,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -107,7 +107,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -119,7 +119,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: expect.any(Error), - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'failure' }); }); @@ -142,7 +142,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); @@ -152,7 +152,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'first response', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'success' }); @@ -171,7 +171,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'first response', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); @@ -182,7 +182,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'second response', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'success' }); }); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index f5c726029ba19..d9fa73b209cb4 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -31,7 +31,7 @@ export function useFetcher( options?: { preservePreviousData?: boolean; } -): Result & { refresh: () => void }; +): Result & { refetch: () => void }; // To avoid infinite rescursion when infering the type of `TState` `initialState` must be given if `prevResult` is consumed export function useFetcher( @@ -41,7 +41,7 @@ export function useFetcher( preservePreviousData?: boolean; initialState: TState; } -): Result & { refresh: () => void }; +): Result & { refetch: () => void }; export function useFetcher( fn: Function, @@ -146,14 +146,13 @@ export function useFetcher( /* eslint-enable react-hooks/exhaustive-deps */ ]); - return useMemo( - () => ({ + return useMemo(() => { + return { ...result, - refresh: () => { + refetch: () => { // this will invalidate the deps to `useEffect` and will result in a new request setCounter(count => count + 1); } - }), - [result] - ); + }; + }, [result]); } diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts index f344452a81693..9fb6c4d341825 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts @@ -6,7 +6,7 @@ import * as kfetchModule from 'ui/kfetch'; import { mockNow } from '../../utils/testHelpers'; -import { _clearCache, callApi } from '../rest/callApi'; +import { clearCache, callApi } from '../rest/callApi'; import { SessionStorageMock } from './SessionStorageMock'; jest.mock('ui/kfetch'); @@ -24,7 +24,7 @@ describe('callApi', () => { afterEach(() => { kfetchSpy.mockClear(); - _clearCache(); + clearCache(); }); describe('apm_debug', () => { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index 27842a19a322a..3cae0604e52fb 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -31,12 +31,12 @@ function fetchOptionsWithDebug(fetchOptions: KFetchOptions) { const cache = new LRU({ max: 100, maxAge: 1000 * 60 * 60 }); -export function _clearCache() { +export function clearCache() { cache.reset(); } export async function callApi( - fetchOptions: KFetchOptions, + fetchOptions: KFetchOptions & { forceCache?: boolean }, options?: KFetchKibanaOptions ): Promise { const cacheKey = getCacheKey(fetchOptions); @@ -57,7 +57,11 @@ export async function callApi( // only cache items that has a time range with `start` and `end` params, // and where `end` is not a timestamp in the future -function isCachable(fetchOptions: KFetchOptions) { +function isCachable(fetchOptions: KFetchOptions & { forceCache?: boolean }) { + if (fetchOptions.forceCache) { + return true; + } + if ( !(fetchOptions.query && fetchOptions.query.start && fetchOptions.query.end) ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index ddd800dc22bf3..71217ccc36f7b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -110,7 +110,7 @@ Object { "myIndex", "myIndex", ], - "terminate_after": 1, + "terminateAfter": 1, } `; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts index ebe0ba9827b53..3c6f684389970 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -9,18 +9,14 @@ import { SERVICE_AGENT_NAME, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; -export type ServiceAgentNameAPIResponse = PromiseReturnType< - typeof getServiceAgentName ->; export async function getServiceAgentName(serviceName: string, setup: Setup) { const { start, end, client, config } = setup; const params = { - terminate_after: 1, + terminateAfter: 1, index: [ config.get('apm_oss.errorIndices'), config.get('apm_oss.transactionIndices'), diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts index c8053c57776db..a1f035da9dc1a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -9,13 +9,9 @@ import { SERVICE_NAME, TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; -export type ServiceTransactionTypesAPIResponse = PromiseReturnType< - typeof getServiceTransactionTypes ->; export async function getServiceTransactionTypes( serviceName: string, setup: Setup diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 4c53563aa41df..d64907691ef9a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -7,7 +7,6 @@ Object { "environments": Object { "terms": Object { "field": "service.environment", - "missing": "ENVIRONMENT_NOT_DEFINED", "size": 100, }, }, @@ -53,21 +52,47 @@ Object { "body": Object { "query": Object { "bool": Object { - "filter": Array [ + "minimum_should_match": 2, + "should": Array [ Object { "term": Object { - "service.name": "foo", + "service.name": Object { + "value": "foo", + }, }, }, Object { "term": Object { - "service.environment": "bar", + "service.environment": Object { + "value": "bar", + }, + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.environment", + }, + }, + ], }, }, ], }, }, - "size": 1, }, "index": "myIndex", } @@ -78,25 +103,40 @@ Object { "body": Object { "query": Object { "bool": Object { - "filter": Array [ + "minimum_should_match": 2, + "should": Array [ Object { "term": Object { - "service.name": "foo", + "service.name": Object { + "value": "foo", + }, }, }, Object { "bool": Object { - "must_not": Object { - "exists": Object { - "field": "service.environment", + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, }, - }, + ], + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.environment", + }, + }, + ], }, }, ], }, }, - "size": 1, }, "index": "myIndex", } @@ -109,7 +149,7 @@ Object { "services": Object { "terms": Object { "field": "service.name", - "size": 100, + "size": 50, }, }, }, @@ -145,8 +185,8 @@ Object { "environments": Object { "terms": Object { "field": "service.environment", - "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "missing": "ALL_OPTION_VALUE", + "size": 50, }, }, }, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts index 9ef8c62472d1d..ea8f50c90c1d3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AgentConfigurationIntake { - settings: { - transaction_sample_rate: number; - }; +export interface AgentConfiguration { + '@timestamp': number; + applied_by_agent?: boolean; + etag?: string; + agent_name?: string; service: { - name: string; + name?: string; environment?: string; }; -} - -export interface AgentConfiguration extends AgentConfigurationIntake { - '@timestamp': number; + settings: { + transaction_sample_rate?: number; + capture_body?: string; + transaction_max_spans?: number; + }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index b1b0faa07e49f..4226c073b1de0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -5,7 +5,7 @@ */ import { InternalCoreSetup } from 'src/core/server'; -import Boom from 'boom'; +import { CallCluster } from '../../../../../../../../src/legacy/core_plugins/elasticsearch'; export async function createApmAgentConfigurationIndex( core: InternalCoreSetup @@ -19,57 +19,84 @@ export async function createApmAgentConfigurationIndex( 'admin' ); const indexExists = await callWithInternalUser('indices.exists', { index }); + const result = indexExists + ? await updateExistingIndex(index, callWithInternalUser) + : await createNewIndex(index, callWithInternalUser); - if (!indexExists) { - const result = await callWithInternalUser('indices.create', { - index, - body: { - settings: { - 'index.auto_expand_replicas': '0-1' - }, - mappings: { - properties: { - '@timestamp': { - type: 'date' - }, - settings: { - properties: { - transaction_sample_rate: { - type: 'scaled_float', - scaling_factor: 1000, - ignore_malformed: true, - coerce: false - } - } - }, - service: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024 - }, - environment: { - type: 'keyword', - ignore_above: 1024 - } - } - } - } - } - } - }); - - if (!result.acknowledged) { - const err = new Error( - `Unable to create APM Agent Configuration index '${index}'` - ); - // eslint-disable-next-line - console.error(err.stack); - throw Boom.boomify(err, { statusCode: 500 }); - } + if (!result.acknowledged) { + const resultError = + result && result.error && JSON.stringify(result.error); + throw new Error( + `Unable to create APM Agent Configuration index '${index}': ${resultError}` + ); } } catch (e) { // eslint-disable-next-line no-console console.error('Could not create APM Agent configuration:', e.message); } } + +function createNewIndex(index: string, callWithInternalUser: CallCluster) { + return callWithInternalUser('indices.create', { + index, + body: { + settings: { 'index.auto_expand_replicas': '0-1' }, + mappings: { properties: mappingProperties } + } + }); +} + +// Necessary for migration reasons +// Added in 7.5: `capture_body`, `transaction_max_spans`, `applied_by_agent`, `agent_name` and `etag` +function updateExistingIndex(index: string, callWithInternalUser: CallCluster) { + return callWithInternalUser('indices.putMapping', { + index, + body: { properties: mappingProperties } + }); +} + +const mappingProperties = { + '@timestamp': { + type: 'date' + }, + service: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024 + }, + environment: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + settings: { + properties: { + transaction_sample_rate: { + type: 'scaled_float', + scaling_factor: 1000, + ignore_malformed: true, + coerce: false + }, + capture_body: { + type: 'keyword', + ignore_above: 1024 + }, + transaction_max_spans: { + type: 'short' + } + } + }, + applied_by_agent: { + type: 'boolean' + }, + agent_name: { + type: 'keyword', + ignore_above: 1024 + }, + etag: { + type: 'keyword', + ignore_above: 1024 + } +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts new file mode 100644 index 0000000000000..6450040098cd4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -0,0 +1,49 @@ +/* + * 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 hash from 'object-hash'; +import { IndexDocumentParams } from 'elasticsearch'; +import { Setup } from '../../helpers/setup_request'; +import { AgentConfiguration } from './configuration_types'; + +export async function createOrUpdateConfiguration({ + configurationId, + configuration, + setup +}: { + configurationId?: string; + configuration: Omit< + AgentConfiguration, + '@timestamp' | 'applied_by_agent' | 'etag' + >; + setup: Setup; +}) { + const { client, config } = setup; + + const params: IndexDocumentParams = { + type: '_doc', + refresh: true, + index: config.get('apm_oss.apmAgentConfigurationIndex'), + body: { + agent_name: configuration.agent_name, + service: { + name: configuration.service.name, + environment: configuration.service.environment + }, + settings: configuration.settings, + '@timestamp': Date.now(), + applied_by_agent: false, + etag: hash(configuration) + } + }; + + // by specifying an id elasticsearch will delete the previous doc and insert the updated doc + if (configurationId) { + params.id = configurationId; + } + + return client.index(params); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts new file mode 100644 index 0000000000000..a18873e86c5a8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -0,0 +1,54 @@ +/* + * 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 { idx } from '@kbn/elastic-idx'; +import { Setup } from '../../helpers/setup_request'; +import { + PROCESSOR_EVENT, + SERVICE_NAME +} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; + +export async function getAgentNameByService({ + serviceName, + setup +}: { + serviceName: string; + setup: Setup; +}) { + const { client, config } = setup; + + const params = { + terminateAfter: 1, + index: [ + config.get('apm_oss.metricsIndices'), + config.get('apm_oss.errorIndices'), + config.get('apm_oss.transactionIndices') + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } + }, + { term: { [SERVICE_NAME]: serviceName } } + ] + } + }, + aggs: { + agent_names: { + terms: { field: SERVICE_AGENT_NAME, size: 1 } + } + } + } + }; + + const { aggregations } = await client.search(params); + const agentName = idx(aggregations, _ => _.agent_names.buckets[0].key); + return { agentName }; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts index 0b27e036d2967..76ebf75aada29 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts @@ -11,17 +11,22 @@ import { SERVICE_NAME, SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration_constants'; export async function getAllEnvironments({ serviceName, setup }: { - serviceName: string; + serviceName: string | undefined; setup: Setup; }) { const { client, config } = setup; + // omit filter for service.name if "All" option is selected + const serviceNameFilter = serviceName + ? [{ term: { [SERVICE_NAME]: serviceName } }] + : []; + const params = { index: [ config.get('apm_oss.metricsIndices'), @@ -36,7 +41,7 @@ export async function getAllEnvironments({ { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { term: { [SERVICE_NAME]: serviceName } } + ...serviceNameFilter ] } }, @@ -44,7 +49,6 @@ export async function getAllEnvironments({ environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED, size: 100 } } @@ -54,5 +58,6 @@ export async function getAllEnvironments({ const resp = await client.search(params); const buckets = idx(resp.aggregations, _ => _.environments.buckets) || []; - return buckets.map(bucket => bucket.key); + const environments = buckets.map(bucket => bucket.key); + return [ALL_OPTION_VALUE, ...environments]; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_unavailable_environments.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts similarity index 69% rename from x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_unavailable_environments.ts rename to x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index fae12549d4dbd..120cc62cc3bc9 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_unavailable_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -10,32 +10,32 @@ import { SERVICE_NAME, SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration_constants'; -export async function getUnavailableEnvironments({ +export async function getExistingEnvironmentsForService({ serviceName, setup }: { - serviceName: string; + serviceName: string | undefined; setup: Setup; }) { const { client, config } = setup; + const bool = serviceName + ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } + : { must_not: [{ exists: { field: SERVICE_NAME } }] }; + const params = { index: config.get('apm_oss.apmAgentConfigurationIndex'), body: { size: 0, - query: { - bool: { - filter: [{ term: { [SERVICE_NAME]: serviceName } }] - } - }, + query: { bool }, aggs: { environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED, - size: 100 + missing: ALL_OPTION_VALUE, + size: 50 } } } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index e4f986ed4184c..c05b4e113deb5 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -7,7 +7,7 @@ import { getAllEnvironments } from './get_all_environments'; import { Setup } from '../../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../typings/common'; -import { getUnavailableEnvironments } from './get_unavailable_environments'; +import { getExistingEnvironmentsForService } from './get_existing_environments_for_service'; export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType< typeof getEnvironments @@ -17,18 +17,18 @@ export async function getEnvironments({ serviceName, setup }: { - serviceName: string; + serviceName: string | undefined; setup: Setup; }) { - const [allEnvironments, unavailableEnvironments] = await Promise.all([ + const [allEnvironments, existingEnvironments] = await Promise.all([ getAllEnvironments({ serviceName, setup }), - getUnavailableEnvironments({ serviceName, setup }) + getExistingEnvironmentsForService({ serviceName, setup }) ]); return allEnvironments.map(environment => { return { name: environment, - available: !unavailableEnvironments.includes(environment) + alreadyConfigured: existingEnvironments.includes(environment) }; }); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index e84334cb7db56..55af96acbc719 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -11,6 +11,7 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; +import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration_constants'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames @@ -37,7 +38,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { services: { terms: { field: SERVICE_NAME, - size: 100 + size: 50 } } } @@ -46,5 +47,6 @@ export async function getServiceNames({ setup }: { setup: Setup }) { const resp = await client.search(params); const buckets = idx(resp.aggregations, _ => _.services.buckets) || []; - return buckets.map(bucket => bucket.key).sort(); + const serviceNames = buckets.map(bucket => bucket.key).sort(); + return [ALL_OPTION_VALUE, ...serviceNames]; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts similarity index 53% rename from x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_configuration.ts rename to x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts index 42d324dda82f2..ea2b15e6985d5 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts @@ -5,30 +5,28 @@ */ import { Setup } from '../../helpers/setup_request'; -import { PromiseReturnType } from '../../../../typings/common'; -import { AgentConfigurationIntake } from './configuration_types'; +import { AgentConfiguration } from './configuration_types'; -export type CreateAgentConfigurationAPIResponse = PromiseReturnType< - typeof createConfiguration ->; -export async function createConfiguration({ - configuration, +export async function markAppliedByAgent({ + id, + body, setup }: { - configuration: AgentConfigurationIntake; + id: string; + body: AgentConfiguration; setup: Setup; }) { const { client, config } = setup; const params = { type: '_doc', - refresh: true, index: config.get('apm_oss.apmAgentConfigurationIndex'), + id, // by specifying the `id` elasticsearch will do an "upsert" body: { - '@timestamp': Date.now(), - ...configuration + ...body, + applied_by_agent: true } }; - return client.index(params); + return client.index(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index 768651c8acec7..31b0937a1b957 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -5,7 +5,7 @@ */ import { getAllEnvironments } from './get_environments/get_all_environments'; -import { getUnavailableEnvironments } from './get_environments/get_unavailable_environments'; +import { getExistingEnvironmentsForService } from './get_environments/get_existing_environments_for_service'; import { getServiceNames } from './get_service_names'; import { listConfigurations } from './list_configurations'; import { searchConfigurations } from './search'; @@ -34,7 +34,7 @@ describe('agent configuration queries', () => { it('fetches unavailable environments', async () => { mock = await inspectSearchParams(setup => - getUnavailableEnvironments({ + getExistingEnvironmentsForService({ serviceName: 'foo', setup }) diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts new file mode 100644 index 0000000000000..982077e2e6665 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts @@ -0,0 +1,74 @@ +/* + * 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 searchMocks = { + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0 + }, + hits: { + total: { + value: 3, + relation: 'eq' + }, + max_score: 0.9808292, + hits: [ + { + _index: '.apm-agent-configuration', + _type: '_doc', + _id: '-aQHsm0BxZLczArvNQYW', + _score: 0.9808292, + _source: { + service: { + environment: 'production' + }, + settings: { + transaction_sample_rate: 0.3 + }, + '@timestamp': 1570649879829, + applied_by_agent: false, + etag: 'c511f4c1df457371c4446c9c4925662e18726f51' + } + }, + { + _index: '.apm-agent-configuration', + _type: '_doc', + _id: '-KQHsm0BxZLczArvNAb0', + _score: 0.18232156, + _source: { + service: { + name: 'my_service' + }, + settings: { + transaction_sample_rate: 0.2 + }, + '@timestamp': 1570649879795, + applied_by_agent: false, + etag: 'a13cd8fee5a2fcc2ae773a60a4deaf7f76b90a65' + } + }, + { + _index: '.apm-agent-configuration', + _type: '_doc', + _id: '96QHsm0BxZLczArvNAbD', + _score: 0.0, + _source: { + service: {}, + settings: { + transaction_sample_rate: 0.1 + }, + '@timestamp': 1570649879743, + applied_by_agent: false, + etag: 'c7f4ba16f00a9c9bf3c49024c5b6d4632ff05ff5' + } + } + ] + } +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts new file mode 100644 index 0000000000000..e8db37891e7ae --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { searchConfigurations } from './search'; +import { searchMocks } from './search.mocks'; +import { Setup } from '../../helpers/setup_request'; + +describe('search configurations', () => { + it('should return configuration by matching on service.name', async () => { + const res = await searchConfigurations({ + serviceName: 'my_service', + environment: 'production', + setup: ({ + config: { get: () => '' }, + client: { search: async () => searchMocks } + } as unknown) as Setup + }); + + expect(res!._source.service).toEqual({ name: 'my_service' }); + expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.2 }); + }); + + it('should return configuration by matching on "production" env', async () => { + const res = await searchConfigurations({ + serviceName: 'non_existing_service', + environment: 'production', + setup: ({ + config: { get: () => '' }, + client: { search: async () => searchMocks } + } as unknown) as Setup + }); + + expect(res!._source.service).toEqual({ environment: 'production' }); + expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.3 }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts index 0cf6987112b0f..664bcb9325472 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from 'elasticsearch'; -import { PromiseReturnType } from '../../../../typings/common'; +import { ESSearchHit } from 'elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT @@ -13,9 +12,6 @@ import { import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from './configuration_types'; -export type SearchAgentConfigurationsAPIResponse = PromiseReturnType< - typeof searchConfigurations ->; export async function searchConfigurations({ serviceName, environment, @@ -27,26 +23,61 @@ export async function searchConfigurations({ }) { const { client, config } = setup; - const filters: ESFilter[] = [{ term: { [SERVICE_NAME]: serviceName } }]; + // sorting order + // 1. exact match: service.name AND service.environment (eg. opbeans-node / production) + // 2. Partial match: service.name and no service.environment (eg. opbeans-node / All) + // 3. Partial match: service.environment and no service.name (eg. All / production) + // 4. Catch all: no service.name and no service.environment (eg. All / All) - if (environment) { - filters.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); - } else { - filters.push({ - bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } } - }); - } + const environmentFilter = environment + ? [{ term: { [SERVICE_ENVIRONMENT]: { value: environment } } }] + : []; const params = { index: config.get('apm_oss.apmAgentConfigurationIndex'), body: { - size: 1, query: { - bool: { filter: filters } + bool: { + minimum_should_match: 2, + should: [ + { term: { [SERVICE_NAME]: { value: serviceName } } }, + ...environmentFilter, + { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, + { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } } + ] + } } } }; const resp = await client.search(params); - return resp.hits.hits[0]; + const { hits } = resp.hits; + + const exactMatch = hits.find( + hit => + hit._source.service.name === serviceName && + hit._source.service.environment === environment + ); + + if (exactMatch) { + return exactMatch; + } + + const matchWithServiceName = hits.find( + hit => hit._source.service.name === serviceName + ); + + if (matchWithServiceName) { + return matchWithServiceName; + } + + const matchWithEnvironment = hits.find( + hit => hit._source.service.environment === environment + ); + + if (matchWithEnvironment) { + return matchWithEnvironment; + } + + return resp.hits.hits[0] as ESSearchHit | undefined; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts deleted file mode 100644 index ad7c789efa94c..0000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts +++ /dev/null @@ -1,37 +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 { Setup } from '../../helpers/setup_request'; -import { PromiseReturnType } from '../../../../typings/common'; -import { AgentConfigurationIntake } from './configuration_types'; - -export type UpdateAgentConfigurationAPIResponse = PromiseReturnType< - typeof updateConfiguration ->; -export async function updateConfiguration({ - configurationId, - configuration, - setup -}: { - configurationId: string; - configuration: AgentConfigurationIntake; - setup: Setup; -}) { - const { client, config } = setup; - - const params = { - type: '_doc', - id: configurationId, - refresh: true, - index: config.get('apm_oss.apmAgentConfigurationIndex'), - body: { - '@timestamp': Date.now(), - ...configuration - } - }; - - return client.index(params); -} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index b15f4bf677927..c440ee9c1ecbe 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -11,14 +11,9 @@ import { TRANSACTION_DURATION, TRANSACTION_TYPE } from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; import { Setup } from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; -export type TransactionAvgDurationByCountryAPIResponse = PromiseReturnType< - typeof getTransactionAvgDurationByCountry ->; - export async function getTransactionAvgDurationByCountry({ setup, serviceName diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts index 1d120e82bc6a2..b3c1c6603f315 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -16,16 +16,11 @@ import { TRANSACTION_BREAKDOWN_COUNT, PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; import { Setup } from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; import { getMetricsDateHistogramParams } from '../../helpers/metrics'; import { MAX_KPIS, COLORS } from './constants'; -export type TransactionBreakdownAPIResponse = PromiseReturnType< - typeof getTransactionBreakdown ->; - export async function getTransactionBreakdown({ setup, serviceName, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts index 60d9a38b5e662..cdaddc3af3e95 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -10,12 +10,10 @@ import { TRACE_ID, TRANSACTION_ID } from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { rangeFilter } from '../../helpers/range_filter'; import { Setup } from '../../helpers/setup_request'; -export type TransactionAPIResponse = PromiseReturnType; export async function getTransaction( transactionId: string, traceId: string, diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts index 93a82bf6db85b..3b48bfc7a9869 100644 --- a/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -11,14 +11,10 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; -export type EnvironmentUIFilterAPIResponse = PromiseReturnType< - typeof getEnvironments ->; export async function getEnvironments(setup: Setup, serviceName?: string) { const { start, end, client, config } = setup; diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index 92cc0503cb005..682ebf27207c4 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -23,7 +23,8 @@ import { deleteAgentConfigurationRoute, listAgentConfigurationEnvironmentsRoute, listAgentConfigurationServicesRoute, - updateAgentConfigurationRoute + updateAgentConfigurationRoute, + agentConfigurationAgentNameRoute } from './settings'; import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; @@ -50,14 +51,22 @@ import { serviceMapRoute } from './services'; const createApmApi = () => { const api = createApi() + // index pattern .add(indexPatternRoute) + + // Errors .add(errorDistributionRoute) .add(errorGroupsRoute) .add(errorsRoute) + + // Services .add(serviceAgentNameRoute) .add(serviceTransactionTypesRoute) .add(servicesRoute) .add(serviceNodeMetadataRoute) + + // Agent configuration + .add(agentConfigurationAgentNameRoute) .add(agentConfigurationRoute) .add(agentConfigurationSearchRoute) .add(createAgentConfigurationRoute) @@ -65,15 +74,23 @@ const createApmApi = () => { .add(listAgentConfigurationEnvironmentsRoute) .add(listAgentConfigurationServicesRoute) .add(updateAgentConfigurationRoute) + + // Metrics .add(metricsChartsRoute) .add(serviceNodesRoute) + + // Traces .add(tracesRoute) .add(tracesByIdRoute) + + // Transaction groups .add(transactionGroupsBreakdownRoute) .add(transactionGroupsChartsRoute) .add(transactionGroupsDistributionRoute) .add(transactionGroupsRoute) .add(transactionGroupsAvgDurationByCountry) + + // UI filters .add(errorGroupsLocalFiltersRoute) .add(metricsLocalFiltersRoute) .add(servicesLocalFiltersRoute) diff --git a/x-pack/legacy/plugins/apm/server/routes/settings.ts b/x-pack/legacy/plugins/apm/server/routes/settings.ts index 2c4a151a98621..8956e7add09e2 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings.ts @@ -7,14 +7,16 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNames } from '../lib/settings/agent_configuration/get_service_names'; -import { createConfiguration } from '../lib/settings/agent_configuration/create_configuration'; -import { updateConfiguration } from '../lib/settings/agent_configuration/update_configuration'; +import { createOrUpdateConfiguration } from '../lib/settings/agent_configuration/create_or_update_configuration'; import { searchConfigurations } from '../lib/settings/agent_configuration/search'; import { listConfigurations } from '../lib/settings/agent_configuration/list_configurations'; import { getEnvironments } from '../lib/settings/agent_configuration/get_environments'; import { deleteConfiguration } from '../lib/settings/agent_configuration/delete_configuration'; import { createRoute } from './create_route'; import { transactionSampleRateRt } from '../../common/runtime_types/transaction_sample_rate_rt'; +import { transactionMaxSpansRt } from '../../common/runtime_types/transaction_max_spans_rt'; +import { getAgentNameByService } from '../lib/settings/agent_configuration/get_agent_name_by_service'; +import { markAppliedByAgent } from '../lib/settings/agent_configuration/mark_applied_by_agent'; // get list of configurations export const agentConfigurationRoute = createRoute(core => ({ @@ -56,36 +58,46 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({ } })); -const agentPayloadRt = t.type({ - settings: t.type({ - transaction_sample_rate: transactionSampleRateRt +const agentPayloadRt = t.intersection([ + t.partial({ agent_name: t.string }), + t.type({ + service: t.intersection([ + t.partial({ name: t.string }), + t.partial({ environment: t.string }) + ]) }), - service: t.intersection([ - t.type({ - name: t.string - }), - t.partial({ - environments: t.array(t.string) - }) - ]) -}); + t.type({ + settings: t.intersection([ + t.partial({ transaction_sample_rate: transactionSampleRateRt }), + t.partial({ capture_body: t.string }), + t.partial({ transaction_max_spans: transactionMaxSpansRt }) + ]) + }) +]); // get environments for service export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({ - path: - '/api/apm/settings/agent-configuration/services/{serviceName}/environments', + path: '/api/apm/settings/agent-configuration/environments', params: { - path: t.type({ - serviceName: t.string - }) + query: t.partial({ serviceName: t.string }) }, - handler: async (req, { path }) => { + handler: async (req, { query }) => { const setup = await setupRequest(req); - const { serviceName } = path; - return await getEnvironments({ - serviceName, - setup - }); + const { serviceName } = query; + return await getEnvironments({ serviceName, setup }); + } +})); + +// get agentName for service +export const agentConfigurationAgentNameRoute = createRoute(() => ({ + path: '/api/apm/settings/agent-configuration/agent_name', + params: { + query: t.type({ serviceName: t.string }) + }, + handler: async (req, { query }) => { + const setup = await setupRequest(req); + const { serviceName } = query; + return await getAgentNameByService({ serviceName, setup }); } })); @@ -97,16 +109,13 @@ export const createAgentConfigurationRoute = createRoute(() => ({ }, handler: async (req, { body }) => { const setup = await setupRequest(req); - return await createConfiguration({ - configuration: body, - setup - }); + return await createOrUpdateConfiguration({ configuration: body, setup }); } })); export const updateAgentConfigurationRoute = createRoute(() => ({ method: 'PUT', - path: `/api/apm/settings/agent-configuration/{configurationId}`, + path: '/api/apm/settings/agent-configuration/{configurationId}', params: { path: t.type({ configurationId: t.string @@ -116,7 +125,7 @@ export const updateAgentConfigurationRoute = createRoute(() => ({ handler: async (req, { path, body }) => { const setup = await setupRequest(req); const { configurationId } = path; - return await updateConfiguration({ + return await createOrUpdateConfiguration({ configurationId, configuration: body, setup @@ -124,7 +133,7 @@ export const updateAgentConfigurationRoute = createRoute(() => ({ } })); -// Lookup single configuration +// Lookup single configuration (used by APM Server) export const agentConfigurationSearchRoute = createRoute(core => ({ method: 'POST', path: '/api/apm/settings/agent-configuration/search', @@ -133,7 +142,8 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ service: t.intersection([ t.type({ name: t.string }), t.partial({ environment: t.string }) - ]) + ]), + etag: t.string }) }, handler: async (req, { body }, h) => { @@ -148,6 +158,11 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ return h.response().code(404); } + // update `applied_by_agent` field if etags match + if (body.etag === config._source.etag && !config._source.applied_by_agent) { + markAppliedByAgent({ id: config._id, body: config._source, setup }); + } + return config; } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index 12bcb3e414d84..a82e8c317902f 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -115,6 +115,7 @@ export type Client = < : undefined >( options: Omit & { + forceCache?: boolean; pathname: TPath; } & (TMethod extends 'GET' ? { method?: TMethod } : { method: TMethod }) & // Makes sure params can only be set when types were defined diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index f2e84be666117..c1a8a7f9b3985 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -6,15 +6,11 @@ import { StringMap, IndexAsString } from './common'; -export interface BoolQuery { - must_not: Array>; - should: Array>; - filter: Array>; -} - declare module 'elasticsearch' { // extending SearchResponse to be able to have typed aggregations + type ESSearchHit = SearchResponse['hits']['hits'][0]; + type AggregationType = | 'date_histogram' | 'histogram' diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 434597c1e857a..8bc861e5823d0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3420,14 +3420,9 @@ "xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "トレースログを表示", "xpack.apm.transactionActionMenu.viewInUptime": "監視ステータスを表示", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示", - "xpack.apm.transactionDetails.durationLabel": "期間", - "xpack.apm.transactionDetails.errorsNone": "なし", - "xpack.apm.transactionDetails.errorsOverviewLabel": "エラー", - "xpack.apm.transactionDetails.errorsOverviewLink": "{errorCount, plural, one {関連エラー} other {関連エラー}}", "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {1 件の関連エラーを表示} other {# 件の関連エラーを表示}}", "xpack.apm.transactionDetails.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした", - "xpack.apm.transactionDetails.percentOfTraceLabel": "トレースの %", "xpack.apm.transactionDetails.resultLabel": "結果", "xpack.apm.transactionDetails.serviceLabel": "サービス", "xpack.apm.transactionDetails.servicesTitle": "サービス", @@ -3437,7 +3432,6 @@ "xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "ナビゲーションタイミング", "xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "スタックトレース", "xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "ディスカバリでスパンを表示", - "xpack.apm.transactionDetails.timestampLabel": "タイムスタンプ", "xpack.apm.transactionDetails.transactionLabel": "トランザクション", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "このバケットに利用可能なサンプルがありません", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# request} 1 {# 件のリクエスト} other {# 件のリクエスト}}", @@ -3449,7 +3443,6 @@ "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "このトランザクションを報告した APM エージェントが、構成に基づき {dropped} 個以上のスパンをドロップしました。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "ドロップされたスパンの詳細。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "トランザクションの詳細", - "xpack.apm.transactionDetails.userIdLabel": "ユーザー ID", "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "完全なトレースを表示", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "現在完全なトレースが表示されています", "xpack.apm.transactions.chart.95thPercentileLabel": "95 パーセンタイル", @@ -3490,10 +3483,6 @@ "xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel": "国ごとの平均ページ読み込み時間の分布", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration": "平均ページ読み込み時間:", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads": "{docCount} ページの読み込み", - "xpack.apm.settings.agentConf.agentConfigDocsLinkLabel": "詳細については、当社のドキュメントをご覧ください。", - "xpack.apm.settings.agentConf.betaBadgeLabel": "ベータ", - "xpack.apm.settings.agentConf.betaBadgeText": "この機能は開発中です。フィードバックがある場合は、ディスカッションフォーラムをご利用ください。", - "xpack.apm.settings.agentConf.betaCallOutTitle": "APM エージェント構成 (ベータ)", "xpack.apm.settings.agentConf.configTable.editButtonDescription": "この構成を編集します", "xpack.apm.settings.agentConf.configTable.editButtonLabel": "編集", "xpack.apm.settings.agentConf.configTable.emptyPromptText": "変更しましょう。直接 Kibana からエージェント構成を微調整できます。再展開する必要はありません。まず、最初の構成を作成します。", @@ -3504,40 +3493,6 @@ "xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel": "サービス名", "xpack.apm.settings.agentConf.configurationsPanelTitle": "構成", "xpack.apm.settings.agentConf.createConfigButtonLabel": "構成の作成", - "xpack.apm.settings.agentConf.createConfigFailedText": "{serviceName}の構成を作成するときに問題が発生しました。エラー: {errorMessage}", - "xpack.apm.settings.agentConf.createConfigFailedTitle": "構成を作成できませんでした", - "xpack.apm.settings.agentConf.createConfigSucceededText": "{serviceName}の構成を正常に作成しました。エージェントに反映するには、少し時間がかかります。", - "xpack.apm.settings.agentConf.createConfigSucceededTitle": "構成が作成されました。", - "xpack.apm.settings.agentConf.deleteConfigFailedText": "{serviceName}の構成を削除するときに問題が発生しました。エラー: {errorMessage}", - "xpack.apm.settings.agentConf.deleteConfigFailedTitle": "構成を削除できませんでした", - "xpack.apm.settings.agentConf.deleteConfigSucceededText": "{serviceName}の構成を正常に削除しました。エージェントに反映するには、少し時間がかかります。", - "xpack.apm.settings.agentConf.deleteConfigSucceededTitle": "構成が削除されました", - "xpack.apm.settings.agentConf.editConfigFailedText": "{serviceName}の構成を編集するときに問題が発生しました。エラー: {errorMessage}", - "xpack.apm.settings.agentConf.editConfigFailedTitle": "構成を編集できませんでした", - "xpack.apm.settings.agentConf.editConfigSucceededText": "{serviceName}の構成を正常に編集しました。エージェントに反映するには、少し時間がかかります。", - "xpack.apm.settings.agentConf.editConfigSucceededTitle": "構成が編集されました", - "xpack.apm.settings.agentConf.flyOut.betaCallOutText": "この最初のバージョンでは、サンプルレート構成のみがサポートされます。今後のリリースで、エージェントのサポートを拡張します。不具合があることを認識してください。", - "xpack.apm.settings.agentConf.flyOut.betaCallOutTitle": "APM エージェント構成 (ベータ)", - "xpack.apm.settings.agentConf.flyOut.cancelButtonLabel": "キャンセル", - "xpack.apm.settings.agentConf.flyOut.configurationSectionTitle": "構成", - "xpack.apm.settings.agentConf.flyOut.createConfigTitle": "構成の作成", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel": "削除", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText": "この構成を削除する場合、APM サーバーと同期するまで、エージェントは、既存の構成を使用し続けます。", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle": "構成の削除", - "xpack.apm.settings.agentConf.flyOut.editConfigTitle": "構成の編集", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputErrorText": "サンプルレートは 0.000 ~ 1 の範囲でなければなりません", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputHelpText": "0.000 ~ 1.0 の範囲のレートを選択してください。既定の構成は 1.0 (100% のトレース) です。", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel": "トランザクションサンプルレート", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText": "サンプルレートの設定", - "xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel": "構成の保存", - "xpack.apm.settings.agentConf.flyOut.selectPlaceholder": "選択してください", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel": "未設定", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText": "構成を保存するには、有効な環境を選択する必要があります。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText": "構成ごとに 1 つの環境のみがサポートされます。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectLabel": "環境", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText": "構成するサービスを選択してください。", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel": "名前", - "xpack.apm.settings.agentConf.flyOut.serviceSectionTitle": "サービス", "xpack.apm.settings.agentConf.pageTitle": "設定", "xpack.apm.settings.agentConf.returnToOverviewLinkLabel": "概要に戻る", "xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 971eabfdd83fb..8b8dd9a0c40dc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3421,14 +3421,9 @@ "xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "显示跟踪日志", "xpack.apm.transactionActionMenu.viewInUptime": "查看监测状态", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档", - "xpack.apm.transactionDetails.durationLabel": "持续时间", - "xpack.apm.transactionDetails.errorsNone": "无", - "xpack.apm.transactionDetails.errorsOverviewLabel": "错误", - "xpack.apm.transactionDetails.errorsOverviewLink": "{errorCount, plural, one {相关错误} other {相关错误}}", "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}", "xpack.apm.transactionDetails.notFoundLabel": "未找到任何事务。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯", - "xpack.apm.transactionDetails.percentOfTraceLabel": "追溯的 %", "xpack.apm.transactionDetails.resultLabel": "结果", "xpack.apm.transactionDetails.serviceLabel": "服务", "xpack.apm.transactionDetails.servicesTitle": "服务", @@ -3438,7 +3433,6 @@ "xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "导航定时", "xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "堆栈追溯", "xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "在 Discover 中查看跨度", - "xpack.apm.transactionDetails.timestampLabel": "时间戳", "xpack.apm.transactionDetails.transactionLabel": "事务", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "此存储桶没有可用样例", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# 个请求} one {# 个请求} other {# 个请求}}", @@ -3450,7 +3444,6 @@ "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "报告此事务的 APM 代理基于其配置丢弃了 {dropped} 个跨度。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "详细了解丢弃的跨度。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "事务详情", - "xpack.apm.transactionDetails.userIdLabel": "用户 ID", "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "查看完整追溯信息", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "当前正在查看完整追溯信息", "xpack.apm.transactions.chart.95thPercentileLabel": "第 95 个百分位", @@ -3491,11 +3484,6 @@ "xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel": "页面加载平均时长分布(按国家/地区)", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration": "页面加载平均时长:", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads": "{docCount} 个页面加载", - "xpack.apm.settings.agentConf.agentConfigDocsLinkLabel": "在我们的文档中详细了解。", - "xpack.apm.settings.agentConf.betaBadgeLabel": "公测版", - "xpack.apm.settings.agentConf.betaBadgeText": "此功能仍在开发之中。如果您有反馈,请在我们的“讨论”论坛中提供。", - "xpack.apm.settings.agentConf.betaCallOutText": "我们很高兴让您第一时间了解 APM 代理配置。{agentConfigDocsLink}", - "xpack.apm.settings.agentConf.betaCallOutTitle": "APM 代理配置(公测版)", "xpack.apm.settings.agentConf.configTable.editButtonDescription": "编辑此配置", "xpack.apm.settings.agentConf.configTable.editButtonLabel": "编辑", "xpack.apm.settings.agentConf.configTable.emptyPromptText": "让我们改动一下!可以直接从 Kibana 微调代理配置,无需重新部署。首先创建您的第一个配置。", @@ -3506,40 +3494,6 @@ "xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel": "服务名称", "xpack.apm.settings.agentConf.configurationsPanelTitle": "配置", "xpack.apm.settings.agentConf.createConfigButtonLabel": "创建配置", - "xpack.apm.settings.agentConf.createConfigFailedText": "为 {serviceName} 创建配置时出现问题。错误:{errorMessage}", - "xpack.apm.settings.agentConf.createConfigFailedTitle": "配置无法创建", - "xpack.apm.settings.agentConf.createConfigSucceededText": "您已成功为 {serviceName} 创建配置。将花费一些时间才能传播到代理。", - "xpack.apm.settings.agentConf.createConfigSucceededTitle": "配置已创建!", - "xpack.apm.settings.agentConf.deleteConfigFailedText": "为 {serviceName} 删除配置时出现问题。错误:{errorMessage}", - "xpack.apm.settings.agentConf.deleteConfigFailedTitle": "配置无法删除", - "xpack.apm.settings.agentConf.deleteConfigSucceededText": "您已成功为 {serviceName} 删除配置。将花费一些时间才能传播到代理。", - "xpack.apm.settings.agentConf.deleteConfigSucceededTitle": "配置已删除", - "xpack.apm.settings.agentConf.editConfigFailedText": "编辑 {serviceName} 的配置时出现问题。错误:{errorMessage}", - "xpack.apm.settings.agentConf.editConfigFailedTitle": "配置无法编辑", - "xpack.apm.settings.agentConf.editConfigSucceededText": "您已成功编辑 {serviceName} 的配置。将花费一些时间才能传播到代理。", - "xpack.apm.settings.agentConf.editConfigSucceededTitle": "配置已编辑", - "xpack.apm.settings.agentConf.flyOut.betaCallOutText": "请注意,在此第一版中仅支持采样速率配置。我们将在未来的版本中提供代理配置的支持。请注意故障。", - "xpack.apm.settings.agentConf.flyOut.betaCallOutTitle": "APM 代理配置(公测版)", - "xpack.apm.settings.agentConf.flyOut.cancelButtonLabel": "取消", - "xpack.apm.settings.agentConf.flyOut.configurationSectionTitle": "配置", - "xpack.apm.settings.agentConf.flyOut.createConfigTitle": "创建配置", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel": "删除", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText": "如果您希望删除此配置,请注意,代理将继续使用现有配置,直至它们与 APM Server 同步。", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle": "删除配置", - "xpack.apm.settings.agentConf.flyOut.editConfigTitle": "编辑配置", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputErrorText": "采样速率必须介于 0.000 和 1 之间", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputHelpText": "选择 0.000 和 1.0 之间的速率。默认配置为 1.0(跟踪的 100%)。", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel": "事务采样速率", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText": "设置采样速率", - "xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel": "保存配置", - "xpack.apm.settings.agentConf.flyOut.selectPlaceholder": "选择", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel": "未设置", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText": "必须选择有效的环境,才能保存配置。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText": "每个配置仅支持单个环境。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectLabel": "环境", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText": "选择要配置的服务。", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel": "名称", - "xpack.apm.settings.agentConf.flyOut.serviceSectionTitle": "服务", "xpack.apm.settings.agentConf.pageTitle": "设置", "xpack.apm.settings.agentConf.returnToOverviewLinkLabel": "返回至概览", "xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪", diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts new file mode 100644 index 0000000000000..43ba8616e6872 --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -0,0 +1,174 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function agentConfigurationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + function searchConfigurations(configuration: any) { + return supertest + .post(`/api/apm/settings/agent-configuration/search`) + .send(configuration) + .set('kbn-xsrf', 'foo'); + } + + let createdConfigIds: any[] = []; + async function createConfiguration(configuration: any) { + const res = await supertest + .post(`/api/apm/settings/agent-configuration/new`) + .send(configuration) + .set('kbn-xsrf', 'foo'); + + createdConfigIds.push(res.body._id); + + return res; + } + + function deleteCreatedConfigurations() { + const promises = Promise.all(createdConfigIds.map(deleteConfiguration)); + createdConfigIds = []; + return promises; + } + + function deleteConfiguration(configurationId: string) { + return supertest + .delete(`/api/apm/settings/agent-configuration/${configurationId}`) + .set('kbn-xsrf', 'foo'); + } + + describe('agent configuration', () => { + describe('when creating four configurations', () => { + before(async () => { + log.debug('creating agent configuration'); + + // all / all + await createConfiguration({ + service: {}, + settings: { transaction_sample_rate: 0.1 }, + }); + + // my_service / all + await createConfiguration({ + service: { name: 'my_service' }, + settings: { transaction_sample_rate: 0.2 }, + }); + + // all / production + await createConfiguration({ + service: { environment: 'production' }, + settings: { transaction_sample_rate: 0.3 }, + }); + + // all / production + await createConfiguration({ + service: { environment: 'development' }, + settings: { transaction_sample_rate: 0.4 }, + }); + + // my_service / production + await createConfiguration({ + service: { name: 'my_service', environment: 'development' }, + settings: { transaction_sample_rate: 0.5 }, + }); + }); + + after(async () => { + log.debug('deleting agent configurations'); + await deleteCreatedConfigurations(); + }); + + const agentsRequests = [ + { + service: { name: 'non_existing_service', environment: 'non_existing_env' }, + expectedSettings: { transaction_sample_rate: 0.1 }, + }, + { + service: { name: 'my_service', environment: 'production' }, + expectedSettings: { transaction_sample_rate: 0.2 }, + }, + { + service: { name: 'non_existing_service', environment: 'production' }, + expectedSettings: { transaction_sample_rate: 0.3 }, + }, + { + service: { name: 'non_existing_service', environment: 'development' }, + expectedSettings: { transaction_sample_rate: 0.4 }, + }, + { + service: { name: 'my_service', environment: 'development' }, + expectedSettings: { transaction_sample_rate: 0.5 }, + }, + ]; + + for (const agentRequest of agentsRequests) { + it(`${agentRequest.service.name} / ${agentRequest.service.environment}`, async () => { + const { statusCode, body } = await searchConfigurations({ + service: agentRequest.service, + etag: 'abc', + }); + + expect(statusCode).to.equal(200); + expect(body._source.settings).to.eql(agentRequest.expectedSettings); + }); + } + }); + + describe('when an agent retrieves a configuration', () => { + before(async () => { + log.debug('creating agent configuration'); + + await createConfiguration({ + service: { name: 'myservice', environment: 'development' }, + settings: { transaction_sample_rate: 0.9 }, + }); + }); + + after(async () => { + log.debug('deleting agent configurations'); + await deleteCreatedConfigurations(); + }); + + it(`should have 'applied_by_agent=false' on first request`, async () => { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', + }); + + expect(body._source.applied_by_agent).to.be(false); + }); + + it(`should have 'applied_by_agent=true' on second request`, async () => { + async function getAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(getAppliedByAgent)).to.be(true); + }); + }); + }); +} + +async function waitFor(cb: () => Promise, retries = 50): Promise { + if (retries === 0) { + throw new Error(`Maximum number of retries reached`); + } + + const res = await cb(); + if (!res) { + await new Promise(resolve => setTimeout(resolve, 100)); + return waitFor(cb, retries - 1); + } + return res; +} diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts index 6677928a19979..7f841747cdcab 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -133,7 +133,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) req: { method: 'post', url: `/api/apm/settings/agent-configuration/search`, - body: { service: { name: 'test-service' } }, + body: { service: { name: 'test-service' }, etag: 'abc' }, }, expectForbidden: expect404, expectResponse: expect200, diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts index 6364c7304118f..c49d577537048 100644 --- a/x-pack/test/api_integration/apis/apm/index.ts +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM', () => { loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./agent_configuration')); }); } From c2dff771f86fd971a1d1262e69fddc43baca5f4b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 10 Oct 2019 14:04:30 +0200 Subject: [PATCH 08/92] improve wording of save modal and listing page (#47443) --- .../saved_query_management_component.tsx | 2 +- .../public/table_list_view/table_list_view.js | 4 +-- .../saved_objects/saved_object_save_modal.tsx | 30 ++++++++----------- .../translations/translations/ja-JP.json | 3 -- .../translations/translations/zh-CN.json | 3 -- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/saved_query_management/saved_query_management_component.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/saved_query_management/saved_query_management_component.tsx index 013941cb4ec95..ac296a4222c84 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/saved_query_management/saved_query_management_component.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/saved_query_management/saved_query_management_component.tsx @@ -248,7 +248,7 @@ export const SavedQueryManagementComponent: FunctionComponent = ({ aria-label={i18n.translate( 'data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel', { - defaultMessage: 'Save as a new saved query', + defaultMessage: 'Save as new saved query', } )} data-test-subj="saved-query-management-save-as-new-button" diff --git a/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js b/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js index df4634b75373e..3148a4a37c9c0 100644 --- a/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js +++ b/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js @@ -189,7 +189,7 @@ class TableListViewUi extends React.Component { title={ diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx index 3887f8998af09..953fbbf348b95 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx @@ -229,15 +229,14 @@ export class SavedObjectSaveModal extends React.Component { }; private renderConfirmButton = () => { - const { isLoading, title, hasTitleDuplicate } = this.state; + const { isLoading, title } = this.state; - let confirmLabel: string | React.ReactNode = hasTitleDuplicate - ? i18n.translate('kibana-react.savedObjects.saveModal.confirmSaveButtonLabel', { - defaultMessage: 'Confirm save', - }) - : i18n.translate('kibana-react.savedObjects.saveModal.saveButtonLabel', { - defaultMessage: 'Save', - }); + let confirmLabel: string | React.ReactNode = i18n.translate( + 'kibana-react.savedObjects.saveModal.saveButtonLabel', + { + defaultMessage: 'Save', + } + ); if (this.props.confirmButtonLabel) { confirmLabel = this.props.confirmButtonLabel; @@ -267,7 +266,7 @@ export class SavedObjectSaveModal extends React.Component { title={ } @@ -277,19 +276,16 @@ export class SavedObjectSaveModal extends React.Component {

{this.props.confirmButtonLabel ? this.props.confirmButtonLabel - : i18n.translate( - 'kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText', - { - defaultMessage: 'Confirm save', - } - )} + : i18n.translate('kibana-react.savedObjects.saveModal.saveButtonLabel', { + defaultMessage: 'Save', + })} ), }} @@ -315,7 +311,7 @@ export class SavedObjectSaveModal extends React.Component { label={ } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8bc861e5823d0..d31f38ed8c3b3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -860,7 +860,6 @@ "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリとして保存", "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", "data.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", @@ -2394,10 +2393,8 @@ "kbn.server.tutorials.zookeeperMetrics.nameTitle": "Zookeeper メトリック", "kbn.server.tutorials.zookeeperMetrics.shortDescription": "Zookeeper サーバーから内部メトリックを取得します。", "kbn.settings.advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", - "kbn.table_list_view.listing.createNewItemButtonLabel": "新規 {entityName} を作成", "kbn.table_list_view.listing.deleteButtonMessage": "{itemCount} 件の {entityName} を削除", "kbn.table_list_view.listing.deleteConfirmModalDescription": "削除された {entityNamePlural} は復元できません。", - "kbn.table_list_view.listing.deleteSelectedConfirmModal.title": "選択された {itemCount} 件の {entityName} を削除しますか?", "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "キャンセル", "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "削除", "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "削除中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8b8dd9a0c40dc..19b97b2973132 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -861,7 +861,6 @@ "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", "data.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", @@ -2395,10 +2394,8 @@ "kbn.server.tutorials.zookeeperMetrics.nameTitle": "Zookeeper 指标", "kbn.server.tutorials.zookeeperMetrics.shortDescription": "从 Zookeeper 服务器提取内部指标。", "kbn.settings.advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", - "kbn.table_list_view.listing.createNewItemButtonLabel": "创建新的{entityName}", "kbn.table_list_view.listing.deleteButtonMessage": "删除 {itemCount} 个{entityName}", "kbn.table_list_view.listing.deleteConfirmModalDescription": "您无法恢复删除的{entityNamePlural}。", - "kbn.table_list_view.listing.deleteSelectedConfirmModal.title": "删除 {itemCount} 个选定{entityName}?", "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.cancelButtonLabel": "取消", "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabel": "删除", "kbn.table_list_view.listing.deleteSelectedItemsConfirmModal.confirmButtonLabelDeleting": "正在删除", From effa11ef711d6c61e96c7f9e091ce5d054d6c04d Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 10 Oct 2019 15:15:25 +0300 Subject: [PATCH 09/92] call isTimeRangeSelectorEnabled (#47809) --- src/legacy/core_plugins/data/public/timefilter/timefilter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts index 64129ea2af5ff..70889038c0ec2 100644 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts @@ -157,7 +157,7 @@ export class Timefilter { } public getActiveBounds(): TimeRangeBounds | undefined { - if (this.isTimeRangeSelectorEnabled) { + if (this.isTimeRangeSelectorEnabled()) { return this.getBounds(); } } From ae28c9b542eaa6647a4a14dcbe22ad1d935c165e Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 10 Oct 2019 08:51:28 -0400 Subject: [PATCH 10/92] Don't throw an error is panel is added, then removed, before embeddable finishes loading (#46788) * Remove this error being thrown as it can be expected in certain situations * change test after change in logic --- src/plugins/embeddable/public/lib/containers/container.ts | 5 ++--- src/plugins/embeddable/public/tests/container.test.ts | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index f4fca4bbd8d6e..bce16747ed48e 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -181,11 +181,10 @@ export abstract class Container< resolve(this.children[id] as TEmbeddable); } - // If a panel is removed before the embeddable was loaded there is a chance this will - // never resolve. + // If we hit this, the panel was removed before the embeddable finished loading. if (this.input.panels[id] === undefined) { subscription.unsubscribe(); - reject(new PanelNotFoundError()); + resolve(undefined); } }); }); diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index c2044057786ae..3bdbcbad857d6 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -27,7 +27,6 @@ import { } from '../lib/test_samples/embeddables/filterable_embeddable'; import { ERROR_EMBEDDABLE_TYPE } from '../lib/embeddables/error_embeddable'; import { Filter, FilterStateStore } from '@kbn/es-query'; -import { PanelNotFoundError } from '../lib/errors'; import { FilterableEmbeddableFactory } from '../lib/test_samples/embeddables/filterable_embeddable_factory'; import { CONTACT_CARD_EMBEDDABLE } from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; @@ -753,7 +752,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy done(); }); -test('untilEmbeddableLoaded rejects with an error if child is subsequently removed', async done => { +test('untilEmbeddableLoaded resolves with undefined if child is subsequently removed', async done => { const { doStart, coreStart, uiActions } = testPlugin( coreMock.createSetup(), coreMock.createStart() @@ -785,8 +784,8 @@ test('untilEmbeddableLoaded rejects with an error if child is subsequently remov } ); - container.untilEmbeddableLoaded('123').catch(error => { - expect(error).toBeInstanceOf(PanelNotFoundError); + container.untilEmbeddableLoaded('123').then(embed => { + expect(embed).toBeUndefined(); done(); }); From 3cb74bd62804ba62aa1264096cf60efda477e01d Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 10 Oct 2019 08:52:08 -0400 Subject: [PATCH 11/92] Phase 1 of search services (#46742) * Phase 1 of search services * First review feedback * Start on tests * Add functional tests for search explorer * Add unload and fix ts error * Add index.test.ts files for coverage completeness * Adding unit tests * use internal route terminology. No reason this should be a public route, at least not yet. * Move search service into data plugin * App mount search context needs to be optional * Add more unit tests for server stuff * wip types fix * fix types for new context container stuff * put back all jest test coverage paths * address review comments * delete the two test files that just tested the instantiation of the search service * expose search fn on StartContract... tested locally only * update mocks to account for new startcontract --- src/dev/jest/config.js | 2 + .../data/common/search/es_search/index.ts | 20 +++ .../data/common/search/es_search/types.ts | 30 ++++ src/plugins/data/common/search/index.ts | 26 ++++ src/plugins/data/common/search/types.ts | 55 +++++++ src/plugins/data/public/index.ts | 3 + src/plugins/data/public/mocks.ts | 3 + src/plugins/data/public/plugin.ts | 8 +- src/plugins/data/public/search/README.md | 13 ++ .../create_app_mount_context_search.test.ts | 69 +++++++++ .../search/create_app_mount_context_search.ts | 57 +++++++ .../es_search/es_search_service.test.ts | 43 ++++++ .../search/es_search/es_search_service.ts | 41 +++++ .../es_search/es_search_strategy.test.ts | 52 +++++++ .../search/es_search/es_search_strategy.ts | 37 +++++ .../public/search/es_search/index.test.ts | 25 +++ .../data/public/search/es_search/index.ts | 25 +++ src/plugins/data/public/search/i_search.ts | 59 +++++++ .../search/i_search_app_mount_context.ts | 24 +++ .../data/public/search/i_search_context.ts | 23 +++ .../data/public/search/i_search_setup.ts | 40 +++++ .../data/public/search/i_search_strategy.ts | 63 ++++++++ src/plugins/data/public/search/index.ts | 42 +++++ src/plugins/data/public/search/mocks.ts | 23 +++ .../data/public/search/search_service.test.ts | 41 +++++ .../data/public/search/search_service.ts | 126 +++++++++++++++ .../data/public/search/strategy_types.ts | 40 +++++ .../search/sync_search_strategy.test.ts | 58 +++++++ .../public/search/sync_search_strategy.ts | 56 +++++++ src/plugins/data/public/types.ts | 3 + src/plugins/data/server/index.ts | 4 + src/plugins/data/server/plugin.ts | 19 ++- src/plugins/data/server/search/README.md | 13 ++ .../data/server/search/create_api.test.ts | 62 ++++++++ src/plugins/data/server/search/create_api.ts | 45 ++++++ .../server/search/es_search/elasticsearch.ts | 34 ++++ .../es_search/es_search_service.test.ts | 60 +++++++ .../search/es_search/es_search_service.ts | 42 +++++ .../es_search/es_search_strategy.test.ts | 102 ++++++++++++ .../search/es_search/es_search_strategy.ts | 53 +++++++ .../data/server/search/es_search/index.ts | 27 ++++ .../search/i_route_handler_search_context.ts | 24 +++ src/plugins/data/server/search/i_search.ts | 42 +++++ .../data/server/search/i_search_context.ts | 23 +++ .../data/server/search/i_search_setup.ts | 51 ++++++ .../data/server/search/i_search_strategy.ts | 66 ++++++++ src/plugins/data/server/search/index.ts | 29 ++++ src/plugins/data/server/search/mocks.ts | 26 ++++ src/plugins/data/server/search/routes.test.ts | 99 ++++++++++++ src/plugins/data/server/search/routes.ts | 46 ++++++ .../data/server/search/search_service.test.ts | 56 +++++++ .../data/server/search/search_service.ts | 97 ++++++++++++ .../data/server/search/strategy_types.ts | 39 +++++ test/functional/services/find.ts | 11 ++ test/functional/services/test_subjects.ts | 21 +++ test/plugin_functional/config.js | 1 + .../plugins/demo_search/common/index.ts | 34 ++++ .../plugins/demo_search/kibana.json | 10 ++ .../plugins/demo_search/package.json | 17 ++ .../public/demo_search_strategy.ts | 70 +++++++++ .../plugins/demo_search/public/index.ts | 28 ++++ .../plugins/demo_search/public/plugin.ts | 61 ++++++++ .../plugins/demo_search/server/constants.ts | 20 +++ .../server/demo_search_strategy.ts | 36 +++++ .../plugins/demo_search/server/index.ts | 25 +++ .../plugins/demo_search/server/plugin.ts | 61 ++++++++ .../plugins/demo_search/tsconfig.json | 16 ++ .../plugins/search_explorer/kibana.json | 10 ++ .../plugins/search_explorer/package.json | 17 ++ .../search_explorer/public/application.tsx | 122 +++++++++++++++ .../search_explorer/public/demo_strategy.tsx | 134 ++++++++++++++++ .../search_explorer/public/do_search.tsx | 141 +++++++++++++++++ .../search_explorer/public/documentation.tsx | 102 ++++++++++++ .../search_explorer/public/es_strategy.tsx | 146 ++++++++++++++++++ .../search_explorer/public/guide_section.tsx | 137 ++++++++++++++++ .../plugins/search_explorer/public/index.ts | 22 +++ .../plugins/search_explorer/public/page.tsx | 51 ++++++ .../plugins/search_explorer/public/plugin.tsx | 42 +++++ .../search_explorer/public/search_api.tsx | 87 +++++++++++ .../plugins/search_explorer/tsconfig.json | 15 ++ .../test_suites/search/demo_data.ts | 36 +++++ .../test_suites/search/es_search.ts | 35 +++++ .../test_suites/search/index.ts | 51 ++++++ 83 files changed, 3721 insertions(+), 4 deletions(-) create mode 100644 src/plugins/data/common/search/es_search/index.ts create mode 100644 src/plugins/data/common/search/es_search/types.ts create mode 100644 src/plugins/data/common/search/index.ts create mode 100644 src/plugins/data/common/search/types.ts create mode 100644 src/plugins/data/public/search/README.md create mode 100644 src/plugins/data/public/search/create_app_mount_context_search.test.ts create mode 100644 src/plugins/data/public/search/create_app_mount_context_search.ts create mode 100644 src/plugins/data/public/search/es_search/es_search_service.test.ts create mode 100644 src/plugins/data/public/search/es_search/es_search_service.ts create mode 100644 src/plugins/data/public/search/es_search/es_search_strategy.test.ts create mode 100644 src/plugins/data/public/search/es_search/es_search_strategy.ts create mode 100644 src/plugins/data/public/search/es_search/index.test.ts create mode 100644 src/plugins/data/public/search/es_search/index.ts create mode 100644 src/plugins/data/public/search/i_search.ts create mode 100644 src/plugins/data/public/search/i_search_app_mount_context.ts create mode 100644 src/plugins/data/public/search/i_search_context.ts create mode 100644 src/plugins/data/public/search/i_search_setup.ts create mode 100644 src/plugins/data/public/search/i_search_strategy.ts create mode 100644 src/plugins/data/public/search/index.ts create mode 100644 src/plugins/data/public/search/mocks.ts create mode 100644 src/plugins/data/public/search/search_service.test.ts create mode 100644 src/plugins/data/public/search/search_service.ts create mode 100644 src/plugins/data/public/search/strategy_types.ts create mode 100644 src/plugins/data/public/search/sync_search_strategy.test.ts create mode 100644 src/plugins/data/public/search/sync_search_strategy.ts create mode 100644 src/plugins/data/server/search/README.md create mode 100644 src/plugins/data/server/search/create_api.test.ts create mode 100644 src/plugins/data/server/search/create_api.ts create mode 100644 src/plugins/data/server/search/es_search/elasticsearch.ts create mode 100644 src/plugins/data/server/search/es_search/es_search_service.test.ts create mode 100644 src/plugins/data/server/search/es_search/es_search_service.ts create mode 100644 src/plugins/data/server/search/es_search/es_search_strategy.test.ts create mode 100644 src/plugins/data/server/search/es_search/es_search_strategy.ts create mode 100644 src/plugins/data/server/search/es_search/index.ts create mode 100644 src/plugins/data/server/search/i_route_handler_search_context.ts create mode 100644 src/plugins/data/server/search/i_search.ts create mode 100644 src/plugins/data/server/search/i_search_context.ts create mode 100644 src/plugins/data/server/search/i_search_setup.ts create mode 100644 src/plugins/data/server/search/i_search_strategy.ts create mode 100644 src/plugins/data/server/search/index.ts create mode 100644 src/plugins/data/server/search/mocks.ts create mode 100644 src/plugins/data/server/search/routes.test.ts create mode 100644 src/plugins/data/server/search/routes.ts create mode 100644 src/plugins/data/server/search/search_service.test.ts create mode 100644 src/plugins/data/server/search/search_service.ts create mode 100644 src/plugins/data/server/search/strategy_types.ts create mode 100644 test/plugin_functional/plugins/demo_search/common/index.ts create mode 100644 test/plugin_functional/plugins/demo_search/kibana.json create mode 100644 test/plugin_functional/plugins/demo_search/package.json create mode 100644 test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts create mode 100644 test/plugin_functional/plugins/demo_search/public/index.ts create mode 100644 test/plugin_functional/plugins/demo_search/public/plugin.ts create mode 100644 test/plugin_functional/plugins/demo_search/server/constants.ts create mode 100644 test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts create mode 100644 test/plugin_functional/plugins/demo_search/server/index.ts create mode 100644 test/plugin_functional/plugins/demo_search/server/plugin.ts create mode 100644 test/plugin_functional/plugins/demo_search/tsconfig.json create mode 100644 test/plugin_functional/plugins/search_explorer/kibana.json create mode 100644 test/plugin_functional/plugins/search_explorer/package.json create mode 100644 test/plugin_functional/plugins/search_explorer/public/application.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/do_search.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/documentation.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/guide_section.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/index.ts create mode 100644 test/plugin_functional/plugins/search_explorer/public/page.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/plugin.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/public/search_api.tsx create mode 100644 test/plugin_functional/plugins/search_explorer/tsconfig.json create mode 100644 test/plugin_functional/test_suites/search/demo_data.ts create mode 100644 test/plugin_functional/test_suites/search/es_search.ts create mode 100644 test/plugin_functional/test_suites/search/index.ts diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 39bd2015c3643..0c785a84bb469 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -39,6 +39,8 @@ export default { '/test/functional/services/remote', ], collectCoverageFrom: [ + 'src/plugins/**/*.{ts,tsx}', + '!src/plugins/**/*.d.ts', 'packages/kbn-ui-framework/src/components/**/*.js', '!packages/kbn-ui-framework/src/components/index.js', '!packages/kbn-ui-framework/src/components/**/*/index.js', diff --git a/src/plugins/data/common/search/es_search/index.ts b/src/plugins/data/common/search/es_search/index.ts new file mode 100644 index 0000000000000..5b605224491b2 --- /dev/null +++ b/src/plugins/data/common/search/es_search/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from './types'; diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts new file mode 100644 index 0000000000000..1576a6e38e36d --- /dev/null +++ b/src/plugins/data/common/search/es_search/types.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SearchParams, SearchResponse } from 'elasticsearch'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; + +export const ES_SEARCH_STRATEGY = 'es'; + +export interface IEsSearchRequest extends IKibanaSearchRequest { + params: SearchParams; +} + +export interface IEsSearchResponse extends IKibanaSearchResponse { + rawResponse: SearchResponse; +} diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts new file mode 100644 index 0000000000000..eb333c67b876d --- /dev/null +++ b/src/plugins/data/common/search/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ES_SEARCH_STRATEGY } from './es_search'; + +export { IKibanaSearchResponse, IKibanaSearchRequest } from './types'; + +export const DEFAULT_SEARCH_STRATEGY = ES_SEARCH_STRATEGY; + +export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from './es_search'; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts new file mode 100644 index 0000000000000..e1fe7d414756a --- /dev/null +++ b/src/plugins/data/common/search/types.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface IKibanaSearchResponse { + /** + * Some responses may contain a unique id to identify the request this response came from. + */ + id?: string; + + /** + * If relevant to the search strategy, return a percentage + * that represents how progress is indicated. + */ + percentComplete?: number; + + /** + * If relevant to the search strategy, return a total number + * that represents how progress is indicated. + */ + total?: number; + + /** + * If relevant to the search strategy, return a loaded number + * that represents how progress is indicated. + */ + loaded?: number; +} + +export interface IKibanaSearchRequest { + /** + * An id can be used to uniquely identify this request. + */ + id?: string; + + /** + * Optionally tell search strategies to output debug information. + */ + debug?: boolean; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 91b94e09607ee..7e1b3801b62a4 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -31,3 +31,6 @@ export * from '../common'; export * from './autocomplete_provider'; export * from './types'; + +export { IRequestTypesMap, IResponseTypesMap } from './search'; +export * from './search'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index b2d311912b982..5e60ca93378d9 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -17,6 +17,7 @@ * under the License. */ import { Plugin } from '.'; +import { searchSetupMock } from './search/mocks'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; @@ -30,6 +31,7 @@ const autocompleteMock: any = { const createSetupContract = (): Setup => { const setupContract: Setup = { autocomplete: autocompleteMock as Setup['autocomplete'], + search: searchSetupMock, }; return setupContract; @@ -39,6 +41,7 @@ const createStartContract = (): Start => { const startContract: Start = { autocomplete: autocompleteMock as Start['autocomplete'], getSuggestions: jest.fn(), + search: { search: jest.fn() }, }; return startContract; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index a3fa8005560ae..935a3c5754503 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -20,16 +20,21 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { AutocompleteProviderRegister } from './autocomplete_provider'; import { DataPublicPluginSetup, DataPublicPluginStart } from './types'; +import { SearchService } from './search/search_service'; import { getSuggestionsProvider } from './suggestions_provider'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); + private readonly searchService: SearchService; - constructor(initializerContext: PluginInitializerContext) {} + constructor(initializerContext: PluginInitializerContext) { + this.searchService = new SearchService(initializerContext); + } public setup(core: CoreSetup): DataPublicPluginSetup { return { autocomplete: this.autocomplete, + search: this.searchService.setup(core), }; } @@ -37,6 +42,7 @@ export class DataPublicPlugin implements Plugin { + it('Returns search fn when there are no strategies', () => { + const context = createAppMountSearchContext({}); + expect(context.search).toBeDefined(); + }); + + it(`Search throws an error when the strategy doesn't exist`, () => { + const context = createAppMountSearchContext({}); + expect(() => context.search({}, {}, 'noexist').toPromise()).toThrowErrorMatchingInlineSnapshot( + `"Strategy with name noexist does not exist"` + ); + }); + + it(`Search fn is called on appropriate strategy name`, done => { + const context = createAppMountSearchContext({ + mysearch: search => + Promise.resolve({ + search: () => from(Promise.resolve({ percentComplete: 98 })), + }), + anothersearch: search => + Promise.resolve({ + search: () => from(Promise.resolve({ percentComplete: 0 })), + }), + }); + + context.search({}, {}, 'mysearch').subscribe(response => { + expect(response).toEqual({ percentComplete: 98 }); + done(); + }); + }); + + it(`Search fn is called with the passed in request object`, done => { + const context = createAppMountSearchContext({ + mysearch: search => { + return Promise.resolve({ + search: request => { + expect(request).toEqual({ greeting: 'hi' }); + return from(Promise.resolve({})); + }, + }); + }, + }); + context + .search({ greeting: 'hi' } as any, {}, 'mysearch') + .subscribe(response => {}, () => {}, done); + }); +}); diff --git a/src/plugins/data/public/search/create_app_mount_context_search.ts b/src/plugins/data/public/search/create_app_mount_context_search.ts new file mode 100644 index 0000000000000..5659a9c863dc1 --- /dev/null +++ b/src/plugins/data/public/search/create_app_mount_context_search.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mergeMap } from 'rxjs/operators'; +import { from } from 'rxjs'; +import { ISearchAppMountContext } from './i_search_app_mount_context'; +import { ISearchGeneric } from './i_search'; +import { + TSearchStrategiesMap, + ISearchStrategy, + TSearchStrategyProviderEnhanced, +} from './i_search_strategy'; +import { TStrategyTypes } from './strategy_types'; +import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; + +export const createAppMountSearchContext = ( + searchStrategies: TSearchStrategiesMap +): ISearchAppMountContext => { + const getSearchStrategy = ( + strategyName?: K + ): Promise> => { + const strategyProvider = searchStrategies[ + strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY + ] as TSearchStrategyProviderEnhanced | undefined; + if (!strategyProvider) { + throw new Error(`Strategy with name ${strategyName} does not exist`); + } + return strategyProvider(search); + }; + + const search: ISearchGeneric = (request, options, strategyName) => { + const strategyPromise = getSearchStrategy(strategyName); + return from(strategyPromise).pipe( + mergeMap(strategy => { + return strategy.search(request, options); + }) + ); + }; + + return { search }; +}; diff --git a/src/plugins/data/public/search/es_search/es_search_service.test.ts b/src/plugins/data/public/search/es_search/es_search_service.test.ts new file mode 100644 index 0000000000000..d1069cd6815fe --- /dev/null +++ b/src/plugins/data/public/search/es_search/es_search_service.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../../core/public/mocks'; +import { EsSearchService } from './es_search_service'; +import { CoreSetup } from '../../../../../core/public'; +import { searchSetupMock } from '../mocks'; + +describe('ES search strategy service', () => { + let service: EsSearchService; + let mockCoreSetup: MockedKeys; + const opaqueId = Symbol(); + + beforeEach(() => { + service = new EsSearchService({ opaqueId }); + mockCoreSetup = coreMock.createSetup(); + }); + + describe('setup()', () => { + it('registers the ES search strategy', async () => { + service.setup(mockCoreSetup, { + search: searchSetupMock, + }); + expect(searchSetupMock.registerSearchStrategyProvider).toBeCalled(); + }); + }); +}); diff --git a/src/plugins/data/public/search/es_search/es_search_service.ts b/src/plugins/data/public/search/es_search/es_search_service.ts new file mode 100644 index 0000000000000..fe74292d9f8fa --- /dev/null +++ b/src/plugins/data/public/search/es_search/es_search_service.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../core/public'; +import { ES_SEARCH_STRATEGY } from '../../../common/search/es_search'; +import { esSearchStrategyProvider } from './es_search_strategy'; +import { ISearchSetup } from '../i_search_setup'; + +export interface IEsSearchSetupDependencies { + search: ISearchSetup; +} + +export class EsSearchService implements Plugin { + constructor(private initializerContext: PluginInitializerContext) {} + public setup(core: CoreSetup, deps: IEsSearchSetupDependencies) { + deps.search.registerSearchStrategyProvider( + this.initializerContext.opaqueId, + ES_SEARCH_STRATEGY, + esSearchStrategyProvider + ); + } + + public start() {} + public stop() {} +} diff --git a/src/plugins/data/public/search/es_search/es_search_strategy.test.ts b/src/plugins/data/public/search/es_search/es_search_strategy.test.ts new file mode 100644 index 0000000000000..915a67412e519 --- /dev/null +++ b/src/plugins/data/public/search/es_search/es_search_strategy.test.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../../core/public/mocks'; +import { esSearchStrategyProvider } from './es_search_strategy'; +import { CoreSetup } from 'kibana/public'; +import { ES_SEARCH_STRATEGY } from '../../../common/search/es_search'; + +describe('ES search strategy', () => { + let mockCoreSetup: MockedKeys; + const mockSearch = jest.fn(); + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockSearch.mockClear(); + }); + + it('returns a strategy with `search` that calls the sync search `search`', () => { + const request = { params: {} }; + const options = {}; + + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockSearch + ); + esSearch.search(request, options); + + expect(mockSearch.mock.calls[0][0]).toEqual({ + ...request, + serverStrategy: ES_SEARCH_STRATEGY, + }); + expect(mockSearch.mock.calls[0][1]).toBe(options); + }); +}); diff --git a/src/plugins/data/public/search/es_search/es_search_strategy.ts b/src/plugins/data/public/search/es_search/es_search_strategy.ts new file mode 100644 index 0000000000000..643ded120799e --- /dev/null +++ b/src/plugins/data/public/search/es_search/es_search_strategy.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../common/search'; +import { SYNC_SEARCH_STRATEGY } from '../sync_search_strategy'; +import { TSearchStrategyProvider, ISearchStrategy, ISearchGeneric, ISearchContext } from '..'; + +export const esSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext, + search: ISearchGeneric +): ISearchStrategy => { + return { + search: (request, options) => + search( + { ...request, serverStrategy: ES_SEARCH_STRATEGY }, + options, + SYNC_SEARCH_STRATEGY + ) as Observable, + }; +}; diff --git a/src/plugins/data/public/search/es_search/index.test.ts b/src/plugins/data/public/search/es_search/index.test.ts new file mode 100644 index 0000000000000..7f7a2ed397d43 --- /dev/null +++ b/src/plugins/data/public/search/es_search/index.test.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { esSearchService } from '.'; + +it('es search service is instantiated', () => { + const esSearch = esSearchService({ opaqueId: Symbol() }); + expect(esSearch).toBeDefined(); +}); diff --git a/src/plugins/data/public/search/es_search/index.ts b/src/plugins/data/public/search/es_search/index.ts new file mode 100644 index 0000000000000..31952f14c63c5 --- /dev/null +++ b/src/plugins/data/public/search/es_search/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; +import { EsSearchService } from './es_search_service'; + +export const esSearchService: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new EsSearchService(initializerContext); diff --git a/src/plugins/data/public/search/i_search.ts b/src/plugins/data/public/search/i_search.ts new file mode 100644 index 0000000000000..0e256b960ffa3 --- /dev/null +++ b/src/plugins/data/public/search/i_search.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { TStrategyTypes } from './strategy_types'; +import { + DEFAULT_SEARCH_STRATEGY, + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../common/search'; +import { SYNC_SEARCH_STRATEGY, ISyncSearchRequest } from './sync_search_strategy'; +import { + ES_SEARCH_STRATEGY, + IEsSearchRequest, + IEsSearchResponse, +} from '../../common/search/es_search'; + +export interface ISearchOptions { + signal?: AbortSignal; +} + +export interface IRequestTypesMap { + [SYNC_SEARCH_STRATEGY]: ISyncSearchRequest; + [ES_SEARCH_STRATEGY]: IEsSearchRequest; + [key: string]: IKibanaSearchRequest; +} + +export interface IResponseTypesMap { + [SYNC_SEARCH_STRATEGY]: IKibanaSearchResponse; + [ES_SEARCH_STRATEGY]: IEsSearchResponse; + [key: string]: IKibanaSearchResponse; +} + +export type ISearchGeneric = ( + request: IRequestTypesMap[T], + options: ISearchOptions, + strategy?: T +) => Observable; + +export type ISearch = ( + request: IRequestTypesMap[T], + options: ISearchOptions +) => Observable; diff --git a/src/plugins/data/public/search/i_search_app_mount_context.ts b/src/plugins/data/public/search/i_search_app_mount_context.ts new file mode 100644 index 0000000000000..36cae0083ce56 --- /dev/null +++ b/src/plugins/data/public/search/i_search_app_mount_context.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISearchGeneric } from './i_search'; + +export interface ISearchAppMountContext { + search: ISearchGeneric; +} diff --git a/src/plugins/data/public/search/i_search_context.ts b/src/plugins/data/public/search/i_search_context.ts new file mode 100644 index 0000000000000..da94988027ff6 --- /dev/null +++ b/src/plugins/data/public/search/i_search_context.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CoreSetup } from '../../../../core/public'; + +export interface ISearchContext { + core: CoreSetup; +} diff --git a/src/plugins/data/public/search/i_search_setup.ts b/src/plugins/data/public/search/i_search_setup.ts new file mode 100644 index 0000000000000..a59c62e2e0d9b --- /dev/null +++ b/src/plugins/data/public/search/i_search_setup.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IContextProvider } from 'kibana/public'; +import { ISearchContext } from './i_search_context'; +import { TRegisterSearchStrategyProvider, TSearchStrategyProvider } from './i_search_strategy'; + +/** + * The setup contract exposed by the Search plugin exposes the search strategy extension + * point. + */ +export interface ISearchSetup { + registerSearchStrategyContext: ( + pluginId: symbol, + contextName: TContextName, + provider: IContextProvider, TContextName> + ) => void; + + /** + * Extension point exposed for other plugins to register their own search + * strategies. + */ + registerSearchStrategyProvider: TRegisterSearchStrategyProvider; +} diff --git a/src/plugins/data/public/search/i_search_strategy.ts b/src/plugins/data/public/search/i_search_strategy.ts new file mode 100644 index 0000000000000..bd67409d5054a --- /dev/null +++ b/src/plugins/data/public/search/i_search_strategy.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISearch, ISearchGeneric } from './i_search'; +import { TStrategyTypes } from './strategy_types'; +import { ISearchContext } from './i_search_context'; + +/** + * Search strategy interface contains a search method that takes in + * a request and returns a promise that resolves to a response. + */ +export interface ISearchStrategy { + search: ISearch; +} + +/** + * Search strategy provider creates an instance of a search strategy with the request + * handler context bound to it. This way every search strategy can use + * whatever information they require from the request context. + */ +export type TSearchStrategyProviderEnhanced = ( + search: ISearchGeneric +) => Promise>; + +/** + * Search strategy provider creates an instance of a search strategy with the request + * handler context bound to it. This way every search strategy can use + * whatever information they require from the request context. + */ +export type TSearchStrategyProvider = ( + context: ISearchContext, + search: ISearchGeneric +) => ISearchStrategy; + +/** + * Extension point exposed for other plugins to register their own search + * strategies. + */ +export type TRegisterSearchStrategyProvider = ( + opaqueId: symbol, + name: T, + searchStrategyProvider: TSearchStrategyProvider +) => void; + +export type TSearchStrategiesMap = { + [K in TStrategyTypes]?: TSearchStrategyProviderEnhanced; +}; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts new file mode 100644 index 0000000000000..d36202debd9b9 --- /dev/null +++ b/src/plugins/data/public/search/index.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ISearchAppMountContext } from './i_search_app_mount_context'; + +export { ISearchSetup } from './i_search_setup'; + +export { ISearchContext } from './i_search_context'; + +export { + ISearch, + ISearchOptions, + IRequestTypesMap, + IResponseTypesMap, + ISearchGeneric, +} from './i_search'; + +export { TSearchStrategyProvider, ISearchStrategy } from './i_search_strategy'; + +export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; + +export { SYNC_SEARCH_STRATEGY } from './sync_search_strategy'; + +export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; + +export { ISearchStart } from './search_service'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts new file mode 100644 index 0000000000000..81a028007bc94 --- /dev/null +++ b/src/plugins/data/public/search/mocks.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const searchSetupMock = { + registerSearchStrategyContext: jest.fn(), + registerSearchStrategyProvider: jest.fn(), +}; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts new file mode 100644 index 0000000000000..8102b0e915fb7 --- /dev/null +++ b/src/plugins/data/public/search/search_service.test.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../core/public/mocks'; + +import { SearchService } from './search_service'; +import { CoreSetup } from '../../../../core/public'; + +describe('Search service', () => { + let searchService: SearchService; + let mockCoreSetup: MockedKeys; + const opaqueId = Symbol(); + beforeEach(() => { + searchService = new SearchService({ opaqueId }); + mockCoreSetup = coreMock.createSetup(); + }); + + describe('setup()', () => { + it('exposes proper contract', async () => { + const setup = searchService.setup(mockCoreSetup); + expect(setup).toHaveProperty('registerSearchStrategyContext'); + expect(setup).toHaveProperty('registerSearchStrategyProvider'); + }); + }); +}); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts new file mode 100644 index 0000000000000..6030884c9f6b1 --- /dev/null +++ b/src/plugins/data/public/search/search_service.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + Plugin, + CoreSetup, + PluginInitializerContext, + CoreStart, + IContextContainer, + PluginOpaqueId, +} from '../../../../core/public'; + +import { ISearchAppMountContext } from './i_search_app_mount_context'; +import { ISearchSetup } from './i_search_setup'; +import { createAppMountSearchContext } from './create_app_mount_context_search'; +import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy'; +import { + TSearchStrategyProvider, + TRegisterSearchStrategyProvider, + TSearchStrategiesMap, +} from './i_search_strategy'; +import { TStrategyTypes } from './strategy_types'; +import { esSearchService } from './es_search'; +import { ISearchGeneric } from './i_search'; + +/** + * Extends the AppMountContext so other plugins have access + * to search functionality in their applications. + */ +declare module 'kibana/public' { + interface AppMountContext { + search?: ISearchAppMountContext; + } +} + +export interface ISearchStart { + search: ISearchGeneric; +} + +/** + * The search plugin exposes two registration methods for other plugins: + * - registerSearchStrategyProvider for plugins to add their own custom + * search strategies + * - registerSearchStrategyContext for plugins to expose information + * and/or functionality for other search strategies to use + * + * It also comes with two search strategy implementations - SYNC_SEARCH_STRATEGY and ES_SEARCH_STRATEGY. + */ +export class SearchService implements Plugin { + /** + * A mapping of search strategies keyed by a unique identifier. Plugins can use this unique identifier + * to override certain strategy implementations. + */ + private searchStrategies: TSearchStrategiesMap = {}; + + /** + * Exposes context to the search strategies. + */ + private contextContainer?: IContextContainer>; + + private search?: ISearchGeneric; + + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup): ISearchSetup { + const search = (this.search = createAppMountSearchContext(this.searchStrategies).search); + core.application.registerMountContext<'search'>('search', () => { + return { search }; + }); + + this.contextContainer = core.context.createContextContainer(); + + const registerSearchStrategyProvider: TRegisterSearchStrategyProvider = < + T extends TStrategyTypes + >( + plugin: PluginOpaqueId, + name: T, + strategyProvider: TSearchStrategyProvider + ) => { + this.searchStrategies[name] = this.contextContainer!.createHandler(plugin, strategyProvider); + }; + + const api = { + registerSearchStrategyContext: this.contextContainer!.registerContext, + registerSearchStrategyProvider, + }; + + api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core); + api.registerSearchStrategyProvider( + this.initializerContext.opaqueId, + SYNC_SEARCH_STRATEGY, + syncSearchStrategyProvider + ); + + // ES search capabilities are written in a way that it could easily be a separate plugin, + // however these two plugins are tightly coupled due to the default search strategy using + // es search types. + esSearchService(this.initializerContext).setup(core, { search: api }); + + return api; + } + + public start(core: CoreStart) { + if (!this.search) { + throw new Error('Search should always be defined'); + } + return { search: this.search }; + } + + public stop() {} +} diff --git a/src/plugins/data/public/search/strategy_types.ts b/src/plugins/data/public/search/strategy_types.ts new file mode 100644 index 0000000000000..9afac9b546362 --- /dev/null +++ b/src/plugins/data/public/search/strategy_types.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ES_SEARCH_STRATEGY } from '../../common/search/es_search'; +import { SYNC_SEARCH_STRATEGY } from './sync_search_strategy'; + +/** + * Contains all known strategy type identifiers that will be used to map to + * request and response shapes. Plugins that wish to add their own custom search + * strategies should extend this type via: + * + * const MY_STRATEGY = 'MY_STRATEGY'; + * + * declare module 'src/plugins/data/public' { + * export interface IRequestTypesMap { + * [MY_STRATEGY]: IMySearchRequest; + * } + * + * export interface IResponseTypesMap { + * [MY_STRATEGY]: IMySearchResponse + * } + * } + */ +export type TStrategyTypes = typeof SYNC_SEARCH_STRATEGY | typeof ES_SEARCH_STRATEGY | string; diff --git a/src/plugins/data/public/search/sync_search_strategy.test.ts b/src/plugins/data/public/search/sync_search_strategy.test.ts new file mode 100644 index 0000000000000..2737a4033a015 --- /dev/null +++ b/src/plugins/data/public/search/sync_search_strategy.test.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../core/public/mocks'; +import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy'; +import { CoreSetup } from '../../../../core/public'; + +describe('Sync search strategy', () => { + let mockCoreSetup: MockedKeys; + const mockSearch = jest.fn(); + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + }); + + it('returns a strategy with `search` that calls the backend API', () => { + mockCoreSetup.http.fetch.mockImplementationOnce(() => Promise.resolve()); + + const syncSearch = syncSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockSearch + ); + syncSearch.search( + { + serverStrategy: SYNC_SEARCH_STRATEGY, + }, + {} + ); + expect(mockCoreSetup.http.fetch.mock.calls[0][0]).toBe( + `/internal/search/${SYNC_SEARCH_STRATEGY}` + ); + expect(mockCoreSetup.http.fetch.mock.calls[0][1]).toEqual({ + body: JSON.stringify({ + serverStrategy: 'SYNC_SEARCH_STRATEGY', + }), + method: 'POST', + signal: undefined, + }); + }); +}); diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts new file mode 100644 index 0000000000000..c412bbb3b104a --- /dev/null +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { from } from 'rxjs'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../common/search'; +import { ISearchContext } from './i_search_context'; +import { ISearch, ISearchOptions } from './i_search'; +import { TSearchStrategyProvider, ISearchStrategy } from './i_search_strategy'; + +export const SYNC_SEARCH_STRATEGY = 'SYNC_SEARCH_STRATEGY'; + +export interface ISyncSearchRequest extends IKibanaSearchRequest { + serverStrategy: string; +} + +export const syncSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext +) => { + const search: ISearch = ( + request: ISyncSearchRequest, + options: ISearchOptions + ) => { + const response: Promise = context.core.http.fetch( + `/internal/search/${request.serverStrategy}`, + { + method: 'POST', + body: JSON.stringify(request), + signal: options.signal, + } + ); + + return from(response); + }; + + const strategy: ISearchStrategy = { + search, + }; + + return strategy; +}; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 70406b4dc0c0a..5f94734fef083 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -20,14 +20,17 @@ export * from './autocomplete_provider/types'; import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.'; +import { ISearchSetup, ISearchStart } from './search'; import { IGetSuggestions } from './suggestions_provider/types'; export interface DataPublicPluginSetup { autocomplete: AutocompletePublicPluginSetup; + search: ISearchSetup; } export interface DataPublicPluginStart { autocomplete: AutocompletePublicPluginStart; getSuggestions: IGetSuggestions; + search: ISearchStart; } export { IGetSuggestions } from './suggestions_provider/types'; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 60734f25b46a3..df933167cee25 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -25,3 +25,7 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { DataServerPlugin as Plugin }; + +export * from './search'; + +export { IRequestTypesMap, IResponseTypesMap } from './search'; diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 3aa04c7a5be7a..9cf08b0702e9e 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -18,10 +18,23 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server'; +import { ISearchSetup } from './search'; +import { SearchService } from './search/search_service'; -export class DataServerPlugin implements Plugin { - constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup) {} +export interface DataPluginSetup { + search: ISearchSetup; +} + +export class DataServerPlugin implements Plugin { + private readonly searchService: SearchService; + constructor(initializerContext: PluginInitializerContext) { + this.searchService = new SearchService(initializerContext); + } + public setup(core: CoreSetup) { + return { + search: this.searchService.setup(core), + }; + } public start(core: CoreStart) {} public stop() {} } diff --git a/src/plugins/data/server/search/README.md b/src/plugins/data/server/search/README.md new file mode 100644 index 0000000000000..33e6d9ab0bd1a --- /dev/null +++ b/src/plugins/data/server/search/README.md @@ -0,0 +1,13 @@ +# search + +The `search` plugin provides the ability to register search strategies that take in a request +object, and return a response object, of a given shape. + +Both client side search strategies can be registered, as well as server side search strategies. + +The `search` plugin includes two one concrete client side implementations - + `SYNC_SEARCH_STRATEGY` and `ES_SEARCH_STRATEGY` which uses `SYNC_SEARCH_STRATEGY`. There is also one + default server side search strategy, `ES_SEARCH_STRATEGY`. + + Includes the `esSearch` plugin in order to search for data from Elasticsearch using Elasticsearch +DSL. diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts new file mode 100644 index 0000000000000..32570a05031f6 --- /dev/null +++ b/src/plugins/data/server/search/create_api.test.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createApi } from './create_api'; + +import { TSearchStrategiesMap } from './i_search_strategy'; +import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; +import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; + +// let mockCoreSetup: MockedKeys; + +const mockDefaultSearch = jest.fn(() => Promise.resolve({ percentComplete: 0 })); +const mockDefaultSearchStrategyProvider = jest.fn(() => + Promise.resolve({ + search: mockDefaultSearch, + }) +); +const mockStrategies: TSearchStrategiesMap = { + [DEFAULT_SEARCH_STRATEGY]: mockDefaultSearchStrategyProvider, +}; + +describe('createApi', () => { + let api: IRouteHandlerSearchContext; + + beforeEach(() => { + api = createApi({ + caller: jest.fn(), + searchStrategies: mockStrategies, + }); + mockDefaultSearchStrategyProvider.mockClear(); + }); + + it('should default to DEFAULT_SEARCH_STRATEGY if none is provided', async () => { + await api.search({ + params: {}, + }); + expect(mockDefaultSearchStrategyProvider).toBeCalled(); + expect(mockDefaultSearch).toBeCalled(); + }); + + it('should throw if no provider is found for the given name', () => { + expect(api.search({}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( + `"No strategy found for noneByThisName"` + ); + }); +}); diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts new file mode 100644 index 0000000000000..4c13dd9e1137c --- /dev/null +++ b/src/plugins/data/server/search/create_api.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { APICaller } from 'kibana/server'; +import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; +import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; +import { TSearchStrategiesMap } from './i_search_strategy'; + +export function createApi({ + caller, + searchStrategies, +}: { + searchStrategies: TSearchStrategiesMap; + caller: APICaller; +}) { + const api: IRouteHandlerSearchContext = { + search: async (request, strategyName) => { + const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; + const strategyProvider = searchStrategies[name]; + if (!strategyProvider) { + throw new Error(`No strategy found for ${strategyName}`); + } + // Give providers access to other search strategies by injecting this function + const strategy = await strategyProvider(caller, api.search); + return strategy.search(request); + }, + }; + return api; +} diff --git a/src/plugins/data/server/search/es_search/elasticsearch.ts b/src/plugins/data/server/search/es_search/elasticsearch.ts new file mode 100644 index 0000000000000..6c3aa771328a8 --- /dev/null +++ b/src/plugins/data/server/search/es_search/elasticsearch.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface StringMap { + [key: string]: T; +} + +export type IndexAsString = { + [k: string]: Map[keyof Map]; +} & Map; + +export type Omit = Pick>; + +export interface BoolQuery { + must_not: Array>; + should: Array>; + filter: Array>; +} diff --git a/src/plugins/data/server/search/es_search/es_search_service.test.ts b/src/plugins/data/server/search/es_search/es_search_service.test.ts new file mode 100644 index 0000000000000..faf9487159c15 --- /dev/null +++ b/src/plugins/data/server/search/es_search/es_search_service.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../../core/server/mocks'; +import { EsSearchService } from './es_search_service'; +import { PluginInitializerContext } from '../../../../../core/server'; +import { searchSetupMock } from '../mocks'; + +describe('ES search strategy service', () => { + let service: EsSearchService; + + const mockCoreSetup = coreMock.createSetup(); + const opaqueId = Symbol(); + const context: PluginInitializerContext = { + opaqueId, + config: { + createIfExists: jest.fn(), + create: jest.fn(), + }, + env: { + mode: { + dev: false, + name: 'development', + prod: false, + }, + }, + logger: { + get: jest.fn(), + }, + }; + + beforeEach(() => { + service = new EsSearchService(context); + }); + + describe('setup()', () => { + it('registers the ES search strategy', async () => { + service.setup(mockCoreSetup, { + search: searchSetupMock, + }); + expect(searchSetupMock.registerSearchStrategyProvider).toBeCalled(); + }); + }); +}); diff --git a/src/plugins/data/server/search/es_search/es_search_service.ts b/src/plugins/data/server/search/es_search/es_search_service.ts new file mode 100644 index 0000000000000..b33b6c6ecd318 --- /dev/null +++ b/src/plugins/data/server/search/es_search/es_search_service.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISearchSetup } from '../i_search_setup'; +import { PluginInitializerContext, CoreSetup, Plugin } from '../../../../../core/server'; +import { esSearchStrategyProvider } from './es_search_strategy'; +import { ES_SEARCH_STRATEGY } from '../../../common/search'; + +interface IEsSearchDependencies { + search: ISearchSetup; +} + +export class EsSearchService implements Plugin { + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, deps: IEsSearchDependencies) { + deps.search.registerSearchStrategyProvider( + this.initializerContext.opaqueId, + ES_SEARCH_STRATEGY, + esSearchStrategyProvider + ); + } + + public start() {} + public stop() {} +} diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts new file mode 100644 index 0000000000000..619a28df839bd --- /dev/null +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../../core/server/mocks'; +import { esSearchStrategyProvider } from './es_search_strategy'; + +describe('ES search strategy', () => { + const mockCoreSetup = coreMock.createSetup(); + const mockApiCaller = jest.fn().mockResolvedValue({ + _shards: { + total: 10, + failed: 1, + skipped: 2, + successful: 7, + }, + }); + const mockSearch = jest.fn(); + + beforeEach(() => { + mockApiCaller.mockClear(); + mockSearch.mockClear(); + }); + + it('returns a strategy with `search`', () => { + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + expect(typeof esSearch.search).toBe('function'); + }); + + it('logs the response if `debug` is set to `true`', () => { + const spy = jest.spyOn(console, 'log'); + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + expect(spy).not.toBeCalled(); + + esSearch.search({ params: {}, debug: true }); + + expect(spy).toBeCalled(); + }); + + it('calls the API caller with the params', () => { + const params = { index: 'logstash-*' }; + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + esSearch.search({ params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('search'); + expect(mockApiCaller.mock.calls[0][1]).toEqual(params); + }); + + it('returns total, loaded, and raw response', async () => { + const params = { index: 'logstash-*' }; + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + const response = await esSearch.search({ params }); + + expect(response).toHaveProperty('total'); + expect(response).toHaveProperty('loaded'); + expect(response).toHaveProperty('rawResponse'); + }); +}); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts new file mode 100644 index 0000000000000..31f4fc15a0989 --- /dev/null +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { APICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; +import { IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../../common/search'; +import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy'; +import { ISearchContext } from '..'; + +export const esSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext, + caller: APICaller +): ISearchStrategy => { + return { + search: async (request: IEsSearchRequest) => { + if (request.debug) { + // eslint-disable-next-line + console.log(JSON.stringify(request, null, 2)); + } + const esSearchResponse = (await caller('search', { + ...request.params, + // TODO: could do something like this here? + // ...getCurrentSearchParams(context), + })) as SearchResponse; + + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + return { + total: esSearchResponse._shards.total, + loaded: + esSearchResponse._shards.failed + + esSearchResponse._shards.skipped + + esSearchResponse._shards.successful, + rawResponse: esSearchResponse, + }; + }, + }; +}; diff --git a/src/plugins/data/server/search/es_search/index.ts b/src/plugins/data/server/search/es_search/index.ts new file mode 100644 index 0000000000000..a1d4070114ad5 --- /dev/null +++ b/src/plugins/data/server/search/es_search/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from '../../../../../core/server'; +import { EsSearchService } from './es_search_service'; + +export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common/search'; + +export function esSearchService(initializerContext: PluginInitializerContext) { + return new EsSearchService(initializerContext); +} diff --git a/src/plugins/data/server/search/i_route_handler_search_context.ts b/src/plugins/data/server/search/i_route_handler_search_context.ts new file mode 100644 index 0000000000000..8a44738a1dcfa --- /dev/null +++ b/src/plugins/data/server/search/i_route_handler_search_context.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISearchGeneric } from './i_search'; + +export interface IRouteHandlerSearchContext { + search: ISearchGeneric; +} diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts new file mode 100644 index 0000000000000..fabcb98ceea72 --- /dev/null +++ b/src/plugins/data/server/search/i_search.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; +import { TStrategyTypes } from './strategy_types'; +import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search'; +import { IEsSearchRequest } from './es_search'; + +export interface IRequestTypesMap { + [ES_SEARCH_STRATEGY]: IEsSearchRequest; + [key: string]: IKibanaSearchRequest; +} + +export interface IResponseTypesMap { + [ES_SEARCH_STRATEGY]: IEsSearchResponse; + [key: string]: IKibanaSearchResponse; +} + +export type ISearchGeneric = ( + request: IRequestTypesMap[T], + strategy?: T +) => Promise; + +export type ISearch = ( + request: IRequestTypesMap[T] +) => Promise; diff --git a/src/plugins/data/server/search/i_search_context.ts b/src/plugins/data/server/search/i_search_context.ts new file mode 100644 index 0000000000000..5f2df5d8e819e --- /dev/null +++ b/src/plugins/data/server/search/i_search_context.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CoreSetup } from '../../../../core/server'; + +export interface ISearchContext { + core: CoreSetup; +} diff --git a/src/plugins/data/server/search/i_search_setup.ts b/src/plugins/data/server/search/i_search_setup.ts new file mode 100644 index 0000000000000..fb84cabfd37be --- /dev/null +++ b/src/plugins/data/server/search/i_search_setup.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IContextProvider, APICaller } from 'kibana/server'; +import { ISearchContext } from './i_search_context'; +import { IResponseTypesMap, IRequestTypesMap } from './i_search'; +import { TRegisterSearchStrategyProvider, TSearchStrategyProvider } from './i_search_strategy'; +import { TStrategyTypes } from './strategy_types'; +import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; + +/** + * The setup contract exposed by the Search plugin exposes the search strategy extension + * point. + */ +export interface ISearchSetup { + registerSearchStrategyContext: ( + pluginId: symbol, + strategyName: TContextName, + provider: IContextProvider, TContextName> + ) => void; + + /** + * Extension point exposed for other plugins to register their own search + * strategies. + */ + registerSearchStrategyProvider: TRegisterSearchStrategyProvider; + + __LEGACY: { + search: ( + caller: APICaller, + request: IRequestTypesMap[T], + strategyName?: T + ) => Promise; + }; +} diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts new file mode 100644 index 0000000000000..d00dd552c9e95 --- /dev/null +++ b/src/plugins/data/server/search/i_search_strategy.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { APICaller } from 'kibana/server'; +import { ISearch, ISearchGeneric } from './i_search'; +import { TStrategyTypes } from './strategy_types'; +import { ISearchContext } from './i_search_context'; + +/** + * Search strategy interface contains a search method that takes in + * a request and returns a promise that resolves to a response. + */ +export interface ISearchStrategy { + search: ISearch; +} + +/** + * Search strategy provider creates an instance of a search strategy with the request + * handler context bound to it. This way every search strategy can use + * whatever information they require from the request context. + */ +export type TSearchStrategyProviderEnhanced = ( + caller: APICaller, + search: ISearchGeneric +) => Promise>; + +/** + * Search strategy provider creates an instance of a search strategy with the request + * handler context bound to it. This way every search strategy can use + * whatever information they require from the request context. + */ +export type TSearchStrategyProvider = ( + context: ISearchContext, + caller: APICaller, + search: ISearchGeneric +) => ISearchStrategy; + +/** + * Extension point exposed for other plugins to register their own search + * strategies. + */ +export type TRegisterSearchStrategyProvider = ( + opaqueId: symbol, + name: T, + searchStrategyProvider: TSearchStrategyProvider +) => void; + +export type TSearchStrategiesMap = { + [K in TStrategyTypes]?: TSearchStrategyProviderEnhanced; +}; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts new file mode 100644 index 0000000000000..e160fd4026c58 --- /dev/null +++ b/src/plugins/data/server/search/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ISearchSetup } from './i_search_setup'; +export * from '../../common'; + +export { ISearchContext } from './i_search_context'; + +export { IRequestTypesMap, IResponseTypesMap } from './i_search'; + +export { TStrategyTypes } from './strategy_types'; + +export { TSearchStrategyProvider } from './i_search_strategy'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts new file mode 100644 index 0000000000000..136e7a1d580c9 --- /dev/null +++ b/src/plugins/data/server/search/mocks.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const searchSetupMock = { + registerSearchStrategyContext: jest.fn(), + registerSearchStrategyProvider: jest.fn(), + __LEGACY: { + search: jest.fn(), + }, +}; diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts new file mode 100644 index 0000000000000..0923b52565097 --- /dev/null +++ b/src/plugins/data/server/search/routes.test.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { httpServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { registerSearchRoute } from './routes'; +import { IRouter, ScopedClusterClient } from 'kibana/server'; + +describe('Search service', () => { + let routerMock: jest.Mocked; + + beforeEach(() => { + routerMock = httpServiceMock.createRouter(); + }); + + it('registers a post route', async () => { + registerSearchRoute(routerMock); + expect(routerMock.post).toBeCalled(); + }); + + it('handler calls context.search.search with the given request and strategy', async () => { + const mockSearch = jest.fn().mockResolvedValue('yay'); + const mockContext = { + core: { + elasticsearch: { + dataClient: {} as ScopedClusterClient, + adminClient: {} as ScopedClusterClient, + }, + }, + search: { + search: mockSearch, + }, + }; + const mockBody = { params: {} }; + const mockParams = { strategy: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + params: mockParams, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerSearchRoute(routerMock); + const handler = routerMock.post.mock.calls[0][1]; + await handler(mockContext, mockRequest, mockResponse); + + expect(mockSearch).toBeCalled(); + expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); + expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); + }); + + it('handler throws internal error if the search throws an error', async () => { + const mockSearch = jest.fn().mockRejectedValue('oh no'); + const mockContext = { + core: { + elasticsearch: { + dataClient: {} as ScopedClusterClient, + adminClient: {} as ScopedClusterClient, + }, + }, + search: { + search: mockSearch, + }, + }; + const mockBody = { params: {} }; + const mockParams = { strategy: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + params: mockParams, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerSearchRoute(routerMock); + const handler = routerMock.post.mock.calls[0][1]; + await handler(mockContext, mockRequest, mockResponse); + + expect(mockSearch).toBeCalled(); + expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); + expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockResponse.internalError).toBeCalled(); + expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); + }); +}); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts new file mode 100644 index 0000000000000..6cb6c28c76014 --- /dev/null +++ b/src/plugins/data/server/search/routes.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../core/server'; + +export function registerSearchRoute(router: IRouter): void { + router.post( + { + path: '/internal/search/{strategy}', + validate: { + params: schema.object({ strategy: schema.string() }), + + query: schema.object({}, { allowUnknowns: true }), + + body: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, res) => { + const searchRequest = request.body; + const strategy = request.params.strategy; + try { + const response = await context.search!.search(searchRequest, strategy); + return res.ok({ body: response }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); +} diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts new file mode 100644 index 0000000000000..2b5c144f8fa67 --- /dev/null +++ b/src/plugins/data/server/search/search_service.test.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../core/server/mocks'; + +import { SearchService } from './search_service'; +import { CoreSetup } from '../../../../core/server'; + +const mockSearchApi = { search: jest.fn() }; +jest.mock('./create_api', () => ({ + createApi: () => mockSearchApi, +})); + +describe('Search service', () => { + let plugin: SearchService; + let mockCoreSetup: MockedKeys; + + beforeEach(() => { + plugin = new SearchService(coreMock.createPluginInitializerContext({})); + mockCoreSetup = coreMock.createSetup(); + mockSearchApi.search.mockClear(); + }); + + describe('setup()', () => { + it('exposes proper contract', async () => { + const setup = plugin.setup(mockCoreSetup); + expect(setup).toHaveProperty('registerSearchStrategyContext'); + expect(setup).toHaveProperty('registerSearchStrategyProvider'); + expect(setup).toHaveProperty('__LEGACY'); + }); + }); + + describe('__LEGACY', () => { + it('calls searchAPI.search', async () => { + const setup = plugin.setup(mockCoreSetup); + setup.__LEGACY.search(jest.fn(), {}, 'foo'); + expect(mockSearchApi.search).toBeCalled(); + }); + }); +}); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts new file mode 100644 index 0000000000000..4edb51300dfaf --- /dev/null +++ b/src/plugins/data/server/search/search_service.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + PluginInitializerContext, + Plugin, + CoreSetup, + IContextContainer, +} from '../../../../core/server'; +import { registerSearchRoute } from './routes'; +import { ISearchSetup } from './i_search_setup'; +import { createApi } from './create_api'; +import { + TSearchStrategiesMap, + TSearchStrategyProvider, + TRegisterSearchStrategyProvider, +} from './i_search_strategy'; +import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; +import { esSearchService } from './es_search'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + search?: IRouteHandlerSearchContext; + } +} + +export class SearchService implements Plugin { + private searchStrategies: TSearchStrategiesMap = {}; + + private contextContainer?: IContextContainer>; + + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup): ISearchSetup { + const router = core.http.createRouter(); + registerSearchRoute(router); + + this.contextContainer = core.context.createContextContainer(); + + core.http.registerRouteHandlerContext<'search'>('search', context => { + return createApi({ + caller: context.core!.elasticsearch.dataClient.callAsCurrentUser, + searchStrategies: this.searchStrategies, + }); + }); + + const registerSearchStrategyProvider: TRegisterSearchStrategyProvider = ( + plugin, + name, + strategyProvider + ) => { + this.searchStrategies[name] = this.contextContainer!.createHandler(plugin, strategyProvider); + }; + + const api: ISearchSetup = { + registerSearchStrategyContext: this.contextContainer!.registerContext, + registerSearchStrategyProvider, + __LEGACY: { + search: (caller, request, strategyName) => { + const searchAPI = createApi({ + caller, + searchStrategies: this.searchStrategies, + }); + return searchAPI.search(request, strategyName); + }, + }, + }; + + api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core); + + // ES search capabilities are written in a way that it could easily be a separate plugin, + // however these two plugins are tightly coupled due to the default search strategy using + // es search types. + esSearchService(this.initializerContext).setup(core, { search: api }); + + return api; + } + + public start() {} + public stop() {} +} diff --git a/src/plugins/data/server/search/strategy_types.ts b/src/plugins/data/server/search/strategy_types.ts new file mode 100644 index 0000000000000..252e0c8f9e6c9 --- /dev/null +++ b/src/plugins/data/server/search/strategy_types.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ES_SEARCH_STRATEGY } from '../../common/search/es_search'; + +/** + * Contains all known strategy type identifiers that will be used to map to + * request and response shapes. Plugins that wish to add their own custom search + * strategies should extend this type via: + * + * const MY_STRATEGY = 'MY_STRATEGY'; + * + * declare module 'src/plugins/search/server' { + * export interface IRequestTypesMap { + * [MY_STRATEGY]: IMySearchRequest; + * } + * + * export interface IResponseTypesMap { + * [MY_STRATEGY]: IMySearchResponse + * } + * } + */ +export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string; diff --git a/test/functional/services/find.ts b/test/functional/services/find.ts index ec602e2df03a1..c65821ea73ace 100644 --- a/test/functional/services/find.ts +++ b/test/functional/services/find.ts @@ -213,6 +213,17 @@ export async function FindProvider({ getService }: FtrProviderContext) { return await this.filterElementIsDisplayed(allElements); } + public async allDescendantDisplayedByTagName( + tagName: string, + parentElement: WebElementWrapper + ): Promise { + log.debug(`Find.allDescendantDisplayedByTagName('${tagName}')`); + const allElements = await wrapAll( + await parentElement._webElement.findElements(By.tagName(tagName)) + ); + return await this.filterElementIsDisplayed(allElements); + } + public async displayedByLinkText( linkText: string, timeout: number = defaultFindTimeout diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index b0c4e8732c267..c3b2076dce9f9 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -72,6 +72,27 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { : find.waitForDeletedByCssSelector(testSubjSelector(selector), timeout)); } + async stringExistsInCodeBlockOrFail(codeBlockSelector: string, stringToFind: string) { + await retry.try(async () => { + const responseCodeBlock = await this.find(codeBlockSelector); + const spans = await find.allDescendantDisplayedByTagName('span', responseCodeBlock); + const foundInSpans = await Promise.all( + spans.map(async span => { + const text = await span.getVisibleText(); + if (text === stringToFind) { + log.debug(`"${text}" matched "${stringToFind}"!`); + return true; + } else { + log.debug(`"${text}" did not match "${stringToFind}"`); + } + }) + ); + if (!foundInSpans.find(foundInSpan => foundInSpan)) { + throw new Error(`"${stringToFind}" was not found. Trying again...`); + } + }); + } + public async append(selector: string, text: string): Promise { return await retry.try(async () => { log.debug(`TestSubjects.append(${selector}, ${text})`); diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index b2479e2c42785..7d4ece98249f8 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -33,6 +33,7 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/custom_visualizations'), require.resolve('./test_suites/embedding_visualizations'), require.resolve('./test_suites/panel_actions'), + require.resolve('./test_suites/search'), /** * @todo Work on re-enabling this test suite after this is merged. These tests pass diff --git a/test/plugin_functional/plugins/demo_search/common/index.ts b/test/plugin_functional/plugins/demo_search/common/index.ts new file mode 100644 index 0000000000000..0339e8fbda8c5 --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/common/index.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../src/plugins/data/common/search'; + +export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY'; + +export interface IDemoRequest extends IKibanaSearchRequest { + mood: string | 'sad' | 'happy'; + name: string; +} + +export interface IDemoResponse extends IKibanaSearchResponse { + greeting: string; +} diff --git a/test/plugin_functional/plugins/demo_search/kibana.json b/test/plugin_functional/plugins/demo_search/kibana.json new file mode 100644 index 0000000000000..0603706b07d1f --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "demoSearch", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["demo_search"], + "server": true, + "ui": true, + "requiredPlugins": ["data"], + "optionalPlugins": [] +} diff --git a/test/plugin_functional/plugins/demo_search/package.json b/test/plugin_functional/plugins/demo_search/package.json new file mode 100644 index 0000000000000..1f4fa1421906a --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/package.json @@ -0,0 +1,17 @@ +{ + "name": "demo_data_search", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/demo_data_search", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts b/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts new file mode 100644 index 0000000000000..377163251010c --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { + ISearchContext, + SYNC_SEARCH_STRATEGY, + ISearchGeneric, +} from '../../../../../src/plugins/data/public'; +import { TSearchStrategyProvider, ISearchStrategy } from '../../../../../src/plugins/data/public'; + +import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; + +/** + * This demo search strategy provider simply provides a shortcut for calling the DEMO_SEARCH_STRATEGY + * on the server side, without users having to pass it in explicitly, and it takes advantage of the + * already registered SYNC_SEARCH_STRATEGY that exists on the client. + * + * so instead of callers having to do: + * + * ``` + * context.search( + * { ...request, serverStrategy: DEMO_SEARCH_STRATEGY }, + * options, + * SYNC_SEARCH_STRATEGY + * ) as Observable, + *``` + + * They can instead just do + * + * ``` + * context.search(request, options, DEMO_SEARCH_STRATEGY); + * ``` + * + * and are ensured type safety in regard to the request and response objects. + * + * @param context - context supplied by other plugins. + * @param search - a search function to access other strategies that have already been registered. + */ +export const demoClientSearchStrategyProvider: TSearchStrategyProvider< + typeof DEMO_SEARCH_STRATEGY +> = ( + context: ISearchContext, + search: ISearchGeneric +): ISearchStrategy => { + return { + search: (request, options) => + search( + { ...request, serverStrategy: DEMO_SEARCH_STRATEGY }, + options, + SYNC_SEARCH_STRATEGY + ) as Observable, + }; +}; diff --git a/test/plugin_functional/plugins/demo_search/public/index.ts b/test/plugin_functional/plugins/demo_search/public/index.ts new file mode 100644 index 0000000000000..7790c2950ac22 --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/public/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; + +import { DemoDataPlugin } from './plugin'; + +export { DEMO_SEARCH_STRATEGY } from '../common'; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new DemoDataPlugin(initializerContext); diff --git a/test/plugin_functional/plugins/demo_search/public/plugin.ts b/test/plugin_functional/plugins/demo_search/public/plugin.ts new file mode 100644 index 0000000000000..37f8d3955708a --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; +import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../src/core/public'; +import { DEMO_SEARCH_STRATEGY } from '../common'; +import { demoClientSearchStrategyProvider } from './demo_search_strategy'; +import { IDemoRequest, IDemoResponse } from '../common'; + +interface DemoDataSearchSetupDependencies { + data: DataPublicPluginSetup; +} + +/** + * Add the typescript mappings for our search strategy to the request and + * response types. This allows typescript to require the right shapes if + * making the call: + * const response = context.search.search(request, {}, DEMO_SEARCH_STRATEGY); + * + * If the caller does not pass in the right `request` shape, typescript will + * complain. The caller will also get a typed response. + */ +declare module '../../../../../src/plugins/data/public' { + export interface IRequestTypesMap { + [DEMO_SEARCH_STRATEGY]: IDemoRequest; + } + + export interface IResponseTypesMap { + [DEMO_SEARCH_STRATEGY]: IDemoResponse; + } +} + +export class DemoDataPlugin implements Plugin { + constructor(private initializerContext: PluginInitializerContext) {} + public setup(core: CoreSetup, deps: DemoDataSearchSetupDependencies) { + deps.data.search.registerSearchStrategyProvider( + this.initializerContext.opaqueId, + DEMO_SEARCH_STRATEGY, + demoClientSearchStrategyProvider + ); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/plugins/demo_search/server/constants.ts b/test/plugin_functional/plugins/demo_search/server/constants.ts new file mode 100644 index 0000000000000..11c258a21d5a8 --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/server/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const FAKE_PROGRESS_STRATEGY = 'FAKE_PROGRESS_STRATEGY'; diff --git a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts b/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts new file mode 100644 index 0000000000000..acb75b15196d6 --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TSearchStrategyProvider } from 'src/plugins/data/server'; +import { DEMO_SEARCH_STRATEGY } from '../common'; + +export const demoSearchStrategyProvider: TSearchStrategyProvider< + typeof DEMO_SEARCH_STRATEGY +> = () => { + return { + search: request => { + return Promise.resolve({ + greeting: + request.mood === 'happy' + ? `Lovely to meet you, ${request.name}` + : `Hope you feel better, ${request.name}`, + }); + }, + }; +}; diff --git a/test/plugin_functional/plugins/demo_search/server/index.ts b/test/plugin_functional/plugins/demo_search/server/index.ts new file mode 100644 index 0000000000000..6289b684b2b1e --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/server/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext, PluginInitializer } from 'kibana/server'; +import { DemoDataPlugin } from './plugin'; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new DemoDataPlugin(initializerContext); diff --git a/test/plugin_functional/plugins/demo_search/server/plugin.ts b/test/plugin_functional/plugins/demo_search/server/plugin.ts new file mode 100644 index 0000000000000..c6628e7c76820 --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/server/plugin.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; +import { DataPluginSetup } from 'src/plugins/data/server/plugin'; +import { demoSearchStrategyProvider } from './demo_search_strategy'; +import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common'; + +interface IDemoSearchExplorerDeps { + data: DataPluginSetup; +} + +/** + * Add the typescript mappings for our search strategy to the request and + * response types. This allows typescript to require the right shapes if + * making the call: + * const response = context.search.search(request, DEMO_SEARCH_STRATEGY); + * + * If the caller does not pass in the right `request` shape, typescript will + * complain. The caller will also get a typed response. + */ +declare module '../../../../../src/plugins/data/server' { + export interface IRequestTypesMap { + [DEMO_SEARCH_STRATEGY]: IDemoRequest; + } + + export interface IResponseTypesMap { + [DEMO_SEARCH_STRATEGY]: IDemoResponse; + } +} + +export class DemoDataPlugin implements Plugin { + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, deps: IDemoSearchExplorerDeps) { + deps.data.search.registerSearchStrategyProvider( + this.initializerContext.opaqueId, + DEMO_SEARCH_STRATEGY, + demoSearchStrategyProvider + ); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/plugins/demo_search/tsconfig.json b/test/plugin_functional/plugins/demo_search/tsconfig.json new file mode 100644 index 0000000000000..304ffdc0a299d --- /dev/null +++ b/test/plugin_functional/plugins/demo_search/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*" + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/search_explorer/kibana.json b/test/plugin_functional/plugins/search_explorer/kibana.json new file mode 100644 index 0000000000000..39b866ca4c2c6 --- /dev/null +++ b/test/plugin_functional/plugins/search_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "search_explorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["search_explorer"], + "server": false, + "ui": true, + "requiredPlugins": ["data", "demoSearch"], + "optionalPlugins": [] +} diff --git a/test/plugin_functional/plugins/search_explorer/package.json b/test/plugin_functional/plugins/search_explorer/package.json new file mode 100644 index 0000000000000..9a5e0e83a2207 --- /dev/null +++ b/test/plugin_functional/plugins/search_explorer/package.json @@ -0,0 +1,17 @@ +{ + "name": "search_explorer", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/search_explorer", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/search_explorer/public/application.tsx b/test/plugin_functional/plugins/search_explorer/public/application.tsx new file mode 100644 index 0000000000000..4762209a548c1 --- /dev/null +++ b/test/plugin_functional/plugins/search_explorer/public/application.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom'; + +import { + EuiPage, + EuiPageSideBar, + // @ts-ignore + EuiSideNav, +} from '@elastic/eui'; + +import { AppMountContext, AppMountParameters } from '../../../../../src/core/public'; +import { EsSearchTest } from './es_strategy'; +import { Page } from './page'; +import { DemoStrategy } from './demo_strategy'; +import { DocumentationPage } from './documentation'; +import { SearchApiPage } from './search_api'; + +const Home = () => ; + +interface PageDef { + title: string; + id: string; + component: React.ReactNode; +} + +type NavProps = RouteComponentProps & { + navigateToApp: AppMountContext['core']['application']['navigateToApp']; + pages: PageDef[]; +}; + +const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => { + const navItems = pages.map(page => ({ + id: page.id, + name: page.title, + onClick: () => history.push(`/${page.id}`), + 'data-test-subj': page.id, + })); + + return ( + + ); +}); + +const buildPage = (page: PageDef) => {page.component}; + +const SearchApp = ({ basename, context }: { basename: string; context: AppMountContext }) => { + const pages: PageDef[] = [ + { + id: 'home', + title: 'Home', + component: , + }, + { + title: 'Search API', + id: 'searchAPI', + component: , + }, + { + title: 'ES search strategy', + id: 'esSearch', + component: , + }, + { + title: 'Demo search strategy', + id: 'demoSearch', + component: , + }, + ]; + + const routes = pages.map((page, i) => ( + buildPage(page)} /> + )); + + return ( + + + +

- + >
- + >