From e23b547fb0fdddab6ed580165e958d5e45707173 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 20 Feb 2020 10:30:00 +0100 Subject: [PATCH 001/113] Use static time for tsvb rollup test (#57701) --- x-pack/test/functional/apps/rollup_job/tsvb.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index fb4a81406f9f1..acb73b170fbf2 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import datemath from '@elastic/datemath'; import expect from '@kbn/expect'; import mockRolledUpData from './hybrid_index_helper'; @@ -12,7 +11,6 @@ export default function({ getService, getPageObjects }) { const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); - const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'common', 'settings', @@ -21,18 +19,16 @@ export default function({ getService, getPageObjects }) { 'timePicker', ]); - // FLAKY: https://github.com/elastic/kibana/issues/56816 - describe.skip('tsvb integration', function() { + describe('tsvb integration', function() { //Since rollups can only be created once with the same name (even if you delete it), //we add the Date.now() to avoid name collision if you run the tests locally back to back. const rollupJobName = `tsvb-test-rollup-job-${Date.now()}`; const rollupSourceIndexName = 'rollup-source-data'; const rollupTargetIndexName = `rollup-target-data`; - const now = new Date(); const pastDates = [ - datemath.parse('now-1m', { forceNow: now }), - datemath.parse('now-2m', { forceNow: now }), - datemath.parse('now-3m', { forceNow: now }), + new Date('October 15, 2019 05:35:32'), + new Date('October 15, 2019 05:34:32'), + new Date('October 15, 2019 05:33:32'), ]; before(async () => { @@ -78,10 +74,12 @@ export default function({ getService, getPageObjects }) { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); - await PageObjects.timePicker.openQuickSelectTimeMenu(); - await testSubjects.click('superDatePickerCommonlyUsed_Last_24 hours'); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.timePicker.setAbsoluteRange( + 'Oct 15, 2019 @ 00:00:01.000', + 'Oct 15, 2019 @ 19:31:44.000' + ); await PageObjects.visualBuilder.clickPanelOptions('metric'); await PageObjects.visualBuilder.setIndexPatternValue(rollupTargetIndexName); await PageObjects.visualBuilder.setIntervalValue('1d'); From 0b644117b845984afeaea20ee5681dac358d5a0e Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 20 Feb 2020 13:40:34 +0300 Subject: [PATCH 002/113] [Visualize] Remove legacy appState in visualize (#57330) * Remove legacy appState in visualize * Read query and linked prop from scope * Fix persisted state instance * Fix functional tests * Bound url updates with an editor * Some improvements * Fix comments --- .../components/editor/controls_tab.test.tsx | 5 +- .../kibana/public/visualize/legacy_imports.ts | 3 - .../public/visualize/np_ready/application.ts | 8 - .../visualize/np_ready/editor/editor.html | 11 +- .../visualize/np_ready/editor/editor.js | 218 +++++++++--------- .../editor/lib/{index.js => index.ts} | 3 +- .../np_ready/editor/lib/make_stateful.ts | 58 +++++ ...rate_app_state.js => migrate_app_state.ts} | 47 ++-- .../editor/lib/visualize_app_state.ts | 112 +++++++++ .../np_ready/editor/visualization.js | 5 +- .../np_ready/editor/visualization_editor.js | 5 +- .../public/visualize/np_ready/types.d.ts | 31 ++- .../public/components/sidebar/sidebar.tsx | 17 +- .../public/components/vis_editor.js | 1 - .../public/embeddable/visualize_embeddable.ts | 8 +- .../public/legacy/build_pipeline.test.ts | 10 +- .../np_ready/public/legacy/build_pipeline.ts | 4 +- .../public/np_ready/public/vis.ts | 8 + .../public/np_ready/public/vis_impl.d.ts | 10 +- .../apps/visualize/_heatmap_chart.js | 4 +- 20 files changed, 400 insertions(+), 168 deletions(-) rename src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/{index.js => index.ts} (87%) create mode 100644 src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts rename src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/{migrate_app_state.js => migrate_app_state.ts} (61%) create mode 100644 src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index 3419d773bd09e..d7a62e07b26f3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -23,6 +23,7 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, @@ -32,7 +33,7 @@ let props: ControlsTabUiProps; beforeEach(() => { props = { deps: getDepsMock(), - vis: { + vis: ({ API: { indexPatterns: indexPatternsMock, }, @@ -46,7 +47,7 @@ beforeEach(() => { requiresSearch: false, hidden: false, }, - }, + } as unknown) as Vis, stateParams: { controls: [ { diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 92433799ba420..8b1bb0fda8c84 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,14 +24,11 @@ * directly where they are needed. */ -// @ts-ignore -export { AppState, AppStateProvider } from 'ui/state_management/app_state'; export { State } from 'ui/state_management/state'; // @ts-ignore export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { PersistedState } from 'ui/persisted_state'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index bd7b478f827a6..6a8d9ce106f9d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -22,8 +22,6 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { - AppStateProvider, - AppState, configureAppAngularModule, createTopNavDirective, createTopNavHelper, @@ -116,12 +114,6 @@ function createLocalStateModule() { 'app/visualize/Promise', 'app/visualize/PersistedState', ]) - .factory('AppState', function(Private: IPrivate) { - return Private(AppStateProvider); - }) - .service('getAppState', function(Private: IPrivate) { - return Private(AppStateProvider).getAppState; - }) .service('globalState', function(Private: IPrivate) { return Private(GlobalStateProvider); }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 6190b92c9be3e..4979d9dc89a0c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -2,7 +2,7 @@
@@ -42,9 +42,9 @@ show-filter-bar="showFilterBar() && isVisible" show-date-picker="showQueryBarTimePicker()" show-auto-refresh-only="!showQueryBarTimePicker()" - query="state.query" + query="query" saved-query="savedQuery" - screen-title="state.vis.title" + screen-title="vis.title" on-query-submit="updateQueryAndFetch" index-patterns="[indexPattern]" filters="filters" @@ -97,7 +97,9 @@ ui-state="uiState" time-range="timeRange" filters="filters" - query="query"/> + query="query" + app-state="appState" + />

diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 409d4b41fbe69..415949f88e9d1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { migrateAppState } from './lib'; +import { makeStateful, useVisualizeAppState } from './lib'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -45,7 +45,6 @@ import { absoluteToParsedUrl, KibanaParsedUrl, migrateLegacyQuery, - stateMonitorFactory, DashboardConstants, } from '../../legacy_imports'; @@ -68,15 +67,14 @@ function VisualizeAppController( $scope, $element, $route, - AppState, $window, $injector, $timeout, kbnUrl, redirectWhenMissing, Promise, - getAppState, - globalState + globalState, + config ) { const { indexPatterns, @@ -99,7 +97,6 @@ function VisualizeAppController( setActiveUrl, } = getServices(); - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; const _applyVis = () => { @@ -113,9 +110,9 @@ function VisualizeAppController( $scope.vis = vis; - const $appStatus = (this.appStatus = { + const $appStatus = { dirty: !savedVis.id, - }); + }; vis.on('dirtyStateChange', ({ isDirty }) => { vis.dirty = isDirty; @@ -265,53 +262,61 @@ function VisualizeAppController( }, ]; - let stateMonitor; - if (savedVis.id) { chrome.docTitle.change(savedVis.title); } + const defaultQuery = { + query: '', + language: + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + }; + // Extract visualization state with filtered aggs. You can see these filtered aggs in the URL. // Consists of things like aggs, params, listeners, title, type, etc. const savedVisState = vis.getState(); const stateDefaults = { uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}, - linked: !!savedVis.savedSearchId, - query: searchSource.getOwnField('query') || { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, + query: searchSource.getOwnField('query') || defaultQuery, filters: searchSource.getOwnField('filter') || [], vis: savedVisState, + linked: !!savedVis.savedSearchId, }; - // Instance of app_state.js. - const $state = (function initState() { - // This is used to sync visualization state with the url when `appState.save()` is called. - const appState = new AppState(stateDefaults); - - // Initializing appState does two things - first it translates the defaults into AppState, - // second it updates appState based on the url (the url trumps the defaults). This means if - // we update the state format at all and want to handle BWC, we must not only migrate the - // data stored with saved vis, but also any old state in the url. - migrateAppState(appState); - - // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the - // defaults applied. If the url was from a previous session which included modifications to the - // appState then they won't be equal. - if (!angular.equals(appState.vis, savedVisState)) { - Promise.try(function() { - vis.setState(appState.vis); - }).catch( - redirectWhenMissing({ - 'index-pattern-field': '/visualize', - }) - ); - } + const useHash = config.get('state:storeInSessionStorage'); + const { stateContainer, stopStateSync } = useVisualizeAppState({ + useHash, + stateDefaults, + }); + + const filterStateManager = new FilterStateManager( + globalState, + () => { + // Temporary AppState replacement + return { + set filters(_filters) { + stateContainer.transitions.set('filters', _filters); + }, + get filters() { + return stateContainer.getState().filters; + }, + }; + }, + filterManager + ); - return appState; - })(); + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the + // defaults applied. If the url was from a previous session which included modifications to the + // appState then they won't be equal. + if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { + try { + vis.setState(stateContainer.getState().vis); + } catch { + redirectWhenMissing({ + 'index-pattern-field': '/visualize', + }); + } + } $scope.filters = filterManager.getFilters(); @@ -330,8 +335,6 @@ function VisualizeAppController( ); function init() { - // export some objects - $scope.savedVis = savedVis; if (vis.indexPattern) { $scope.indexPattern = vis.indexPattern; } else { @@ -340,14 +343,28 @@ function VisualizeAppController( }); } + const initialState = stateContainer.getState(); + + $scope.appState = { + // mock implementation of the legacy appState.save() + // this could be even replaced by passing only "updateAppState" callback + save() { + stateContainer.transitions.updateVisState(vis.getState()); + }, + }; + + // Create a PersistedState instance for uiState. + const { persistedState, unsubscribePersisted, persistOnChange } = makeStateful( + 'uiState', + stateContainer + ); + $scope.uiState = persistedState; + $scope.savedVis = savedVis; + $scope.query = initialState.query; + $scope.linked = initialState.linked; $scope.searchSource = searchSource; - $scope.state = $state; $scope.refreshInterval = timefilter.getRefreshInterval(); - // Create a PersistedState instance. - $scope.uiState = $state.makeStateful('uiState'); - $scope.appStatus = $appStatus; - const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); @@ -372,22 +389,23 @@ function VisualizeAppController( $scope.timeRange = timefilter.getTime(); $scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode'); - stateMonitor = stateMonitorFactory.create($state, stateDefaults); - stateMonitor.ignoreProps(['vis.listeners']).onChange(status => { - $appStatus.dirty = status.dirty || !savedVis.id; - }); + const unsubscribeStateUpdates = stateContainer.subscribe(state => { + const newQuery = migrateLegacyQuery(state.query); + if (!_.isEqual(state.query, newQuery)) { + stateContainer.transitions.set('query', newQuery); + } + persistOnChange(state); - $scope.$watch('state.query', (newQuery, oldQuery) => { - if (!_.isEqual(newQuery, oldQuery)) { - const query = migrateLegacyQuery(newQuery); - if (!_.isEqual(query, newQuery)) { - $state.query = query; - } - $scope.fetch(); + // if the browser history was changed manually we need to reflect changes in the editor + if (!_.isEqual(vis.getState(), state.vis)) { + vis.setState(state.vis); + vis.forceReload(); + vis.emit('updateEditor'); } - }); - $state.replace(); + $appStatus.dirty = true; + $scope.fetch(); + }); const updateTimeRange = () => { $scope.timeRange = timefilter.getTime(); @@ -419,10 +437,11 @@ function VisualizeAppController( // update the searchSource when query updates $scope.fetch = function() { - $state.save(); - $scope.query = $state.query; - savedVis.searchSource.setField('query', $state.query); - savedVis.searchSource.setField('filter', $state.filters); + const { query, filters, linked } = stateContainer.getState(); + $scope.query = query; + $scope.linked = linked; + savedVis.searchSource.setField('query', query); + savedVis.searchSource.setField('filter', filters); $scope.$broadcast('render'); }; @@ -446,10 +465,13 @@ function VisualizeAppController( $scope._handler.destroy(); } savedVis.destroy(); - stateMonitor.destroy(); filterStateManager.destroy(); subscriptions.unsubscribe(); $scope.vis.off('apply', _applyVis); + + unsubscribePersisted(); + unsubscribeStateUpdates(); + stopStateSync(); }); $timeout(() => { @@ -459,10 +481,10 @@ function VisualizeAppController( $scope.updateQueryAndFetch = function({ query, dateRange }) { const isUpdate = - (query && !_.isEqual(query, $state.query)) || + (query && !_.isEqual(query, stateContainer.getState().query)) || (dateRange && !_.isEqual(dateRange, $scope.timeRange)); - $state.query = query; + stateContainer.transitions.set('query', query); timefilter.setTime(dateRange); // If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes @@ -488,20 +510,13 @@ function VisualizeAppController( $scope.onClearSavedQuery = () => { delete $scope.savedQuery; - delete $state.savedQuery; - $state.query = { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }; + stateContainer.transitions.removeSavedQuery(defaultQuery); filterManager.setFilters(filterManager.getGlobalFilters()); - $state.save(); $scope.fetch(); }; const updateStateFromSavedQuery = savedQuery => { - $state.query = savedQuery.attributes.query; - $state.save(); + stateContainer.transitions.set('query', savedQuery.attributes.query); const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -520,44 +535,38 @@ function VisualizeAppController( $scope.fetch(); }; + // update the query if savedQuery is stored + if (stateContainer.getState().savedQuery) { + savedQueryService.getSavedQuery(stateContainer.getState().savedQuery).then(savedQuery => { + $scope.$evalAsync(() => { + $scope.savedQuery = savedQuery; + }); + }); + } + $scope.$watch('savedQuery', newSavedQuery => { if (!newSavedQuery) return; - $state.savedQuery = newSavedQuery.id; - $state.save(); + stateContainer.transitions.set('savedQuery', newSavedQuery.id); updateStateFromSavedQuery(newSavedQuery); }); - $scope.$watch('state.savedQuery', newSavedQueryId => { - if (!newSavedQueryId) { - $scope.savedQuery = undefined; - return; - } - if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then(savedQuery => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - updateStateFromSavedQuery(savedQuery); - }); - }); - } - }); - /** * Called when the user clicks "Save" button. */ function doSave(saveOptions) { // vis.title was not bound and it's needed to reflect title into visState - $state.vis.title = savedVis.title; - $state.vis.type = savedVis.type || $state.vis.type; - savedVis.visState = $state.vis; + stateContainer.transitions.setVis({ + title: savedVis.title, + type: savedVis.type || stateContainer.getState().vis.type, + }); + savedVis.visState = stateContainer.getState().vis; savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); + $appStatus.dirty = false; return savedVis.save(saveOptions).then( function(id) { $scope.$evalAsync(() => { - stateMonitor.setInitialState($state.toJSON()); - if (id) { toastNotifications.addSuccess({ title: i18n.translate( @@ -601,8 +610,6 @@ function VisualizeAppController( chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs)); savedVis.vis.title = savedVis.title; savedVis.vis.description = savedVis.description; - // it's needed to save the state to update url string - $state.save(); } else { kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }); } @@ -632,9 +639,8 @@ function VisualizeAppController( } $scope.unlink = function() { - if (!$state.linked) return; + if (!$scope.linked) return; - $state.linked = false; const searchSourceParent = searchSource.getParent(); const searchSourceGrandparent = searchSourceParent.getParent(); @@ -645,8 +651,10 @@ function VisualizeAppController( _.union(searchSource.getOwnField('filter'), searchSourceParent.getOwnField('filter')) ); - $state.query = searchSourceParent.getField('query'); - $state.filters = searchSourceParent.getField('filter'); + stateContainer.transitions.unlinkSavedSearch( + searchSourceParent.getField('query'), + searchSourceParent.getField('filter') + ); searchSource.setField('index', searchSourceParent.getField('index')); searchSource.setParent(searchSourceGrandparent); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts similarity index 87% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts index 42284c9a03dcd..fa5b91b00edaf 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { migrateAppState } from './migrate_app_state'; +export { useVisualizeAppState } from './visualize_app_state'; +export { makeStateful } from './make_stateful'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts new file mode 100644 index 0000000000000..137d4de1fe9a8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.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 { PersistedState } from '../../../legacy_imports'; +import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; +import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +/** + * @returns Create a PersistedState instance, initialize state changes subscriber/unsubscriber + */ +export function makeStateful( + prop: keyof VisualizeAppState, + stateContainer: ReduxLikeStateContainer +) { + // set up the persistedState state + const persistedState = new PersistedState(); + + // update the appState when the stateful instance changes + const updateOnChange = function() { + stateContainer.transitions.set(prop, persistedState.getChanges()); + }; + + const handlerOnChange = (method: 'on' | 'off') => + persistedState[method]('change', updateOnChange); + + handlerOnChange('on'); + const unsubscribePersisted = () => handlerOnChange('off'); + + // update the stateful object when the app state changes + const persistOnChange = function(state: VisualizeAppState) { + if (state[prop]) { + persistedState.set(state[prop]); + } + }; + + const appState = stateContainer.getState(); + + // if the thing we're making stateful has an appState value, write to persisted state + if (appState[prop]) persistedState.setSilent(appState[prop]); + + return { persistedState, unsubscribePersisted, persistOnChange }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts similarity index 61% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts index 049ce048239db..7e09aece52e09 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts @@ -18,6 +18,7 @@ */ import { get, omit } from 'lodash'; +import { VisualizeAppState } from '../../types'; /** * Creates a new instance of AppState based on the table vis state. @@ -25,37 +26,41 @@ import { get, omit } from 'lodash'; * Dashboards have a similar implementation; see * core_plugins/kibana/public/dashboard/lib/migrate_app_state * - * @param appState {AppState} AppState class to instantiate + * @param appState {VisualizeAppState} */ -export function migrateAppState(appState) { +export function migrateAppState(appState: VisualizeAppState) { // For BWC in pre 7.0 versions where table visualizations could have multiple aggs // with `schema === 'split'`. This ensures that bookmarked URLs with deprecated params // are rewritten to the correct state. See core_plugins/table_vis/migrations. if (appState.vis.type !== 'table') { - return; + return appState; } - const visAggs = get(appState, 'vis.aggs', []); - let splitCount = 0; - const migratedAggs = visAggs.map(agg => { - if (agg.schema !== 'split') { + const visAggs: any = get(appState, 'vis.aggs'); + + if (visAggs) { + let splitCount = 0; + const migratedAggs = visAggs.map((agg: any) => { + if (agg.schema !== 'split') { + return agg; + } + + splitCount++; + if (splitCount === 1) { + return agg; // leave the first split agg unchanged + } + agg.schema = 'bucket'; + // the `row` param is exclusively used by split aggs, so we remove it + agg.params = omit(agg.params, ['row']); return agg; - } + }); - splitCount++; - if (splitCount === 1) { - return agg; // leave the first split agg unchanged + if (splitCount <= 1) { + return appState; // do nothing; we only want to touch tables with multiple split aggs } - agg.schema = 'bucket'; - // the `row` param is exclusively used by split aggs, so we remove it - agg.params = omit(agg.params, ['row']); - return agg; - }); - - if (splitCount <= 1) { - return; // do nothing; we only want to touch tables with multiple split aggs + + appState.vis.aggs = migratedAggs; } - appState.vis.aggs = migratedAggs; - appState.save(); + return appState; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts new file mode 100644 index 0000000000000..d8de81193d857 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts @@ -0,0 +1,112 @@ +/* + * 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 { createHashHistory } from 'history'; +import { isFunction, omit } from 'lodash'; + +import { migrateAppState } from './migrate_app_state'; +import { + createKbnUrlStateStorage, + createStateContainer, + syncState, +} from '../../../../../../../../plugins/kibana_utils/public'; +import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +const STATE_STORAGE_KEY = '_a'; + +interface Arguments { + useHash: boolean; + stateDefaults: VisualizeAppState; +} + +function toObject(state: PureVisState): PureVisState { + return omit(state, (value, key: string) => { + return key.charAt(0) === '$' || key.charAt(0) === '_' || isFunction(value); + }); +} + +export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) { + const history = createHashHistory(); + const kbnUrlStateStorage = createKbnUrlStateStorage({ + useHash, + history, + }); + const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); + const initialState = migrateAppState({ + ...stateDefaults, + ...urlState, + }); + + /* + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ + kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); + + const stateContainer = createStateContainer( + initialState, + { + set: state => (prop, value) => ({ ...state, [prop]: value }), + setVis: state => vis => ({ + ...state, + vis: { + ...state.vis, + ...vis, + }, + }), + removeSavedQuery: state => defaultQuery => { + const { savedQuery, ...rest } = state; + + return { + ...rest, + query: defaultQuery, + }; + }, + unlinkSavedSearch: state => (query, filters) => ({ + ...state, + query, + filters, + linked: false, + }), + updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }), + } + ); + + const { start: startStateSync, stop: stopStateSync } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: { + ...stateContainer, + set: state => { + if (state) { + // syncState utils requires to handle incoming "null" value + stateContainer.set(state); + } + }, + }, + stateStorage: kbnUrlStateStorage, + }); + + // start syncing the appState with the ('_a') url + startStateSync(); + + return { stateContainer, stopStateSync }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 502bd6e56fb1f..6acdb0abdd0b5 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -18,7 +18,7 @@ */ export function initVisualizationDirective(app, deps) { - app.directive('visualizationEmbedded', function($timeout, getAppState) { + app.directive('visualizationEmbedded', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisualizationDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { $scope.renderFunction = async () => { @@ -37,7 +38,7 @@ export function initVisualizationDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters || [], query: $scope.query, - appState: getAppState(), + appState: $scope.appState, uiState: $scope.uiState, }); $scope._handler.render(element[0]); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index 8032152f88173..c40a10115ae4e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -18,7 +18,7 @@ */ export function initVisEditorDirective(app, deps) { - app.directive('visualizationEditor', function($timeout, getAppState) { + app.directive('visualizationEditor', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisEditorDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { const Editor = $scope.savedObj.vis.type.editor; @@ -41,7 +42,7 @@ export function initVisEditorDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters, query: $scope.query, - appState: getAppState(), + appState: $scope.appState, }); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 524bc4b3196b7..139c247aa29cc 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -20,10 +20,37 @@ import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { LegacyCoreStart } from 'kibana/public'; -import { VisSavedObject, AppState, PersistedState } from '../legacy_imports'; +import { VisState, Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { VisSavedObject, PersistedState } from '../legacy_imports'; + +export type PureVisState = ReturnType; + +export interface VisualizeAppState { + filters: Filter[]; + uiState: PersistedState; + vis: PureVisState; + query: Query; + savedQuery?: string; + linked: boolean; +} + +export interface VisualizeAppStateTransitions { + set: ( + state: VisualizeAppState + ) => ( + prop: T, + value: VisualizeAppState[T] + ) => VisualizeAppState; + setVis: (state: VisualizeAppState) => (vis: Partial) => VisualizeAppState; + removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; + unlinkSavedSearch: ( + state: VisualizeAppState + ) => (query: Query, filters: Filter[]) => VisualizeAppState; + updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; +} export interface EditorRenderProps { - appState: AppState; + appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; embeddable: IEmbeddableStart; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index e33e83fd19fec..8615bcdd1bfbd 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -26,7 +26,7 @@ import { Vis } from 'src/legacy/core_plugins/visualizations/public'; import { PersistedState, AggGroupNames } from '../../legacy_imports'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; -import { setStateParamValue, useEditorReducer, useEditorFormState } from './state'; +import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; interface DefaultEditorSideBarProps { @@ -104,15 +104,26 @@ function DefaultEditorSideBar({ ); useEffect(() => { - vis.on('dirtyStateChange', ({ isDirty: dirty }: { isDirty: boolean }) => { + const changeHandler = ({ isDirty: dirty }: { isDirty: boolean }) => { setDirty(dirty); if (!dirty) { resetValidity(); } - }); + }; + vis.on('dirtyStateChange', changeHandler); + + return () => vis.off('dirtyStateChange', changeHandler); }, [resetValidity, vis]); + // subscribe on external vis changes using browser history, for example press back button + useEffect(() => { + const resetHandler = () => dispatch(discardChanges(vis)); + vis.on('updateEditor', resetHandler); + + return () => vis.off('updateEditor', resetHandler); + }, [dispatch, vis]); + const dataTabProps = { dispatch, formIsTouched: formState.touched, diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index b2dd1813e6d20..0263f5b2c851c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -82,7 +82,6 @@ export class VisEditor extends Component { // This check should be redundant, since this method should only be called when we're in editor // mode where there's also an appState passed into us. if (this.props.appState) { - this.props.appState.vis = this.props.vis.getState(); this.props.appState.save(); } }, VIS_STATE_DEBOUNCE_DELAY); diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 9e388832283fa..e7ca5ea803701 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -23,7 +23,6 @@ import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; @@ -68,7 +67,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: IIndexPattern[]; editUrl: string; editable: boolean; - appState?: AppState; + appState?: { save(): void }; uiState?: PersistedState; } @@ -79,7 +78,7 @@ export interface VisualizeInput extends EmbeddableInput { vis?: { colors?: { [key: string]: string }; }; - appState?: AppState; + appState?: { save(): void }; uiState?: PersistedState; } @@ -95,7 +94,7 @@ type ExpressionLoader = InstanceType { private handler?: ExpressionLoader; private savedVisualization: VisSavedObject; - private appState: AppState | undefined; + private appState: { save(): void } | undefined; private uiState: PersistedState; private timeRange?: TimeRange; private query?: Query; @@ -389,7 +388,6 @@ export class VisualizeEmbeddable extends Embeddable { if (this.appState) { - this.appState.vis = this.savedVisualization.vis.getState(); this.appState.save(); } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts index d1017de35474a..f73dc3e19d0ef 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts @@ -26,7 +26,7 @@ import { SchemaConfig, Schemas, } from './build_pipeline'; -import { Vis, VisState } from '..'; +import { Vis } from '..'; import { IAggConfig } from '../../../legacy_imports'; import { searchSourceMock } from '../../../legacy_mocks'; @@ -83,7 +83,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { }); describe('buildPipelineVisFunction', () => { - let visStateDef: VisState; + let visStateDef: ReturnType; let schemaConfig: SchemaConfig; let schemasDef: Schemas; let uiState: any; @@ -94,7 +94,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { // @ts-ignore type: 'type', params: {}, - }; + } as ReturnType; schemaConfig = { accessor: 0, @@ -349,7 +349,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { describe('buildPipeline', () => { it('calls toExpression on vis_type if it exists', async () => { - const vis: Vis = { + const vis = ({ getCurrentState: () => {}, getUiState: () => null, isHierarchical: () => false, @@ -360,7 +360,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { type: { toExpression: () => 'testing custom expressions', }, - }; + } as unknown) as Vis; const expression = await buildPipeline(vis, { searchSource: searchSourceMock }); expect(expression).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index 04a296a888e87..025eef834ca86 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -28,7 +28,7 @@ import { isDateHistogramBucketAggConfig, createFormat, } from '../../../legacy_imports'; -import { Vis, VisParams, VisState } from '..'; +import { Vis, VisParams } from '..'; interface SchemaConfigParams { precision?: number; @@ -59,7 +59,7 @@ export interface Schemas { } type buildVisFunction = ( - visState: VisState, + visState: ReturnType, schemas: Schemas, uiState: any, meta?: { savedObjectId?: string } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts index 59a8013523ef6..19375e25a9fb7 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts @@ -23,6 +23,14 @@ import { Status } from './legacy/update_status'; export interface Vis { type: VisType; + getCurrentState: ( + includeDisabled?: boolean + ) => { + title: string; + type: string; + params: VisParams; + aggs: Array<{ [key: string]: any }>; + }; // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts index 45d65efb5dcdf..f9b7db5c02d93 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Vis, VisState } from './vis'; +import { Vis, VisState, VisParams } from './vis'; import { VisType } from './types'; import { IIndexPattern } from '../../../../../../plugins/data/common'; @@ -35,6 +35,14 @@ export declare class VisImpl implements Vis { constructor(indexPattern: IIndexPattern, visState?: InitVisStateType); type: VisType; + getCurrentState: ( + includeDisabled?: boolean + ) => { + title: string; + type: string; + params: VisParams; + aggs: Array<{ [key: string]: any }>; + }; // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index 2cea861d0f64d..cbf7ab8df6831 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -96,6 +96,8 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickOptionsTab(); await PageObjects.visEditor.changeHeatmapColorNumbers(6); await PageObjects.visEditor.clickGo(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 267', @@ -121,9 +123,9 @@ export default function({ getService, getPageObjects }) { log.debug('customize 2 last ranges'); await PageObjects.visEditor.setCustomRangeByIndex(6, '650', '720'); await PageObjects.visEditor.setCustomRangeByIndex(7, '800', '905'); + await PageObjects.visEditor.clickGo(); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 100', From dd8eb62d9aad1e4e279fc338a242a8a39d0422b6 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 20 Feb 2020 12:05:08 +0100 Subject: [PATCH 003/113] Test NP logging config reload on SIGHUP (#57681) * TSify reload config test and simplify logic * get rid of mutable config in tests * increase timeouts * address comments --- .../reload_logging_config/kibana.test.yml | 3 + .../kibana_log_console.test.yml | 22 ++ .../kibana_log_file.test.yml | 22 ++ .../reload_logging_config.test.js | 224 --------------- .../reload_logging_config.test.ts | 263 ++++++++++++++++++ 5 files changed, 310 insertions(+), 224 deletions(-) create mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml create mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml delete mode 100644 src/cli/serve/integration_tests/reload_logging_config.test.js create mode 100644 src/cli/serve/integration_tests/reload_logging_config.test.ts diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml index 23f33940283c0..594c2efc8adc9 100644 --- a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml @@ -1,4 +1,5 @@ server: + autoListen: false port: 8274 logging: json: true @@ -6,3 +7,5 @@ optimize: enabled: false plugins: initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml new file mode 100644 index 0000000000000..33dd4787efad9 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - console + level: debug + appenders: + console: + kind: console + layout: + kind: json + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml new file mode 100644 index 0000000000000..f5148899ff854 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - file + level: debug + appenders: + file: + kind: file + layout: + kind: pattern + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js deleted file mode 100644 index 82d514877aff6..0000000000000 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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 { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import del from 'del'; - -import { safeDump } from 'js-yaml'; -import { - createMapStream, - createSplitStream, - createPromiseFromStreams, -} from '../../../legacy/utils/streams'; -import { getConfigFromFiles } from '../../../core/server/config/read_config'; - -const testConfigFile = follow('__fixtures__/reload_logging_config/kibana.test.yml'); -const kibanaPath = follow('../../../../scripts/kibana.js'); - -const second = 1000; -const minute = second * 60; - -const tempDir = path.join(os.tmpdir(), 'kbn-reload-test'); - -function follow(file) { - return path.relative(process.cwd(), path.resolve(__dirname, file)); -} - -function setLoggingJson(enabled) { - const conf = getConfigFromFiles([testConfigFile]); - conf.logging = conf.logging || {}; - conf.logging.json = enabled; - - const yaml = safeDump(conf); - - fs.writeFileSync(testConfigFile, yaml); -} - -describe('Server logging configuration', function() { - let child; - let isJson; - - beforeEach(() => { - isJson = true; - setLoggingJson(true); - - fs.mkdirSync(tempDir, { recursive: true }); - }); - - afterEach(() => { - isJson = true; - setLoggingJson(true); - - if (child !== undefined) { - child.kill(); - child = undefined; - } - - del.sync(tempDir, { force: true }); - }); - - const isWindows = /^win/.test(process.platform); - if (isWindows) { - it('SIGHUP is not a feature of Windows.', () => { - // nothing to do for Windows - }); - } else { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - expect.assertions(3); - - child = spawn( - process.execPath, - [kibanaPath, '--config', testConfigFile, '--oss', '--verbose'], - { - stdio: 'pipe', - } - ); - - let sawJson = false; - let sawNonjson = false; - - const [exitCode] = await Promise.all([ - Promise.race([ - new Promise(r => child.once('exit', r)).then(code => (code === null ? 0 : code)), - - new Promise(r => child.once('error', r)).then(err => { - throw new Error( - `error in child process while attempting to reload config. ${err.stack || - err.message || - err}` - ); - }), - ]), - - createPromiseFromStreams([ - child.stdout, - createSplitStream('\n'), - createMapStream(async line => { - if (!line) { - // skip empty lines - return; - } - - if (isJson) { - const data = JSON.parse(line); - sawJson = true; - - // We know the sighup handler will be registered before - // root.setup() is called - if (data.message.includes('setting up root')) { - isJson = false; - setLoggingJson(false); - - // Reload logging config. We give it a little bit of time to just make - // sure the process sighup handler is registered. - await new Promise(r => setTimeout(r, 100)); - child.kill('SIGHUP'); - } - } else if (line.startsWith('{')) { - // We have told Kibana to stop logging json, but it hasn't completed - // the switch yet, so we ignore before switching over. - } else { - // Kibana has successfully stopped logging json, so kill the server. - sawNonjson = true; - - child && child.kill(); - child = undefined; - } - }), - ]), - ]); - - expect(exitCode).toEqual(0); - expect(sawJson).toEqual(true); - expect(sawNonjson).toEqual(true); - }, - minute - ); - - it( - 'should recreate file handler on SIGHUP', - function(done) { - expect.hasAssertions(); - - const logPath = path.resolve(tempDir, 'kibana.log'); - const logPathArchived = path.resolve(tempDir, 'kibana_archive.log'); - - function watchFileUntil(path, matcher, timeout) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - fs.unwatchFile(path); - reject(`watchFileUntil timed out for "${matcher}"`); - }, timeout); - - fs.watchFile(path, () => { - try { - const contents = fs.readFileSync(path); - - if (matcher.test(contents)) { - clearTimeout(timeoutHandle); - fs.unwatchFile(path); - resolve(contents); - } - } catch (e) { - // noop - } - }); - }); - } - - child = spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - testConfigFile, - '--logging.dest', - logPath, - '--plugins.initialize', - 'false', - '--logging.json', - 'false', - '--verbose', - ]); - - watchFileUntil(logPath, /starting server/, 2 * minute) - .then(() => { - // once the server is running, archive the log file and issue SIGHUP - fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - }) - .then(() => - watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 10 * second) - ) - .then(contents => { - const lines = contents.toString().split('\n'); - // should be the first line of the new log file - expect(lines[0]).toMatch(/Reloaded logging configuration due to SIGHUP/); - child.kill(); - }) - .then(done, done); - }, - 3 * minute - ); - } -}); diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts new file mode 100644 index 0000000000000..2def3569828d3 --- /dev/null +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -0,0 +1,263 @@ +/* + * 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 Child from 'child_process'; +import Fs from 'fs'; +import Path from 'path'; +import Os from 'os'; +import Del from 'del'; + +import * as Rx from 'rxjs'; +import { map, filter, take } from 'rxjs/operators'; +import { safeDump } from 'js-yaml'; + +import { getConfigFromFiles } from '../../../core/server/config/read_config'; + +const legacyConfig = follow('__fixtures__/reload_logging_config/kibana.test.yml'); +const configFileLogConsole = follow( + '__fixtures__/reload_logging_config/kibana_log_console.test.yml' +); +const configFileLogFile = follow('__fixtures__/reload_logging_config/kibana_log_file.test.yml'); + +const kibanaPath = follow('../../../../scripts/kibana.js'); + +const second = 1000; +const minute = second * 60; + +const tempDir = Path.join(Os.tmpdir(), 'kbn-reload-test'); + +function follow(file: string) { + return Path.relative(process.cwd(), Path.resolve(__dirname, file)); +} + +function watchFileUntil(path: string, matcher: RegExp, timeout: number) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + Fs.unwatchFile(path); + reject(`watchFileUntil timed out for "${matcher}"`); + }, timeout); + + Fs.watchFile(path, () => { + try { + const contents = Fs.readFileSync(path, 'utf-8'); + + if (matcher.test(contents)) { + clearTimeout(timeoutHandle); + Fs.unwatchFile(path); + resolve(contents); + } + } catch (e) { + // noop + } + }); + }); +} + +function containsJsonOnly(content: string[]) { + return content.every(line => line.startsWith('{')); +} + +function createConfigManager(configPath: string) { + return { + modify(fn: (input: Record) => Record) { + const oldContent = getConfigFromFiles([configPath]); + const yaml = safeDump(fn(oldContent)); + Fs.writeFileSync(configPath, yaml); + }, + }; +} + +describe('Server logging configuration', function() { + let child: Child.ChildProcess; + beforeEach(() => { + Fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(async () => { + if (child !== undefined) { + child.kill(); + // wait for child to be killed otherwise jest complains that process not finished + await new Promise(res => setTimeout(res, 1000)); + } + Del.sync(tempDir, { force: true }); + }); + + const isWindows = /^win/.test(process.platform); + if (isWindows) { + it('SIGHUP is not a feature of Windows.', () => { + // nothing to do for Windows + }); + } else { + describe('legacy logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(legacyConfig, configFilePath); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + configFilePath, + '--verbose', + ]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.json = false; + return oldConfig; + }); + + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + minute + ); + + it( + 'should recreate file handle on SIGHUP', + async function() { + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + legacyConfig, + '--logging.dest', + logPath, + '--verbose', + ]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + + describe('platform logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogConsole, configFilePath); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.console.layout.kind = 'pattern'; + return oldConfig; + }); + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + 30 * second + ); + it( + 'should recreate file handle on SIGHUP', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogFile, configFilePath); + + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.file.path = logPath; + return oldConfig; + }); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + } +}); From d6aff77485ce28bc7d50a5f4bac513024c70ea57 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Thu, 20 Feb 2020 13:12:04 +0100 Subject: [PATCH 004/113] remove NODE_ENV=test (#58037) Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_coverage | 6 +----- test/scripts/jenkins_unit.sh | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index e40cc584dc376..fa1e141be93ea 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -13,11 +13,7 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a ]) { parallel([ 'kibana-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - } + kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() }, 'x-pack-intake-agent': { withEnv([ diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index fe67594ad8ac2..a9751003e8425 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -5,7 +5,6 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; else - echo "NODE_ENV=$NODE_ENV" echo " -> Running jest tests with coverage" node scripts/jest --ci --verbose --coverage echo "" From b07aa096060266e16366766d716fd3d5c7b1a30c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 20 Feb 2020 13:25:02 +0100 Subject: [PATCH 005/113] [Watcher] Refactor to use client from `RequestHandlerContext` (#57834) * Remove elasticsearch setup service from route deps Removed the callWithRequestFactory entirely. This setup was introducing a pattern where route handlers were not pulling the ES client fromt the route handler context. For this refactor we need to extend the route handler context with watcher specific client actions and so we also extend RequestHandlerContext globally. In this commit we also update the types for params, query and body schema on each route to avoid using any everwhere. * Add generic types to license wrapper Adding to the license wrapper made it a transparent wrapper from a type perspective so we can remove the need to eplicitly set RequestHandler on the handler. Also cleaned up a variable name "response" -> "searchResults" Also removed elasticsearch from the RouteDependencies type. Co-authored-by: Elastic Machine --- x-pack/plugins/watcher/server/index.ts | 2 + .../server/lib/call_with_request_factory.ts | 28 ---- .../fetch_all_from_scroll.ts | 29 ++-- .../license_pre_routing_factory.ts | 6 +- x-pack/plugins/watcher/server/plugin.ts | 31 +++- .../routes/api/indices/register_get_route.ts | 96 ++++++------ .../api/license/register_refresh_route.ts | 10 +- .../routes/api/register_list_fields_route.ts | 64 ++++---- .../routes/api/register_load_history_route.ts | 82 +++++----- .../api/settings/register_load_route.ts | 33 ++--- .../action/register_acknowledge_route.ts | 72 +++++---- .../api/watch/register_activate_route.ts | 75 +++++----- .../api/watch/register_deactivate_route.ts | 76 +++++----- .../routes/api/watch/register_delete_route.ts | 55 ++++--- .../api/watch/register_execute_route.ts | 88 +++++------ .../api/watch/register_history_route.ts | 103 ++++++------- .../routes/api/watch/register_load_route.ts | 82 +++++----- .../routes/api/watch/register_save_route.ts | 140 +++++++++--------- .../api/watch/register_visualize_route.ts | 68 +++++---- .../api/watches/register_delete_route.ts | 42 +++--- .../routes/api/watches/register_list_route.ts | 95 ++++++------ x-pack/plugins/watcher/server/types.ts | 4 +- 22 files changed, 633 insertions(+), 648 deletions(-) delete mode 100644 x-pack/plugins/watcher/server/lib/call_with_request_factory.ts diff --git a/x-pack/plugins/watcher/server/index.ts b/x-pack/plugins/watcher/server/index.ts index 51eb7bfa543fe..356be781fb194 100644 --- a/x-pack/plugins/watcher/server/index.ts +++ b/x-pack/plugins/watcher/server/index.ts @@ -6,4 +6,6 @@ import { PluginInitializerContext } from 'kibana/server'; import { WatcherServerPlugin } from './plugin'; +export { WatcherContext } from './plugin'; + export const plugin = (ctx: PluginInitializerContext) => new WatcherServerPlugin(ctx); diff --git a/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts b/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts deleted file mode 100644 index 4884c75436c24..0000000000000 --- a/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ElasticsearchServiceSetup } from 'kibana/server'; -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; - -const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { - const config = { plugins: [elasticsearchJsPlugin] }; - return elasticsearchService.createClient('watcher', config); -}); - -export const callWithRequestFactory = ( - elasticsearchService: ElasticsearchServiceSetup, - request: any -) => { - return (...args: any[]) => { - return ( - callWithRequest(elasticsearchService) - .asScoped(request) - // @ts-ignore - .callAsCurrentUser(...args) - ); - }; -}; diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts index de01bd5965504..8e8ca369dd02b 100644 --- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts +++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts @@ -4,24 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; import { ES_SCROLL_SETTINGS } from '../../../common/constants'; -export function fetchAllFromScroll(response: any, callWithRequest: any, hits: any[] = []) { - const newHits = get(response, 'hits.hits', []); - const scrollId = get(response, '_scroll_id'); +export function fetchAllFromScroll( + searchResuls: any, + dataClient: IScopedClusterClient, + hits: any[] = [] +): Promise { + const newHits = get(searchResuls, 'hits.hits', []); + const scrollId = get(searchResuls, '_scroll_id'); if (newHits.length > 0) { hits.push(...newHits); - return callWithRequest('scroll', { - body: { - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - scroll_id: scrollId, - }, - }).then((innerResponse: any) => { - return fetchAllFromScroll(innerResponse, callWithRequest, hits); - }); + return dataClient + .callAsCurrentUser('scroll', { + body: { + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + scroll_id: scrollId, + }, + }) + .then((innerResponse: any) => { + return fetchAllFromScroll(innerResponse, dataClient, hits); + }); } return Promise.resolve(hits); diff --git a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts index d010a23952725..1b2476fc78b45 100644 --- a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -12,13 +12,13 @@ import { } from 'kibana/server'; import { RouteDependencies } from '../../types'; -export const licensePreRoutingFactory = ( +export const licensePreRoutingFactory = ( { getLicenseStatus }: RouteDependencies, - handler: RequestHandler + handler: RequestHandler ) => { return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = getLicenseStatus(); diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 1f7b3823609ec..51d85c2001bd2 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -3,7 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + watcher?: WatcherContext; + } +} + +import { + CoreSetup, + IScopedClusterClient, + Logger, + Plugin, + PluginInitializerContext, +} from 'kibana/server'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; import { LICENSE_CHECK_STATE } from '../../licensing/server'; @@ -15,6 +28,11 @@ import { registerWatchesRoutes } from './routes/api/watches'; import { registerWatchRoutes } from './routes/api/watch'; import { registerListFieldsRoute } from './routes/api/register_list_fields_route'; import { registerLoadHistoryRoute } from './routes/api/register_load_history_route'; +import { elasticsearchJsPlugin } from './lib/elasticsearch_js_plugin'; + +export interface WatcherContext { + client: IScopedClusterClient; +} export class WatcherServerPlugin implements Plugin { log: Logger; @@ -31,15 +49,20 @@ export class WatcherServerPlugin implements Plugin { { http, elasticsearch: elasticsearchService }: CoreSetup, { licensing }: Dependencies ) { - const elasticsearch = await elasticsearchService.adminClient; const router = http.createRouter(); const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, router, getLicenseStatus: () => this.licenseStatus, }; + const config = { plugins: [elasticsearchJsPlugin] }; + const watcherESClient = elasticsearchService.createClient('watcher', config); + http.registerRouteHandlerContext('watcher', (ctx, request) => { + return { + client: watcherESClient.asScoped(request), + }; + }); + registerListFieldsRoute(routeDependencies); registerLoadHistoryRoute(routeDependencies); registerIndicesRoutes(routeDependencies); diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts index 30607b82e3295..df6f62135baeb 100644 --- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts @@ -5,13 +5,14 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { reduce, size } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +const bodySchema = schema.object({ pattern: schema.string() }, { allowUnknowns: true }); + function getIndexNamesFromAliasesResponse(json: Record) { return reduce( json, @@ -26,67 +27,66 @@ function getIndexNamesFromAliasesResponse(json: Record) { ); } -function getIndices(callWithRequest: any, pattern: string, limit = 10) { - return callWithRequest('indices.getAlias', { - index: pattern, - ignore: [404], - }).then((aliasResult: any) => { - if (aliasResult.status !== 404) { - const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); - return indicesFromAliasResponse.slice(0, limit); - } - - const params = { +function getIndices(dataClient: IScopedClusterClient, pattern: string, limit = 10) { + return dataClient + .callAsCurrentUser('indices.getAlias', { index: pattern, ignore: [404], - body: { - size: 0, // no hits - aggs: { - indices: { - terms: { - field: '_index', - size: limit, + }) + .then((aliasResult: any) => { + if (aliasResult.status !== 404) { + const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); + return indicesFromAliasResponse.slice(0, limit); + } + + const params = { + index: pattern, + ignore: [404], + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: limit, + }, }, }, }, - }, - }; + }; - return callWithRequest('search', params).then((response: any) => { - if (response.status === 404 || !response.aggregations) { - return []; - } - return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + return dataClient.callAsCurrentUser('search', params).then((response: any) => { + if (response.status === 404 || !response.aggregations) { + return []; + } + return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + }); }); - }); } export function registerGetRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { pattern } = request.body; - - try { - const indices = await getIndices(callWithRequest, pattern); - return response.ok({ body: { indices } }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/indices', validate: { - body: schema.object({}, { allowUnknowns: true }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { pattern } = request.body; + + try { + const indices = await getIndices(ctx.watcher!.client, pattern); + return response.ok({ body: { indices } }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts b/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts index a61fd16e8be4a..bd537cd6d21ab 100644 --- a/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; /* @@ -13,16 +12,15 @@ it needs to make a round-trip to the kibana server. This refresh endpoint is pro for when the client needs to check the license, but doesn't need to pull data from the server for any reason, i.e., when adding a new watch. */ -export function registerRefreshRoute(deps: RouteDependencies) { - const handler: RequestHandler = (ctx, request, response) => { - return response.ok({ body: { success: true } }); - }; +export function registerRefreshRoute(deps: RouteDependencies) { deps.router.get( { path: '/api/watcher/license/refresh', validate: false, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, (ctx, request, response) => { + return response.ok({ body: { success: true } }); + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts index 7c47379b87589..d72e5ad2f817d 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts @@ -5,15 +5,18 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../lib/is_es_error'; // @ts-ignore import { Fields } from '../../models/fields/index'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { RouteDependencies } from '../../types'; -function fetchFields(callWithRequest: any, indexes: string[]) { +const bodySchema = schema.object({ + indexes: schema.arrayOf(schema.string()), +}); + +function fetchFields(dataClient: IScopedClusterClient, indexes: string[]) { const params = { index: indexes, fields: ['*'], @@ -22,44 +25,39 @@ function fetchFields(callWithRequest: any, indexes: string[]) { ignore: 404, }; - return callWithRequest('fieldCaps', params); + return dataClient.callAsCurrentUser('fieldCaps', params); } export function registerListFieldsRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { indexes } = request.body; - - try { - const fieldsResponse = await fetchFields(callWithRequest, indexes); - const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; - const fields = Fields.fromUpstreamJson(json); - return response.ok({ body: fields.downstreamJson }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ - statusCode: e.statusCode, - body: { - message: e.message, - }, - }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/fields', validate: { - body: schema.object({ - indexes: schema.arrayOf(schema.string()), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { indexes } = request.body; + + try { + const fieldsResponse = await fetchFields(ctx.watcher!.client, indexes); + const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; + const fields = Fields.fromUpstreamJson(json); + return response.ok({ body: fields.downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts index 1be8477df79bc..8c9068123ce8d 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts @@ -6,8 +6,7 @@ import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../lib/is_es_error'; import { INDEX_NAMES } from '../../../common/constants'; import { RouteDependencies } from '../../types'; @@ -15,8 +14,12 @@ import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory' // @ts-ignore import { WatchHistoryItem } from '../../models/watch_history_item/index'; -function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { - return callWithRequest('search', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +function fetchHistoryItem(dataClient: IScopedClusterClient, watchHistoryItemId: string) { + return dataClient.callAsCurrentUser('search', { index: INDEX_NAMES.WATCHER_HISTORY, body: { query: { @@ -29,49 +32,44 @@ function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { } export function registerLoadHistoryRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const id = request.params.id; - - try { - const responseFromES = await fetchHistoryItem(callWithRequest, id); - const hit = get(responseFromES, 'hits.hits[0]'); - if (!hit) { - return response.notFound({ body: `Watch History Item with id = ${id} not found` }); - } - const watchHistoryItemJson = get(hit, '_source'); - const watchId = get(hit, '_source.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true, - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return response.ok({ - body: { watchHistoryItem: watchHistoryItem.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.get( { path: '/api/watcher/history/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const id = request.params.id; + + try { + const responseFromES = await fetchHistoryItem(ctx.watcher!.client, id); + const hit = get(responseFromES, 'hits.hits[0]'); + if (!hit) { + return response.notFound({ body: `Watch History Item with id = ${id} not found` }); + } + const watchHistoryItemJson = get(hit, '_source'); + const watchId = get(hit, '_source.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { watchHistoryItem: watchHistoryItem.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts index 6c70c2d0d07b6..fe9dd32735692 100644 --- a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IClusterClient, RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; // @ts-ignore import { Settings } from '../../../models/settings/index'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function fetchClusterSettings(client: IClusterClient) { +function fetchClusterSettings(client: IScopedClusterClient) { return client.callAsInternalUser('cluster.getSettings', { includeDefaults: true, filterPath: '**.xpack.notification', @@ -19,25 +19,24 @@ function fetchClusterSettings(client: IClusterClient) { } export function registerLoadRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - try { - const settings = await fetchClusterSettings(deps.elasticsearch); - return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; deps.router.get( { path: '/api/watcher/settings', validate: false, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const settings = await fetchClusterSettings(ctx.watcher!.client); + return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts index 08eec7456e3a5..9e024a63b82c5 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts @@ -6,60 +6,58 @@ import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../../lib/is_es_error'; // @ts-ignore import { WatchStatus } from '../../../../models/watch_status/index'; import { RouteDependencies } from '../../../../types'; import { licensePreRoutingFactory } from '../../../../lib/license_pre_routing_factory'; -function acknowledgeAction(callWithRequest: any, watchId: string, actionId: string) { - return callWithRequest('watcher.ackWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), + actionId: schema.string(), +}); + +function acknowledgeAction(dataClient: IScopedClusterClient, watchId: string, actionId: string) { + return dataClient.callAsCurrentUser('watcher.ackWatch', { id: watchId, action: actionId, }); } export function registerAcknowledgeRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { watchId, actionId } = request.params; - - try { - const hit = await acknowledgeAction(callWithRequest, watchId, actionId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { watchStatus: watchStatus.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', validate: { - params: schema.object({ - watchId: schema.string(), - actionId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId, actionId } = request.params; + + try { + const hit = await acknowledgeAction(ctx.watcher!.client, watchId, actionId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { watchStatus: watchStatus.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts index fdc20854ed8c2..1afeeb4e80efb 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts @@ -5,62 +5,59 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { WatchStatus } from '../../../models/watch_status/index'; -function activateWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.activateWatch', { +function activateWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.activateWatch', { id: watchId, }); } -export function registerActivateRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - const hit = await activateWatch(callWithRequest, watchId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { - watchStatus: watchStatus.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; +const paramsSchema = schema.object({ + watchId: schema.string(), +}); +export function registerActivateRoute(deps: RouteDependencies) { deps.router.put( { path: '/api/watcher/watch/{watchId}/activate', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + const hit = await activateWatch(ctx.watcher!.client, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts index 08d99f42df054..3171d8ee2e1e5 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts @@ -3,63 +3,61 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { WatchStatus } from '../../../models/watch_status/index'; -function deactivateWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.deactivateWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +function deactivateWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.deactivateWatch', { id: watchId, }); } export function registerDeactivateRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - const hit = await deactivateWatch(callWithRequest, watchId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { - watchStatus: watchStatus.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/{watchId}/deactivate', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + const hit = await deactivateWatch(ctx.watcher!.client, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts index 6e95cf959bc9c..bfdf328550bbe 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts @@ -5,49 +5,46 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function deleteWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.deleteWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +function deleteWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.deleteWatch', { id: watchId, }); } export function registerDeleteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - return response.ok({ - body: await deleteWatch(callWithRequest, watchId), - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.delete( { path: '/api/watcher/watch/{watchId}', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + return response.ok({ + body: await deleteWatch(ctx.watcher!.client, watchId), + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts index fef6d07317da5..7aaa77c05a5f0 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts @@ -5,9 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; @@ -19,60 +18,63 @@ import { Watch } from '../../../models/watch/index'; // @ts-ignore import { WatchHistoryItem } from '../../../models/watch_history_item/index'; -function executeWatch(callWithRequest: any, executeDetails: any, watchJson: any) { +const bodySchema = schema.object({ + executeDetails: schema.object({}, { allowUnknowns: true }), + watch: schema.object({}, { allowUnknowns: true }), +}); + +function executeWatch(dataClient: IScopedClusterClient, executeDetails: any, watchJson: any) { const body = executeDetails; body.watch = watchJson; - return callWithRequest('watcher.executeWatch', { + return dataClient.callAsCurrentUser('watcher.executeWatch', { body, }); } export function registerExecuteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); - const watch = Watch.fromDownstreamJson(request.body.watch); - - try { - const hit = await executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson); - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, 'watch_record'); - const watchId = get(hit, 'watch_record.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true, - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return response.ok({ - body: { - watchHistoryItem: watchHistoryItem.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/execute', validate: { - body: schema.object({ - executeDetails: schema.object({}, { allowUnknowns: true }), - watch: schema.object({}, { allowUnknowns: true }), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); + const watch = Watch.fromDownstreamJson(request.body.watch); + + try { + const hit = await executeWatch( + ctx.watcher!.client, + executeDetails.upstreamJson, + watch.watchJson + ); + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, 'watch_record'); + const watchId = get(hit, 'watch_record.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { + watchHistoryItem: watchHistoryItem.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts index 7f0f1ac8d66a3..b64c28e114b72 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts @@ -5,9 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { isEsError } from '../../../lib/is_es_error'; @@ -16,7 +15,15 @@ import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_facto // @ts-ignore import { WatchHistoryItem } from '../../../models/watch_history_item/index'; -function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +const querySchema = schema.object({ + startTime: schema.string(), +}); + +function fetchHistoryItems(dataClient: IScopedClusterClient, watchId: any, startTime: any) { const params: any = { index: INDEX_NAMES.WATCHER_HISTORY, scroll: ES_SCROLL_SETTINGS.KEEPALIVE, @@ -37,61 +44,57 @@ function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { params.body.query.bool.must.push(timeRangeQuery); } - return callWithRequest('search', params).then((response: any) => - fetchAllFromScroll(response, callWithRequest) - ); + return dataClient + .callAsCurrentUser('search', params) + .then((response: any) => fetchAllFromScroll(response, dataClient)); } export function registerHistoryRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { watchId } = request.params; - const { startTime } = request.query; - - try { - const hits = await fetchHistoryItems(callWithRequest, watchId, startTime); - const watchHistoryItems = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, '_source'); - - const opts = { includeDetails: false }; - return WatchHistoryItem.fromUpstreamJson( - { - id, - watchId, - watchHistoryItemJson, - }, - opts - ); - }); - - return response.ok({ - body: { - watchHistoryItems: watchHistoryItems.map( - (watchHistoryItem: any) => watchHistoryItem.downstreamJson - ), - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.get( { path: '/api/watcher/watch/{watchId}/history', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, + query: querySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + const { startTime } = request.query; + + try { + const hits = await fetchHistoryItems(ctx.watcher!.client, watchId, startTime); + const watchHistoryItems = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, '_source'); + + const opts = { includeDetails: false }; + return WatchHistoryItem.fromUpstreamJson( + { + id, + watchId, + watchHistoryItemJson, + }, + opts + ); + }); + + return response.ok({ + body: { + watchHistoryItems: watchHistoryItems.map( + (watchHistoryItem: any) => watchHistoryItem.downstreamJson + ), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts index 91d71cd737121..6363054921333 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts @@ -5,65 +5,63 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { Watch } from '../../../models/watch/index'; import { RouteDependencies } from '../../../types'; -function fetchWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.getWatch', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.getWatch', { id: watchId, }); } export function registerLoadRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const id = request.params.id; - - try { - const hit = await fetchWatch(callWithRequest, id); - const watchJson = get(hit, 'watch'); - const watchStatusJson = get(hit, 'status'); - const json = { - id, - watchJson, - watchStatusJson, - }; - - const watch = Watch.fromUpstreamJson(json, { - throwExceptions: { - Action: false, - }, - }); - return response.ok({ - body: { watch: watch.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; deps.router.get( { path: '/api/watcher/watch/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const id = request.params.id; + + try { + const hit = await fetchWatch(ctx.watcher!.client, id); + const watchJson = get(hit, 'watch'); + const watchStatusJson = get(hit, 'status'); + const json = { + id, + watchJson, + watchStatusJson, + }; + + const watch = Watch.fromUpstreamJson(json, { + throwExceptions: { + Action: false, + }, + }); + return response.ok({ + body: { watch: watch.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 7986424e6229a..572790f12a5f8 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -5,98 +5,104 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { WATCH_TYPES } from '../../../../common/constants'; import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function fetchWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.getWatch', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object( + { + type: schema.string(), + isNew: schema.boolean(), + }, + { allowUnknowns: true } +); + +function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.getWatch', { id: watchId, }); } -function saveWatch(callWithRequest: any, id: string, body: any) { - return callWithRequest('watcher.putWatch', { +function saveWatch(dataClient: IScopedClusterClient, id: string, body: any) { + return dataClient.callAsCurrentUser('watcher.putWatch', { id, body, }); } export function registerSaveRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { id } = request.params; - const { type, isNew, ...watchConfig } = request.body; + deps.router.put( + { + path: '/api/watcher/watch/{id}', + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { id } = request.params; + const { type, isNew, ...watchConfig } = request.body; - // For new watches, verify watch with the same ID doesn't already exist - if (isNew) { - try { - const existingWatch = await fetchWatch(callWithRequest, id); - if (existingWatch.found) { - return response.conflict({ - body: { - message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { - defaultMessage: "There is already a watch with ID '{watchId}'.", - values: { - watchId: id, - }, - }), - }, - }); - } - } catch (e) { - const es404 = isEsError(e) && e.statusCode === 404; - if (!es404) { - return response.internalError({ body: e }); + // For new watches, verify watch with the same ID doesn't already exist + if (isNew) { + try { + const existingWatch = await fetchWatch(ctx.watcher!.client, id); + if (existingWatch.found) { + return response.conflict({ + body: { + message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { + defaultMessage: "There is already a watch with ID '{watchId}'.", + values: { + watchId: id, + }, + }), + }, + }); + } + } catch (e) { + const es404 = isEsError(e) && e.statusCode === 404; + if (!es404) { + return response.internalError({ body: e }); + } + // Else continue... } - // Else continue... } - } - let serializedWatch; + let serializedWatch; - switch (type) { - case WATCH_TYPES.JSON: - const { name, watch } = watchConfig; - serializedWatch = serializeJsonWatch(name, watch); - break; + switch (type) { + case WATCH_TYPES.JSON: + const { name, watch } = watchConfig as any; + serializedWatch = serializeJsonWatch(name, watch); + break; - case WATCH_TYPES.THRESHOLD: - serializedWatch = serializeThresholdWatch(watchConfig); - break; - } - - try { - // Create new watch - return response.ok({ - body: await saveWatch(callWithRequest, id, serializedWatch), - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); + case WATCH_TYPES.THRESHOLD: + serializedWatch = serializeThresholdWatch(watchConfig); + break; } - // Case: default - return response.internalError({ body: e }); - } - }; + try { + // Create new watch + return response.ok({ + body: await saveWatch(ctx.watcher!.client, id, serializedWatch), + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } - deps.router.put( - { - path: '/api/watcher/watch/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({}, { allowUnknowns: true }), - }, - }, - licensePreRoutingFactory(deps, handler) + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index f2110bcc0ebdb..200b35953b6f2 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -5,8 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; @@ -16,7 +15,12 @@ import { Watch } from '../../../models/watch/index'; // @ts-ignore import { VisualizeOptions } from '../../../models/visualize_options/index'; -function fetchVisualizeData(callWithRequest: any, index: any, body: any) { +const bodySchema = schema.object({ + watch: schema.object({}, { allowUnknowns: true }), + options: schema.object({}, { allowUnknowns: true }), +}); + +function fetchVisualizeData(dataClient: IScopedClusterClient, index: any, body: any) { const params = { index, body, @@ -25,46 +29,40 @@ function fetchVisualizeData(callWithRequest: any, index: any, body: any) { ignore: [404], }; - return callWithRequest('search', params); + return dataClient.callAsCurrentUser('search', params); } export function registerVisualizeRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const watch = Watch.fromDownstreamJson(request.body.watch); - const options = VisualizeOptions.fromDownstreamJson(request.body.options); - const body = watch.getVisualizeQuery(options); - - try { - const hits = await fetchVisualizeData(callWithRequest, watch.index, body); - const visualizeData = watch.formatVisualizeData(hits); - - return response.ok({ - body: { - visualizeData, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/watch/visualize', validate: { - body: schema.object({ - watch: schema.object({}, { allowUnknowns: true }), - options: schema.object({}, { allowUnknowns: true }), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const watch = Watch.fromDownstreamJson(request.body.watch); + const options = VisualizeOptions.fromDownstreamJson(request.body.options); + const body = watch.getVisualizeQuery(options); + + try { + const hits = await fetchVisualizeData(ctx.watcher!.client, watch.index, body); + const visualizeData = watch.formatVisualizeData(hits); + + return response.ok({ + body: { + visualizeData, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts index 2ac824529f9a6..71e0a77bff972 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts @@ -5,16 +5,20 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function deleteWatches(callWithRequest: any, watchIds: string[]) { +const bodySchema = schema.object({ + watchIds: schema.arrayOf(schema.string()), +}); + +function deleteWatches(dataClient: IScopedClusterClient, watchIds: string[]) { const deletePromises = watchIds.map(watchId => { - return callWithRequest('watcher.deleteWatch', { - id: watchId, - }) + return dataClient + .callAsCurrentUser('watcher.deleteWatch', { + id: watchId, + }) .then((success: Array<{ _id: string }>) => ({ success })) .catch((error: Array<{ _id: string }>) => ({ error })); }); @@ -22,7 +26,7 @@ function deleteWatches(callWithRequest: any, watchIds: string[]) { return Promise.all(deletePromises).then(results => { const errors: Error[] = []; const successes: boolean[] = []; - results.forEach(({ success, error }) => { + results.forEach(({ success, error }: { success?: any; error?: any }) => { if (success) { successes.push(success._id); } else if (error) { @@ -38,26 +42,20 @@ function deleteWatches(callWithRequest: any, watchIds: string[]) { } export function registerDeleteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const results = await deleteWatches(callWithRequest, request.body.watchIds); - return response.ok({ body: { results } }); - } catch (e) { - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/watches/delete', validate: { - body: schema.object({ - watchIds: schema.arrayOf(schema.string()), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); + return response.ok({ body: { results } }); + } catch (e) { + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts index fcbdf688a2ab4..5e823a0a8d2de 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { isEsError } from '../../../lib/is_es_error'; @@ -15,7 +14,7 @@ import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_facto // @ts-ignore import { Watch } from '../../../models/watch/index'; -function fetchWatches(callWithRequest: any) { +function fetchWatches(dataClient: IScopedClusterClient) { const params = { index: INDEX_NAMES.WATCHES, scroll: ES_SCROLL_SETTINGS.KEEPALIVE, @@ -25,62 +24,58 @@ function fetchWatches(callWithRequest: any) { ignore: [404], }; - return callWithRequest('search', params).then((response: any) => - fetchAllFromScroll(response, callWithRequest) - ); + return dataClient + .callAsCurrentUser('search', params) + .then((response: any) => fetchAllFromScroll(response, dataClient)); } export function registerListRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const hits = await fetchWatches(callWithRequest); - const watches = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchJson = get(hit, '_source'); - const watchStatusJson = get(hit, '_source.status'); + deps.router.get( + { + path: '/api/watcher/watches', + validate: false, + }, + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const hits = await fetchWatches(ctx.watcher!.client); + const watches = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchJson = get(hit, '_source'); + const watchStatusJson = get(hit, '_source.status'); - return Watch.fromUpstreamJson( - { - id, - watchJson, - watchStatusJson, - }, - { - throwExceptions: { - Action: false, + return Watch.fromUpstreamJson( + { + id, + watchJson, + watchStatusJson, }, - } - ); - }); + { + throwExceptions: { + Action: false, + }, + } + ); + }); - return response.ok({ - body: { - watches: watches.map((watch: any) => watch.downstreamJson), - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ - statusCode: e.statusCode, + return response.ok({ body: { - message: e.message, + watches: watches.map((watch: any) => watch.downstreamJson), }, }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } - deps.router.get( - { - path: '/api/watcher/watches', - validate: false, - }, - licensePreRoutingFactory(deps, handler) + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index d9f2d3c3b1e7a..dd941054114a8 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'kibana/server'; +import { IRouter } from 'kibana/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; @@ -24,8 +24,6 @@ export interface ServerShim { export interface RouteDependencies { router: IRouter; getLicenseStatus: () => LicenseStatus; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; } export interface LicenseStatus { From 16eb81628f7a56af2e826cf97d8a7e2db77910fb Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 20 Feb 2020 13:31:04 +0100 Subject: [PATCH 006/113] [ML] Transform functional tests - bootstrap transform job for clone test (#57992) This PR adds transform bootstrapping to the functional test transform.api service and demonstrates the usage in a new cloning test file, which will be completed as part of the transform cloning PR. --- .../test/functional/apps/transform/cloning.ts | 65 +++++++++++++ .../test/functional/apps/transform/index.ts | 1 + .../functional/services/transform_ui/api.ts | 91 +++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 x-pack/test/functional/apps/transform/cloning.ts diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts new file mode 100644 index 0000000000000..f06dc0a14a383 --- /dev/null +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { TransformPivotConfig } from '../../../../legacy/plugins/transform/public/app/common'; + +function getTransformConfig(): TransformPivotConfig { + const date = Date.now(); + return { + id: `ec_2_${date}`, + source: { index: ['ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: + 'ecommerce batch transform with avg(products.base_price) grouped by terms(category.keyword)', + dest: { index: `user-ec_2_${date}` }, + }; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('cloning', function() { + this.tags(['smoke']); + const transformConfig = getTransformConfig(); + + before(async () => { + await esArchiver.load('ml/ecommerce'); + await transform.api.createAndRunTransform(transformConfig); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + await transform.api.deleteIndices(transformConfig.dest.index); + await transform.api.cleanTransformIndices(); + }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms group and avg agg', + expected: {}, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + // await transform.api.deleteIndices(); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 66a55105b3ca8..60b72f122f113 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -23,5 +23,6 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./creation_index_pattern')); loadTestFile(require.resolve('./creation_saved_search')); + loadTestFile(require.resolve('./cloning')); }); } diff --git a/x-pack/test/functional/services/transform_ui/api.ts b/x-pack/test/functional/services/transform_ui/api.ts index a6756e5940d72..6a4a1dfff6ea1 100644 --- a/x-pack/test/functional/services/transform_ui/api.ts +++ b/x-pack/test/functional/services/transform_ui/api.ts @@ -7,10 +7,17 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + TRANSFORM_STATE, + TransformPivotConfig, + TransformStats, +} from '../../../../legacy/plugins/transform/public/app/common'; + export function TransformAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); + const esSupertest = getService('esSupertest'); return { async deleteIndices(indices: string) { @@ -39,5 +46,89 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { async cleanTransformIndices() { await this.deleteIndices('.transform-*'); }, + + async getTransformStats(transformId: string): Promise { + log.debug(`Fetching transform stats for transform ${transformId}`); + const statsResponse = await esSupertest + .get(`/_transform/${transformId}/_stats`) + .expect(200) + .then((res: any) => res.body); + + expect(statsResponse.transforms).to.have.length(1); + return statsResponse.transforms[0]; + }, + + async getTransformState(transformId: string): Promise { + const stats = await this.getTransformStats(transformId); + const state: TRANSFORM_STATE = stats.state; + + return state; + }, + + async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) { + await retry.waitForWithTimeout( + `transform state to be ${expectedState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getTransformState(transformId); + if (state === expectedState) { + return true; + } else { + throw new Error(`expected transform state to be ${expectedState} but got ${state}`); + } + } + ); + }, + + async waitForBatchTransformToComplete(transformId: string) { + await retry.waitForWithTimeout(`batch transform to complete`, 2 * 60 * 1000, async () => { + const stats = await this.getTransformStats(transformId); + if (stats.state === TRANSFORM_STATE.STOPPED && stats.checkpointing.last.checkpoint === 1) { + return true; + } else { + throw new Error( + `expected batch transform to be stopped with last checkpoint = 1 (got status: '${stats.state}', checkpoint: '${stats.checkpointing.last.checkpoint}')` + ); + } + }); + }, + + async getTransform(transformId: string) { + return await esSupertest.get(`/_transform/${transformId}`).expect(200); + }, + + async createTransform(transformConfig: TransformPivotConfig) { + const transformId = transformConfig.id; + log.debug(`Creating transform with id '${transformId}'...`); + await esSupertest + .put(`/_transform/${transformId}`) + .send(transformConfig) + .expect(200); + + await retry.waitForWithTimeout(`'${transformId}' to be created`, 5 * 1000, async () => { + if (await this.getTransform(transformId)) { + return true; + } else { + throw new Error(`expected transform '${transformId}' to be created`); + } + }); + }, + + async startTransform(transformId: string) { + log.debug(`Starting transform '${transformId}' ...`); + await esSupertest.post(`/_transform/${transformId}/_start`).expect(200); + }, + + async createAndRunTransform(transformConfig: TransformPivotConfig) { + await this.createTransform(transformConfig); + await this.startTransform(transformConfig.id); + if (transformConfig.sync === undefined) { + // batch mode + await this.waitForBatchTransformToComplete(transformConfig.id); + } else { + // continuous mode + await this.waitForTransformState(transformConfig.id, TRANSFORM_STATE.STARTED); + } + }, }; } From ed2ca68d796fbb99d219336cd3dddc6ac8d71ba7 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 20 Feb 2020 15:40:00 +0300 Subject: [PATCH 007/113] [ui/agg_response/tabify] -> TypeScript & Jest & Shim (#57660) * [ui/agg_response/tabify] -> TypeScript & Jest & Shim [ui/agg_response/tabify] -> TypeScript & Jest & Shim Part of #57394 * fix CI * move tabify into new folder * TypeScript _bucket.js * rename _buckets -> bucket * fix CI * tabify.test.js -> tabify.test.ts * tabify.js -> tabify.ts * fix JEST * Update src/legacy/core_plugins/data/public/search/tabify/types.ts Co-Authored-By: Luke Elmers * fake_hierarchical_data.js -> fake_hierarchical_data.ts * TimeRange -> TabbedRangeFilterParams Co-authored-by: Elastic Machine Co-authored-by: Luke Elmers --- ...ical_data.js => fake_hierarchical_data.ts} | 16 +- src/legacy/core_plugins/data/public/index.ts | 2 + .../data/public/search/aggs/agg_configs.ts | 6 +- .../data/public/search/aggs/agg_params.ts | 4 +- .../data/public/search/aggs/index.ts | 3 + .../data/public/search/expressions/esaggs.ts | 3 +- .../core_plugins/data/public/search/index.ts | 1 + .../public/search/tabify/buckets.test.ts} | 112 +++++----- .../data/public/search/tabify/buckets.ts | 135 ++++++++++++ .../public/search/tabify/get_columns.test.ts | 191 +++++++++++++++++ .../data/public/search/tabify/get_columns.ts} | 4 +- .../data/public/search/tabify/index.ts} | 1 + .../search/tabify/response_writer.test.ts | 170 +++++++++++++++ .../public/search/tabify/response_writer.ts | 88 ++++++++ .../data/public/search/tabify/tabify.test.ts | 172 +++++++++++++++ .../data/public/search/tabify/tabify.ts | 173 +++++++++++++++ .../data/public/search/tabify/types.ts} | 18 +- .../data/public/search/utils/types.ts | 6 + .../kibana/public/discover/kibana_services.ts | 3 +- .../public/agg_table/__tests__/agg_table.js | 12 +- .../agg_table/__tests__/agg_table_group.js | 6 +- .../vis_type_table/public/legacy_imports.ts | 4 +- .../vis_type_vislib/public/legacy_imports.ts | 4 +- .../__tests__/visualizations/pie_chart.js | 6 +- src/legacy/ui/public/agg_response/index.js | 2 +- .../tabify/__tests__/_get_columns.js | 199 ------------------ .../tabify/__tests__/_integration.js | 175 --------------- .../tabify/__tests__/_response_writer.js | 186 ---------------- .../ui/public/agg_response/tabify/_buckets.js | 123 ----------- .../agg_response/tabify/_response_writer.js | 97 --------- .../ui/public/agg_response/tabify/tabify.js | 134 ------------ .../es_geo_grid_source/es_geo_grid_source.js | 2 +- 32 files changed, 1053 insertions(+), 1005 deletions(-) rename src/fixtures/{fake_hierarchical_data.js => fake_hierarchical_data.ts} (98%) rename src/legacy/{ui/public/agg_response/tabify/__tests__/_buckets.js => core_plugins/data/public/search/tabify/buckets.test.ts} (66%) create mode 100644 src/legacy/core_plugins/data/public/search/tabify/buckets.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts rename src/legacy/{ui/public/agg_response/tabify/_get_columns.ts => core_plugins/data/public/search/tabify/get_columns.ts} (96%) rename src/legacy/{ui/public/agg_response/tabify/index.js => core_plugins/data/public/search/tabify/index.ts} (94%) create mode 100644 src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/response_writer.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/tabify.ts rename src/legacy/{ui/public/agg_response/tabify/__tests__/tabify.js => core_plugins/data/public/search/tabify/types.ts} (69%) delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/_buckets.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/_response_writer.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/tabify.js diff --git a/src/fixtures/fake_hierarchical_data.js b/src/fixtures/fake_hierarchical_data.ts similarity index 98% rename from src/fixtures/fake_hierarchical_data.js rename to src/fixtures/fake_hierarchical_data.ts index b4ae02a487049..4480caae39664 100644 --- a/src/fixtures/fake_hierarchical_data.js +++ b/src/fixtures/fake_hierarchical_data.ts @@ -17,16 +17,14 @@ * under the License. */ -const data = {}; - -data.metricOnly = { +export const metricOnly = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_1: { value: 412032 }, }, }; -data.threeTermBuckets = { +export const threeTermBuckets = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_2: { @@ -129,7 +127,7 @@ data.threeTermBuckets = { }, }; -data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { +export const oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_3: { @@ -520,7 +518,7 @@ data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { }, }; -data.oneRangeBucket = { +export const oneRangeBucket = { took: 35, timed_out: false, _shards: { @@ -555,7 +553,7 @@ data.oneRangeBucket = { }, }; -data.oneFilterBucket = { +export const oneFilterBucket = { took: 11, timed_out: false, _shards: { @@ -582,7 +580,7 @@ data.oneFilterBucket = { }, }; -data.oneHistogramBucket = { +export const oneHistogramBucket = { took: 37, timed_out: false, _shards: { @@ -632,5 +630,3 @@ data.oneHistogramBucket = { }, }, }; - -export default data; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 50120292a627a..ce46f534141f4 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -81,4 +81,6 @@ export { // search_source getRequestInspectorStats, getResponseInspectorStats, + tabifyAggResponse, + tabifyGetColumns, } from './search'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index 7e7e4944b00da..8e091ed5f21ae 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -27,7 +27,7 @@ */ import _ from 'lodash'; -import { AggConfig, AggConfigOptions } from './agg_config'; +import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { @@ -63,7 +63,7 @@ export class AggConfigs { public schemas: any; public timeRange?: TimeRange; - aggs: AggConfig[]; + aggs: IAggConfig[]; constructor(indexPattern: IndexPattern, configStates = [] as any, schemas?: any) { configStates = AggConfig.ensureIds(configStates); @@ -74,7 +74,7 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); - if (this.schemas) { + if (schemas) { this.initializeDefaultsFromSchemas(schemas); } } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts index 34727ff4614b9..551cb81529a0a 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts @@ -76,7 +76,9 @@ export const writeParams = < aggs?: IAggConfigs, locals?: Record ) => { - const output = { params: {} as Record }; + const output: Record = { + params: {} as Record, + }; locals = locals || {}; params.forEach(param => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index 0fef7f38aae74..0bdb92b8de65e 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -50,3 +50,6 @@ export { isValidJson, isValidInterval } from './utils'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; + +// types +export { IAggConfig, IAggConfigs } from './types'; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 9aee7124c9521..302527e4ed549 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -39,8 +39,7 @@ import { import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; -// @ts-ignore -import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; +import { tabifyAggResponse } from '../../../../../core_plugins/data/public'; import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index 90e191b769a8d..96d2825559da2 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -20,3 +20,4 @@ export * from './aggs'; export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; export { serializeAggConfig } from './expressions/utils'; +export { tabifyAggResponse, tabifyGetColumns } from './tabify'; diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts similarity index 66% rename from src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js rename to src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts index b85b45d3c5820..ef2748102623a 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts @@ -17,31 +17,36 @@ * under the License. */ -import expect from '@kbn/expect'; -import { TabifyBuckets } from '../_buckets'; +import { TabifyBuckets } from './buckets'; +import { AggGroupNames } from '../aggs'; -describe('Buckets wrapper', function() { - function test(aggResp, count, keys) { - it('reads the length', function() { +jest.mock('ui/new_platform'); + +describe('Buckets wrapper', () => { + const check = (aggResp: any, count: number, keys: string[]) => { + test('reads the length', () => { const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(count); + expect(buckets).toHaveLength(count); }); - it('iterates properly, passing in the key', function() { + test('iterates properly, passing in the key', () => { const buckets = new TabifyBuckets(aggResp); - const keysSent = []; - buckets.forEach(function(bucket, key) { - keysSent.push(key); + const keysSent: any[] = []; + + buckets.forEach((bucket, key) => { + if (key) { + keysSent.push(key); + } }); - expect(keysSent).to.have.length(count); - expect(keysSent).to.eql(keys); + expect(keysSent).toHaveLength(count); + expect(keysSent).toEqual(keys); }); - } + }; - describe('with object style buckets', function() { - const aggResp = { - buckets: { + describe('with object style buckets', () => { + let aggResp: any = { + [AggGroupNames.Buckets]: { '0-100': {}, '100-200': {}, '200-300': {}, @@ -51,11 +56,11 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); - it('should accept filters agg queries with strings', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with strings', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -75,15 +80,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query_string queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query_string queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -103,15 +110,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query dsl queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query dsl queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { '{match_all: {}}': {}, }, }; @@ -126,16 +135,18 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); }); - describe('with array style buckets', function() { + describe('with array style buckets', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: '0-100', value: {} }, { key: '100-200', value: {} }, { key: '200-300', value: {} }, @@ -145,23 +156,24 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); }); - describe('with single bucket aggregations (filter)', function() { - it('creates single bucket from agg content', function() { + describe('with single bucket aggregations (filter)', () => { + test('creates single bucket from agg content', () => { const aggResp = { single_bucket: {}, doc_count: 5, }; const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); }); - describe('drop_partial option', function() { + describe('drop_partial option', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: 0, value: {} }, { key: 100, value: {} }, { key: 200, value: {} }, @@ -169,7 +181,7 @@ describe('Buckets wrapper', function() { ], }; - it('drops partial buckets when enabled', function() { + test('drops partial buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -182,10 +194,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); - it('keeps partial buckets when disabled', function() { + test('keeps partial buckets when disabled', () => { const aggParams = { drop_partials: false, field: { @@ -198,10 +211,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); - it('keeps aligned buckets when enabled', function() { + test('keeps aligned buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -214,10 +228,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(3); + + expect(buckets).toHaveLength(3); }); - it('does not drop buckets for non-timerange fields', function() { + test('does not drop buckets for non-timerange fields', () => { const aggParams = { drop_partials: true, field: { @@ -230,7 +245,8 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/tabify/buckets.ts b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts new file mode 100644 index 0000000000000..8078136299f8c --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts @@ -0,0 +1,135 @@ +/* + * 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 { get, isPlainObject, keys, findKey } from 'lodash'; +import moment from 'moment'; +import { IAggConfig } from '../aggs'; +import { TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; + +type AggParams = IAggConfig['params'] & { + drop_partials: boolean; + ranges: TabbedRangeFilterParams[]; +}; + +const isRangeEqual = (range1: TabbedRangeFilterParams, range2: TabbedRangeFilterParams) => + range1?.from === range2?.from && range1?.to === range2?.to; + +export class TabifyBuckets { + length: number; + objectMode: boolean; + buckets: any; + _keys: any[] = []; + + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + if (aggResp && aggResp.buckets) { + this.buckets = aggResp.buckets; + } else if (aggResp) { + // Some Bucket Aggs only return a single bucket (like filter). + // In those instances, the aggResp is the content of the single bucket. + this.buckets = [aggResp]; + } else { + this.buckets = []; + } + + this.objectMode = isPlainObject(this.buckets); + + if (this.objectMode) { + this._keys = keys(this.buckets); + this.length = this._keys.length; + } else { + this.length = this.buckets.length; + } + + if (this.length && aggParams) { + this.orderBucketsAccordingToParams(aggParams); + if (aggParams.drop_partials) { + this.dropPartials(aggParams, timeRange); + } + } + } + + forEach(fn: (bucket: any, key: any) => void) { + const buckets = this.buckets; + + if (this.objectMode) { + this._keys.forEach(key => { + fn(buckets[key], key); + }); + } else { + buckets.forEach((bucket: AggResponseBucket) => { + fn(bucket, bucket.key); + }); + } + } + + private orderBucketsAccordingToParams(params: AggParams) { + if (params.filters && this.objectMode) { + this._keys = params.filters.map((filter: any) => { + const query = get(filter, 'input.query.query_string.query', filter.input.query); + const queryString = typeof query === 'string' ? query : JSON.stringify(query); + + return filter.label || queryString || '*'; + }); + } else if (params.ranges && this.objectMode) { + this._keys = params.ranges.map((range: TabbedRangeFilterParams) => + findKey(this.buckets, (el: TabbedRangeFilterParams) => isRangeEqual(el, range)) + ); + } else if (params.ranges && params.field.type !== 'date') { + let ranges = params.ranges; + if (params.ipRangeType) { + ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; + } + this.buckets = ranges.map((range: any) => { + if (range.mask) { + return this.buckets.find((el: AggResponseBucket) => el.key === range.mask); + } + + return this.buckets.find((el: TabbedRangeFilterParams) => isRangeEqual(el, range)); + }); + } + } + + // dropPartials should only be called if the aggParam setting is enabled, + // and the agg field is the same as the Time Range. + private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + if ( + !timeRange || + this.buckets.length <= 1 || + this.objectMode || + params.field.name !== timeRange.name + ) { + return; + } + + const interval = this.buckets[1].key - this.buckets[0].key; + + this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { + if (moment(bucket.key).isBefore(timeRange.gte)) { + return false; + } + if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + return false; + } + return true; + }); + + this.length = this.buckets.length; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts new file mode 100644 index 0000000000000..0328e87d8b832 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -0,0 +1,191 @@ +/* + * 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 { tabifyGetColumns, AggColumn } from './get_columns'; +import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; + +jest.mock('ui/new_platform'); + +describe('get columns', () => { + const createAggConfigs = (aggs: any[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + test('should inject a count metric if no aggs exist', () => { + const columns = tabifyGetColumns(createAggConfigs().aggs, true); + + expect(columns).toHaveLength(1); + expect(columns[0]).toHaveProperty('aggConfig'); + expect(columns[0].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject a count metric if only buckets exist', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + true + ); + + expect(columns).toHaveLength(2); + expect(columns[1]).toHaveProperty('aggConfig'); + expect(columns[1].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject the metric after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns).toHaveLength(8); + + columns.forEach((column, i) => { + expect(column).toHaveProperty('aggConfig'); + expect(column.aggConfig.type).toHaveProperty('name', i % 2 ? 'count' : 'date_histogram'); + }); + }); + + test('should inject the multiple metrics after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + function checkColumns(column: AggColumn, i: number) { + expect(column).toHaveProperty('aggConfig'); + + switch (i) { + case 0: + expect(column.aggConfig.type).toHaveProperty('name', 'date_histogram'); + break; + case 1: + expect(column.aggConfig.type).toHaveProperty('name', 'avg'); + break; + case 2: + expect(column.aggConfig.type).toHaveProperty('name', 'sum'); + break; + } + } + + expect(columns).toHaveLength(12); + + for (let i = 0; i < columns.length; i += 3) { + columns.slice(i, i + 3).forEach(checkColumns); + } + }); + + test('should put all metrics at the end of the columns if the vis is not hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '20s' }, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns.map(c => c.name)).toEqual([ + '@timestamp per 20 seconds', + 'Sum of @timestamp', + '@timestamp per 10 seconds', + 'Sum of @timestamp', + ]); + }); +}); diff --git a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts similarity index 96% rename from src/legacy/ui/public/agg_response/tabify/_get_columns.ts rename to src/legacy/core_plugins/data/public/search/tabify/get_columns.ts index 4144d5be16012..54f09f6c6364f 100644 --- a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts @@ -18,7 +18,7 @@ */ import { groupBy } from 'lodash'; -import { IAggConfig } from '../../agg_types'; +import { IAggConfig } from '../aggs'; export interface AggColumn { aggConfig: IAggConfig; @@ -40,7 +40,7 @@ const getColumn = (agg: IAggConfig, i: number): AggColumn => { * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates * @param {boolean} minimalColumns - setting to true will only return a column for the last bucket/metric instead of one for each level */ -export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean) { +export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean): AggColumn[] { // pick the columns if (minimalColumns) { return aggs.map((agg, i) => getColumn(agg, i)); diff --git a/src/legacy/ui/public/agg_response/tabify/index.js b/src/legacy/core_plugins/data/public/search/tabify/index.ts similarity index 94% rename from src/legacy/ui/public/agg_response/tabify/index.js rename to src/legacy/core_plugins/data/public/search/tabify/index.ts index f14ca647e4b32..be8d64510033c 100644 --- a/src/legacy/ui/public/agg_response/tabify/index.js +++ b/src/legacy/core_plugins/data/public/search/tabify/index.ts @@ -18,3 +18,4 @@ */ export { tabifyAggResponse } from './tabify'; +export { tabifyGetColumns } from './get_columns'; diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts new file mode 100644 index 0000000000000..f5df0a683ca00 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -0,0 +1,170 @@ +/* + * 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 { TabbedAggResponseWriter } from './response_writer'; +import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; + +import { TabbedResponseWriterOptions } from './types'; + +jest.mock('ui/new_platform'); + +describe('TabbedAggResponseWriter class', () => { + let responseWriter: TabbedAggResponseWriter; + + const splitAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + ]; + + const twoSplitsAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'machine.os.raw', + }, + }, + ]; + + const createResponseWritter = (aggs: any[] = [], opts?: Partial) => { + const field = { + name: 'geo.src', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new TabbedAggResponseWriter( + new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ), + { + metricsAtAllLevels: false, + partialRows: false, + ...opts, + } + ); + }; + + describe('Constructor', () => { + beforeEach(() => { + responseWriter = createResponseWritter(twoSplitsAggConfig); + }); + + test('generates columns', () => { + expect(responseWriter.columns.length).toEqual(3); + }); + + test('correctly generates columns with metricsAtAllLevels set to true', () => { + const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { + metricsAtAllLevels: true, + }); + + expect(minimalColumnsResponseWriter.columns.length).toEqual(4); + }); + + describe('row()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('adds the row to the array', () => { + responseWriter.bucketBuffer = [{ id: 'col-0', value: 'US' }]; + responseWriter.metricBuffer = [{ id: 'col-1', value: 5 }]; + + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(1); + expect(responseWriter.rows[0]).toEqual({ 'col-0': 'US', 'col-1': 5 }); + }); + + test("doesn't add an empty row", () => { + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(0); + }); + }); + + describe('response()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('produces correct response', () => { + responseWriter.bucketBuffer = [ + { id: 'col-0-1', value: 'US' }, + { id: 'col-1-2', value: 5 }, + ]; + responseWriter.row(); + + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows).toEqual([{ 'col-0-1': 'US', 'col-1-2': 5 }]); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + + test('produces correct response for no data', () => { + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows.length).toBe(0); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts new file mode 100644 index 0000000000000..4c4578e505b71 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts @@ -0,0 +1,88 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { IAggConfigs } from '../aggs/agg_configs'; +import { AggColumn, tabifyGetColumns } from './get_columns'; + +import { TabbedResponseWriterOptions } from './types'; + +interface TabbedAggColumn { + id: string; + value: string | number; +} + +type TabbedAggRow = Record; + +/** + * Writer class that collects information about an aggregation response and + * produces a table, or a series of tables. + */ +export class TabbedAggResponseWriter { + columns: AggColumn[]; + rows: TabbedAggRow[] = []; + bucketBuffer: TabbedAggColumn[] = []; + metricBuffer: TabbedAggColumn[] = []; + + private readonly partialRows: boolean; + + /** + * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates + * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket + * @param {boolean} partialRows - setting to true will not remove rows with missing values + */ + constructor( + aggs: IAggConfigs, + { metricsAtAllLevels = false, partialRows = false }: Partial + ) { + this.partialRows = partialRows; + + this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); + this.rows = []; + } + + /** + * Create a new row by reading the row buffer and bucketBuffer + */ + row() { + const rowBuffer: TabbedAggRow = {}; + + this.bucketBuffer.forEach(bucket => { + rowBuffer[bucket.id] = bucket.value; + }); + + this.metricBuffer.forEach(metric => { + rowBuffer[metric.id] = metric.value; + }); + + const isPartialRow = + this.partialRows && !this.columns.every(column => rowBuffer.hasOwnProperty(column.id)); + + if (!isEmpty(rowBuffer) && !isPartialRow) { + this.rows.push(rowBuffer); + } + } + + response() { + return { + columns: this.columns, + rows: this.rows, + }; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts new file mode 100644 index 0000000000000..13fe7719b0a85 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -0,0 +1,172 @@ +/* + * 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 { IndexPattern } from '../../../../../../plugins/data/public'; +import { tabifyAggResponse } from './tabify'; +import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; + +jest.mock('ui/new_platform'); + +describe('tabifyAggResponse Integration', () => { + const createAggConfigs = (aggs: IAggConfig[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = ({ + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as unknown) as IndexPattern; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; + + test('transforms a simple response properly', () => { + const aggConfigs = createAggConfigs(); + + const resp = tabifyAggResponse(aggConfigs, metricOnly, { + metricsAtAllLevels: true, + }); + + expect(resp).toHaveProperty('rows'); + expect(resp).toHaveProperty('columns'); + + expect(resp.rows).toHaveLength(1); + expect(resp.columns).toHaveLength(1); + + expect(resp.rows[0]).toEqual({ 'col-0-1': 1000 }); + expect(resp.columns[0]).toHaveProperty('aggConfig', aggConfigs.aggs[0]); + }); + + describe('transforms a complex response', () => { + let esResp: typeof threeTermBuckets; + let aggConfigs: IAggConfigs; + let avg: IAggConfig; + let ext: IAggConfig; + let src: IAggConfig; + let os: IAggConfig; + + beforeEach(() => { + aggConfigs = createAggConfigs([ + mockAggConfig({ type: 'avg', schema: 'metric', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'split', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + ]); + + [avg, ext, src, os] = aggConfigs.aggs; + + esResp = threeTermBuckets; + esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; + }); + + // check that the columns of a table are formed properly + function expectColumns(table: ReturnType, aggs: IAggConfig[]) { + expect(table.columns).toHaveLength(aggs.length); + + aggs.forEach((agg, i) => { + expect(table.columns[i]).toHaveProperty('aggConfig', agg); + }); + } + + // check that a row has expected values + function expectRow( + row: Record, + asserts: Array<(val: string | number) => void> + ) { + expect(typeof row).toBe('object'); + + asserts.forEach((assert, i: number) => { + if (row[`col-${i}`]) { + assert(row[`col-${i}`]); + } + }); + } + + // check for two character country code + function expectCountry(val: string | number) { + expect(typeof val).toBe('string'); + expect(val).toHaveLength(2); + } + + // check for an OS term + function expectExtension(val: string | number) { + expect(val).toMatch(/^(js|png|html|css|jpg)$/); + } + + // check for an OS term + function expectOS(val: string | number) { + expect(val).toMatch(/^(win|mac|linux)$/); + } + + // check for something like an average bytes result + function expectAvgBytes(val: string | number) { + expect(typeof val).toBe('number'); + expect(val === 0 || val > 1000).toBeDefined(); + } + + test('for non-hierarchical vis', () => { + // the default for a non-hierarchical vis is to display + // only complete rows, and only put the metrics at the end. + + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: false }); + + expectColumns(tabbed, [ext, src, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); + }); + }); + + test('for hierarchical vis', () => { + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: true }); + + expectColumns(tabbed, [ext, avg, src, avg, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [ + expectExtension, + expectAvgBytes, + expectCountry, + expectAvgBytes, + expectOS, + expectAvgBytes, + ]); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts new file mode 100644 index 0000000000000..078d3f7f72759 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts @@ -0,0 +1,173 @@ +/* + * 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 { get } from 'lodash'; +import { TabbedAggResponseWriter } from './response_writer'; +import { TabifyBuckets } from './buckets'; +import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; +import { IAggConfigs, AggGroupNames } from '../aggs'; + +/** + * Sets up the ResponseWriter and kicks off bucket collection. + */ +export function tabifyAggResponse( + aggConfigs: IAggConfigs, + esResponse: Record, + respOpts?: Partial +) { + /** + * read an aggregation from a bucket, which *might* be found at key (if + * the response came in object form), and will recurse down the aggregation + * tree and will pass the read values to the ResponseWriter. + */ + function collectBucket( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + const aggInfo = agg.write(aggs); + aggScale *= aggInfo.metricScale || 1; + + switch (agg.type.type) { + case AggGroupNames.Buckets: + const aggBucket = get(bucket, agg.id); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + + if (tabifyBuckets.length) { + tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { + // if the bucket doesn't have value don't add it to the row + // we don't want rows like: { column1: undefined, column2: 10 } + const bucketValue = agg.getKey(subBucket, tabifyBucketKey); + const hasBucketValue = typeof bucketValue !== 'undefined'; + + if (hasBucketValue) { + write.bucketBuffer.push({ id: column.id, value: bucketValue }); + } + + collectBucket( + aggs, + write, + subBucket, + agg.getKey(subBucket, tabifyBucketKey), + aggScale + ); + + if (hasBucketValue) { + write.bucketBuffer.pop(); + } + }); + } else if (respOpts?.partialRows) { + // we don't have any buckets, but we do have metrics at this + // level, then pass all the empty buckets and jump back in for + // the metrics. + write.columns.unshift(column); + passEmptyBuckets(aggs, write, bucket, key, aggScale); + write.columns.shift(); + } else { + // we don't have any buckets, and we don't have isHierarchical + // data, so no metrics, just try to write the row + write.row(); + } + break; + case AggGroupNames.Metrics: + let value = agg.getValue(bucket); + // since the aggregation could be a non integer (such as a max date) + // only do the scaling calculation if it is needed. + if (aggScale !== 1) { + value *= aggScale; + } + write.metricBuffer.push({ id: column.id, value }); + + if (!write.columns.length) { + // row complete + write.row(); + } else { + // process the next agg at this same level + collectBucket(aggs, write, bucket, key, aggScale); + } + + write.metricBuffer.pop(); + + break; + } + + write.columns.unshift(column); + } + } + + // write empty values for each bucket agg, then write + // the metrics from the initial bucket using collectBucket() + function passEmptyBuckets( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + + switch (agg.type.type) { + case AggGroupNames.Metrics: + // pass control back to collectBucket() + write.columns.unshift(column); + collectBucket(aggs, write, bucket, key, aggScale); + return; + + case AggGroupNames.Buckets: + passEmptyBuckets(aggs, write, bucket, key, aggScale); + } + + write.columns.unshift(column); + } + } + + const write = new TabbedAggResponseWriter(aggConfigs, respOpts || {}); + const topLevelBucket: AggResponseBucket = { + ...esResponse.aggregations, + doc_count: esResponse.hits.total, + }; + + let timeRange: TabbedRangeFilterParams | undefined; + + // Extract the time range object if provided + if (respOpts && respOpts.timeRange) { + const [timeRangeKey] = Object.keys(respOpts.timeRange); + + if (timeRangeKey) { + timeRange = { + name: timeRangeKey, + ...respOpts.timeRange[timeRangeKey], + }; + } + } + + collectBucket(aggConfigs, write, topLevelBucket, '', 1); + + return write.response(); +} diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js b/src/legacy/core_plugins/data/public/search/tabify/types.ts similarity index 69% rename from src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js rename to src/legacy/core_plugins/data/public/search/tabify/types.ts index 38ed5408b603e..3a02a2b64f0c3 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js +++ b/src/legacy/core_plugins/data/public/search/tabify/types.ts @@ -17,8 +17,16 @@ * under the License. */ -import './_get_columns'; -import './_buckets'; -import './_response_writer'; -import './_integration'; -describe('Tabify Agg Response', function() {}); +import { RangeFilterParams } from '../../../../../../plugins/data/public'; + +/** @internal **/ +export interface TabbedRangeFilterParams extends RangeFilterParams { + name: string; +} + +/** @internal **/ +export interface TabbedResponseWriterOptions { + metricsAtAllLevels: boolean; + partialRows: boolean; + timeRange?: { [key: string]: RangeFilterParams }; +} diff --git a/src/legacy/core_plugins/data/public/search/utils/types.ts b/src/legacy/core_plugins/data/public/search/utils/types.ts index 305f27a86b398..e0afe99aa81fa 100644 --- a/src/legacy/core_plugins/data/public/search/utils/types.ts +++ b/src/legacy/core_plugins/data/public/search/utils/types.ts @@ -31,3 +31,9 @@ export interface RequestInspectorStats { hits?: InspectorStat; requestTime?: InspectorStat; } + +export interface AggResponseBucket { + key_as_string: string; + key: number; + doc_count: number; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index b0bb17ce1ac7f..91b5c7f13dc95 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -58,8 +58,7 @@ export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse } from '../../../data/public'; export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { migrateLegacyQuery, diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js index 0dbff60613cb0..9fe7920588cd2 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js @@ -21,7 +21,11 @@ import $ from 'jquery'; import moment from 'moment'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { + metricOnly, + threeTermBuckets, + oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative, +} from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; import { tabifyAggResponse, npStart } from '../../legacy_imports'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; @@ -44,7 +48,7 @@ describe('Table Vis - AggTable Directive', function() { const init = () => { const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); - tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, fixtures.metricOnly); + tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); const vis2 = new visualizationsStart.Vis(indexPattern, { type: 'table', @@ -61,7 +65,7 @@ describe('Table Vis - AggTable Directive', function() { vis2.aggs.aggs.forEach(function(agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets, { + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); @@ -94,7 +98,7 @@ describe('Table Vis - AggTable Directive', function() { tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = tabifyAggResponse( vis3.aggs, - fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative + oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative ); }; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js index f6ae41b024b7d..79d4d7c40d355 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; import { tabifyAggResponse, npStart } from '../../legacy_imports'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { getAngularModule } from '../../get_inner_angular'; @@ -36,7 +36,7 @@ describe('Table Vis - AggTableGroup Directive', function() { const init = () => { const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); - tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, fixtures.metricOnly); + tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); const vis2 = new visualizationsStart.Vis(indexPattern, { type: 'pie', @@ -50,7 +50,7 @@ describe('Table Vis - AggTableGroup Directive', function() { vis2.aggs.aggs.forEach(function(agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets); + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets); }; const initLocalAngular = () => { diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts index cb44814897bcf..90929150de9c3 100644 --- a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts @@ -24,9 +24,7 @@ export { IAggConfig, AggGroupNames, Schemas } from 'ui/agg_types'; export { PaginateDirectiveProvider } from 'ui/directives/paginate'; // @ts-ignore export { PaginateControlsDirectiveProvider } from 'ui/directives/paginate'; -export { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse, tabifyGetColumns } from '../../data/public'; export { configureAppAngularModule, KbnAccessibleClickProvider, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index 9c79be98a320c..1c8e679f7d61f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -19,10 +19,8 @@ export { AggType, AggGroupNames, IAggConfig, IAggType, Schemas } from 'ui/agg_types'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse, tabifyGetColumns } from '../../data/public'; // @ts-ignore export { buildHierarchicalData } from 'ui/agg_response/hierarchical/build_hierarchical_data'; // @ts-ignore export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; -export { tabifyGetColumns } from '../../../ui/public/agg_response/tabify/_get_columns'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js index 534a523103774..9c9c5a84f046c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { threeTermBuckets } from 'fixtures/fake_hierarchical_data'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { start as visualizationsStart } from '../../../../../visualizations/public/np_ready/public/legacy'; @@ -147,7 +147,7 @@ describe('No global chart settings', function() { }); beforeEach(async () => { - const table1 = tabifyAggResponse(stubVis1.aggs, fixtures.threeTermBuckets, { + const table1 = tabifyAggResponse(stubVis1.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); data1 = await responseHandler(table1, rowAggDimensions); @@ -234,7 +234,7 @@ describe('Vislib PieChart Class Test Suite', function() { }); beforeEach(async () => { - const table = tabifyAggResponse(stubVis.aggs, fixtures.threeTermBuckets, { + const table = tabifyAggResponse(stubVis.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); data = await responseHandler(table, dataDimensions); diff --git a/src/legacy/ui/public/agg_response/index.js b/src/legacy/ui/public/agg_response/index.js index 41d45d1a06ca4..139a124356de2 100644 --- a/src/legacy/ui/public/agg_response/index.js +++ b/src/legacy/ui/public/agg_response/index.js @@ -19,7 +19,7 @@ import { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; import { buildPointSeriesData } from './point_series/point_series'; -import { tabifyAggResponse } from './tabify/tabify'; +import { tabifyAggResponse } from '../../../core_plugins/data/public'; export const aggResponseIndex = { hierarchical: buildHierarchicalData, diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js deleted file mode 100644 index 3eb41c03050d0..0000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { tabifyGetColumns } from '../_get_columns'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -describe('get columns', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - it('should inject a count metric if no aggs exist', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - }); - while (vis.aggs.length) vis.aggs.pop(); - const columns = tabifyGetColumns( - vis.getAggConfig().getResponseAggs(), - null, - vis.isHierarchical() - ); - - expect(columns).to.have.length(1); - expect(columns[0]).to.have.property('aggConfig'); - expect(columns[0].aggConfig.type).to.have.property('name', 'count'); - }); - - it('should inject a count metric if only buckets exist', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - expect(columns).to.have.length(2); - expect(columns[1]).to.have.property('aggConfig'); - expect(columns[1].aggConfig.type).to.have.property('name', 'count'); - }); - - it('should inject the metric after each bucket if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - expect(columns).to.have.length(8); - columns.forEach(function(column, i) { - expect(column).to.have.property('aggConfig'); - expect(column.aggConfig.type).to.have.property('name', i % 2 ? 'count' : 'date_histogram'); - }); - }); - - it('should inject the multiple metrics after each bucket if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - function checkColumns(column, i) { - expect(column).to.have.property('aggConfig'); - switch (i) { - case 0: - expect(column.aggConfig.type).to.have.property('name', 'date_histogram'); - break; - case 1: - expect(column.aggConfig.type).to.have.property('name', 'avg'); - break; - case 2: - expect(column.aggConfig.type).to.have.property('name', 'sum'); - break; - } - } - - expect(columns).to.have.length(12); - for (let i = 0; i < columns.length; i += 3) { - columns.slice(i, i + 3).forEach(checkColumns); - } - }); - - it('should put all metrics at the end of the columns if the vis is not hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - expect(columns).to.have.length(6); - - // sum should be last - expect(columns.pop().aggConfig.type).to.have.property('name', 'sum'); - // avg should be before that - expect(columns.pop().aggConfig.type).to.have.property('name', 'avg'); - // the rest are date_histograms - while (columns.length) { - expect(columns.pop().aggConfig.type).to.have.property('name', 'date_histogram'); - } - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js deleted file mode 100644 index f3f2e20149acf..0000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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 'lodash'; -import fixtures from 'fixtures/fake_hierarchical_data'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { tabifyAggResponse } from '../tabify'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('tabifyAggResponse Integration', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - function normalizeIds(vis) { - vis.aggs.aggs.forEach(function(agg, i) { - agg.id = 'agg_' + (i + 1); - }); - } - - it('transforms a simple response properly', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [], - }); - normalizeIds(vis); - - const resp = tabifyAggResponse(vis.getAggConfig(), fixtures.metricOnly, { - metricsAtAllLevels: vis.isHierarchical(), - }); - - expect(resp) - .to.have.property('rows') - .and.property('columns'); - expect(resp.rows).to.have.length(1); - expect(resp.columns).to.have.length(1); - - expect(resp.rows[0]).to.eql({ 'col-0-agg_1': 1000 }); - expect(resp.columns[0]).to.have.property('aggConfig', vis.aggs[0]); - }); - - describe('transforms a complex response', function() { - this.slow(1000); - - let vis; - let avg; - let ext; - let src; - let os; - let esResp; - - beforeEach(function() { - vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'terms', schema: 'split', params: { field: 'extension' } }, - { type: 'terms', schema: 'segment', params: { field: 'geo.src' } }, - { type: 'terms', schema: 'segment', params: { field: 'machine.os' } }, - ], - }); - normalizeIds(vis); - - avg = vis.aggs[0]; - ext = vis.aggs[1]; - src = vis.aggs[2]; - os = vis.aggs[3]; - - esResp = _.cloneDeep(fixtures.threeTermBuckets); - // remove the buckets for css in MX - esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; - }); - - // check that the columns of a table are formed properly - function expectColumns(table, aggs) { - expect(table.columns) - .to.be.an('array') - .and.have.length(aggs.length); - aggs.forEach(function(agg, i) { - expect(table.columns[i]).to.have.property('aggConfig', agg); - }); - } - - // check that a row has expected values - function expectRow(row, asserts) { - expect(row).to.be.an('object'); - asserts.forEach(function(assert, i) { - if (row[`col-${i}`]) { - assert(row[`col-${i}`]); - } - }); - } - - // check for two character country code - function expectCountry(val) { - expect(val).to.be.a('string'); - expect(val).to.have.length(2); - } - - // check for an OS term - function expectExtension(val) { - expect(val).to.match(/^(js|png|html|css|jpg)$/); - } - - // check for an OS term - function expectOS(val) { - expect(val).to.match(/^(win|mac|linux)$/); - } - - // check for something like an average bytes result - function expectAvgBytes(val) { - expect(val).to.be.a('number'); - expect(val === 0 || val > 1000).to.be.ok(); - } - - it('for non-hierarchical vis', function() { - // the default for a non-hierarchical vis is to display - // only complete rows, and only put the metrics at the end. - - const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: false }); - - expectColumns(tabbed, [ext, src, os, avg]); - - tabbed.rows.forEach(function(row) { - expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); - }); - }); - - it('for hierarchical vis', function() { - // since we have partialRows we expect that one row will have some empty - // values, and since the vis is hierarchical and we are NOT using - // minimalColumns we should expect the partial row to be completely after - // the existing bucket and it's metric - - vis.isHierarchical = _.constant(true); - const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: true }); - - expectColumns(tabbed, [ext, avg, src, avg, os, avg]); - - tabbed.rows.forEach(function(row) { - expectRow(row, [ - expectExtension, - expectAvgBytes, - expectCountry, - expectAvgBytes, - expectOS, - expectAvgBytes, - ]); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js deleted file mode 100644 index b0c0f2f3d9100..0000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { TabbedAggResponseWriter } from '../_response_writer'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('TabbedAggResponseWriter class', function() { - let Private; - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - Private = $injector.get('Private'); - - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - const splitAggConfig = [ - { - type: 'terms', - params: { - field: 'geo.src', - }, - }, - ]; - - const twoSplitsAggConfig = [ - { - type: 'terms', - params: { - field: 'geo.src', - }, - }, - { - type: 'terms', - params: { - field: 'machine.os.raw', - }, - }, - ]; - - const createResponseWritter = (aggs = [], opts = {}) => { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: aggs }); - return new TabbedAggResponseWriter(vis.getAggConfig(), opts); - }; - - describe('Constructor', function() { - let responseWriter; - beforeEach(() => { - responseWriter = createResponseWritter(twoSplitsAggConfig); - }); - - it('creates aggStack', () => { - expect(responseWriter.aggStack.length).to.eql(3); - }); - - it('generates columns', () => { - expect(responseWriter.columns.length).to.eql(3); - }); - - it('correctly generates columns with metricsAtAllLevels set to true', () => { - const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { - metricsAtAllLevels: true, - }); - expect(minimalColumnsResponseWriter.columns.length).to.eql(4); - }); - - describe('sets timeRange', function() { - it("to the first nested object's range", function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: [] }); - const range = { - gte: 0, - lte: 100, - }; - - const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { - timeRange: { - '@timestamp': range, - }, - }); - - expect(writer.timeRange.gte).to.be(range.gte); - expect(writer.timeRange.lte).to.be(range.lte); - expect(writer.timeRange.name).to.be('@timestamp'); - }); - - it('to undefined if no nested object', function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: [] }); - - const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { - timeRange: {}, - }); - expect(writer).to.have.property('timeRange', undefined); - }); - }); - }); - - describe('row()', function() { - let responseWriter; - - beforeEach(() => { - responseWriter = createResponseWritter(splitAggConfig, { partialRows: true }); - }); - - it('adds the row to the array', () => { - responseWriter.rowBuffer['col-0'] = 'US'; - responseWriter.rowBuffer['col-1'] = 5; - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(1); - expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 }); - }); - - it('correctly handles bucketBuffer', () => { - responseWriter.bucketBuffer.push({ id: 'col-0', value: 'US' }); - responseWriter.rowBuffer['col-1'] = 5; - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(1); - expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 }); - }); - - it("doesn't add an empty row", () => { - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(0); - }); - }); - - describe('response()', () => { - let responseWriter; - - beforeEach(() => { - responseWriter = createResponseWritter(splitAggConfig); - }); - - it('produces correct response', () => { - responseWriter.rowBuffer['col-0-1'] = 'US'; - responseWriter.rowBuffer['col-1-2'] = 5; - responseWriter.row(); - const response = responseWriter.response(); - expect(response).to.have.property('rows'); - expect(response.rows).to.eql([{ 'col-0-1': 'US', 'col-1-2': 5 }]); - expect(response).to.have.property('columns'); - expect(response.columns.length).to.equal(2); - expect(response.columns[0]).to.have.property('id', 'col-0-1'); - expect(response.columns[0]).to.have.property('name', 'geo.src: Descending'); - expect(response.columns[0]).to.have.property('aggConfig'); - expect(response.columns[1]).to.have.property('id', 'col-1-2'); - expect(response.columns[1]).to.have.property('name', 'Count'); - expect(response.columns[1]).to.have.property('aggConfig'); - }); - - it('produces correct response for no data', () => { - const response = responseWriter.response(); - expect(response).to.have.property('rows'); - expect(response.rows.length).to.be(0); - expect(response).to.have.property('columns'); - expect(response.columns.length).to.equal(2); - expect(response.columns[0]).to.have.property('id', 'col-0-1'); - expect(response.columns[0]).to.have.property('name', 'geo.src: Descending'); - expect(response.columns[0]).to.have.property('aggConfig'); - expect(response.columns[1]).to.have.property('id', 'col-1-2'); - expect(response.columns[1]).to.have.property('name', 'Count'); - expect(response.columns[1]).to.have.property('aggConfig'); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/_buckets.js b/src/legacy/ui/public/agg_response/tabify/_buckets.js deleted file mode 100644 index 7180a056ab0ca..0000000000000 --- a/src/legacy/ui/public/agg_response/tabify/_buckets.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 'lodash'; -import moment from 'moment'; - -function TabifyBuckets(aggResp, aggParams, timeRange) { - if (_.has(aggResp, 'buckets')) { - this.buckets = aggResp.buckets; - } else if (aggResp) { - // Some Bucket Aggs only return a single bucket (like filter). - // In those instances, the aggResp is the content of the single bucket. - this.buckets = [aggResp]; - } else { - this.buckets = []; - } - - this.objectMode = _.isPlainObject(this.buckets); - if (this.objectMode) { - this._keys = _.keys(this.buckets); - this.length = this._keys.length; - } else { - this.length = this.buckets.length; - } - - if (this.length && aggParams) { - this._orderBucketsAccordingToParams(aggParams); - if (aggParams.drop_partials) { - this._dropPartials(aggParams, timeRange); - } - } -} - -TabifyBuckets.prototype.forEach = function(fn) { - const buckets = this.buckets; - - if (this.objectMode) { - this._keys.forEach(function(key) { - fn(buckets[key], key); - }); - } else { - buckets.forEach(function(bucket) { - fn(bucket, bucket.key); - }); - } -}; - -TabifyBuckets.prototype._isRangeEqual = function(range1, range2) { - return ( - _.get(range1, 'from', null) === _.get(range2, 'from', null) && - _.get(range1, 'to', null) === _.get(range2, 'to', null) - ); -}; - -TabifyBuckets.prototype._orderBucketsAccordingToParams = function(params) { - if (params.filters && this.objectMode) { - this._keys = params.filters.map(filter => { - const query = _.get(filter, 'input.query.query_string.query', filter.input.query); - const queryString = typeof query === 'string' ? query : JSON.stringify(query); - return filter.label || queryString || '*'; - }); - } else if (params.ranges && this.objectMode) { - this._keys = params.ranges.map(range => { - return _.findKey(this.buckets, el => this._isRangeEqual(el, range)); - }); - } else if (params.ranges && params.field.type !== 'date') { - let ranges = params.ranges; - if (params.ipRangeType) { - ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; - } - this.buckets = ranges.map(range => { - if (range.mask) { - return this.buckets.find(el => el.key === range.mask); - } - return this.buckets.find(el => this._isRangeEqual(el, range)); - }); - } -}; - -// dropPartials should only be called if the aggParam setting is enabled, -// and the agg field is the same as the Time Range. -TabifyBuckets.prototype._dropPartials = function(params, timeRange) { - if ( - !timeRange || - this.buckets.length <= 1 || - this.objectMode || - params.field.name !== timeRange.name - ) { - return; - } - - const interval = this.buckets[1].key - this.buckets[0].key; - - this.buckets = this.buckets.filter(bucket => { - if (moment(bucket.key).isBefore(timeRange.gte)) { - return false; - } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { - return false; - } - return true; - }); - - this.length = this.buckets.length; -}; - -export { TabifyBuckets }; diff --git a/src/legacy/ui/public/agg_response/tabify/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/_response_writer.js deleted file mode 100644 index 85586c7ca7fda..0000000000000 --- a/src/legacy/ui/public/agg_response/tabify/_response_writer.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 { toArray } from 'lodash'; -import { tabifyGetColumns } from './_get_columns'; - -/** - * Writer class that collects information about an aggregation response and - * produces a table, or a series of tables. - * - * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates - * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket - * @param {boolean} partialRows - setting to true will not remove rows with missing values - * @param {Object} timeRange - time range object, if provided - */ -function TabbedAggResponseWriter( - aggs, - { metricsAtAllLevels = false, partialRows = false, timeRange } = {} -) { - // Private - this._removePartialRows = !partialRows; - - // Public - this.rowBuffer = {}; - this.bucketBuffer = []; - this.metricBuffer = []; - this.aggs = aggs; - this.partialRows = partialRows; - this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); - this.aggStack = [...this.columns]; - this.rows = []; - // Extract the time range object if provided - if (timeRange) { - const timeRangeKey = Object.keys(timeRange)[0]; - this.timeRange = timeRange[timeRangeKey]; - if (this.timeRange) { - this.timeRange.name = timeRangeKey; - } - } -} - -TabbedAggResponseWriter.prototype.isPartialRow = function(row) { - return !this.columns.map(column => row.hasOwnProperty(column.id)).every(c => c === true); -}; - -/** - * Create a new row by reading the row buffer and bucketBuffer - */ -TabbedAggResponseWriter.prototype.row = function() { - this.bucketBuffer.forEach(bucket => { - this.rowBuffer[bucket.id] = bucket.value; - }); - - this.metricBuffer.forEach(metric => { - this.rowBuffer[metric.id] = metric.value; - }); - - if ( - !toArray(this.rowBuffer).length || - (this._removePartialRows && this.isPartialRow(this.rowBuffer)) - ) { - return; - } - - this.rows.push(this.rowBuffer); - this.rowBuffer = {}; -}; - -/** - * Get the actual response - * - * @return {object} - the final table - */ -TabbedAggResponseWriter.prototype.response = function() { - return { - columns: this.columns, - rows: this.rows, - }; -}; - -export { TabbedAggResponseWriter }; diff --git a/src/legacy/ui/public/agg_response/tabify/tabify.js b/src/legacy/ui/public/agg_response/tabify/tabify.js deleted file mode 100644 index 8316055cb15cc..0000000000000 --- a/src/legacy/ui/public/agg_response/tabify/tabify.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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 'lodash'; -import { TabbedAggResponseWriter } from './_response_writer'; -import { TabifyBuckets } from './_buckets'; - -/** - * Sets up the ResponseWriter and kicks off bucket collection. - * - * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates - * @param {Object} esResponse - response that came back from Elasticsearch - * @param {Object} respOpts - options object for the ResponseWriter with params set by Courier - * @param {boolean} respOpts.metricsAtAllLevels - setting to true will produce metrics for every bucket - * @param {boolean} respOpts.partialRows - setting to true will not remove rows with missing values - * @param {Object} respOpts.timeRange - time range object, if provided - */ -export function tabifyAggResponse(aggs, esResponse, respOpts = {}) { - const write = new TabbedAggResponseWriter(aggs, respOpts); - - const topLevelBucket = _.assign({}, esResponse.aggregations, { - doc_count: esResponse.hits.total, - }); - - collectBucket(write, topLevelBucket, '', 1); - - return write.response(); -} - -/** - * read an aggregation from a bucket, which *might* be found at key (if - * the response came in object form), and will recurse down the aggregation - * tree and will pass the read values to the ResponseWriter. - * - * @param {object} bucket - a bucket from the aggResponse - * @param {undefined|string} key - the key where the bucket was found - * @returns {undefined} - */ -function collectBucket(write, bucket, key, aggScale) { - const column = write.aggStack.shift(); - const agg = column.aggConfig; - const aggInfo = agg.write(write.aggs); - aggScale *= aggInfo.metricScale || 1; - - switch (agg.type.type) { - case 'buckets': - const buckets = new TabifyBuckets(bucket[agg.id], agg.params, write.timeRange); - if (buckets.length) { - buckets.forEach(function(subBucket, key) { - // if the bucket doesn't have value don't add it to the row - // we don't want rows like: { column1: undefined, column2: 10 } - const bucketValue = agg.getKey(subBucket, key); - const hasBucketValue = typeof bucketValue !== 'undefined'; - if (hasBucketValue) { - write.bucketBuffer.push({ id: column.id, value: bucketValue }); - } - collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale); - if (hasBucketValue) { - write.bucketBuffer.pop(); - } - }); - } else if (write.partialRows) { - // we don't have any buckets, but we do have metrics at this - // level, then pass all the empty buckets and jump back in for - // the metrics. - write.aggStack.unshift(column); - passEmptyBuckets(write, bucket, key, aggScale); - write.aggStack.shift(); - } else { - // we don't have any buckets, and we don't have isHierarchical - // data, so no metrics, just try to write the row - write.row(); - } - break; - case 'metrics': - let value = agg.getValue(bucket); - // since the aggregation could be a non integer (such as a max date) - // only do the scaling calculation if it is needed. - if (aggScale !== 1) { - value *= aggScale; - } - write.metricBuffer.push({ id: column.id, value: value }); - - if (!write.aggStack.length) { - // row complete - write.row(); - } else { - // process the next agg at this same level - collectBucket(write, bucket, key, aggScale); - } - - write.metricBuffer.pop(); - - break; - } - - write.aggStack.unshift(column); -} - -// write empty values for each bucket agg, then write -// the metrics from the initial bucket using collectBucket() -function passEmptyBuckets(write, bucket, key, aggScale) { - const column = write.aggStack.shift(); - const agg = column.aggConfig; - - switch (agg.type.type) { - case 'metrics': - // pass control back to collectBucket() - write.aggStack.unshift(column); - collectBucket(write, bucket, key, aggScale); - return; - - case 'buckets': - passEmptyBuckets(write, bucket, key, aggScale); - } - - write.aggStack.unshift(column); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index cb8b43a6c312b..0912e5a9f1283 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -11,7 +11,7 @@ import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { HeatmapLayer } from '../../heatmap_layer'; import { VectorLayer } from '../../vector_layer'; import { AggConfigs, Schemas } from 'ui/agg_types'; -import { tabifyAggResponse } from 'ui/agg_response/tabify'; +import { tabifyAggResponse } from '../../../../../../../../src/legacy/core_plugins/data/public'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; import { From 39c835af605e9c832cde2d2a40200ef8283a46f8 Mon Sep 17 00:00:00 2001 From: Sebastian Grodzicki Date: Thu, 20 Feb 2020 14:05:21 +0100 Subject: [PATCH 008/113] Add owners for monitoring plugin (#57987) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 56db8d3793f57..bea10a1c8b31c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -75,6 +75,7 @@ /x-pack/plugins/ingest_manager/ @elastic/ingest /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui From 47bb7c61b67c1370a73fe247f4248326b8f696b4 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 20 Feb 2020 09:13:49 -0500 Subject: [PATCH 009/113] update estimateBucketSpan schema and api test (#58004) --- .../new_platform/job_validation_schema.ts | 1 + .../apis/ml/bucket_span_estimator.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts index 1cc6e8a97ffc0..5917ec50884d8 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts @@ -11,6 +11,7 @@ export const estimateBucketSpanSchema = schema.object({ aggTypes: schema.arrayOf(schema.nullable(schema.string())), duration: schema.object({ start: schema.number(), end: schema.number() }), fields: schema.arrayOf(schema.nullable(schema.string())), + filters: schema.maybe(schema.arrayOf(schema.any())), index: schema.string(), query: schema.any(), splitField: schema.maybe(schema.string()), diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts index 47afe7553fe62..1c7245234b089 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -69,6 +69,23 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { name: '3h', ms: 10800000 }, }, }, + { + testTitleSuffix: 'with 1 field, 1 agg, no split, and empty filters', + user: USER.ML_POWERUSER, + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + filters: [], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '15m', ms: 900000 }, + }, + }, ]; describe('bucket span estimator', function() { From 5cfc5ef9fffb3f39fedc06b495aecc881d1b731a Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 20 Feb 2020 09:14:11 -0500 Subject: [PATCH 010/113] Reverting grouped nav UI (#57724) * reverting grouped nav ui * removing Management name change --- docs/management/advanced-options.asciidoc | 3 - src/core/public/chrome/chrome_service.tsx | 1 - .../__snapshots__/nav_drawer.test.tsx.snap | 5283 ----------------- src/core/public/chrome/ui/header/header.tsx | 12 +- src/core/public/chrome/ui/header/index.ts | 1 - .../chrome/ui/header/nav_drawer.test.tsx | 103 - .../public/chrome/ui/header/nav_drawer.tsx | 118 +- src/legacy/core_plugins/kibana/index.js | 2 +- .../home/np_ready/components/home.test.js | 2 +- .../kibana/public/management/index.js | 2 +- .../kibana/ui_setting_defaults.js | 19 - .../ui/public/management/breadcrumbs.ts | 2 +- .../management_sidebar_nav.tsx | 3 +- .../public/legacy/sections_register.js | 4 +- .../management/public/management_app.tsx | 3 +- src/plugins/management/public/plugin.ts | 4 +- .../dashboard/create_and_add_embeddables.js | 1 - .../apps/management/_index_pattern_filter.js | 2 +- test/functional/page_objects/header_page.js | 2 +- test/functional/page_objects/settings_page.ts | 7 - .../core_plugins/application_status.ts | 6 +- .../test_suites/core_plugins/applications.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../advanced_settings_security.ts | 8 +- .../advanced_settings_spaces.ts | 3 +- .../apps/apm/feature_controls/apm_security.ts | 4 +- .../apps/apm/feature_controls/apm_spaces.ts | 3 +- .../feature_controls/canvas_security.ts | 4 +- .../canvas/feature_controls/canvas_spaces.ts | 3 +- .../feature_controls/dashboard_security.ts | 4 +- .../feature_controls/dashboard_spaces.ts | 9 +- .../dashboard_mode/dashboard_view_mode.js | 9 +- .../feature_controls/dev_tools_security.ts | 4 +- .../feature_controls/dev_tools_spaces.ts | 9 +- .../feature_controls/discover_security.ts | 4 +- .../feature_controls/discover_spaces.ts | 2 - .../graph/feature_controls/graph_security.ts | 4 +- .../graph/feature_controls/graph_spaces.ts | 3 +- .../index_patterns_security.ts | 8 +- .../feature_controls/index_patterns_spaces.ts | 3 +- .../infrastructure_security.ts | 4 +- .../feature_controls/infrastructure_spaces.ts | 9 +- .../infra/feature_controls/logs_security.ts | 4 +- .../infra/feature_controls/logs_spaces.ts | 9 +- .../feature_controls/ml_security.ts | 3 +- .../feature_controls/ml_spaces.ts | 3 +- .../maps/feature_controls/maps_security.ts | 6 +- .../feature_controls/monitoring_security.ts | 3 +- .../feature_controls/monitoring_spaces.ts | 3 +- .../feature_controls/spaces_security.ts | 5 +- .../feature_controls/timelion_security.ts | 4 +- .../feature_controls/timelion_spaces.ts | 9 +- .../feature_controls/uptime_security.ts | 4 +- .../uptime/feature_controls/uptime_spaces.ts | 3 +- .../feature_controls/visualize_security.ts | 4 +- .../feature_controls/visualize_spaces.ts | 9 +- 57 files changed, 79 insertions(+), 5671 deletions(-) delete mode 100644 src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap delete mode 100644 src/core/public/chrome/ui/header/nav_drawer.test.tsx diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ec626677d0902..80c9053dc5ae6 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -70,9 +70,6 @@ into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`pageNavigation`:: The style of navigation menu for Kibana. -Choices are Individual, the legacy style where every plugin is represented in the nav, -and Grouped, a new format that bundles related plugins together in nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 6ab9fe158742a..2b0b115ce068e 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -193,7 +193,6 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} - navSetting$={uiSettings.get$('pageNavigation')} /> ), diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap deleted file mode 100644 index cf3b48f237286..0000000000000 --- a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap +++ /dev/null @@ -1,5283 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there are less than 7 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there is only 1 category 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to individual renders individual items 1`] = ` - - - - - - - -`; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c3cefd180b16f..c9a583f39b30c 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -42,7 +42,7 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; import { HeaderBadge } from './header_badge'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; @@ -69,7 +69,6 @@ export interface HeaderProps { navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; isLocked?: boolean; - navSetting$: Rx.Observable; onIsLockedUpdate?: OnIsLockedUpdate; } @@ -81,7 +80,6 @@ interface State { forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; - navSetting: NavSetting; currentAppId: string | undefined; } @@ -100,7 +98,6 @@ export class Header extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], - navSetting: 'grouped', currentAppId: '', }; } @@ -116,8 +113,7 @@ export class Header extends Component { Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.navSetting$ + this.props.application.currentAppId$ ) ).subscribe({ next: ([ @@ -126,7 +122,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, navSetting], + [navControlsLeft, navControlsRight, currentAppId], ]) => { this.setState({ appTitle, @@ -136,7 +132,6 @@ export class Header extends Component { recentlyAccessed, navControlsLeft, navControlsRight, - navSetting, currentAppId, }); }, @@ -225,7 +220,6 @@ export class Header extends Component { void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.test.tsx b/src/core/public/chrome/ui/header/nav_drawer.test.tsx deleted file mode 100644 index 7272935b93a52..0000000000000 --- a/src/core/public/chrome/ui/header/nav_drawer.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { cloneDeep } from 'lodash'; -import { mount } from 'enzyme'; -import React from 'react'; -import { NavSetting } from './'; -import { ChromeNavLink } from '../../../'; -import { AppCategory } from 'src/core/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../utils'; -import { NavDrawer } from './nav_drawer'; -import { euiNavLink } from './nav_link'; - -const { analyze, management, observability, security } = DEFAULT_APP_CATEGORIES; -const mockIBasePath = { - get: () => '/app', - prepend: () => '/app', - remove: () => '/app', -}; - -const getMockProps = (chromeNavLinks: ChromeNavLink[], navSetting: NavSetting = 'grouped') => ({ - navSetting, - navLinks: chromeNavLinks.map(link => - euiNavLink(link, true, undefined, mockIBasePath, () => Promise.resolve()) - ), - chromeNavLinks, - recentlyAccessedItems: [], - basePath: mockIBasePath, -}); - -const makeLink = (id: string, order: number, category?: AppCategory) => ({ - id, - category, - order, - title: id, - baseUrl: `http://localhost:5601/app/${id}`, - legacy: true, -}); - -const getMockChromeNavLink = () => - cloneDeep([ - makeLink('discover', 100, analyze), - makeLink('siem', 500, security), - makeLink('metrics', 600, observability), - makeLink('monitoring', 800, management), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, analyze), - makeLink('canvas', 400, { label: 'customCategory' }), - makeLink('logs', 700, observability), - ]); - -describe('NavDrawer', () => { - describe('Advanced setting set to individual', () => { - it('renders individual items', () => { - const component = mount( - - ); - expect(component).toMatchSnapshot(); - }); - }); - describe('Advanced setting set to grouped', () => { - it('renders individual items if there are less than 7', () => { - const links = getMockChromeNavLink().slice(0, 5); - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders individual items if there is only 1 category', () => { - // management doesn't count as a category - const navLinks = [ - makeLink('discover', 100, analyze), - makeLink('siem', 500, analyze), - makeLink('metrics', 600, analyze), - makeLink('monitoring', 800, analyze), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, management), - makeLink('canvas', 400, management), - makeLink('logs', 700, management), - ]; - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders grouped items', () => { - const component = mount(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index dbb68d5dd3901..c57faec1e428d 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -18,39 +18,16 @@ */ import React from 'react'; -import { groupBy, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { AppCategory } from '../../../../types'; import { HttpStart } from '../../../http'; import { NavLink } from './nav_link'; import { RecentLinks } from './recent_links'; -function getAllCategories(allCategorizedLinks: Record) { - const allCategories = {} as Record; - - for (const [key, value] of Object.entries(allCategorizedLinks)) { - allCategories[key] = value[0].category; - } - - return allCategories; -} - -function getOrderedCategories( - mainCategories: Record, - categoryDictionary: ReturnType -) { - return sortBy( - Object.keys(mainCategories), - categoryName => categoryDictionary[categoryName]?.order - ); -} - export interface Props { - navSetting: NavSetting; isLocked?: boolean; onIsLockedUpdate?: OnIsLockedUpdate; navLinks: NavLink[]; @@ -60,26 +37,9 @@ export interface Props { } function navDrawerRenderer( - { - navSetting, - isLocked, - onIsLockedUpdate, - navLinks, - chromeNavLinks, - recentlyAccessedItems, - basePath, - }: Props, + { isLocked, onIsLockedUpdate, navLinks, chromeNavLinks, recentlyAccessedItems, basePath }: Props, ref: React.Ref ) { - const disableGroupedNavSetting = navSetting === 'individual'; - const groupedNavLinks = groupBy(navLinks, link => link?.category?.label); - const { undefined: unknowns, ...allCategorizedLinks } = groupedNavLinks; - const { Management: management, ...mainCategories } = allCategorizedLinks; - const categoryDictionary = getAllCategories(allCategorizedLinks); - const orderedCategories = getOrderedCategories(mainCategories, categoryDictionary); - const showUngroupedNav = - disableGroupedNavSetting || navLinks.length < 7 || Object.keys(mainCategories).length === 1; - return ( - {showUngroupedNav ? ( - - ) : ( - <> - { - const category = categoryDictionary[categoryName]!; - const links = mainCategories[categoryName]; - - if (links.length === 1) { - return { - ...links[0], - label: category.label, - iconType: category.euiIconType || links[0].iconType, - }; - } - - return { - 'data-test-subj': 'navDrawerCategory', - iconType: category.euiIconType, - label: category.label, - flyoutMenu: { - title: category.label, - listItems: sortBy(links, 'order').map(link => { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }; - }), - ...sortBy(unknowns, 'order'), - ]} - /> - - { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }, - ]} - /> - - )} + ); } diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e6bae0b588bc..221133a17d59a 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -115,7 +115,7 @@ export default function(kibana) { { id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index b0d94711be7b6..db24cb3e3c1b7 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -131,7 +131,7 @@ describe('home', () => { test('should not render directory entry when showOnHomePage is false', async () => { const directoryEntry = { id: 'stack-management', - title: 'Stack Management', + title: 'Management', description: 'Your center console for managing the Elastic Stack.', icon: 'managementApp', path: 'management_landing_page', diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 2cba9fab7be22..6a36391c56b5c 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -69,7 +69,7 @@ export function updateLandingPage(version) {

diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 744ede891b84a..f92694eabe58d 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -1174,24 +1174,5 @@ export function getUiSettingDefaults() { category: ['accessibility'], requiresPageReload: true, }, - pageNavigation: { - name: i18n.translate('kbn.advancedSettings.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'grouped', - description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['grouped', 'individual'], - optionLabels: { - grouped: i18n.translate('kbn.advancedSettings.pageNavigationGrouped', { - defaultMessage: 'Grouped', - }), - individual: i18n.translate('kbn.advancedSettings.pageNavigationIndividual', { - defaultMessage: 'Individual', - }), - }, - }, }; } diff --git a/src/legacy/ui/public/management/breadcrumbs.ts b/src/legacy/ui/public/management/breadcrumbs.ts index 936e99caff565..e6156b6639ac4 100644 --- a/src/legacy/ui/public/management/breadcrumbs.ts +++ b/src/legacy/ui/public/management/breadcrumbs.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; export const MANAGEMENT_BREADCRUMB = Object.freeze({ text: i18n.translate('common.ui.stackManagement.breadcrumb', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), href: '#/management', }); diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index 69ba813d2347e..01a98eb0ddb1f 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -168,8 +168,7 @@ export class ManagementSidebarNav extends React.Component<

{i18n.translate('management.nav.label', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', })}

diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index ca35db56c340b..63d919377f89e 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -27,8 +27,7 @@ export class LegacyManagementAdapter { 'management', { display: i18n.translate('management.displayName', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), }, capabilities @@ -36,7 +35,6 @@ export class LegacyManagementAdapter { this.main.register('data', { display: i18n.translate('management.connectDataDisplayName', { - // todo defaultMessage: 'Connect Data', }), order: 0, diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index 02b3ea306c23d..705d98eaaf2ff 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -64,8 +64,7 @@ export class ManagementApp { coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), href: '#/management', }, diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index df2398412dac2..1c9e1d5c89550 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -36,8 +36,8 @@ export class ManagementPlugin implements Plugin { - before(async () => { - await PageObjects.settings.setNavType('individual'); - }); - beforeEach(async () => { await PageObjects.common.navigateToApp('app_status_start'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 6567837f65309..f50d460532556 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -122,7 +122,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('can navigate from NP apps to legacy apps', async () => { - await appsMenu.clickLink('Stack Management'); + await appsMenu.clickLink('Management'); await loadingScreenShown(); await testSubjects.existOrFail('managementNav'); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b171863f26c21..47bf2ae634048 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1449,7 +1449,6 @@ "kbn.management.indexPatterns.listBreadcrumb": "インデックスパターン", "kbn.management.indexPatternTable.createBtn": "インデックスパターンの作成", "kbn.management.indexPatternTable.title": "インデックスパターン", - "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 050e9bd40f58d..a94a602e48d9b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1449,7 +1449,6 @@ "kbn.management.indexPatterns.listBreadcrumb": "索引模式", "kbn.management.indexPatternTable.createBtn": "创建索引模式", "kbn.management.indexPatternTable.title": "索引模式", - "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "在左侧菜单中可找到完整工具列表", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除", diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 261a853387619..0d4c6b2c87666 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -55,7 +55,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expectSpaceSelector: false, } ); - await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); + await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -69,7 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`allows settings to be changed`, async () => { @@ -125,7 +125,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`does not allow settings to be changed`, async () => { @@ -176,7 +176,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`does not allow navigation to advanced settings; redirects to management home`, async () => { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 53202089e8961..fc4f385df3694 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -41,9 +41,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`allows settings to be changed`, async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 7c9c9f9c8c155..e2d5efac4644c 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['APM', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { @@ -109,7 +109,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['APM', 'Stack Management']); + expect(navLinks).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts index 474240b201fac..1ac1784e0e05d 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('APM'); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 71c10bd8248be..d0e37ec8e3f35 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas', 'Management']); }); it(`landing page shows "Create new workpad" button`, async () => { @@ -142,7 +142,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas', 'Management']); }); it(`landing page shows disabled "Create new workpad" button`, async () => { diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 5395f125bbd22..28b572401892b 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector', 'settings']); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('spaces feature controls', function() { @@ -40,7 +40,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Canvas'); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index bfffefaecd94c..b966d37becc3f 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -77,7 +77,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Management']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -261,7 +261,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks).to.eql(['Dashboard', 'Management']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index 1f4f0f33a061e..5ab26e4189096 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -14,13 +14,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'dashboard', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -50,7 +44,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dashboard'); }); diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index b521c47585d58..b9c0b0095b96b 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -37,10 +37,7 @@ export default function({ getService, getPageObjects }) { log.debug('Dashboard View Mode:initTests'); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('dashboard_view_mode'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - pageNavigation: 'individual', - }); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await browser.setWindowSize(1600, 1000); await PageObjects.common.navigateToApp('discover'); @@ -200,7 +197,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mixeduser', '123456'); - if (await appsMenu.linkExists('Stack Management')) { + if (await appsMenu.linkExists('Management')) { throw new Error('Expected management nav link to not be shown'); } }); @@ -209,7 +206,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mysuperuser', '123456'); - if (!(await appsMenu.linkExists('Stack Management'))) { + if (!(await appsMenu.linkExists('Management'))) { throw new Error('Expected management nav link to be shown'); } }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 162bf23c29490..3d17d235b7f4f 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { @@ -145,7 +145,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it(`shows 'Dev Tools' navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts index 561b7f64eb77d..d1eddfe89c59e 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -10,13 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'dashboard', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); const grokDebugger = getService('grokDebugger'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dev Tools'); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 53d0872b810fe..87ae5231d1031 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -83,7 +83,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Management']); }); it('shows save button', async () => { @@ -170,7 +170,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`doesn't show save button`, async () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index ba7d4077b2740..4bedc757f0b57 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -16,7 +16,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { 'timePicker', 'security', 'spaceSelector', - 'settings', ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -51,7 +50,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Discover'); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index 37de93a0a7e91..a2b062e6ef84f 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Management']); }); it('landing page shows "Create new graph" button', async () => { @@ -127,7 +127,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Graph', 'Stack Management']); + expect(navLinks).to.eql(['Graph', 'Management']); }); it('does not show a "Create new Workspace" button', async () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts index d0d0232b5a8b1..a0b0d5bef9668 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'graph', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -34,7 +34,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Graph'); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 0783767d8f152..d72c9b970204a 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -71,7 +71,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`index pattern listing shows create button`, async () => { @@ -114,7 +114,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { } ); - await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); + await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -125,7 +125,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`index pattern listing doesn't show create button`, async () => { @@ -177,7 +177,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`doesn't show Index Patterns in management side-nav`, async () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index d4422e94d10cf..7d9bee37bbbc4 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -41,9 +41,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`index pattern listing shows create button`, async () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index ede77b7d9afa7..bf35d4dc06aa2 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -61,7 +61,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Metrics', 'Management']); }); describe('infrastructure landing page without data', () => { @@ -177,7 +177,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Metrics', 'Management']); }); describe('infrastructure landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 3bbcc1aa7043c..37056c7f17ca1 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'infraHome', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Metrics'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 48ad4e90fd413..e5a6e27a0fadb 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Stack Management']); + expect(navLinks).to.eql(['Logs', 'Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Stack Management']); + expect(navLinks).to.eql(['Logs', 'Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 0094d227514c0..985131113c535 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'infraHome', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -42,7 +36,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Logs'); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index c25c1bfe4b731..8fb6f21c778d3 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'security']); describe('security', () => { before(async () => { @@ -94,7 +94,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); - await PageObjects.settings.setNavType('individual'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index c633852a2da0a..fc94688e98811 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -39,7 +39,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Machine Learning'); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index ece162cbd96cc..804ad5725edfd 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps', 'Management']); }); it(`allows a map to be created`, async () => { @@ -153,7 +153,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps', 'Management']); }); it(`does not show create new button`, async () => { @@ -248,7 +248,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('does not show Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`returns a 404`, async () => { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index 130aefb3cae2a..d985da42ab5ed 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'security']); describe('security', () => { before(async () => { @@ -97,7 +97,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows monitoring navlink', async () => { - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 0465cbcf54541..9e306b074d214 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const find = getService('find'); @@ -41,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 9ca314ba5ec18..49b684a37079e 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -16,7 +16,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { describe('security feature controls', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.settings.setNavType('individual'); }); after(async () => { @@ -57,7 +56,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`displays Spaces management section`, async () => { @@ -135,7 +134,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`doesn't display Spaces management section`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index 62483a10552e3..dea45f161e451 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion', 'Management']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion', 'Management']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index 7e0fe731301a6..fb203a23359bd 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'timelion', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('timelion', () => { @@ -44,7 +38,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Timelion'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index 4ff82484db91c..a004f8db66823 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { @@ -115,7 +115,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Uptime', 'Stack Management']); + expect(navLinks).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts index c3dcb1b27771f..77c5b323340bf 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Uptime'); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 1876f46038326..e5b6512d1c1b0 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -76,7 +76,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize', 'Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -200,7 +200,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize', 'Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index b1cb156caad90..4f12dd16247f6 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -11,13 +11,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'visualize', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Visualize'); }); From 3cba05128e21772307bce491cebbedcd34ff6b2b Mon Sep 17 00:00:00 2001 From: Maggie Ghamry <46542915+maggieghamry@users.noreply.github.com> Date: Thu, 20 Feb 2020 10:12:19 -0500 Subject: [PATCH 011/113] [Canvas] Adds argument to open all links in new tab within markdown element (#57017) * Adds toggle to open links in new tab within markdown element * Updating test for markdown function * Switch to using Markdown Component * Update ui.ts Update to change toggle verbiage per Ryan's request, and reuse the Kibana Markdown per Corey's help and recommendation. Still working on updating the styles (consulting Ryan) * Update toggle.js Update to prevent text for "Open links in new tab from wrapping" - the example from the horizontal bar chart is quite differently, and reads from "axisConfig" - when I changed the argType to "axisConfig", the layout was the same, but I'll need some input on which specific styles to add to the "ToggleArgInput" - I think this is where the style updates need to occur to get the toggle to stay on the same line, but may be wrong. * Update ui.ts Update to original message string per Ryan's feedback, now that there is no wrapping * Update to UI styles per Ryan's feedback * updating message per Ryan's request * Update ui.ts update to fix internationalization issues Co-authored-by: Corey Robertson --- .../functions/browser/markdown.test.js | 29 ++++++++++++++++- .../functions/browser/markdown.ts | 8 +++++ .../renderers/markdown/index.js | 14 +++------ .../canvas_plugin_src/uis/arguments/toggle.js | 31 +++++++++++-------- .../canvas_plugin_src/uis/views/markdown.js | 11 +++++++ .../canvas/i18n/functions/dict/markdown.ts | 4 +++ x-pack/legacy/plugins/canvas/i18n/ui.ts | 12 +++++++ 7 files changed, 86 insertions(+), 23 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js index f9912c270a46a..27ea290fb4dcc 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.test.js @@ -19,7 +19,7 @@ describe('markdown', () => { }); describe('args', () => { - describe('expression', () => { + describe('content', () => { it('sets the content to all strings in expression concatenated', () => { const result = fn(null, { content: ['# this ', 'is ', 'some ', 'markdown'], @@ -54,5 +54,32 @@ describe('markdown', () => { // TODO: write test when using an instance of the interpreter // it("defaults to the expression '{font}'", () => {}); }); + describe('openLinksInNewTab', () => { + it('sets the value of openLinksInNewTab to true ', () => { + const result = fn(null, { + content: ['some ', 'markdown'], + openLinksInNewTab: true, + }); + + expect(result.value).toHaveProperty('openLinksInNewTab', true); + }); + + it('sets the value of openLinksInNewTab to false ', () => { + const result = fn(null, { + content: ['some ', 'markdown'], + openLinksInNewTab: false, + }); + + expect(result.value).toHaveProperty('openLinksInNewTab', false); + }); + + it('defaults the value of openLinksInNewTab to false ', () => { + const result = fn(null, { + content: ['some ', 'markdown'], + }); + + expect(result.value).toHaveProperty('openLinksInNewTab', false); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts index 95859feeed5f3..e94b9f201a174 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts @@ -19,11 +19,13 @@ type Context = Datatable | null; interface Arguments { content: string[]; font: Style; + openLinksInNewTab: boolean; } interface Return { content: string; font: Style; + openLinksInNewTab: boolean; } export function markdown(): ExpressionFunctionDefinition< @@ -53,6 +55,11 @@ export function markdown(): ExpressionFunctionDefinition< help: argHelp.font, default: '{font}', }, + openLinksInNewTab: { + types: ['boolean'], + help: argHelp.openLinksInNewTab, + default: false, + }, }, fn: (input, args) => { const compileFunctions = args.content.map(str => @@ -71,6 +78,7 @@ export function markdown(): ExpressionFunctionDefinition< value: { content: compileFunctions.map(fn => fn(ctx)).join(''), font: args.font, + openLinksInNewTab: args.openLinksInNewTab, }, }; }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js index 82c63d5e7d529..c1bfd7c99ac41 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/markdown/index.js @@ -6,33 +6,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import Markdown from 'markdown-it'; import { RendererStrings } from '../../../i18n'; +import { Markdown } from '../../../../../../../src/legacy/core_plugins/kibana_react/public'; const { markdown: strings } = RendererStrings; -const md = new Markdown(); - export const markdown = () => ({ name: 'markdown', displayName: strings.getDisplayName(), help: strings.getHelpDescription(), reuseDomNode: true, render(domNode, config, handlers) { - const html = { __html: md.render(String(config.content)) }; const fontStyle = config.font ? config.font.spec : {}; - /* eslint-disable react/no-danger */ ReactDOM.render( -
, domNode, () => handlers.done() ); - /* eslint-enable */ handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index bcad4678e0b6a..299f96ff1b4e8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -19,18 +19,21 @@ const ToggleArgInput = ({ onValueChange, argValue, argId, renderError, typeInsta return null; } return ( - - - +
+ + + +
); }; @@ -38,6 +41,8 @@ ToggleArgInput.propTypes = { onValueChange: PropTypes.func.isRequired, argValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.object]).isRequired, argId: PropTypes.string.isRequired, + labelValue: PropTypes.string, + showLabelValue: PropTypes.bool, renderError: PropTypes.func.isRequired, }; @@ -45,6 +50,6 @@ export const toggle = () => ({ name: 'toggle', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(ToggleArgInput), + template: templateFromReactComponent(ToggleArgInput), default: 'false', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js index 1c46bc6dd57c2..edae739ee0d3d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/markdown.js @@ -29,5 +29,16 @@ export const markdown = () => ({ name: 'font', argType: 'font', }, + { + name: 'openLinksInNewTab', + displayName: strings.getOpenLinksInNewTabDisplayName(), + help: strings.getOpenLinksInNewTabHelp(), + label: strings.getOpenLinksInNewTabLabelName(), + argType: 'toggle', + default: false, + options: { + labelValue: 'Open all links in a new tab', + }, + }, ], }); diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts index d5271e14436e2..aa2845ba4ec3a 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/markdown.ts @@ -37,5 +37,9 @@ export const help: FunctionHelp> = { fontWeight: 'font-weight', }, }), + openLinksInNewTab: i18n.translate('xpack.canvas.functions.markdown.args.openLinkHelpText', { + defaultMessage: + 'A true/false value for opening links in a new tab. Default value is false. Setting to true will open all links in a new tab.', + }), }, }; diff --git a/x-pack/legacy/plugins/canvas/i18n/ui.ts b/x-pack/legacy/plugins/canvas/i18n/ui.ts index 323a6c97fd967..5b94cb0435b31 100644 --- a/x-pack/legacy/plugins/canvas/i18n/ui.ts +++ b/x-pack/legacy/plugins/canvas/i18n/ui.ts @@ -628,6 +628,18 @@ export const ViewStrings = { markdown: MARKDOWN, }, }), + getOpenLinksInNewTabDisplayName: () => + i18n.translate('xpack.canvas.uis.views.openLinksInNewTabTitle', { + defaultMessage: 'Markdown link settings', + }), + getOpenLinksInNewTabLabelName: () => + i18n.translate('xpack.canvas.uis.views.openLinksInNewTabLabel', { + defaultMessage: 'Open all links in a new tab', + }), + getOpenLinksInNewTabHelp: () => + i18n.translate('xpack.canvas.uis.views.openLinksInNewTabHelpLabel', { + defaultMessage: 'Set links to open in new tab', + }), }, Metric: { getDisplayName: () => From 602aca338780c20269e59d113d5d63f6840b2797 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 20 Feb 2020 10:39:43 -0500 Subject: [PATCH 012/113] [SIEM] Detections container/rules unit tests (#58055) * add unit test for rules api * add unit test for useFetchIndexPatterns * fix useFetchIndexPatterns and add unit test for usePersistRule * add more unit test for container/rules * review Co-authored-by: Elastic Machine --- .../detection_engine/rules/__mocks__/api.ts | 81 ++ .../detection_engine/rules/api.test.ts | 860 ++++++++++++++++++ .../rules/fetch_index_patterns.test.tsx | 460 ++++++++++ .../rules/fetch_index_patterns.tsx | 2 +- .../containers/detection_engine/rules/mock.ts | 139 +++ .../rules/persist_rule.test.tsx | 44 + .../detection_engine/rules/persist_rule.tsx | 5 +- .../detection_engine/rules/types.ts | 2 +- .../rules/use_pre_packaged_rules.test.tsx | 267 ++++++ .../rules/use_pre_packaged_rules.tsx | 8 +- .../detection_engine/rules/use_rule.test.tsx | 84 ++ .../detection_engine/rules/use_rule.tsx | 5 +- .../rules/use_rule_status.test.tsx | 65 ++ .../rules/use_rule_status.tsx | 5 +- .../detection_engine/rules/use_rules.test.tsx | 216 +++++ .../detection_engine/rules/use_rules.tsx | 7 +- .../detection_engine/rules/use_tags.test.tsx | 29 + .../detection_engine/rules/use_tags.tsx | 4 +- .../plugins/siem/public/mock/hook_wrapper.tsx | 1 + 19 files changed, 2267 insertions(+), 17 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts new file mode 100644 index 0000000000000..9f37f3fecd508 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts @@ -0,0 +1,81 @@ +/* + * 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 { + AddRulesProps, + NewRule, + PrePackagedRulesStatusResponse, + BasicFetchProps, + RuleStatusResponse, + Rule, + FetchRuleProps, + FetchRulesResponse, + FetchRulesProps, +} from '../types'; +import { ruleMock, savedRuleMock, rulesMock } from '../mock'; + +export const addRule = async ({ rule, signal }: AddRulesProps): Promise => + Promise.resolve(ruleMock); + +export const getPrePackagedRulesStatus = async ({ + signal, +}: { + signal: AbortSignal; +}): Promise => + Promise.resolve({ + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 0, + }); + +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => + Promise.resolve(true); + +export const getRuleStatusById = async ({ + id, + signal, +}: { + id: string; + signal: AbortSignal; +}): Promise => + Promise.resolve({ + myOwnRuleID: { + current_status: { + alert_id: 'alertId', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + status: 'succeeded', + last_failure_at: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_failure_message: null, + last_success_message: 'it is a success', + }, + failures: [], + }, + }); + +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => + Promise.resolve(savedRuleMock); + +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + signal, +}: FetchRulesProps): Promise => Promise.resolve(rulesMock); + +export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => + Promise.resolve(['elastic', 'love', 'quality', 'code']); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts new file mode 100644 index 0000000000000..b348678e789f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts @@ -0,0 +1,860 @@ +/* + * 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 { KibanaServices } from '../../../lib/kibana'; +import { + addRule, + fetchRules, + fetchRuleById, + enableRules, + deleteRules, + duplicateRules, + createPrepackagedRules, + importRules, + exportRules, + getRuleStatusById, + fetchTags, + getPrePackagedRulesStatus, +} from './api'; +import { ruleMock, rulesMock } from './mock'; +import { ToasterErrors } from '../../../components/ml/api/throw_if_not_ok'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../lib/kibana'); + +const mockfetchSuccess = (body: unknown, fetchMock?: jest.Mock) => { + if (fetchMock) { + mockKibanaServices.mockImplementation(() => ({ + http: { + fetch: fetchMock, + }, + })); + } else { + mockKibanaServices.mockImplementation(() => ({ + http: { + fetch: () => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body, + }), + }, + })); + } +}; + +const mockfetchError = () => { + mockKibanaServices.mockImplementation(() => ({ + http: { + fetch: () => ({ + response: { + ok: false, + text: () => + JSON.stringify({ + message: 'super mega error, it is not that bad', + }), + }, + body: null, + }), + }, + })); +}; + +describe('Detections Rules API', () => { + const fetchMock = jest.fn(); + describe('addRule', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body', async () => { + mockfetchSuccess(null, fetchMock); + + await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + asResponse: true, + body: + '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[]}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + test('happy path', async () => { + mockfetchSuccess(ruleMock); + const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + expect(ruleResp).toEqual(ruleMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('fetchRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: rulesMock, + })); + }); + + test('check parameter url, query without any options', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with a filter', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: 'hello world', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.name: hello world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with showCustomRules', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: false, + tags: [], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.tags: "__internal_immutable:false"', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with showElasticRules', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: true, + tags: [], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.tags: "__internal_immutable:true"', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with tags', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: ['hello', 'world'], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with all options', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRules({ + filterOptions: { + filter: 'ruleName', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['hello', 'world'], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + asResponse: true, + method: 'GET', + query: { + filter: + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + mockfetchSuccess(rulesMock); + + const rulesResp = await fetchRules({ signal: abortCtrl.signal }); + expect(rulesResp).toEqual(rulesMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await fetchRules({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('fetchRuleById', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, query', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + asResponse: true, + query: { + id: 'mySuperRuleId', + }, + method: 'GET', + signal: abortCtrl.signal, + }); + }); + test('happy path', async () => { + mockfetchSuccess(ruleMock); + const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(ruleResp).toEqual(ruleMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('enableRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body when enabling rules', async () => { + mockfetchSuccess(null, fetchMock); + + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { + asResponse: true, + body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]', + method: 'PATCH', + }); + }); + test('check parameter url, body when disabling rules', async () => { + mockfetchSuccess(null, fetchMock); + + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { + asResponse: true, + body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]', + method: 'PATCH', + }); + }); + test('happy path', async () => { + mockfetchSuccess(rulesMock.data); + const ruleResp = await enableRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + enabled: true, + }); + expect(ruleResp).toEqual(rulesMock.data); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('deleteRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body when deleting rules', async () => { + mockfetchSuccess(null, fetchMock); + + await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', { + asResponse: true, + body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]', + method: 'DELETE', + }); + }); + test('happy path', async () => { + mockfetchSuccess(ruleMock); + const ruleResp = await deleteRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + }); + expect(ruleResp).toEqual(ruleMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('duplicateRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body when duplicating rules', async () => { + mockfetchSuccess(null, fetchMock); + + await duplicateRules({ rules: rulesMock.data }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { + asResponse: true, + body: + '[{"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1},{"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1}]', + method: 'POST', + }); + }); + test('happy path', async () => { + mockfetchSuccess(rulesMock.data); + const ruleResp = await duplicateRules({ rules: rulesMock.data }); + expect(ruleResp).toEqual(rulesMock.data); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await duplicateRules({ rules: rulesMock.data }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('createPrepackagedRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url when creating pre-packaged rules', async () => { + mockfetchSuccess(null, fetchMock); + + await createPrepackagedRules({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', { + asResponse: true, + signal: abortCtrl.signal, + method: 'PUT', + }); + }); + test('happy path', async () => { + mockfetchSuccess(true); + const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); + expect(resp).toEqual(true); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await createPrepackagedRules({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('importRules', () => { + const fileToImport: File = { + lastModified: 33, + name: 'fileToImport', + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as File; + const formData = new FormData(); + formData.append('file', fileToImport); + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, body and query when importing rules', async () => { + mockfetchSuccess(null, fetchMock); + await importRules({ fileToImport, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: formData, + headers: { + 'Content-Type': undefined, + }, + query: { + overwrite: false, + }, + }); + }); + + test('check parameter url, body and query when importing rules with overwrite', async () => { + mockfetchSuccess(null, fetchMock); + + await importRules({ fileToImport, overwrite: true, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: formData, + headers: { + 'Content-Type': undefined, + }, + query: { + overwrite: true, + }, + }); + }); + + test('happy path', async () => { + mockfetchSuccess({ + success: true, + success_count: 33, + errors: [], + }); + const resp = await importRules({ fileToImport, signal: abortCtrl.signal }); + expect(resp).toEqual({ + success: true, + success_count: 33, + errors: [], + }); + }); + + test('unhappy path', async () => { + mockfetchError(); + try { + await importRules({ fileToImport, signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('exportRules', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + + test('check parameter url, body and query when exporting rules', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: false, + file_name: 'rules_export.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with excludeExportDetails', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + excludeExportDetails: true, + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: true, + file_name: 'rules_export.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with fileName', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + filename: 'myFileName.ndjson', + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: false, + file_name: 'myFileName.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with all options', async () => { + mockfetchSuccess(null, fetchMock); + await exportRules({ + excludeExportDetails: true, + filename: 'myFileName.ndjson', + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + asResponse: true, + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: true, + file_name: 'myFileName.ndjson', + }, + }); + }); + + test('happy path', async () => { + const blob: Blob = { + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as Blob; + mockfetchSuccess(blob); + const resp = await exportRules({ + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(resp).toEqual(blob); + }); + + test('unhappy path', async () => { + mockfetchError(); + try { + await exportRules({ + ruleIds: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('getRuleStatusById', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url, query', async () => { + mockfetchSuccess(null, fetchMock); + + await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { + asResponse: true, + query: { + ids: '["mySuperRuleId"]', + }, + method: 'GET', + signal: abortCtrl.signal, + }); + }); + test('happy path', async () => { + const statusMock = { + myRule: { + current_status: { + alert_id: 'alertId', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + status: 'succeeded', + last_failure_at: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_failure_message: null, + last_success_message: 'it is a success', + }, + failures: [], + }, + }; + mockfetchSuccess(statusMock); + const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(ruleResp).toEqual(statusMock); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('fetchTags', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url when fetching tags', async () => { + mockfetchSuccess(null, fetchMock); + + await fetchTags({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', { + asResponse: true, + signal: abortCtrl.signal, + method: 'GET', + }); + }); + test('happy path', async () => { + mockfetchSuccess(['hello', 'tags']); + const resp = await fetchTags({ signal: abortCtrl.signal }); + expect(resp).toEqual(['hello', 'tags']); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await fetchTags({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); + + describe('getPrePackagedRulesStatus', () => { + beforeEach(() => { + mockKibanaServices.mockClear(); + fetchMock.mockClear(); + fetchMock.mockImplementation(() => ({ + response: { + ok: true, + message: 'success', + text: 'success', + }, + body: ruleMock, + })); + }); + test('check parameter url when fetching tags', async () => { + mockfetchSuccess(null, fetchMock); + + await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged/_status', { + asResponse: true, + signal: abortCtrl.signal, + method: 'GET', + }); + }); + test('happy path', async () => { + const prePackagesRulesStatus = { + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 2, + }; + mockfetchSuccess(prePackagesRulesStatus); + const resp = await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + expect(resp).toEqual(prePackagesRulesStatus); + }); + test('unhappy path', async () => { + mockfetchError(); + try { + await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + } catch (exp) { + expect(exp).toBeInstanceOf(ToasterErrors); + expect(exp.message).toEqual('super mega error, it is not that bad'); + } + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx new file mode 100644 index 0000000000000..cad78ac565903 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -0,0 +1,460 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { useApolloClient } from '../../../utils/apollo_context'; +import { mocksSource } from '../../source/mock'; + +import { useFetchIndexPatterns, Return } from './fetch_index_patterns'; + +const mockUseApolloClient = useApolloClient as jest.Mock; +jest.mock('../../../utils/apollo_context'); + +describe('useFetchIndexPatterns', () => { + beforeEach(() => { + mockUseApolloClient.mockClear(); + }); + test('happy path', async () => { + await act(async () => { + mockUseApolloClient.mockImplementation(() => ({ + query: () => Promise.resolve(mocksSource[0].result), + })); + const { result, waitForNextUpdate } = renderHook(() => + useFetchIndexPatterns(defaultIndexPattern) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual([ + { + browserFields: { + base: { + fields: { + '@timestamp': { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + }, + }, + agent: { + fields: { + 'agent.ephemeral_id': { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'agent.hostname': { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'agent.id': { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'agent.name': { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + auditd: { + fields: { + 'auditd.data.a0': { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'auditd.data.a1': { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'auditd.data.a2': { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + client: { + fields: { + 'client.address': { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'client.bytes': { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + 'client.domain': { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'client.geo.country_iso_code': { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'cloud.availability_zone': { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + container: { + fields: { + 'container.id': { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'container.image.name': { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'container.image.tag': { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + }, + }, + destination: { + fields: { + 'destination.address': { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'destination.bytes': { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + 'destination.domain': { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + 'destination.ip': { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + 'destination.port': { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + }, + }, + source: { + fields: { + 'source.ip': { + aggregatable: true, + category: 'source', + description: + 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + 'source.port': { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + }, + }, + event: { + fields: { + 'event.end': { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + name: 'event.end', + searchable: true, + type: 'date', + }, + }, + }, + }, + isLoading: false, + indices: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + indicesExists: true, + indexPatterns: { + fields: [ + { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, + { name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.id', searchable: true, type: 'string', aggregatable: true }, + { name: 'agent.name', searchable: true, type: 'string', aggregatable: true }, + { name: 'auditd.data.a0', searchable: true, type: 'string', aggregatable: true }, + { name: 'auditd.data.a1', searchable: true, type: 'string', aggregatable: true }, + { name: 'auditd.data.a2', searchable: true, type: 'string', aggregatable: true }, + { name: 'client.address', searchable: true, type: 'string', aggregatable: true }, + { name: 'client.bytes', searchable: true, type: 'number', aggregatable: true }, + { name: 'client.domain', searchable: true, type: 'string', aggregatable: true }, + { + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { name: 'cloud.account.id', searchable: true, type: 'string', aggregatable: true }, + { + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { name: 'container.id', searchable: true, type: 'string', aggregatable: true }, + { + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { name: 'container.image.tag', searchable: true, type: 'string', aggregatable: true }, + { name: 'destination.address', searchable: true, type: 'string', aggregatable: true }, + { name: 'destination.bytes', searchable: true, type: 'number', aggregatable: true }, + { name: 'destination.domain', searchable: true, type: 'string', aggregatable: true }, + { name: 'destination.ip', searchable: true, type: 'ip', aggregatable: true }, + { name: 'destination.port', searchable: true, type: 'long', aggregatable: true }, + { name: 'source.ip', searchable: true, type: 'ip', aggregatable: true }, + { name: 'source.port', searchable: true, type: 'long', aggregatable: true }, + { name: 'event.end', searchable: true, type: 'date', aggregatable: true }, + ], + title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + }, + }, + result.current[1], + ]); + }); + }); + + test('unhappy path', async () => { + await act(async () => { + mockUseApolloClient.mockImplementation(() => ({ + query: () => Promise.reject(new Error('Something went wrong')), + })); + const { result, waitForNextUpdate } = renderHook(() => + useFetchIndexPatterns(defaultIndexPattern) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual([ + { + browserFields: {}, + indexPatterns: { + fields: [], + title: '', + }, + indices: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + indicesExists: false, + isLoading: false, + }, + result.current[1], + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index d376a1d6ad178..b7ad41b8ba1bb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -29,7 +29,7 @@ interface FetchIndexPatternReturn { indexPatterns: IIndexPattern; } -type Return = [FetchIndexPatternReturn, Dispatch>]; +export type Return = [FetchIndexPatternReturn, Dispatch>]; export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { const apolloClient = useApolloClient(); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts new file mode 100644 index 0000000000000..51526c0ab9949 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts @@ -0,0 +1,139 @@ +/* + * 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 { NewRule, FetchRulesResponse, Rule } from './types'; + +export const ruleMock: NewRule = { + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + language: 'kuery', + risk_score: 75, + name: 'Test rule', + query: "user.email: 'root@elastic.co'", + references: [], + severity: 'high', + tags: ['APM'], + to: 'now', + type: 'query', + threat: [], +}; + +export const savedRuleMock: Rule = { + created_at: 'mm/dd/yyyyTHH:MM:sssz', + created_by: 'mockUser', + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + id: '12345678987654321', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + immutable: false, + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + language: 'kuery', + risk_score: 75, + name: 'Test rule', + max_signals: 100, + query: "user.email: 'root@elastic.co'", + references: [], + severity: 'high', + tags: ['APM'], + to: 'now', + type: 'query', + threat: [], + updated_at: 'mm/dd/yyyyTHH:MM:sssz', + updated_by: 'mockUser', +}; + +export const rulesMock: FetchRulesResponse = { + page: 1, + perPage: 2, + total: 2, + data: [ + { + created_at: '2020-02-14T19:49:28.178Z', + updated_at: '2020-02-14T19:49:28.320Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + from: 'now-660s', + id: '80c59768-8e1f-400e-908e-7b25c4ce29c3', + immutable: true, + index: ['endgame-*'], + interval: '10m', + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 73, + name: 'Credential Dumping - Detected - Elastic Endpoint', + query: + 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', + filters: [], + references: [], + severity: 'high', + updated_by: 'elastic', + tags: ['Elastic', 'Endpoint'], + to: 'now', + type: 'query', + threat: [], + version: 1, + }, + { + created_at: '2020-02-14T19:49:28.189Z', + updated_at: '2020-02-14T19:49:28.326Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + from: 'now-660s', + id: '2e846086-bd64-4dbc-9c56-42b46b5b2c8c', + immutable: true, + index: ['endgame-*'], + interval: '10m', + rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 47, + name: 'Adversary Behavior - Detected - Elastic Endpoint', + query: 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', + filters: [], + references: [], + severity: 'medium', + updated_by: 'elastic', + tags: ['Elastic', 'Endpoint'], + to: 'now', + type: 'query', + threat: [], + version: 1, + }, + ], +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx new file mode 100644 index 0000000000000..1bf21623992e6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { usePersistRule, ReturnPersistRule } from './persist_rule'; +import { ruleMock } from './mock'; + +jest.mock('./api'); + +describe('usePersistRule', () => { + test('init', async () => { + const { result } = renderHook(() => usePersistRule()); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + }); + + test('saving rule with isLoading === true', async () => { + await act(async () => { + const { result, rerender, waitForNextUpdate } = renderHook(() => + usePersistRule() + ); + await waitForNextUpdate(); + result.current[1](ruleMock); + rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); + }); + }); + + test('saved rule with isSaved === true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePersistRule() + ); + await waitForNextUpdate(); + result.current[1](ruleMock); + await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx index ea03c34ec31ba..e720a1e70f153 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx @@ -18,9 +18,9 @@ interface PersistRuleReturn { isSaved: boolean; } -type Return = [PersistRuleReturn, Dispatch]; +export type ReturnPersistRule = [PersistRuleReturn, Dispatch]; -export const usePersistRule = (): Return => { +export const usePersistRule = (): ReturnPersistRule => { const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -35,7 +35,6 @@ export const usePersistRule = (): Return => { try { setIsLoading(true); await persistRule({ rule, signal: abortCtrl.signal }); - if (isSubscribed) { setIsSaved(true); } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 0aaffb7b86b28..ff49bb8a8c3a2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -64,7 +64,6 @@ export const RuleSchema = t.intersection([ language: t.string, name: t.string, max_signals: t.number, - meta: MetaRule, query: t.string, references: t.array(t.string), risk_score: t.number, @@ -80,6 +79,7 @@ export const RuleSchema = t.intersection([ t.partial({ last_failure_at: t.string, last_failure_message: t.string, + meta: MetaRule, output_index: t.string, saved_id: t.string, status: t.string, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx new file mode 100644 index 0000000000000..426a1ab9238dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -0,0 +1,267 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { ReturnPrePackagedRules, usePrePackagedRules } from './use_pre_packaged_rules'; +import * as api from './api'; + +jest.mock('./api'); + +describe('usePersistRule', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: null, + hasIndexWrite: null, + hasManageApiKey: null, + isAuthenticated: null, + hasEncryptionKey: null, + isSignalIndexExists: null, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + createPrePackagedRules: null, + loading: true, + loadingCreatePrePackagedRules: false, + refetchPrePackagedRulesStatus: null, + rulesCustomInstalled: null, + rulesInstalled: null, + rulesNotInstalled: null, + rulesNotUpdated: null, + }); + }); + }); + + test('fetch getPrePackagedRulesStatus', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: null, + hasIndexWrite: null, + hasManageApiKey: null, + isAuthenticated: null, + hasEncryptionKey: null, + isSignalIndexExists: null, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + createPrePackagedRules: result.current.createPrePackagedRules, + loading: false, + loadingCreatePrePackagedRules: false, + refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus, + rulesCustomInstalled: 33, + rulesInstalled: 12, + rulesNotInstalled: 0, + rulesNotUpdated: 0, + }); + }); + }); + + test('happy path to createPrePackagedRules', async () => { + const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(true); + expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); + expect(result.current).toEqual({ + createPrePackagedRules: result.current.createPrePackagedRules, + loading: false, + loadingCreatePrePackagedRules: false, + refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus, + rulesCustomInstalled: 33, + rulesInstalled: 12, + rulesNotInstalled: 0, + rulesNotUpdated: 0, + }); + }); + }); + + test('unhappy path to createPrePackagedRules', async () => { + const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); + spyOnCreatePrepackagedRules.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); + }); + }); + + test('can NOT createPrePackagedRules because canUserCrud === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: false, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because hasIndexWrite === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: false, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because hasManageApiKey === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: false, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because isAuthenticated === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: false, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because hasEncryptionKey === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: false, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); + + test('can NOT createPrePackagedRules because isSignalIndexExists === false', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + hasManageApiKey: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: false, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + let resp = null; + if (result.current.createPrePackagedRules) { + resp = await result.current.createPrePackagedRules(); + } + expect(resp).toEqual(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index d77d6283692a2..04d7e3ef67da4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; type Func = () => void; export type CreatePreBuiltRules = () => Promise; -interface Return { +export interface ReturnPrePackagedRules { createPrePackagedRules: null | CreatePreBuiltRules; loading: boolean; loadingCreatePrePackagedRules: boolean; @@ -50,10 +50,10 @@ export const usePrePackagedRules = ({ isAuthenticated, hasEncryptionKey, isSignalIndexExists, -}: UsePrePackagedRuleProps): Return => { +}: UsePrePackagedRuleProps): ReturnPrePackagedRules => { const [rulesStatus, setRuleStatus] = useState< Pick< - Return, + ReturnPrePackagedRules, | 'createPrePackagedRules' | 'refetchPrePackagedRulesStatus' | 'rulesCustomInstalled' @@ -167,6 +167,8 @@ export const usePrePackagedRules = ({ }, 300); timeoutId = reFetch(); } + } else { + resolve(false); } } catch (error) { if (isSubscribed) { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx new file mode 100644 index 0000000000000..e0bf2c4907370 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useRule, ReturnRule } from './use_rule'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useRule', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRule('myOwnRuleID') + ); + await waitForNextUpdate(); + expect(result.current).toEqual([true, null]); + }); + }); + + test('fetch rule', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRule('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + created_at: 'mm/dd/yyyyTHH:MM:sssz', + created_by: 'mockUser', + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + id: '12345678987654321', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'Test rule', + max_signals: 100, + query: "user.email: 'root@elastic.co'", + references: [], + risk_score: 75, + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + severity: 'high', + tags: ['APM'], + threat: [], + to: 'now', + type: 'query', + updated_at: 'mm/dd/yyyyTHH:MM:sssz', + updated_by: 'mockUser', + }, + ]); + }); + }); + + test('fetch a new rule', async () => { + const spyOnfetchRuleById = jest.spyOn(api, 'fetchRuleById'); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook(id => useRule(id), { + initialProps: 'myOwnRuleID', + }); + await waitForNextUpdate(); + await waitForNextUpdate(); + rerender('newRuleId'); + await waitForNextUpdate(); + expect(spyOnfetchRuleById).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx index 22ba86cd09f74..ab08bd39688ce 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx @@ -12,7 +12,7 @@ import { fetchRuleById } from './api'; import * as i18n from './translations'; import { Rule } from './types'; -type Return = [boolean, Rule | null]; +export type ReturnRule = [boolean, Rule | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -20,7 +20,7 @@ type Return = [boolean, Rule | null]; * @param id desired Rule ID's (not rule_id) * */ -export const useRule = (id: string | undefined): Return => { +export const useRule = (id: string | undefined): ReturnRule => { const [rule, setRule] = useState(null); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -36,7 +36,6 @@ export const useRule = (id: string | undefined): Return => { id: idToFetch, signal: abortCtrl.signal, }); - if (isSubscribed) { setRule(ruleResponse); } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx new file mode 100644 index 0000000000000..25011adcfe98b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useRuleStatus, ReturnRuleStatus } from './use_rule_status'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useRuleStatus', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + expect(result.current).toEqual([true, null, null]); + }); + }); + + test('fetch rule status', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + current_status: { + alert_id: 'alertId', + last_failure_at: null, + last_failure_message: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_success_message: 'it is a success', + status: 'succeeded', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + }, + failures: [], + }, + result.current[2], + ]); + }); + }); + + test('re-fetch rule status', async () => { + const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current[2]) { + result.current[2]('myOwnRuleID'); + } + await waitForNextUpdate(); + expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index 466c2cddac97d..fcf95ac061ba3 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; import { RuleStatus } from './types'; type Func = (ruleId: string) => void; -type Return = [boolean, RuleStatus | null, Func | null]; +export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -21,7 +21,7 @@ type Return = [boolean, RuleStatus | null, Func | null]; * @param id desired Rule ID's (not rule_id) * */ -export const useRuleStatus = (id: string | undefined | null): Return => { +export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus => { const [ruleStatus, setRuleStatus] = useState(null); const fetchRuleStatus = useRef(null); const [loading, setLoading] = useState(true); @@ -34,6 +34,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => { const fetchData = async (idToFetch: string) => { try { setLoading(true); + const ruleStatusResponse = await getRuleStatusById({ id: idToFetch, signal: abortCtrl.signal, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx new file mode 100644 index 0000000000000..b369d3a50730d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx @@ -0,0 +1,216 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useRules, ReturnRules } from './use_rules'; +import * as api from './api'; +import { PaginationOptions, FilterOptions } from '.'; + +jest.mock('./api'); + +describe('useRules', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(props => + useRules( + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + } + ) + ); + await waitForNextUpdate(); + expect(result.current).toEqual([ + true, + { + data: [], + page: 1, + perPage: 20, + total: 0, + }, + null, + ]); + }); + }); + + test('fetch rules', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(() => + useRules( + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + } + ) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + data: [ + { + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: '80c59768-8e1f-400e-908e-7b25c4ce29c3', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: + 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + { + created_at: '2020-02-14T19:49:28.189Z', + created_by: 'elastic', + description: + 'Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: '2e846086-bd64-4dbc-9c56-42b46b5b2c8c', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Adversary Behavior - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: + 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', + references: [], + risk_score: 47, + rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69', + severity: 'medium', + tags: ['Elastic', 'Endpoint'], + threat: [], + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.326Z', + updated_by: 'elastic', + version: 1, + }, + ], + page: 1, + perPage: 2, + total: 2, + }, + result.current[2], + ]); + }); + }); + + test('re-fetch rules', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(id => + useRules( + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + } + ) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current[2]) { + result.current[2](); + } + await waitForNextUpdate(); + expect(spyOnfetchRules).toHaveBeenCalledTimes(2); + }); + }); + + test('fetch rules if props changes', async () => { + const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); + await act(async () => { + const { rerender, waitForNextUpdate } = renderHook< + [PaginationOptions, FilterOptions], + ReturnRules + >(args => useRules(args[0], args[1]), { + initialProps: [ + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + }, + ], + }); + await waitForNextUpdate(); + await waitForNextUpdate(); + rerender([ + { + page: 1, + perPage: 10, + total: 100, + }, + { + filter: 'hello world', + sortField: 'created_at', + sortOrder: 'desc', + }, + ]); + await waitForNextUpdate(); + expect(spyOnfetchRules).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index af6e437255acd..301a68dc6f445 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -13,7 +13,7 @@ import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; type Func = () => void; -type Return = [boolean, FetchRulesResponse, Func | null]; +export type ReturnRules = [boolean, FetchRulesResponse, Func | null]; /** * Hook for using the list of Rules from the Detection Engine API @@ -21,7 +21,10 @@ type Return = [boolean, FetchRulesResponse, Func | null]; * @param pagination desired pagination options (e.g. page/perPage) * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) */ -export const useRules = (pagination: PaginationOptions, filterOptions: FilterOptions): Return => { +export const useRules = ( + pagination: PaginationOptions, + filterOptions: FilterOptions +): ReturnRules => { const [rules, setRules] = useState({ page: 1, perPage: 20, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx new file mode 100644 index 0000000000000..4a796efa5b0cb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useTags, ReturnTags } from './use_tags'; + +jest.mock('./api'); + +describe('useTags', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useTags()); + await waitForNextUpdate(); + expect(result.current).toEqual([true, []]); + }); + }); + + test('fetch tags', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([false, ['elastic', 'love', 'quality', 'code']]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx index 1c961d530422a..196d4b1420561 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx @@ -10,13 +10,13 @@ import { fetchTags } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -type Return = [boolean, string[]]; +export type ReturnTags = [boolean, string[]]; /** * Hook for using the list of Tags from the Detection Engine API * */ -export const useTags = (): Return => { +export const useTags = (): ReturnTags => { const [tags, setTags] = useState([]); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); diff --git a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx b/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx index 292ddc036dcaf..70c76de01e95a 100644 --- a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx @@ -12,6 +12,7 @@ interface HookWrapperProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any hookProps?: any; } + export const HookWrapper = ({ hook, hookProps }: HookWrapperProps) => { const myHook = hook ? (hookProps ? hook(hookProps) : hook()) : null; return
{JSON.stringify(myHook)}
; From 783663fa523b70329c3094e2703613f9a68ebfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 20 Feb 2020 10:46:28 -0500 Subject: [PATCH 013/113] Skip flaky alert details test (#58120) * Skip flaky test * Skip suite * Skip suite --- .../functional_with_es_ssl/apps/triggers_actions_ui/details.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 3db4731f0adfb..86fc3d6cd6a6c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -18,7 +18,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alerting = getService('alerting'); const retry = getService('retry'); - describe('Alert Details', function() { + // FLAKY: https://github.com/elastic/kibana/issues/57426 + describe.skip('Alert Details', function() { describe('Header', function() { const testRunUuid = uuid.v4(); before(async () => { From 289b2fa611503ab198339f2bae26966a1aeffdfa Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Thu, 20 Feb 2020 08:59:44 -0700 Subject: [PATCH 014/113] Clarify Precision function in Timelion Kibana (#58031) * Closes issue 26100 Co-authored-by: Elastic Machine --- src/plugins/timelion/server/series_functions/precision.js | 4 ++-- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plugins/timelion/server/series_functions/precision.js b/src/plugins/timelion/server/series_functions/precision.js index 756fb067f2335..71c15fdd46fdd 100644 --- a/src/plugins/timelion/server/series_functions/precision.js +++ b/src/plugins/timelion/server/series_functions/precision.js @@ -32,12 +32,12 @@ export default new Chainable('precision', { name: 'precision', types: ['number'], help: i18n.translate('timelion.help.functions.precision.args.precisionHelpText', { - defaultMessage: 'Number of digits to round each value to', + defaultMessage: 'The number of digits to truncate each value to', }), }, ], help: i18n.translate('timelion.help.functions.precisionHelpText', { - defaultMessage: 'number of digits to round the decimal portion of the value to', + defaultMessage: 'The number of digits to truncate the decimal portion of the value to', }), fn: async function precisionFn(args) { await alter(args, function(eachSeries, precision) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47bf2ae634048..380e78cf6fa19 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2697,8 +2697,6 @@ "timelion.help.functions.points.args.symbolHelpText": "点のシンボルです。{validSymbols} の 1 つ", "timelion.help.functions.points.args.weightHelpText": "点の周りの太さです", "timelion.help.functions.pointsHelpText": "数列を点として表示します", - "timelion.help.functions.precision.args.precisionHelpText": "各値を四捨五入する桁数です", - "timelion.help.functions.precisionHelpText": "値の小数点以下の四捨五入する桁数です", "timelion.help.functions.props.args.globalHelpText": "各数列に対し、seriesList にプロップを設定します", "timelion.help.functions.propsHelpText": "数列に任意のプロパティを設定するため、自己責任で行ってください。例: {example}", "timelion.help.functions.quandl.args.codeHelpText": "プロットする Quandl コードです。これらは quandl.com に掲載されています。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a94a602e48d9b..23e822821fea4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2697,8 +2697,6 @@ "timelion.help.functions.points.args.symbolHelpText": "点符号。以下选项之一:{validSymbols}", "timelion.help.functions.points.args.weightHelpText": "围绕点的线条粗细", "timelion.help.functions.pointsHelpText": "将序列显示为点", - "timelion.help.functions.precision.args.precisionHelpText": "将每个值舍入到的小数位数", - "timelion.help.functions.precisionHelpText": "将值的小数部分舍入到的小数位数", "timelion.help.functions.props.args.globalHelpText": "在 seriesList 与每个序列上设置属性", "timelion.help.functions.propsHelpText": "在序列上可设置任意属性,但请自担风险。例如 {example}", "timelion.help.functions.quandl.args.codeHelpText": "要绘图的 quandl 代码。可以在 quandl.com 找到这些内容。", From c3001c4469e015f5942d5fdaa6a37872129f4aa4 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 20 Feb 2020 08:27:53 -0800 Subject: [PATCH 015/113] Add filter for ILM phase to Index Management (revert #45486) (#57402) --- .../extend_index_management.test.js.snap | 24 +++++++++++++ .../np_ready/extend_index_management/index.js | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index 92aaa171551a0..74c3e7408fe7c 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -63,6 +63,30 @@ Array [ ], "type": "field_value_selection", }, + Object { + "field": "ilm.phase", + "multiSelect": "or", + "name": "Lifecycle phase", + "options": Array [ + Object { + "value": "hot", + "view": "Hot", + }, + Object { + "value": "warm", + "view": "Warm", + }, + Object { + "value": "cold", + "view": "Cold", + }, + Object { + "value": "delete", + "view": "Delete", + }, + ], + "type": "field_value_selection", + }, ] `; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js index 6958c4ecce0cc..0e662b78b2c18 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js @@ -200,6 +200,40 @@ export const ilmFilterExtension = indices => { }, ], }, + { + type: 'field_value_selection', + field: 'ilm.phase', + name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', { + defaultMessage: 'Lifecycle phase', + }), + multiSelect: 'or', + options: [ + { + value: 'hot', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', { + defaultMessage: 'Hot', + }), + }, + { + value: 'warm', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', { + defaultMessage: 'Warm', + }), + }, + { + value: 'cold', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { + defaultMessage: 'Cold', + }), + }, + { + value: 'delete', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', { + defaultMessage: 'Delete', + }), + }, + ], + }, ]; } }; From dfd19596e1aa39af9d9ea6acc30316d8823b20a9 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 20 Feb 2020 10:40:35 -0600 Subject: [PATCH 016/113] Fix browser date format (#57714) * fix browser date formatter --- .../constants/base_formatters.ts | 2 -- .../common/field_formats/converters/index.ts | 1 - .../field_formats/field_formats_registry.ts | 4 +++ .../data/common/field_formats/index.ts | 1 - .../data/common/field_formats/types.ts | 1 + src/plugins/data/common/types.ts | 1 + .../data/public/field_formats/constants.ts | 23 +++++++++++++ .../field_formats/converters/date.test.ts | 0 .../field_formats/converters/date.ts | 9 +++-- .../public/field_formats/converters/index.ts | 20 +++++++++++ .../field_formats_service.test.ts | 34 +++++++++++++++++++ .../field_formats/field_formats_service.ts | 19 +++++++---- .../data/public/field_formats/index.ts | 2 ++ src/plugins/data/public/index.ts | 4 ++- src/plugins/data/public/mocks.ts | 1 + .../query_string_input.test.tsx.snap | 6 ++++ .../field_formats/converters/date_server.ts | 6 ++-- .../server/field_formats/converters/index.ts | 20 +++++++++++ .../field_formats_service.test.ts | 34 +++++++++++++++++++ .../field_formats/field_formats_service.ts | 3 +- src/plugins/data/server/index.ts | 4 --- src/test_utils/public/stub_field_formats.ts | 3 +- 22 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 src/plugins/data/public/field_formats/constants.ts rename src/plugins/data/{common => public}/field_formats/converters/date.test.ts (100%) rename src/plugins/data/{common => public}/field_formats/converters/date.ts (92%) create mode 100644 src/plugins/data/public/field_formats/converters/index.ts create mode 100644 src/plugins/data/public/field_formats/field_formats_service.test.ts rename src/plugins/data/{common => server}/field_formats/converters/date_server.ts (95%) create mode 100644 src/plugins/data/server/field_formats/converters/index.ts create mode 100644 src/plugins/data/server/field_formats/field_formats_service.test.ts diff --git a/src/plugins/data/common/field_formats/constants/base_formatters.ts b/src/plugins/data/common/field_formats/constants/base_formatters.ts index 95aedd02d16d6..6befe8cea71f5 100644 --- a/src/plugins/data/common/field_formats/constants/base_formatters.ts +++ b/src/plugins/data/common/field_formats/constants/base_formatters.ts @@ -23,7 +23,6 @@ import { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, @@ -41,7 +40,6 @@ export const baseFormatters: IFieldFormatType[] = [ BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, diff --git a/src/plugins/data/common/field_formats/converters/index.ts b/src/plugins/data/common/field_formats/converters/index.ts index f7e50539b44d8..cc9fae7fc9965 100644 --- a/src/plugins/data/common/field_formats/converters/index.ts +++ b/src/plugins/data/common/field_formats/converters/index.ts @@ -19,7 +19,6 @@ export { UrlFormat } from './url'; export { BytesFormat } from './bytes'; -export { DateFormat } from './date_server'; export { DateNanosFormat } from './date_nanos'; export { RelativeDateFormat } from './relative_date'; export { DurationFormat } from './duration'; diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 9fe9a31307b6a..9fdf1ad9c80fb 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -95,6 +95,10 @@ export class FieldFormatsRegistry { return undefined; }; + getTypeWithoutMetaParams = (formatId: FieldFormatId): IFieldFormatType | undefined => { + return this.fieldFormats.get(formatId); + }; + /** * Get the default FieldFormat type (class) for * a field type, using the format:defaultTypeMap. diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index d7858966f2620..13d3d9d73d43a 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -27,7 +27,6 @@ export { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 24aa92c67b694..0c16d9f1ac8bf 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -18,6 +18,7 @@ */ import { FieldFormat } from './field_format'; +export { FieldFormat }; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index be0d3230b3a0e..93629c3dbaf62 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -21,3 +21,4 @@ export * from './timefilter/types'; export * from './query/types'; export * from './kbn_field_types/types'; export * from './index_patterns/types'; +export { TextContextTypeConvert, IFieldFormatMetaParams } from './field_formats/types'; diff --git a/src/plugins/data/public/field_formats/constants.ts b/src/plugins/data/public/field_formats/constants.ts new file mode 100644 index 0000000000000..a5c2b4e379908 --- /dev/null +++ b/src/plugins/data/public/field_formats/constants.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 { baseFormatters } from '../../common'; +import { DateFormat } from './converters/date'; + +export const baseFormattersPublic = [DateFormat, ...baseFormatters]; diff --git a/src/plugins/data/common/field_formats/converters/date.test.ts b/src/plugins/data/public/field_formats/converters/date.test.ts similarity index 100% rename from src/plugins/data/common/field_formats/converters/date.test.ts rename to src/plugins/data/public/field_formats/converters/date.test.ts diff --git a/src/plugins/data/common/field_formats/converters/date.ts b/src/plugins/data/public/field_formats/converters/date.ts similarity index 92% rename from src/plugins/data/common/field_formats/converters/date.ts rename to src/plugins/data/public/field_formats/converters/date.ts index 3888df051b118..3e1efdc69dec8 100644 --- a/src/plugins/data/common/field_formats/converters/date.ts +++ b/src/plugins/data/public/field_formats/converters/date.ts @@ -20,9 +20,12 @@ import { i18n } from '@kbn/i18n'; import { memoize, noop } from 'lodash'; import moment from 'moment'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { + FieldFormat, + KBN_FIELD_TYPES, + TextContextTypeConvert, + FIELD_FORMAT_IDS, +} from '../../../common'; export class DateFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.DATE; diff --git a/src/plugins/data/public/field_formats/converters/index.ts b/src/plugins/data/public/field_formats/converters/index.ts new file mode 100644 index 0000000000000..c51111092beca --- /dev/null +++ b/src/plugins/data/public/field_formats/converters/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 { DateFormat } from './date'; diff --git a/src/plugins/data/public/field_formats/field_formats_service.test.ts b/src/plugins/data/public/field_formats/field_formats_service.test.ts new file mode 100644 index 0000000000000..e066af28f4699 --- /dev/null +++ b/src/plugins/data/public/field_formats/field_formats_service.test.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 { FieldFormatsService } from './field_formats_service'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { DateFormat } from './converters/date'; + +describe('FieldFormatService', () => { + test('DateFormat is public version', () => { + const mockCore = coreMock.createSetup(); + const service = new FieldFormatsService(); + service.setup(mockCore); + const fieldFormatsRegistry = service.start(); + const DateFormatFromRegsitry = fieldFormatsRegistry.getTypeWithoutMetaParams('date'); + + expect(DateFormatFromRegsitry).toEqual(DateFormat); + }); +}); diff --git a/src/plugins/data/public/field_formats/field_formats_service.ts b/src/plugins/data/public/field_formats/field_formats_service.ts index 785bedf9b35d3..22c7e90c06130 100644 --- a/src/plugins/data/public/field_formats/field_formats_service.ts +++ b/src/plugins/data/public/field_formats/field_formats_service.ts @@ -18,9 +18,10 @@ */ import { CoreSetup } from 'src/core/public'; -import { FieldFormatsRegistry } from '../../common/field_formats'; +import { FieldFormatsRegistry } from '../../common'; import { deserializeFieldFormat } from './utils/deserialize'; import { FormatFactory } from '../../common/field_formats/utils'; +import { baseFormattersPublic } from './constants'; export class FieldFormatsService { private readonly fieldFormatsRegistry: FieldFormatsRegistry = new FieldFormatsRegistry(); @@ -34,13 +35,17 @@ export class FieldFormatsService { const getConfig = core.uiSettings.get.bind(core.uiSettings); - this.fieldFormatsRegistry.init(getConfig, { - parsedUrl: { - origin: window.location.origin, - pathname: window.location.pathname, - basePath: core.http.basePath.get(), + this.fieldFormatsRegistry.init( + getConfig, + { + parsedUrl: { + origin: window.location.origin, + pathname: window.location.pathname, + basePath: core.http.basePath.get(), + }, }, - }); + baseFormattersPublic + ); return this.fieldFormatsRegistry as FieldFormatsSetup; } diff --git a/src/plugins/data/public/field_formats/index.ts b/src/plugins/data/public/field_formats/index.ts index 4550a5781535f..015d5b39561bb 100644 --- a/src/plugins/data/public/field_formats/index.ts +++ b/src/plugins/data/public/field_formats/index.ts @@ -18,3 +18,5 @@ */ export { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats_service'; +export { DateFormat } from './converters'; +export { baseFormattersPublic } from './constants'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index cdc4167f545af..cbd4bfd348797 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -156,7 +156,6 @@ import { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, @@ -171,6 +170,9 @@ import { serializeFieldFormat, } from '../common/field_formats'; +import { DateFormat } from './field_formats'; +export { baseFormattersPublic } from './field_formats'; + // Field formats helpers namespace: export const fieldFormats = { FieldFormat, diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 2d5cc72597ec4..a2a1a2424fc90 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -50,6 +50,7 @@ const fieldFormatsMock: IFieldFormatsRegistry = { register: jest.fn(), parseDefaultTypeMap: jest.fn(), deserialize: jest.fn(), + getTypeWithoutMetaParams: jest.fn(), }; const createSetupContract = (): Setup => { diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 06e56aaf3eb0a..93af543fba1a8 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -184,6 +184,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -839,6 +840,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -1476,6 +1478,7 @@ exports[`QueryStringInput Should pass the query language to the language switche "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -2128,6 +2131,7 @@ exports[`QueryStringInput Should pass the query language to the language switche "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -2765,6 +2769,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], @@ -3417,6 +3422,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` "getInstance": [MockFunction], "getType": [MockFunction], "getTypeNameByEsTypes": [MockFunction], + "getTypeWithoutMetaParams": [MockFunction], "init": [MockFunction], "parseDefaultTypeMap": [MockFunction], "register": [MockFunction], diff --git a/src/plugins/data/common/field_formats/converters/date_server.ts b/src/plugins/data/server/field_formats/converters/date_server.ts similarity index 95% rename from src/plugins/data/common/field_formats/converters/date_server.ts rename to src/plugins/data/server/field_formats/converters/date_server.ts index 216af133bb5f5..f4e6296259196 100644 --- a/src/plugins/data/common/field_formats/converters/date_server.ts +++ b/src/plugins/data/server/field_formats/converters/date_server.ts @@ -20,14 +20,14 @@ import { i18n } from '@kbn/i18n'; import { memoize, noop } from 'lodash'; import moment from 'moment-timezone'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { FieldFormat } from '../field_format'; import { + FieldFormat, + KBN_FIELD_TYPES, TextContextTypeConvert, FIELD_FORMAT_IDS, FieldFormatsGetConfigFn, IFieldFormatMetaParams, -} from '../types'; +} from '../../../common'; export class DateFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.DATE; diff --git a/src/plugins/data/server/field_formats/converters/index.ts b/src/plugins/data/server/field_formats/converters/index.ts new file mode 100644 index 0000000000000..f5c69df972869 --- /dev/null +++ b/src/plugins/data/server/field_formats/converters/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 { DateFormat } from './date_server'; diff --git a/src/plugins/data/server/field_formats/field_formats_service.test.ts b/src/plugins/data/server/field_formats/field_formats_service.test.ts new file mode 100644 index 0000000000000..2e7ce0fa435a7 --- /dev/null +++ b/src/plugins/data/server/field_formats/field_formats_service.test.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 { FieldFormatsService } from './field_formats_service'; +import { DateFormat } from './converters/date_server'; +import { coreMock } from '../../../../core/server/mocks'; + +describe('FieldFormatService', () => { + test('DateFormat is server version', async () => { + const service = new FieldFormatsService(); + const fieldFormatsService = await service.start(); + const uiSettings = coreMock.createStart().uiSettings.asScopedToClient({} as any); + const fieldFormatsRegistry = await fieldFormatsService.fieldFormatServiceFactory(uiSettings); + const DateFormatFromRegsitry = fieldFormatsRegistry.getTypeWithoutMetaParams('date'); + + expect(DateFormatFromRegsitry).toEqual(DateFormat); + }); +}); diff --git a/src/plugins/data/server/field_formats/field_formats_service.ts b/src/plugins/data/server/field_formats/field_formats_service.ts index a31e5927ab800..0dac64fb5dc1d 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -19,9 +19,10 @@ import { has } from 'lodash'; import { FieldFormatsRegistry, IFieldFormatType, baseFormatters } from '../../common/field_formats'; import { IUiSettingsClient } from '../../../../core/server'; +import { DateFormat } from './converters'; export class FieldFormatsService { - private readonly fieldFormatClasses: IFieldFormatType[] = baseFormatters; + private readonly fieldFormatClasses: IFieldFormatType[] = [DateFormat, ...baseFormatters]; public setup() { return { diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 3ee98a318de35..40d367138b60d 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -83,7 +83,6 @@ import { BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, @@ -101,13 +100,10 @@ import { export const fieldFormats = { FieldFormatsRegistry, FieldFormat, - serializeFieldFormat, - BoolFormat, BytesFormat, ColorFormat, - DateFormat, DateNanosFormat, DurationFormat, IpFormat, diff --git a/src/test_utils/public/stub_field_formats.ts b/src/test_utils/public/stub_field_formats.ts index 5a20823134ebd..589e93fd600c2 100644 --- a/src/test_utils/public/stub_field_formats.ts +++ b/src/test_utils/public/stub_field_formats.ts @@ -19,12 +19,13 @@ import { CoreSetup } from 'kibana/public'; import { DataPublicPluginStart, fieldFormats } from '../../plugins/data/public'; import { deserializeFieldFormat } from '../../plugins/data/public/field_formats/utils/deserialize'; +import { baseFormattersPublic } from '../../plugins/data/public'; export const getFieldFormatsRegistry = (core: CoreSetup) => { const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); const getConfig = core.uiSettings.get.bind(core.uiSettings); - fieldFormatsRegistry.init(getConfig, {}); + fieldFormatsRegistry.init(getConfig, {}, baseFormattersPublic); fieldFormatsRegistry.deserialize = deserializeFieldFormat.bind( fieldFormatsRegistry as DataPublicPluginStart['fieldFormats'] From 5953e62a726bacd5b49976a3e0863e1d2cb5aa06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Thu, 20 Feb 2020 17:57:00 +0100 Subject: [PATCH 017/113] [Logs UI] Fix column reordering in settings page (#58104) * Ensure only one element has scroll Apparently having multiple elements with scroll confuses the `react-beautiful-dnd` mechanism to determine the position of the elements. Adding `overflowY` to the app root fixes it. * Fix upper bound check for log column reordering Co-authored-by: Elastic Machine --- x-pack/plugins/infra/public/apps/start_app.tsx | 1 + .../log_columns_configuration_form_state.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index 300d97d3c45b1..66e699abd22b4 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -79,6 +79,7 @@ export async function startApp( // expected. element.style.height = '100%'; element.style.display = 'flex'; + element.style.overflowY = 'hidden'; // Prevent having scroll within a container having scroll. It messes up with drag-n-drop elements element.className += ` ${CONTAINER_CLASSNAME}`; ReactDOM.render(, element); diff --git a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx index c5398cf79ef43..0b6a92ed98507 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_form_state.tsx @@ -108,7 +108,7 @@ export const useLogColumnsConfigurationFormState = ({ const moveLogColumn = useCallback( (sourceIndex, destinationIndex) => { - if (destinationIndex >= 0 && sourceIndex < formState.logColumns.length - 1) { + if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) { const newLogColumns = [...formState.logColumns]; newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); setFormStateChanges(changes => ({ From 857f9f8379632c74f10b64f16d375ba2cd1ddd66 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Thu, 20 Feb 2020 21:20:23 +0300 Subject: [PATCH 018/113] [NP] Move ui/saved_objects to NP (#57452) * Move saved_objects to NP * Update path for imports * Remove ui/saved_objects * Update i18n IDs * Convert test * Replace Bluebird to Promise; fix unit tests * Mock openConfirm in test * Add kibana.json * Check unit test * Update unit tests --- src/core/MIGRATION.md | 2 +- .../simple_saved_object.test.ts} | 35 +- .../kibana/public/dashboard/legacy_imports.ts | 2 - .../public/dashboard/np_ready/application.ts | 2 +- .../np_ready/dashboard_app_controller.tsx | 3 +- .../dashboard/np_ready/lib/save_dashboard.ts | 2 +- .../saved_dashboard/saved_dashboard.ts | 7 +- .../saved_dashboard/saved_dashboards.ts | 6 +- .../discover/saved_searches/_saved_search.ts | 7 +- .../discover/saved_searches/saved_searches.ts | 7 +- .../management/saved_object_registry.ts | 2 +- .../render.test.js | 1 + .../saved_object_save_as_checkbox.html | 2 +- .../timelion/public/services/_saved_sheet.ts | 6 +- .../timelion/public/services/saved_sheets.ts | 2 +- .../public/embeddable/visualize_embeddable.ts | 2 +- .../public/saved_visualizations/_saved_vis.ts | 7 +- .../saved_visualizations.ts | 6 +- src/plugins/data/public/mocks.ts | 1 + .../query_string_input.test.tsx.snap | 6 + src/plugins/saved_objects/kibana.json | 7 + .../saved_objects/public}/constants.ts | 11 +- src/plugins/saved_objects/public/index.ts | 6 + src/plugins/saved_objects/public/plugin.ts | 25 + .../saved_object}/helpers/apply_es_resp.ts | 8 +- .../helpers/build_saved_object.ts | 4 +- .../helpers/check_for_duplicate_title.ts | 4 +- .../helpers/confirm_modal_promise.tsx | 11 +- .../saved_object}/helpers/create_source.ts | 17 +- .../display_duplicate_title_confirm_modal.ts | 17 +- .../helpers/find_object_by_title.test.ts} | 28 +- .../helpers/find_object_by_title.ts | 8 +- .../helpers/hydrate_index_pattern.ts | 4 +- .../helpers/initialize_saved_object.ts | 2 +- .../helpers/parse_search_source.ts | 6 +- .../helpers/save_saved_object.ts | 4 +- .../helpers/serialize_saved_object.ts | 4 +- .../helpers/string_utils.test.ts | 1 + .../saved_object}/helpers/string_utils.ts | 0 .../public/saved_object}/index.ts | 2 +- .../public/saved_object/saved_object.test.ts} | 569 +++++++++--------- .../public/saved_object}/saved_object.ts | 2 +- .../saved_object}/saved_object_loader.ts | 2 +- .../saved_objects/public}/types.ts | 6 +- .../plugins/graph/public/legacy_imports.ts | 2 - .../services/persistence/saved_workspace.ts | 4 +- .../persistence/saved_workspace_loader.ts | 2 +- .../plugins/graph/public/types/persistence.ts | 2 +- .../services/gis_map_saved_object_loader.js | 2 +- .../public/angular/services/saved_gis_map.js | 2 +- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- 52 files changed, 467 insertions(+), 431 deletions(-) rename src/{legacy/ui/public/saved_objects/__tests__/simple_saved_object.js => core/public/saved_objects/simple_saved_object.test.ts} (67%) create mode 100644 src/plugins/saved_objects/kibana.json rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public}/constants.ts (83%) create mode 100644 src/plugins/saved_objects/public/plugin.ts rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/apply_es_resp.ts (91%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/build_saved_object.ts (98%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/check_for_duplicate_title.ts (95%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/confirm_modal_promise.tsx (86%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/create_source.ts (85%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/display_duplicate_title_confirm_modal.ts (79%) rename src/{legacy/ui/public/saved_objects/__tests__/find_object_by_title.js => plugins/saved_objects/public/saved_object/helpers/find_object_by_title.test.ts} (72%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/find_object_by_title.ts (90%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/hydrate_index_pattern.ts (92%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/initialize_saved_object.ts (96%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/parse_search_source.ts (94%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/save_saved_object.ts (99%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/serialize_saved_object.ts (96%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/string_utils.test.ts (99%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/helpers/string_utils.ts (100%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/index.ts (92%) rename src/{legacy/ui/public/saved_objects/__tests__/saved_object.js => plugins/saved_objects/public/saved_object/saved_object.test.ts} (52%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/saved_object.ts (98%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public/saved_object}/saved_object_loader.ts (98%) rename src/{legacy/ui/public/saved_objects => plugins/saved_objects/public}/types.ts (96%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 9e57fc4c36876..19f62a7d48923 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1170,7 +1170,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | -| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | +| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../saved_objects/public'` | | | `core_plugins/interpreter` | `data.expressions` | still in progress | | `ui/courier` | `data.search` | still in progress | | `ui/embeddable` | `embeddables` | still in progress | diff --git a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js b/src/core/public/saved_objects/simple_saved_object.test.ts similarity index 67% rename from src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js rename to src/core/public/saved_objects/simple_saved_object.test.ts index f2fc9bfe232e2..99676f6b78d42 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js +++ b/src/core/public/saved_objects/simple_saved_object.test.ts @@ -17,36 +17,43 @@ * under the License. */ -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { SimpleSavedObject } from '../../../../../core/public'; +import { SavedObject } from '../../server'; +import { SimpleSavedObject } from './simple_saved_object'; +import { SavedObjectsClientContract } from './saved_objects_client'; describe('SimpleSavedObject', () => { + let client: SavedObjectsClientContract; + + beforeEach(() => { + client = { + update: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + } as any; + }); + it('persists type and id', () => { const id = 'logstash-*'; const type = 'index-pattern'; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { id, type }); + const savedObject = new SimpleSavedObject(client, { id, type } as SavedObject); - expect(savedObject.id).to.be(id); - expect(savedObject.type).to.be(type); + expect(savedObject.id).toEqual(id); + expect(savedObject.type).toEqual(type); }); it('persists attributes', () => { const attributes = { title: 'My title' }; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { attributes }); + const savedObject = new SimpleSavedObject(client, { attributes } as SavedObject); - expect(savedObject.attributes).to.be(attributes); + expect(savedObject.attributes).toEqual(attributes); }); it('persists version', () => { - const version = 2; + const version = '2'; - const client = sinon.stub(); - const savedObject = new SimpleSavedObject(client, { version }); - expect(savedObject._version).to.be(version); + const savedObject = new SimpleSavedObject(client, { version } as SavedObject); + expect(savedObject._version).toEqual(version); }); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index d5198dc557f04..c1f679e9eb7ac 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -24,7 +24,6 @@ * directly where they are needed. */ -export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { KbnUrl } from 'ui/url/kbn_url'; @@ -33,7 +32,6 @@ export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_to // @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; -export { SavedObjectLoader } from 'ui/saved_objects'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index cc104c1a931d0..7239d8f2258a7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -38,7 +38,6 @@ import { PrivateProvider, PromiseServiceCreator, RedirectWhenMissingProvider, - SavedObjectLoader, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -47,6 +46,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../../../. import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; +import { SavedObjectLoader } from '../../../../../../plugins/saved_objects/public'; export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 465203be0d34c..075516d52bab6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -26,9 +26,10 @@ import angular from 'angular'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { History } from 'history'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -import { migrateLegacyQuery, SavedObjectSaveOpts, subscribeWithScope } from '../legacy_imports'; +import { migrateLegacyQuery, subscribeWithScope } from '../legacy_imports'; import { esFilters, IndexPattern, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts index d80208ce27ffe..db2b1f15247de 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts @@ -18,7 +18,7 @@ */ import { TimefilterContract } from 'src/plugins/data/public'; -import { SavedObjectSaveOpts } from '../../legacy_imports'; +import { SavedObjectSaveOpts } from '../../../../../../../plugins/saved_objects/public'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts index 5babaf8061de9..c5ac05b5a77eb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; +import { + createSavedObjectClass, + SavedObject, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts index 4ece5d46358ba..2ff76da9c5ca6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.ts @@ -17,8 +17,10 @@ * under the License. */ -import { SavedObjectLoader } from 'ui/saved_objects'; -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { + SavedObjectLoader, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; export function createSavedDashboardLoader(services: SavedObjectKibanaServices) { diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts index 113d13287bd12..7bd0eef8c19af 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; + +import { + createSavedObjectClass, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; export function createSavedSearchClass(services: SavedObjectKibanaServices) { const SavedObjectClass = createSavedObjectClass(services); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 0b34652461026..ebd341eba99fd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectLoader } from 'ui/saved_objects'; -import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; + +import { + SavedObjectLoader, + SavedObjectKibanaServices, +} from '../../../../../../plugins/saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 0a6ac20502669..e0756b2e78e25 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; -import { SavedObjectLoader } from 'ui/saved_objects'; +import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; import { createSavedDashboardLoader } from '../dashboard'; import { createSavedSearchesLoader } from '../discover'; import { TypesService, createSavedVisLoader } from '../../../visualizations/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js index af580547b11ed..1b9dafb6daf23 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/render.test.js @@ -50,6 +50,7 @@ describe('CreateIndexPatternWizardRender', () => { config: {}, changeUrl: () => {}, indexPatternCreationType: {}, + openConfirm: jest.fn(), }); expect(render.mock.calls.length).toBe(1); diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html index 3e4a1526113c3..5adce4286010a 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_save_as_checkbox.html @@ -5,7 +5,7 @@ i18n-id="timelion.savedObjects.howToSaveAsNewDescription" i18n-default-message="In previous versions of Kibana, changing the name of a {savedObjectName} would make a copy with the new name. Use the 'Save as a new {savedObjectName}' checkbox to do this now." i18n-values="{ savedObjectName: savedObject.getDisplayName() }" - i18n-description="'Save as a new {savedObjectName}' refers to common.ui.savedObjects.saveAsNewLabel and should be the same text." + i18n-description="'Save as a new {savedObjectName}' refers to timelion.savedObjects.saveAsNewLabel and should be the same text." >

+
- test.test.test - - + + test.test.test + +
+
`; exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = ` - - - + + + + +
- test - - + + test + +
+ `; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 95720bee38df8..54e1c1706a856 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -18,7 +18,9 @@ */ import React from 'react'; import classNames from 'classnames'; -import { FieldIcon } from '../../../../../../../../../plugins/kibana_react/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FieldIcon, FieldIconProps } from '../../../../../../../../../plugins/kibana_react/public'; import { shortenDottedString } from '../../../../../../../../../plugins/data/common/utils'; import { getFieldTypeName } from './field_type_name'; @@ -35,25 +37,35 @@ interface Props { fieldName?: string; fieldType?: string; useShortDots?: boolean; + fieldIconProps?: Omit; } -export function FieldName({ field, fieldName, fieldType, useShortDots }: Props) { +export function FieldName({ field, fieldName, fieldType, useShortDots, fieldIconProps }: Props) { const type = field ? String(field.type) : String(fieldType); const typeName = getFieldTypeName(type); const name = field ? String(field.name) : String(fieldName); const displayName = useShortDots ? shortenDottedString(name) : name; - const className = classNames({ - 'dscField--noResults': field ? !field.rowCount && !field.scripted : false, - // this is currently not styled, should display an icon - scripted: field ? field.scripted : false, + const noResults = field ? !field.rowCount && !field.scripted : false; + + const className = classNames('dscFieldName', { + 'dscFieldName--noResults': noResults, }); return ( - - - {displayName} - + + + + + + {displayName} + + ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss index fe13ac2fafa01..b05775c4ee95c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/_field_chooser.scss @@ -10,7 +10,6 @@ .dscFieldName { color: $euiColorDarkShade; - padding-left: $euiSizeS; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx index 7a78e89416361..5b13f6b3655c3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx @@ -87,7 +87,12 @@ export function DocViewTableRow({ )} - + {isCollapsible && ( diff --git a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap index 870dbdc533267..cde6a625ac8e8 100644 --- a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -1,37 +1,159 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FieldIcon renders a blackwhite icon for a string 1`] = ` - `; -exports[`FieldIcon renders a colored icon for a number 1`] = ` - `; -exports[`FieldIcon renders an icon for an unknown type 1`] = ` - +`; + +exports[`FieldIcon renders known field types boolean is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types conflict is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types date is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types geo_point is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types geo_shape is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types ip is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types murmur3 is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types nested is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types number is rendered 1`] = ` + +`; + +exports[`FieldIcon renders known field types string is rendered 1`] = ` + `; exports[`FieldIcon renders with className if provided 1`] = ` - +`; + +exports[`FieldIcon supports same props as EuiToken 1`] = ` + `; diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx index 90a858e31b4f3..51ff5f603ea37 100644 --- a/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx +++ b/src/plugins/kibana_react/public/field_icon/field_icon.test.tsx @@ -18,24 +18,44 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldIcon } from './field_icon'; +import { FieldIcon, typeToEuiIconMap } from './field_icon'; -test('FieldIcon renders a blackwhite icon for a string', () => { - const component = shallow(); +const availableTypes = Object.keys(typeToEuiIconMap); + +describe('FieldIcon renders known field types', () => { + availableTypes.forEach(type => { + test(`${type} is rendered`, () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); + +test('FieldIcon renders an icon for an unknown type', () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); -test('FieldIcon renders a colored icon for a number', () => { - const component = shallow(); +test('FieldIcon supports same props as EuiToken', () => { + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); -test('FieldIcon renders an icon for an unknown type', () => { - const component = shallow(); +test('FieldIcon changes fill when scripted is true', () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); test('FieldIcon renders with className if provided', () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.tsx index 2e199a7471a64..2da1eba31e254 100644 --- a/src/plugins/kibana_react/public/field_icon/field_icon.tsx +++ b/src/plugins/kibana_react/public/field_icon/field_icon.tsx @@ -17,14 +17,10 @@ * under the License. */ import React from 'react'; -import { euiPaletteColorBlind, EuiIcon } from '@elastic/eui'; -import { IconSize } from '@elastic/eui/src/components/icon/icon'; +import classNames from 'classnames'; +import { EuiToken, EuiTokenProps } from '@elastic/eui'; -interface IconMapEntry { - icon: string; - color: string; -} -interface FieldIconProps { +export interface FieldIconProps extends Omit { type: | 'boolean' | 'conflict' @@ -39,51 +35,50 @@ interface FieldIconProps { | string | 'nested'; label?: string; - size?: IconSize; - useColor?: boolean; - className?: string; + scripted?: boolean; } -const colors = euiPaletteColorBlind(); - // defaultIcon => a unknown datatype -const defaultIcon = { icon: 'questionInCircle', color: colors[0] }; +const defaultIcon = { iconType: 'questionInCircle', color: 'gray' }; -export const typeToEuiIconMap: Partial> = { - boolean: { icon: 'invert', color: colors[5] }, +export const typeToEuiIconMap: Partial> = { + boolean: { iconType: 'tokenBoolean' }, // icon for an index pattern mapping conflict in discover - conflict: { icon: 'alert', color: colors[8] }, - date: { icon: 'calendar', color: colors[7] }, - geo_point: { icon: 'globe', color: colors[2] }, - geo_shape: { icon: 'globe', color: colors[2] }, - ip: { icon: 'storage', color: colors[8] }, + conflict: { iconType: 'alert', color: 'euiVisColor9' }, + date: { iconType: 'tokenDate' }, + geo_point: { iconType: 'tokenGeo' }, + geo_shape: { iconType: 'tokenGeo' }, + ip: { iconType: 'tokenIP' }, // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html - murmur3: { icon: 'document', color: colors[1] }, - number: { icon: 'number', color: colors[0] }, - _source: { icon: 'editorCodeBlock', color: colors[3] }, - string: { icon: 'string', color: colors[4] }, - nested: { icon: 'nested', color: colors[2] }, + murmur3: { iconType: 'tokenFile' }, + number: { iconType: 'tokenNumber' }, + _source: { iconType: 'editorCodeBlock', color: 'gray' }, + string: { iconType: 'tokenString' }, + nested: { iconType: 'tokenNested' }, }; /** - * Field icon used across the app + * Field token icon used across the app */ export function FieldIcon({ type, label, size = 's', - useColor = false, - className = undefined, + scripted, + className, + ...rest }: FieldIconProps) { - const euiIcon = typeToEuiIconMap[type] || defaultIcon; + const token = typeToEuiIconMap[type] || defaultIcon; return ( - ); } diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx index 6152f33350917..f2a4c28afcdae 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx @@ -237,10 +237,18 @@ export function FieldEditor({ renderOption={(option, searchValue, contentClassName) => { const { type, label } = option; return ( - - {' '} - {label} - + + + + + + {label} + + ); }} compressed diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx deleted file mode 100644 index 0c099135f631d..0000000000000 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_icon.tsx +++ /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 React from 'react'; -import { ICON_TYPES, euiPaletteColorBlind, EuiIcon } from '@elastic/eui'; - -function stringToNum(s: string) { - return Array.from(s).reduce((acc, ch) => acc + ch.charCodeAt(0), 1); -} - -function getIconForDataType(dataType: string) { - const icons: Partial>> = { - boolean: 'invert', - date: 'calendar', - geo_point: 'globe', - ip: 'storage', - }; - return icons[dataType] || ICON_TYPES.find(t => t === dataType) || 'document'; -} - -export function getColorForDataType(type: string) { - const iconType = getIconForDataType(type); - const colors = euiPaletteColorBlind(); - const colorIndex = stringToNum(iconType) % colors.length; - return colors[colorIndex]; -} - -export type UnwrapArray = T extends Array ? P : T; - -export function FieldIcon({ type }: { type: string }) { - const iconType = getIconForDataType(type); - - return ; -} diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx index b38e3f8430980..30f1fcffd4f67 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_picker.tsx @@ -122,7 +122,7 @@ function toOptions( .filter(field => isExplorable(field) || field.selected) .map(field => ({ label: field.name, - prepend: , + prepend: , checked: field.selected ? 'on' : undefined, })) ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap index 5593a1af00d70..8bbe49b2e0d7f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap @@ -1,10 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`LensFieldIcon accepts FieldIcon props 1`] = ` + +`; + exports[`LensFieldIcon renders properly 1`] = ` `; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss index ed39beeb7d088..77d4b41a0413c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss @@ -52,3 +52,16 @@ @include euiFormControlLayoutPadding(1, 'right'); @include euiFormControlLayoutPadding(1, 'left'); } + +.lnsInnerIndexPatternDataPanel__filterType { + padding: $euiSizeS; +} + +.lnsInnerIndexPatternDataPanel__filterTypeInner { + display: flex; + align-items: center; + + .lnsFieldListPanel__fieldIcon { + margin-right: $euiSizeS; + } +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss index 54f9a3787466d..89f6bbf908419 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss @@ -14,7 +14,7 @@ } .lnsFieldItem--missing { - background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade); + background: lightOrDarkTheme(transparentize($euiColorMediumShade, 0.9), $euiColorEmptyShade); color: $euiColorDarkShade; } @@ -24,10 +24,10 @@ display: flex; align-items: flex-start; transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance, - background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation + background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation .lnsFieldItem__name { - margin-left: $euiSizeXS; + margin-left: $euiSizeS; flex-grow: 1; } @@ -37,7 +37,8 @@ } .lnsFieldListPanel__fieldIcon { - margin-top: 2px; + margin-top: $euiSizeXS / 2; + margin-right: $euiSizeXS / 2; } .lnsFieldItem__infoIcon { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 3231ab7d7ff12..69982aed78b40 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -384,6 +384,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ data-test-subj="lnsIndexPatternTypeFilterOptions" items={(availableFieldTypes as DataType[]).map(type => ( - {fieldTypeNames[type]} + + {fieldTypeNames[type]} + ))} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 46d7233ba9587..77435fcdf3eed 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -169,6 +169,7 @@ export function FieldSelect({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx deleted file mode 100644 index 6b12bb5feef1b..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx +++ /dev/null @@ -1,55 +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. - */ - -/* - * 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 { shallow } from 'enzyme'; -import React from 'react'; -import { FieldIcon } from './field_icon'; - -describe('FieldIcon', () => { - it('should render icons', () => { - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - expect(shallow()).toMatchInlineSnapshot(` - - `); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx deleted file mode 100644 index 796f200bffd97..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx +++ /dev/null @@ -1,43 +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 { ICON_TYPES, euiPaletteColorBlind, EuiIcon } from '@elastic/eui'; -import classNames from 'classnames'; -import { DataType } from '../types'; - -function stringToNum(s: string) { - return Array.from(s).reduce((acc, ch) => acc + ch.charCodeAt(0), 1); -} - -function getIconForDataType(dataType: string) { - const icons: Partial>> = { - boolean: 'invert', - date: 'calendar', - ip: 'ip', - }; - return icons[dataType] || ICON_TYPES.find(t => t === dataType) || 'empty'; -} - -export function getColorForDataType(type: string) { - const iconType = getIconForDataType(type); - const colors = euiPaletteColorBlind(); - const colorIndex = stringToNum(iconType) % colors.length; - return colors[colorIndex]; -} - -export type UnwrapArray = T extends Array ? P : T; - -export function FieldIcon({ type }: { type: DataType }) { - const iconType = getIconForDataType(type); - - const classes = classNames( - 'lnsFieldListPanel__fieldIcon', - `lnsFieldListPanel__fieldIcon--${type}` - ); - - return ; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx index 0271d2ca021c5..94d644e6590e1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -46,7 +46,7 @@ import { DragDrop } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../../../../plugins/lens/common'; import { IndexPattern, IndexPatternField } from './types'; -import { getColorForDataType, LensFieldIcon } from './lens_field_icon'; +import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; export interface FieldItemProps { @@ -294,11 +294,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { ); } - const euiButtonColor = - field.type === 'string' ? 'accent' : field.type === 'number' ? 'secondary' : 'primary'; - const euiTextColor = - field.type === 'string' ? 'accent' : field.type === 'number' ? 'secondary' : 'default'; - const fromDate = DateMath.parse(dateRange.fromDate); const toDate = DateMath.parse(dateRange.toDate); @@ -391,8 +386,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { const specId = i18n.translate('xpack.lens.indexPattern.fieldStatsCountLabel', { defaultMessage: 'Count', }); - const expectedColor = getColorForDataType(field.type); - const seriesColors = expectedColor ? [expectedColor] : undefined; if (field.type === 'date') { return wrapInPopover( @@ -429,7 +422,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { yAccessors={['count']} xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} - customSeriesColors={seriesColors} timeZone="local" /> @@ -453,7 +445,6 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { yAccessors={['count']} xScaleType={ScaleType.Linear} yScaleType={ScaleType.Linear} - customSeriesColors={seriesColors} /> ); @@ -486,7 +477,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { )} - + {Math.round((topValue.count / props.sampledValues!) * 100)}% @@ -497,7 +488,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { value={topValue.count / props.sampledValues!} max={1} size="s" - color={euiButtonColor} + color="accent" /> ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx index 961e22380bdca..317ce8f032f94 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx @@ -11,19 +11,14 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import { LensFieldIcon, getColorForDataType } from './lens_field_icon'; +import { LensFieldIcon } from './lens_field_icon'; test('LensFieldIcon renders properly', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); -test('LensFieldIcon getColorForDataType for a valid type', () => { - const color = getColorForDataType('date'); - expect(color).toEqual('#DA8B45'); -}); - -test('LensFieldIcon getColorForDataType for an invalid type', () => { - const color = getColorForDataType('invalid'); - expect(color).toEqual('#54B399'); +test('LensFieldIcon accepts FieldIcon props', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx index 2e6a5fcd8115f..06eda73748cef 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx @@ -5,26 +5,16 @@ */ import React from 'react'; -import { euiPaletteColorBlind } from '@elastic/eui'; -import { FieldIcon, typeToEuiIconMap } from '../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon, FieldIconProps } from '../../../../../../src/plugins/kibana_react/public'; import { DataType } from '../types'; import { normalizeOperationDataType } from './utils'; -export function getColorForDataType(type: string) { - const iconMap = typeToEuiIconMap[normalizeOperationDataType(type as DataType)]; - if (iconMap) { - return iconMap.color; - } - return euiPaletteColorBlind()[0]; -} - -export function LensFieldIcon({ type }: { type: DataType }) { +export function LensFieldIcon({ type, ...rest }: FieldIconProps & { type: DataType }) { return ( ); } diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap index f37dfdd879c5b..d0cdbe7243abe 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap @@ -23,7 +23,7 @@ exports[`Should remove selected fields from selectable 1`] = ` id="addTooltipFieldPopover" isOpen={false} ownFocus={true} - panelPaddingSize="m" + panelPaddingSize="none" > , "value": "@timestamp", }, ] } + searchProps={ + Object { + "compressed": true, + } + } searchable={true} singleSelection={false} > @@ -88,7 +93,7 @@ exports[`Should render 1`] = ` id="addTooltipFieldPopover" isOpen={false} ownFocus={true} - panelPaddingSize="m" + panelPaddingSize="none" > , "value": "@timestamp", }, Object { "label": "custom label for prop1", "prepend": , "value": "prop1", }, Object { "label": "prop2", "prepend": , "value": "prop2", }, ] } + searchProps={ + Object { + "compressed": true, + } + } searchable={true} singleSelection={false} > diff --git a/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js b/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js index bddb74596f4ef..07bc54663c1d8 100644 --- a/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js +++ b/x-pack/legacy/plugins/maps/public/components/add_tooltip_field_popover.js @@ -39,7 +39,10 @@ function getOptions(fields, selectedFields) { .map(field => { return { value: field.name, - prepend: 'type' in field ? : null, + prepend: + 'type' in field ? ( + + ) : null, label: 'label' in field ? field.label : field.name, }; }) @@ -127,7 +130,12 @@ export class AddTooltipFieldPopover extends Component { return ( - + {(list, search) => (
{search} @@ -161,6 +169,7 @@ export class AddTooltipFieldPopover extends Component { button={this._renderAddButton()} isOpen={this.state.isPopoverOpen} closePopover={this._closePopover} + panelPaddingSize="none" ownFocus > {this._renderContent()} diff --git a/x-pack/legacy/plugins/maps/public/components/single_field_select.js b/x-pack/legacy/plugins/maps/public/components/single_field_select.js index 7351ce7691a82..98e33454b041b 100644 --- a/x-pack/legacy/plugins/maps/public/components/single_field_select.js +++ b/x-pack/legacy/plugins/maps/public/components/single_field_select.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; -import { EuiComboBox, EuiHighlight } from '@elastic/eui'; +import { EuiComboBox, EuiHighlight, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; function fieldsToOptions(fields) { @@ -30,11 +30,14 @@ function fieldsToOptions(fields) { function renderOption(option, searchValue, contentClassName) { return ( - - -   - {option.label} - + + + + + + {option.label} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js index a32c2ce04d735..cf0ec5589d6bc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_select.js @@ -7,18 +7,21 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { EuiComboBox, EuiHighlight } from '@elastic/eui'; +import { EuiComboBox, EuiHighlight, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FIELD_ORIGIN } from '../../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { FieldIcon } from '../../../../../../../../../src/plugins/kibana_react/public'; function renderOption(option, searchValue, contentClassName) { return ( - - -   - {option.label} - + + + + + + {option.label} + + ); } From 9c8c47befb177ba9fd5e4a102ae363140e2668a7 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 21 Feb 2020 16:37:39 -0700 Subject: [PATCH 050/113] [File upload] Update remaining File Upload dependencies for NP migration (#58128) * Remove old route ref, no longer used * Use core elasticsearch service * Remove getSavedObjectsRepository, use NP internalRepository * Update tests and clean up * Remove unused test vars --- x-pack/legacy/plugins/file_upload/index.js | 22 ++++------- .../call_with_internal_user_factory.d.ts | 7 ---- .../client/call_with_internal_user_factory.js | 18 --------- .../call_with_internal_user_factory.test.ts | 22 ----------- .../client/call_with_request_factory.js | 15 ++++--- .../server/kibana_server_services.js | 18 +++++++++ .../plugins/file_upload/server/plugin.js | 22 ++++++----- .../file_upload/server/routes/file_upload.js | 10 ++--- .../telemetry/file_upload_usage_collector.ts | 12 +----- .../server/telemetry/telemetry.test.ts | 10 +---- .../file_upload/server/telemetry/telemetry.ts | 39 ++++--------------- 11 files changed, 63 insertions(+), 132 deletions(-) delete mode 100644 x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts delete mode 100644 x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js delete mode 100644 x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts create mode 100644 x-pack/legacy/plugins/file_upload/server/kibana_server_services.js diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index d29226c802b06..23e1e1d98aa7f 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -8,9 +8,10 @@ import { mappings } from './mappings'; export const fileUpload = kibana => { return new kibana.Plugin({ - require: ['elasticsearch', 'xpack_main'], + require: ['elasticsearch'], name: 'file_upload', id: 'file_upload', + // TODO: uiExports and savedObjectSchemas to be removed on migration uiExports: { mappings, }, @@ -22,23 +23,14 @@ export const fileUpload = kibana => { init(server) { const coreSetup = server.newPlatform.setup.core; + const coreStart = server.newPlatform.start.core; const { usageCollection } = server.newPlatform.setup.plugins; - const pluginsSetup = { + const pluginsStart = { usageCollection, }; - - // legacy dependencies - const __LEGACY = { - route: server.route.bind(server), - plugins: { - elasticsearch: server.plugins.elasticsearch, - }, - savedObjects: { - getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository, - }, - }; - - new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); + const fileUploadPlugin = new FileUploadPlugin(); + fileUploadPlugin.setup(coreSetup); + fileUploadPlugin.start(coreStart, pluginsStart); }, }); }; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts deleted file mode 100644 index 9c1000db8cb56..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function callWithInternalUserFactory(elasticsearchPlugin: any): any; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js deleted file mode 100644 index 2e5431bdd6ce2..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; - -const _callWithInternalUser = once(elasticsearchPlugin => { - const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = elasticsearchPlugin => { - return (...args) => { - return _callWithInternalUser(elasticsearchPlugin)(...args); - }; -}; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts deleted file mode 100644 index 04c5013ed8e67..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts +++ /dev/null @@ -1,22 +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 { callWithInternalUserFactory } from './call_with_internal_user_factory'; - -describe('call_with_internal_user_factory', () => { - describe('callWithInternalUserFactory', () => { - it('should use internal user "admin"', () => { - const callWithInternalUser: any = jest.fn(); - const elasticsearchPlugin: any = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); - callWithInternalUserInstance(); - - expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js index a0b0d2d1c7ce3..bef6c369fd9ac 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js @@ -5,14 +5,17 @@ */ import { once } from 'lodash'; +import { getDataClient } from '../kibana_server_services'; -const callWithRequest = once(elasticsearchPlugin => { - const cluster = elasticsearchPlugin.getCluster('data'); - return cluster.callWithRequest; -}); +const callWithRequest = once(() => getDataClient()); -export const callWithRequestFactory = (elasticsearchPlugin, request) => { +export const callWithRequestFactory = request => { return (...args) => { - return callWithRequest(elasticsearchPlugin)(request, ...args); + return ( + callWithRequest() + .asScoped(request) + // @ts-ignore + .callAsCurrentUser(...args) + ); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/kibana_server_services.js b/x-pack/legacy/plugins/file_upload/server/kibana_server_services.js new file mode 100644 index 0000000000000..104e49015ba80 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/kibana_server_services.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + +let dataClient; + +export const setElasticsearchClientServices = elasticsearch => { + ({ dataClient } = elasticsearch); +}; +export const getDataClient = () => dataClient; + +let internalRepository; +export const setInternalRepository = createInternalRepository => { + internalRepository = createInternalRepository(); +}; +export const getInternalRepository = () => internalRepository; diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js index 23fb8bda897f0..c448676f813ea 100644 --- a/x-pack/legacy/plugins/file_upload/server/plugin.js +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -5,19 +5,23 @@ */ import { initRoutes } from './routes/file_upload'; +import { setElasticsearchClientServices, setInternalRepository } from './kibana_server_services'; import { registerFileUploadUsageCollector } from './telemetry'; export class FileUploadPlugin { - setup(core, plugins, __LEGACY) { - const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; - const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; - const router = core.http.createRouter(); + constructor() { + this.router = null; + } + + setup(core) { + setElasticsearchClientServices(core.elasticsearch); + this.router = core.http.createRouter(); + } - initRoutes(router, elasticsearchPlugin, getSavedObjectsRepository); + start(core, plugins) { + initRoutes(this.router, core.savedObjects.getSavedObjectsRepository); + setInternalRepository(core.savedObjects.createInternalRepository); - registerFileUploadUsageCollector(plugins.usageCollection, { - elasticsearchPlugin, - getSavedObjectsRepository, - }); + registerFileUploadUsageCollector(plugins.usageCollection); } } diff --git a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js index 1c27c2d7d68e9..acbc907729d95 100644 --- a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js +++ b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js @@ -75,7 +75,7 @@ export const idConditionalValidation = (body, boolHasId) => ) .validate(body); -const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepository) => { +const finishValidationAndProcessReq = () => { return async (con, req, { ok, badRequest }) => { const { query: { id }, @@ -86,7 +86,7 @@ const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepos let resp; try { const validIdReqData = idConditionalValidation(body, boolHasId); - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, req); + const callWithRequest = callWithRequestFactory(req); const { importData: importDataFunc } = importDataProvider(callWithRequest); const { index, settings, mappings, ingestPipeline, data } = validIdReqData; @@ -103,7 +103,7 @@ const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepos resp = ok({ body: processedReq }); // If no id's been established then this is a new index, update telemetry if (!boolHasId) { - await updateTelemetry({ elasticsearchPlugin, getSavedObjectsRepository }); + await updateTelemetry(); } } else { resp = badRequest(`Error processing request 1: ${processedReq.error.message}`, ['body']); @@ -115,7 +115,7 @@ const finishValidationAndProcessReq = (elasticsearchPlugin, getSavedObjectsRepos }; }; -export const initRoutes = (router, esPlugin, getSavedObjectsRepository) => { +export const initRoutes = router => { router.post( { path: `${IMPORT_ROUTE}{id?}`, @@ -125,6 +125,6 @@ export const initRoutes = (router, esPlugin, getSavedObjectsRepository) => { }, options, }, - finishValidationAndProcessReq(esPlugin, getSavedObjectsRepository) + finishValidationAndProcessReq() ); }; diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts index a2b359ae11638..2c2b1183fd5bf 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts @@ -9,19 +9,11 @@ import { getTelemetry, initTelemetry } from './telemetry'; const TELEMETRY_TYPE = 'fileUploadTelemetry'; -export function registerFileUploadUsageCollector( - usageCollection: UsageCollectionSetup, - deps: { - elasticsearchPlugin: any; - getSavedObjectsRepository: any; - } -): void { - const { elasticsearchPlugin, getSavedObjectsRepository } = deps; +export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void { const fileUploadUsageCollector = usageCollection.makeUsageCollector({ type: TELEMETRY_TYPE, isReady: () => true, - fetch: async () => - (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry(), + fetch: async () => (await getTelemetry()) || initTelemetry(), }); usageCollection.registerCollector(fileUploadUsageCollector); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts index 1c785d8e7b61c..fadad307c0710 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts @@ -6,8 +6,6 @@ import { getTelemetry, updateTelemetry } from './telemetry'; -const elasticsearchPlugin: any = null; -const getSavedObjectsRepository: any = null; const internalRepository = () => ({ get: jest.fn(() => null), create: jest.fn(() => ({ attributes: 'test' })), @@ -25,7 +23,7 @@ describe('file upload plugin telemetry', () => { describe('getTelemetry', () => { it('should get existing telemetry', async () => { const internalRepo = mockInit(); - await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository, internalRepo); + await getTelemetry(internalRepo); expect(internalRepo.update.mock.calls.length).toBe(0); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); @@ -40,11 +38,7 @@ describe('file upload plugin telemetry', () => { }, }); - await updateTelemetry({ - elasticsearchPlugin, - getSavedObjectsRepository, - internalRepo, - }); + await updateTelemetry(internalRepo); expect(internalRepo.update.mock.calls.length).toBe(1); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts index 5ffa735f4c83a..2978dec7aa68d 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts @@ -5,7 +5,8 @@ */ import _ from 'lodash'; -import { callWithInternalUserFactory } from '../client/call_with_internal_user_factory'; +// @ts-ignore +import { getInternalRepository } from '../kibana_server_services'; export const TELEMETRY_DOC_ID = 'file-upload-telemetry'; @@ -17,27 +18,14 @@ export interface TelemetrySavedObject { attributes: Telemetry; } -export function getInternalRepository( - elasticsearchPlugin: any, - getSavedObjectsRepository: any -): any { - const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); - return getSavedObjectsRepository(callWithInternalUser); -} - export function initTelemetry(): Telemetry { return { filesUploadedTotalCount: 0, }; } -export async function getTelemetry( - elasticsearchPlugin: any, - getSavedObjectsRepository: any, - internalRepo?: object -): Promise { - const internalRepository = - internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); +export async function getTelemetry(internalRepo?: object): Promise { + const internalRepository = internalRepo || getInternalRepository(); let telemetrySavedObject; try { @@ -49,22 +37,9 @@ export async function getTelemetry( return telemetrySavedObject ? telemetrySavedObject.attributes : null; } -export async function updateTelemetry({ - elasticsearchPlugin, - getSavedObjectsRepository, - internalRepo, -}: { - elasticsearchPlugin: any; - getSavedObjectsRepository: any; - internalRepo?: any; -}) { - const internalRepository = - internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); - let telemetry = await getTelemetry( - elasticsearchPlugin, - getSavedObjectsRepository, - internalRepository - ); +export async function updateTelemetry(internalRepo?: any) { + const internalRepository = internalRepo || getInternalRepository(); + let telemetry = await getTelemetry(internalRepository); // Create if doesn't exist if (!telemetry || _.isEmpty(telemetry)) { const newTelemetrySavedObject = await internalRepository.create( From 98aa1d2d4f974f72a9a5397b1b91f11509f6fb7a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sat, 22 Feb 2020 07:29:44 -0700 Subject: [PATCH 051/113] [SIEM] [Case] Enable case by default. Snake to camel on UI (#57936) --- .../siem/public/components/links/index.tsx | 5 +- .../components/navigation/index.test.tsx | 4 +- .../siem/public/containers/case/api.ts | 21 ++-- .../siem/public/containers/case/types.ts | 40 ++++++-- .../public/containers/case/use_get_case.tsx | 8 +- .../public/containers/case/use_get_cases.tsx | 20 ++-- .../containers/case/use_update_case.tsx | 6 +- .../siem/public/containers/case/utils.ts | 33 +++++++ .../components/all_cases/__mock__/index.tsx | 77 +++++++++++++++ .../case/components/all_cases/columns.tsx | 42 +++++--- .../case/components/all_cases/index.test.tsx | 97 +++++++++++++++++++ .../pages/case/components/all_cases/index.tsx | 34 +++---- .../components/case_view/__mock__/index.tsx | 34 +++++++ .../case/components/case_view/index.test.tsx | 82 ++++++++++++++++ .../pages/case/components/case_view/index.tsx | 35 +++++-- .../pages/case/components/create/index.tsx | 4 +- .../pages/case/components/user_list/index.tsx | 4 +- .../public/pages/home/home_navigations.tsx | 2 +- x-pack/plugins/case/server/config.ts | 4 +- x-pack/plugins/case/server/plugin.ts | 1 + .../routes/api/__tests__/update_case.test.ts | 39 +++++++- .../case/server/routes/api/get_all_cases.ts | 7 +- .../plugins/case/server/routes/api/schema.ts | 5 + .../plugins/case/server/routes/api/types.ts | 13 +++ .../case/server/routes/api/update_case.ts | 71 ++++++++++++-- .../plugins/case/server/routes/api/utils.ts | 18 ++++ 26 files changed, 605 insertions(+), 101 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index 4f74f9ff2f5d6..b6548e3e950ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -44,7 +44,10 @@ const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailNam children, detailName, }) => ( - + {children ? children : detailName} ); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index 8eb08bd3d62f0..e1b3951a2317d 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -67,7 +67,7 @@ describe('SIEM Navigation', () => { detailName: undefined, navTabs: { case: { - disabled: true, + disabled: false, href: '#/link-to/case', id: 'case', name: 'Case', @@ -160,7 +160,7 @@ describe('SIEM Navigation', () => { filters: [], navTabs: { case: { - disabled: true, + disabled: false, href: '#/link-to/case', id: 'case', name: 'Case', diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 830e00c70975e..bff3bfd62a85c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -5,12 +5,12 @@ */ import { KibanaServices } from '../../lib/kibana'; -import { AllCases, FetchCasesProps, Case, NewCase, SortFieldCase } from './types'; -import { Direction } from '../../graphql/types'; +import { FetchCasesProps, Case, NewCase, SortFieldCase, AllCases, CaseSnake } from './types'; import { throwIfNotOk } from '../../hooks/api/api'; import { CASES_URL } from './constants'; +import { convertToCamelCase, convertAllCasesToCamel } from './utils'; -export const getCase = async (caseId: string, includeComments: boolean) => { +export const getCase = async (caseId: string, includeComments: boolean): Promise => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'GET', asResponse: true, @@ -19,7 +19,7 @@ export const getCase = async (caseId: string, includeComments: boolean) => { }, }); await throwIfNotOk(response.response); - return response.body!; + return convertToCamelCase(response.body!); }; export const getCases = async ({ @@ -31,7 +31,7 @@ export const getCases = async ({ page: 1, perPage: 20, sortField: SortFieldCase.createdAt, - sortOrder: Direction.desc, + sortOrder: 'desc', }, }: FetchCasesProps): Promise => { const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; @@ -46,7 +46,7 @@ export const getCases = async ({ asResponse: true, }); await throwIfNotOk(response.response); - return response.body!; + return convertAllCasesToCamel(response.body!); }; export const createCase = async (newCase: NewCase): Promise => { @@ -56,18 +56,19 @@ export const createCase = async (newCase: NewCase): Promise => { body: JSON.stringify(newCase), }); await throwIfNotOk(response.response); - return response.body!; + return convertToCamelCase(response.body!); }; export const updateCaseProperty = async ( caseId: string, - updatedCase: Partial + updatedCase: Partial, + version: string ): Promise> => { const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { method: 'PATCH', asResponse: true, - body: JSON.stringify(updatedCase), + body: JSON.stringify({ case: updatedCase, version }), }); await throwIfNotOk(response.response); - return response.body!; + return convertToCamelCase, Partial>(response.body!); }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 0f80b2327a30c..1aea0b0f50a89 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Direction } from '../../graphql/types'; interface FormData { isNew?: boolean; } @@ -15,22 +14,35 @@ export interface NewCase extends FormData { title: string; } -export interface Case { +export interface CaseSnake { case_id: string; created_at: string; - created_by: ElasticUser; + created_by: ElasticUserSnake; description: string; state: string; tags: string[]; title: string; updated_at: string; + version?: string; +} + +export interface Case { + caseId: string; + createdAt: string; + createdBy: ElasticUser; + description: string; + state: string; + tags: string[]; + title: string; + updatedAt: string; + version?: string; } export interface QueryParams { page: number; perPage: number; sortField: SortFieldCase; - sortOrder: Direction; + sortOrder: 'asc' | 'desc'; } export interface FilterOptions { @@ -38,21 +50,33 @@ export interface FilterOptions { tags: string[]; } +export interface AllCasesSnake { + cases: CaseSnake[]; + page: number; + per_page: number; + total: number; +} + export interface AllCases { cases: Case[]; page: number; - per_page: number; + perPage: number; total: number; } export enum SortFieldCase { - createdAt = 'created_at', + createdAt = 'createdAt', state = 'state', - updatedAt = 'updated_at', + updatedAt = 'updatedAt', +} + +export interface ElasticUserSnake { + readonly username: string; + readonly full_name?: string | null; } export interface ElasticUser { readonly username: string; - readonly full_name?: string; + readonly fullName?: string | null; } export interface FetchCasesProps { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index 8cc961c68fdf0..bf76b69ef22d6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -50,16 +50,16 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { } }; const initialData: Case = { - case_id: '', - created_at: '', - created_by: { + caseId: '', + createdAt: '', + createdBy: { username: '', }, description: '', state: '', tags: [], title: '', - updated_at: '', + updatedAt: '', }; export const useGetCase = (caseId: string): [CaseState] => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index db9c07747ba04..4037823ccfc94 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -17,7 +17,6 @@ import { } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; import { getTypedPayload } from './utils'; -import { Direction } from '../../graphql/types'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; @@ -31,16 +30,9 @@ export interface UseGetCasesState { filterOptions: FilterOptions; } -export interface QueryArgs { - page?: number; - perPage?: number; - sortField?: SortFieldCase; - sortOrder?: Direction; -} - export interface Action { type: string; - payload?: AllCases | QueryArgs | FilterOptions; + payload?: AllCases | Partial | FilterOptions; } const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { switch (action.type) { @@ -83,13 +75,13 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS const initialData: AllCases = { page: 0, - per_page: 0, + perPage: 0, total: 0, cases: [], }; export const useGetCases = (): [ UseGetCasesState, - Dispatch>, + Dispatch>>, Dispatch> ] => { const [state, dispatch] = useReducer(dataFetchReducer, { @@ -104,11 +96,11 @@ export const useGetCases = (): [ page: DEFAULT_TABLE_ACTIVE_PAGE, perPage: DEFAULT_TABLE_LIMIT, sortField: SortFieldCase.createdAt, - sortOrder: Direction.desc, + sortOrder: 'desc', }, }); - const [queryParams, setQueryParams] = useState(state.queryParams as QueryArgs); - const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions); + const [queryParams, setQueryParams] = useState>(state.queryParams); + const [filterQuery, setFilters] = useState(state.filterOptions); const [, dispatchToaster] = useStateToaster(); useEffect(() => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 68592c17e58dc..62e3d87b528c0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -96,7 +96,11 @@ export const useUpdateCase = ( const updateData = async (updateKey: keyof Case) => { dispatch({ type: FETCH_INIT }); try { - const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] }); + const response = await updateCaseProperty( + caseId, + { [updateKey]: state.data[updateKey] }, + state.data.version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); dispatch({ type: FETCH_SUCCESS, payload: response }); } catch (error) { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts index 8e6eaca1a8f0c..14a3819bdfdad 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -4,4 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ +import { camelCase, isArray, isObject, set } from 'lodash'; +import { AllCases, AllCasesSnake, Case, CaseSnake } from './types'; + export const getTypedPayload = (a: unknown): T => a as T; + +export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => + arrayOfSnakes.reduce((acc: unknown[], value) => { + if (isArray(value)) { + return [...acc, convertArrayToCamelCase(value)]; + } else if (isObject(value)) { + return [...acc, convertToCamelCase(value)]; + } else { + return [...acc, value]; + } + }, []); + +export const convertToCamelCase = (snakeCase: T): U => + Object.entries(snakeCase).reduce((acc, [key, value]) => { + if (isArray(value)) { + set(acc, camelCase(key), convertArrayToCamelCase(value)); + } else if (isObject(value)) { + set(acc, camelCase(key), convertToCamelCase(value)); + } else { + set(acc, camelCase(key), value); + } + return acc; + }, {} as U); + +export const convertAllCasesToCamel = (snakeCases: AllCasesSnake): AllCases => ({ + cases: snakeCases.cases.map(snakeCase => convertToCamelCase(snakeCase)), + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx new file mode 100644 index 0000000000000..98a67304fcf1f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -0,0 +1,77 @@ +/* + * 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 { SortFieldCase } from '../../../../../containers/case/types'; +import { UseGetCasesState } from '../../../../../containers/case/use_get_cases'; + +export const useGetCasesMockState: UseGetCasesState = { + data: { + cases: [ + { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:23.627Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['defacement'], + title: 'Another horrible breach', + updatedAt: '2020-02-13T19:44:23.627Z', + }, + { + caseId: '362a5c10-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:13.328Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['phishing'], + title: 'Bad email', + updatedAt: '2020-02-13T19:44:13.328Z', + }, + { + caseId: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:11.328Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['phishing'], + title: 'Bad email', + updatedAt: '2020-02-13T19:44:11.328Z', + }, + { + caseId: '31890e90-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:05.563Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'closed', + tags: ['phishing'], + title: 'Uh oh', + updatedAt: '2020-02-18T21:32:24.056Z', + }, + { + caseId: '2f5b3210-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:01.901Z', + createdBy: { username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['phishing'], + title: 'Uh oh', + updatedAt: '2020-02-13T19:44:01.901Z', + }, + ], + page: 1, + perPage: 5, + total: 10, + }, + isLoading: false, + isError: false, + queryParams: { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + filterOptions: { search: '', tags: [] }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 92cd16fd2000e..4c47bf605051d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -14,14 +14,15 @@ import * as i18n from './translations'; export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; -const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? {field} : getEmptyTagValue(); export const getCasesColumns = (): CasesColumns[] => [ { name: i18n.CASE_TITLE, render: (theCase: Case) => { - if (theCase.case_id != null && theCase.title != null) { - return {theCase.title}; + if (theCase.caseId != null && theCase.title != null) { + return {theCase.title}; } return getEmptyTagValue(); }, @@ -34,7 +35,11 @@ export const getCasesColumns = (): CasesColumns[] => [ return ( {tags.map((tag: string, i: number) => ( - + {tag} ))} @@ -46,28 +51,39 @@ export const getCasesColumns = (): CasesColumns[] => [ truncateText: true, }, { - field: 'created_at', + field: 'createdAt', name: i18n.CREATED_AT, sortable: true, - render: (createdAt: Case['created_at']) => { + render: (createdAt: Case['createdAt']) => { if (createdAt != null) { - return ; + return ( + + ); } return getEmptyTagValue(); }, }, { - field: 'created_by.username', + field: 'createdBy.username', name: i18n.REPORTER, - render: (createdBy: Case['created_by']['username']) => renderStringField(createdBy), + render: (createdBy: Case['createdBy']['username']) => + renderStringField(createdBy, `case-table-column-username`), }, { - field: 'updated_at', + field: 'updatedAt', name: i18n.LAST_UPDATED, sortable: true, - render: (updatedAt: Case['updated_at']) => { + render: (updatedAt: Case['updatedAt']) => { if (updatedAt != null) { - return ; + return ( + + ); } return getEmptyTagValue(); }, @@ -76,6 +92,6 @@ export const getCasesColumns = (): CasesColumns[] => [ field: 'state', name: i18n.STATE, sortable: true, - render: (state: Case['state']) => renderStringField(state), + render: (state: Case['state']) => renderStringField(state, `case-table-column-state`), }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx new file mode 100644 index 0000000000000..5a87cf53142f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { mount } from 'enzyme'; +import moment from 'moment-timezone'; +import { AllCases } from './'; +import { TestProviders } from '../../../../mock'; +import { useGetCasesMockState } from './__mock__'; +import * as apiHook from '../../../../containers/case/use_get_cases'; + +describe('AllCases', () => { + const setQueryParams = jest.fn(); + const setFilters = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + jest + .spyOn(apiHook, 'useGetCases') + .mockReturnValue([useGetCasesMockState, setQueryParams, setFilters]); + moment.tz.setDefault('UTC'); + }); + it('should render AllCases', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .prop('href') + ).toEqual(`#/link-to/case/${useGetCasesMockState.data.cases[0].caseId}`); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].title); + expect( + wrapper + .find(`[data-test-subj="case-table-column-state"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].state); + expect( + wrapper + .find(`span[data-test-subj="case-table-column-tags-0"]`) + .first() + .prop('title') + ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-table-column-username"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].createdBy.username); + expect( + wrapper + .find(`[data-test-subj="case-table-column-createdAt"]`) + .first() + .prop('value') + ).toEqual(useGetCasesMockState.data.cases[0].createdAt); + expect( + wrapper + .find(`[data-test-subj="case-table-column-updatedAt"]`) + .first() + .prop('value') + ).toEqual(useGetCasesMockState.data.cases[0].updatedAt); + + expect( + wrapper + .find(`[data-test-subj="case-table-case-count"]`) + .first() + .text() + ).toEqual('Showing 10 cases'); + }); + it('should tableHeaderSortButton AllCases', () => { + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="tableHeaderCell_state_5"] [data-test-subj="tableHeaderSortButton"]') + .simulate('click'); + expect(setQueryParams).toBeCalledWith({ + page: 1, + perPage: 5, + sortField: 'state', + sortOrder: 'asc', + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index b1dd39c95e191..3253a036c2990 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -18,7 +18,6 @@ import * as i18n from './translations'; import { getCasesColumns } from './columns'; import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; -import { Direction } from '../../../../graphql/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; @@ -32,7 +31,16 @@ import { UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; import { getCreateCaseUrl } from '../../../../components/link_to'; - +const getSortField = (field: string): SortFieldCase => { + if (field === SortFieldCase.createdAt) { + return SortFieldCase.createdAt; + } else if (field === SortFieldCase.state) { + return SortFieldCase.state; + } else if (field === SortFieldCase.updatedAt) { + return SortFieldCase.updatedAt; + } + return SortFieldCase.createdAt; +}; export const AllCases = React.memo(() => { const [ { data, isLoading, queryParams, filterOptions }, @@ -44,24 +52,10 @@ export const AllCases = React.memo(() => { ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; if (sort) { - let newSort; - switch (sort.field) { - case 'state': - newSort = SortFieldCase.state; - break; - case 'created_at': - newSort = SortFieldCase.createdAt; - break; - case 'updated_at': - newSort = SortFieldCase.updatedAt; - break; - default: - newSort = SortFieldCase.createdAt; - } newQueryParams = { ...newQueryParams, - sortField: newSort, - sortOrder: sort.direction as Direction, + sortField: getSortField(sort.field), + sortOrder: sort.direction, }; } if (page) { @@ -114,7 +108,9 @@ export const AllCases = React.memo(() => { - {i18n.SHOWING_CASES(data.total ?? 0)} + + {i18n.SHOWING_CASES(data.total ?? 0)} + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx new file mode 100644 index 0000000000000..7480c4fc4bb2a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseProps } from '../index'; +import { Case } from '../../../../../containers/case/types'; + +export const caseProps: CaseProps = { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + initialData: { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:23.627Z', + createdBy: { fullName: null, username: 'elastic' }, + description: 'Security banana Issue', + state: 'open', + tags: ['defacement'], + title: 'Another horrible breach!!', + updatedAt: '2020-02-19T15:02:57.995Z', + }, + isLoading: false, +}; + +export const data: Case = { + caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + createdAt: '2020-02-13T19:44:23.627Z', + createdBy: { username: 'elastic', fullName: null }, + description: 'Security banana Issue', + state: 'open', + tags: ['defacement'], + title: 'Another horrible breach!!', + updatedAt: '2020-02-19T15:02:57.995Z', +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx new file mode 100644 index 0000000000000..a9e694bad705d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { mount } from 'enzyme'; +import { CaseComponent } from './'; +import * as apiHook from '../../../../containers/case/use_update_case'; +import { caseProps, data } from './__mock__'; +import { TestProviders } from '../../../../mock'; + +describe('CaseView ', () => { + const dispatchUpdateCaseProperty = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(apiHook, 'useUpdateCase').mockReturnValue([{ data }, dispatchUpdateCaseProperty]); + }); + + it('should render CaseComponent', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="case-view-title"]`) + .first() + .prop('title') + ).toEqual(data.title); + expect( + wrapper + .find(`[data-test-subj="case-view-state"]`) + .first() + .text() + ).toEqual(data.state); + expect( + wrapper + .find(`[data-test-subj="case-view-tag-list"] .euiBadge__text`) + .first() + .text() + ).toEqual(data.tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-view-username"]`) + .first() + .text() + ).toEqual(data.createdBy.username); + expect( + wrapper + .find(`[data-test-subj="case-view-createdAt"]`) + .first() + .prop('value') + ).toEqual(data.createdAt); + expect( + wrapper + .find(`[data-test-subj="case-view-description"]`) + .first() + .prop('raw') + ).toEqual(data.description); + }); + + it('should dispatch update state when button is toggled', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('input[data-test-subj="toggle-case-state"]') + .simulate('change', { target: { value: false } }); + + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + updateKey: 'state', + updateValue: 'closed', + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index a92cf99097fce..5cd71c5855d34 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -60,13 +60,13 @@ const BackgroundWrapper = styled.div` `} `; -interface CasesProps { +export interface CaseProps { caseId: string; initialData: Case; isLoading: boolean; } -export const Cases = React.memo(({ caseId, initialData, isLoading }) => { +export const CaseComponent = React.memo(({ caseId, initialData, isLoading }) => { const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); const [isEditDescription, setIsEditDescription] = useState(false); const [isEditTags, setIsEditTags] = useState(false); @@ -162,14 +162,14 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) ]; const userActions = [ { - avatarName: data.created_by.username, + avatarName: data.createdBy.username, title: (

- {`${data.created_by.username}`} + {`${data.createdBy.username}`} {` ${i18n.ADDED_DESCRIPTION} `}{' '} - + {/* STEPH FIX come back and add label `on` */}

@@ -206,7 +206,7 @@ export const Cases = React.memo(({ caseId, initialData, isLoading })
) : ( - + ), }, ]; @@ -229,6 +229,7 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) href: getCaseUrl(), text: i18n.BACK_TO_ALL, }} + data-test-subj="case-view-title" titleNode={titleNode} title={title} > @@ -239,13 +240,21 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) {i18n.STATUS} - {data.state} + + {data.state} + {i18n.CASE_OPENED} - + @@ -255,6 +264,7 @@ export const Cases = React.memo(({ caseId, initialData, isLoading }) (({ caseId, initialData, isLoading }) - + { ); } - return ; + return ; }); CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 9fd1525003b0b..7d79e287b22e7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -48,8 +48,8 @@ export const Create = React.memo(() => { } }, [form]); - if (newCase && newCase.case_id) { - return ; + if (newCase && newCase.caseId) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index b80ee58f8abbf..33e0a9541c5b4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -42,7 +42,7 @@ const renderUsers = (users: ElasticUser[]) => {

- {username} + {username}

@@ -50,7 +50,7 @@ const renderUsers = (users: ElasticUser[]) => {
window.alert('Email clicked')} + onClick={() => {}} // TO DO iconType="email" aria-label="email" /> diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index 42d333f4f893e..a087dca38de00 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -55,7 +55,7 @@ export const navTabs: SiemNavTab = { id: SiemPageName.case, name: i18n.CASE, href: getCaseUrl(), - disabled: true, + disabled: false, urlKey: 'case', }, }; diff --git a/x-pack/plugins/case/server/config.ts b/x-pack/plugins/case/server/config.ts index a7cb117198f9b..8ff9a793b17dc 100644 --- a/x-pack/plugins/case/server/config.ts +++ b/x-pack/plugins/case/server/config.ts @@ -7,9 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), - indexPattern: schema.string({ defaultValue: '.case-test-2' }), - secret: schema.string({ defaultValue: 'Cool secret huh?' }), + enabled: schema.boolean({ defaultValue: true }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 37d087433a2ed..5ca640f0b25c3 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -34,6 +34,7 @@ export class CasePlugin { if (!config.enabled) { return; } + const service = new CaseService(this.log); this.log.debug( diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts index 23283d7f8a5be..25d5cafb4bb06 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts @@ -27,7 +27,8 @@ describe('UPDATE case', () => { id: 'mock-id-1', }, body: { - state: 'closed', + case: { state: 'closed' }, + version: 'WzAsMV0=', }, }); @@ -38,6 +39,42 @@ describe('UPDATE case', () => { expect(typeof response.payload.updated_at).toBe('string'); expect(response.payload.state).toEqual('closed'); }); + it(`Fails with 409 if version does not match`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'patch', + params: { + id: 'mock-id-1', + }, + body: { + case: { state: 'closed' }, + version: 'badv=', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); + it(`Fails with 406 if updated field is unchanged`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases/{id}', + method: 'patch', + params: { + id: 'mock-id-1', + }, + body: { + case: { state: 'open' }, + version: 'WzAsMV0=', + }, + }); + + const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases)); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(406); + }); it(`Returns an error if updateCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts index 09075a32ac377..ba26a07dc2394 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_cases.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { formatAllCases, wrapError } from './utils'; +import { formatAllCases, sortToSnake, wrapError } from './utils'; import { SavedObjectsFindOptionsSchema } from './schema'; import { AllCases } from './types'; @@ -23,7 +23,10 @@ export function initGetAllCasesApi({ caseService, router }: RouteDeps) { const args = request.query ? { client: context.core.savedObjects.client, - options: request.query, + options: { + ...request.query, + sortField: sortToSnake(request.query.sortField ?? ''), + }, } : { client: context.core.savedObjects.client, diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 962dc474254f0..468abc8e7226f 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -41,6 +41,11 @@ export const UpdatedCaseSchema = schema.object({ title: schema.maybe(schema.string()), }); +export const UpdateCaseArguments = schema.object({ + case: UpdatedCaseSchema, + version: schema.string(), +}); + export const SavedObjectsFindOptionsSchema = schema.object({ defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), fields: schema.maybe(schema.arrayOf(schema.string())), diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index 2d1a88bcf1429..5f1c207bf9829 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -32,12 +32,14 @@ export interface CaseAttributes extends NewCaseType, SavedObjectAttributes { export type FlattenedCaseSavedObject = CaseAttributes & { case_id: string; + version: string; comments: FlattenedCommentSavedObject[]; }; export type FlattenedCasesSavedObject = Array< CaseAttributes & { case_id: string; + version: string; // TO DO it is partial because we need to add it the commentCount commentCount?: number; } @@ -52,6 +54,7 @@ export interface AllCases { export type FlattenedCommentSavedObject = CommentAttributes & { comment_id: string; + version: string; // TO DO We might want to add the case_id where this comment is related too }; @@ -69,3 +72,13 @@ export interface UpdatedCaseType { title?: UpdatedCaseTyped['title']; updated_at: string; } + +export enum SortFieldCase { + createdAt = 'created_at', + state = 'state', + updatedAt = 'updated_at', +} + +export type Writable = { + -readonly [K in keyof T]: T[K]; +}; diff --git a/x-pack/plugins/case/server/routes/api/update_case.ts b/x-pack/plugins/case/server/routes/api/update_case.ts index 2a814c7259e4a..1c1a56dfe9b3a 100644 --- a/x-pack/plugins/case/server/routes/api/update_case.ts +++ b/x-pack/plugins/case/server/routes/api/update_case.ts @@ -5,9 +5,17 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObject } from 'kibana/server'; +import Boom from 'boom'; +import { difference } from 'lodash'; import { wrapError } from './utils'; import { RouteDeps } from '.'; -import { UpdatedCaseSchema } from './schema'; +import { UpdateCaseArguments } from './schema'; +import { CaseAttributes, UpdatedCaseTyped, Writable } from './types'; + +interface UpdateCase extends Writable { + [key: string]: any; +} export function initUpdateCaseApi({ caseService, router }: RouteDeps) { router.patch( @@ -17,23 +25,70 @@ export function initUpdateCaseApi({ caseService, router }: RouteDeps) { params: schema.object({ id: schema.string(), }), - body: UpdatedCaseSchema, + body: UpdateCaseArguments, }, }, async (context, request, response) => { + let theCase: SavedObject; try { - const updatedCase = await caseService.updateCase({ + theCase = await caseService.getCase({ client: context.core.savedObjects.client, caseId: request.params.id, - updatedAttributes: { - ...request.body, - updated_at: new Date().toISOString(), - }, }); - return response.ok({ body: updatedCase.attributes }); } catch (error) { return response.customError(wrapError(error)); } + + if (request.body.version !== theCase.version) { + return response.customError( + wrapError( + Boom.conflict( + 'This case has been updated. Please refresh before saving additional updates.' + ) + ) + ); + } + const currentCase = theCase.attributes; + const updateCase: Partial = Object.entries(request.body.case).reduce( + (acc, [key, value]) => { + const currentValue = currentCase[key]; + if ( + Array.isArray(value) && + Array.isArray(currentValue) && + difference(value, currentValue).length !== 0 + ) { + return { + ...acc, + [key]: value, + }; + } else if (value !== currentCase[key]) { + return { + ...acc, + [key]: value, + }; + } + return acc; + }, + {} + ); + if (Object.keys(updateCase).length > 0) { + try { + const updatedCase = await caseService.updateCase({ + client: context.core.savedObjects.client, + caseId: request.params.id, + updatedAttributes: { + ...updateCase, + updated_at: new Date().toISOString(), + }, + }); + return response.ok({ body: { ...updatedCase.attributes, version: updatedCase.version } }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + return response.customError( + wrapError(Boom.notAcceptable('All update fields are identical to current version.')) + ); } ); } diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 51944b04836ab..32de41e1c01c5 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -20,6 +20,7 @@ import { AllCases, NewCaseType, NewCommentType, + SortFieldCase, UserType, } from './types'; @@ -80,6 +81,7 @@ export const flattenCaseSavedObject = ( comments: Array> ): FlattenedCaseSavedObject => ({ case_id: savedObject.id, + version: savedObject.version ? savedObject.version : '0', comments: flattenCommentSavedObjects(comments), ...savedObject.attributes, }); @@ -107,5 +109,21 @@ export const flattenCommentSavedObject = ( savedObject: SavedObject ): FlattenedCommentSavedObject => ({ comment_id: savedObject.id, + version: savedObject.version ? savedObject.version : '0', ...savedObject.attributes, }); + +export const sortToSnake = (sortField: string): SortFieldCase => { + switch (sortField) { + case 'state': + return SortFieldCase.state; + case 'createdAt': + case 'created_at': + return SortFieldCase.createdAt; + case 'updatedAt': + case 'updated_at': + return SortFieldCase.updatedAt; + default: + return SortFieldCase.createdAt; + } +}; From db0a9cc61ef78b7865e74862895a5b63fe50154e Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Mon, 24 Feb 2020 14:03:16 +0300 Subject: [PATCH 052/113] [Bug fix] Update nav link when it belongs to the same plugin (#58008) * Update nav link when it belongs to the same plugin * Move the plugin name check to history listener * Add isUrlBelongsToApp function * Code review comments * Update unit tests Co-authored-by: Elastic Machine --- .../dashboard/np_ready/dashboard_constants.ts | 2 + .../kibana/public/dashboard/plugin.ts | 15 +++- .../url/kbn_url_tracker.test.ts | 79 ++++++++++++++----- .../state_management/url/kbn_url_tracker.ts | 19 ++++- 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts index fe42e07912799..0820ebd371004 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts @@ -23,6 +23,8 @@ export const DashboardConstants = { CREATE_NEW_DASHBOARD_URL: '/dashboard', ADD_EMBEDDABLE_ID: 'addEmbeddableId', ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + DASHBOARDS_ID: 'dashboards', + DASHBOARD_ID: 'dashboard', }; export function createDashboardEditUrl(id: string) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 7d330676e79ed..7d64ee969212f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -83,7 +83,14 @@ export class DashboardPlugin implements Plugin { ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), - defaultSubUrl: '#/dashboards', + defaultSubUrl: `#${DashboardConstants.LANDING_PAGE_PATH}`, + shouldTrackUrlUpdate: pathname => { + const targetAppName = pathname.split('/')[1]; + return ( + targetAppName === DashboardConstants.DASHBOARDS_ID || + targetAppName === DashboardConstants.DASHBOARD_ID + ); + }, storageKey: 'lastUrl:dashboard', navLinkUpdater$: this.appStateUpdater, toastNotifications: core.notifications.toasts, @@ -150,15 +157,15 @@ export class DashboardPlugin implements Plugin { }; kibanaLegacy.registerLegacyApp({ ...app, - id: 'dashboard', + id: DashboardConstants.DASHBOARD_ID, // only register the updater in once app, otherwise all updates would happen twice updater$: this.appStateUpdater.asObservable(), navLinkId: 'kibana:dashboard', }); - kibanaLegacy.registerLegacyApp({ ...app, id: 'dashboards' }); + kibanaLegacy.registerLegacyApp({ ...app, id: DashboardConstants.DASHBOARDS_ID }); home.featureCatalogue.register({ - id: 'dashboard', + id: DashboardConstants.DASHBOARD_ID, title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', { defaultMessage: 'Dashboard', }), diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts index 4cf74d991ceb9..701154c06a2ff 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts @@ -38,7 +38,7 @@ describe('kbnUrlTracker', () => { let navLinkUpdaterSubject: BehaviorSubject<(app: AppBase) => { activeUrl?: string } | undefined>; let toastService: jest.Mocked; - function createTracker() { + function createTracker(shouldTrackUrlUpdate?: (pathname: string) => boolean) { urlTracker = createKbnUrlTracker({ baseUrl: '/app/test', defaultSubUrl: '#/start', @@ -57,6 +57,7 @@ describe('kbnUrlTracker', () => { ], navLinkUpdater$: navLinkUpdaterSubject, toastNotifications: toastService, + shouldTrackUrlUpdate, }); } @@ -82,44 +83,44 @@ describe('kbnUrlTracker', () => { }); test('set nav link to session storage value if defined', () => { - storage.setItem('storageKey', '#/deep/path'); + storage.setItem('storageKey', '#/start/deep/path'); createTracker(); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path'); }); test('set nav link to default if app gets mounted', () => { - storage.setItem('storageKey', '#/deep/path'); + storage.setItem('storageKey', '#/start/deep/path'); createTracker(); urlTracker.appMounted(); expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); }); test('keep nav link to default if path gets changed while app mounted', () => { - storage.setItem('storageKey', '#/deep/path'); + storage.setItem('storageKey', '#/start/deep/path'); createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); + history.push('/start/deep/path/2'); expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); }); test('change nav link to last visited url within app after unmount', () => { createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); - history.push('/deep/path/3'); + history.push('/start/deep/path/2'); + history.push('/start/deep/path/3'); urlTracker.appUnMounted(); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path/3'); }); test('unhash all urls that are recorded while app is mounted', () => { (unhashUrl as jest.Mock).mockImplementation(x => x + '?unhashed'); createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); - history.push('/deep/path/3'); + history.push('/start/deep/path/2'); + history.push('/start/deep/path/3'); urlTracker.appUnMounted(); expect(unhashUrl).toHaveBeenCalledTimes(2); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3?unhashed'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path/3?unhashed'); }); test('show warning and use hashed url if unhashing does not work', () => { @@ -128,17 +129,17 @@ describe('kbnUrlTracker', () => { }); createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); + history.push('/start/deep/path/2'); urlTracker.appUnMounted(); - expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/2'); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path/2'); expect(toastService.addDanger).toHaveBeenCalledWith('unhash broke'); }); test('change nav link back to default if app gets mounted again', () => { createTracker(); urlTracker.appMounted(); - history.push('/deep/path/2'); - history.push('/deep/path/3'); + history.push('/start/deep/path/2'); + history.push('/start/deep/path/3'); urlTracker.appUnMounted(); urlTracker.appMounted(); expect(getActiveNavLinkUrl()).toEqual('/app/test#/start'); @@ -151,11 +152,11 @@ describe('kbnUrlTracker', () => { }); test('update state param without overwriting rest of the url when app is not mounted', () => { - storage.setItem('storageKey', '#/deep/path?extrastate=1'); + storage.setItem('storageKey', '#/start/deep/path?extrastate=1'); createTracker(); state1Subject.next({ key1: 'abc' }); expect(getActiveNavLinkUrl()).toMatchInlineSnapshot( - `"/app/test#/deep/path?extrastate=1&state1=(key1:abc)"` + `"/app/test#/start/deep/path?extrastate=1&state1=(key1:abc)"` ); }); @@ -184,7 +185,45 @@ describe('kbnUrlTracker', () => { test('set url to storage when setActiveUrl was called', () => { createTracker(); - urlTracker.setActiveUrl('/deep/path/4'); - expect(storage.getItem('storageKey')).toEqual('#/deep/path/4'); + urlTracker.setActiveUrl('/start/deep/path/4'); + expect(storage.getItem('storageKey')).toEqual('#/start/deep/path/4'); + }); + + describe('shouldTrackUrlUpdate', () => { + test('change nav link when shouldTrackUrlUpdate is not overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(); + urlTracker.appMounted(); + history.push('/start/path'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/path'); + }); + + test('not change nav link when shouldTrackUrlUpdate is not overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(); + urlTracker.appMounted(); + history.push('/setup/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path'); + }); + + test('change nav link when shouldTrackUrlUpdate is overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(() => true); + urlTracker.appMounted(); + history.push('/setup/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/setup/path/2'); + }); + + test('not change nav link when shouldTrackUrlUpdate is overridden', () => { + storage.setItem('storageKey', '#/start/deep/path'); + createTracker(() => false); + urlTracker.appMounted(); + history.push('/setup/path/2'); + urlTracker.appUnMounted(); + expect(getActiveNavLinkUrl()).toEqual('/app/test#/start/deep/path'); + }); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts index 2edd135c184ec..b778535a2d428 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -58,6 +58,12 @@ export function createKbnUrlTracker({ toastNotifications, history, storage, + shouldTrackUrlUpdate = pathname => { + const currentAppName = defaultSubUrl.slice(2); // cut hash and slash symbols + const targetAppName = pathname.split('/')[1]; + + return currentAppName === targetAppName; + }, }: { /** * Base url of the current app. This will be used as a prefix for the @@ -82,7 +88,7 @@ export function createKbnUrlTracker({ stateUpdate$: Observable; }>; /** - * Key used to store the current sub url in session storage. This key should only be used for one active url tracker at any given ntime. + * Key used to store the current sub url in session storage. This key should only be used for one active url tracker at any given time. */ storageKey: string; /** @@ -101,6 +107,13 @@ export function createKbnUrlTracker({ * Storage object to use to persist currently active url. If this isn't provided, the browser wide session storage instance will be used. */ storage?: Storage; + /** + * Checks if pathname belongs to current app. It's used in history listener to define whether it's necessary to set pathname as active url or not. + * The default implementation compares the app name to the first part of pathname. Consumers can override this function for more complex cases. + * + * @param {string} pathname A location's pathname which comes to history listener + */ + shouldTrackUrlUpdate?: (pathname: string) => boolean; }): KbnUrlTracker { const historyInstance = history || createHashHistory(); const storageInstance = storage || sessionStorage; @@ -148,7 +161,9 @@ export function createKbnUrlTracker({ unsubscribe(); // track current hash when within app unsubscribeURLHistory = historyInstance.listen(location => { - setActiveUrl(location.pathname + location.search); + if (shouldTrackUrlUpdate(location.pathname)) { + setActiveUrl(location.pathname + location.search); + } }); } From e64eff0a3d5fde205256ab74b731a63766822f9a Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 24 Feb 2020 06:02:27 -0800 Subject: [PATCH 053/113] Temporarily removes kbn-optimizer cache key tests (#58318) While we investigate why they are interfering with other tests. Signed-off-by: Tyler Smalley Co-authored-by: Elastic Machine --- .../src/optimizer/cache_keys.test.ts | 206 ------------------ 1 file changed, 206 deletions(-) delete mode 100644 packages/kbn-optimizer/src/optimizer/cache_keys.test.ts diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts deleted file mode 100644 index 2337017f54ed8..0000000000000 --- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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 Path from 'path'; - -import jestDiff from 'jest-diff'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; - -import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; -import { OptimizerConfig } from './optimizer_config'; - -jest.mock('./get_changes.ts', () => ({ - getChanges: async () => - new Map([ - ['/foo/bar/a', 'modified'], - ['/foo/bar/b', 'modified'], - ['/foo/bar/c', 'deleted'], - ]), -})); - -jest.mock('./get_mtimes.ts', () => ({ - getMtimes: async (paths: string[]) => new Map(paths.map(path => [path, 12345])), -})); - -jest.mock('execa'); - -jest.mock('fs', () => { - const realFs = jest.requireActual('fs'); - jest.spyOn(realFs, 'readFile'); - return realFs; -}); - -expect.addSnapshotSerializer(createAbsolutePathSerializer()); - -jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { - expect(cmd).toBe('git'); - expect(args).toEqual([ - 'log', - '-n', - '1', - '--pretty=format:%H', - '--', - expect.stringContaining('kbn-optimizer'), - ]); - expect(opts).toEqual({ - cwd: REPO_ROOT, - }); - - return { - stdout: '', - }; -}); - -describe('getOptimizerCacheKey()', () => { - it('uses latest commit, bootstrap cache, and changed files to create unique value', async () => { - jest - .requireMock('fs') - .readFile.mockImplementation( - (path: string, enc: string, cb: (err: null, file: string) => void) => { - expect(path).toBe( - Path.resolve(REPO_ROOT, 'packages/kbn-optimizer/target/.bootstrap-cache') - ); - expect(enc).toBe('utf8'); - cb(null, ''); - } - ); - - const config = OptimizerConfig.create({ - repoRoot: REPO_ROOT, - }); - - await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` - Object { - "bootstrap": "", - "deletedPaths": Array [ - "/foo/bar/c", - ], - "lastCommit": "", - "modifiedTimes": Object { - "/foo/bar/a": 12345, - "/foo/bar/b": 12345, - }, - "workerConfig": Object { - "browserslistEnv": "dev", - "cache": true, - "dist": false, - "optimizerCacheKey": "♻", - "profileWebpack": false, - "repoRoot": , - "watch": false, - }, - } - `); - }); -}); - -describe('diffCacheKey()', () => { - it('returns undefined if values are equal', () => { - expect(diffCacheKey('1', '1')).toBe(undefined); - expect(diffCacheKey(1, 1)).toBe(undefined); - expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { a: 'b' }])).toBe(undefined); - expect( - diffCacheKey( - { - a: '1', - b: '2', - }, - { - b: '2', - a: '1', - } - ) - ).toBe(undefined); - }); - - it('returns a diff if the values are different', () => { - expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { b: 'a' }])).toMatchInlineSnapshot(` - "- Expected - + Received - -  Array [ -  \\"1\\", -  \\"2\\", -  Object { - - \\"a\\": \\"b\\", - + \\"b\\": \\"a\\", -  }, -  ]" - `); - expect( - diffCacheKey( - { - a: '1', - b: '1', - }, - { - b: '2', - a: '2', - } - ) - ).toMatchInlineSnapshot(` - "- Expected - + Received - -  Object { - - \\"a\\": \\"1\\", - - \\"b\\": \\"1\\", - + \\"a\\": \\"2\\", - + \\"b\\": \\"2\\", -  }" - `); - }); -}); - -describe('reformatJestDiff()', () => { - it('reformats large jestDiff output to focus on the changed lines', () => { - const diff = jestDiff( - { - a: ['1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1', '1', '1', '1', '1', '1'], - }, - { - b: ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1'], - } - ); - - expect(reformatJestDiff(diff)).toMatchInlineSnapshot(` - "- Expected - + Received - -  Object { - - \\"a\\": Array [ - + \\"b\\": Array [ -  \\"1\\", -  \\"1\\", -  ... -  \\"1\\", -  \\"1\\", - - \\"2\\", -  \\"1\\", -  \\"1\\", -  ... -  \\"1\\", -  \\"1\\", - + \\"2\\", -  \\"1\\", -  \\"1\\", -  ..." - `); - }); -}); From 581f1bd6e9329b66db5bc3cb3348475595db5aa2 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 24 Feb 2020 16:26:34 +0100 Subject: [PATCH 054/113] Expressions debug mode (#57841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add ability to execute expression in debug mode * feat: 🎸 store resolved arguments in debug information * feat: 🎸 track function execution time and set "success" flag * feat: 🎸 store debug information for functions that throw * feat: 🎸 use performance.now, safe "fn" reference, fix typo --- src/plugins/expressions/common/ast/types.ts | 53 ++++ .../common/execution/execution.test.ts | 253 +++++++++++++++++- .../expressions/common/execution/execution.ts | 67 ++++- .../expressions/common/executor/executor.ts | 26 +- 4 files changed, 379 insertions(+), 20 deletions(-) diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts index 82a7578dd4b89..0b505f117a580 100644 --- a/src/plugins/expressions/common/ast/types.ts +++ b/src/plugins/expressions/common/ast/types.ts @@ -17,6 +17,9 @@ * under the License. */ +import { ExpressionValue, ExpressionValueError } from '../expression_types'; +import { ExpressionFunction } from '../../public'; + export type ExpressionAstNode = | ExpressionAstExpression | ExpressionAstFunction @@ -31,6 +34,56 @@ export interface ExpressionAstFunction { type: 'function'; function: string; arguments: Record; + + /** + * Debug information added to each function when expression is executed in *debug mode*. + */ + debug?: ExpressionAstFunctionDebug; +} + +export interface ExpressionAstFunctionDebug { + /** + * True if function successfully returned output, false if function threw. + */ + success: boolean; + + /** + * Reference to the expression function this AST node represents. + */ + fn: ExpressionFunction; + + /** + * Input that expression function received as its first argument. + */ + input: ExpressionValue; + + /** + * Map of resolved arguments expression function received as its second argument. + */ + args: Record; + + /** + * Result returned by the expression function. Including an error result + * if it was returned by the function (not thrown). + */ + output?: ExpressionValue; + + /** + * Error that function threw normalized to `ExpressionValueError`. + */ + error?: ExpressionValueError; + + /** + * Raw error that was thrown by the function, if any. + */ + rawError?: any | Error; + + /** + * Time in milliseconds it took to execute the function. Duration can be + * `undefined` if error happened during argument resolution, because function + * timing starts after the arguments have been resolved. + */ + duration: number | undefined; } export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression; diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index b6c1721e33eef..f6ff9efca848b 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -18,20 +18,28 @@ */ import { Execution } from './execution'; -import { parseExpression } from '../ast'; +import { parseExpression, ExpressionAstExpression } from '../ast'; import { createUnitTestExecutor } from '../test_helpers'; import { ExpressionFunctionDefinition } from '../../public'; import { ExecutionContract } from './execution_contract'; +beforeAll(() => { + if (typeof performance === 'undefined') { + (global as any).performance = { now: Date.now }; + } +}); + const createExecution = ( expression: string = 'foo bar=123', - context: Record = {} + context: Record = {}, + debug: boolean = false ) => { const executor = createUnitTestExecutor(); const execution = new Execution({ executor, ast: parseExpression(expression), context, + debug, }); return execution; }; @@ -406,4 +414,245 @@ describe('Execution', () => { }); }); }); + + describe('debug mode', () => { + test('can execute expression in debug mode', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 5, + }); + }); + + test('can execute expression with sub-expression in debug mode', async () => { + const execution = createExecution( + 'add val={var_set name=foo value=5 | var name=foo} | add val=10', + {}, + true + ); + execution.start(0); + const result = await execution.result; + + expect(result).toEqual({ + type: 'num', + value: 15, + }); + }); + + describe('when functions succeed', () => { + test('sets "success" flag on all functions to true', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(node.debug?.success).toBe(true); + } + }); + + test('stores "fn" reference to the function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(node.debug?.fn.name).toBe('add'); + } + }); + + test('saves duration it took to execute each function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(typeof node.debug?.duration).toBe('number'); + expect(node.debug?.duration).toBeLessThan(100); + expect(node.debug?.duration).toBeGreaterThanOrEqual(0); + } + }); + + test('sets duration to 10 milliseconds when function executes 10 milliseconds', async () => { + const execution = createExecution('sleep 10', {}, true); + execution.start(-1); + await execution.result; + + const node = execution.state.get().ast.chain[0]; + expect(typeof node.debug?.duration).toBe('number'); + expect(node.debug?.duration).toBeLessThan(50); + expect(node.debug?.duration).toBeGreaterThanOrEqual(5); + }); + + test('adds .debug field in expression AST on each executed function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + for (const node of execution.state.get().ast.chain) { + expect(typeof node.debug).toBe('object'); + expect(!!node.debug).toBe(true); + } + }); + + test('stores input of each function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + const { chain } = execution.state.get().ast; + + expect(chain[0].debug!.input).toBe(-1); + expect(chain[1].debug!.input).toEqual({ + type: 'num', + value: 0, + }); + expect(chain[2].debug!.input).toEqual({ + type: 'num', + value: 2, + }); + }); + + test('stores output of each function', async () => { + const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); + execution.start(-1); + await execution.result; + + const { chain } = execution.state.get().ast; + + expect(chain[0].debug!.output).toEqual({ + type: 'num', + value: 0, + }); + expect(chain[1].debug!.output).toEqual({ + type: 'num', + value: 2, + }); + expect(chain[2].debug!.output).toEqual({ + type: 'num', + value: 5, + }); + }); + + test('stores resolved arguments of a function', async () => { + const execution = createExecution( + 'add val={var_set name=foo value=5 | var name=foo} | add val=10', + {}, + true + ); + execution.start(-1); + await execution.result; + + const { chain } = execution.state.get().ast; + + expect(chain[0].debug!.args).toEqual({ + val: 5, + }); + + expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual( + { + name: 'foo', + value: 5, + } + ); + }); + + test('store debug information about sub-expressions', async () => { + const execution = createExecution( + 'add val={var_set name=foo value=5 | var name=foo} | add val=10', + {}, + true + ); + execution.start(0); + await execution.result; + + const { chain } = execution.state.get().ast.chain[0].arguments + .val[0] as ExpressionAstExpression; + + expect(typeof chain[0].debug).toBe('object'); + expect(typeof chain[1].debug).toBe('object'); + expect(!!chain[0].debug).toBe(true); + expect(!!chain[1].debug).toBe(true); + + expect(chain[0].debug!.input).toBe(0); + expect(chain[0].debug!.output).toBe(0); + expect(chain[1].debug!.input).toBe(0); + expect(chain[1].debug!.output).toBe(5); + }); + }); + + describe('when expression throws', () => { + const executor = createUnitTestExecutor(); + executor.registerFunction({ + name: 'throws', + args: {}, + help: '', + fn: () => { + throw new Error('foo'); + }, + }); + + test('stores debug information up until the function that throws', async () => { + const execution = new Execution({ + executor, + ast: parseExpression('add val=1 | throws | add val=3'), + debug: true, + }); + execution.start(0); + await execution.result; + + const node1 = execution.state.get().ast.chain[0]; + const node2 = execution.state.get().ast.chain[1]; + const node3 = execution.state.get().ast.chain[2]; + + expect(typeof node1.debug).toBe('object'); + expect(typeof node2.debug).toBe('object'); + expect(typeof node3.debug).toBe('undefined'); + }); + + test('stores error thrown in debug information', async () => { + const execution = new Execution({ + executor, + ast: parseExpression('add val=1 | throws | add val=3'), + debug: true, + }); + execution.start(0); + await execution.result; + + const node2 = execution.state.get().ast.chain[1]; + + expect(node2.debug?.error).toMatchObject({ + type: 'error', + error: { + message: '[throws] > foo', + }, + }); + expect(node2.debug?.rawError).toBeInstanceOf(Error); + }); + + test('sets .debug object to expected shape', async () => { + const execution = new Execution({ + executor, + ast: parseExpression('add val=1 | throws | add val=3'), + debug: true, + }); + execution.start(0); + await execution.result; + + const node2 = execution.state.get().ast.chain[1]; + + expect(node2.debug).toMatchObject({ + success: false, + fn: expect.any(Object), + input: expect.any(Object), + args: expect.any(Object), + error: expect.any(Object), + rawError: expect.any(Error), + duration: expect.any(Number), + }); + }); + }); + }); }); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 2a272e187cffc..7e7df822724ae 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer } from '../../../kibana_utils/common'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; -import { isExpressionValueError } from '../expression_types/specs/error'; +import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { ExpressionAstExpression, ExpressionAstFunction, @@ -32,7 +32,7 @@ import { parseExpression, } from '../ast'; import { ExecutionContext, DefaultInspectorAdapters } from './types'; -import { getType } from '../expression_types'; +import { getType, ExpressionValue } from '../expression_types'; import { ArgumentType, ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; @@ -44,6 +44,13 @@ export interface ExecutionParams< ast?: ExpressionAstExpression; expression?: string; context?: ExtraContext; + + /** + * Whether to execute expression in *debug mode*. In *debug mode* inputs and + * outputs as well as all resolved arguments and time it took to execute each + * function are saved and are available for introspection. + */ + debug?: boolean; } const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ @@ -190,23 +197,55 @@ export class Execution< } const { function: fnName, arguments: fnArgs } = link; - const fnDef = getByAlias(this.state.get().functions, fnName); + const fn = getByAlias(this.state.get().functions, fnName); - if (!fnDef) { + if (!fn) { return createError({ message: `Function ${fnName} could not be found.` }); } + let args: Record = {}; + let timeStart: number | undefined; + try { - // Resolve arguments before passing to function - // resolveArgs returns an object because the arguments themselves might - // actually have a 'then' function which would be treated as a promise - const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs); - const output = await this.invokeFunction(fnDef, input, resolvedArgs); + // `resolveArgs` returns an object because the arguments themselves might + // actually have a `then` function which would be treated as a `Promise`. + const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs); + args = resolvedArgs; + timeStart = this.params.debug ? performance.now() : 0; + const output = await this.invokeFunction(fn, input, resolvedArgs); + + if (this.params.debug) { + const timeEnd: number = performance.now(); + (link as ExpressionAstFunction).debug = { + success: true, + fn, + input, + args: resolvedArgs, + output, + duration: timeEnd - timeStart, + }; + } + if (getType(output) === 'error') return output; input = output; - } catch (e) { - e.message = `[${fnName}] > ${e.message}`; - return createError(e); + } catch (rawError) { + const timeEnd: number = this.params.debug ? performance.now() : 0; + rawError.message = `[${fnName}] > ${rawError.message}`; + const error = createError(rawError) as ExpressionValueError; + + if (this.params.debug) { + (link as ExpressionAstFunction).debug = { + success: false, + fn, + input, + args, + error, + rawError, + duration: timeStart ? timeEnd - timeStart : undefined, + }; + } + + return error; } } @@ -327,7 +366,9 @@ export class Execution< const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { return asts.map((item: ExpressionAstExpression) => { return async (subInput = input) => { - const output = await this.params.executor.interpret(item, subInput); + const output = await this.params.executor.interpret(item, subInput, { + debug: this.params.debug, + }); if (isExpressionValueError(output)) throw output.error; const casted = this.cast(output, argDefs[argName as any].types); return casted; diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index af3662d13de4e..2ecbc5f75a9e8 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -31,6 +31,15 @@ import { ExpressionAstExpression, ExpressionAstNode } from '../ast'; import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; +export interface ExpressionExecOptions { + /** + * Whether to execute expression in *debug mode*. In *debug mode* inputs and + * outputs as well as all resolved arguments and time it took to execute each + * function are saved and are available for introspection. + */ + debug?: boolean; +} + export class TypesRegistry implements IRegistry { constructor(private readonly executor: Executor) {} @@ -145,10 +154,14 @@ export class Executor = Record(ast: ExpressionAstNode, input: T): Promise { + public async interpret( + ast: ExpressionAstNode, + input: T, + options?: ExpressionExecOptions + ): Promise { switch (getType(ast)) { case 'expression': - return await this.interpretExpression(ast as ExpressionAstExpression, input); + return await this.interpretExpression(ast as ExpressionAstExpression, input, options); case 'string': case 'number': case 'null': @@ -161,9 +174,10 @@ export class Executor = Record( ast: string | ExpressionAstExpression, - input: T + input: T, + options?: ExpressionExecOptions ): Promise { - const execution = this.createExecution(ast); + const execution = this.createExecution(ast, undefined, options); execution.start(input); return await execution.result; } @@ -192,7 +206,8 @@ export class Executor = Record( ast: string | ExpressionAstExpression, - context: ExtraContext = {} as ExtraContext + context: ExtraContext = {} as ExtraContext, + { debug }: ExpressionExecOptions = {} as ExpressionExecOptions ): Execution { const params: ExecutionParams = { executor: this, @@ -200,6 +215,7 @@ export class Executor = Record Date: Mon, 24 Feb 2020 16:40:20 +0100 Subject: [PATCH 055/113] no sparse array by default. (#58212) Co-authored-by: Elastic Machine --- .../src/types/array_type.test.ts | 37 ++++++++++++++++--- .../kbn-config-schema/src/types/array_type.ts | 4 +- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/kbn-config-schema/src/types/array_type.test.ts b/packages/kbn-config-schema/src/types/array_type.test.ts index 73661ef849cf4..66b72096a593d 100644 --- a/packages/kbn-config-schema/src/types/array_type.test.ts +++ b/packages/kbn-config-schema/src/types/array_type.test.ts @@ -85,14 +85,29 @@ test('fails if mixed types of content in array', () => { ); }); -test('returns empty array if input is empty but type has default value', () => { - const type = schema.arrayOf(schema.string({ defaultValue: 'test' })); +test('fails if sparse content in array', () => { + const type = schema.arrayOf(schema.string()); expect(type.validate([])).toEqual([]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); }); -test('returns empty array if input is empty even if type is required', () => { - const type = schema.arrayOf(schema.string()); +test('fails if sparse content in array if optional', () => { + const type = schema.arrayOf(schema.maybe(schema.string())); + expect(type.validate([])).toEqual([]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); +}); + +test('fails if sparse content in array if nullable', () => { + const type = schema.arrayOf(schema.nullable(schema.string())); expect(type.validate([])).toEqual([]); + expect(type.validate([null])).toEqual([null]); + expect(() => type.validate([undefined])).toThrowErrorMatchingInlineSnapshot( + `"[0]: sparse array are not allowed"` + ); }); test('fails for null values if optional', () => { @@ -102,9 +117,19 @@ test('fails for null values if optional', () => { ); }); +test('returns empty array if input is empty but type has default value', () => { + const type = schema.arrayOf(schema.string({ defaultValue: 'test' })); + expect(type.validate([])).toEqual([]); +}); + +test('returns empty array if input is empty even if type is required', () => { + const type = schema.arrayOf(schema.string()); + expect(type.validate([])).toEqual([]); +}); + test('handles default values for undefined values', () => { - const type = schema.arrayOf(schema.string({ defaultValue: 'foo' })); - expect(type.validate([undefined])).toEqual(['foo']); + const type = schema.arrayOf(schema.string(), { defaultValue: ['foo'] }); + expect(type.validate(undefined)).toEqual(['foo']); }); test('array within array', () => { diff --git a/packages/kbn-config-schema/src/types/array_type.ts b/packages/kbn-config-schema/src/types/array_type.ts index ad74f375588ad..a0353e8348ddd 100644 --- a/packages/kbn-config-schema/src/types/array_type.ts +++ b/packages/kbn-config-schema/src/types/array_type.ts @@ -31,7 +31,7 @@ export class ArrayType extends Type { let schema = internals .array() .items(type.getSchema().optional()) - .sparse(); + .sparse(false); if (options.minSize !== undefined) { schema = schema.min(options.minSize); @@ -49,6 +49,8 @@ export class ArrayType extends Type { case 'any.required': case 'array.base': return `expected value of type [array] but got [${typeDetect(value)}]`; + case 'array.sparse': + return `sparse array are not allowed`; case 'array.parse': return `could not parse array value from [${value}]`; case 'array.min': From 6b735c9ca016a65cc0c2e29fb144a1046020f1d3 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 24 Feb 2020 11:17:29 -0500 Subject: [PATCH 056/113] [ML] New Platform server shim: update usage collector to use core savedObjects (#58058) * use NP savedObjects and createInternalRepository for usage collection * fix route doc typo * update MlTelemetrySavedObject type * remove fileDataVisualizer routes dependency on legacy es plugin * update mlTelemetry tests * remove deprecated use of getSavedObjectsClient --- x-pack/legacy/plugins/ml/index.ts | 3 +- .../ml/server/lib/ml_telemetry/index.ts | 1 - .../ml_telemetry/make_ml_usage_collector.ts | 15 ++- .../lib/ml_telemetry/ml_telemetry.test.ts | 102 +++++------------- .../server/lib/ml_telemetry/ml_telemetry.ts | 39 +++---- .../plugins/ml/server/new_platform/plugin.ts | 21 ++-- .../ml/server/routes/file_data_visualizer.ts | 2 +- .../ml/server/routes/job_audit_messages.ts | 2 +- 8 files changed, 55 insertions(+), 130 deletions(-) diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 0ef5e14e44f71..09f1b9ccedce4 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -81,7 +81,8 @@ export const ml = (kibana: any) => { injectUiAppVars: server.injectUiAppVars, http: mlHttpService, savedObjects: server.savedObjects, - elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, // NP + coreSavedObjects: kbnServer.newPlatform.start.core.savedObjects, + elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, }; const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins; const plugins = { diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts index 5da4f6b62bcec..dffd95f50e0d9 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts @@ -6,7 +6,6 @@ export { createMlTelemetry, - getSavedObjectsClient, incrementFileDataVisualizerIndexCreationCount, storeMlTelemetry, MlTelemetry, diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index 293480b2aa5dc..a120450bbb2b0 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -5,19 +5,17 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { SavedObjectsServiceStart } from 'src/core/server'; import { createMlTelemetry, - getSavedObjectsClient, ML_TELEMETRY_DOC_ID, MlTelemetry, MlTelemetrySavedObject, } from './ml_telemetry'; -import { UsageInitialization } from '../../new_platform/plugin'; - export function makeMlUsageCollector( usageCollection: UsageCollectionSetup | undefined, - { elasticsearchPlugin, savedObjects }: UsageInitialization + savedObjects: SavedObjectsServiceStart ): void { if (!usageCollection) { return; @@ -28,11 +26,10 @@ export function makeMlUsageCollector( isReady: () => true, fetch: async (): Promise => { try { - const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - const mlTelemetrySavedObject = (await savedObjectsClient.get( - 'ml-telemetry', - ML_TELEMETRY_DOC_ID - )) as MlTelemetrySavedObject; + const mlTelemetrySavedObject: MlTelemetrySavedObject = await savedObjects + .createInternalRepository() + .get('ml-telemetry', ML_TELEMETRY_DOC_ID); + return mlTelemetrySavedObject.attributes; } catch (err) { return createMlTelemetry(); diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts index fcf3763626b6f..9d14ffb31be63 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts @@ -6,7 +6,6 @@ import { createMlTelemetry, - getSavedObjectsClient, incrementFileDataVisualizerIndexCreationCount, ML_TELEMETRY_DOC_ID, MlTelemetry, @@ -26,22 +25,11 @@ describe('ml_telemetry', () => { }); describe('storeMlTelemetry', () => { - let elasticsearchPlugin: any; - let savedObjects: any; let mlTelemetry: MlTelemetry; - let savedObjectsClientInstance: any; + let internalRepository: any; beforeEach(() => { - savedObjectsClientInstance = { create: jest.fn() }; - const callWithInternalUser = jest.fn(); - const internalRepository = jest.fn(); - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - savedObjects = { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }; + internalRepository = { create: jest.fn(), get: jest.fn() }; mlTelemetry = { file_data_visualizer: { index_creation_count: 1, @@ -49,59 +37,28 @@ describe('ml_telemetry', () => { }; }); - it('should call savedObjectsClient create with the given MlTelemetry object', () => { - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); - expect(savedObjectsClientInstance.create.mock.calls[0][1]).toBe(mlTelemetry); + it('should call internalRepository create with the given MlTelemetry object', () => { + storeMlTelemetry(internalRepository, mlTelemetry); + expect(internalRepository.create.mock.calls[0][1]).toBe(mlTelemetry); }); - it('should call savedObjectsClient create with the ml-telemetry document type and ID', () => { - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); - expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(savedObjectsClientInstance.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); + it('should call internalRepository create with the ml-telemetry document type and ID', () => { + storeMlTelemetry(internalRepository, mlTelemetry); + expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(internalRepository.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); }); - it('should call savedObjectsClient create with overwrite: true', () => { - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); - expect(savedObjectsClientInstance.create.mock.calls[0][2].overwrite).toBe(true); - }); - }); - - describe('getSavedObjectsClient', () => { - let elasticsearchPlugin: any; - let savedObjects: any; - let savedObjectsClientInstance: any; - let callWithInternalUser: any; - let internalRepository: any; - - beforeEach(() => { - savedObjectsClientInstance = { create: jest.fn() }; - callWithInternalUser = jest.fn(); - internalRepository = jest.fn(); - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - savedObjects = { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }; - }); - - it('should return a SavedObjectsClient initialized with the saved objects internal repository', () => { - const result = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - - expect(result).toBe(savedObjectsClientInstance); - expect(savedObjects.SavedObjectsClient).toHaveBeenCalledWith(internalRepository); + it('should call internalRepository create with overwrite: true', () => { + storeMlTelemetry(internalRepository, mlTelemetry); + expect(internalRepository.create.mock.calls[0][2].overwrite).toBe(true); }); }); describe('incrementFileDataVisualizerIndexCreationCount', () => { - let elasticsearchPlugin: any; let savedObjects: any; - let savedObjectsClientInstance: any; - let callWithInternalUser: any; let internalRepository: any; - function createSavedObjectsClientInstance( + function createInternalRepositoryInstance( telemetryEnabled?: boolean, indexCreationCount?: number ) { @@ -136,51 +93,42 @@ describe('ml_telemetry', () => { } function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void { - savedObjectsClientInstance = createSavedObjectsClientInstance( - telemetryEnabled, - indexCreationCount - ); - callWithInternalUser = jest.fn(); - internalRepository = jest.fn(); + internalRepository = createInternalRepositoryInstance(telemetryEnabled, indexCreationCount); savedObjects = { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }; - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), + createInternalRepository: jest.fn(() => internalRepository), }; } it('should not increment if telemetry status cannot be determined', async () => { mockInit(); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0); + expect(internalRepository.create.mock.calls).toHaveLength(0); }); it('should not increment if telemetry status is disabled', async () => { mockInit(false); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0); + expect(internalRepository.create.mock.calls).toHaveLength(0); }); it('should initialize index_creation_count with 1', async () => { mockInit(true); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({ + expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(internalRepository.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 1 }, }); }); it('should increment index_creation_count to 2', async () => { mockInit(true, 1); - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjects); - expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({ + expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(internalRepository.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 2 }, }); }); diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts index 1bac3f1780644..d76b1ee94e21e 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; -import { callWithInternalUserFactory } from '../../client/call_with_internal_user_factory'; +import { + SavedObjectAttributes, + SavedObjectsServiceStart, + ISavedObjectsRepository, +} from 'src/core/server'; -export interface MlTelemetry { +export interface MlTelemetry extends SavedObjectAttributes { file_data_visualizer: { index_creation_count: number; }; @@ -29,35 +31,22 @@ export function createMlTelemetry(count: number = 0): MlTelemetry { } // savedObjects export function storeMlTelemetry( - elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsLegacyService, + internalRepository: ISavedObjectsRepository, mlTelemetry: MlTelemetry ): void { - const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - savedObjectsClient.create('ml-telemetry', mlTelemetry, { + internalRepository.create('ml-telemetry', mlTelemetry, { id: ML_TELEMETRY_DOC_ID, overwrite: true, }); } -// needs savedObjects and elasticsearchPlugin -export function getSavedObjectsClient( - elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsLegacyService -): any { - const { SavedObjectsClient, getSavedObjectsRepository } = savedObjects; - const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - return new SavedObjectsClient(internalRepository); -} export async function incrementFileDataVisualizerIndexCreationCount( - elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsLegacyService + savedObjects: SavedObjectsServiceStart ): Promise { - const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); - + const internalRepository = await savedObjects.createInternalRepository(); try { - const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry'); + const { attributes } = await internalRepository.get('telemetry', 'telemetry'); + if (attributes.enabled === false) { return; } @@ -70,7 +59,7 @@ export async function incrementFileDataVisualizerIndexCreationCount( let indicesCount = 1; try { - const { attributes } = (await savedObjectsClient.get( + const { attributes } = (await internalRepository.get( 'ml-telemetry', ML_TELEMETRY_DOC_ID )) as MlTelemetrySavedObject; @@ -80,5 +69,5 @@ export async function incrementFileDataVisualizerIndexCreationCount( } const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); + storeMlTelemetry(internalRepository, mlTelemetry); } diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index 10961182be841..43c276ac63a13 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -14,6 +14,7 @@ import { CoreSetup, IRouter, IScopedClusterClient, + SavedObjectsServiceStart, } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -28,12 +29,10 @@ import { LICENSE_TYPE } from '../../common/constants/license'; import { annotationRoutes } from '../routes/annotations'; import { jobRoutes } from '../routes/anomaly_detectors'; import { dataFeedRoutes } from '../routes/datafeeds'; -// @ts-ignore: could not find declaration file for module import { indicesRoutes } from '../routes/indices'; import { jobValidationRoutes } from '../routes/job_validation'; import { makeMlUsageCollector } from '../lib/ml_telemetry'; import { notificationRoutes } from '../routes/notification_settings'; -// @ts-ignore: could not find declaration file for module import { systemRoutes } from '../routes/system'; import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics'; import { dataRecognizer } from '../routes/modules'; @@ -45,7 +44,6 @@ import { filtersRoutes } from '../routes/filters'; import { resultsServiceRoutes } from '../routes/results_service'; import { jobServiceRoutes } from '../routes/job_service'; import { jobAuditMessagesRoutes } from '../routes/job_audit_messages'; -// @ts-ignore: could not find declaration file for module import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer'; import { initMlServerLog, LogInitialization } from '../client/log'; import { HomeServerPluginSetup } from '../../../../../../src/plugins/home/server'; @@ -67,6 +65,7 @@ export interface MlCoreSetup { injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; savedObjects: SavedObjectsLegacyService; + coreSavedObjects: SavedObjectsServiceStart; elasticsearch: ElasticsearchServiceSetup; } export interface MlInitializerContext extends PluginInitializerContext { @@ -93,15 +92,11 @@ export interface RouteInitialization { route(route: ServerRoute | ServerRoute[]): void; router: IRouter; xpackMainPlugin: MlXpackMainPlugin; - savedObjects?: SavedObjectsLegacyService; + savedObjects?: SavedObjectsServiceStart; spacesPlugin: any; securityPlugin: any; cloud?: CloudSetup; } -export interface UsageInitialization { - elasticsearchPlugin: ElasticsearchPlugin; - savedObjects: SavedObjectsLegacyService; -} declare module 'kibana/server' { interface RequestHandlerContext { @@ -123,7 +118,7 @@ export class Plugin { public setup(core: MlCoreSetup, plugins: PluginsSetup) { const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; - const { http } = core; + const { http, coreSavedObjects } = core; const pluginId = this.pluginId; mirrorPluginStatus(xpackMainPlugin, plugins.ml); @@ -208,14 +203,10 @@ export class Plugin { const extendedRouteInitializationDeps: RouteInitialization = { ...routeInitializationDeps, config: this.config, - savedObjects: core.savedObjects, + savedObjects: coreSavedObjects, spacesPlugin: plugins.spaces, cloud: plugins.cloud, }; - const usageInitializationDeps: UsageInitialization = { - elasticsearchPlugin: plugins.elasticsearch, - savedObjects: core.savedObjects, - }; const logInitializationDeps: LogInitialization = { log: this.log, @@ -240,7 +231,7 @@ export class Plugin { fileDataVisualizerRoutes(extendedRouteInitializationDeps); initMlServerLog(logInitializationDeps); - makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps); + makeMlUsageCollector(plugins.usageCollection, coreSavedObjects); } public stop() {} diff --git a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts index 95f2a9fe7298f..d5a992c933293 100644 --- a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts @@ -138,7 +138,7 @@ export function fileDataVisualizerRoutes({ // follow-up import calls to just add additional data will include the `id` of the created // index, we'll ignore those and don't increment the counter. if (id === undefined) { - await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects!); + await incrementFileDataVisualizerIndexCreationCount(savedObjects!); } const result = await importData( diff --git a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts index 7298312990005..76986b935b993 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts @@ -50,7 +50,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial /** * @apiGroup JobAuditMessages * - * @api {get} /api/ml/results/anomalies_table_data Get all audit messages + * @api {get} /api/ml/job_audit_messages/messages Get all audit messages * @apiName GetAllJobAuditMessages * @apiDescription Returns all audit messages */ From c6f5fdd061d93ad0d67335a658449be92e24640c Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 24 Feb 2020 17:42:34 +0100 Subject: [PATCH 057/113] Advanced settings UI change to centralize save state (#53693) --- .../advanced_settings/public/_index.scss | 2 +- .../public/management_app/_index.scss | 3 + .../management_app/advanced_settings.scss | 40 +- .../management_app/advanced_settings.tsx | 16 +- .../management_app/components/_index.scss | 1 + .../field/__snapshots__/field.test.tsx.snap | 7069 ++++++++--------- .../components/field/field.test.tsx | 267 +- .../management_app/components/field/field.tsx | 545 +- .../management_app/components/field/index.ts | 2 +- .../form/__snapshots__/form.test.tsx.snap | 1228 ++- .../management_app/components/form/_form.scss | 13 + .../components/form/_index.scss | 1 + .../components/form/form.test.tsx | 114 +- .../management_app/components/form/form.tsx | 280 +- .../public/management_app/types.ts | 13 + .../telemetry_management_section.tsx | 54 +- test/functional/page_objects/settings_page.ts | 6 +- .../translations/translations/ja-JP.json | 11 - .../translations/translations/zh-CN.json | 10 - 19 files changed, 5086 insertions(+), 4589 deletions(-) create mode 100644 src/plugins/advanced_settings/public/management_app/_index.scss create mode 100644 src/plugins/advanced_settings/public/management_app/components/_index.scss create mode 100644 src/plugins/advanced_settings/public/management_app/components/form/_form.scss create mode 100644 src/plugins/advanced_settings/public/management_app/components/form/_index.scss diff --git a/src/plugins/advanced_settings/public/_index.scss b/src/plugins/advanced_settings/public/_index.scss index f3fe78bf6a9c0..d13c37bff32d0 100644 --- a/src/plugins/advanced_settings/public/_index.scss +++ b/src/plugins/advanced_settings/public/_index.scss @@ -17,4 +17,4 @@ * under the License. */ - @import './management_app/advanced_settings'; +@import './management_app/index'; diff --git a/src/plugins/advanced_settings/public/management_app/_index.scss b/src/plugins/advanced_settings/public/management_app/_index.scss new file mode 100644 index 0000000000000..aa1980692f7b7 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/_index.scss @@ -0,0 +1,3 @@ +@import './advanced_settings'; + +@import './components/index'; diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss index 79b6feccb6b7d..016edb2817da8 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.scss +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.scss @@ -17,21 +17,27 @@ * under the License. */ -.mgtAdvancedSettings__field { + .mgtAdvancedSettings__field { + * { margin-top: $euiSize; } - &Wrapper { - width: 640px; - @include internetExplorerOnly() { - min-height: 1px; - } + padding-left: $euiSizeS; + margin-left: -$euiSizeS; + &--unsaved { + // Simulates a left side border without shifting content + box-shadow: -$euiSizeXS 0px $euiColorSecondary; } - - &Actions { - padding-top: $euiSizeM; + &--invalid { + // Simulates a left side border without shifting content + box-shadow: -$euiSizeXS 0px $euiColorDanger; + } + @include internetExplorerOnly() { + min-height: 1px; + } + &Row { + padding-left: $euiSizeS; } @include internetExplorerOnly { @@ -40,3 +46,19 @@ } } } + +.mgtAdvancedSettingsForm__unsavedCount { + @include euiBreakpoint('xs', 's') { + display: none; + } +} + +.mgtAdvancedSettingsForm__unsavedCountMessage{ + // Simulates a left side border without shifting content + box-shadow: -$euiSizeXS 0px $euiColorSecondary; + padding-left: $euiSizeS; +} + +.mgtAdvancedSettingsForm__button { + width: 100%; +} diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index 5057d072e3e41..39312c9340ff9 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -38,7 +38,7 @@ import { ComponentRegistry } from '../'; import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; -import { FieldSetting, IQuery } from './types'; +import { FieldSetting, IQuery, SettingsChanges } from './types'; interface AdvancedSettingsProps { enableSaving: boolean; @@ -177,6 +177,13 @@ export class AdvancedSettingsComponent extends Component< }); }; + saveConfig = async (changes: SettingsChanges) => { + const arr = Object.entries(changes).map(([key, value]) => + this.props.uiSettings.set(key, value) + ); + return Promise.all(arr); + }; + render() { const { filteredSettings, query, footerQueryMatched } = this.state; const componentRegistry = this.props.componentRegistry; @@ -205,18 +212,19 @@ export class AdvancedSettingsComponent extends Component<
+
+ + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } > - - -
- - } - title={ -

- Array test setting - -

- } - > - - - - - - - + + + `; exports[`Field for array setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - default_value - , - } - } - /> - - - - - } - title={ -

- Array test setting - -

- } - > - + default_value + , + } + } /> - - } - isInvalid={false} - label="array:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } +> + - - - - - - + + } + label="array:test:setting" + labelType="label" + > + +
+ `; exports[`Field for array setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Array test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Array test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for array setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } > - - -
- - } - title={ -

- Array test setting - -

- } - > - + + +`; + +exports[`Field for array setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Array test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for array setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + default_value + , + } + } /> - - - - default_value - , - } - } - /> - - - - } - title={ -

- Array test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="array:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Array test setting + +

+ } +> + + + + + +     + + + } + label="array:test:setting" + labelType="label" + > + + + `; exports[`Field for boolean setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } > - - -
- - } - title={ -

- Boolean test setting - -

- } - > - - - } - onChange={[Function]} - onKeyDown={[Function]} + - - - - - + } + onChange={[Function]} + /> + + `; exports[`Field for boolean setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - true - , - } - } - /> - - - - - } - title={ -

- Boolean test setting - -

- } - > - + true + , + } + } /> - - } - isInvalid={false} - label="boolean:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } +> + - - } - onChange={[Function]} - onKeyDown={[Function]} + + + } + label="boolean:test:setting" + labelType="label" + > + - - - - - + } + onChange={[Function]} + /> +
+ `; exports[`Field for boolean setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Boolean test setting - - } - type="asterisk" - /> -

- } - > - - - } - onChange={[Function]} - onKeyDown={[Function]} + - - - - - + } + onChange={[Function]} + /> + + `; exports[`Field for boolean setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } > - - -
- - } - title={ -

- Boolean test setting - -

+ } - > - - + onChange={[Function]} + /> + + +`; + +exports[`Field for boolean setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Boolean test setting + + } + type="asterisk" + /> +

+ } +> + + - - - - - + } + onChange={[Function]} + /> + +

+ Setting is currently not saved. +

+
+ + `; exports[`Field for boolean setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + true + , + } + } /> - - - - true - , - } - } - /> - - - - } - title={ -

- Boolean test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="boolean:test:setting" - labelType="label" - > - + + + } + fullWidth={true} + title={ +

+ Boolean test setting + +

+ } +> + + + - } - onChange={[Function]} - onKeyDown={[Function]} + +     + + + } + label="boolean:test:setting" + labelType="label" + > + - - - - - + } + onChange={[Function]} + /> +
+ `; exports[`Field for image setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } > - - -
- - } - title={ -

- Image test setting - -

- } - > - - - - - - - + + + `; exports[`Field for image setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - null - , - } - } - /> - - - - - } - title={ -

- Image test setting - -

- } - > - + null + , + } + } /> - - } - isInvalid={false} - label="image:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } +> + - - - - - - + + } + label="image:test:setting" + labelType="label" + > + +
+ `; exports[`Field for image setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Image test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Image test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for image setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } > - - -
- - } - title={ -

- Image test setting - -

- } - > - + + +`; + +exports[`Field for image setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Image test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for image setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- Image test setting - -

- } - > - - - - - -     - - - - - - - - } - isInvalid={false} - label="image:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Image test setting + +

+ } +> + + + + + +     + + + + + + + + } + label="image:test:setting" + labelType="label" + > + + + `; exports[`Field for json setting should render as read only if saving is disabled 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + +
- -
- -
-
- - - - + +
+
+ `; exports[`Field for json setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + + + + } + label="json:test:setting" + labelType="label" + > +
- - - + -
- -
-
- - - - + fullWidth={true} + height="auto" + isReadOnly={true} + maxLines={30} + minLines={6} + mode="json" + onChange={[Function]} + setOptions={ + Object { + "showLineNumbers": false, + "tabSize": 2, + } + } + showGutter={false} + theme="textmate" + value="{\\"hello\\": \\"world\\"}" + width="100%" + /> +
+
+ `; exports[`Field for json setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Json test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Json test setting - - } - type="asterisk" - /> -

- } +
- -
- -
-
- - - - + +
+ + `; exports[`Field for json setting should render default value if there is no user value set 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + + + + + +     + + + } + label="json:test:setting" + labelType="label" + > +
- - - - - -     - - - } - isInvalid={false} - label="json:test:setting" - labelType="label" + +
+
+ +`; + +exports[`Field for json setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Json test setting + + } + type="asterisk" + /> +

+ } +> + +
+ +
+ +

-

- -
-
- - - - + Setting is currently not saved. +

+ + + `; exports[`Field for json setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + {} + , + } + } /> - - - - {} - , - } - } - /> - - - - } - title={ -

- Json test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Json test setting + +

+ } +> + + + + + +     + + + } + label="json:test:setting" + labelType="label" + > +
- - - - - -     - - - } - isInvalid={false} - label="json:test:setting" - labelType="label" - > -
- -
-
- - - - + +
+
+ `; exports[`Field for markdown setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } > - - -
- - } - title={ -

- Markdown test setting - -

- } +
- -
- -
-
- - - - + +
+ + `; exports[`Field for markdown setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- Markdown test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } +> + + + + } + label="markdown:test:setting" + labelType="label" + > +
- - - + -
- -
-
- - - - + fullWidth={true} + height="auto" + isReadOnly={true} + maxLines={30} + minLines={6} + mode="markdown" + onChange={[Function]} + setOptions={ + Object { + "showLineNumbers": false, + "tabSize": 2, + } + } + showGutter={false} + theme="textmate" + value="**bold**" + width="100%" + /> +
+
+ `; exports[`Field for markdown setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Markdown test setting - - } - type="asterisk" - /> -

- } +
- -
- -
-
- - - - + +
+ + `; exports[`Field for markdown setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } > - - -
- - } - title={ -

- Markdown test setting - -

- } +
+ +
+ + +`; + +exports[`Field for markdown setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Markdown test setting + + } + type="asterisk" + /> +

+ } +> + +
- +
+ +

-

- -
-
- - - - + Setting is currently not saved. +

+ + + `; exports[`Field for markdown setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- Markdown test setting - -

- } + + + + } + fullWidth={true} + title={ +

+ Markdown test setting + +

+ } +> + + + + + +     + + + } + label="markdown:test:setting" + labelType="label" + > +
- - - - - -     - - - } - isInvalid={false} - label="markdown:test:setting" - labelType="label" - > -
- -
-
- - - - + +
+
+ `; exports[`Field for number setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } > - - -
- - } - title={ -

- Number test setting - -

- } - > - - - - - - - + + + `; exports[`Field for number setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - 5 - , - } - } - /> - - - - - } - title={ -

- Number test setting - -

- } - > - + 5 + , + } + } /> - - } - isInvalid={false} - label="number:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } +> + - - - - - - + + } + label="number:test:setting" + labelType="label" + > + +
+ `; exports[`Field for number setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Number test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Number test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + +`; + +exports[`Field for number setting should render default value if there is no user value set 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } +> + + + + `; -exports[`Field for number setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Number test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Number test setting - -

- } - > - + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for number setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + 5 + , + } + } /> - - - - 5 - , - } - } - /> - - - - } - title={ -

- Number test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="number:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Number test setting + +

+ } +> + + + + + +     + + + } + label="number:test:setting" + labelType="label" + > + + + `; exports[`Field for select setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } > - - -
- - } - title={ -

- Select test setting - -

- } - > - - - - - - - + + + `; exports[`Field for select setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - Orange - , - } - } - /> - - - - - } - title={ -

- Select test setting - -

- } - > - + Orange + , + } + } /> - - } - isInvalid={false} - label="select:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } +> + - - - - - - + + } + label="select:test:setting" + labelType="label" + > + +
+ `; exports[`Field for select setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Select test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- Select test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for select setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } > - - -
- - } - title={ -

- Select test setting - -

- } - > - + + +`; + +exports[`Field for select setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ Select test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for select setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + Orange + , + } + } /> - - - - Orange - , - } - } - /> - - - - } - title={ -

- Select test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="select:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ Select test setting + +

+ } +> + + + + + +     + + + } + label="select:test:setting" + labelType="label" + > + + + `; exports[`Field for string setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test setting + +

+ } > - - -
- - } - title={ -

- String test setting - -

- } - > - - - - - - - + + + `; exports[`Field for string setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - null - , - } - } - /> - - - - - } - title={ -

- String test setting - -

- } - > - + null + , + } + } /> - - } - isInvalid={false} - label="string:test:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ String test setting + +

+ } +> + - - - - - - + + } + label="string:test:setting" + labelType="label" + > + +
+ `; exports[`Field for string setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- String test setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for string setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test setting + +

+ } > - - -
- - } - title={ -

- String test setting - -

- } - > - + + +`; + +exports[`Field for string setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ String test setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for string setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + null + , + } + } /> - - - - null - , - } - } - /> - - - - } - title={ -

- String test setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="string:test:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ String test setting + +

+ } +> + + + + + +     + + + } + label="string:test:setting" + labelType="label" + > + + + `; exports[`Field for stringWithValidation setting should render as read only if saving is disabled 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } > - - -
- - } - title={ -

- String test validation setting - -

- } - > - - - - - - - + + + `; exports[`Field for stringWithValidation setting should render as read only with help text if overridden 1`] = ` - - - -
+ description={ + +
+ + + - - - - - foo-default - , - } - } - /> - - - - - } - title={ -

- String test validation setting - -

- } - > - + foo-default + , + } + } /> - - } - isInvalid={false} - label="string:test-validation:setting" - labelType="label" + + + + + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } +> + - - - - - - + + } + label="string:test-validation:setting" + labelType="label" + > + +
+ `; exports[`Field for stringWithValidation setting should render custom setting icon if it is custom 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + + } + type="asterisk" + /> +

+ } > - - -
- - } - title={ -

- String test validation setting - - } - type="asterisk" - /> -

- } - > - - - - - - - + + + `; exports[`Field for stringWithValidation setting should render default value if there is no user value set 1`] = ` - +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } > - - -
- - } - title={ -

- String test validation setting - -

- } - > - + + +`; + +exports[`Field for stringWithValidation setting should render unsaved value if there are unsaved changes 1`] = ` + +
+ + } + fullWidth={true} + title={ +

+ String test validation setting + + } + type="asterisk" + /> +

+ } +> + + + +

- - - - - - + Setting is currently not saved. +

+
+
+ `; exports[`Field for stringWithValidation setting should render user value if there is user value is set 1`] = ` - - - -
+ description={ + +
+ + + - + foo-default + , + } + } /> - - - - foo-default - , - } - } - /> - - - - } - title={ -

- String test validation setting - -

- } - > - - - - - -     - - - } - isInvalid={false} - label="string:test-validation:setting" - labelType="label" - > - - - - - - + + + + } + fullWidth={true} + title={ +

+ String test validation setting + +

+ } +> + + + + + +     + + + } + label="string:test-validation:setting" + labelType="label" + > + + + `; diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index 81df22ccf6e43..8e41fed685898 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -20,21 +20,14 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { FieldSetting } from '../../types'; import { UiSettingsType, StringValidation } from '../../../../../../core/public'; import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { Field } from './field'; - -jest.mock('ui/notify', () => ({ - toastNotifications: { - addDanger: () => {}, - add: jest.fn(), - }, -})); +import { Field, getEditableValue } from './field'; jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); jest.mock('brace/mode/markdown', () => 'brace/mode/markdown'); @@ -45,6 +38,18 @@ const defaults = { category: ['category'], }; +const exampleValues = { + array: ['example_value'], + boolean: false, + image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=', + json: { foo: 'bar2' }, + markdown: 'Hello World', + number: 1, + select: 'banana', + string: 'hello world', + stringWithValidation: 'foo', +}; + const settings: Record = { array: { name: 'array:test:setting', @@ -161,7 +166,7 @@ const settings: Record = { description: 'Description for String test validation setting', type: 'string', validation: { - regex: new RegExp('/^foo'), + regex: new RegExp('^foo'), message: 'must start with "foo"', }, value: undefined, @@ -182,11 +187,22 @@ const userValues = { string: 'foo', stringWithValidation: 'fooUserValue', }; + const invalidUserValues = { stringWithValidation: 'invalidUserValue', }; -const save = jest.fn(() => Promise.resolve(true)); -const clear = jest.fn(() => Promise.resolve(true)); + +const handleChange = jest.fn(); +const clearChange = jest.fn(); + +const getFieldSettingValue = (wrapper: ReactWrapper, name: string, type: string) => { + const field = findTestSubject(wrapper, `advancedSetting-editField-${name}`); + if (type === 'boolean') { + return field.props()['aria-checked']; + } else { + return field.props().value; + } +}; describe('Field', () => { Object.keys(settings).forEach(type => { @@ -197,8 +213,7 @@ describe('Field', () => { const component = shallowWithI18nProvider( { value: userValues[type], isOverridden: true, }} - save={save} - clear={clear} + handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} @@ -232,14 +246,12 @@ describe('Field', () => { const component = shallowWithI18nProvider( ); - expect(component).toMatchSnapshot(); }); @@ -251,8 +263,7 @@ describe('Field', () => { // @ts-ignore value: userValues[type], }} - save={save} - clear={clear} + handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} @@ -269,48 +280,44 @@ describe('Field', () => { ...setting, isCustom: true, }} - save={save} - clear={clear} + handleChange={handleChange} enableSaving={true} toasts={notificationServiceMock.createStartContract().toasts} dockLinks={docLinksServiceMock.createStartContract().links} /> ); - expect(component).toMatchSnapshot(); }); - }); - - if (type === 'select') { - it('should use options for rendering values', () => { - const component = mountWithI18nProvider( + it('should render unsaved value if there are unsaved changes', async () => { + const component = shallowWithI18nProvider( ); - const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); - // @ts-ignore - const labels = select.find('option').map(option => option.prop('value')); - expect(labels).toEqual(['apple', 'orange', 'banana']); + expect(component).toMatchSnapshot(); }); + }); - it('should use optionLabels for rendering labels', () => { + if (type === 'select') { + it('should use options for rendering values and optionsLabels for rendering labels', () => { const component = mountWithI18nProvider( { ); const select = findTestSubject(component, `advancedSetting-editField-${setting.name}`); // @ts-ignore + const values = select.find('option').map(option => option.prop('value')); + expect(values).toEqual(['apple', 'orange', 'banana']); + // @ts-ignore const labels = select.find('option').map(option => option.text()); expect(labels).toEqual(['Apple', 'Orange', 'banana']); }); @@ -328,8 +338,8 @@ describe('Field', () => { { const userValue = userValues[type]; (component.instance() as Field).getImageAsBase64 = ({}: Blob) => Promise.resolve(''); - it('should be able to change value from no value and cancel', async () => { - await (component.instance() as Field).onImageChange([userValue]); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate( - 'click' - ); - expect( - (component.instance() as Field).state.unsavedValue === - (component.instance() as Field).state.savedValue - ).toBe(true); - }); - - it('should be able to change value and save', async () => { - await (component.instance() as Field).onImageChange([userValue]); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: userValue }); + it('should be able to change value and cancel', async () => { + (component.instance() as Field).onImageChange([userValue]); + expect(handleChange).toBeCalled(); await wrapper.setProps({ + unsavedChanges: { + value: userValue, + changeImage: true, + }, setting: { ...(component.instance() as Field).props.setting, value: userValue, }, }); - await (component.instance() as Field).cancelChangeImage(); + expect(clearChange).toBeCalledWith(setting.name); wrapper.update(); }); - it('should be able to change value from existing value and save', async () => { + it('should be able to change value from existing value', async () => { + await wrapper.setProps({ + unsavedChanges: {}, + }); const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-changeImage-${setting.name}`).simulate('click'); - const newUserValue = `${userValue}=`; await (component.instance() as Field).onImageChange([newUserValue]); - const updated2 = wrapper.update(); - findTestSubject(updated2, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: newUserValue }); - await wrapper.setProps({ - setting: { - ...(component.instance() as Field).props.setting, - value: newUserValue, - }, - }); - wrapper.update(); + expect(handleChange).toBeCalled(); }); it('should be able to reset to default value', async () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); - expect(clear).toBeCalled(); + expect(handleChange).toBeCalledWith(setting.name, { + value: getEditableValue(setting.type, setting.defVal), + changeImage: true, + }); }); }); } else if (type === 'markdown' || type === 'json') { describe(`for changing ${type} setting`, () => { const { wrapper, component } = setup(); const userValue = userValues[type]; - const fieldUserValue = userValue; - - it('should be able to change value and cancel', async () => { - (component.instance() as Field).onCodeEditorChange(fieldUserValue as UiSettingsType); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate( - 'click' - ); - expect( - (component.instance() as Field).state.unsavedValue === - (component.instance() as Field).state.savedValue - ).toBe(true); - }); - it('should be able to change value and save', async () => { - (component.instance() as Field).onCodeEditorChange(fieldUserValue as UiSettingsType); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: fieldUserValue }); + it('should be able to change value', async () => { + (component.instance() as Field).onCodeEditorChange(userValue as UiSettingsType); + expect(handleChange).toBeCalledWith(setting.name, { value: userValue }); await wrapper.setProps({ setting: { ...(component.instance() as Field).props.setting, @@ -445,19 +417,21 @@ describe('Field', () => { wrapper.update(); }); + it('should be able to reset to default value', async () => { + const updated = wrapper.update(); + findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); + expect(handleChange).toBeCalledWith(setting.name, { + value: getEditableValue(setting.type, setting.defVal), + }); + }); + if (type === 'json') { it('should be able to clear value and have empty object populate', async () => { - (component.instance() as Field).onCodeEditorChange('' as UiSettingsType); + await (component.instance() as Field).onCodeEditorChange('' as UiSettingsType); wrapper.update(); - expect((component.instance() as Field).state.unsavedValue).toEqual('{}'); + expect(handleChange).toBeCalledWith(setting.name, { value: setting.defVal }); }); } - - it('should be able to reset to default value', async () => { - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); - expect(clear).toBeCalled(); - }); }); } else { describe(`for changing ${type} setting`, () => { @@ -470,76 +444,45 @@ describe('Field', () => { // @ts-ignore const invalidUserValue = invalidUserValues[type]; it('should display an error when validation fails', async () => { - (component.instance() as Field).onFieldChange(invalidUserValue); + await (component.instance() as Field).onFieldChange(invalidUserValue); + const expectedUnsavedChanges = { + value: invalidUserValue, + error: (setting.validation as StringValidation).message, + isInvalid: true, + }; + expect(handleChange).toBeCalledWith(setting.name, expectedUnsavedChanges); + wrapper.setProps({ unsavedChanges: expectedUnsavedChanges }); const updated = wrapper.update(); const errorMessage = updated.find('.euiFormErrorText').text(); - expect(errorMessage).toEqual((setting.validation as StringValidation).message); + expect(errorMessage).toEqual(expectedUnsavedChanges.error); }); } - it('should be able to change value and cancel', async () => { - (component.instance() as Field).onFieldChange(fieldUserValue); + it('should be able to change value', async () => { + await (component.instance() as Field).onFieldChange(fieldUserValue); const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-cancelEditField-${setting.name}`).simulate( - 'click' - ); - expect( - (component.instance() as Field).state.unsavedValue === - (component.instance() as Field).state.savedValue - ).toBe(true); + expect(handleChange).toBeCalledWith(setting.name, { value: fieldUserValue }); + updated.setProps({ unsavedChanges: { value: fieldUserValue } }); + const currentValue = getFieldSettingValue(updated, setting.name, type); + expect(currentValue).toEqual(fieldUserValue); }); - it('should be able to change value and save', async () => { - (component.instance() as Field).onFieldChange(fieldUserValue); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate( - 'click' - ); - expect(save).toBeCalled(); - component.setState({ savedValue: fieldUserValue }); + it('should be able to reset to default value', async () => { await wrapper.setProps({ - setting: { - ...(component.instance() as Field).props.setting, - value: userValue, - }, + unsavedChanges: {}, + setting: { ...setting, value: fieldUserValue }, }); - wrapper.update(); - }); - - it('should be able to reset to default value', async () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); - expect(clear).toBeCalled(); + const expectedEditableValue = getEditableValue(setting.type, setting.defVal); + expect(handleChange).toBeCalledWith(setting.name, { + value: expectedEditableValue, + }); + updated.setProps({ unsavedChanges: { value: expectedEditableValue } }); + const currentValue = getFieldSettingValue(updated, setting.name, type); + expect(currentValue).toEqual(expectedEditableValue); }); }); } }); - - it('should show a reload toast when saving setting requiring a page reload', async () => { - const setting = { - ...settings.string, - requiresPageReload: true, - }; - const toasts = notificationServiceMock.createStartContract().toasts; - const wrapper = mountWithI18nProvider( - - ); - (wrapper.instance() as Field).onFieldChange({ target: { value: 'a new value' } }); - const updated = wrapper.update(); - findTestSubject(updated, `advancedSetting-saveEditField-${setting.name}`).simulate('click'); - expect(save).toHaveBeenCalled(); - await save(); - expect(toasts.add).toHaveBeenCalledWith( - expect.objectContaining({ - title: expect.stringContaining('Please reload the page'), - }) - ); - }); }); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 7158e3d5e7b3e..d9c3752d1c0a5 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -18,17 +18,16 @@ */ import React, { PureComponent, Fragment } from 'react'; -import ReactDOM from 'react-dom'; +import classNames from 'classnames'; import 'brace/theme/textmate'; import 'brace/mode/markdown'; import { EuiBadge, - EuiButton, - EuiButtonEmpty, EuiCode, EuiCodeBlock, + EuiScreenReaderOnly, // @ts-ignore EuiCodeEditor, EuiDescribedFormGroup, @@ -36,23 +35,20 @@ import { EuiFieldText, // @ts-ignore EuiFilePicker, - EuiFlexGroup, - EuiFlexItem, EuiFormRow, EuiIconTip, EuiImage, EuiLink, EuiSpacer, - EuiToolTip, EuiText, EuiSelect, EuiSwitch, EuiSwitchEvent, - keyCodes, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldSetting } from '../../types'; +import { FieldSetting, FieldState } from '../../types'; import { isDefaultValue } from '../../lib'; import { UiSettingsType, @@ -64,71 +60,37 @@ import { interface FieldProps { setting: FieldSetting; - save: (name: string, value: string) => Promise; - clear: (name: string) => Promise; + handleChange: (name: string, value: FieldState) => void; enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + clearChange?: (name: string) => void; + unsavedChanges?: FieldState; + loading?: boolean; } -interface FieldState { - unsavedValue: any; - savedValue: any; - loading: boolean; - isInvalid: boolean; - error: string | null; - changeImage: boolean; - isJsonArray: boolean; -} - -export class Field extends PureComponent { - private changeImageForm: EuiFilePicker | undefined; - constructor(props: FieldProps) { - super(props); - const { type, value, defVal } = this.props.setting; - const editableValue = this.getEditableValue(type, value, defVal); - - this.state = { - isInvalid: false, - error: null, - loading: false, - changeImage: false, - savedValue: editableValue, - unsavedValue: editableValue, - isJsonArray: type === 'json' ? Array.isArray(JSON.parse(String(defVal) || '{}')) : false, - }; - } - - UNSAFE_componentWillReceiveProps(nextProps: FieldProps) { - const { unsavedValue } = this.state; - const { type, value, defVal } = nextProps.setting; - const editableValue = this.getEditableValue(type, value, defVal); - - this.setState({ - savedValue: editableValue, - unsavedValue: value === null || value === undefined ? editableValue : unsavedValue, - }); +export const getEditableValue = ( + type: UiSettingsType, + value: FieldSetting['value'], + defVal?: FieldSetting['defVal'] +) => { + const val = value === null || value === undefined ? defVal : value; + switch (type) { + case 'array': + return (val as string[]).join(', '); + case 'boolean': + return !!val; + case 'number': + return Number(val); + case 'image': + return val; + default: + return val || ''; } +}; - getEditableValue( - type: UiSettingsType, - value: FieldSetting['value'], - defVal: FieldSetting['defVal'] - ) { - const val = value === null || value === undefined ? defVal : value; - switch (type) { - case 'array': - return (val as string[]).join(', '); - case 'boolean': - return !!val; - case 'number': - return Number(val); - case 'image': - return val; - default: - return val || ''; - } - } +export class Field extends PureComponent { + private changeImageForm: EuiFilePicker | undefined = React.createRef(); getDisplayedDefaultValue( type: UiSettingsType, @@ -150,47 +112,60 @@ export class Field extends PureComponent { } } - setLoading(loading: boolean) { - this.setState({ - loading, - }); - } + handleChange = (unsavedChanges: FieldState) => { + this.props.handleChange(this.props.setting.name, unsavedChanges); + }; - clearError() { - this.setState({ - isInvalid: false, - error: null, - }); + resetField = () => { + const { type, defVal } = this.props.setting; + if (type === 'image') { + this.cancelChangeImage(); + return this.handleChange({ + value: getEditableValue(type, defVal), + changeImage: true, + }); + } + return this.handleChange({ value: getEditableValue(type, defVal) }); + }; + + componentDidUpdate(prevProps: FieldProps) { + if ( + prevProps.setting.type === 'image' && + prevProps.unsavedChanges?.value && + !this.props.unsavedChanges?.value + ) { + this.cancelChangeImage(); + } } onCodeEditorChange = (value: UiSettingsType) => { - const { type } = this.props.setting; - const { isJsonArray } = this.state; + const { defVal, type } = this.props.setting; let newUnsavedValue; - let isInvalid = false; - let error = null; + let errorParams = {}; switch (type) { case 'json': + const isJsonArray = Array.isArray(JSON.parse((defVal as string) || '{}')); newUnsavedValue = value.trim() || (isJsonArray ? '[]' : '{}'); try { JSON.parse(newUnsavedValue); } catch (e) { - isInvalid = true; - error = i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', { - defaultMessage: 'Invalid JSON syntax', - }); + errorParams = { + error: i18n.translate('advancedSettings.field.codeEditorSyntaxErrorMessage', { + defaultMessage: 'Invalid JSON syntax', + }), + isInvalid: true, + }; } break; default: newUnsavedValue = value; } - this.setState({ - error, - isInvalid, - unsavedValue: newUnsavedValue, + this.handleChange({ + value: newUnsavedValue, + ...errorParams, }); }; @@ -201,58 +176,44 @@ export class Field extends PureComponent { onFieldChangeEvent = (e: React.ChangeEvent) => this.onFieldChange(e.target.value); - onFieldChange = (value: any) => { - const { type, validation } = this.props.setting; - const { unsavedValue } = this.state; - + onFieldChange = (targetValue: any) => { + const { type, validation, value, defVal } = this.props.setting; let newUnsavedValue; switch (type) { case 'boolean': - newUnsavedValue = !unsavedValue; + const { unsavedChanges } = this.props; + const currentValue = unsavedChanges + ? unsavedChanges.value + : getEditableValue(type, value, defVal); + newUnsavedValue = !currentValue; break; case 'number': - newUnsavedValue = Number(value); + newUnsavedValue = Number(targetValue); break; default: - newUnsavedValue = value; + newUnsavedValue = targetValue; } - let isInvalid = false; - let error = null; + let errorParams = {}; - if (validation && (validation as StringValidationRegex).regex) { + if ((validation as StringValidationRegex)?.regex) { if (!(validation as StringValidationRegex).regex!.test(newUnsavedValue.toString())) { - error = (validation as StringValidationRegex).message; - isInvalid = true; + errorParams = { + error: (validation as StringValidationRegex).message, + isInvalid: true, + }; } } - this.setState({ - unsavedValue: newUnsavedValue, - isInvalid, - error, + this.handleChange({ + value: newUnsavedValue, + ...errorParams, }); }; - onFieldKeyDown = ({ keyCode }: { keyCode: number }) => { - if (keyCode === keyCodes.ENTER) { - this.saveEdit(); - } - if (keyCode === keyCodes.ESCAPE) { - this.cancelEdit(); - } - }; - - onFieldEscape = ({ keyCode }: { keyCode: number }) => { - if (keyCode === keyCodes.ESCAPE) { - this.cancelEdit(); - } - }; - onImageChange = async (files: any[]) => { if (!files.length) { - this.clearError(); this.setState({ unsavedValue: null, }); @@ -266,19 +227,24 @@ export class Field extends PureComponent { if (file instanceof File) { base64Image = (await this.getImageAsBase64(file)) as string; } - const isInvalid = !!(maxSize && maxSize.length && base64Image.length > maxSize.length); - this.setState({ - isInvalid, - error: isInvalid - ? i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', { - defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}', - values: { - maxSizeDescription: maxSize.description, - }, - }) - : null, + + let errorParams = {}; + const isInvalid = !!(maxSize?.length && base64Image.length > maxSize.length); + if (isInvalid) { + errorParams = { + isInvalid, + error: i18n.translate('advancedSettings.field.imageTooLargeErrorMessage', { + defaultMessage: 'Image is too large, maximum size is {maxSizeDescription}', + values: { + maxSizeDescription: maxSize.description, + }, + }), + }; + } + this.handleChange({ changeImage: true, - unsavedValue: base64Image, + value: base64Image, + ...errorParams, }); } catch (err) { this.props.toasts.addDanger( @@ -305,152 +271,62 @@ export class Field extends PureComponent { } changeImage = () => { - this.setState({ + this.handleChange({ + value: null, changeImage: true, }); }; cancelChangeImage = () => { - const { savedValue } = this.state; - - if (this.changeImageForm) { - this.changeImageForm.fileInput.value = null; - this.changeImageForm.handleChange(); - } - - this.setState({ - changeImage: false, - unsavedValue: savedValue, - }); - }; - - cancelEdit = () => { - const { savedValue } = this.state; - this.clearError(); - this.setState({ - unsavedValue: savedValue, - }); - }; - - showPageReloadToast = () => { - if (this.props.setting.requiresPageReload) { - this.props.toasts.add({ - title: i18n.translate('advancedSettings.field.requiresPageReloadToastDescription', { - defaultMessage: 'Please reload the page for the "{settingName}" setting to take effect.', - values: { - settingName: this.props.setting.displayName || this.props.setting.name, - }, - }), - text: element => { - const content = ( - <> - - - window.location.reload()}> - {i18n.translate('advancedSettings.field.requiresPageReloadToastButtonLabel', { - defaultMessage: 'Reload page', - })} - - - - - ); - ReactDOM.render(content, element); - return () => ReactDOM.unmountComponentAtNode(element); - }, - color: 'success', - }); - } - }; - - saveEdit = async () => { - const { name, defVal, type } = this.props.setting; - const { changeImage, savedValue, unsavedValue, isJsonArray } = this.state; - - if (savedValue === unsavedValue) { - return; - } - - let valueToSave = unsavedValue; - let isSameValue = false; - - switch (type) { - case 'array': - valueToSave = valueToSave.split(',').map((val: string) => val.trim()); - isSameValue = valueToSave.join(',') === (defVal as string[]).join(','); - break; - case 'json': - valueToSave = valueToSave.trim(); - valueToSave = valueToSave || (isJsonArray ? '[]' : '{}'); - default: - isSameValue = valueToSave === defVal; - } - - this.setLoading(true); - try { - if (isSameValue) { - await this.props.clear(name); - } else { - await this.props.save(name, valueToSave); - } - - this.showPageReloadToast(); - - if (changeImage) { - this.cancelChangeImage(); - } - } catch (e) { - this.props.toasts.addDanger( - i18n.translate('advancedSettings.field.saveFieldErrorMessage', { - defaultMessage: 'Unable to save {name}', - values: { name }, - }) - ); + if (this.changeImageForm.current) { + this.changeImageForm.current.fileInput.value = null; + this.changeImageForm.current.handleChange({}); } - this.setLoading(false); - }; - - resetField = async () => { - const { name } = this.props.setting; - this.setLoading(true); - try { - await this.props.clear(name); - this.showPageReloadToast(); - this.cancelChangeImage(); - this.clearError(); - } catch (e) { - this.props.toasts.addDanger( - i18n.translate('advancedSettings.field.resetFieldErrorMessage', { - defaultMessage: 'Unable to reset {name}', - values: { name }, - }) - ); + if (this.props.clearChange) { + this.props.clearChange(this.props.setting.name); } - this.setLoading(false); }; - renderField(setting: FieldSetting) { - const { enableSaving } = this.props; - const { loading, changeImage, unsavedValue } = this.state; - const { name, value, type, options, optionLabels = {}, isOverridden, ariaName } = setting; + renderField(id: string, setting: FieldSetting) { + const { enableSaving, unsavedChanges, loading } = this.props; + const { + name, + value, + type, + options, + optionLabels = {}, + isOverridden, + defVal, + ariaName, + } = setting; + const a11yProps: { [key: string]: string } = unsavedChanges + ? { + 'aria-label': ariaName, + 'aria-describedby': id, + } + : { + 'aria-label': ariaName, + }; + const currentValue = unsavedChanges + ? unsavedChanges.value + : getEditableValue(type, value, defVal); switch (type) { case 'boolean': return ( ) : ( ) } - checked={!!unsavedValue} + checked={!!currentValue} onChange={this.onFieldChangeSwitch} disabled={loading || isOverridden || !enableSaving} - onKeyDown={this.onFieldKeyDown} data-test-subj={`advancedSetting-editField-${name}`} - aria-label={ariaName} + {...a11yProps} /> ); case 'markdown': @@ -458,10 +334,10 @@ export class Field extends PureComponent { return (
{ $blockScrolling: Infinity, }} showGutter={false} + fullWidth />
); case 'image': + const changeImage = unsavedChanges?.changeImage; if (!isDefaultValue(setting) && !changeImage) { - return ( - - ); + return ; } else { return ( { - this.changeImageForm = input; - }} - onKeyDown={this.onFieldEscape} + ref={this.changeImageForm} + fullWidth data-test-subj={`advancedSetting-editField-${name}`} /> ); @@ -501,8 +375,8 @@ export class Field extends PureComponent { case 'select': return ( { return { text: optionLabels.hasOwnProperty(option) ? optionLabels[option] : option, @@ -512,31 +386,31 @@ export class Field extends PureComponent { onChange={this.onFieldChangeEvent} isLoading={loading} disabled={loading || isOverridden || !enableSaving} - onKeyDown={this.onFieldKeyDown} + fullWidth data-test-subj={`advancedSetting-editField-${name}`} /> ); case 'number': return ( ); default: return ( ); @@ -699,8 +573,12 @@ export class Field extends PureComponent { } renderResetToDefaultLink(setting: FieldSetting) { - const { ariaName, name } = setting; - if (isDefaultValue(setting)) { + const { defVal, ariaName, name } = setting; + if ( + defVal === this.props.unsavedChanges?.value || + isDefaultValue(setting) || + this.props.loading + ) { return; } return ( @@ -726,7 +604,7 @@ export class Field extends PureComponent { } renderChangeImageLink(setting: FieldSetting) { - const { changeImage } = this.state; + const changeImage = this.props.unsavedChanges?.changeImage; const { type, value, ariaName, name } = setting; if (type !== 'image' || !value || changeImage) { return; @@ -752,84 +630,49 @@ export class Field extends PureComponent { ); } - renderActions(setting: FieldSetting) { - const { ariaName, name } = setting; - const { loading, isInvalid, changeImage, savedValue, unsavedValue } = this.state; - const isDisabled = loading || setting.isOverridden; - - if (savedValue === unsavedValue && !changeImage) { - return; - } - - return ( - - - - - - - - - (changeImage ? this.cancelChangeImage() : this.cancelEdit())} - disabled={isDisabled} - data-test-subj={`advancedSetting-cancelEditField-${name}`} - > - - - - - - ); - } - render() { - const { setting } = this.props; - const { error, isInvalid } = this.state; + const { setting, unsavedChanges } = this.props; + const error = unsavedChanges?.error; + const isInvalid = unsavedChanges?.isInvalid; + + const className = classNames('mgtAdvancedSettings__field', { + 'mgtAdvancedSettings__field--unsaved': unsavedChanges, + 'mgtAdvancedSettings__field--invalid': isInvalid, + }); + const id = setting.name; return ( - - - - - {this.renderField(setting)} - - - - {this.renderActions(setting)} - + + + <> + {this.renderField(id, setting)} + {unsavedChanges && ( + +

+ {unsavedChanges.error + ? unsavedChanges.error + : i18n.translate('advancedSettings.field.settingIsUnsaved', { + defaultMessage: 'Setting is currently not saved.', + })} +

+
+ )} + +
+
); } } diff --git a/src/plugins/advanced_settings/public/management_app/components/field/index.ts b/src/plugins/advanced_settings/public/management_app/components/field/index.ts index 5c86519116fe9..d1b9b34515532 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/index.ts +++ b/src/plugins/advanced_settings/public/management_app/components/field/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Field } from './field'; +export { Field, getEditableValue } from './field'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap index 8c471f5f5be9c..bce9cb67537db 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap @@ -1,449 +1,849 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Form should not render no settings message when instructed not to 1`] = ``; +exports[`Form should not render no settings message when instructed not to 1`] = ` + +
+ + + + + +

+ General +

+
+
+
+ + + +
+
+ + + + + + +

+ Dashboard +

+
+
+
+ + +
+
+ + + + + + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } + } + /> + + +
+
+ + +
+
+ +
+
+`; exports[`Form should render no settings message when there are no settings 1`] = ` - - + + + + - - , - } - } + +

+ General +

+
+
+
+ + + +
+
+ -
+ + + + + +

+ Dashboard +

+
+
+
+ + +
+
+ + + + + + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } + } + /> + + +
+
+ + +
+
+ +
`; exports[`Form should render normally 1`] = ` - - - - - + + + + -

- General -

-
-
-
- - +

+ General +

+ + + + + - + -
-
- - - - - - + + + + + + + -

- Dashboard -

- -
-
- - +

+ Dashboard +

+ + + + + -
-
- - - - - - -

- X-pack -

-
- +
+
+ + + + + - - - - - - , - "settingsCount": 9, + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } } - } - /> - - -
-
- - + + + + + + -
-
- + toasts={Object {}} + /> + + + +
`; exports[`Form should render read-only when saving is disabled 1`] = ` - - - - - + + + + -

- General -

-
-
-
- - +

+ General +

+
+
+ + + - + - - - - - - - - + + + + + + + -

- Dashboard -

- -
-
- - +

+ Dashboard +

+ + + + + -
-
- - - - - - -

- X-pack -

-
- +
+
+ + + + + - - - - - - , - "settingsCount": 9, + +

+ X-pack +

+
+ + + + + + + , + "settingsCount": 9, + } } - } - /> - - -
-
- - + + + + + + -
-
- + toasts={Object {}} + /> + + + +
`; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss new file mode 100644 index 0000000000000..02ebb90221d90 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss @@ -0,0 +1,13 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; + +.mgtAdvancedSettingsForm__bottomBar { + margin-left: $euiNavDrawerWidthCollapsed; + z-index: 9; // Puts it inuder the nav drawer when expanded + &--pushForNav { + margin-left: $euiNavDrawerWidthExpanded; + } + @include euiBreakpoint('xs', 's') { + margin-left: 0; + } +} diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_index.scss b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss new file mode 100644 index 0000000000000..2ef4ef1d20ce9 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/components/form/_index.scss @@ -0,0 +1 @@ +@import './form'; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 468cfbfc70820..0e942665b23a9 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -18,9 +18,14 @@ */ import React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { UiSettingsType } from '../../../../../../core/public'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; + +import { notificationServiceMock } from '../../../../../../core/public/mocks'; +import { SettingsChanges } from '../../types'; import { Form } from './form'; jest.mock('../field', () => ({ @@ -29,6 +34,25 @@ jest.mock('../field', () => ({ }, })); +beforeAll(() => { + const localStorage: Record = { + 'core.chrome.isLocked': true, + }; + + Object.defineProperty(window, 'localStorage', { + value: { + getItem: (key: string) => { + return localStorage[key] || null; + }, + }, + writable: true, + }); +}); + +afterAll(() => { + delete (window as any).localStorage; +}); + const defaults = { requiresPageReload: false, readOnly: false, @@ -43,50 +67,52 @@ const defaults = { const settings = { dashboard: [ { + ...defaults, name: 'dashboard:test:setting', ariaName: 'dashboard test setting', displayName: 'Dashboard test setting', category: ['dashboard'], - ...defaults, + requiresPageReload: true, }, ], general: [ { + ...defaults, name: 'general:test:date', ariaName: 'general test date', displayName: 'Test date', description: 'bar', category: ['general'], - ...defaults, }, { + ...defaults, name: 'setting:test', ariaName: 'setting test', displayName: 'Test setting', description: 'foo', category: ['general'], - ...defaults, }, ], 'x-pack': [ { + ...defaults, name: 'xpack:test:setting', ariaName: 'xpack test setting', displayName: 'X-Pack test setting', category: ['x-pack'], description: 'bar', - ...defaults, }, ], }; + const categories = ['general', 'dashboard', 'hiddenCategory', 'x-pack']; const categoryCounts = { general: 2, dashboard: 1, 'x-pack': 10, }; -const save = (key: string, value: any) => Promise.resolve(true); -const clear = (key: string) => Promise.resolve(true); +const save = jest.fn((changes: SettingsChanges) => Promise.resolve([true])); + const clearQuery = () => {}; describe('Form', () => { @@ -94,10 +120,10 @@ describe('Form', () => { const component = shallowWithI18nProvider( { const component = shallowWithI18nProvider( { const component = shallowWithI18nProvider( { const component = shallowWithI18nProvider( { expect(component).toMatchSnapshot(); }); + + it('should hide bottom bar when clicking on the cancel changes button', async () => { + const wrapper = mountWithI18nProvider( + + ); + (wrapper.instance() as Form).setState({ + unsavedChanges: { + 'dashboard:test:setting': { + value: 'changedValue', + }, + }, + }); + const updated = wrapper.update(); + expect(updated.exists('[data-test-subj="advancedSetting-bottomBar"]')).toEqual(true); + await findTestSubject(updated, `advancedSetting-cancelButton`).simulate('click'); + updated.update(); + expect(updated.exists('[data-test-subj="advancedSetting-bottomBar"]')).toEqual(false); + }); + + it('should show a reload toast when saving setting requiring a page reload', async () => { + const toasts = notificationServiceMock.createStartContract().toasts; + const wrapper = mountWithI18nProvider( + + ); + (wrapper.instance() as Form).setState({ + unsavedChanges: { + 'dashboard:test:setting': { + value: 'changedValue', + }, + }, + }); + const updated = wrapper.update(); + + findTestSubject(updated, `advancedSetting-saveButton`).simulate('click'); + expect(save).toHaveBeenCalled(); + await save({ 'dashboard:test:setting': 'changedValue' }); + expect(toasts.add).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.stringContaining( + 'One or more settings require you to reload the page to take effect.' + ), + }) + ); + }); }); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 91d587866836e..ef433dd990d33 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -18,7 +18,7 @@ */ import React, { PureComponent, Fragment } from 'react'; - +import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, @@ -27,30 +27,188 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiTextColor, + EuiBottomBar, + EuiButton, + EuiToolTip, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../kibana_react/public'; import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; import { getCategoryName } from '../../lib'; -import { Field } from '../field'; -import { FieldSetting } from '../../types'; +import { Field, getEditableValue } from '../field'; +import { FieldSetting, SettingsChanges, FieldState } from '../../types'; type Category = string; +const NAV_IS_LOCKED_KEY = 'core.chrome.isLocked'; interface FormProps { settings: Record; + visibleSettings: Record; categories: Category[]; categoryCounts: Record; clearQuery: () => void; - save: (key: string, value: any) => Promise; - clear: (key: string) => Promise; + save: (changes: SettingsChanges) => Promise; showNoResultsMessage: boolean; enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; } +interface FormState { + unsavedChanges: { + [key: string]: FieldState; + }; + loading: boolean; +} + export class Form extends PureComponent { + state: FormState = { + unsavedChanges: {}, + loading: false, + }; + + setLoading(loading: boolean) { + this.setState({ + loading, + }); + } + + getSettingByKey = (key: string): FieldSetting | undefined => { + return Object.values(this.props.settings) + .flat() + .find(el => el.name === key); + }; + + getCountOfUnsavedChanges = (): number => { + return Object.keys(this.state.unsavedChanges).length; + }; + + getCountOfHiddenUnsavedChanges = (): number => { + const shownSettings = Object.values(this.props.visibleSettings) + .flat() + .map(setting => setting.name); + return Object.keys(this.state.unsavedChanges).filter(key => !shownSettings.includes(key)) + .length; + }; + + areChangesInvalid = (): boolean => { + const { unsavedChanges } = this.state; + return Object.values(unsavedChanges).some(({ isInvalid }) => isInvalid); + }; + + handleChange = (key: string, change: FieldState) => { + const setting = this.getSettingByKey(key); + if (!setting) { + return; + } + const { type, defVal, value } = setting; + const savedValue = getEditableValue(type, value, defVal); + if (change.value === savedValue) { + return this.clearChange(key); + } + this.setState({ + unsavedChanges: { + ...this.state.unsavedChanges, + [key]: change, + }, + }); + }; + + clearChange = (key: string) => { + if (!this.state.unsavedChanges[key]) { + return; + } + const unsavedChanges = { ...this.state.unsavedChanges }; + delete unsavedChanges[key]; + + this.setState({ + unsavedChanges, + }); + }; + + clearAllUnsaved = () => { + this.setState({ unsavedChanges: {} }); + }; + + saveAll = async () => { + this.setLoading(true); + const { unsavedChanges } = this.state; + + if (isEmpty(unsavedChanges)) { + return; + } + const configToSave: SettingsChanges = {}; + let requiresReload = false; + + Object.entries(unsavedChanges).forEach(([name, { value }]) => { + const setting = this.getSettingByKey(name); + if (!setting) { + return; + } + const { defVal, type, requiresPageReload } = setting; + let valueToSave = value; + let equalsToDefault = false; + switch (type) { + case 'array': + valueToSave = valueToSave.split(',').map((val: string) => val.trim()); + equalsToDefault = valueToSave.join(',') === (defVal as string[]).join(','); + break; + case 'json': + const isArray = Array.isArray(JSON.parse((defVal as string) || '{}')); + valueToSave = valueToSave.trim(); + valueToSave = valueToSave || (isArray ? '[]' : '{}'); + default: + equalsToDefault = valueToSave === defVal; + } + if (requiresPageReload) { + requiresReload = true; + } + configToSave[name] = equalsToDefault ? null : valueToSave; + }); + + try { + await this.props.save(configToSave); + this.clearAllUnsaved(); + if (requiresReload) { + this.renderPageReloadToast(); + } + } catch (e) { + this.props.toasts.addDanger( + i18n.translate('advancedSettings.form.saveErrorMessage', { + defaultMessage: 'Unable to save', + }) + ); + } + this.setLoading(false); + }; + + renderPageReloadToast = () => { + this.props.toasts.add({ + title: i18n.translate('advancedSettings.form.requiresPageReloadToastDescription', { + defaultMessage: 'One or more settings require you to reload the page to take effect.', + }), + text: toMountPoint( + <> + + + window.location.reload()}> + {i18n.translate('advancedSettings.form.requiresPageReloadToastButtonLabel', { + defaultMessage: 'Reload page', + })} + + + + + ), + color: 'success', + }); + }; + renderClearQueryLink(totalSettings: number, currentSettings: number) { const { clearQuery } = this.props; @@ -102,8 +260,9 @@ export class Form extends PureComponent { { return null; } + renderCountOfUnsaved = () => { + const unsavedCount = this.getCountOfUnsavedChanges(); + const hiddenUnsavedCount = this.getCountOfHiddenUnsavedChanges(); + return ( + + + + ); + }; + + renderBottomBar = () => { + const areChangesInvalid = this.areChangesInvalid(); + const bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { + 'mgtAdvancedSettingsForm__bottomBar--pushForNav': + localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', + }); + return ( + + + +

{this.renderCountOfUnsaved()}

+
+ + + + + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', + })} + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + + + + + +
+
+ ); + }; + render() { - const { settings, categories, categoryCounts, clearQuery } = this.props; + const { unsavedChanges } = this.state; + const { visibleSettings, categories, categoryCounts, clearQuery } = this.props; const currentCategories: Category[] = []; categories.forEach(category => { - if (settings[category] && settings[category].length) { + if (visibleSettings[category] && visibleSettings[category].length) { currentCategories.push(category); } }); return ( - {currentCategories.length - ? currentCategories.map(category => { - return this.renderCategory(category, settings[category], categoryCounts[category]); - }) - : this.maybeRenderNoSettings(clearQuery)} +
+ {currentCategories.length + ? currentCategories.map(category => { + return this.renderCategory( + category, + visibleSettings[category], + categoryCounts[category] + ); + }) + : this.maybeRenderNoSettings(clearQuery)} +
+ {!isEmpty(unsavedChanges) && this.renderBottomBar()}
); } diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index 05bb5e754563d..d44a05ce36f5d 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -47,6 +47,19 @@ export interface FieldSetting { } // until eui searchbar and query are typed + +export interface SettingsChanges { + [key: string]: any; +} + +export interface FieldState { + value?: any; + changeImage?: boolean; + loading?: boolean; + isInvalid?: boolean; + error?: string | null; +} + export interface IQuery { ast: any; // incomplete text: string; diff --git a/src/plugins/telemetry/public/components/telemetry_management_section.tsx b/src/plugins/telemetry/public/components/telemetry_management_section.tsx index 20c8873b13272..bf14c33a48048 100644 --- a/src/plugins/telemetry/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry/public/components/telemetry_management_section.tsx @@ -33,8 +33,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { PRIVACY_STATEMENT_URL } from '../../common/constants'; import { OptInExampleFlyout } from './opt_in_example_flyout'; -// @ts-ignore import { Field } from '../../../advanced_settings/public'; +import { ToastsStart } from '../../../../core/public/'; import { TelemetryService } from '../services/telemetry_service'; const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data']; @@ -44,12 +44,14 @@ interface Props { showAppliesSettingMessage: boolean; enableSaving: boolean; query?: any; + toasts: ToastsStart; } interface State { processing: boolean; showExample: boolean; queryMatches: boolean | null; + enabled: boolean; } export class TelemetryManagementSection extends Component { @@ -57,6 +59,7 @@ export class TelemetryManagementSection extends Component { processing: false, showExample: false, queryMatches: null, + enabled: this.props.telemetryService.getIsOptedIn() || false, }; UNSAFE_componentWillReceiveProps(nextProps: Props) { @@ -79,7 +82,7 @@ export class TelemetryManagementSection extends Component { render() { const { telemetryService } = this.props; - const { showExample, queryMatches } = this.state; + const { showExample, queryMatches, enabled, processing } = this.state; if (!telemetryService.getCanChangeOptInStatus()) { return null; @@ -119,7 +122,7 @@ export class TelemetryManagementSection extends Component { displayName: i18n.translate('telemetry.provideUsageStatisticsTitle', { defaultMessage: 'Provide usage statistics', }), - value: telemetryService.getIsOptedIn(), + value: enabled, description: this.renderDescription(), defVal: true, ariaName: i18n.translate('telemetry.provideUsageStatisticsAriaName', { @@ -127,10 +130,10 @@ export class TelemetryManagementSection extends Component { }), } as any } + loading={processing} dockLinks={null as any} toasts={null as any} - save={this.toggleOptIn} - clear={this.toggleOptIn} + handleChange={this.toggleOptIn} enableSaving={this.props.enableSaving} /> @@ -151,13 +154,13 @@ export class TelemetryManagementSection extends Component {

), @@ -200,20 +203,35 @@ export class TelemetryManagementSection extends Component { ); toggleOptIn = async (): Promise => { - const { telemetryService } = this.props; - const newOptInValue = !telemetryService.getIsOptedIn(); + const { telemetryService, toasts } = this.props; + const newOptInValue = !this.state.enabled; return new Promise((resolve, reject) => { - this.setState({ processing: true }, async () => { - try { - await telemetryService.setOptIn(newOptInValue); - this.setState({ processing: false }); - resolve(true); - } catch (err) { - this.setState({ processing: false }); - reject(err); + this.setState( + { + processing: true, + enabled: newOptInValue, + }, + async () => { + try { + await telemetryService.setOptIn(newOptInValue); + this.setState({ processing: false }); + toasts.addSuccess( + newOptInValue + ? i18n.translate('telemetry.optInSuccessOn', { + defaultMessage: 'Usage data collection turned on.', + }) + : i18n.translate('telemetry.optInSuccessOff', { + defaultMessage: 'Usage data collection turned off.', + }) + ); + resolve(true); + } catch (err) { + this.setState({ processing: false }); + reject(err); + } } - }); + ); }); }; diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index d7e5064cf7280..ff340c6b0abcd 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -94,7 +94,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider `[data-test-subj="advancedSetting-editField-${propertyName}"] option[value="${propertyValue}"]` ); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); + await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -102,14 +102,14 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider const input = await testSubjects.find(`advancedSetting-editField-${propertyName}`); await input.clearValue(); await input.type(propertyValue); - await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); + await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); } async toggleAdvancedSettingCheckbox(propertyName: string) { testSubjects.click(`advancedSetting-editField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); + await testSubjects.click(`advancedSetting-saveButton`); await PageObjects.header.waitUntilLoadingHasFinished(); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b06645cdfe04..78bb39dd22dea 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1618,8 +1618,6 @@ "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", "advancedSettings.categorySearchLabel": "カテゴリー", - "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", - "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", "advancedSettings.field.changeImageLinkText": "画像を変更", "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", @@ -1632,17 +1630,10 @@ "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", "advancedSettings.field.offLabel": "オフ", "advancedSettings.field.onLabel": "オン", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", - "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", - "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", - "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", - "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", "advancedSettings.pageTitle": "設定", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", @@ -2474,8 +2465,6 @@ "statusPage.statusApp.statusTitle": "プラグインステータス", "statusPage.statusTable.columns.idHeader": "ID", "statusPage.statusTable.columns.statusHeader": "ステータス", - "telemetry.callout.appliesSettingTitle": "この設定は {allOfKibanaText} に適用されます", - "telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて", "telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、シャード、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。", "telemetry.callout.clusterStatisticsTitle": "クラスター統計", "telemetry.callout.errorLoadingClusterStatisticsDescription": "クラスター統計の取得中に予期せぬエラーが発生しました。Elasticsearch、Kibana、またはネットワークのエラーが原因の可能性があります。Kibana を確認し、ページを再読み込みして再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ecf4dfbb33be6..fc9dacf0e50f7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1618,8 +1618,6 @@ "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可视化", "advancedSettings.categorySearchLabel": "类别", - "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", - "advancedSettings.field.cancelEditingButtonLabel": "取消", "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", "advancedSettings.field.changeImageLinkText": "更改图片", "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", @@ -1632,14 +1630,8 @@ "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", "advancedSettings.field.offLabel": "关闭", "advancedSettings.field.onLabel": "开启", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", - "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", - "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", - "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", "advancedSettings.form.clearSearchResultText": "(清除搜索)", "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", @@ -2474,8 +2466,6 @@ "statusPage.statusApp.statusTitle": "插件状态", "statusPage.statusTable.columns.idHeader": "ID", "statusPage.statusTable.columns.statusHeader": "状态", - "telemetry.callout.appliesSettingTitle": "此设置适用于{allOfKibanaText}", - "telemetry.callout.appliesSettingTitle.allOfKibanaText": "所有 Kibana。", "telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括概括性的使用情况统计信息,例如监测是否打开。", "telemetry.callout.clusterStatisticsTitle": "集群统计信息", "telemetry.callout.errorLoadingClusterStatisticsDescription": "尝试提取集群统计信息时发生意外错误。发生此问题的原因可能是 Elasticsearch 出故障、Kibana 出故障或者有网络错误。检查 Kibana,然后重新加载页面并重试。", From 256e4ab67c7fccae9aae38aac22f3788466f8b2f Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 24 Feb 2020 09:40:37 -0800 Subject: [PATCH 058/113] Adding xpack.encryptedSavedObjects.encryptionKey to docker allow-list (#58291) Co-authored-by: Elastic Machine --- .../os_packages/docker_generator/resources/bin/kibana-docker | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 34ba25f92beb6..d4d2e86e1e96b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -142,6 +142,7 @@ kibana_vars=( xpack.code.security.enableGitCertCheck xpack.code.security.gitHostWhitelist xpack.code.security.gitProtocolWhitelist + xpack.encryptedSavedObjects.encryptionKey xpack.graph.enabled xpack.graph.canEditDrillDownUrls xpack.graph.savePolicy From b88b99140bc0d63036c0789d1ddc8dc9597e2b5e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Feb 2020 18:44:24 +0100 Subject: [PATCH 059/113] [ML] Fix transforms license check. (#58343) Fixes an error where the transforms page would load blank with an expired license. Fixes the issue by adding a type guard. With an expired license, the page now renders again correctly the error message. --- .../lib/authorization/components/common.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts index 5aec2ac041db3..27556e0d673a8 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/common.ts @@ -21,19 +21,33 @@ export interface Privileges { missingPrivileges: MissingPrivileges; } +function isPrivileges(arg: any): arg is Privileges { + return ( + typeof arg === 'object' && + arg !== null && + arg.hasOwnProperty('hasAllPrivileges') && + typeof arg.hasAllPrivileges === 'boolean' && + arg.hasOwnProperty('missingPrivileges') && + typeof arg.missingPrivileges === 'object' && + arg.missingPrivileges !== null + ); +} + export interface MissingPrivileges { [key: string]: string[] | undefined; } export const toArray = (value: string | string[]): string[] => Array.isArray(value) ? value : [value]; -export const hasPrivilegeFactory = (privileges: Privileges) => (privilege: Privilege) => { +export const hasPrivilegeFactory = (privileges: Privileges | undefined | null) => ( + privilege: Privilege +) => { const [section, requiredPrivilege] = privilege; - if (!privileges.missingPrivileges[section]) { + if (isPrivileges(privileges) && !privileges.missingPrivileges[section]) { // if the section does not exist in our missingPrivileges, everything is OK return true; } - if (privileges.missingPrivileges[section]!.length === 0) { + if (isPrivileges(privileges) && privileges.missingPrivileges[section]!.length === 0) { return true; } if (requiredPrivilege === '*') { @@ -42,7 +56,9 @@ export const hasPrivilegeFactory = (privileges: Privileges) => (privilege: Privi } // If we require _some_ privilege, we make sure that the one // we require is *not* in the missingPrivilege array - return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); + return ( + isPrivileges(privileges) && !privileges.missingPrivileges[section]!.includes(requiredPrivilege) + ); }; // create the text for button's tooltips if the user From 12f35d5788f5250803434b6d8f25d9df82ac0940 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 24 Feb 2020 10:23:44 -0800 Subject: [PATCH 060/113] Add ingest manager header component (#58300) Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/ingest_manager/index.ts | 2 + .../ingest_manager/components/header.tsx | 62 ++++ .../ingest_manager/components/index.ts | 1 + .../ingest_manager/layouts/default.tsx | 29 +- .../ingest_manager/layouts/index.tsx | 1 + .../ingest_manager/layouts/with_header.tsx | 29 ++ .../sections/agent_config/list_page/index.tsx | 291 +++++++++--------- ...illustration_kibana_getting_started@2x.png | Bin 0 -> 131132 bytes .../ingest_manager/sections/epm/index.tsx | 69 ++++- .../ingest_manager/sections/fleet/index.tsx | 46 ++- .../sections/overview/index.tsx | 32 +- 11 files changed, 397 insertions(+), 165 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_kibana_getting_started@2x.png diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts index c20cc7225d780..7ed5599b234a3 100644 --- a/x-pack/legacy/plugins/ingest_manager/index.ts +++ b/x-pack/legacy/plugins/ingest_manager/index.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { resolve } from 'path'; import { savedObjectMappings, OUTPUT_SAVED_OBJECT_TYPE, @@ -18,6 +19,7 @@ import { export function ingestManager(kibana: any) { return new kibana.Plugin({ id: 'ingestManager', + publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'), uiExports: { savedObjectSchemas: { [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx new file mode 100644 index 0000000000000..0936b5dcfed10 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx @@ -0,0 +1,62 @@ +/* + * 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 styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; +import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; + +const Container = styled.div` + border-bottom: ${props => props.theme.eui.euiBorderThin}; + background-color: ${props => props.theme.eui.euiPageBackgroundColor}; +`; + +const Wrapper = styled.div` + max-width: 1200px; + margin-left: auto; + margin-right: auto; + padding-top: ${props => props.theme.eui.paddingSizes.xl}; +`; + +const Tabs = styled(EuiTabs)` + top: 1px; + &:before { + height: 0px; + } +`; + +export interface HeaderProps { + leftColumn?: JSX.Element; + rightColumn?: JSX.Element; + tabs?: EuiTabProps[]; +} + +export const Header: React.FC = ({ leftColumn, rightColumn, tabs }) => ( + + + + {leftColumn ? {leftColumn} : null} + {rightColumn ? {rightColumn} : null} + + + {tabs ? ( + + + {tabs.map(props => ( + + {props.name} + + ))} + + + ) : ( + + + + )} + + + +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts index 5133d82588494..b6bb29462c569 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts @@ -4,3 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { Loading } from './loading'; +export { Header, HeaderProps } from './header'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index eaf49fed3d933..f99d1bfe50026 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -4,17 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { - EuiPage, - EuiPageBody, - EuiTabs, - EuiTab, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, -} from '@elastic/eui'; +import styled from 'styled-components'; +import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import euiStyled from '../../../../../../legacy/common/eui_styled_components'; import { Section } from '../sections'; import { useLink, useConfig } from '../hooks'; import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; @@ -24,7 +16,12 @@ interface Props { children?: React.ReactNode; } -const Nav = euiStyled.nav` +const Container = styled.div` + min-height: calc(100vh - ${props => props.theme.eui.euiHeaderChildSize}); + background: ${props => props.theme.eui.euiColorEmptyShade}; +`; + +const Nav = styled.nav` background: ${props => props.theme.eui.euiColorEmptyShade}; border-bottom: ${props => props.theme.eui.euiBorderThin}; padding: ${props => @@ -32,13 +29,13 @@ const Nav = euiStyled.nav` .euiTabs { padding-left: 3px; margin-left: -3px; - }; + } `; export const DefaultLayout: React.FunctionComponent = ({ section, children }) => { const { epm, fleet } = useConfig(); return ( -

+ - - {children} - -
+ {children} + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx index 858951bd0d38f..a9ef7f1656260 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx @@ -4,3 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { DefaultLayout } from './default'; +export { WithHeaderLayout } from './with_header'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx new file mode 100644 index 0000000000000..d59c99316c8b8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx @@ -0,0 +1,29 @@ +/* + * 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, { Fragment } from 'react'; +import styled from 'styled-components'; +import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; +import { Header, HeaderProps } from '../components'; + +const Page = styled(EuiPage)` + background: ${props => props.theme.eui.euiColorEmptyShade}; +`; + +interface Props extends HeaderProps { + children?: React.ReactNode; +} + +export const WithHeaderLayout: React.FC = ({ children, ...rest }) => ( + +
+ + + + {children} + + + +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index ca9fb195166f6..ef5a38d486901 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -5,9 +5,6 @@ */ import React, { useState } from 'react'; import { - EuiPageBody, - EuiPageContent, - EuiTitle, EuiSpacer, EuiText, EuiFlexGroup, @@ -24,11 +21,43 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentConfig } from '../../../types'; import { DEFAULT_AGENT_CONFIG_ID, AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { WithHeaderLayout } from '../../../layouts'; // import { SearchBar } from '../../../components'; import { useGetAgentConfigs, usePagination, useLink } from '../../../hooks'; import { AgentConfigDeleteProvider } from '../components'; import { CreateAgentConfigFlyout } from './components'; +const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => ( + + + +

+ +

+
+
+ + +

+ +

+
+
+ + } + > + {children} +
+); + export const AgentConfigListPage: React.FunctionComponent<{}> = () => { // Create agent config flyout state const [isCreateAgentConfigFlyoutOpen, setIsCreateAgentConfigFlyoutOpen] = useState( @@ -123,71 +152,46 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { ); return ( - - - {isCreateAgentConfigFlyoutOpen ? ( - { - setIsCreateAgentConfigFlyoutOpen(false); - sendRequest(); - }} - /> - ) : null} - - -

- -

-
- - - - - - - - - - - - - - {selectedAgentConfigs.length ? ( - - - {deleteAgentConfigsPrompt => ( - { - deleteAgentConfigsPrompt( - selectedAgentConfigs.map(agentConfig => agentConfig.id), - () => { - sendRequest(); - setSelectedAgentConfigs([]); - } - ); + + {isCreateAgentConfigFlyoutOpen ? ( + { + setIsCreateAgentConfigFlyoutOpen(false); + sendRequest(); + }} + /> + ) : null} + + {selectedAgentConfigs.length ? ( + + + {deleteAgentConfigsPrompt => ( + { + deleteAgentConfigsPrompt( + selectedAgentConfigs.map(agentConfig => agentConfig.id), + () => { + sendRequest(); + setSelectedAgentConfigs([]); + } + ); + }} + > + - - - )} - - - ) : null} - - {/* + + )} + + + ) : null} + + {/* { setPagination({ @@ -198,83 +202,82 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { }} fieldPrefix={AGENT_CONFIG_SAVED_OBJECT_TYPE} /> */} - - - sendRequest()}> - - - - - setIsCreateAgentConfigFlyoutOpen(true)} - > - - - - + + + sendRequest()}> + + + + + setIsCreateAgentConfigFlyoutOpen(true)} + > + + + + - - - ) : !search.trim() && agentConfigData?.total === 0 ? ( - emptyPrompt - ) : ( - setSearch('')}> - - - ), - }} - /> - ) - } - items={agentConfigData ? agentConfigData.items : []} - itemId="id" - columns={columns} - isSelectable={true} - selection={{ - selectable: (agentConfig: AgentConfig) => agentConfig.id !== DEFAULT_AGENT_CONFIG_ID, - onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { - setSelectedAgentConfigs(newSelectedAgentConfigs); - }, - }} - pagination={{ - pageIndex: pagination.currentPage - 1, - pageSize: pagination.pageSize, - totalItemCount: agentConfigData ? agentConfigData.total : 0, - }} - onChange={({ page }: { page: { index: number; size: number } }) => { - const newPagination = { - ...pagination, - currentPage: page.index + 1, - pageSize: page.size, - }; - setPagination(newPagination); - sendRequest(); // todo: fix this to send pagination options - }} - /> -
-
+ + + ) : !search.trim() && agentConfigData?.total === 0 ? ( + emptyPrompt + ) : ( + setSearch('')}> + + + ), + }} + /> + ) + } + items={agentConfigData ? agentConfigData.items : []} + itemId="id" + columns={columns} + isSelectable={true} + selection={{ + selectable: (agentConfig: AgentConfig) => agentConfig.id !== DEFAULT_AGENT_CONFIG_ID, + onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { + setSelectedAgentConfigs(newSelectedAgentConfigs); + }, + }} + pagination={{ + pageIndex: pagination.currentPage - 1, + pageSize: pagination.pageSize, + totalItemCount: agentConfigData ? agentConfigData.total : 0, + }} + onChange={({ page }: { page: { index: number; size: number } }) => { + const newPagination = { + ...pagination, + currentPage: page.index + 1, + pageSize: page.size, + }; + setPagination(newPagination); + sendRequest(); // todo: fix this to send pagination options + }} + /> + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_kibana_getting_started@2x.png b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_kibana_getting_started@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cad64be0b6e36e79012970814c7018776bee5f73 GIT binary patch literal 131132 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ͤum)*z|9JCu4oQ+`+gia z>tkZE4h&~S*WS}EW1>S}W5utS1V`i$9kmF}hDdbeWvY$`Bq^9ZX?$7AYTbgwi1Evm z3cD2b=h74otypxaDnnn_3D?smqCnDXT^d-ey&#b>`pyv@69EnP^x;L2J;gfSFl(Fw z-;nmv;kP6{qn@!pmwv_xlJxw6(J_7o6x+_X0w}1qAAig*zf!qyO2QrJdYi8ed~Cm| zMs!OsI_VeaUxj!tx+F)AbVbtMTL zQJA`iikwR&Y@a`oo-SlcWUu`VCooF`g`ZxsGN%ptgFs+vulIoaz z>K$$puG8P$UW3e%X4l(uipzF;6{>|z@d{VefC#H6 zzibKMV(HTdUPqAO=DY6b_%=xln#VUNY((eV-se&JRS}+aB{Wct4YUkBX-uP9ZdT$8 z8{^?~#GBSe!D}<~wVbZ3WV6MjXXU$9uZ2xpId@*&or~8*mn4bX<0EMYz+bDA%gE~# zREfYoOy@^lRrD5m1xD$3d;`qNH|v_^8tWe{L~P;Ux?6*kmk#2VGG3hFRmaKT{KLjw z9G|$)Gvd*o8?TMqXJ~#*Qd7{Hb~>6xn)+)*uDG5_#98Wm#7=v4GO=nGyW2)U(%5yE zr;0~8HvQMvIyoV8xbi6MW6wMF)=LUJ`^%Udva(h_>MwN^<*RgIFJ@_0^Z?b9E;`vG zN!i?rakD|%(lB(H%J~e=;@t@oSz?pN4H5SN=Q0uQE^oXaIPtZ#vDl0y55E=N>nr0N zUVHo8SP!+Htv8DHp8Ta)SPc$G;XgicFBMYcwj_f^*%WylcZbGdIe8)&^VaE>8kK2L6PBKPFp+*Dmg8{oK*;mM8vl zzjgc~aF&MY-)`96?wQ~1+#u4LAevfv>j%sc;O6(Mh`1W}Q$RqjzS*o+hfuJ8^2*)5 z$z{{G0(&3#M;JPOqiOTIk)K&}UnW!d0Qak_8TsA!gK=+CjJk32?d;etuervjm&JLE zv))IKm-l#~km|Y`iJJ$U9u#!95ZAs#wIq&t)zQKj#ibad7m?3r?WC-^t-d02uVclL z(-}0J&_z%q!2n=97!lQe>is`7U1dDo@B81x#2Jn_n9iw#sZAS>>F)0C?shmBX4=Hq zw8`n_=uJ*d*EG}3|FiG!^?$Sn_QdV}T-Up<>y|B5;Ha;HC!7yT56ttxQewTz=bCJn zZ>)(kJcb!Wh|7q-@#HK=p;wpkt_$JflOr983v_+LfcCGl{iF1#PQsb^h~BQctPFou z;xZ^*4gqY0-O7FGeI&K#zan@XgU$}AWjObT{yUGbs>l_gg_&uS%d7QAT;EkaY2k~o zDy8x?pYuOyNBPc64{BtOoFtQZ+RrP>e5MfcQJ%y#h0ESA?kgLN6voeP0`Y_W3KbfH zOe2X8HYb0;=9eHO9eR|)5 zZ}YOrAAEW~lMgrV8yxo8VeCobRT!4|i^^~IxA)K`C(3CtF+X#a=E?qwte~7KiRfe@ zV&?P0z0^eyBU0vo66qF)kw%_e>?30X7QbmQqqsUG2}G`R|F&sdSX9R!ewIY}qOmq} zk?FtRbnF>M-#b;%nYI1jtVPztvI{UqDor`+9<9-$*8yqUQRX-fNx7Dp1&!PnlKld; z*UGU>OeNnNFTPyUn%x%#XXba@B{CdPC}=Q&>E0ovX5M(MeR`e2xGsrDz*o^^_CAzZ zv!?!Nfb9KbCv}HwZ+ltFnChR0UnMzW@lMqob5b9^urleBpM@0{oL4)m|3kdjO+J^8 z#_`9r%_JeSpNr7&5h~}r1o>bbrh0C7Z$nn`K z>xy#j*==@y37XW5>VQPn-?Ss+ugZ5eaPD1@a|aBA{_#K_(8Y7k7Ugjn8Y{AzP929PtE{QCrE}q3z z#`kDU8d@PWMZPLSKMWm!Z_^;lLgWUmo|fqvfK37$>&rvWY6c_jQTj2Jl|Qt6QM@!R zdi#E8^N!9N6Jp0r3sP@X#2RZ=(kbp1%#k(UJS_iAV>IjYOfkIT#jXSCQMH6^ zET#_2Z*u45xa)vC2H^L^>=tx0aX6bkj%L{nMW7$=I{6;_mVWRfjNpR-oZwF#LFFr1 zuxb}xhtq8#xUyYjX98B?RYMj5<*#`+vi#Ff(ta?}HzDXzi1DPg!2NBW|Lcwmjw&k` z50^ZdamtcRzN~Ayl%mtti0H?gp`>T}mIc7po3x@KjZ`MIY0k#cpQ+QkJ0>({b%adc zMQ>C_zeFTPTNm^5^_UUSg}`~#p)#RvujO;ri%D0#VsI@7Myzx{=YRgP|HDJGH3rw3 zxjis~QXAMsbYVEc9=yEzfE;mzemLjZ?$YDr1oj8{fRtYQcA?xXk{^QkSPr=oQ?YJxN2;A4fS%?boaj84N{4nh!yte;t+U?sZ zzfjUUyLFzG?g)5QS7OxcAOHGM`()N6v%ctKvJj6Xh>2-tpQ8P&Rq~eV=R~E^PVC!4 zQwJRkVi?K6C^f^3myGiC!*GkhMvE_RAd7bTT^#wq)vAbyfY%1#`3K&T`huj0%NFKZ zJIY_<{I#*62B?MfbA zw7g?Ul^s7zB-E%LW~*3c3BI;To{vNB~(t?1tXdwLVQpG`Esd??<=C zY*KBb`6v{VQza4FYEe@yGxbdDwIECgT6S=tH2*r#*RHi===wPzcUb24!{>dyaZTOoVrmGX4E9%l&4_S%@8w_lLVmb@!Y*c zU}{WdCn>7rDk0(guzKYnhyCMyc+ywOY&!R48|4TnakY^jM+%j|(!|Yvw_iV7i)h8= ztR&GmZ%yPgoewozBzN;72}4aN#@@+X75pN@Nf@ixC#>zv@GBbTpFdokwLE-^cPp^b zSc&R-bm42>P;lAzW*KrA4^gW5e9&Ic#7KEk2)wa!+NAj@T|{GBbX9+#&UT}Hmm~Ud zY0@F>e;P}~6F%9$>D;d{?GeduF(nelmu~+aslpzQjSe7*;vR;MTF2OT=R5}~``vMT z-!CbQro+k)#*c*gm18gZ-p|d5o`9uTIY_?S7mmmWEWdhz+CGUEMogjWG?=$>Ya~r{ z@UTplBDShi*M7h5$KvB~NjQN?Ahf|X-)RmDJN(uz48bkC)e`_s zxxW=k;J)M&dslIB9C?KIcbI*tY61ktxj6=i5dSGk#A}^)gqC=%o;Y5COIOHW zl#b=W4QhUH(J=o?-UIL7g7nI9@C2wgVJiSm4}(s9J$77z<9|8Bm8sor@)o`~SxFJl zhg-bN=RN=7dPuR$s63jy!$~zbk*AGYzIU2uDNiKw`sYa56HLv-4>Ft3)Z!YQ{Ig2K z`9NF$!My#W;EXJ3CcF5i)XqfU6jZ&;U)*LYFWRF!7-twbuKGg3v-I!U>`vm$hD;tuRWQprnwfp4Ayjp_t+Yujl+J#PBVW+qS*j}HEY zdp=i(Yz2cOwG41_ciIFh|WRBL=y3izclj$kG6*= z^U)cd;=`GA3HK-KDD9A16^ofPD&u)f%NI$YT1IZpx!%N1rB1IuaG`;RrD)BDNKn~b zD%+s^_hnn6zQL^${sZYJv=oe92PSzh8qnVYU=6}wLf0&=2dl)^0oRp>5=mnxZQZC- zuOcOKAsoZ8x*Wc5BndHS_3{`*6`oFl^|znSPkg3!Fby@-URt4PscMCV-By!L$Z-~xn6{b| zcEpjtk3XUUSLjrOqg*tjf->m-;l)iKyKGFQdRmM3=oeVL{6z*-%Da$-;Ek-Zv^|oc zv0Cl%`lcd3{E)|*`O^PU!D@nGQD(ZaG7Ch*QS}H*lBozAW(#oxC z8(w}IO>|%HrOd5-{YKGiIw7~qaZ?aiYODw*C>8arxHa8eI^_$U-8*ihIE;7DaNs>= zpM>-W`>1a3C)Oz`a)}vAZk5CrjuA*Uwfr<)Z~gDe<`H}NwB=7NFPX=sAE(AX994CX z<}uUugHRi^TynO1x&;kW+x{!NG??TG9wu0P@0Ary{g%w>jOzNCI^Ia;`jp3ZAG0$X zsp2gFTTo*8{8o*KYyGfpsvh=*_IOAc+yYV+7}tE_IrqgI!#=>5%DTs2)G*b4J)mH? zFRhMGKs{!?ozJEBZtpbH8T$qS&x_~~>KfQLZrmrXPxUWgS;eZtPTPxG{CI!g5rI^n z_Lk_zA8#t<6uP}u&dfVSzCAOR%Gcskld*IAj34U?du668EIT0%CO(;6e!YX&aS=l`TpM86LfVR>!hitleQML$X9%Fxi3EL%whUqhEasDUqyO}fMvH{83kK^Vlx z8xnqV==1gOed=^BfNtG(zebACDas{1vd#K?*iW*3PJ%Z!WgbmDeMqt1UNlIm^dT|2 z)&$Zw_#66{-(8BV`Cy8Y8ns%SzY;98ZoZx9x!0~2{T3P1-dc;sg}tcnYHB+vK#aX8 zq6lOzpUe>Eh#};uF4?6T(^zl2W$C;JEwTEO?a4Os@z{(v215-taK{<))d5l$O5;OI zg-QE-54b9lw_^i66g?^muhQj;-(=)RJ$$QudRP;9)15awrDiwGStOueCG&hJ737D{Bx*93a}(%=}}TuMm>Gtv!Nk+;tNd^d z&P=u&<3eg*fLvU2;}k)g1JXDT_kZeq_a6J4_MVh2XV3V2ED+L1Qth`Kecoc6{q@#) zZoc-OZ==hVu7@OzuvR z`B|)v7{BZ^V~gN#Dvt7r{bS7;p7v{sIsb~?ZBipv$tDtI_iI0B#$QVH$Pq&ga3u|^ zI3kQ0-fJ{)qbjMN@%3e!)h@P5#cijI6RcW#I85;pyBT8qSq_&oT4!}{MD5JRhepYJ+0DhUw~SCA z?O{hIv|UhC+4%laH`7R`Nq<)z$4LlK9E|)2Ul}%tDT-B#X}Le-9#-G7J~xZ#**f~8 zRBm+q%OYCz9EozjFF*Zmf1FA{Jf(!qQH(kYAkz6^bB8|zWxmUTtvr| zMvm5OU|e!?W8%uHLI8l%MljJCU&e2T{8w_)Gsm8IR%Fubm zVBIG#K$rMGTrm3_5dwU!JCO|owG(8B9QNOyi)`Rxf)~q<1^RWk<$paJtH1)k*l+HF zgU}5HTQ54|L6!;ae+?2J&h|JbdZb%sSQD}ak53XY?sAv!P7Kcq)1FsLTF^DrS7+O> ziifv&kKIKt_1ZivJ1b7^amb9n^}spYP1l^V7AhhGI&b*Z-^hGa-IAfWL2EKWSg}N}R%}24M z>QdG2CKG}zZ_0Y?C-d?|JY%4m(?*_BJNVqv1VQ6|FW*IfGWtEY(e+XC8ZRhis1|)$ zmoNLvM07zO%7{Uz947}i;?7-eRdyRs}u!uMP>Auo$qf_D|B0xd_`KWzC<<$GfqXC8L6_wER={`^rZ1S&W# zsdl$hLOs@T*b-=q9lS`M{e98P<)U0-o1m#tnL?6qdsyZ-ifr&z#~hTjMo< z`@Pba_8vf(<%ZUx6xzYp11R?+Tk#j)Iqmpf1v;*7Fj$WMQ1P2#1}WP2jxD)wD?A8@ zT5*PioX2Y8oG-v3JOS0k9xq4Z$&t4-G#NcuMl`}Zob!|8?Zd|Yss5FXKEn33wQ|br zz4`Awl36Jmv!@AQSZ#q{D6kwb8te5~+-Gt{B}d{&8tyUA(pukVVRXPM)?HZq18y;6 zmEh{uv#aI>>J+ELZ9XwSh43e7!dq%m{i?=dA>=RCaLWi_<#O;vttaF&^auu)?sbwxT!8)0773$QzMhm0%miz29ilM-L+U^$41#1WrL$((kr}pvX zOiF^1r^a?gLZLRK3QW6?2(P5``zzmS>A~OAyGKFIu-s+NX;o(S3dVw|y?RAGmb}-m z!03xrh;1g)Sl1dOk3URDZ~IXldVQ}KZVOO7n`blHfbcF+ke47T1&!2TvLhozRwpp6TZg&`}Js(h1HXTn09F=nCy}5Rx z(VBoL=~H|hC?^MG-QF)E>+a(z1}+p+qwyDe?2k~5xO8~nXAnq43js1)TswpI6RwU) zq!9;m6bG@1{C@Z@eC8%j8v83Py%K-Puc=ZC_0a|lLrX~-isi!}oU&hoBia9BfPS$A z6;9gnCiMO1&(?yy{hh3_x`( zw=_6qkAC;v9!e#H4RzYGWJm=LnJmsNlFKQnl&R1*cZ7FZ?jIXIX?woJp1ll8C?d&M z2I8{shDxz@Q?eAD#`9!;te7Xptn^-9Cn{OD!*UE)gUVOx&kMOT7|qMuYR+X?hqzj% zZt~~=N43dD+CPCgXXEg7V;PMBqnDGpDQ3QAfsWZ_@*~VnxIBbXI7i*ntV5mu0L) zluwCAprz7St+853j7teP%pKdW;4o4OquSAq*`)i$(~W>5wDKK?^X9F_ygdjjMK-Mn zWr4*fI$Y;qzeG9ZY>UJpcF63MX3j~k{5}tYx!ZO#7tvR6{IpLc%ehA&FT8B2tB$G@ zK2;Hd{@o+FRnshDKfqqQvwQNpKecB3T}UN~^_IX82~l-Y3?^ALske!^aM)W*r1}2H zgt=<3VbZL z4LMOx2^-^RzV`A`M?dbeu^TjnfaOh}J)Tg`qc)(xvuomQeJ~4d5Y`H{zS~t;0LZM2D0uH^hCC2B%~AuJl6$S(R&e}*gtAJEb|pv z>ml&}2zjg@bbR%jVxOb92fbnRwUeNs(A_1TSmz=xq|uRrPUe~w8>oUCXkdB!lzSR- zj6+JTI&m+%;pq6~e~zWT2xqwPT~0L<##I`J`Y*bNJ+Kl3t0YRj5uGWPq#Jo#c2{vXg8?fJXb z60LTK=H}Yx%kC7-o2U#akaG@<>~W^Z*ia9Rv~*{8kJ5GG>;Ct z87aM11PNXkR0DSaha#Hl{8 z=o7K=y@Bf55=$cG*3~TnHZb)3W`xyn%ZQD?a^*9CnbZVYwxypO5J+RX=9ZxmAfK<+ zgss4p%3Pjv`5r3K&6M?lVtL#(^I|maz$2WP#x6FdV&GaAD3kU^Ji7`%kJQBi3dr)@ zggVaY=-Flduzzz$%GiGM3?~^QXWcK#;!U@&KSxf~Af8=| zu8tq9vAhl6dw`Jrj9>?-Mi-44Ql()xYG_-UuqM3)6{wVUhu(H;_x)_&`-$6%<*C}D z+rbx6OXru$PJWbRbkl%r{{8;ry$G9E6sYX`Rlv-xE_2h_=(_IBF{&Zap_o?~C-_Hf zPVGibDT`)@U-0cMyTHZ>3A5SGMz5lmy7+TRv@y8OrXK~#`$L&Td!Z@YC}{d>Bkh~i zLVIXLh9vnUWdy0|JgNQs45aC}XyCR5uR6qH^rw4QbK8oyezpG4d80oN2(`7!8EP_^ zszWc*^zcETBkdT}&n+Q_GYQ0Y_)EqsLp2k!?GZK*7~bl4?20|E*nNl8A&_njH!2b3 z#iJJTT7Y$EU=qBSLqZ`YOalochVe8G{;=@xE_uUWQ-KRb-hb}}&<=cyxLSOao-^^U zc?5HB)G+pC$%)rDwLCk_*B<^57GLAOx}I>4pJQ^%BAKUR@f607g#U=tO?1jyXs!qV zY!!^y^U?;4-NMG`eme)<+U1l!<-#`8tU`PHLxP%^zhBo${@3!$HHj;2Dv}JBRv~K#jY~Nk0wiV@xGxVm^(4=fK0c!3S$O zdMifk>b>ARG{AKEV}yF-6As%oxMkAx@ihKCC#Yq z=@HH*MTCkb>y!f7*)xo}M2MGVbt42Rrda!@olH34dsEZ7KgB=rTGgwvsLgf{8fZJo z1hxIkT-l@`9;P$-R%=R?=*q8@%F}8mJw0XG8OT^j_1lq) z_0e+w8rTkLuVpn_!^U#`F54*%9EpO)Pq9((`oEtoKp#O9fWcX`SZSkHvYm=cO{jW$ui};swn-NB=ok~nI zmNP^{i;K9x_;Ne0H%S&lp%Q=(%tNMS^7}l2x7QGc;vV4HBwgYT)-r`}&tEb*X?@>0 zM1vm81Sfpyu;L!%U=pFSj3N^Jwi062#mqZC(qoVB(Gg{&j*tg`o@IoL-`V74T96{N zTs)uhZ1&%T$-)st5>MEE{?zW}Zfru?BJSvOKh?CJ(o=2Pw~Gf?urXUn)3#&^4%}<% zt<$1z_n1X#38j-yUjG*y(T{+jLs^_jrcwS?G2(wr2l`2rR7F(;swDGVhyQ@2QJQeS z`zCJ2a4f0l#duf4{ZAhK8^%v!u9@#RXXnp@#)^RBU=~{p#1}HqNpm`a))e->w`(90 zEUD6PV|0^;(EaotCh7=$zaW`Qj|$&!e&;k!Td6@C_)2cXBNQFXVU33+w@I%K)j_Wo zJ8X2j-!{2o3qms^h9!L{-u!VbfjICZQjVku_k6q3?RB1nb#GL#q{Idl`F+bld6|E( zc(lPNrjTe&T_BeS6Qru$OLWrRAZ6aFbWz}R5s^{|{}IaDQngY_2U(BB#kty>dmU?l zI7<0LJWw}$C%k5jP@4{kL3x&hm4tQJzQ+97@6K=a4ElV!TwqhHgPLSB3?^Q+%Zia_rX81o! z19R;`poiB&G()l_E&uwe$J=D?(XvHv9& zg*G^g=3OTje3qJ7%sr9>A%%D}Z9B1uZ;csF6uWz0b7>Oj5IbMj%=|gLI#Wd{8qnJIR^4ea<@SM*(Aht`|M+UuNyvNW59)~KxICOGT0 za7GC}H-T%;lkJiLG8j|Gdy%0LD1Vt8EeY*Q!ke%u;aKIn%=fr&h(6I6nGVswxcAvw zmBw_@8;rgThBe|ykijb69lw37-;}^XvLP(kR$K2P&%KS&nYB~W2Ekni&#JM_lGm@` z2gm%ja-H1;Bm|}3B|;mytN z6Je;VuerZ_=yQyYOJM4NYgZVM$>RJ)G%DhVn20D?v3p1-dYUG(hS?07`*M(@;#p!7 zBwM>{ql>!T)ZEBafmQ?kn&MItMvzbR62EVKL%a%W9}9mZr=#={BtHy+3jlc9vc}GA?T2O# zki3}ZwT*Jl+T(GZdb>+X_Dj~y`*mWH=oo=~H`8H+_SZs!i*5n*au0J7RY0Dd!a7_s zB~G42FIZ3n7p?rj3Yksj503Z}39eE(j7OIsVc8-^t3u$Lu9moXL{h5rjp|mw3P?yA z!Ao^)565A})PiZRGOAi@KJ+I&m*kS7pij3VeF^R!A#Rng_2uw70f8yE+HAfg5%u{09V_vi zt|3T5lVwZj^ILH&_U)FKi1xRu4|1tNCt-(7hIzNUkHUhA$)tL$u?Yo%mk=0VnWFg9 zHN9MrQJb4-FUF)smj1q<7_(c4UwJL(3Al_#FRuu@O|3Utpr*d4X@QbU9EL*c24r+L zY+>(M^*kle8|WoS_hT28OOK+m4LLbJ=&OWD!2kz0Mk?{`lP_@%W5H=6XqvG$|ISAY5bPTZ;Sn!#a?+Jb@7XEw@`;i4F1f@kT_BWn z*Y!^Wi~g#kIX(4QEniVc7?ErNf2K(4*N=AvZBI|37S+S7t&UBtt!PF_wtouEcBFZk zgFg@cBKv67nEb!=Pub_T%hee*-W_EsSO9p0sU?&g!`OkAIj@y+uXlnsD}KD7XK@c1 z{J4?79Ee_|ZHF{=Q3B`D6z@1HOom@inMS&b4oP2kyDIhk(< z_8^p=RE$O_RUIzNkhDQ}3$9)2GK1CAPEjx)6u?#Rb@wvC=GSDL)ou;yLOzo7GQoS5 zAE^>*Wj2Yhp=ja}HEM&Um=PtR*GWZ9pjdkUp{~eF_W~U{CS!|GObG%CEoQq068D7! z_&jzIQwgugFrE8K4G^@_*=Cr^MgQ31r2*Q&SB}`j-dh)T2{KiEzPGBw&oRB8!%mty zF&J4bk>zgTK_XB&_j&E&Nm4 zV;IBwny_piCE?C^N*8jlgkNv8QMARJxvLanwiRnx6EcRQ>_KbdTc~tRomJArPQU|j z7~^JIC3t9UG<|R=D&t0JX}>O1b0#!t_~&gxt%qGAIuku%(TPSp9aa)=qjKaLRADMy z=W|ZXNBt^t!Ll&-2viXIU%f?(o*?Anq!0$N`*}hlew^CzmtYB~e4g=O$%pl33r;H* zAfRdcQtlTHtI{an0yH{IGir2_m7!5rvsE^VQc#99lPVaF0dL&47srF!Y$TKxt0?uv_y4LR2f+iXFk+ zSooJh@zpcayaOVjpdIqb2kBmk{g%9Ws=;!CpH*(=4|m zVs9i4I5)Qcf_!hzF0HO>2ruxYf&YaAByUZYWyxap_-;EM=cIri zTJJPvj3B8M@L=z&^hr`73Flui?9JL0%)=d#e5e-*AE@{SerFU;fKI2Fjh`$iia!Sv z9%jMwIVEs<=LFzJtc^J)#9a&$XSA+jkhM@dK~wNKy+}6-1eW+&8iYc6q%ZX}6vUKT zQma1vJ6|2$|F9GL;&!q_LbZqCcF)*=AeJ}(Zjc8`>MHC)BW1;{3n?FSgpcB3%DSkV zc6o+enLJqwE|~M>@WV6E4jEoIOfYbky(+~p(*maK7!%(QFFEEo6i1$Auqv2h2F$qb#G-^pYq&EI6Y3!G0Nei0qxNOdDb*#M& zzhAy?cHg02&d8TE7VPTQYI*CktJTR(IGh`RA`* zrAZeYjuo<8`5OY3tRtK%4i*Thf{A^)lv?JEHvY(j>)aHP^AlYQiK=l9&s|U%!%WPx_4r5J7>j zMQ{@sD3}2h_Z+7DHn#n>e{n}|N>XyeZ)b*JEveSiuqv{O^?7ZSRaU-I3RRp1O&3{L zi&1*kG(WYKhrfxs`EH@b5&R~$zK_H&38%4i8Vud3MDF&|c!?^>p62bedmG6#v;}xf zdD1GGE?TW=(pMQf5b}TjooB#Y-=-*{mrxj|_(O=jypG~%Xb~nG>t7hgOmhQ-WmGY(@f{X_l7FY3hl9FOSl*TM~I zjPTA5x;LJaH1vp|OQ)1!_lWeHJ{@}BKL60kUl_9f+w7u$L2txmzRq-RT3~Z$1!%BQlN{f`h z)CXUyOo+abHWF@knD`u76NqUPI}{pyUaPx=$C)#K*7zUg`HnPXFK3v$qQFi(M zn}Zbph5*XLWFT)?{6?N6<|S@5K}EaJ;el08h*9P>6ABE}z)2^gO0Ej8I&+4q+p(>+ z>z;_d-*f#jrV*(3Wb1R$7x`>GxpM46)@S(?f=yNIX_L;cQewT!hp~J*ZkD5iE)4hE zv89h$rNmefGSo;`OzGyG{>AqZ01&SKDWNW~C5=fd2aisVJWA#9;BZjh|DA_Ml5qL0 zKObt8cZt1u;9|Z?@g7$GJ5g%r+f=emmQdx5R%;jyB=cSIT;8Z869cnkXZU<+d+w`r zZp>;BSI@PtmqT^2=DraH$-njx+FNq1nGZ)`u;{`LCJ&`}X8mv?s$g}0J0uxnR} zU|p^=l`kMe|r;_irsn!I1c$Hw6cr9B#^* ztJ}|8vfqLy=^252OONtG()SbzX%jmIYPW00xZYlRQZTH$5f8;g;St8|-`F85g|tD9jRvG4=~}icE7*siTystx zJPfTTNb0@#$AmTW7M9l^^AMKRpEroG_lqaKF}GyXNVt{wFOZ}goVV9olawy%K6Sy# z|AKw^Z2?p?n+@Nh=IR3G`6kSXd!?i1ntIK&Ja+gZ5WwTuwPp5J@eQa}f`g#H^$Dsw zhTa#7Tbd#ic)WIoxED0RZBxL?F?iSvh@=C#|i8k8Gp8UOAv)HOW0DtU$*>ViQWulsH*wWr_! zs3`h9=|(gha5#Wm4zI5RfpT+C+IcZMig7qRP25B1dZa0`U3Zhw;U5pLe)aHh$tgtb z=kT}a|FZ!5sM$IPE3=jp-wvD86g=)cUP!~D{fina`4i3_?_V|5OXg6zak5~f>TBFi z^Gy&EL*hrT)t_RRf7KOq{?dOpgFV7hx$owW2V+fcCJ$F-RTd2L^AzkKmvHA5-)y40 zv_m%`ppB=fJ}do#-H@`69V^rItLF(4^*}wALic$;L0l+v%6J3BE$lk6&!Pt_il3MY z4_1Cd^{uLw?Rl0tf`iWJg@;!i3X+X;++w?n>?!`h!;~1q{m~KKWuo&81zMOI5Th`T zszRcu(~Zz+yV|9~*&B<0z`}-nm)YSn$^I4CQetZ9!?i0ABwx6WgRRx{QL|j%H8()+ ztEKG1XpuE01ZE+pnYj#Ge9k&ETg8suPOKhA0=T}+&eF0%eqH&hVz>x?S|l50NMi-Z zYRw5jbmqN@J`#KGo8`A7f7pLYD=RRTbVLgHYn~|_P!y?7WS8Lsp&+@Onsg9j6a5u%shW!zzwcj!k>E=x(EakV zW`6xAmW!&T?Cz2fjr!m2ViB_jbpXnOgho7;IMt~YYsO%uN!Wop;QA*9h6uEG$(@ku zJ9$i{W04ocUul%OxX`PD4`)hK18*q}qCxMj8$)#>99IZ>j`u&@mNg#*_V86e)i}!E z-RpPx#YHeLXm?PnO%i^iEU8DbtpH;|-_HHvX$ncFu}Ac$>>UR5nQx0FqEhVVg-yZt zTH6(7wDzy0s7N|nzV?5CO5~}twL3=aH=)Pi;8pj?@pvrZ)r;>4O@~+Ysy_os1%Kr+2V22hY~zIVbU^UKt@EBi>`#P1>C1!A=xkc2 z30AoTu0B`Q5&+!w4jW2igv$5BrsSQj@Ac9-pU%t!_;UJGTgs3$0~nPx}9h4?C#UzOhpKs^Gg5e<c_3P&t|N-AEGnA$1_VPd%9Yx`{G_sqhfYeCd>P$^2h6 z{B+as70s-Sb#uWX&uyG|m22r`iGumV?RkKRAW&-frSdq~0K!)}W)85-U3BSMsiCJ5O)T>?N^yfVWsq_0|BXJBn*eq@Z?xCqA4zjo`2Ipf+wOBr4Cpeu z2E1~S6Axjf$~E_fV0Z6HQ8U>m@fdHkvm$R-UV#J)_e%<9h#E|D#f4wFCf;Svh&YkfSC(>+Ai;cuAxLUg{pd1`81j zVBm7N;DyguQ{hK-hW=BEE}7!+XXcNijlC&$Z03MQiO#)p_DS${Z~x(5qC0 zSOvWU$F3B25(LPanX(*8W8gQ1&uSQat`zm1FfLM6Kt~~3L#*aN9iq^=V0Ay;Px140 z$mYHltf$n#oRSCRuADd*DzSf$yYmszH`_sf_BTH6ym$oZ(Ri6s^Vhe#UCMv?c=?V? zT&;JX->dePY}~>4C$ptHjA{P2#S10d_1Kj*L(~m}nxyUJ`083rCAP|q4}pHJEE0$Y zi9Cf>Irqx#Dlpu0r5(FDuiK<1uM7?9{;F?LL)=x6!okJ0OA(&KiqH&r-%IfLT(xA^ zAXI;QfVZW2<))jV`?Gfm6~Y7|zNRBysX3N`F0IqJhkXn1#YF=)QRxb4AkR43KqX9d z=M=GMbIxDsB#Jbl|I7X5>tYboi(X;d?=tFbA zmzf$Y2z$*b@L(?j#$w?Q<-m*{-(LP3`AOcGjz7;z4KC!|I0B`NMW`J^N4u(eSc%Q^ zl-WuyMGxO764!&dEV#;4J`0U>Aia&g5#vKswpX@a;ba~>vhd~>j|JIZi;;aFUh;W= z(I?~{y|Yz!q~w5nmy4S77``(C+gg>0`7^wT7t&}|RXgL>k&?HE(VWmotx#k$=Fst` zR{N+YNH9%lIpG_{W4!sOhpUwYwQFTeDLUs>o!r0;6v?j1y!2X5rKv?cYVs83aSE6S z<|i+8vVqtZn21`Xa#(JKj|gAhhWsA{WZds&+^^{-Pz3jCe8EHULMcx);9SMlgrkimQjb&5Bnb|f zCl*n?yobBSh@J*MQ1TF9aTsuNS~bmJlOFSD?4b2OU9Imv>#iEZjDmRE=AE*VF?sK! zoV@c+@&}YQLVGBcGiw-2xapWNIur4NZeyjvOB~$#?~elSsmd>n??ir& zcgDwwgo^S>3Mi#u{NEilzqQ!wpjy zWrb^t-|8!i*i`qSf3zSS23TIE;(rC_bD?=6|197X7q-0M5p`n<{0G;+3{hK>f^Inl0P@wd>u!D)RhRML>1y{>ckdG{fgaLz{PW)GSkV5t}w$02fR zPa1kp;z%50ygxRWzh~76BX7UuJ0Z4``6Mda_e(ba%lcPMiWpqHn}Wmb@0pn;UPivn zvWZ=^6JCdfLn)QC)-y|rllD`%%=%25jVIu)AJ@2ihYevopj^jBr^uzA$U9wnAa*bX zY)83CC#XLB=6KmfWpI=Go?teOpg5A1#Ln5gXe#VzAq<@Sd`@;{7TZAQYXr0$ z)ihcj;TfFU7{9hA_moip_BCI-xta)zVJaFhn#k2$ z*m}#?DyN$1XW=%q3tHgvU8(YSW||@$X9=Tk;>dFpVL^x3+z%Hm8h);cl=9$;m8_a~ zNl+ur6^WraD1mDLC($ZT)Vc->zi{XW>t{b8SX3M^{BMil1`mfn-fWxnd=*o-CebV~ zArznzDyzz^Qpet-`9lJ`U->t>af(&=^4azvvrtEI_{Tux)20qK4~O^(mg9G2Jj}svrnX@LR^Y-5_Ol{TCdzOk%s*r%s)t=^Qq?8@gI?Y;jVY z0BJQH{$ijyQ!-L|IU};URIfKs_v=@)$>M*E-Fpd?pI3pS z)uy$q&#IaUDoVg`an{JT4zyC7jr}quQzcvj#IocrJqp+RpW!z)#_jkK=W-dA%;F3ME)n13!@sFnXDT-2stYzv8a*jFfG+V=LjYCWPD%aI z`4pyifwh)VnusgMD&;ehxj9hc9S?5Ltvzx2)Rir1>s>v$pbHYXpyI`EybO!KN_)g|Q+@ zbdt6Lq6fva`s;NxT+QvT;g%!wWlA8*Lm|N_o`?jkT&tlKoXa8o0Z%Aqp~|E*wh7ds zNfYHhP))&BCLzCi*ZU8%H{nXZ4v4*4^OZfg@DIY__Aa^|cbpdhC+S~3dFkl|k)mZd z8SP7Pe#+I(gYqij(iCfVmV(Erru5W_E#rF(5aJ&-gC znuWISPEao<#;jo!LGcgo*odu{bx!U~P%8diLDqdb{DCeTX(iSd-`D%vE@}e$pmnhS z>fq<7)W;;}?wf-_Zj4XQ@=jB?`@_>nDv0p$*q*P4VB2v75)R=vYX1%e_I$#h$qcSZ zeqc;8W00}Ji7_Txa!)FimTHY6VOzRsTS|mi2Yq(EuMRCozS>AN<5C>blmYQei$gjBy4$Ga6mrjS7Jx+?h zgmQd&Avk10XB5wxL>aZo&Rf-#h1n4No~J*Y5Q9^TktIax=18oiy`fGb+mR8Ql)zlX zkbvFGeTUOlFMPH1i2j@olBCJ1#i$f3#YUpDIsDO(+xcdXwU+udS!CUHK3=*GA1pt~ z*xKP@mDV&{E9NY+n1^UFyLQj!Ti)VRgL->?R^IkP^ z)Yum?a<%!kT8vk7+tv%o>euG@St&n&>J}H?)(~zUXuw*l$DL<*Y>wZuF2X$B!Cqm- zdyP#Hh5TIruI`7(IbUwTVIWy)hbA1ubQdM`RxZ>e`mZ1{UKZMly>R6{*WfsuiC4#(>=k>>lid7^S{tlbI;)ug7S)JeO+!4obpAlH&$@L~KJp zY*t1Q%ht<_zugGQ<~7Ynt`71pmJ6K`%Gsi9m4U zWe#;a6Z+b_ytEPhyM-}qe5~@xsO{eB^zqQ$TbZm?m8g(gvZPWR6iHkEXiYL%MJ%I* zCGsNZ1Sg#E%A&IDsHYjD5yu0LG8mltVB5Q!f+@r2;DY@xNO5j*t-6nwOo}KatA5)8 zSH#8^9AvsTmh2c4m}C8_hfqO0FPJD6U#(S!Tdm5PR*<(h9nUD;SGkgkRi5M4?~c4< zJmw1dL2M1Bo|Oj zQioUD9RhKZDS54$cYN2NimEQX?nA61kP6!bo9nISytB1Tb|ULvEPfkbJfe6+e^Kw_ zy7WjO>+q_d%l%Kcba|t8^@DddvF2A?#4bPKxHC#a$XJehEUCKCVAMLiMBOlaeOsR- zfJ-&}^MoJtMi-$b|H;lxkk2&;Y&GKzHI?_TNg^>9> zYTzjLKaKCkw(&%Xk$ECg{mF`H&sHnh&k6fN35-(Kyzp>po- z=f(@D2cZEh$xzryKFGwFt7J`6w5x>e&ugjyPNf%&^2$t)7zztzSUS>2ZYW!kQZji+ zxjR*m;LvSEM2U7j6PL)Li6yakH{Oh#nAA^XA77(FuUxkoqUD{;{1_f_2ElP^EnV@^ zM4=w&!0|O&D#gil^EUaxM(p^d9yMG2)ap~eOHp1(Iz zPAHDoL3r>|k>sba{` z<9N6sA{6pC?8{~ve41T5{l4HqOILHTpSCSbn)xe>1*ZqugK-u)rJP$#k<^sqtBkG+ zWuCmnpwH)#>i%BJm98k*AdEY%{OK2Yh1c6KsZx*)w45M(yH2FNK0)gsv*+#W3}QxR zCl4N?b>MBoE;}GK4rARc3r49#>cMxxdfYp4ks^N%DV&pWPvxz(wTIx|g@53l>q6l` z`O+NkA)`*@k*FxssV!u13dcr=0umTHsluM&awekS^JXnT_Ncw+StL&kgs3HLuD9;< z%OwMR{XXDj?a78IYc49iCl~sL7(b~}HU0M=&6BG9cj+x?nZKnxDkXCRIBi7N$&CTY z2-ni(vOrS~%b~NN4`NcLq&RYoqi<(xJ-a~y|53}k*FGlw=a|gg>W&qm-ch7nMCsvQ zFIP47xgC}mU;4Wu?iy?l3pDtgh8Fb_hz+qOy@AFAX+_y-*u?maya-+gIx9G`#c}bRk7@_?fGDFfViu<6aE)uO^Jr6yzk24>Sf`rTH06V*}EQS^;FjNUX?Iv2iwaNVrg zi<+^r!kV=M6g6O`bbrNwh;nTr#Z#m6Ik8m)XKdlq*y*I0joo&h43Vr#M|c}m`3+@UJY+CCuH0J1u4P}fj+DzbEygqtl@A3g^f z7bf5R=BR3lZfZ(OtwW}SmDNcmH^*g&#lN&1JTa=_CuXg3UYyCJeP|eN!20e(ic};$ zigaqKGGYv5JEEv(e{kiFu{0CYdO!P2c|8OPJYs(RN!aw9&bl3Iq?vA1PRf{>3G>0o zj4S@-3Kl#0fS^@042pK$H$V$azB49r*Y)T&88!xDJGsNa`{AId1ICZN7w|mOGq_Vy z!?PIEIk_aD661}Kvpy>BjFF#_FOydnWK&65h&72*$8(Ml+1>V_J^X3!n+~~laer_DI2wrSG?jrlliTbbEEe(;z{{toyJsSraWlu8} zbxKB;`lm4?41BRIQPNrC-`<$zAnMgt#pfogW5 zrUVY_=1`iYtMLeYI-Tg`iD~}eTWq@g_v-}x5?HH(t6Awnbg~2A{y5W2Y;()7N0$nG-ew?q=k4a{6q(|WSacuJ`F{lA z=Jm9;D8f7^2RY*iHkf|wD_ufXvW{)a#4*X%B~j>w?mwgDFV2X-KXiLRYwqq)X!n@( z>l#q`^+LBI%aM5%)>iy;2rFejf_pL4`iH{jhKv6l(Y@_^ZMx3gr%+v!_<6>C6mX{B z`E4Z(%CEoO+eg8rgvY1`E(y;<*XPo|g3{c5tbp=^ft&JS;wA>`z{}_d1?=!`IOZWv z=n|@WTeJ`-yssIZt*55BM&-^^fJVmmx{l5R^QG?IJc}G=B;Pn@j1`erNZ~jM+f)=` zg%3_8DOK5H{?xFSN};VAO`;!<&`eJcXIU`&o%%Cl(|wIgs?iaOYs#vxq--Xh#`-U{ zmB>1UEP2?PppvzBd1zny&foCq!^FK*D0Gk0#_5>-aiV@d5rFDx ze%f&$ymCgpJczYVx<%NX6Bl}lKe|-!8(7P=@=fhmd;6cThTB25 z1auK2+1oo;V6A2&KNad`kV7I zftw3vPht-}1_7Y%j)avw31o3bqv|Bw_k`lG zf~JFpCyltp4^!6OB_4A7Vp}IfE zxOFWCV_=YA5RNXSR4E8mo+R?}c_599TIE?dtN#Y%*_yqJ<2D^_lAa3TO#3>mV1CH$ zrEH>zVOA-r+p3Us*9;q;UwZ3K0c8KCM)}TLOeGSy>mp)Ny}scQqHM z=oeu7oXMj6!MH6wY1C{32(2vHOZ~HcBFX964V!8$b>>m4sg95>|Gt6y{r_nJAZUWk zZ6<+W6YB&-s#OF!^IfRK+MPe_PmW+|R*z6ZNEXToWAkXd39Pg$5}+&1GlN-NFM77c z!0Z()s--E)vR@tz>m_{Q9|ooTjxT+`oWaL)ME9sL@bvwxe8;<97F@oyZm1BwTjY7$ z7_wDbKZ&*6Fl;}HRRmzFW`K3{btRFi+4AxyvV~!Wmzux5|3zrI~M0 zO|*E<7@pyh^`KVn?3Saq#p8FXZ{s)r*_TT3VZWl6fLojJCa`{&lpc)1pxhvjt?4@m zU`XoSse%K3iYX_rbiNP@QOoWS@)r&Mvsaj)U{JNR#X>GDObTHBVXLQl>nSn1=5ft_ zTX%d0jIHsfdWv%F6Sr?2GuZjPN3`DI9+a*CbKq+DJ?nkycy-U???gHHjR>RnC-={p zunS7Y17SXYAe2@W_ZL5V49|Vx`RAGDdG_QrwY5#Mu#0aAB=j$#=^QL6c2e&xCb9@> z29^YF5!9NT30Q5#K57PFyF#hm0mZdpTjrt#XJiP+*A~Ym&8+QsfrQf(uXoO=xClR2 zH}X_d4lmUsJX0V1$41Yy+aOKKjQh%pvF4gedku^jqQosi*=0>xl~>0{PI-nD@Y09PWjT3 zz(om~mVs)A!RizVCN8cj4a1*;g_|IbYK{EWabzIo0!NRi#_m?1g!%2LKmsEPhDd{) z3hP0d$as|PGY0zf4o|Rd~GXo7g+-VMC;Lv^3pp6&B1ApeHA zbV*Sa$4U@Zu=iGd8+F`$cDQAuN?{_<(XA|ZOc;_GzdGqK!#{UffA1z{ZsXnlHHmmd zA`b0}LuZ2YVT#b8Q*xpRP#n^_{zw5YHT+=YKcg`B`l1>~wc=u^@}5}|KmkzX%z*YDIO_4vRnU>t}iVhI{qh6B{#R{oH`J0XWQoYIckV zkw9$5@5fCODM~QIlFfa!_l&X44JbN@Q|%2nVX0HfqnfyAwwimh$i!I$uV0*PI4(rJ zdL3+&M{^ZJ?>I5)33RT}WqThrFy*~-D#^!U9q9guJ;AJPXHJ-r5|c_f5oB7 z-X6<+ZUYtptGU!)9V$7IP_+J>!>v~`BY*zAnLS^itbC!J&T@`mXlmBXf=Jhb<%qRa zLpb8e}-V5aQo0v%RL*+{`sw+AR0dlsG8b%h`g;w5ONZMUnYh+&G^xG@l zqiE)^?R~kxs&)onRs~u9l1|ui>Np=Jb1adwILJ4!kSqkkPn8<_OMO)43|Z+?jjtn0 z`5dnWy@4bN$!Hy^l%7OtkLM04M3O-6>ZbZ8$bX#SBr9s*lpe#ZVKS;|X3}LbvV7@| zA&u=mPIRmXGwd=&b#qSzzP&rsO69sk%M%mC4q2+&wNyjlt21})eE1G{R_YvDrvAX@ zxK*>BtkqqMgQ~PNIT8;7%Fpd=2##FFbe5 z!S3;VY+jyUdaaVWzZNWc(!7B3%P$KMh(C1xm=dKj#%g+%C8#!kC2hxrnU>`D$W|-@B8o`1O z?jMhw%QtIC#z(rQH}WfgoC@)Gde*1>h8=3Mu|I!rBn~;iX0j1%Tv}#dc>$=SQCix6 zwjotV6VNZjQ-8do_H4vlcHoGqUqA?2vraL~n=avBx|&-;!(E5iF;nMB@la>ZMLOp6 zyQ6k353~JRk^b$>(Z0|jdyG6oyVuM?C+HG?7E^Sp5(V_3#^@(SFKoq78K>-hf?zMR zUup(8oyI{=t{gV7?eHLd2o#7lnO7>Gwk8-5geo&W9ft+)?nZ6T_jMgpd6pqq@KQH` zKkI%iADmnVv!0g9fWMh9(0E95%<;3>IWhX*6HjPw0>?Dj83n`6vb8PN%z7jc#jT&( zc2t*B^U$`&-|0>sK3y}u?byZ8Ec&Oa+hjHx|3;xoLvyNfsW527+=;iK_*4;hQFlgR zc4k)0f2_Vd@GUbf{}>SW-dw8j+U&LN^rA*Hcm46#(5?ODc+Z&H1G&~;UvY(MnPd6W z-|;P_<0=P*<<-0CA(puR+*Tfm`a20wG7M0HaLLp_>4MbU1G^z1S7cLqhb}OqQ@_K? z@G(HR9+R``Uq(n{#?OnGD%W`p+{MXO(UqK68%8G8qJNTjbhHmjo`M`eXh$o_ojF4S zq$CztvOn_jj&Ip7yy;*)sgNi^brx?GCj%Q!B10jA>!2~V@ZJNr3?&U1~BdH zvGC!rkCRf(opA#`Ted`RJ#M61hskSNqd$FA&sTXVy0V;aETSIQ@lbB_nhl?l*;n^bQc*`g7 zV?p)ebVEL-oU|N7A!=1%NnSP<%itu?DFI$rwD>Zpl(ep~v>p#J#|%I47|Bh>x3FOp z6islgO5fatvc!7R#w~4HtZ2<`G%{PYBVh8vLUE{bXAnHR`6TGF976H5mgq=yW$P~& zu`nJ_kuV6Rby;2}^g-}TttW;&p42qF}0-UR$Uq&kh=gG1X_FH zeI*nUmZb#WxzrafiOH(LZJ!jm9zTWt^o(tJ=KT+EjtC{SshTyr;MdHE1^*YMiv_OJ)Hy161S1EoNH- z6!f^QFRyNegXA=pZaiLZRX+B*m5pO9i(l?JI}(My#~-Qu^{b&<18&&fSwEKNgqLcH zY7_gAM)-7x6roVLDxu6Dda$Tflyjpe{ga{;Be<_A*=9gxV9ybaEO%}S0h>SS6U4oX ze;2zagXHxKd-uFjtVv8|Y&JBDTSUQZJ2aPnZ)W z7J>_x4V#grHpqVy-8ua%!!TOpt#~n=--&3TP#PDd%!cPR!Q?u+k}y+rcou~cOdY00 zpSlOgfP%Nmr(H+FJz`EWW z^f{X$B*Efo8!NW!!gwuldmweWdVR#4o2>Dn> z#mVH4>G26!r_)h2Hv6$LZR^UhzN&EP7t!O~RQ?Y71~8Fomha-t&735*xPJ+8yHf1M8$d8!b|J<2-fD>=9ic`9pa-^QqB0`(vRl;Zx9h8YL24bUYueS)h6O1txM{N zNioDY7l<21UZ$Ald}YMwlz_fa_(PM_BHL;{@ko=3OYI}rhR!>ofl$Zj^jyYm`0uqH z?Q;{nd*~7S?VX%_Zw~z;4NzK?rcUR2lBHe<|F~E=xZ(8A*~pilG-m zhag736JM=f>J-Ccd048RCF8$<6iTi5zLw%~%p~*-sFM^{u9CYjnSc0KUU9MHMt^23 zoP@8hpfY&r=cQ$61~WXo2B}jc50~5*b)BN#_ZP>grpu_=K|<%7H&?{os0(9&9zAhL z*nN>Y9ME-+bhG&bP21-Q?-aKEyI-TJbw4j_Xfef=acmzy(hhPnOmz*SFQzRJ!DEgU zSHBoRZ9D=cQQWWdEv&2Y6;s2`l+HcJf=#0Z`^4p^FibKgma_Ere|(3~rhSG##f6U} zoe?yF#oUyY2;b`*2+1g&{zAI%ogjG(J!isgt21Z14Uv(WFPMx`5IT|la%xhdq{=HY z$@F9)LDnB=No3EroijDmWO;^xKLE?8QDJ#V;x1|QN{5U4#kSk=cS4Cr?}=?#)~V$w z?d4j=i2_v*mUq#;Y{B+)+{>q2qYuT&2FUT3q9IQ|jc)fD3=c2K%j;XLI+yP3C9Och zL$RFLm8a-u_ipcT)zi*{fg{tq zjw~xKwxT)wc5BlfGS5`Dev9EAe^7l&`~x(}S3Dxd7+tiwZcZ4lbjSHtu|!8|=nS2B zNVtPDGAGL1^@-CDP*2H=W99O*=#=)^`l?cU$QEm8j9jx^>8$lX+|=$=wxlRxw|Gie zt?}b)wY z-`g|F50$2!$-##bD*+=(9&48jV7y$PK?R_)m+u0x21X;Z$306a9B2P7b>{Q%>J6P3 z$uoM)ByAsN#d01hb7_WgAb?~!f$F>@VWsN4*gUip%!vW6^Pgtk9aNR957Etg6n9Mx7U};br zL}RSc`@XCIxhq&MzYqGXIftHZh1OOJDxJ^q-rbMtG=Y->55L(v&-^3BZ&lURKfJee zMnA zA8kCGsF40irh|?Xrol{0xcU;WsrQ@cC$c6U>2mLzF4;BY8iIxZ`$a=rA`L^XW>w$Cy9SK{2Iv z0WtnPm@1fyXEK!vP*Y)?XUxiIt)Oxg{1+S)ko(SWb7i#8BXso+)3ryRdQCa6pq6C! z%bAs(KFQ%KIlJ}mPhY1sI;4XShWl`r`|P1^LoRtLITm~Ef1JNdRF(!?s=aF-RUVbO z2`G{~ds}q$Kz@j%e*97{cv5j**!f4tRfg6Ut`V{r2*m^lbg^+(2RlRpSSSXtRPaSp zywi-ajSuCk))sRyNPcqiI}I@5BBKTkue(m$(jBa#ZYg%WVK6vhnWxxE?4W;-@!KlP`oa}*ABIi$YI zG@X4r8^yYpgdI8cNw8gf9~+X&@{NFpnHYuhdSHha+$Gr#KQvD~(gIP4X@;s$el(#P zRY@tx6BK#w_Fk=Q@b5Bi*{FSehrwsXRFbTrIG4bq^=O6aMlsKr`b?W5^yk!ed}U+hv@-}jZ^fgVnoDD25s;|HM?J9`#4+= zH)xYWkp;p*iz*RL7N&M1*C1R@b?nwko@Me_#vi7D2{?sW>K`V~n4?w(=cc74y)Td; zox6Mx5_Wh>!t*%maU4_3wv?JLrUXi2VB9sc!ROK@XU}ohx@u{;bY73^+aM2}05RxV zc*jE=pHnde@vIx!P#avO$MZL+sgOsfN40^%2Sc&0iFcmSd~cA#Q#atAbW zI&@Qfop(4B&$WqsBK3WbuUgg>xv_<0`yheg)&+F7_NEOn#PJmy)Su z04e)X81Z&`hda_r(Kl0gnX+VrLKl1UTZ;{xa_|_`0`g8u-8hBo;RW$nt;D#ruZT&@ z0sZpEdjr3gg026inN|uXa{0j0B zFo}NTAgM%;#jHeJNuN}iCvLNlmf)60X#~>g&qZrA1ou@dig1_i>GKo8?)zlX{As7` zTnpR7F(@S)jz0%}mAo^h9XM0`eA`uG!YBP&O-P!)I0=vn$>#ehE_Ycc>zE0l?8owa zv7~Kj39%UA-Hj5TU)+E2pWX2)H6ltM?hPigqLi?*!ZWl|1yXXjM4NdyWsya^dmA%! zy7*!CgGp<|1k4I1dgudl5!WrLG4cbCi{=C!=7#i%uaA5RM8g{?5}zbx-~Sy5bx=*P zIR!wk4!ucTd5Ase@5>U;q~z;dDE2iEqvbb(TevaWR$CE)L07Kz+aG#Ev3w8De^fCW zn7{(s6iVU~+*E24-cSzC{Wq`y>|||~G25>OtSO?KCBG!FcaeF_8mN$(sncF32dXc^ zQ@-yOLd?*93M45xZ@C#d+_AT{fBYtKqId09gNn(HB;Y((SzCGNxJx)-3^X#Tybhx^ z@PNAVco{p7wB_u_74aO!AOV5)I81S>TatkeKJd}y7?3&_r>zFR#jJ~m4v|^Nk+G5K zU;W+>TY@8z8{szfvZmns@y&lb>b6uWgAg2IW*%{6)(3rkhhdB5aU9yK5NsxC0#D4Z zubrKBv){bc7$QvJGfNRV35Gkx zTXi^1ss${{0~vD^TQ&*GJ7%1ff$ddRGhVXhHRJ)rXlFYovs?hV$t+--XneW=QSFnBY*(^~+>Uz+OwjBpsVz`ZrIAN&J2y9^}9Al|Nn# z3>CU!?tV0_T*6x^og|h`_y-xN>mmlQ!GlvUD~lY0xh%X&2%QkQ-#wQbeGE-q{7tD! z51mirl=w~n5##L(b6Dfx_%(GC1tlTeV12a@g+JpB&BrNig5U8IZ^bkIAX@Hz5O^bJ z`Ui1hb-d_7Ab!N54&9*0XDG2t-6jdy?Iff>l*aF;l*VzUNmogT<50sD+l+A3AFHs_ zUB@Sk!EX#(RRB%2N??s;tf`LlD25|j7pM;zx>5;$Ycg4PizMN46TyF!uf8U;y+YY{rq zk4xYPQ=U>6B@4p@|3{C>ZnaPdWeiYzk?ex*V6QNVf!R&1NOhH3x5Me=IE#JCvb{Cp4qU23=CU-$*^5+ z<}P#*4`PF^nKnP!=q?`NG`K#Ijf=6}{1Lvan4P9_+Yf)JfDOE~=;7xSla)2uHA8Sxt3iSq#>~DAGJ|}QIUQQx$iQpi z7H1XCp=k;Et!ipM@Zn18t9)0>50Woa&hX?XFu}U?s33EwUmYdVYubFSyTA-rGHqe$ zOZR0GdWVA><=}6K`NmJY13!hYsL4D`;hRPPAK0T@`qeqUfrvZ%8y z^JOAV6m-N3i*azc%8O$Ia|9%RG&GogNg^X0r+F|Q$1bc37#aZqr3}PZn62Fpxn{r7 zXl&qW(*Nkj`SC7*qmpKH=Y+Y6D^AtWbQ8 zOL3UBWm+9V zEMf?5B1BT+C==joK4NeT!H8gyJYN{&XLqSV%9CC+XKpPpC(N2_q~%+%FfHS`DHV(P zFFj?daZ=>+1U9q8spiQL1LFuN*GjMqdZI7iwkADgBItGX!(WQ*b)7UW+U>|uNSaNP zqXTnP3a1|==Kc}*_^sk$AXd-p%Wpb$khbuRU68g3)5ZZPGidU8Z}=aglGL=pGY8l> zSPUqOkd8Zziew29ahXYf1+z)r-F@tcmYzNVhhYvcwXY2?^({|W#E@t3-uiWCj^2p5 z#UtbGPoXgdvu$)Nw1BBnVZ3!r5>EUKrx2zwX1x?_f9PK1BX^2?kaTW=hH7s6sEBqu z9eGfJ5}%J;$a{z+5OeabeejFhOTIFSrp&+4B zultIDvW6(Xv96S(NoJe{1|yNd(t6iA9@E;MrEOeP(UtNafW_R7w4c8s`XbhRqO{;sga_^XIac z0J{bedqQk6$W?$)P$B~Cy5?aalsu$VTaqPegl2Fww{b_GSN?RrV9)XyLg}Ea`m>Er zi%CPAj@>AI=KpB{B1OnBa0ZARai(6{OkvyG7tDQp<1sBvWFaYwEGx9?MT4v*Tzjkj z%KlX+zcVUKr)V^Ppl2kZE_nF1Pq2}0oJK)?Xm`UecdPGZ`u5~2Sq9aCN-}0SQK$pN zmdnZ=&z629E?I_?6`S35s)FTDT2@Jg*_SnO9h{Iaod$<*nChNS8MGuh!_W4J<1J&K z?ZvUsYRaK1XN0br&do`Z=f@Lyh;`D>qjQOx< z{at<~Cq7wm2FGq!d%4o|bQX`|fXk-Gi-$^q%UNn};2#P$J9C1tN(_b(W`uLi*a6p|?%VH!-zm;EG|o~ou? zEW}cCavMpxmO3T$z7ddpJ=4qn;bK$IaU5Z6fs!^5LS*Ofw)WiV#gM<+WblM&b^)Q` zdyDX-cgcu=*?cFGjbv``G~G?4$K z^U~1yVSlfoI3-H(pJU-uRe=LRU@pI_7ay)eq&msL7Ndz~0*Tq)OW8NH82?RnGrkjA z83QxQ=$w>B0unP$4AtWW811-+@1irwy&1=)j&$g9_~=tzv` zlB60sh-0Ek2)&wRu|-NM=Xv=dM2z`XpQDD>P6A}>A`ygoYyBkmML&s`tZT{DGQD-$B0G%m#-Px&O-~~okASe6(mPJi)a%@UtJeD;ti3Gq?27-YaVOgF@lguH<9m;P?eaw_)meo}^*9Q`2O`F{?)hKof zJ)O*al?*2c75KSMpm*Lh)}8u}uUO#AV(!=7G^Vol31qYMGFJM`(R@>?TV-ubf&Y$I zekBPrB_AcWA5{!dMu9z+G!T@mnmfG;)Lgu=UTF@9n=V?9*&DpHR(6~k^xy)M*|*5j zm7mDnQO^qjr{lpidsJEYLJg&GxT=`G#r9QXT#Ga!R>5Zc42l5Rzy?Ntazzlyx=_I* z^bBItNd2KhVAsn1O1C3#J@5D#<(S$~-u;rk=p09b!i~BZvy35yUHPpRWh8*b*oqnt znS^{sYcOC{x(qQXChcmhe1ZmM%^%UG4F@0KoVj$uia!tmnZT;Fly$colNf<+VxlJ&eVx z`^WY2I|c0&mjIJAucT?iZ3bu(UF!W+I?J_}G|4Hcfr#sSqJ>sZG9-of5`O=2Y^s6> zi8C2XVn4nVK2TKQN1A}An3qz%%xCx9h&pP@E35T@9z14}{}zH>c)>%`N~yw50km{? z`)$f0M@*?TUAwzYjr=pP5O{P|xCBq-)*`X_wrF!=S!n(Z_=>5XX3=wW*663p(qun)k0fJY}-Wcq7uw=;RPn_CGV^sAPz0P+2eh zdUF{`+U#bf$XkEq_h66|a$P$vbRKP9_){QZ@=G69Y*B6PgYp8=3og6g{+J$^VD#rj zl?*@LhYFoVy=_YqX#^ClyO_+|}q2ISxd(S2g{39#~;f8{BCb zc5dfAgux2`(==Kfa#XZz*Mn&Oq!rk_zEM{8w*xsV^CKj`Ac^TR!iKP4U@C^ook8^} zJvEJk+J|Arn5y)QDyL@`A;BQRNpWG7FE>D0B)IwC^)>L7X#vgwyYI5J-m~yLN zpkd0l?@zD*AsM5v^b3+h8AYHiI;oZQHwsM*b-g6mp=4bYOJd#2ZMSvux}mjY_NmxU2G<`q zey62%`G48rMp`vH#PFj=1t?O4Dki8!$C!ZHotpXuF?Cj6sv(AuSP_x6~&gM@F52MsZjMk;r>cs0&QaOIP z^LK`$oDQ>np7e@}MS$d9|5bDxmXavYvJq(QUMFvNiQ;=N;<&?DgRz!uMrsovm`gK2 zvXReHC*BKvh?QP{x5ZizkRS2E`kPdoyUcwWkp9SuS-aVJMD{0#3|kj}$Un)APxozVIj%yFXa0v$v=~tp)`Wu9FkFX z+$6+c3QGflq%5Uqxc|Sl`hzJe73)TkDd^fmD8C}lf^Tl+uXT}qEQjs9<{RRRIAMmx<#AL{ z0xgL{1kB}Eo(yq02`c#k3X%mvWw~;T^K*P6I4#EN@~r)mWW@=kDYVPs&!0z&P$g~V zP7rpN%5!V3;gWY4wXRI~C7?>mwd$1%7qjVcVPR-AHE}KWQ_0}KtP1PuH z|01Sj>P8W}m^nqSh2XDgMs=_)mO4ykb16u;?CPh0K_lu8i1)ucAR}!|CQ4a8=v(`83*v?#OUzs_LV|Oyb z|NQd^dA;3I81iPVqt3A+6(^RR?y7q1V1zMUXEDr};72r`1LxNH_(q2#c}YqV*W0yD zH1+5Qhd)Gjzd!Vztv9N?##FIQQ$994l5EtqDQuxbM8uz+iM)fh|L>J4ifX4Q+ zZ&NY;GWYNIk`!$aCDCM>(TEGia%M)bmEY%cpTABN(SM~yj)IIGFfc;zq~uhg>+O{y zO%xIa=EfQ@b|UEy<&I?f!ugr`!dI^9fGVCPJ4w%&KW2&< zg+mgnAy*z9srEs{_o0_%(!ndmtN|4krKNCX$>whZOQu|PTd!VMy2!Bq|Bn(KN4pA{ zfK)JXg7*`Ipy_E{0j#m%w z-B}Iq#VJr71p(sNY{RTUxqcjr?D-J@kpwY?L9h*27HqI))rddtu{LJ&Bc1!-dO)*R zkmuIGF{Bt}x83mElh8 zsN4iW?QQYlPUU$@IM+mEHI6;ByRBw_NZ`->R(B)9 zA4l&aJB`E%2JgP<*~lU0)OQ+2gN|ll`jRhVStuOIS+dFN(HE03DCw(953Jtk-o^hr z5oMvscgXFDD{fR=xr=SC z<>dl4NxyIe>;K4BsnHo|DQDmC=|G8% z{tr!89Trvlv|kaVluoH-=}x6Pm+q2~7U^z~uBBN*N?<8zr5jvYK^74trKG!A;ydr_ z@4GJGj|KLeea<{H_uO;OJU~YT@U@}X52NtGBpZnB)IM|7R=L?3eKe)(a`-vus1*2eIi=Jv5&eHTelRhqYTe$v3aS(Y*IW4QNNPYJA<7?vN-aCF z%?#I8fLSQZ(z{0uIYI&yZr~@_QaGT#@0L-X(scT&a1tWZ+AH=kVfu?Yh6wG|xHol; zOQpY^6Kp7K$OC$A2_YmMyE_p{i15yvLMgBK``icCvx==2w5Zt47_`fZ`Dg##KQuL+ zF-Z|(m5QpLkSjAO#@UZ~-=9Vxi*#%)f#MgmOv+em%~lC9qyn0B42TKWxqoT>qJ7HX zCyrb-x9bJ+h@Q*77*>lbm5Jy9qG5monPTH5qgg|*K4M~a} z%v%;Sbo9oOej%pzggk1LRYR=}!sXgX$PlsB+<)z`(t*>=ni{p{F&XH8_@%3^p~=@& zYJa2Fh&4G7K&STum*dD5N#QRgVXV$Q9o-rhb<+W~@5eUYxY$K&@NhLC*kLM|cR&BP zjbfyiD3ZY(XdK7lMB||;@}>3B5{iD3E9Xn9Kmh9Qk>Zc9u+FCY{E^Z+1%QTDg#PE- za^^*vKn;kKeGapaC+JNMPN_waDR-U+$3%=roYE$H(Ldo{viuyIw>$}FglWo*M3}{K zy85_*Ep4x*?%#&{L2K8`Ul&pk%Aa6csg3~$_io^D441=`I2D-4*5|7)0|_SxFIT<< zyW9)YC+*Tw&08Y~b&_KxPs(uaP70;e$EMR8+h|5>_w(})Dn4|Kf=1_Ce0?h!TE0yT z!e{8m`Z8a%)6XRz)k@`uwfhP-skJwO3U7?*t-abJJp|8o!< z#c*|vfBz5Z=&~lzdsBbZ*Gjwobo6_N0~C(9H%=+RECSq%pM=Tk%aEYW||l1E*}lyJ~`6U{p{T85#Lj z8-8B#!-`pXr4eg=p&Xj>W3C>EBW7B-x{QsVduH5_H=L$sudT5SsXpE3` z=Qdv#h48hzL(E5yxW2xAN=tA`8>y~7_#IQW2fz4llKkBzUT za7@Aw!{gaDk9vlG(0tYCSruK#yQMpIjJesl;8e)s`2oOc;2086VOJ&~IwTjSpdbPl3ITO1b~o zx?zn!iu@FYum7|BsYPTR)v|p%79tMQh$yjxg%DyLJzIl?NDem`p}qmGwoQ zR1v@A_kJfGwqG7ulN_^5_6oHLGDm-TXMZAGTJBnOocwZS`5q@mc<`&Ns}y+wF>MFU zaUkBj)fr(WPoAQP(!=>HoU;TvahY-iINxgwfp9bNOO!7Fucf6B z8pVW6%|1=nugySmvvIu}3ITlpRb7?H35K%owVou*_`g6z)enuTn>SHSBC+R(5}Dgu zgAIi->ke_&!Y8`QWu@*f&|qw)VFcFU=ZaoKhF!mGM1W#;JE&f3eaWfG$Z2tTVf_sD z>cHjieNynFkq>=uuMjT|sE;PZqL{t<@ZW7Jl5c+uXiw${_hq6rGp+>>o-9c&m-^}0 z6(>F>x?S`Q=DKS0HHDiV)aV&s&2T0ITKW%fr@wHsWmKXR)=d#hOB@zMSmqFe7fyS7 zi%A(7xvTwm$__H(GD!p2tPADHNm=WsH=6}XO)x@0|YMQY@;8rFHvlMgcy;-koxpK> z(em3eofVzS-gGCwTVy}l^8eck?FqAl{I@3UM8wE$39g4JRG^c*E*Gypp#ppv#$3PN zj;N7*b22*22teU1!L~+P+ih)ROS^#>kJOv=LHx-m?@5)rBt?KWyBY|jza;JnolFg1 z>5$Q8+Uu4_OVQby4BrdCO$$HkLa7$1h3oZKSk6O4iRpQQf$6H+Tx6tuD#vAGm@Efk8v4{%Bk2}|vjm7Gx6bMY68pQCSHXy}zK>P7Bc zEOvC_&>Ju!)>0Au`$nY%4=zojUarCEKZP#V3tfmYF2>AEfDm&f+*2?==R8+AW86Ma z;}`2bWGbn-k!{ZNH|UpqkGt-$!|A2!p)1##6OXCWHBYAAX^LkUciet+TuPjF9k4py5~1wUU2 z!|V%pE_7FtNi59i9ioyrGTPkm%RCuQ@F z`8rBoQc4M7A#08>np(YBObq<7ZCWC`eTbel5nNlCP-2D6@l;mOTa-paSB84g*rMF3 zNE#P7_oT_olwjb1z=%aNZ?;8B{QiFMi7In!Esl**`Da(}FW0|}B`<%CK5a5V4bjto zbXo2g8tQF0H*G-DLsoacj%-b5iRHhmQy0)l_E5 zh(qh!@H9;;M8z#jL9YA#KYlGdydJ=Hh?+*zmvpB@#qnC|V=3S^`N+{n9rxRbs2Mx7 zWTKL2XvgB%2wz=2y~be699$T2V;Yd%CLP^)AkUC{k~P6}A0C-;GWIh89P`iM1E?l^ z(JWJuKyj;U4pwJxZ|4_OzbDQdp9e2XdDm|ZDgo8&WzvfP?}Xl*&z2?Pun``-DuO7j zQ(n*f50?*;oFI7%KXod$(ObK#o*ZLim6N2BVh=tpRb)~(P4&XC5QY<&TwaK6-PZ_* z*{($>+BF!eMW->>-@r7k9^G5@-z8hN8N5pK7FK4*Ek4E>t3E;nBsCx2{nPY3uz?l+ z0aWc*+MmzMw^7!xAnO;)32)M~D$4F^m=3%LdX#e-S6_d>rrtPt5c|e?YXa=4${vJ0 z2kHBq9LumMy>zkhtn@}=#p{$_c2b;14(kBz*43@0I}~R^eW-XhO3>b3?wYX}_cA|| zPr)g<(Qsl48_j%@U^6W3s@TgT-^g_D7v82XJF~a#UkCh@j!?s1Vf@WuI>#sOvM%Gp z`ucW*jtp>ZFAKmwC>h7y1;{E*gJM;^Xjr-IC`u6MnMG?65UC*7iw>NqTaVV96TCiu zRm*2|hIa1uKL;ESi$PbCICFHx8k?NnqPTkR*?dZ;7TAh493Qsmfx-sMx<<7Q>&M#$ z*|bzyczyp}7iW*xH3P6~9zTE=oCSp;1K{m-$lUA#Jes7rga?O?K#4&0>S#lMfWvMk zgfrO5co6-twr=Wnf!ce{S&|v?b5*uAz$zm}$36Oop{jul!e7Gg&sU@FiN9&FqG;MD zQ5Ir`th^bit6PUuoja^@3vh7^(ZP^o(dp3Zr#uJe@0seMUjg#}!=tjg3{^HWqnC=p zPv90fbW)W{6L(jK`NgIOd?A=+4X0VGS@WeiQB0#7fw%=)c*_nw@z2G|*|sv;tz+75 zo!K)(lSOOLNI81cuKPo`Sj?vJm4%z;obP{6KY4{J&0+qK^5cX}beny@3`s(M5!28g zxV!Qes%-4Ag1R{14!sy49TBmV61DRCz8R5rRe{I-*FCW@SE>d5uI4o4sjHo?9nI}@ z>E9;wfA+b+<13^8v?e5|XW@w0DB`_Ms-OvpkaHgw!x(_zE0URv@!zi0#eaxb_5MX+ zsfZQwXZ}ExZzaTfiUDISR3TMWTX5c4RGaYoa7>UAMr1y*APON)omE@Phes0Y zXcA~C%NF78hE0`=q(-ezg)`RffXjI=FdHJ*Ng@r#wkwb)z8D^H#QKC12D4;~ChCWL zDY~;gIQ-;!yE*AB4i4_AEppFsH}~jf{v!2Z=93!?Fo&qd^4Q_(`PWEzttkM`a{yII zYNO!pn9^S5CqCJ=@6zZm@|W14$1)U4D z8TmRToKm!o6>J&xgKRtz-h!JvrUaFE{&$f@cx;ciRq1{LL$A8OdnF(CIxim=td9dHM(Q_p__jIXKv>8Z4lm7Q z7!^v(lo>OYjW?3GGRJHsF&V3m&VP>Mu~fOaTtU~aQQ>Z1F18Nisr2>m+S7{u(kpyM zpbs|s{r32+*0*8T*k_70fRK^;vui?);2UaHwb$xGa-{!K$yID)-%ZfQVlg~+mIf7D zgv}-!#7WAOF4Q(W#O-2}ID?r6nUN$%PcuhF}zZM`so@DT!1_?=PDU&0- zl!|`7bLv|w|GnMufVt7JzHQBvyd;bU$u7Xdx|_=X9QBwU zolCTi!>-=8o5ki-p=2=KVZg9c;|q~xE7W{QjY5#wkY+V(o-PJUD+0=$Li!3E4V@|` zroDB>W-n^3l}Oly^ZtRysLBxbxx2FCc?a@?iU7Q=+kG?X-1ff!&c|hACFlZ3HqFPG z929)$GCD&!%l6C^ex6&OJZIT4Gu&l;$)5<^z@Ott`-hWv22;u?3|ltN5+2^?!iP2Y zn+X*<4Z~@>0FJ0}b#ha|1s+N)8`ei%7LEGfvOT0a1_XL3Dk-|!ajaPEFiTPHSacXi z0f2*QLfn)X7WDv;CSVeSjeI);`HXDml$4Lfv5XpyMuCE>yK&P|RP2zXqC5}K_EZZz zpK-gZSCr?XT-)bG|6vH-c!X_w$ZPza{M1IUuj*^c)C`*i{9??KcdEeUso&S=8JWt6 zySKm?a=qZ1LwF5lm3r}e@58S?7Dz6VYEW!M?1i7T2Vc6t5&eAb=CwhhPRY|by5;R` zJRwO1c>@38_-w7>!5Kwn4S-cMOvzB0WprRV=Qav+vu%qPTBBns)>IV7ORx=g=v6q3 zpuQ6bv!xaTtcS-zD(}h37i}F=2uGs$%N<`+=vv=)I?Hy^TNoKRnJ}fdW_xLhM|dmp zSfXVHz}?$P|GbSMRXqD%O4=eypQg!b<4deX@LJuz`un2PIFKKLpV77M^RC0IcAc6J9J+0{$1ib+&(;aT6ueZX=7&PE?zBdI1o5tthUwk-@6_<)@= zyJ@w8#;}{+3ly2qe}e3%rgVT<_C_91GG0BcM33`EElFgmw-eFJ!$u3!43SCtk0)KD z@JV&r!$B8ecUB7og=S&sBGIV>29XGGswRQMm9zuu&&5SiPZ)t4^IwWD8~g<9iKi0Z zdBZq}mTcmZq)-iHI~1DYuM+4x!8N6Z~$=Mq3O`uN{U@C(TsF==~!N;1R@{C z)`O*!$FJf~2<4%S><9-UOu6LD<9{EUCbRfy7M^t#BQ$xNc3&M(K=X$bm!_J!9KBI zUqJ#7+&I>j*1Je3k=3hrcPoxTA21V?t~3hPgRSO8>Jo5H-j1mnIRqrgr)^uiQNgn= z-Wc&j>6)D@`NGfR1T`hS%HsH=!gYA9Z9SmE!wd6~u01xq!K(s~R73 zeHj6po2C_o>O6BIp`?`*<1;$O98fkHjXTC9{LWxKqp3qMS6#!f%{sW_JYXU|(xjl4 zq-#=6%fmp(D+(6!O#4rHHE&r(%kl8q9qJR$YLVP-A(SS_a|MDH~VH_X~f(;S%D) zm^}ZTBM?L<=~XHnTW`?u%{l~g`~CaWX>8J#A2fk|S5`yBZ~R>!J}*iBcV*tEgXA)R zXWc4G)9!A8?CkK?`6}i=r)uvlKAJO4QudW_Ff&pl10p~^)CNNqpV(+H3^-Az7l}is z%)baj^Cf4jOd0KOPB8|0yX-C3wKfQ8hk^}dzUz+Gn(4bsXKYKlQ=6R=te^TXh4w)l zQW@KLFJhgKDRsVKpp6gW{H1Rcdx(mlCo~)01Y`waJcQb}gl|}iBiL14ZHAcg z74v&f>PEhoE&~HCHuhal2eI|7ehC-x`2bsuZf&ba2Y?uH|8@`%O(rO>4>-pV_8CKL z!gMMT;STPWVG87znj#wq%0@+BaDoj|EbrqZOgix!X z24BP^CoViOG&s3GL1n{G+oil(SdXGeNL_q&4GA=WH2JqA$1s=plZR;Zh@+DzvbZ1b zvw*=Ppf5*8|6TU!+IQ-5zJB*hi)Wwz*wUqV8`Y%7O73-rtZ7th7sOaz0Xs+%xwJ@e zA%+WKs~&Rd4iE)+bFYQ;p;q(#1GAfenn4Fs9LI1x}S`4 zX~Z-l>n1HXk`HCxTytP4J|Zlmi&#Clx#?9|`nwIvVykgDEs)j8mA#9>W`aK(pSL#v zj(YQ73_9db8*K?JmNJn%WGy(NA23ItNZ5qyKEhGW8VkdyOa@<^e^WP?At+YeAd&3g zz((^j?D9ap3|Iigrv|FGG9{hxq~d zO2qhLKi~X^yZNFP)J!_9uEl0e?1`&SE7|S4Mi_*tUM~97Y z{M|4S^r5Gk>&tga2(#JILCj}z^?23WVD;CDJNe0tYijEhMV;v${}MQ1`PYqJiZqVv zqCw*OBA^!=CAQnj1`f*^%Q2+@{0*j7o8mjDk`!big;;$Egrh@KXPt1>oMo%U% z)t4nS9(Pt_RmV{=kT`g9l}1$4ff{P*PTl**V*CjrNz=5I6~0Z3BLURDY|ajQ;~AJs zti#@v7El4|^E%_j(|9W~!o^|#KM$ig0>H!B9(QMoh(Yw9Uhe$czmncIKi6IXDsBPc zUvpT)HlWXF_08F4Y`&h%e(MKP;_>pckO4FZMr!1zv+FhypiI7@-s0=ymshXHsncb? zS-^Q_JHS+?wPfz0Na)>>M$)S^sfqs=K}5Uztv_2v%y1BocR|cLM|ht^0e~0`q3#r1 z<#3l+3BqF%=6||AZT(ohiwVLlCV>NxCspJ5Zrg`pC;!pk^`9feo zA%(CeOHi1rnGs1;NSh14`itd&Pbpa#=;2*s*Kp2|^2sESWOvDH?3wZHh{|#z5G3l$ z&>@eQ_}9dbk z>OfmbPj4pwfL9}X5+RMQfUH<(ATvrUR#Bm@N$8AM?~X$R&~IoBm10&cpZyDlryma#XmWA6 zpQtGPL!Yf(V->R|d4vb97jbt|&Z6qL?Yu;{h9@uSDE!@R>z9LW*%#8OE%bqZkPzGA3am zK4>EtG79n&%uYU%Yqp7ZDwUg<4PI`gIxDL_v0O9{Q=fePMjvZoba^WIv%@r>0tTS2 z>w!E2N_XQQZmWmd&>?qK${wRgw&>#@!P&AgacUfGV4yk%a!$Bp1a zptWOy?a%E}rgWpfODD&;h5q1T+xC8LBA}`{OvqwF5|HXmpCpZUv*p4zJ_3_Nh!ebD zt3N^xWguEZhSlGu2By`w{c^n-WiU99b^?++e*YT4`EKeevcWdfkv=tYWQ}s>D-Bmp z|9NanXkkoMf}y(kv!@TT!g$v!5r{xY#->Pf} z8;mU^XLQR_imgw@4GhRJRit#vCX?zI|GnYCBUUop!%qGAR*$OY)QdlDe8RU;?M^}8 zv)=%|7t;Z4ceqPV3AAk%-UuTWP@;<~`2w#~w%bdm&1L*H7|!@`!{!>w#vP2|U&?{w z;^KPx@g3$Pl^^M>i;z ze*QhJIpuX>QrjT}YN}%Kvvu3BET{KhUk5ceAMahZH{Kj{=cyVeKiTwXTS&j(7byy+X(0uRY1=Eq$t7F*@($23{#t%iR7&-TvN7 z$(F;b{C^%6m;74WDbAM3(K0EU|WON&)kBJ(t~MFlxLcaID=TTChq)x1j|VG%)M<6 zxo1VukqzjqNR4cGNaeMo-rd&;6$Bs283*AFo*n1*++l8t=8sqD)n{;!lF_8Fb|1YP zu1I(jrWJP9`x2IuQ!BjcymL-VFCZo~gr?B9_WV;g8-j@Tb63+esTKiWY2>tud#$r( zB%9~y>Eda0P+voRPjp^#0o^!@!QjSeV@Jmr|1;G4oP68Ksm#;CwRn$2Wdr-^Q$x@n zJ=qm%A0OY2%N3tI;)}{lxg|>}VdjDeNrM)LVb8w(j>IRfF`MrklTQcQoSmJI?Juv> zl&cdHpDq0i9cOR7n)4FntO0#F^It{PH??iw3%wqQ8Eo`8UD;XHT>R*KOH2qM-#Dbn z6atLCZV7lHRQ^n{P0~;o{@s?)%0ZmwlzCm`TI@1;{qSNJ2%D{Cp}4@GNn4qvdfm^* zZdtTb$tI25#l^Oc_UASl_v5nz962*Bu~E9F2^t5k)Rqh52pv{x^@Jw?z>tc6o?PRS zqXJ(3>E=rvoZ^Bypte(Yp>{M`A#P9p~ia9-In+6JXlyGn~20kH6kP(hZ9P8y3 zd-hS-KB(t*T7%vS)Nl(Yk4w=G+ZcQ9RLB2hv+UAw4y%$)96$3F*!}3%?I1;^6`xL6 zXisUPigCLxq)&3e^UL)gg}F+Sh09J+8ZtEkN77X2)prD}@HY~jY)k;+v=X?>(s1;` zCRdVn2A}Tc?%TY|=RuONw{ekd==U8ua1zN{m7N`dmbX+qBcq<|bw5!?!kfxf0SvHz zJ!tBMJP~3_aZ)-UuLNhv8+4MHbn>g4}l|HkN&)LK=pEuBlPOfk@>;y z1>i}T4$?Ktl(3>8TYV~NFNHAKe%n~^3@zMI1k^wAu9ib;K1|~pr=}q($%IyaX{I@u zo^jwft4$3*t+6|Or2hK~ov)=sI#W7aT9*D8y-f~m3$g(@3SvjyIe5e?J$Z3W%n+(a z`{FKD5~3i3yb9;w&Qb8Ft?dj$|1xxO@lq9Jxa6JZiPV;mC@+H#xh>tra#$!|TW8!i z`*kM(_4r8d)7^O-rOcB!O z2}BwD-UqAGCc9K8C?|_tnoY`Q-C>5Z-a@DV0+=`%yLyDhA<|8PA!<3jp2#vyjY4rj zWcz)hSk&~y@n^C}$r6RJ;a?jVtlvX8%zY@ouReCI?WG9>`%? zPfih2Ut{I5RTY;5Em0i0Oawad`j0<@PX@3BiTu}8*u=?D@fwv3e?W(J^ges(%%Xt3;iSYF`Y66pa-E;#$$u+dK{fe7%H;{)=LPdU=Yo1#{ znT1^{RUb+8Eilp_~cip0ICgG--nox6(8df_l(&Y?0>x{;7-MP@{K1@X)0+Z43*&&EGN;c z_2hX0;+E(?f)l+wy=z`pI~M5EljzGt%u9B+8=xiusFoJ_B8iQ;o;ka^PP#&^S0&Ox z>PHMy{VK&KPfo}nrhs8D+rzVNY6kG7+D*`B5@WD5_VfmWL7H-jQ<$&86ZIrB$adg!+sE3h(iKVCnm3VruLRkXlx{e83dsu0%&N@tf9lOKS~EnZe{*MbGfYbZ8z0aYlk^=U zc?Sh;3*9A*lA#TlRdm17r=`4B5hi5B?|^EpHKfR>T4r#om0H0|Od;`4+ofEj^qh3S zzy;7~5-d{;dLh_T-&^7YOnJT4BONE62aQA$3boLwZV(b_U-kBMNa%Q2s1oV zJPK06MA3}N!pd}Nz;Rq=$J%vb@+9$Ri-%1~eyhg^r#f*;NxdBFr@1y1>FVptvH2QD z-q$RHM=mm~+u8XSS|XeTljZB(z66so$K|VErUybl<|+f{w@s7MaOv?D`&Bg;xsvT> zuMV{8Kbs7V3RWiC`{-qdp)-8o;tLo0P-^e^W*AM4MZac{+ zosm4m|L$2GBINt5->>!bb2i#F0wBM1LWXS7QH8;#>7=c9dUouW*9by}M1o-kJ_VFv z-UK;z7zc_!-@JV88kQ%cqBH>GQ3`^T6h|>BRU(ij>(w^o7eLX*>*RuXv&^HT3m{U4RF% zP*93)(DT59NX&jdIJ@N~T7Evu&Ubq(v|l?nH@B0txAKx!A}?nff_E*U&bSR~iOq*g zQ+H{-e;LpI+82P)oXAF)l#fzc$VqaEm9nn&+=_md{zFCq3{AV4JJ?t`dgXR;oQp;V z(QxMHVmLjnaoC@9(CxnHc5v_iRt2{>ej}A-7VVdvYMN<4(tE5^HjyrU_QNATaaUM) zy9Z-!WiIn>8#gj^a5Fp2O#li3(Zdez7sKFFL#cMfU0!Jur|;WkKkodq7I^S*yU335 zYpArsy6AKVK*{MYqpB_)2SLFQ(T;H?sQ5($PRG~tGN94V93Tl;gE>DJHcb{co{sEb zSt9+P>4VNDBVN4S&SI`Fn8+54p*Gb0e=R^FSOdpyyGLrbhd)VR@w%b5*F$lxb)DLF zONWA1A30u03oirYM9JS0&iW*a4VH9UsZ@zH*tI};tC5-UoTR#+KaCa?^dv2Qi>F>| z%wp*9vXwhY({}OrYBos#RyN%AY_T`~6DxH29+7xixfO9Xg~(z4cA_ysabFXzp0{_JJjc9CYWR{q8M&vIKvx8>!ZNW+c$u8XX=t(a3?prwmKVwOEJU3D&DK`oJtW(|flG#$D@%it1uyih@;$^SExjC3O4CZ4F< z-q=L>!?F}(&BN$0b(`q{)jBkZErmif^UsBKx$in{t5{^ilum1xcJw0~ z$tOjgZwFJgwWX_n^QjY>3A<7gJAkp6Zv$(MpwzbO=sE~tx{;6uDXiSItRx-Ep{k4h z10O?2sI>)oR!IKX@!Giv_#E!Q0&|gkpN7=O*MG7KsKpc4hq)HUKWP>m{uxyCZIYo0 z{NZJwgfzjhhZAq=`il0snSHN%AtrPgwRdOeMDci25h+Y1S_${Nvy#7@ZieTR7;2n% zrZQDlD=|)b>t7@~OK$B?9agss5BE4Iq{y_Ix#_-=+&X#wgD=r~VvKLC*+HLs5FMyW zd5CD-8IF!&OGWzGc0FbG8~k^%)&PW+sP?*)tBI4LD(BEzHbY7SE{n809dMiN9#-xl76BoT0>Q}zW7LH?tfcL{r6m-8(1v6-F3ED(*sc2#FFOkE#S5Nyy z$Z$HJ#Tl)BI#(>|Ao`H>TVY==u!_WtQfnZQxydcbg(a(O)JS;6L>>xhA3EL0q$NiA zc6=1RBhl76L=sKLIF>Cl?731|M}9&p&i&di;z6W7NO$MC3VV{E&~5jAC;mCghHdN2 zB^Ao|d@#dJLRRfunK71@N2GM&yt^A5n!chwu&fZ)F3|6*Ie250s&na>)~!<@d!pK2 zn-odXF14HINM;!DWCDLBvDfm}ESi+nF#5cmdpERW3~a$;_gfE2`0s*2P*EugVcrZ1 zSP})L0jZ{lnM{eK=sL`pW=Pbc8k{!MXbcH+Ld~>St_OFdcY9^ zDRa&m3rFnUo}NLu91e|ds4wNvFqTxJ!m!w|s0^Rb`@@<|KB&+hnDzr_kDWnDW#@j_ zW)N0_VQ)p3Vt;6{6*G)kRPw}^VKqoc=A-i5s+-T0R)S3U`G1jwmBsP)Xa$E@(wjzU zk2kP_DJYWnQSHVrJV*D4MT9$`Yu0n%#CiiHBOPf zz7Be;nPaI&pzS&?$y^dRL-U9f=JoLG5|_{{S&V4+Ih@2L>ki)4;J>!A1E!78_+Fkj zk21RxCd~U%GdDQDemK)8BsDZb`U-1^_fy=RH*Xu`U9rq_=7co`2cFi?7u@P;pyv=s z2%UtHT#uaxl(F(th}s|~tX0Zr@P65X4yiXX2YwbkiGjpmu)D_zFyX|jEIC0tv2pbL z+@Vrnkynecd3t(k%3B+3#OM)A9ahY-y&Y2)i39Gkg9mlJsyN6j9rL5sk!82@qDzrj zIBx8iH|wjX?=lV%RZ)7pxT!{H{q7)^iKVz+6lAcGzUqVZLdtSO(m>ZS#oFX#2~X(+ zJdjdW>6X99NRsR(%8)->SP&{H!(V!Cw-U0y7XH;^?1oPx_Rv0qL=cM2ba1##-qiys zI6bgP4v5eSD0f3yz=WcCuICxuWYA|7>T-8WWy}In9az$VGMBZqQ9iKcu&c4*$>eRu zcTuT6=El2yDn!pqItTwHRVA|;eZSYz3goORZNfOQ^r+|rlO!Af0TXa86^Vbx2yHCo zK@2;r9b-qCypl2e_vEpphsu<(ETk^I=XoIz* zC)n5~;WKGs74n}}UYcrJ$b0AUDQh4`D&S({Iq7MBd*uuxt*x{?uy@aX;SmwfC=k6_ zCJIDG^NAqt5)pB3P`qM%mG=7Dv1PQpviMgknrRy33N~hYn<^t0n${2e+3R&!z26md ze9eHY>-}UgtH`|VB?=Tim~6MS5pj5=dp#5YdCh+b40D7{q;=vl3W1#0*!0q1VuEz{8uZft`2HCNCAAr9N@ z(1W*7XQ&;Za=}Lw#9FFhQRpY^EI$qZt+`?UWldu{U^&j`*TpEXUzm(jkKX1T5xF2r zZ&1pr5(P~kfdL83#8mWoYRE`g=Fqk??Ua0suwHQ|B*8u~^lP@)>LVy5TYVg1QA4tYnijgyN{>DisDslPNo)ARY-D>U;4XYW)F+O2hJAm)>eg6j1P>;aikYB zRZlid-sW_kzoyhhv_I&*+8?L6wx3w`*NLRXZU5{t{W7H&*@JP^{(Fj_*3ZmSUJG7%3rpL!%j1%+Yo-Ibgt#y;)_%}Xe~+l zwvz2KL?S|Cp#4S(HlH803x{yuQq5>#gXN9W#Ltpgv8K!Col z-ZTXHXZ|R$r63;cUY^g_csDP(T~;VBx!9^dkF~y*4Cam~tG;?BugnGxXklEXJB^zC z0Q{i{fH4N-cEi_5ug@(HJCo(_gy z_sNJmX0=*x^{zpWoy@<`r8rI=>)cBha!J-+Wx5g*>i8%`0a1vf5QJavnz6p-7!1u_ zLU!|!`&Lh4%;55C*S#8|v_*J&L;+!j5Osg=+%#VW@EAbZa;OXYoe*ShG&o{6y0dRK zcn?T6Cq|rwVj6A=yrZE`%+p|DypmKAJG^dsH ziG&(&0c2OZIeU*1lfLuTGh4u3*GZP7{%p8^9NVFl%s;CKR0! zi7-&fo8zcIZrf(zRvh~1XXIQzy)#;23`}C`Njk4&j9Bk;z7mQgmru@p=TZ3Bf14fl zn!x5g+G!O2_~-3&#}#+ZBG9(H!F94{=_@fSgmx6^I5}OI=iPBo>yf#RnS1Mymto?M zJ9ue%t5=V?eNL$k;bu|)(@OrmOQDe6n;ZlU<)970d574}Wyp`^5!D;tO5kv*XQz}8 zuKJXO;=7l|MV)4h*yVFaRV_bL-=ckI%Dwh}eT#A18fnyF0ZjPm|H!o`fDl1O>`L4ID_#Gt$dhMI1 z);UaNpPimWe$&#PwL7vBC$+Us5&xa^Tr!iOc08jOgP|yiQ?)TgXGhsqbuwH!AL3#f3{Mc8I!;9O6p6R&LZ@bg_5x<8ebeM0i%CWl&M*(*@e&-f9=`f?_&6%ZH5WdP) zZb+8kZqcO{nqwnDX}1Xtn%sVvCOy=0eUay(NL)?SY}h-k*JQrJj^XQkOwGv$+%Y^^ zzk=9RNr~#!w3eOFQi?YOeD1jVM_BUa4HihxCKG8<8p568cjygz%JPDQZGCTRmpVfZ6JuJVxIAQxXge>bjzk z?{_oFudcO^&^>SV^$B0@Tn51=Rm#^hBn+}9AJSN@(S|`7ycIu;>M%FQ3)e>DiEf|Y zNjhDpxl;v2@4R$Znx8sG^F;0Mzq|FwxxoDRQiU=gBMq3OZi%`^TF~`^mvwyML6~@|qP!F)}j!&0G|&_XX{pH=_83Ig|fxt`%-MN{X>FbnFF3&to4_zA2DeOM_hrw`lqpt^j3R#9Q*tArD;~+?Pyy`m#P$K1(e*+HB5Mp zY;fzWTP+Ir3zr^2BL}{UvK1GvehfKDsu41R0~GeVQo&}DP}_U*-8#m{z)k*?QsL8*X@!Av6K#fUP3 zB)sAuhP!ft4O~ho?v;|L+ekvAyMr~^Sknvi)X;ssCx}F0&Q{7lqY)_SOO z3CriM5y^7SO#M{{ye#Fx9`DV5|BAXB(^E!EeX+nyrPGH1701k}%uik`<5!7A1$>Q- zkEqt^h}(hQN~QY2ebO94t9MpEM%N0;1q4$ea`SPNXCg+}Z}P@?m$1@(m+pH-z)Tq# z{ZRLfAEh94a&d}F zu^<5?siID9=e9jq)x!81&m>77KVyE~T(m=vT*D|b2gAmjzciYK>lwccBdv(nN=>21 z=Djx7_&=JyGAzpO`Fyd*x)B5gl$36UZls6qk`n0-k(N@rL%Qp| z{ax?>%UtvE+~@3AYwdmZv0|`{80KQmEqo0pF+FtYe?ImsKj2mRidsL|6_BGuE{@;$ zO6c#*6Jhia?LIAih5@=@AWqC@Mf=TMXcf`Z8=vzP%iYR#@n-*XSy)nT!GS}im1D5xh9G)hX6aN*jDXtT{}`}1L!~E)omG8XzkHJ} zGE_lOTqd%FY9SzK$%e)T9`A!W^T69Mt4!SYS*Y~9}LpOV^G3aBoyrm!-4 zVC^*vW{yTA)qO8Sn5DOtsiWXbC|p+j06OSA$1s zGg;&a@5RXpNj!QO*m;mJmB8}mEzv`fSahzTJ{jrebVZ~IELlN=tMFhqLcV0>p*`oR zC3}=7wKD_v5qc~3LVCKRG@tsG9*aCKl)yoIUmsC*630Vaaf6GyCe8n}C{g{nbSO6c z=;~Dy9`1 z3?T!ZI$E``s+SuqO9=s?AI4fvYFSoUn45EYTu@Gk2H}H=phA8Kk{FDVFBHr45uI1-UAYGrS_E{Qd#Ak(5~WH@9pt z=(hhJ&|xwqm(EMOAyP$s@#D@+T?{<4b_^c-QL8u5MR(4#m?0*mnQyVZk32SPW9g6r ztBKEtaZJ9WmAwDfXl!1=PjSkHRiSW7@o3B_T`}udrt@x3xiK|UMLli!+JQ^>&*8c) zGV|HvRgo_19STfa%VBlBj?ouWeE?d|nq>J#sqzYe_UnWp_P0kV9=TMLQ>brooY_;L zfU^gbv-FGoHU@nBr9vToY&qvI1@lYqhyE9YDP1r;KKCyd(8Lgq-wBCtjpF5GV-;tU zb?)Z<^zi5<=ZAh{=+jv<=B-+~5m$6LqA;%s;g?Ligi0|lJIbY3K?}y!dXl0lP^M0I zMV*4e#adC^Qs4fUN z9^Qs7)Ae^ju``H zylXYzXgiI+3)tWw!>9&gO(pJggTD8rK|_rZD>q;wfG-@ZpSVh=OZfPExsv8V!+}N_ z^V64t73p&75;7wDPvJHJ;(<*=N0Bph>m_gK`&0M^`zVi+uqnP&zuh3jn{#rFQlevC zaa@n=E{iO5z`+uY`_@VTYqY%&*IF5NcgZx~=b7`uC_aXQOpex{m|by~?DJ|MWFa=t z79XB8865*$&`LHH^WuK?URNoY5q3cet>69ZLATkZY}ts-R2v3Xq>lc2Hd#h>YihNM z#i_H3%RfaUaH+y2JuLs<#2^qi(C?X*pQjHl{Ls7_U-emWav4jh)=V30M0>oDCP9sn zr9XXD@0+DQXzxj6edyOKr8sHFf~M6Iw%2U_@SY>r;K)kSeRAhLqCOWb!O$H}pCBX1 zMx36#(ng+D9L>}ga-eJn!c4A3U6l@F+Qaw6n zTVf14uw<*+PcSO^+fehHReHpc8TKNde*c~*e=2OZPnOvkj`%rCHvDcs zlnli+@-BUGOND9EUF8x|a&&uv<$EuP7f*p0LBz35@`S=wC6iF~KE^f|>cukQQzdwhDk`He_LI{L3U6|)o= z`?BnyKq=)?5wtqFz8FR+U^LcGMH_;nYi2A8b+ZCm??)_ls*&+9HUy(ZubtArqtNYsi2wM=R|1bSwLB>s_x@}OFG+2?g z`hu+Rv>WNS9>eLg2()cC9GleWbmKfU|8bcR=jCfIkW^EdNb@tP>hA5lBaTMRw})EQ z2=aDN4y@xyk=izQRIRi(;t+8ifh5ZQ79hPJMJ_y@gJl@vuZv^V??9Dl96sCWIZ z+7n603M8FuEn};Z&67Kr8If^UH3b+e1|GETXGnvCk~Vf`Fks~`G+OVFkKf#|cl`2)B{R7z59S5^FQ8LL+FZeC0AOwydK0G^#YYC#V z@P#Q6({u0o(_5quSt`P8#;ttb_w7&=9}*M4n4OAkq}E4ikg}e@zgINrq-mopB*Ee4 zV6I8md~KN^7c(C5NQ_{JBa1gn_2$4gMk7wp4&3`bIFmgtm8f6;a^_kVs+9D|>a4uQ zn5=jIlaeiu_}Ax8YDz8&zLdomt=QUrixO9-i}NqES;Husdf0ziE*QJMCeQ%T53hMw z+i!j2cg)3GQ78bx43Kg>1aRN?CM_n{&AN1Z*$M_(F4+X>c34;KB1h#jW=2<#hAxX@ zr3+(C^q|?kXPlnuI+e_ZS5xN1ky3l=b!JbrG;v^@9k(d6iIXJYUY-}|s-h=S_%V6U{ZmI^*+Z3)ly|O1 z6$vIX@|LD8ZvHG|YbuIeCi40I98e<%23TM9#q(|}U>SJumjw@!X;iWSw%Bf?^X&+#<^yco9DS~af} zz(_Z%dGc}21QT}9Vrj-eaIiw1gpR zg8QWHBH2fNnX5MQ4TepANH?%-z!eI~7D+Iu7C$MXQ_hdW;!Gp=lMJA{j32wW*J+VM z>|)Bd^TbN?rgn*P9t(TuTD(GLs>Q&2N2#0moZ}SKU>VQH{K}GKEng8~dtXW&0tz+? ze!Kb?B_^)lO6hglM>R)z5*HeIrFoaokGfu^8ePBKX}WCzfcD@6Xj^%+pP3;2{!_E}aD#O8W^@oAM`jxi-$aIg?9I|+apfd9eht7#A)%|uhF4)|)ItR{~$QK$dWZt!Qg=0V;v5=DM6*GudQmIAi&0KMad7Tfs;-=SFjqr+;_I5ANL0C=kK-l9B)v9DQfv-K zrMHie-4LwT*ZAtmzE_D$sk<6bhQS_# zT!;yCp(tc0GCSIO+9#-ae!@2Gp81)W(Ml85vFT?p-V7{ei^M#f8I7O@MCVb1Xj&_L z=!KrbKpq^#y%?Hl^A#h$_SPT{SIl-GkfCN34XN-Nc@ z6J8U%nMy{EBr{yfV~*%toiB;Cye3cAP?w&%6(n5FmO3tvMVBv=^jYr+$nUwJB32xf zBqjgX3jopDmr}Mai-DB>J`z$#I78zcImqfWlp!ww3naLW$Yc|nQGB5dAr3--rD!fK zZP?+mkh_a2$}yR{wfXEPQRQ=} zJr$H?VO2ptzud*%2S3@NlU&Zl@>G|+JANm^R==+hi#9i`OzY!frmgLQvjB*20n{$| zh2lOsHRZJLC}*XdGAxk*=5BJ*H<#I78-lpKUzDF^nSd~HA5;Um92vF9p~^|;_~T}G zYz2GR&-+h9_wDh9@_$qXeBX)I9%9pfFGJBmQQ>75lG^jDOnhy5tG=*`<1aX$s~m&{ zqW|K!m1;sg!K}_|6O#-pT+MG{;{5QEY(8K)O?*g~z$Aqz6FK`kqYKI<3EO;z7Vjli zWlR1!{uHN_ftEi}!XUqkl56N?O;j5S%%Dq~Ri65Kmqd?sad1)IjeI=T02d0Cnd5CP z*ioxfd4oSngThhjT>XV)4G7;z$cYXHTRqtWP{_A%kU>VrD`sd)id_o#bpP?YtP}`S zEf_ThfHyA*>$=OcSY=Mc6l^2~>8=z0J9Asi`Oy`Lm9Xbkn2|5g6VR~u=S2kRoy8WJ zK7{d=9$LaWdZP@H<0sYEq~vo7APv>l$(_h923Rb13$~8Q#U*vcXnw4eEOmC8iqkLl z&D%tx*!ID6ox+1{ZQanzezltnEhCqh8C*qPt{WroA`*CPUJ7{9>p}CEoS##H@`Bba z{`fVV&OXH6q!K$kqlE*5!lf|lk3bux?lXtnsDX>4C)GwFWdy~NavpmaGA^IoM3E`| zRKyU?U7c?&O7Jd+T~1n3zjPrwd5=u>oLB$--20WJOz-Bne76CsE=)fGav*}DZx z)cSn*IK7kpfVjGWQJ7$#gy64qWS<73L))HWNU zl^k;-*+3bKjKV0h)?;QS8dt-(c0IR?m<-B|lwD5>FJ^x6tg5c=gGJJ^HI9k#$VlQl zh*!@&Ld``*D%d$|XM&~^SOmeSl;zni<0?M2zsr-(-zAUw5?@aL5?T08E78sbp@oM! zm38Magvei?ksh$n8Drp!vSpGDyXjgYvgk+CSv9h8`Wn;F)WshKTViHGtq&dMtB$ps zj~b?*WOO&D(CC9Zb-n-m`OntLeg0ADpY83j)!*q8NqweY8HtO;e_5r^cqn_>| z@GDvR3V;Qd$-aiCZ`3rqs`jt`!*>E7OyXhoKUk)7<}*z{`v8ZT^1Kv#ZEsxTq%$X$ zGLX^*2N(j}LXs`0Yv0zedx)}yBO~b8+XI*~dyAwE_z~XQXq(K76E729(AmTJI3}8c zIoNM^0{NNa$_Pg>{?8w>=8x$m#FMhJaJBp!#d4mSVkikHB!N_3ot#dn3B-F-PnzgaeHR}F;WcFQII8B=wSuB_#22e4WSik$S z$#ca!9BA{3laVxsft1B~zr80*ad>IfugM|kMS@(_BgRZ9mrk(~1po-rUr;XBJ^F>n zPeFY>LLaFMjNVW)0OA<9H=s0J{XY)DkfYDOcxsn}6yO#wJ=IoTi~_96Dhn(|5}XwS zX4nXG4az!e0u~k#OcvD5kIfVtb@&aH1S=AX?US_n(T3Q>v11wgW(cCL&@fHrgtq4v z)CUjJIGW++sS^Mtl0t6Mj}Fmt5lsq>X6(&Ph@r%}_@S5HN4wM>4<@?e90SNvgNefW z!oKOS@1$-Ljh>ukBtJ}LO-tW$jWd<-KfaI8ub79wo1(#lmf!&3;qEEh!%>=14_L#H z@5+{*&-VeG{eLzp`v=-e->CD<5sF!TARbJTsZP&MsC$`Wx$3gD-?#u&H?|j+6xUM~ zDXmx7=?fJRvVEi=ZCT4&x|UIe$OsU9D`OehhKLrE%Qa?=p{M1gmi3DgZFyZw0Zowv z37vR#VQAiY!NI3_#HS=*DPK=i`9l|}xxb`S(!xhcrW@}P6d6rLc&HJYc%(Xwr}=m6 z?_TodlZ#(K|JZ>kooj! z4eH&TXhs_bY5@jzGQY6k#Zi`TT)ewU;0B-=0Jr_|z+BcwzD3oAL8R!V`cifH`u}j@ z$yO)e{E+L?95Kqn%gk~tgIVxn;Z4R-+24G8L6kpZCHs0gP&vc}bd!d-qbCl%p!l*# zY%j>mTPNfUtoVYoV_sWq-86)QR9k~iPDQe~7nzAs5e8fY>&on(F7i4WlEmihug#-n z=e+EMQFQ@A+N4Zr&i*PYi^HK4f*Ozrt?cY}!oT@emPVo^&;+i1#sbM%LpZ9ey;niZ z-O`c{Ii(K19+Ib9FeBb!B3rH%rI1S*7Q*XRfheJ};Ap3tEb@5wnNA{0vkXH7#N+MO zn)5u`O6ntpm$hzrBh{Y~%)&7^G6oEXTi9^^{kB~Cix?sUI!$1cICsMd^wRU?AHxSI z#m`E$?L)L2xR`3dGJ);w?f*H8G$S0~%BKwZ(usnyf=WG+I$Ftk3K9>mi)e}VKwBT> z2AN-_|G}r%u+E?<84hKuG(0WBVTZ`a+qI)z238_1aBEV&?{~x`|3;vS$qsH0|L^M1 z`O7n+52%j)XdVx2F=R^lVeHx}b*EeY%KMr3OfVxO%TZsd0OA;8b1ZEO7*l>% zafz&K&K8C)rfV*xv{8=Nt$8`?*{kncsE?-S_nSMwQpovc0?|9=lg%+NmtYWxee?Id zBt9}}P@0s=dQavDof_@nTJu7GK1K}Ib@S;|n zKpCd)XW$khw@RUfrJ+G2#^bXN_h56u2!oG} zFEu0GgqfTnQX2)>a+&H7@J4j)&U0zf_t9!}SwbThva8p6RqDL%&NdRe=cNSy&R5j{ z``x?nqG_NY7!ky6;5rgA zA}-dBl$e-S82BP)4Fw}4;YQtY+IvAbo^&~y){5gKBzr9#zH?&CJH~;Ulcb+|@*qZl zi-xh)%D1vp6rlj>(RIIRih~n!;je}C@81Ge8Uej)F#AK1Wyo5_y<3H6kob=Dznx1b z+`6Am;1*%;%Eq~kxHDtJLt)nU6DzAVJk#x(7ok2&cnSCn9`63-T{DQ+4V9g}no*(i zO{lCxX~0Y!;H3H9)Q^g^?DTb;eymRMSUhQQ+IjQ&E0^93e zo?AT_$5GcHJc5C_AcB*A77Wsqm0hsibIezlXiAJ!da3v68w}-zj;Y5Vw=`kzi?_Xu zS$@@Koqi`@dnBTCOgla`1?U75MK4y#>Uu~55B~s_K<(bW{RYddwJ6ju_E5sG{Wxu( z74}GUT|HYa;p|k~qnwSG4N9qO;Odb;W%EnOKZn846#M2A4jB~b3#xcdq31fIA^D~H zRhDJoJvgvv`v{KcN~xHeN=iz4@rEQpztX|7A6&Bj4R#bgm{vZEf11#z<+r7CnSXv>4`?0LN9Q}z(ky;UvvCwRR}&AYP*Vu zByb1Aqs$*FXrfW(ox+S^uLVbAk^G7T5-Y!-?k(2GNtM!|d`&#mh%UYd8D$P;AH{ar zk#ZR|y+;P3=&?VU$YoyE`zsa%QgZe&;hb?V>3=9<7w(GvZN2}?lc>p(2RN%DYi(tE zj^*(n$E@LhRwlE4Q6msR*MxkaN65rmmi`1 z>VrPs3sWFMbZMqdBP~piNWST33xItg3C*N=W=G}sOe07QgoleYGt2Hq*=i|gBtmZN%riV8>29`ME60@i57Rh6$l;fYU(~3g?HeOom z$w@9}p8b|ThG!~RQ6X9yCBznT>IFbx!wWCVz_!=4lVv+PzC{pHpPPbQPZRoth*#Ly zVBLHa3kYjtT(WV~n#e8VA4_6)4PD8_tG|1DP~QpNI!)f>-Q?pUX&IMha z^2RpqpMf7zjPs(588OI7(>wLc8!d|rqewCVJh1oC#*wnPXDW1mEy(bDGh8U(ip>z-g^$BoxYIjM<%RmZPI!{nRemr1uU~G*qxZ@$~c-=p(q_IY*w2-60)E$&MiR!UF4D@4@=Pu=<0`zOCvl$a{b zeCqVtkayH2wIGru0~u$rmqKu5i%dAc43QRJ>(hZ64pX3R*{^!{SCYys(e!x%sGB5~ zOx&mFMSALq`pllT>CLZv`EWw>kN?@wCrJv#_{X8uHSnGYBS0b5KGVR70DgNST}Tub zGiIA0bEy_;$?E+7fs#51yWigTq8az%E{4n$OLeNqp9EwW9@;IEU?`Fg&wByW)$)dV z>8kbX__4{O>Q|di3rP33^fa3b#`}pTvnxvc3!HC=s+!IdFC{KzFv=n@yGFP;Dex;5 z(xnj*1pm+{yi}*dR@Xqw9?J+ofPkw@BiFP2p(8qojuq}Fq_;{^6Gu-)E1`ldnrk97 zS<7}C)d9E`)^8tY`SxGjHD1%Uc+r3mD688}oW|8E3!+<^mR>`(9#F?P`8d3%6XF^r z?%#Gv$`J-MJGNg4bQxDWVMuvH#l&*Fwh_77}|UFN^A0k zbxDXfJpM%Ikzot$dv%h)pr(uANRm9|`FOh{GiwY-0Fo-)rGH+%F!F(ZYNVvKp2xZP z3fO8fRse@82QXrtQ_^Rl2ygWRk|aq1O4j6slpKH8jVfYQk{>#C5J7`DNRR+_w-873Dx9+?N(Yu~sMslj4fE2<+^1fy$*)PD z6p#+Jq?OOXXgSP-SezA^w1 zCEmV!Im9qt*~Y1$6dUBiHmf1?WHp>en;t4#zvoS&mzyvN9 z1hA$B9@tS_9ap{e zwNM~4?tkxp`{=W%f5BpA`T|z_hgF4ouBoU~l7&UCVnJr8>qacSo@`C`!+L{w*Vvsp5?Jv=y~XVmWe=xyG`rM5r<#}A&u`br??*;#zYF@OQq_Ox8o;^j1x zKjeCjivfZPKCd$N_}<)v^eB(01__RwkU*#=?@WNcrikr=g@K7^>fw^C5wCNeOA{Y+ zw+77)Wf9(Jz#%-!F>A)`#x$Ajrm$1QRUS#P2)F8Pk&aMYrlOTjxn3vTm6-VsY|Nd| z9L~G#a_8ci=%%|Shembx?WkOLsrmim@5_TZg0`RXN7<4kVs&5!bX^O!0ni_~|9vd5 zDjHI}IEd9>2I-O6VxXRUeQO0^%P5$A)n-aQ>f6~UwMnUeaSvz65@p{0ZAude6V~0r z`ISs_0PmCizB)>v72_l$HdQ=S*eu@OUmT?pDf0*;lK9s^`Vv_W7Eg3qEtKVejb;*J zwOjFCU!us0vpGX~xW)By@o;ly<^W%q9b53ge+yL?|96KQ$4M=Wl}boX4voh9#P6~~ zCz^PKi!uhN+x4(|Hm|=~)WHh~%~Mm-qZ1#NnRLUhgWKvy<9iGLYAoMEk|Z&JBQQn7 zGODgfLlwNOi>r3IUB#f-11PdV4JvrpoKjoK)L)@rOL7cnnzc5aR5ynjvQ?+Pqbti4 zj)dXJVSSoZ;(dRj_8Zf?`vt~9olcV0EGFAu^LiXoQ2TfkjCIildkK# zmI)P<(aS$mp-D6{fQgnuaeDy-UyjdS#*5K~*1;glRCPzkYnzZ>J*;&;;Q zEQ!z8hiKlo`updh?kSph#!)~rtYDCiCt2DuM|Wm)yD>@Y28nwJBAWe*A<}Y#UUp1EE$msF4YM!WldvD2%~3f z>ljU6V;LTOZjjmF5nQ!OmeQQpAl$nLt7(r2sKDdjA zrM(%2o*zO|%t_#;=1+iBu^(ClVY!J&^}D|~`PkhEY2(Zxzh8F}{cvPwVVJ`rzG1}q zHATB)yeuvu-(J(9nqygtIX*x9Uy|aJ99j7y_;aTxJyHLVUxx~bsL&~%tqCvSbrBAQ z`NTKzUvVRTqu&prMw@tDl#zUZn=dZIh1&Zpx5ZZ*E z;}6#oT$qn?0i{BRRv0TQPuHfqyX-?){Vy4gxhSPEfE{j(aM0se7N*p7`$(|ck`7_f zGUC<1@zJp1O`IFznz0UK#G-o2Wn#eN-m||vaJ|?D@(X z1EHgp?ry*O`(Fop+Q`UAV3IFK7zInUaeJ!wZuk8wbVc4X=oY5sa$vA=diQxF=gims`~ueTV(KU@ywDB1x+6X;!T#L>D>Tv>eXax<5$m^*z#?(S|8Z(p zq)1(qNH_ZRhtjMkA{gNyY4+K&JtmZjlnPl5sdAv`aGy``@On{gENI{MYZ5#8N0Gct zInAAeXU=H#uN@!5G;{`s^;x~(ph8<^jP!Ph!dv}?|J1FG2q$umpn(f)oeerE^`Z^=a@e6xX&^qbO&WP~ zhCqdP8`98E>dlql{a=h--0^KYQ;vyx;&8cN7AAIl|4w!YQjY(l!KQXJJ`RnAY;zw$ zD;6<|8c@4oWX*OyHAvL6P|vsi3%^0YL;ppxH=?OOVnn=1&T-QeWgI(W2br1KOtZ^Hd zd*;&kbp7LxL6>p*ehE+a!Iuv~R?{eJ2DYZG8NG}0jjJHp_z$ikytU#bh9LlLVR!*K zDEnsm8SDM;6o;cVF8$D`R=p~s7+fCxk~|w>8Z-?;Q4rx^YKdh#PDN=ukqadRa@ zb`<{UK#ZbyRUh$+bct?W62mJ}11G@A6%Q_a#<-NXNzD&;+}XzaUstSGr| zg@oZ%lf(SbT;)dFldQz?FPsn6KTkf+6tHr43Ek7rjQVgYP1RYneY6dh7y1-XOAdL@rV5vAj6<>8>& zN@=oo-wc!)E+#@NlNzC5zmXoOKmdaa4r=WZS|aoLv2L40ERC5@8xAd zkKbN)Mi|F9a+I1Rsg_g|WQk3vPnj-}6;zfjZPCOh9y%_hcTdmto@@-sz~s-L%tVpR zGY4)a_T(;=PC8#fR$MWs;MNu_&Dp!A^=%+UR~lnAnCp+}82tOTT?Wa! zxBX&h*a#_{5?;5n7HUZxf3;6hGvhCP&f7vSp*N1st(X&94fc8OPEHCw97=7jy4A z1UytSGqa?$!7hmZnIdY|Mke!6@cJn`{3N;WD?I9@EQvK+)7qZgeQwj!_?IQ+^9yaW zhmn{+-tCPzjDYq4Abf((2K9iU3iTgfdqObEJKzEGsW@`}b-&AI`Mrmd&I)L8Skt}i zGKAfmW7g~~xwXF=$^i*{S|fUF6iO(vPmFxz39e3E-=2 z(+fM0m8r8Frsqu%yGZjpRWm<4j1Um|n;eEn%09gV}?81O!X?27z|EHOuKfg(-oZ-XGvDJW^(G?Ct zZ_Ahql_}ZC$ZEz}TGjcBaXi_`uG~*te$A;+niqRN^H$S7o9KBuT}Z{YMsK#wcNdL~ zt=1g9f!`q8$#5a`iB+M&*E~wO588c}(plNjq%fw%`Z`n_V3R7HSMQFErj!CL!BXrM zfT48dkXvH;{R%ISuPg1tW&g&-SubW$^f&FGSUo%K@Wcfi9O^!H$K8Eio&+DLrB}PF zga-g24@YhgB*!QDjgNDgapzLVA6n>z(=5pE;L8`w8gwKeNM>s0I%Lnw^jA`oeyg5|G3zuFO_SLlA2g{hID zm;?%TIZ>e|D>z#^8CDiNIE+v>QrW~;43E0;VCwT+T8aq(Xi4sR#rJj)N(m5ks!Oitd)n}Z-7QQS*Xi8$?F_ZO=E(WI3C zX@7C{4q%6erHh_?@`f3f0kuF^I*20XpruJ`OfJ4PQ&Zf@{W}VD(RKO!>TbPmGtUc$ zfC`{M2#B+@+jqZAzd_^(d>E5c{+JUwlB0Eud7r721TEqua;ImG;Qx97q$>zm{c2C( zmd$#?GE3nulchs*`K?_FO`M7!1j(>T+?=F31gzku@XXhwj^2=;n25)mK|$H`shzpQ#V; zWn96Mwgm-P6B;yRe)!265F!+cnULj^SAj85SgCD64IrHn1sa5o*7Q7quup$gCgx^=*vaoGn~eq*CXd@e7_fTJExt zk6-#5+23sS^C#N|^3o4#X;lhLA2(B4EkjcAy#bl&N1jGB7!FDAzHX%y55EvV)>JEU9P6H%9d;{?G)*^oe*haq=3Lu)1G z$K2I)fC^WCKvfjoW)eWSmeS6rSOiuqvKidpsDy&KLcaJfEg|+9VxsQRhNYGyltT(` z{AreiloT6I@rDw~b*{g5s+CDY^ier1f+C*r6m{<5D1pr=da#w%v4A2Pt- zIQSeBs5u~ck|cT#eJuv`Du=HM@*US2n5>J5r5PO_1cgS?G?JHe_$@=ul?l_|WSP;- zRYZTRLv<5TTaP(GcZqEumoORAP-R?hZ!Xb8g(q}H0^rc8)~n7z((6_E@OQp|s;Rcu&&Qt0y-A<$lXtWG0k z&Z)s1{e@VzS}xwehGC#dQ?9Y_CPPKtjDNrw*DLpK3v~KiAC(+j*4{>%$;BPp(j_eX z*ifhexkH>jp2X|VJel=$)`aMjVLmdU9e#K<@H=u2g`0D-<{&nu^LVGDdB$PS?y25e zZIfD+l?AZEJ zJaGk(u!(txE6_ny-f9y?hKi;ftfLeTuXJdmor-B(+eGy&&Mm_JLX(?fc>P%yb?TFi z-{?resgfWZOyOsd_vYj|E(ngT3-Kvj|6y+xQ+*mM%2*R?G5etk&Cgo2Y3XeYK5J3q zZN~#+Mp`AMns}79VXx#+uj{sKD+<}=X>)YSqy&T98(%8(Zi~@!c1(P%-1ZbH<%`}q z>?)rE7XZ46!-!!-Q{8Owlz-(vRbdrWPtSmzQ1_WNuyR;7g(y}!E~52g6_EiHXI)*0 zCLBjOk$x|I>Vn654fPX!2xpOBTgXR~p1QQtjeV={5q!psEtiH)J0itk4_&nq%c8XvBYhmdtr%pIcKGrCybm}ffVFo77%r<%sOfk?UG$a z;7=qWo;>Mp#}lK(oMfaDOR|=9BNpZRBLs93w3}GrE&{=2a#0tMKMt-*i4l#sMHz{h zhKMml{Cv~E04V`d)~LAuW9WbK7b()bl|p_%{Apsr`DE5J9%x6IGo}*TpX}};E#?EJ zsWjjYXA`@7F?Y?){_|_I%YyPljMev`Z&my1bmCg9-eCY3ag46ad4PJmzhM6-$AJU* z%%_#5k^?#6jpF?8+%sWOZqj2*H?t^Y1jjxNZnb&V7Mf-D@VaSg^y zrR83)xWlA29C4ds=toHV><)p4+Fj>`XqL=+4R(%huY=lOh)Yn~V*EwQ^bfBBdx46b zi9EmFEZv%**}G9#kG0dWMWb8r<;ZV|f6?n2s6H3^d+Z&nACIx4>G*9xhFshVbMXb! z^qUKh-4?k;Q;NMvi|c}hV6@LU5pUu@qF2*L9K1}i5N0*_&&6*hH_a)R|IOtdAmFf> z03{FPpQhiuoo*4B$8n89J@zLjR-5Rd0)NQ~M8rR9wap}?BVRuR`u*YnyFf)ng=ug& z1c{NBoa<7Ep3I~}9hR@g*1xB9o!3R6i+gWxt&-b^phrqizQQ_VuQZ=&9WWXDCsNmT zz514x)SHsdo}`mC+{UwMYh}}MS|~|#^CM-N$HKJdo$w(`D5f2YzWT@Xh^C|r7B$|v z$$^{_{eV1CO5IZ)9NuAK%B-XuY`PBh^)*ZVZ);ay$3a`hkHVizg(I-5<@I}nHeDLg z-BhU#yZJfdB6Xw^-J!;NVtUoo*SA`TKPvN2&%6U|$#;ZOYwR&*3tT>a)BI1qA#snw z4s%(a!yfHlA`m+>1VGjz=-HN_uoCC2Zfqm?VX&8qUJ-R7W|e0=0rvs$Amd``~qm4u-q|-a;%@n z$$1yI{)S42sZU&E#HrS^*QG0yeReT-m7M0r{O{}mxtU{$W&`TolM5A1U1nB!O$Km7 zkUb2giQt8%H2V+b-@`1T&eD-6qN=&luNSR;S-)rAG{{V66laG;{0vSPp4^ipebN#C zxQjPco}a8T1^!60XL!HJ)|T5!9>EQL7E#yjZoRak92it*k$XX$T7Ch+{bOfZ`Y#_;Cy4?K|kT=1w;H(KKMf7;s@ael9C$~J(`pF~VNy2br;dEGB$X1bX$ z($AzJszpemZnva%evQmFCD)PrUs83%%sG>z1Z%mg+~?gg0#eH#>Z~aCv?~x-suf|! z_0M`|B+=a-8seB{F*|11BUMG-?bUtVZ_VA}e_A>fWDE#J(bsUA7 z*59h;sPxsd2X8f|qO=&{LVuat0{NU`5o&}aqY#FJj$}!~xCWRa$Ms=w0}={@D}A6- z?@$v_MbOFj1nCG#9hvxtMOjPmH$xE~Jt?F2nDa1g@$8@SLl} z2g&uqTd6y)(s{RX^p=MDKU5Sqvpd?WOCh~4*#6^?bF~|BFa6~$-?d*=N6!sUpSE_~ zNDO)3&;MD5Jx2fh=5?&l*3~KD zZ^wbC@^B&NUPPo-R__v=Rr$DAreWsqw?&)3V@FE6#h0zBUU;t}uFO{&8MZ(^p^S82 zL-KmJYLexgooVK7k%ZA;g{=3Cws&7RTI%9D@iZC?*qpMFhDe!HjKex`gKicY7Ihoi z(nhYzJD{SlbjWS=8Ua#S2uATCX8D>IwC3Q=X@b0*OoKgf2swIMn(!9s&V5o4bPLDB z#&N@hWT1~TSU@oo?cqP!v-t6rqK5Smo33qm zEgbe=m6G*9-JkIZjUq#%!F%@;HQC>ovi{bR&XX%?)a5Eq5*w`w=}V7Rom9WccwAVv zR_8wd)IdP&4-XGJ@K&sjEj6&nsctByWJTAXK%uU91aeofQ_$F0X<-!oAJ4ZD?@aT~ z%f???pZxebHl{#Q&6@!iep|p$ppO4^)@ObXg`ga)DW+vL3P^ zB0Vb_57q}q*zjfha3oGtT%&mx2^OK4ssf^>SU1L!(mc&Z63 zP!%KH%fh49{V2TRB4C9mZw5WwsL{rhW##nmJ(=RS?D9mZNd>f%3!xP#aj!*9^{KTX zKr5q05^k{j$|$-x8TPlhPxX$(6e|Q;7B?=e@e*vwKV;$Uz0)-ColYuu)zEzM`0-hw zquU;6sgQT14sXkEd0nund!UK_D;8#h+P_6; zsmh*UEUt-8m2~x}VGdr#qdTwVkskjHW^oz(l?aVbtx6sxrkTw=jMB97JA5Z7e+eR& zd=<;`<7msyNUj@(@)V#^$Kb;-qKB@&tk<-MR>9?x%Vx0iIb_hH$nblBuWuu?noHVk z;Qr^@{FOBqWRM2+wk-u;<>-dAly>2wk#+D(k?nl_ivzj+N?apeZPPS{a^ak=Z$IUYQSJ})_;2KXT+1fyKPMkH_DK>;(M|qeo9XQRaw*d*nDL*F+xnJ-hs2 z5J;F5VH7m+`xEOlg*c%UC7Yf4(%HDYz zF3XsIjf51w=a#-W26}Iv;hG#pKm1<(3KAM5C$z5zMvLBLoThJo{HRCo*6?NM&L4f_ z)~reN6t7(4J)aW39CK2js>4g6zY6?0I!DRS{;Qa;z#s*M7~bhmPa1w#WokgbC=QEy z0fpT*fO(IX&}8B#T@(7`BCf|C(sC=bPEDg9!3l=Tf-a>(dFt=vs{SZV2>HeEZjTM5 zn|%UjCWI`dp2TI5M8UY|r^LtEMY)*7!*R>7M(8nl5xtFHqQ73~#DKI4h0~Ea(w&-L&G^DABeEvE1KIv=B&2Nc;P6W=sJvjzI zZSS_AFw3~cdb8L*1HFPD(%g|pRdZ1fXSl#ja*DJk#WJ|!MC7ZAa<06MblDk)vzZY) z*POvZ{lHsgG-Uo#qIrDdxd0t`GpF~&?YQe7?c>7f$GR;dHASi-Pva{@2&G)n6F^e5Z+!JjTb z#8j%fS)>E*gnKGVVaNa7bV$x}RyOAwCEk+$&SCFDSB@Y#fsTwsW~-2sOkMFwWL3tU zJf9nk{f@54a;D-KBbL|rPTIG5m92$F%KZNV(JC(0)hkw@QIrpO5D6Di%`emBwJAEx z-J_6!$5IaCugqf*WZ-g4VzDUFFb_bcvs&a8tEd>yiT4hqx>;kI0+Wp6+T=wa9|u9h z<7NRB-S!c>AS<55zqX=l)>3Er$Y8#XA2!DPkDgm zdc96X4N6z8ULz$|KS-jJct+XJy>rMTVcN?`UX+!p$#EYcd&=-0DiyMmQM$SZy1|MeB9`(!|T8f`xZ z(pRe2>@mcq^^f3lWpn%cxFFgmCj!R)zEr9;BF(so69tt|G_xdijisTF{b<46)HeZ!S)`?phiq;;TGJ zvR<5*-Pds|4|fbA4c?BGSDWaA0=|C*8QY6Y0gBUZ8Dl;%mz*ZmK93vQuhlj7a+VTW zG5i=r@`~p2ON+;nc4~u-9j^pDfYZ^q|(XBUQdDs$lzIl!Q@FE zleM!@y+pOezFEycfg=MEmvW*N$Ubuh19(`UW51}l#05WGVQO#ZV=rUU>M~@G1+jX% zDZROD#T!-#ysE#LOS81Ozh)X(KuYKOLlDZ{= zR5>0N97>lWJR2693^>{J`vg}1xUXl0+R)jL>>%c9+zBGr$M`zB+| z+pwP>30cj36f4fY>z)_wvePFT6GR-_Ca~mw(hey+rA}u*|8(^oU#@=TXO+BCC|W|` zkN1)P?^AEN?{B~2=+xBYx8uGNYmo@|YL|6G5&K?m#dA~apPw)35S6ri9yjYh%G)+Q zL2N}WW^ONZWq2{Dh3yxhK#z)>I9BLp?8_=NFG&{1c0nxsM%hC2Re{AGrhHCN64GD{Lot+$`W(>b<5|93r(AJr`5;di}q{ql;lz>{Sdt z21$8H^SQ#}v1H%;3C976dc8jGeK2$oYL%1COt4)h;bLE;vgjFu-9&&9w*6D$rBzp& zomj1z#qpX}m^miVP4#;_Xm_R8>ZeEe{9&4EPk0Zc7Bq96T9xBl=?#Q)j*5#p+ZFTEy7CU=eC2r#ZfC&s=gkVfD*dw5`Au_T)K=W1h2K zx5wTp0=s_;^^(N8@DeFIuBA)m#`usRWDL|yAGaqD=(~HVdZc)HUttLGohyl-9|cKP z7KBj&{jkpi%~E^G2NeCC5dVh;^41t9^5!FAjJb^I?d{WIJTrl{AdK+oy1vvH^SxpF zk>5LO|G#03d6#%;)vA*C=DFnbABq3%ij<9hhmYHV^7_@UZ2s)#R_)iecf1^-LZOI> zqel*#xtG0^emjeWSl@5H52jqIX6t^;3kFTwI5@uh2jUYvw~FEzO(~-Ae2Tp=*Dl04 zMfS6!t^6GBS)A4mWQ-=`2G6<8iiNQtWy*Dq!nm4oPp?d)Ma;E@=jEn|V=l`APC}jb z1}sWN&O@x2fN?>n=@_ecKJp?l7eqMl+<)pzU-{tf?w*W)Ru+#Xn;LhD{$p(;SnUnV zFz6PxxY9NKs6l&gpbD(v1pCyqCCj#yFEfyb_asNR3FX+nNu%7pQmx$sG9WD#FV^Rp zPK(5YZV5prSK#BE`Oo#rpTSuDgzJ@QCICf0t%?*W#lL{P+WkS9~PqQ-+f&jsNf^()4h z?;&3&GEPTTH;KGwC6@!H<03`x1SSQuP%2RXc8_BW+ig7#f{M&a|0Q0tiH`-Od*sna zd5j|;c}0r2AI9%_n2i(TaTez7(&LH=&>KSC$m%$$*gC<%IL62CiT0fVX%(!pW5U7P zljoZ(y6~__4<@4Mr|?~LO$gKGw!$-Ojqk4#X#O)V*ij< zz(rw%C9K*;5m>MBy#6lh|Knmz5J5Rvsn(KWOwjROX^i2la)>^B~pydC>{l&x~rlm_@e8Dl;zRwzG%>t2AW zhb!BGY8Om+M76D1)jXcH_cTi|MjWHu0qcyG;7VadTfLQ-f&XTV`Dy+~ zq)e_O&MjhJSvlj5ZU`x&SDe2lUdM_^?OKSDX0fQ1) z9R_Z?PE*c7gYcayb+Q7BC+#dP$fyNY>saYpjg>>}!(PGcRX%Z%^XNU^GqbqPn=yze zyA=LBCZtaEBCZ|)03SL@L_t&^3M=KYT@rHrjQ!xp_xnIl%CCzRP8+P)dYv)mrZMKdSP|t3 zo%G4dAsF9Bpb3(88D{l4kXQ?X?7aXr((hSm7da0x$b=oWd)o1S#;RyMzD-??MK+liIu3WuF%9*8y9=u;p zmY1#P{(%KT+|RO28mb>qh{MmP9Oa=DvOihjbzeGoFM)V;kd+K6C~%`v{er66&GaFz zTeFLrtXek?b$rQsUbL*f>jZ>NM8>K>n8tH|2_Ba6JHm?oc_@vgWkAt-&i#ck=GEf= z%kaT!%xmy{<9Ww#_Om|}{edc_Mhi+{bskFyUZSeAG!3CtGF&3I-F>!VkQoC^{zyY= z`CyWhS`OZ}tW`6YGNC6yl~9+kdGiBk=K|W$sOD9v)*g-sRKDbRt)Yy75~Yj@_^hpH z;VNcm6Psg8V()}Sy3C4nM)9}Sy44{%SC*$0X+KXFO4x@E#A(GKvxE|`|ce$9uqinB6b<$ zbq14*SgE4s_IHQ2sab>&S0eh`*5~tle?yy@*rs;Bt5U7KQH*mo%uk6RzKnEFzUO2e z9%pQ*yW@ENeFVRox&E$+fmDI<_pk&)%~nNi;k}o@hm43+QgH3H;v| z^Wg#atcrXt;JG!^qj)tNvZ!H?wVo4Lmi-6Mhg{V*^D%5bRvj_m8Bw+OS@GVwRP%e_ zdj2aEiXno2fjbPj5)z_rX%(~Lfi-Im%52Nc8eWEh)2uiJtx~PMm(sQ7lGASv+YZ6o zvLvKHfLy}FPWDH^WpawoUl0@cW=+8(jmNF(J!SlGkg_C{E+tt>htj;nZgLTU%722N zjl^Z~_mT)MTne3FVhV!fyg7(1`Cv!ivwu`Z1Ppcna~fpHeIS3N;dKtzT^goKPmfa+ zoa0yzNPh+Kxm75WT| z7q5Q=@3|B?VHJ90keLN>@^$Rf0uKpqe~9tNo@!qp!*%p|(O6ZfxM~pyRqlWLK6V%t z4Sx;i`1g!4pMu|+jU|k)14SLs=j&*fO(rp+=DUX;IA@H@`*0mo=3u2yy7vENE;;=H zkNGUCFrI>%@lP?nQ)uU`I4-5iZa}#e^wpKC*RoH3wtwj&3WcHpNn{lsdhq^2mwbno zT|QR~q0llijtPOixYCt-Nw2aK5Vw{+)fnye9e z1aC5c%R9V?3{14k)@1&8M9Stm6Hkuz^L0PYl^?~qmOX@ng#4}?msRO3rJJoNVqCNc z##P7a-pinjV;`}fG{$@rJV;!rlI-R5E|hEjIuMg&r2YNIm{(YvoE2)NGWwAx2?tgO zLB2@>i^;`C^*Sqkj#Kl3$p8qCGw{RuDqPDgD0qIy(MPKh1XLQtdr_FoOZ;nVJj!jE z%#3s$hl9Z4 zd!4UuGVtCgx8-8_ z3tzf;_>hZ(RVWmFM3B0E^%H+_@vb}XG?SB)zLu=K{%c>}bZjW@VLnw&-dgC4I)-id z9cdp%@}4fz?H{YbGfOi4xQp zW&4H>f@B2`hmJ~??Yw-C8F){C2UFVe>f*om5dtCA@=`(`UyNYtXKc?KKu%KAZ2m3G zp$~W;gpqU>io)ca@f?0rN>SAq^9OUu>F@Jw&w=YXJ9tA1qH&N|=RFmBY{UElRv(wb zOk#KnuXXQi(G3vcG!W^7&NhlI&yE{Ca zN)NSn3;!=yK(;R}RhWD8*dk@n-xr@}r5+JN|Iiro-{rlAS7|-RTD2!MLIG947%ISY zm{VYEqtknR{pCqxqx8XlU#Zsq7Sw7kmpY2hf&^%spX@4(MLLNGcnCvPAW zUtg)#9zylVrycu~6_ZvR_lJ4w`$J@DsR@Pcl1PhWa60UPw9~1{iErQD_V4T^(=MAX zg+ehZ;EJ+1on?`dn2+hP6eniOh_pmf%e)`Y@MT|0pHNz_2w%lnYjIU#(^8%{4YY|> zG~TwMO>xefRnV=NI`;*LH}8u&_TsVR^4dmKzSQw^y`b#v$oJt> zj`Qaf#IyJzCD#v%Qqwl>a8+8WRBOK#Qu*#JA$?_FR=y<8<9%33-^Fw4sAHQ~AcMEdx!Nhk5MduKH4K-`m%d4p$@cD$m3un#m3Oq5iN~MJ@RJuTX)fX;mS{ zYZi*K_d-#aF}DNe(>Rv}M*u=(#@q1sg0TWsPNl%=TS<@=pLd+Y7FPbs@Ey7*WE}ZO zLnd{2EZPJhDZhTcQmy@;7<&b@@tZ-eIEhV%A6On~ikl%TsJFOJg!c(oqq9{+bie;z zV~kJ1XF$r3Lg}N}pRE`oyqx-uebCJ9Ye*zgosESBUI#QIP zGaZ`jDHMt!f~881D$3yU-^_~VdZEP_uNa|lvY%gVnbYGr*K8&1X3mQjXgcPNBUC_z zP8rV&Kify57C&QRCB!156X54nt8sk_#FtA+*sw0ZLRwnPCCI|yZjOtsO6CiWs-6Xp zeTG7z7-A@aHK4%6=n`(oHkrU_X3hEur3CR_T>^8CQ3O7 zkIASR(QltK#=MFMh8R_ABxHp^8*C!atB-k6C6UOZ9}!jl3cjGdQmxI0DSvQJ$U9PM z`TO@c#xMC`eFGFuyze}!US@gK68DiAYjP5$q?1O%JjN4HpK+StIuugB7O^j5k@WWc zXB{cD#~shDO)Fj7E_QGTRf_iJlGDFvy~pX0Qi*i}-?ylH%_iE+6Gor<>=gQy$IC0N z_xT>ghP?k(pUn!zSfT*$$NOTW!MWZqSQV4^LBwuea?9gkl@kjok_I*7{mh`(pp z7@Lkk1PM*R@!+R@{FH+jqPBd|bM6wm*8lVJ`XI2qi0k@0=pQ?vvHR=0tpC}+?SH`a zT;`Q%;8}-hEsdW)=j(5M{;IWaKHp2$abJbszChdc`fq@YDu5`l3DAB0jCK)ON~?yy zVT}1skp^)Q0Xu&-jdLe2rzai9Cc^b&AWZg=SDx=5kw$Nn-bO`Ch*ClKc)=Z@*89e`%^`3NS&kMC-hy#{Fp%`{J zQnd3>WX2&Md?{NQ+%FIK>fxUMC=|4@7H#khJCAMU;=q<;v5{5&n|Lngt$odM{D4?x zNfbLVh{>Ep17hOkX4OfwSc5p4o^PEZhjMtnbu5bkS4zPVY%5eIBC);6jgCu&+H zuP_GAV=my@$Nw5<5eMU4UW0GM__>JKls}h+O55f4`hix?L7`Ce7rM$jd{|<0aVfoZ zs9fwc9)ky3n3Px&M^wdQ)}x4F$+uhubFFM~8F-h<$(FyDM0LG=L4hks0GYu`ni-iU z=a_Vs@jz$ANf`!^C8Q0p0_A&PCjAo;D|~Nqq;R^(J1*1Q@G+u?wWqXy3TEGLW!0BR zJtNZKh^)wm2mHM_?g|1wmsirfW5Uel|7%EP;uN$6u0ANPJGJKzjWKT!n`eburik{+ zab=SNc^rfL=*#?uW850-YgwdKtz%m`7BeumH^OjVUVmP3oiw+{P!{BfR0kGg1dQAG zJ`vIIcPrHzRWO|Me3naXXE2`L3_qiHdp>_rRJJSPT;Aj`I)8F5IsH13W>6M>OC=9U z8)k%l9166<@U%+VEAL`5v=8XRyP-HtV_e!38TtMB!H{;Gl!`s{-8@X|50oQE8^77n zCQehHfwxr-_sW!guN3>&oXs5u5(zG&N&qa*!<|&`wY2xmC_jVAV2u$lGE3qy2^rx_=u;?mqG#}?t@K1j1XQ!#?ttkf`o6x zY8JOsdV-ZfVjPfpi_a{htvQSdIaWhcV7cO;Opz2u{LTqJx5=~1|0)7!gX?)&lwf8u ziz^k$pkw}xXp^f--yfLUqzk8XE?~?oIKB@=`u!DF zBz<@llwOo>`E!+OEw1;*vKZUPv5LBi)DIWo9k%kj2{M(<(&>A2wTVIiOkVw%=)doQ zs*$S-QNa5yW6Y-=B;69mlTEsEbwD=QPhm{`6gr72l8jq zp6bCaY~q;x%@vfnp@k)ihc~f1w0ck0AV$(ruV)hM^FW4%2|;6=T0glj93w@--h4 zTrsj1c||@opR#K2(I<8gleSk@@z5=~RBi~IeCKdE?Apm7%}T+`3PnnmE4yF}XM|q` zdy}0(DkdAyQ+tU-_@HHg?;>Aa%Dr25yRGj_v5m=n4gLz>Bwk~JW#8&Dd=~OJ*L%Be?sHickLzUHeZgw2y5&b@gWh|sc!uq%e(PB6EL+JgyK7A90k#{!MJV z4x&wtr@Z6d`DyF_1uK2#tBf(1+_naOg?6KgD-lst<(CGp!;ZMY+b^KpT( zdm28j3m}%tBD)~WT)iNkftOHpWftdgfGm{$w)`57|I>&K-jBujqT{%oN-b3=6g`3x zSP>B9KrV){IR_yw(x7CS^}1x)xB1Sa1!LH!dD0eMF{k<)m*3AnC7!vHmApvIL_IM~ed>K_7)vWrB*wJusj zz=eMBIeZjn3&; zk$Prhi&vVt%^+N!@SHz`$H6KlDqPx}Nt2@p#-y?*>E2av-3-RZ5h1C!QO0T}85uT=$c#h^5UU&lR`uG5Aw<`~Ap*W)vf zfz-3fD~Ty6`dIPA_oQb*GBOGF-IZ$Xec~SQv(|nj@%LqXXR;1*@8uzBKw*Z$wqjJF zP>d#Szz*Rfm1>O&Zy1m-XW)y(>ZLdZZ1UFqhSaBltc9EG= zJ_2n2++&)2uUc(pr%=X1Dxaf_A&Zt8UiM4IiVHKs(!|^M()I-Vkc&>_{dokQS9XQw zuwqUMg<=4q1Xc(@GVo1qA!4dTU7ama9vehvJS_;HQ}}#!E;;>^7;Cnw=uaGKvLAx- ziSP)>FyjcSF9{QH=}UA9gq18Dz~{@P;Z4W)HRJMas#I$~WBD#fJO^Uz6~foSK#9Wv z*l&8ab%D2E661pLIF7}>S-*noWDxjs@P7*GsPBY_)nR-OuZz|Fe+_>x2KCu58)JSP z{rQMc-PvP9g2=$PAyv!Qr|!yvmf2Vq96r>3!6xD z_#*B@F5Cop^((ly*`8r8FG9w1^nLbB4s-Pv`y|ZAOrcN=De8OmbyjNA;sL70N*t*8 z8w2ljd?bhXi~T;Gi--$m-$e2|1y#gjO0Kd8BCnTMd@6vc%WSIm6iG(nrJF~+UTk*3NVNGy3wJ>`B}?>)~- zlZc=t3K_I9?NUW~OioW&ilu^B(fK?P5{~P@B-s`o&^fCr;%N@paE(H*lO$0+lIs(= zJ`Z4Ag$1T5v5m|4+a7<B?RN$wQsC_bb)peuYz~AXx0lF<_f)F24Kv^gt^8e`rSLSEH91PS-ID`0&cNTM>v=Ub2h>cR{v8NS+j@8cjDzhV(sgl`jn zvscQdNI!iq2&c;&jDo!Wevm+~c92&Lm*S1_&2!1=->+0_w> zpQH$a%^r8}g5mxT;yd^)xWD`hW6VqOnU6UXf4t8vA1P0L_NmX--jhfq=>D-&uh*ZO zoSb-#shb;#i9|B!_qy5K+1*{mCQ}m=6ZcL`Bwl9fW_NdQZ;iH@m`L0^k(hXyNrXPX zZtd*s29-M}CMLdSVj?jwV}sq@y+5tj>o*dK#L+||k*wG2)yc_;*V)&pUa$Yh=0FySsbd zYM0dZiHXF^6N$tz-)HLe`d{zv?tOA{a$+%I5_j#@_rAEdSFZ+rI5F{Rq0Vz`Y&LiI z_WojWa^j8lTYG!;&+YB)J?-h21AE%t-TMSxJG6Uta&jUdPj_~9|9EO@@(n1UAaP@7 zXZO!|GUfvv-z3b@-QB%?>9JFjlgAt5qi!~X{+~#^3j6(2{v7o~B9XYeUN<-Bd>sAz zDtaG^=>~Rp_g3rm`lfw6dOeW{#;uziw#N6=)Z}qKcN%MZd-V-`FTOUBNW8pWuYZ}& zEoe(3aj&nhc|2QvkTQw=1iRgCbNl|Na6!bVH=wNkw6#A{ zTRkIQd%JZ_?zQGDM-jJt694}ekL%Ap{nw;C|4=vkNzr{Q9!nP2HmYa1V8P { const { epm } = useConfig(); - return epm.enabled ?
hello world - epm app
: null; + const { http } = useCore(); + + if (!epm.enabled) { + return null; + } + + return ( + + + +

+ +

+
+
+ + +

+ +

+
+
+ + } + rightColumn={ + + + + } + tabs={[ + { + id: 'all_packages', + name: 'All packages', + isSelected: true, + }, + { + id: 'installed_packages', + name: 'Installed packages', + }, + ]} + > + hello world - fleet app +
+ ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx index 978414769004d..c4e8c576a1d7d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -4,9 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { WithHeaderLayout } from '../../layouts'; import { useConfig } from '../../hooks'; export const FleetApp: React.FunctionComponent = () => { const { fleet } = useConfig(); - return fleet.enabled ?
hello world - fleet app
: null; + if (!fleet.enabled) { + return null; + } + + return ( + + + +

+ +

+
+
+ + +

+ +

+
+
+ + } + tabs={[ + { + id: 'agents', + name: 'Agents', + isSelected: true, + }, + { + id: 'enrollment_keys', + name: 'Enrollment keys', + }, + ]} + > + hello world - fleet app +
+ ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index da4a78a39e2fe..ea6b045f504ec 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -4,7 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; +import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { WithHeaderLayout } from '../../layouts'; export const IngestManagerOverview: React.FunctionComponent = () => { - return
Ingest manager overview page
; + return ( + + + +

+ +

+
+
+ + +

+ +

+
+
+ + } + /> + ); }; From 63d5e382d888728f6a96eede327c61eaae271989 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Mon, 24 Feb 2020 13:40:33 -0500 Subject: [PATCH 061/113] removes extraHandlers (#58336) --- .../public/embeddable/visualize_embeddable.ts | 4 +--- src/plugins/expressions/public/loader.ts | 6 +++--- src/plugins/expressions/public/render.ts | 4 ++-- src/plugins/expressions/public/types/index.ts | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 72a0ef72b5693..32bbae13b79b8 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -369,9 +369,7 @@ export class VisualizeEmbeddable extends Embeddable { + render = async (data: any, uiState: any = {}) => { if (!data || typeof data !== 'object') { return this.handleRenderError(new Error('invalid data provided to the expression renderer')); } @@ -119,7 +119,7 @@ export class ExpressionRenderHandler { .get(data.as)! .render(this.element, data.value, { ...this.handlers, - ...extraHandlers, + uiState, } as any); } catch (e) { return this.handleRenderError(e); diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index c77698d3661c2..b5781ef213fd0 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -48,7 +48,7 @@ export interface IExpressionLoaderParams { disableCaching?: boolean; customFunctions?: []; customRenderers?: []; - extraHandlers?: Record; + uiState?: unknown; inspectorAdapters?: Adapters; onRenderError?: RenderErrorHandlerFnType; } From d1df0e5da5a642b889bbe626a5ebaad8bf973f9a Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Mon, 24 Feb 2020 20:47:32 +0200 Subject: [PATCH 062/113] [Telemetry] Server backpressure mechanism (#57556) * finish backpressure mechanism * usage fetch method send OPTIONS before sending POST * add unit test for get_telemetry_failure_details * get currentVersion in constructor * fix type check Co-authored-by: Elastic Machine --- .../core_plugins/telemetry/mappings.json | 7 ++ .../core_plugins/telemetry/server/fetcher.ts | 68 ++++++++++--- .../get_telemetry_failure_details.test.ts | 96 +++++++++++++++++++ .../get_telemetry_failure_details.ts | 45 +++++++++ .../server/telemetry_config/index.ts | 4 + .../server/telemetry_repository/index.ts | 2 + src/plugins/telemetry/public/plugin.ts | 12 +-- 7 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.ts diff --git a/src/legacy/core_plugins/telemetry/mappings.json b/src/legacy/core_plugins/telemetry/mappings.json index a88372a5578e8..fa9cc93d6363a 100644 --- a/src/legacy/core_plugins/telemetry/mappings.json +++ b/src/legacy/core_plugins/telemetry/mappings.json @@ -17,6 +17,13 @@ }, "userHasSeenNotice": { "type": "boolean" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "ignore_above": 256, + "type": "keyword" } } } diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts index 6e16328c4abd8..d30ee10066813 100644 --- a/src/legacy/core_plugins/telemetry/server/fetcher.ts +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -21,19 +21,26 @@ import moment from 'moment'; // @ts-ignore import fetch from 'node-fetch'; import { telemetryCollectionManager } from './collection_manager'; -import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config'; +import { + getTelemetryOptIn, + getTelemetrySendUsageFrom, + getTelemetryFailureDetails, +} from './telemetry_config'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository'; import { REPORT_INTERVAL_MS } from '../common/constants'; export class FetcherTask { - private readonly checkDurationMs = 60 * 1000 * 5; + private readonly initialCheckDelayMs = 60 * 1000 * 5; + private readonly checkIntervalMs = 60 * 1000 * 60 * 12; private intervalId?: NodeJS.Timeout; private lastReported?: number; + private currentVersion: string; private isSending = false; private server: any; constructor(server: any) { this.server = server; + this.currentVersion = this.server.config().get('pkg.version'); } private getInternalRepository = () => { @@ -52,6 +59,9 @@ export class FetcherTask { const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); const configTelemetryOptIn = config.get('telemetry.optIn'); const telemetryUrl = config.get('telemetry.url') as string; + const { failureCount, failureVersion } = await getTelemetryFailureDetails({ + telemetrySavedObject, + }); return { telemetryOptIn: getTelemetryOptIn({ @@ -65,6 +75,8 @@ export class FetcherTask { configTelemetrySendUsageFrom, }), telemetryUrl, + failureCount, + failureVersion, }; }; @@ -72,11 +84,31 @@ export class FetcherTask { const internalRepository = this.getInternalRepository(); this.lastReported = Date.now(); updateTelemetrySavedObject(internalRepository, { + reportFailureCount: 0, lastReported: this.lastReported, }); }; - private shouldSendReport = ({ telemetryOptIn, telemetrySendUsageFrom }: any) => { + private updateReportFailure = async ({ failureCount }: { failureCount: number }) => { + const internalRepository = this.getInternalRepository(); + + updateTelemetrySavedObject(internalRepository, { + reportFailureCount: failureCount + 1, + reportFailureVersion: this.currentVersion, + }); + }; + + private shouldSendReport = ({ + telemetryOptIn, + telemetrySendUsageFrom, + reportFailureCount, + currentVersion, + reportFailureVersion, + }: any) => { + if (reportFailureCount > 2 && reportFailureVersion === currentVersion) { + return false; + } + if (telemetryOptIn && telemetrySendUsageFrom === 'server') { if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) { return true; @@ -98,6 +130,14 @@ export class FetcherTask { private sendTelemetry = async (url: string, cluster: any): Promise => { this.server.log(['debug', 'telemetry', 'fetcher'], `Sending usage stats.`); + /** + * send OPTIONS before sending usage data. + * OPTIONS is less intrusive as it does not contain any payload and is used here to check if the endpoint is reachable. + */ + await fetch(url, { + method: 'options', + }); + await fetch(url, { method: 'post', body: cluster, @@ -108,21 +148,23 @@ export class FetcherTask { if (this.isSending) { return; } - try { - const telemetryConfig = await this.getCurrentConfigs(); - if (!this.shouldSendReport(telemetryConfig)) { - return; - } + const telemetryConfig = await this.getCurrentConfigs(); + if (!this.shouldSendReport(telemetryConfig)) { + return; + } - // mark that we are working so future requests are ignored until we're done + try { this.isSending = true; const clusters = await this.fetchTelemetry(); + const { telemetryUrl } = telemetryConfig; for (const cluster of clusters) { - await this.sendTelemetry(telemetryConfig.telemetryUrl, cluster); + await this.sendTelemetry(telemetryUrl, cluster); } await this.updateLastReported(); } catch (err) { + await this.updateReportFailure(telemetryConfig); + this.server.log( ['warning', 'telemetry', 'fetcher'], `Error sending telemetry usage data: ${err}` @@ -132,8 +174,12 @@ export class FetcherTask { }; public start = () => { - this.intervalId = setInterval(() => this.sendIfDue(), this.checkDurationMs); + setTimeout(() => { + this.sendIfDue(); + this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs); + }, this.initialCheckDelayMs); }; + public stop = () => { if (this.intervalId) { clearInterval(this.intervalId); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts new file mode 100644 index 0000000000000..c92696838e8e8 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { getTelemetryFailureDetails } from './get_telemetry_failure_details'; + +describe('getTelemetryFailureDetails: get details about server usage fetcher failures', () => { + it('returns `failureCount: 0` and `failureVersion: undefined` when telemetry does not have any custom configs in saved Object', () => { + const telemetrySavedObject = null; + const failureDetails = getTelemetryFailureDetails({ telemetrySavedObject }); + expect(failureDetails).toStrictEqual({ + failureVersion: undefined, + failureCount: 0, + }); + }); + + it('returns telemetryFailureCount and reportFailureVersion from telemetry saved Object', () => { + const telemetrySavedObject = { + reportFailureCount: 12, + reportFailureVersion: '8.0.0', + }; + const failureDetails = getTelemetryFailureDetails({ telemetrySavedObject }); + expect(failureDetails).toStrictEqual({ + failureVersion: '8.0.0', + failureCount: 12, + }); + }); + + it('returns `failureCount: 0` on malformed reportFailureCount telemetry saved Object', () => { + const failureVersion = '8.0.0'; + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureCount: null, + reportFailureVersion: failureVersion, + } as any, + }) + ).toStrictEqual({ failureVersion, failureCount: 0 }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureCount: undefined, + reportFailureVersion: failureVersion, + }, + }) + ).toStrictEqual({ failureVersion, failureCount: 0 }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureCount: 'not_a_number', + reportFailureVersion: failureVersion, + } as any, + }) + ).toStrictEqual({ failureVersion, failureCount: 0 }); + }); + + it('returns `failureVersion: undefined` on malformed reportFailureCount telemetry saved Object', () => { + const failureCount = 0; + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureVersion: null, + reportFailureCount: failureCount, + } as any, + }) + ).toStrictEqual({ failureCount, failureVersion: undefined }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { reportFailureVersion: undefined, reportFailureCount: failureCount }, + }) + ).toStrictEqual({ failureCount, failureVersion: undefined }); + expect( + getTelemetryFailureDetails({ + telemetrySavedObject: { + reportFailureVersion: 123, + reportFailureCount: failureCount, + } as any, + }) + ).toStrictEqual({ failureCount, failureVersion: undefined }); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.ts new file mode 100644 index 0000000000000..2952fa96a5cf3 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_failure_details.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 { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; + +interface GetTelemetryFailureDetailsConfig { + telemetrySavedObject: TelemetrySavedObject; +} + +export interface TelemetryFailureDetails { + failureCount: number; + failureVersion?: string; +} + +export function getTelemetryFailureDetails({ + telemetrySavedObject, +}: GetTelemetryFailureDetailsConfig): TelemetryFailureDetails { + if (!telemetrySavedObject) { + return { + failureVersion: undefined, + failureCount: 0, + }; + } + const { reportFailureCount, reportFailureVersion } = telemetrySavedObject; + + return { + failureCount: typeof reportFailureCount === 'number' ? reportFailureCount : 0, + failureVersion: typeof reportFailureVersion === 'string' ? reportFailureVersion : undefined, + }; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts index ab30dac1c3666..bf9855ce7538e 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts @@ -21,3 +21,7 @@ export { replaceTelemetryInjectedVars } from './replace_injected_vars'; export { getTelemetryOptIn } from './get_telemetry_opt_in'; export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; +export { + getTelemetryFailureDetails, + TelemetryFailureDetails, +} from './get_telemetry_failure_details'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts index b9ba2ce5573c3..f1735d1bb2866 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts @@ -27,4 +27,6 @@ export interface TelemetrySavedObjectAttributes { lastReported?: number; telemetryAllowChangingOptInStatus?: boolean; userHasSeenNotice?: boolean; + reportFailureCount?: number; + reportFailureVersion?: string; } diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 7ba51cacd1949..9cfb4ca1ec395 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -54,9 +54,6 @@ export class TelemetryPlugin implements Plugin Date: Mon, 24 Feb 2020 13:24:46 -0600 Subject: [PATCH 063/113] Show blank lines instead of N/A on service map popovers (#57014) * Show blank lines instead of N/A on service map popovers Because RUM agents never show CPU or memory usage, we don't want to always show the metric with N/A. If a metric is null, just hide the lines. Break up the display and fetching components and update the popover stories to show the list. Update some types. * Fix metric typings --- .../app/ServiceMap/Popover/Contents.tsx | 4 +- .../ServiceMap/Popover/Popover.stories.tsx | 86 +++++++++-------- .../Popover/ServiceMetricFetcher.tsx | 41 +++++++++ .../ServiceMap/Popover/ServiceMetricList.tsx | 92 ++++++------------- x-pack/plugins/apm/common/service_map.ts | 9 ++ .../get_service_map_service_node_info.ts | 31 ++++--- 6 files changed, 147 insertions(+), 116 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 378ad9509c217..f1c53673c8755 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -14,7 +14,7 @@ import cytoscape from 'cytoscape'; import React from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; -import { ServiceMetricList } from './ServiceMetricList'; +import { ServiceMetricFetcher } from './ServiceMetricFetcher'; const popoverMinWidth = 280; @@ -49,7 +49,7 @@ export function Contents({ {isService ? ( - + ) : ( )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index b26488c5ef7de..e5962afd76eb8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -6,44 +6,50 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { - ApmPluginContext, - ApmPluginContextValue -} from '../../../../context/ApmPluginContext'; -import { Contents } from './Contents'; +import { ServiceMetricList } from './ServiceMetricList'; -const selectedNodeData = { - id: 'opbeans-node', - label: 'opbeans-node', - href: - '#/services/opbeans-node/service-map?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', - agentName: 'nodejs', - type: 'service' -}; - -storiesOf('app/ServiceMap/Popover/Contents', module).add( - 'example', - () => { - return ( - - {}} - selectedNodeServiceName="opbeans-node" - /> - - ); - }, - { - info: { - propTablesExclude: [ApmPluginContext.Provider], - source: false - } - } -); +storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) + .add('example', () => ( + + )) + .add('loading', () => ( + + )) + .add('some null values', () => ( + + )) + .add('all null values', () => ( + + )); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx new file mode 100644 index 0000000000000..b0a5e892b5a7e --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { ServiceMetricList } from './ServiceMetricList'; + +interface ServiceMetricFetcherProps { + serviceName: string; +} + +export function ServiceMetricFetcher({ + serviceName +}: ServiceMetricFetcherProps) { + const { + urlParams: { start, end, environment } + } = useUrlParams(); + + const { data = {} as ServiceNodeMetrics, status } = useFetcher( + callApmApi => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/service-map/service/{serviceName}', + params: { path: { serviceName }, query: { start, end, environment } } + }); + } + }, + [serviceName, start, end, environment], + { + preservePreviousData: false + } + ); + const isLoading = status === 'loading'; + + return ; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index e91eb5e006d82..50ce918ea7037 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -5,26 +5,23 @@ */ import { + EuiBadge, EuiFlexGroup, - EuiLoadingSpinner, EuiFlexItem, - EuiBadge + EuiLoadingSpinner } from '@elastic/eui'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/server/lib/service_map/get_service_map_service_node_info'; +import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; import { asDuration, asPercent, toMicroseconds, tpmUnit } from '../../../../utils/formatters'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; function LoadingSpinner() { return ( @@ -51,53 +48,19 @@ const ItemDescription = styled('td')` text-align: right; `; -const na = i18n.translate('xpack.apm.serviceMap.NotAvailableMetric', { - defaultMessage: 'N/A' -}); - -interface MetricListProps { - serviceName: string; +interface ServiceMetricListProps extends ServiceNodeMetrics { + isLoading: boolean; } -export function ServiceMetricList({ serviceName }: MetricListProps) { - const { - urlParams: { start, end, environment } - } = useUrlParams(); - - const { data = {} as ServiceNodeMetrics, status } = useFetcher( - callApmApi => { - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/service-map/service/{serviceName}', - params: { - path: { - serviceName - }, - query: { - start, - end, - environment - } - } - }); - } - }, - [serviceName, start, end, environment], - { - preservePreviousData: false - } - ); - - const { - avgTransactionDuration, - avgRequestsPerMinute, - avgErrorsPerMinute, - avgCpuUsage, - avgMemoryUsage, - numInstances - } = data; - const isLoading = status === 'loading'; - +export function ServiceMetricList({ + avgTransactionDuration, + avgRequestsPerMinute, + avgErrorsPerMinute, + avgCpuUsage, + avgMemoryUsage, + numInstances, + isLoading +}: ServiceMetricListProps) { const listItems = [ { title: i18n.translate( @@ -108,7 +71,7 @@ export function ServiceMetricList({ serviceName }: MetricListProps) { ), description: isNumber(avgTransactionDuration) ? asDuration(toMicroseconds(avgTransactionDuration, 'milliseconds')) - : na + : null }, { title: i18n.translate( @@ -119,7 +82,7 @@ export function ServiceMetricList({ serviceName }: MetricListProps) { ), description: isNumber(avgRequestsPerMinute) ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}` - : na + : null }, { title: i18n.translate( @@ -128,13 +91,13 @@ export function ServiceMetricList({ serviceName }: MetricListProps) { defaultMessage: 'Errors per minute (avg.)' } ), - description: avgErrorsPerMinute?.toFixed(2) ?? na + description: avgErrorsPerMinute?.toFixed(2) }, { title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverMetric', { defaultMessage: 'CPU usage (avg.)' }), - description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : na + description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : null }, { title: i18n.translate( @@ -143,7 +106,9 @@ export function ServiceMetricList({ serviceName }: MetricListProps) { defaultMessage: 'Memory usage (avg.)' } ), - description: isNumber(avgMemoryUsage) ? asPercent(avgMemoryUsage, 1) : na + description: isNumber(avgMemoryUsage) + ? asPercent(avgMemoryUsage, 1) + : null } ]; return isLoading ? ( @@ -165,12 +130,15 @@ export function ServiceMetricList({ serviceName }: MetricListProps) { - {listItems.map(({ title, description }) => ( - - {title} - {description} - - ))} + {listItems.map( + ({ title, description }) => + description && ( + + {title} + {description} + + ) + )}
diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index fbaa489c45039..528aec2f70ad9 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -21,3 +21,12 @@ export interface Connection { source: ConnectionNode; destination: ConnectionNode; } + +export interface ServiceNodeMetrics { + numInstances: number; + avgMemoryUsage: number | null; + avgCpuUsage: number | null; + avgTransactionDuration: number | null; + avgRequestsPerMinute: number | null; + avgErrorsPerMinute: number | null; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 6c4d540103cec..0fe825e8ace35 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -18,7 +18,6 @@ import { SERVICE_NODE_NAME } from '../../../common/elasticsearch_fieldnames'; import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; -import { PromiseReturnType } from '../../../typings/common'; interface Options { setup: Setup & SetupTimeRange; @@ -32,10 +31,6 @@ interface TaskParameters { filter: ESFilter[]; } -export type ServiceNodeMetrics = PromiseReturnType< - typeof getServiceMapServiceNodeInfo ->; - export async function getServiceMapServiceNodeInfo({ serviceName, environment, @@ -112,7 +107,10 @@ async function getTransactionMetrics({ setup, filter, minutes -}: TaskParameters) { +}: TaskParameters): Promise<{ + avgTransactionDuration: number | null; + avgRequestsPerMinute: number | null; +}> { const { indices, client } = setup; const response = await client.search({ @@ -140,13 +138,16 @@ async function getTransactionMetrics({ }); return { - avgTransactionDuration: response.aggregations?.duration.value, + avgTransactionDuration: response.aggregations?.duration.value ?? null, avgRequestsPerMinute: response.hits.total.value > 0 ? response.hits.total.value / minutes : null }; } -async function getCpuMetrics({ setup, filter }: TaskParameters) { +async function getCpuMetrics({ + setup, + filter +}: TaskParameters): Promise<{ avgCpuUsage: number | null }> { const { indices, client } = setup; const response = await client.search({ @@ -180,11 +181,14 @@ async function getCpuMetrics({ setup, filter }: TaskParameters) { }); return { - avgCpuUsage: response.aggregations?.avgCpuUsage.value + avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null }; } -async function getMemoryMetrics({ setup, filter }: TaskParameters) { +async function getMemoryMetrics({ + setup, + filter +}: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { const { client, indices } = setup; const response = await client.search({ index: indices['apm_oss.metricsIndices'], @@ -221,11 +225,14 @@ async function getMemoryMetrics({ setup, filter }: TaskParameters) { }); return { - avgMemoryUsage: response.aggregations?.avgMemoryUsage.value + avgMemoryUsage: response.aggregations?.avgMemoryUsage.value ?? null }; } -async function getNumInstances({ setup, filter }: TaskParameters) { +async function getNumInstances({ + setup, + filter +}: TaskParameters): Promise<{ numInstances: number }> { const { client, indices } = setup; const response = await client.search({ index: indices['apm_oss.transactionIndices'], From 858fe2e9251e748bfbcd85623f22d9c1eac71076 Mon Sep 17 00:00:00 2001 From: Maggie Ghamry <46542915+maggieghamry@users.noreply.github.com> Date: Mon, 24 Feb 2020 14:44:19 -0500 Subject: [PATCH 064/113] [Canvas] Toggles footer editable controls when you turn off edit mode #52786 (#58044) * Bug fix update update to toggle footer edit controls "off" when locking edit controls. * Update to toggle Update to toggle, so that only the "Expression Editor" and tray are hidden when locking controls. * Update to toggle logic Added canUserWrite property, and added a condition to ensure the "page" tray still shows once the Expression editor is locked * Update to property definition Update to consolidate isWriteable and canUserWrite into one variable instead of two. * Adding Test (in progress) code Adding Toolbar test code (so far) - needs to be completed to address nested compononet storybook issue * Adding issue link Adding issue link in TODO comments Co-authored-by: Elastic Machine --- .../toolbar/__examples__/toolbar.stories.tsx | 39 +++++++++++++++++++ .../canvas/public/components/toolbar/index.js | 3 ++ .../public/components/toolbar/toolbar.tsx | 11 +++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx new file mode 100644 index 0000000000000..5907c932ddabb --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/toolbar/__examples__/toolbar.stories.tsx @@ -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. + */ + +/* + TODO: uncomment and fix this test to address storybook errors as a result of nested component dependencies - https://github.com/elastic/kibana/issues/58289 + */ + +/* +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { Toolbar } from '../toolbar'; + +storiesOf('components/Toolbar', module) + .addDecorator(story => ( +
+ {story()} +
+ )) + .add('with null metric', () => ( + + )); +*/ diff --git a/x-pack/legacy/plugins/canvas/public/components/toolbar/index.js b/x-pack/legacy/plugins/canvas/public/components/toolbar/index.js index c834304739a4c..294a44ba0415a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/toolbar/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/toolbar/index.js @@ -7,12 +7,14 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { pure, compose, withState, getContext, withHandlers } from 'recompose'; +import { canUserWrite } from '../../state/selectors/app'; import { getWorkpad, getWorkpadName, getSelectedPageIndex, getSelectedElement, + isWriteable, } from '../../state/selectors/workpad'; import { Toolbar as Component } from './toolbar'; @@ -23,6 +25,7 @@ const mapStateToProps = state => ({ totalPages: getWorkpad(state).pages.length, selectedPageNumber: getSelectedPageIndex(state) + 1, selectedElement: getSelectedElement(state), + isWriteable: isWriteable(state) && canUserWrite(state), }); export const Toolbar = compose( diff --git a/x-pack/legacy/plugins/canvas/public/components/toolbar/toolbar.tsx b/x-pack/legacy/plugins/canvas/public/components/toolbar/toolbar.tsx index 089f021ccdc32..0f8204e6bc261 100644 --- a/x-pack/legacy/plugins/canvas/public/components/toolbar/toolbar.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/toolbar/toolbar.tsx @@ -39,7 +39,8 @@ enum TrayType { interface Props { workpadName: string; - + isWriteable: boolean; + canUserWrite: boolean; tray: TrayType | null; setTray: (tray: TrayType | null) => void; @@ -66,12 +67,17 @@ export const Toolbar = (props: Props) => { totalPages, showWorkpadManager, setShowWorkpadManager, + isWriteable, } = props; const elementIsSelected = Boolean(selectedElement); const done = () => setTray(null); + if (!isWriteable && tray === TrayType.expression) { + done(); + } + const showHideTray = (exp: TrayType) => { if (tray && tray === exp) { return done(); @@ -135,7 +141,7 @@ export const Toolbar = (props: Props) => { />
- {elementIsSelected && ( + {elementIsSelected && isWriteable && ( Date: Mon, 24 Feb 2020 20:09:38 +0000 Subject: [PATCH 065/113] chore(NA): remove empty filter check from thread loader pool config getter (#58385) --- src/optimize/base_optimizer.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index a833204eaa0e2..a94f251c292f9 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -152,11 +152,7 @@ export default class BaseOptimizer { getThreadLoaderPoolConfig() { // Calculate the node options from the NODE_OPTIONS env var - const parsedNodeOptions = process.env.NODE_OPTIONS - ? // thread-loader could not receive empty string as options - // or it would break that's why we need to filter here - process.env.NODE_OPTIONS.split(/\s/).filter(opt => !!opt) - : []; + const parsedNodeOptions = process.env.NODE_OPTIONS ? process.env.NODE_OPTIONS.split(/\s/) : []; return { name: 'optimizer-thread-loader-main-pool', From 77fe83e7db27b410cd1eb63084c22ca7bb32d278 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 24 Feb 2020 13:04:30 -0800 Subject: [PATCH 066/113] Remove restriction that route must start with `/api` to use api authorization (#58351) Co-authored-by: Elastic Machine --- .../authorization/api_authorization.test.ts | 36 +++++-------------- .../server/authorization/api_authorization.ts | 4 +-- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/api_authorization.test.ts b/x-pack/plugins/security/server/authorization/api_authorization.test.ts index a5902f251b082..409f998cfe8d2 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.test.ts @@ -15,27 +15,7 @@ import { import { authorizationMock } from './index.mock'; describe('initAPIAuthorization', () => { - test(`route that doesn't start with "/api/" continues`, async () => { - const mockHTTPSetup = coreMock.createSetup().http; - initAPIAuthorization( - mockHTTPSetup, - authorizationMock.create(), - loggingServiceMock.create().get() - ); - - const [[postAuthHandler]] = mockHTTPSetup.registerOnPostAuth.mock.calls; - - const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', path: '/app/foo' }); - const mockResponse = httpServerMock.createResponseFactory(); - const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit(); - - await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit); - - expect(mockResponse.notFound).not.toHaveBeenCalled(); - expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); - }); - - test(`protected route that starts with "/api/", but "mode.useRbacForRequest()" returns false continues`, async () => { + test(`protected route when "mode.useRbacForRequest()" returns false continues`, async () => { const mockHTTPSetup = coreMock.createSetup().http; const mockAuthz = authorizationMock.create(); initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); @@ -44,7 +24,7 @@ describe('initAPIAuthorization', () => { const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', - path: '/api/foo', + path: '/foo/bar', routeTags: ['access:foo'], }); const mockResponse = httpServerMock.createResponseFactory(); @@ -59,7 +39,7 @@ describe('initAPIAuthorization', () => { expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); - test(`unprotected route that starts with "/api/", but "mode.useRbacForRequest()" returns true continues`, async () => { + test(`unprotected route when "mode.useRbacForRequest()" returns true continues`, async () => { const mockHTTPSetup = coreMock.createSetup().http; const mockAuthz = authorizationMock.create(); initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); @@ -68,7 +48,7 @@ describe('initAPIAuthorization', () => { const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', - path: '/api/foo', + path: '/foo/bar', routeTags: ['not-access:foo'], }); const mockResponse = httpServerMock.createResponseFactory(); @@ -83,7 +63,7 @@ describe('initAPIAuthorization', () => { expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); - test(`protected route that starts with "/api/", "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { + test(`protected route when "mode.useRbacForRequest()" returns true and user is authorized continues`, async () => { const mockHTTPSetup = coreMock.createSetup().http; const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); @@ -93,7 +73,7 @@ describe('initAPIAuthorization', () => { const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', - path: '/api/foo', + path: '/foo/bar', headers, routeTags: ['access:foo'], }); @@ -118,7 +98,7 @@ describe('initAPIAuthorization', () => { expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); - test(`protected route that starts with "/api/", "mode.useRbacForRequest()" returns true and user isn't authorized responds with a 404`, async () => { + test(`protected route when "mode.useRbacForRequest()" returns true and user isn't authorized responds with a 404`, async () => { const mockHTTPSetup = coreMock.createSetup().http; const mockAuthz = authorizationMock.create({ version: '1.0.0-zeta1' }); initAPIAuthorization(mockHTTPSetup, mockAuthz, loggingServiceMock.create().get()); @@ -128,7 +108,7 @@ describe('initAPIAuthorization', () => { const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'get', - path: '/api/foo', + path: '/foo/bar', headers, routeTags: ['access:foo'], }); diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index b280cc74c230f..cc672fbc69e06 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -13,8 +13,8 @@ export function initAPIAuthorization( logger: Logger ) { http.registerOnPostAuth(async (request, response, toolkit) => { - // if the api doesn't start with "/api/" or we aren't using RBAC for this request, just continue - if (!request.url.path!.startsWith('/api/') || !mode.useRbacForRequest(request)) { + // if we aren't using RBAC for this request, just continue + if (!mode.useRbacForRequest(request)) { return toolkit.next(); } From 7e087633d26dfebe5cf262bbfde2cd4c29770464 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 24 Feb 2020 14:44:03 -0700 Subject: [PATCH 067/113] Remove unused indexPattern:fieldMapping:lookBack advanced setting (#58147) * Remove unused indexPattern:fieldMapping:lookBack advanced setting * Remove unused translations Co-authored-by: Elastic Machine --- docs/management/advanced-options.asciidoc | 2 -- src/legacy/core_plugins/kibana/ui_setting_defaults.js | 11 ----------- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 4 files changed, 17 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 80c9053dc5ae6..9d4052bbd0156 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -62,8 +62,6 @@ mentioned use "\_default_". `histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values when necessary. `history:limit`:: In fields that have history, such as query inputs, show this many recent values. -`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, -look for this many recent matching patterns from which to query the field mapping. `indexPattern:placeholder`:: The default placeholder value to use in Management > Index Patterns > Create Index Pattern. `metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index f92694eabe58d..c0628b72c2ce7 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -690,17 +690,6 @@ export function getUiSettingDefaults() { 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', }), }, - 'indexPattern:fieldMapping:lookBack': { - name: i18n.translate('kbn.advancedSettings.indexPattern.recentMatchingTitle', { - defaultMessage: 'Recent matching patterns', - }), - value: 5, - description: i18n.translate('kbn.advancedSettings.indexPattern.recentMatchingText', { - defaultMessage: - 'For index patterns containing timestamps in their names, look for this many recent matching ' + - 'patterns from which to query the field mapping', - }), - }, 'format:defaultTypeMap': { name: i18n.translate('kbn.advancedSettings.format.defaultTypeMapTitle', { defaultMessage: 'Field type format name', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 78bb39dd22dea..dc97a96d4fcba 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -878,8 +878,6 @@ "kbn.advancedSettings.histogram.maxBarsTitle": "最高バー数", "kbn.advancedSettings.historyLimitText": "履歴があるフィールド (例: クエリインプット) に個の数の最近の値が表示されます", "kbn.advancedSettings.historyLimitTitle": "履歴制限数", - "kbn.advancedSettings.indexPattern.recentMatchingText": "名前にタイムスタンプが含まれているインデックスパターンで、フィールドマッチングをクエリする最近の一致したパターンが、この数検索されます", - "kbn.advancedSettings.indexPattern.recentMatchingTitle": "最近一致したパターン", "kbn.advancedSettings.indexPatternPlaceholderText": "「管理 > インデックスパターン > インデックスパターンを作成」で使用される「インデックスパターン名」フィールドのプレースホルダーです。", "kbn.advancedSettings.indexPatternPlaceholderTitle": "インデックスパターンのプレースホルダー", "kbn.advancedSettings.maxBucketsText": "1 つのデータソースが返せるバケットの最大数です", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc9dacf0e50f7..2532cdb0c4d07 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -878,8 +878,6 @@ "kbn.advancedSettings.histogram.maxBarsTitle": "最大条形数", "kbn.advancedSettings.historyLimitText": "在具有历史记录(例如查询输入)的字段中,显示此数目的最近值", "kbn.advancedSettings.historyLimitTitle": "历史记录限制", - "kbn.advancedSettings.indexPattern.recentMatchingText": "对于名称中包含时间戳的索引模式,寻找此数目的最近匹配模式,以从其中查询字段映射", - "kbn.advancedSettings.indexPattern.recentMatchingTitle": "最近匹配模式", "kbn.advancedSettings.indexPatternPlaceholderText": "在“管理 > 索引模式 > 创建索引模式”中“索引模式名称”的占位符。", "kbn.advancedSettings.indexPatternPlaceholderTitle": "索引模式占位符", "kbn.advancedSettings.maxBucketsText": "单个数据源可以返回的最大存储桶数目", From 13eacb51f0fd030f123c05e68c545bdb99f6ac70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 24 Feb 2020 23:43:40 +0100 Subject: [PATCH 068/113] [APM] Stabilize agent configuration API (#57767) --- docs/apm/api.asciidoc | 260 ++++++++++++++++++ docs/apm/index.asciidoc | 2 + .../AddEditFlyout/DeleteButton.tsx | 10 +- .../AddEditFlyout/index.tsx | 2 +- .../AddEditFlyout/saveConfig.ts | 30 +- x-pack/legacy/plugins/apm/readme.md | 22 ++ .../index.test.ts | 41 +++ .../agent_configuration_intake_rt/index.ts | 26 ++ .../apm/server/lib/helpers/es_client.ts | 7 +- .../lib/services/get_service_node_metadata.ts | 4 +- .../__snapshots__/queries.test.ts.snap | 234 ++++++++++------ .../configuration_types.d.ts | 19 +- .../create_or_update_configuration.ts | 22 +- .../find_exact_configuration.ts | 46 ++++ .../get_agent_name_by_service.ts | 6 +- .../agent_configuration/queries.test.ts | 146 ++++++---- .../{search.ts => search_configurations.ts} | 35 ++- .../apm/server/routes/create_apm_api.ts | 6 +- .../routes/settings/agent_configuration.ts | 137 ++++----- .../apis/apm/agent_configuration.ts | 163 ++++++----- .../apis/apm/feature_controls.ts | 42 +-- x-pack/test/api_integration/apis/apm/index.ts | 2 +- x-pack/test/functional/apps/apm/index.ts | 2 +- 23 files changed, 894 insertions(+), 370 deletions(-) create mode 100644 docs/apm/api.asciidoc create mode 100644 x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts create mode 100644 x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts create mode 100644 x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts rename x-pack/plugins/apm/server/lib/settings/agent_configuration/{search.ts => search_configurations.ts} (68%) diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc new file mode 100644 index 0000000000000..b520cc46bef8d --- /dev/null +++ b/docs/apm/api.asciidoc @@ -0,0 +1,260 @@ +[role="xpack"] +[[apm-api]] +== API + +Some APM app features are provided via a REST API: + +* <> + +TIP: Kibana provides additional <>, +and general information on <>. + +//// +******************************************************* +//// + +[[agent-config-api]] +=== Agent Configuration API + +The Agent configuration API allows you to fine-tune your APM agent configuration, +without needing to redeploy your application. + +The following Agent configuration APIs are available: + +* <> to create or update an Agent configuration +* <> to delete an Agent configuration. +* <> to list all Agent configurations. +* <> to search for an Agent configuration. + +//// +******************************************************* +//// + +[[apm-update-config]] +==== Create or update configuration + +[[apm-update-config-req]] +===== Request + +`PUT /api/apm/settings/agent-configuration` + +[[apm-update-config-req-body]] +===== Request body + +`service`:: +(required, object) Service identifying the configuration to create or update. + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + +`settings`:: +(required) Key/value object with settings and their corresponding value. + +`agent_name`:: +(optional) The agent name is used by the UI to determine which settings to display. + + +[[apm-update-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +PUT /api/apm/settings/agent-configuration +{ + "service" : { + "name" : "frontend", + "environment" : "production" + }, + "settings" : { + "transaction_sample_rate" : 0.4, + "capture_body" : "off", + "transaction_max_spans" : 500 + }, + "agent_name": "nodejs" +} +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-delete-config]] +==== Delete configuration + +[[apm-delete-config-req]] +===== Request + +`DELETE /api/apm/settings/agent-configuration` + +[[apm-delete-config-req-body]] +===== Request body +`service`:: +(required, object) Service identifying the configuration to delete + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + + +[[apm-delete-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +DELETE /api/apm/settings/agent-configuration +{ + "service" : { + "name" : "frontend", + "environment": "production" + } +} +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-list-config]] +==== List configuration + + +[[apm-list-config-req]] +===== Request + +`GET /api/apm/settings/agent-configuration` + +[[apm-list-config-body]] +===== Response body + +[source,js] +-------------------------------------------------- +[ + { + "agent_name": "go", + "service": { + "name": "opbeans-go", + "environment": "production" + }, + "settings": { + "transaction_sample_rate": 1, + "capture_body": "off", + "transaction_max_spans": 200 + }, + "@timestamp": 1581934104843, + "applied_by_agent": false, + "etag": "1e58c178efeebae15c25c539da740d21dee422fc" + }, + { + "agent_name": "go", + "service": { + "name": "opbeans-go" + }, + "settings": { + "transaction_sample_rate": 1, + "capture_body": "off", + "transaction_max_spans": 300 + }, + "@timestamp": 1581934111727, + "applied_by_agent": false, + "etag": "3eed916d3db434d9fb7f039daa681c7a04539a64" + }, + { + "agent_name": "nodejs", + "service": { + "name": "frontend" + }, + "settings": { + "transaction_sample_rate": 1, + }, + "@timestamp": 1582031336265, + "applied_by_agent": false, + "etag": "5080ed25785b7b19f32713681e79f46996801a5b" + } +] +-------------------------------------------------- + +[[apm-list-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +GET /api/apm/settings/agent-configuration +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-search-config]] +==== Search configuration + +[[apm-search-config-req]] +===== Request + +`POST /api/apm/settings/agent-configuration/search` + +[[apm-search-config-req-body]] +===== Request body + +`service`:: +(required, object) Service identifying the configuration. + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + +`etag`:: +(required) etag is sent by the agent to indicate the etag of the last successfully applied configuration. If the etag matches an existing configuration its `applied_by_agent` property will be set to `true`. Every time a configuration is edited `applied_by_agent` is reset to `false`. + +[[apm-search-config-body]] +===== Response body + +[source,js] +-------------------------------------------------- +{ + "_index": ".apm-agent-configuration", + "_id": "CIaqXXABmQCdPphWj8EJ", + "_score": 2, + "_source": { + "agent_name": "nodejs", + "service": { + "name": "frontend" + }, + "settings": { + "transaction_sample_rate": 1, + }, + "@timestamp": 1582031336265, + "applied_by_agent": false, + "etag": "5080ed25785b7b19f32713681e79f46996801a5b" + } +} +-------------------------------------------------- + +[[apm-search-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +POST /api/apm/settings/agent-configuration/search +{ + "etag" : "1e58c178efeebae15c25c539da740d21dee422fc", + "service" : { + "name" : "frontend", + "environment": "production" + } +} +-------------------------------------------------- + +//// +******************************************************* +//// diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index 7eb7278cf0358..d3f0dc5b7f11f 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -24,3 +24,5 @@ include::getting-started.asciidoc[] include::bottlenecks.asciidoc[] include::using-the-apm-ui.asciidoc[] + +include::api.asciidoc[] 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 index 496147b02589b..1564f1ae746a9 100644 --- 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 @@ -51,12 +51,18 @@ async function deleteConfig( ) { try { await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', + pathname: '/api/apm/settings/agent-configuration', method: 'DELETE', params: { - path: { configurationId: selectedConfig.id } + body: { + service: { + name: selectedConfig.service.name, + environment: selectedConfig.service.environment + } + } } }); + toasts.addSuccess({ title: i18n.translate( 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle', 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 index 653dedea733f2..c77617fbb424f 100644 --- 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 @@ -135,8 +135,8 @@ export function AddEditFlyout({ sampleRate, captureBody, transactionMaxSpans, - configurationId: selectedConfig ? selectedConfig.id : undefined, agentName, + isExistingConfig: Boolean(selectedConfig), toasts, trackApmEvent }); 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 index 19934cafb4694..d36120a054795 100644 --- 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 @@ -27,8 +27,8 @@ export async function saveConfig({ sampleRate, captureBody, transactionMaxSpans, - configurationId, agentName, + isExistingConfig, toasts, trackApmEvent }: { @@ -38,8 +38,8 @@ export async function saveConfig({ sampleRate: string; captureBody: string; transactionMaxSpans: string; - configurationId?: string; agentName?: string; + isExistingConfig: boolean; toasts: NotificationsStart['toasts']; trackApmEvent: UiTracker; }) { @@ -64,24 +64,14 @@ export async function saveConfig({ 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 - } - }); - } + await callApmApi({ + pathname: '/api/apm/settings/agent-configuration', + method: 'PUT', + params: { + query: { overwrite: isExistingConfig }, + body: configuration + } + }); toasts.addSuccess({ title: i18n.translate( diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index 2106243d12aea..a513249c296db 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -71,6 +71,28 @@ node scripts/jest.js plugins/apm --watch node scripts/jest.js plugins/apm --updateSnapshot ``` +### Functional tests + +**Start server** +`node scripts/functional_tests_server --config x-pack/test/functional/config.js` + +**Run tests** +`node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs'` + +APM tests are located in `x-pack/test/functional/apps/apm`. +For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + +### API integration tests + +**Start server** +`node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` + +**Run tests** +`node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs'` + +APM tests are located in `x-pack/test/api_integration/apis/apm`. +For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + ### Linting _Note: Run the following commands from `kibana/`._ diff --git a/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts new file mode 100644 index 0000000000000..4c9dc78eb41e9 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { agentConfigurationIntakeRt } from './index'; +import { isRight } from 'fp-ts/lib/Either'; + +describe('agentConfigurationIntakeRt', () => { + it('is valid when required parameters are given', () => { + const config = { + service: {}, + settings: {} + }; + + expect(isConfigValid(config)).toBe(true); + }); + + it('is valid when required and optional parameters are given', () => { + const config = { + service: { name: 'my-service', environment: 'my-environment' }, + settings: { + transaction_sample_rate: 0.5, + capture_body: 'foo', + transaction_max_spans: 10 + } + }; + + expect(isConfigValid(config)).toBe(true); + }); + + it('is invalid when required parameters are not given', () => { + const config = {}; + expect(isConfigValid(config)).toBe(false); + }); +}); + +function isConfigValid(config: any) { + return isRight(agentConfigurationIntakeRt.decode(config)); +} diff --git a/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts new file mode 100644 index 0000000000000..32a2832b5eaf3 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts @@ -0,0 +1,26 @@ +/* + * 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'; +import { transactionSampleRateRt } from '../transaction_sample_rate_rt'; +import { transactionMaxSpansRt } from '../transaction_max_spans_rt'; + +export const serviceRt = t.partial({ + name: t.string, + environment: t.string +}); + +export const agentConfigurationIntakeRt = t.intersection([ + t.partial({ agent_name: t.string }), + t.type({ + service: serviceRt, + settings: t.partial({ + transaction_sample_rate: transactionSampleRateRt, + capture_body: t.string, + transaction_max_spans: transactionMaxSpansRt + }) + }) +]); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index 8ada02d085631..86eb1dba507f0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -7,9 +7,10 @@ /* eslint-disable no-console */ import { IndexDocumentParams, - IndicesCreateParams, IndicesDeleteParams, - SearchParams + SearchParams, + IndicesCreateParams, + DeleteDocumentResponse } from 'elasticsearch'; import { cloneDeep, isString, merge, uniqueId } from 'lodash'; import { KibanaRequest } from 'src/core/server'; @@ -188,7 +189,7 @@ export function getESClient( index: (params: APMIndexDocumentParams) => { return withTime(() => callMethod('index', params)); }, - delete: (params: IndicesDeleteParams) => { + delete: (params: IndicesDeleteParams): Promise => { return withTime(() => callMethod('delete', params)); }, indicesCreate: (params: IndicesCreateParams) => { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index 7120d3bca6c25..ccd8b123e23e2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -58,8 +58,8 @@ export async function getServiceNodeMetadata({ const response = await client.search(query); return { - host: response.aggregations?.host.buckets[0].key || NOT_AVAILABLE_LABEL, + host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, containerId: - response.aggregations?.containerId.buckets[0].key || NOT_AVAILABLE_LABEL + response.aggregations?.containerId.buckets[0]?.key || NOT_AVAILABLE_LABEL }; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 542fdd99e2635..db34b4d5d20b5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -1,6 +1,90 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`agent configuration queries fetches all environments 1`] = ` +exports[`agent configuration queries findExactConfiguration find configuration by service.environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, + }, + ], + }, + }, + Object { + "term": Object { + "service.environment": "bar", + }, + }, + ], + }, + }, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries findExactConfiguration find configuration by service.name 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.environment", + }, + }, + ], + }, + }, + ], + }, + }, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries findExactConfiguration find configuration by service.name and service.environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "service.environment": "bar", + }, + }, + ], + }, + }, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries getAllEnvironments fetches all environments 1`] = ` Object { "body": Object { "aggs": Object { @@ -41,14 +125,79 @@ Object { } `; -exports[`agent configuration queries fetches configurations 1`] = ` +exports[`agent configuration queries getExistingEnvironmentsForService fetches unavailable environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ALL_OPTION_VALUE", + "size": 50, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries getServiceNames fetches service names 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 50, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`agent configuration queries listConfigurations fetches configurations 1`] = ` Object { "index": "myIndex", "size": 200, } `; -exports[`agent configuration queries fetches filtered configurations with an environment 1`] = ` +exports[`agent configuration queries searchConfigurations fetches filtered configurations with an environment 1`] = ` Object { "body": Object { "query": Object { @@ -60,9 +209,7 @@ Object { "boost": 2, "filter": Object { "term": Object { - "service.name": Object { - "value": "foo", - }, + "service.name": "foo", }, }, }, @@ -72,9 +219,7 @@ Object { "boost": 1, "filter": Object { "term": Object { - "service.environment": Object { - "value": "bar", - }, + "service.environment": "bar", }, }, }, @@ -109,7 +254,7 @@ Object { } `; -exports[`agent configuration queries fetches filtered configurations without an environment 1`] = ` +exports[`agent configuration queries searchConfigurations fetches filtered configurations without an environment 1`] = ` Object { "body": Object { "query": Object { @@ -121,9 +266,7 @@ Object { "boost": 2, "filter": Object { "term": Object { - "service.name": Object { - "value": "foo", - }, + "service.name": "foo", }, }, }, @@ -157,68 +300,3 @@ Object { "index": "myIndex", } `; - -exports[`agent configuration queries fetches service names 1`] = ` -Object { - "body": Object { - "aggs": Object { - "services": Object { - "terms": Object { - "field": "service.name", - "size": 50, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, - "size": 0, - }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], -} -`; - -exports[`agent configuration queries fetches unavailable environments 1`] = ` -Object { - "body": Object { - "aggs": Object { - "environments": Object { - "terms": Object { - "field": "service.environment", - "missing": "ALL_OPTION_VALUE", - "size": 50, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "foo", - }, - }, - ], - }, - }, - "size": 0, - }, - "index": "myIndex", -} -`; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts index ea8f50c90c1d3..ddbe6892c5441 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts @@ -4,18 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AgentConfiguration { +import t from 'io-ts'; +import { agentConfigurationIntakeRt } from '../../../../common/runtime_types/agent_configuration_intake_rt'; + +export type AgentConfigurationIntake = t.TypeOf< + typeof agentConfigurationIntakeRt +>; +export type AgentConfiguration = { '@timestamp': number; applied_by_agent?: boolean; etag?: string; agent_name?: string; - service: { - name?: string; - environment?: string; - }; - settings: { - transaction_sample_rate?: number; - capture_body?: string; - transaction_max_spans?: number; - }; -} +} & AgentConfigurationIntake; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 5a67f78de6f65..74fcc61dde863 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -6,19 +6,19 @@ import hash from 'object-hash'; import { Setup } from '../../helpers/setup_request'; -import { AgentConfiguration } from './configuration_types'; +import { + AgentConfiguration, + AgentConfigurationIntake +} from './configuration_types'; import { APMIndexDocumentParams } from '../../helpers/es_client'; export async function createOrUpdateConfiguration({ configurationId, - configuration, + configurationIntake, setup }: { configurationId?: string; - configuration: Omit< - AgentConfiguration, - '@timestamp' | 'applied_by_agent' | 'etag' - >; + configurationIntake: AgentConfigurationIntake; setup: Setup; }) { const { internalClient, indices } = setup; @@ -27,15 +27,15 @@ export async function createOrUpdateConfiguration({ refresh: true, index: indices.apmAgentConfigurationIndex, body: { - agent_name: configuration.agent_name, + agent_name: configurationIntake.agent_name, service: { - name: configuration.service.name, - environment: configuration.service.environment + name: configurationIntake.service.name, + environment: configurationIntake.service.environment }, - settings: configuration.settings, + settings: configurationIntake.settings, '@timestamp': Date.now(), applied_by_agent: false, - etag: hash(configuration) + etag: hash(configurationIntake) } }; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts new file mode 100644 index 0000000000000..eea409882f876 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SERVICE_NAME, + SERVICE_ENVIRONMENT +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../helpers/setup_request'; +import { AgentConfiguration } from './configuration_types'; +import { ESSearchHit } from '../../../../typings/elasticsearch'; + +export async function findExactConfiguration({ + service, + setup +}: { + service: AgentConfiguration['service']; + setup: Setup; +}) { + const { internalClient, indices } = setup; + + const serviceNameFilter = service.name + ? { term: { [SERVICE_NAME]: service.name } } + : { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }; + + const environmentFilter = service.environment + ? { term: { [SERVICE_ENVIRONMENT]: service.environment } } + : { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } }; + + const params = { + index: indices.apmAgentConfigurationIndex, + body: { + query: { + bool: { filter: [serviceNameFilter, environmentFilter] } + } + } + }; + + const resp = await internalClient.search( + params + ); + + return resp.hits.hits[0] as ESSearchHit | undefined; +} diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index dccf8b110d082..a9af1f6174fd5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -48,8 +48,6 @@ export async function getAgentNameByService({ }; const { aggregations } = await client.search(params); - const agentName = aggregations?.agent_names.buckets[0].key as - | string - | undefined; - return { agentName }; + const agentName = aggregations?.agent_names.buckets[0]?.key; + return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index a82d148781ad8..b951b7f350eed 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -8,11 +8,12 @@ import { getAllEnvironments } from './get_environments/get_all_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'; +import { searchConfigurations } from './search_configurations'; import { SearchParamsMock, inspectSearchParams } from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +import { findExactConfiguration } from './find_exact_configuration'; describe('agent configuration queries', () => { let mock: SearchParamsMock; @@ -21,68 +22,117 @@ describe('agent configuration queries', () => { mock.teardown(); }); - it('fetches all environments', async () => { - mock = await inspectSearchParams(setup => - getAllEnvironments({ - serviceName: 'foo', - setup - }) - ); + describe('getAllEnvironments', () => { + it('fetches all environments', async () => { + mock = await inspectSearchParams(setup => + getAllEnvironments({ + serviceName: 'foo', + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches unavailable environments', async () => { - mock = await inspectSearchParams(setup => - getExistingEnvironmentsForService({ - serviceName: 'foo', - setup - }) - ); + describe('getExistingEnvironmentsForService', () => { + it('fetches unavailable environments', async () => { + mock = await inspectSearchParams(setup => + getExistingEnvironmentsForService({ + serviceName: 'foo', + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches service names', async () => { - mock = await inspectSearchParams(setup => - getServiceNames({ - setup - }) - ); + describe('getServiceNames', () => { + it('fetches service names', async () => { + mock = await inspectSearchParams(setup => + getServiceNames({ + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches configurations', async () => { - mock = await inspectSearchParams(setup => - listConfigurations({ - setup - }) - ); + describe('listConfigurations', () => { + it('fetches configurations', async () => { + mock = await inspectSearchParams(setup => + listConfigurations({ + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches filtered configurations without an environment', async () => { - mock = await inspectSearchParams(setup => - searchConfigurations({ - serviceName: 'foo', - setup - }) - ); + describe('searchConfigurations', () => { + it('fetches filtered configurations without an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + service: { + name: 'foo' + }, + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches filtered configurations with an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + service: { + name: 'foo', + environment: 'bar' + }, + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches filtered configurations with an environment', async () => { - mock = await inspectSearchParams(setup => - searchConfigurations({ - serviceName: 'foo', - environment: 'bar', - setup - }) - ); + describe('findExactConfiguration', () => { + it('find configuration by service.name', async () => { + mock = await inspectSearchParams(setup => + findExactConfiguration({ + service: { name: 'foo' }, + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('find configuration by service.environment', async () => { + mock = await inspectSearchParams(setup => + findExactConfiguration({ + service: { environment: 'bar' }, + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('find configuration by service.name and service.environment', async () => { + mock = await inspectSearchParams(setup => + findExactConfiguration({ + service: { name: 'foo', environment: 'bar' }, + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); }); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts similarity index 68% rename from x-pack/plugins/apm/server/lib/settings/agent_configuration/search.ts rename to x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 766baead006b6..9bbdc96a3a797 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -12,29 +12,39 @@ import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from './configuration_types'; export async function searchConfigurations({ - serviceName, - environment, + service, setup }: { - serviceName: string; - environment?: string; + service: AgentConfiguration['service']; setup: Setup; }) { const { internalClient, indices } = setup; - const environmentFilter = environment + + // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring). + // Additionally a boost has been added to service.name to ensure it scores higher. + // If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins + const serviceNameFilter = service.name + ? [ + { + constant_score: { + filter: { term: { [SERVICE_NAME]: service.name } }, + boost: 2 + } + } + ] + : []; + + const environmentFilter = service.environment ? [ { constant_score: { - filter: { term: { [SERVICE_ENVIRONMENT]: { value: environment } } }, + filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } }, boost: 1 } } ] : []; - // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring) - // Additionally a boost has been added to service.name to ensure it scores higher - // if there is tie between a config with a matching service.name and a config with a matching environment const params = { index: indices.apmAgentConfigurationIndex, body: { @@ -42,12 +52,7 @@ export async function searchConfigurations({ bool: { minimum_should_match: 2, should: [ - { - constant_score: { - filter: { term: { [SERVICE_NAME]: { value: serviceName } } }, - boost: 2 - } - }, + ...serviceNameFilter, ...environmentFilter, { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } } diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index f65e271389938..21392edbb2c48 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -23,11 +23,10 @@ import { import { agentConfigurationRoute, agentConfigurationSearchRoute, - createAgentConfigurationRoute, deleteAgentConfigurationRoute, listAgentConfigurationEnvironmentsRoute, listAgentConfigurationServicesRoute, - updateAgentConfigurationRoute, + createOrUpdateAgentConfigurationRoute, agentConfigurationAgentNameRoute } from './settings/agent_configuration'; import { @@ -83,11 +82,10 @@ const createApmApi = () => { .add(agentConfigurationAgentNameRoute) .add(agentConfigurationRoute) .add(agentConfigurationSearchRoute) - .add(createAgentConfigurationRoute) .add(deleteAgentConfigurationRoute) .add(listAgentConfigurationEnvironmentsRoute) .add(listAgentConfigurationServicesRoute) - .add(updateAgentConfigurationRoute) + .add(createOrUpdateAgentConfigurationRoute) // APM indices .add(apmIndexSettingsRoute) diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index ddd6a27025131..83b845b1fc436 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -9,15 +9,19 @@ import Boom from 'boom'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names'; import { createOrUpdateConfiguration } from '../../lib/settings/agent_configuration/create_or_update_configuration'; -import { searchConfigurations } from '../../lib/settings/agent_configuration/search'; +import { searchConfigurations } from '../../lib/settings/agent_configuration/search_configurations'; +import { findExactConfiguration } from '../../lib/settings/agent_configuration/find_exact_configuration'; 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'; +import { + serviceRt, + agentConfigurationIntakeRt +} from '../../../common/runtime_types/agent_configuration_intake_rt'; +import { jsonRt } from '../../../common/runtime_types/json_rt'; // get list of configurations export const agentConfigurationRoute = createRoute(core => ({ @@ -31,20 +35,34 @@ export const agentConfigurationRoute = createRoute(core => ({ // delete configuration export const deleteAgentConfigurationRoute = createRoute(() => ({ method: 'DELETE', - path: '/api/apm/settings/agent-configuration/{configurationId}', + path: '/api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'] }, params: { - path: t.type({ - configurationId: t.string + body: t.type({ + service: serviceRt }) }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { configurationId } = context.params.path; + const { service } = context.params.body; + + const config = await findExactConfiguration({ service, setup }); + if (!config) { + context.logger.info( + `Config was not found for ${service.name}/${service.environment}` + ); + + throw Boom.notFound(); + } + + context.logger.info( + `Deleting config ${service.name}/${service.environment} (${config._id})` + ); + return await deleteConfiguration({ - configurationId, + configurationId: config._id, setup }); } @@ -62,23 +80,6 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({ } })); -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 }) - ]) - }), - 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/environments', @@ -102,55 +103,47 @@ export const agentConfigurationAgentNameRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { serviceName } = context.params.query; const agentName = await getAgentNameByService({ serviceName, setup }); - return agentName; + return { agentName }; } })); -export const createAgentConfigurationRoute = createRoute(() => ({ - method: 'POST', - path: '/api/apm/settings/agent-configuration/new', - params: { - body: agentPayloadRt - }, +export const createOrUpdateAgentConfigurationRoute = createRoute(() => ({ + method: 'PUT', + path: '/api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'] }, + params: { + query: t.partial({ overwrite: jsonRt.pipe(t.boolean) }), + body: agentConfigurationIntakeRt + }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const configuration = context.params.body; + const { body, query } = context.params; - // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) - context.logger.info( - `Hitting: /api/apm/settings/agent-configuration/new with ${configuration.service.name}/${configuration.service.environment}` - ); - const res = await createOrUpdateConfiguration({ - configuration, + // if the config already exists, it is fetched and updated + // this is to avoid creating two configs with identical service params + const config = await findExactConfiguration({ + service: body.service, setup }); - context.logger.info(`Created agent configuration`); - return res; - } -})); + // if the config exists ?overwrite=true is required + if (config && !query.overwrite) { + throw Boom.badRequest( + `A configuration already exists for "${body.service.name}/${body.service.environment}. Use ?overwrite=true to overwrite the existing configuration.` + ); + } + + context.logger.info( + `${config ? 'Updating' : 'Creating'} config ${body.service.name}/${ + body.service.environment + }` + ); -export const updateAgentConfigurationRoute = createRoute(() => ({ - method: 'PUT', - path: '/api/apm/settings/agent-configuration/{configurationId}', - options: { - tags: ['access:apm', 'access:apm_write'] - }, - params: { - path: t.type({ - configurationId: t.string - }), - body: agentPayloadRt - }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { configurationId } = context.params.path; return await createOrUpdateConfiguration({ - configurationId, - configuration: context.params.body, + configurationId: config?._id, + configurationIntake: body, setup }); } @@ -162,41 +155,33 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ path: '/api/apm/settings/agent-configuration/search', params: { body: t.type({ - service: t.intersection([ - t.type({ name: t.string }), - t.partial({ environment: t.string }) - ]), + service: serviceRt, etag: t.string }) }, handler: async ({ context, request }) => { - const { body } = context.params; - - // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) - context.logger.info( - `Hitting: /api/apm/settings/agent-configuration/search for ${body.service.name}/${body.service.environment}` - ); + const { service, etag } = context.params.body; const setup = await setupRequest(context, request); const config = await searchConfigurations({ - serviceName: body.service.name, - environment: body.service.environment, + service, setup }); if (!config) { context.logger.info( - `Config was not found for ${body.service.name}/${body.service.environment}` + `Config was not found for ${service.name}/${service.environment}` ); - throw new Boom('Not found', { statusCode: 404 }); + throw Boom.notFound(); } context.logger.info( - `Config was found for ${body.service.name}/${body.service.environment}` + `Config was found for ${service.name}/${service.environment}` ); // update `applied_by_agent` field if etags match - if (body.etag === config._source.etag && !config._source.applied_by_agent) { + // this happens in the background and doesn't block the response + if (etag === config._source.etag && !config._source.applied_by_agent) { markAppliedByAgent({ id: config._id, body: config._source, setup }); } diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts index 12e08869fa586..959a0c97acfa3 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { AgentConfigurationIntake } from '../../../../plugins/apm/server/lib/settings/agent_configuration/configuration_types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { @@ -18,108 +19,122 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte .set('kbn-xsrf', 'foo'); } - let createdConfigIds: any[] = []; - async function createConfiguration(configuration: any) { + async function createConfiguration(config: AgentConfigurationIntake) { + log.debug('creating configuration', config.service); const res = await supertest - .post(`/api/apm/settings/agent-configuration/new`) - .send(configuration) + .put(`/api/apm/settings/agent-configuration`) + .send(config) .set('kbn-xsrf', 'foo'); - createdConfigIds.push(res.body._id); + throwOnError(res); return res; } - function deleteCreatedConfigurations() { - const promises = Promise.all(createdConfigIds.map(deleteConfiguration)); - return promises; + async function updateConfiguration(config: AgentConfigurationIntake) { + log.debug('updating configuration', config.service); + const res = await supertest + .put(`/api/apm/settings/agent-configuration?overwrite=true`) + .send(config) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; } - function deleteConfiguration(configurationId: string) { - return supertest - .delete(`/api/apm/settings/agent-configuration/${configurationId}`) - .set('kbn-xsrf', 'foo') - .then((response: any) => { - createdConfigIds = createdConfigIds.filter(id => id === configurationId); - return response; - }); + async function deleteConfiguration({ service }: AgentConfigurationIntake) { + log.debug('deleting configuration', service); + const res = await supertest + .delete(`/api/apm/settings/agent-configuration`) + .send({ service }) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + throw new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + } } describe('agent configuration', () => { describe('when creating one configuration', () => { - let createdConfigId: string; + const newConfig = { + service: {}, + settings: { transaction_sample_rate: 0.55 }, + }; - const parameters = { + const searchParams = { service: { name: 'myservice', environment: 'development' }, etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', }; before(async () => { - log.debug('creating agent configuration'); - - // all / all - const { body } = await createConfiguration({ - service: {}, - settings: { transaction_sample_rate: 0.1 }, - }); - - createdConfigId = body._id; + await createConfiguration(newConfig); }); - it('returns the created configuration', async () => { - const { statusCode, body } = await searchConfigurations(parameters); - + it('can find the created config', async () => { + const { statusCode, body } = await searchConfigurations(searchParams); expect(statusCode).to.equal(200); - expect(body._id).to.equal(createdConfigId); + expect(body._source.service).to.eql({}); + expect(body._source.settings).to.eql({ transaction_sample_rate: 0.55 }); }); - it('succesfully deletes the configuration', async () => { - await deleteConfiguration(createdConfigId); + it('can update the created config', async () => { + await updateConfiguration({ service: {}, settings: { transaction_sample_rate: 0.85 } }); - const { statusCode } = await searchConfigurations(parameters); + const { statusCode, body } = await searchConfigurations(searchParams); + expect(statusCode).to.equal(200); + expect(body._source.service).to.eql({}); + expect(body._source.settings).to.eql({ transaction_sample_rate: 0.85 }); + }); + it('can delete the created config', async () => { + await deleteConfiguration(newConfig); + const { statusCode } = await searchConfigurations(searchParams); expect(statusCode).to.equal(404); }); }); - describe('when creating four configurations', () => { - before(async () => { - log.debug('creating agent configuration'); - - // all / all - await createConfiguration({ + describe('when creating multiple configurations', () => { + const configs = [ + { 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' }, + }, + { + service: { name: 'my_service', environment: 'development' }, settings: { transaction_sample_rate: 0.3 }, - }); - - // all / production - await createConfiguration({ - service: { environment: 'development' }, + }, + { + service: { environment: 'production' }, settings: { transaction_sample_rate: 0.4 }, - }); - - // my_service / production - await createConfiguration({ - service: { name: 'my_service', environment: 'development' }, + }, + { + service: { environment: 'development' }, settings: { transaction_sample_rate: 0.5 }, - }); + }, + ]; + + before(async () => { + await Promise.all(configs.map(config => createConfiguration(config))); }); after(async () => { - log.debug('deleting agent configurations'); - await deleteCreatedConfigurations(); + await Promise.all(configs.map(config => deleteConfiguration(config))); }); const agentsRequests = [ @@ -127,20 +142,24 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'non_existing_service', environment: 'non_existing_env' }, expectedSettings: { transaction_sample_rate: 0.1 }, }, + { + service: { name: 'my_service', environment: 'non_existing_env' }, + expectedSettings: { transaction_sample_rate: 0.2 }, + }, { service: { name: 'my_service', environment: 'production' }, expectedSettings: { transaction_sample_rate: 0.2 }, }, { - service: { name: 'non_existing_service', environment: 'production' }, + service: { name: 'my_service', environment: 'development' }, expectedSettings: { transaction_sample_rate: 0.3 }, }, { - service: { name: 'non_existing_service', environment: 'development' }, + service: { name: 'non_existing_service', environment: 'production' }, expectedSettings: { transaction_sample_rate: 0.4 }, }, { - service: { name: 'my_service', environment: 'development' }, + service: { name: 'non_existing_service', environment: 'development' }, expectedSettings: { transaction_sample_rate: 0.5 }, }, ]; @@ -159,18 +178,18 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); describe('when an agent retrieves a configuration', () => { + const config = { + service: { name: 'myservice', environment: 'development' }, + settings: { transaction_sample_rate: 0.9 }, + }; + before(async () => { log.debug('creating agent configuration'); - - await createConfiguration({ - service: { name: 'myservice', environment: 'development' }, - settings: { transaction_sample_rate: 0.9 }, - }); + await createConfiguration(config); }); after(async () => { - log.debug('deleting agent configurations'); - await deleteCreatedConfigurations(); + await deleteConfiguration(config); }); it(`should have 'applied_by_agent=false' on first request`, async () => { 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 ec2bbca23ddd2..3c5314d0d3261 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable no-console */ - import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -14,8 +12,8 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const spaces = getService('spaces'); - const log = getService('log'); const es = getService('legacyEs'); + const log = getService('log'); const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); const end = encodeURIComponent(new Date().toISOString()); @@ -33,7 +31,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) interface Endpoint { req: { url: string; - method?: 'get' | 'post' | 'delete'; + method?: 'get' | 'post' | 'delete' | 'put'; body?: any; }; expectForbidden: (result: any) => void; @@ -148,7 +146,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) index: '.apm-agent-configuration', }); - console.warn(JSON.stringify(res, null, 2)); + log.error(JSON.stringify(res, null, 2)); }, }, ]; @@ -196,7 +194,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const { statusCode, req } = response; if (statusCode !== 200) { - log.debug(`Endpoint: ${req.method} ${req.path} + throw new Error(`Endpoint: ${req.method} ${req.path} Status code: ${statusCode} Response: ${response.body.message}`); } @@ -216,9 +214,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext) spaceId?: string; }) { for (const endpoint of endpoints) { - console.log(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`); + log.info(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`); const result = await executeAsUser(endpoint.req, username, password, spaceId); - console.log(`Responded: ${endpoint.req.url}`); + log.info(`Responded: ${endpoint.req.url}`); try { if (expectation === 'forbidden') { @@ -244,26 +242,28 @@ export default function featureControlsTests({ getService }: FtrProviderContext) } describe('apm feature controls', () => { - let res: any; + const config = { + service: { name: 'test-service' }, + settings: { transaction_sample_rate: 0.5 }, + }; before(async () => { - console.log(`Creating agent configuration`); - res = await executeAsAdmin({ - method: 'post', - url: '/api/apm/settings/agent-configuration/new', - body: { - service: { name: 'test-service' }, - settings: { transaction_sample_rate: 0.5 }, - }, + log.info(`Creating agent configuration`); + await executeAsAdmin({ + method: 'put', + url: '/api/apm/settings/agent-configuration', + body: config, }); - console.log(`Agent configuration created`); + log.info(`Agent configuration created`); }); after(async () => { - console.log('deleting agent configuration'); - const configurationId = res.body._id; + log.info('deleting agent configuration'); await executeAsAdmin({ method: 'delete', - url: `/api/apm/settings/agent-configuration/${configurationId}`, + url: `/api/apm/settings/agent-configuration`, + body: { + service: config.service, + }, }); }); diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts index c49d577537048..6f41f4abfecc3 100644 --- a/x-pack/test/api_integration/apis/apm/index.ts +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('APM', () => { + describe('APM specs', () => { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./agent_configuration')); }); diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts index 945af09183f03..bf254f9b9b419 100644 --- a/x-pack/test/functional/apps/apm/index.ts +++ b/x-pack/test/functional/apps/apm/index.ts @@ -6,7 +6,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - describe('APM', function() { + describe('APM specs', function() { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); }); From 5eefdbb84266e2af1998c35d4876f18bc13d1fe0 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Mon, 24 Feb 2020 16:46:58 -0600 Subject: [PATCH 069/113] [Uptime] Use scripted metric for snapshot calculation (#58247) (#58389) Fixes #58079 This is an improved version of #58078 Note, this is a bugfix targeting 7.6.1 . I've decided to open this PR directly against 7.6 in the interest of time. We can forward-port this to 7.x / master later. This patch improves the handling of timespans with snapshot counts. This feature originally worked, but suffered a regression when we increased the default timespan in the query context to 5m. This means that without this patch the counts you get are the maximum total number of monitors that were down over the past 5m, which is not really that useful. We now use a scripted metric to always count precisely the number of up/down monitors. On my box this could process 400k summary docs in ~600ms. This should scale as shards are added. I attempted to keep memory usage relatively slow by using simple maps of strings. --- .../lib/requests/get_snapshot_counts.ts | 194 +++++++++++++----- .../apis/uptime/rest/snapshot.ts | 105 +++++----- 2 files changed, 192 insertions(+), 107 deletions(-) diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 6236971146015..8d84c0f4d6769 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -6,7 +6,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { Snapshot } from '../../../common/runtime_types'; -import { QueryContext, MonitorGroupIterator } from './search'; +import { QueryContext } from './search'; import { CONTEXT_DEFAULTS, INDEX_NAMES } from '../../../common/constants'; export interface GetSnapshotCountParams { @@ -16,49 +16,6 @@ export interface GetSnapshotCountParams { statusFilter?: string; } -const fastStatusCount = async (context: QueryContext): Promise => { - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - size: 0, - query: { bool: { filter: await context.dateAndCustomFilters() } }, - aggs: { - unique: { - // We set the precision threshold to 40k which is the max precision supported by cardinality - cardinality: { field: 'monitor.id', precision_threshold: 40000 }, - }, - down: { - filter: { range: { 'summary.down': { gt: 0 } } }, - aggs: { - unique: { cardinality: { field: 'monitor.id', precision_threshold: 40000 } }, - }, - }, - }, - }, - }; - - const statistics = await context.search(params); - const total = statistics.aggregations.unique.value; - const down = statistics.aggregations.down.unique.value; - - return { - total, - down, - up: total - down, - }; -}; - -const slowStatusCount = async (context: QueryContext, status: string): Promise => { - const downContext = context.clone(); - downContext.statusFilter = status; - const iterator = new MonitorGroupIterator(downContext); - let count = 0; - while (await iterator.next()) { - count++; - } - return count; -}; - export const getSnapshotCount: UMElasticsearchQueryFn = async ({ callES, dateRangeStart, @@ -81,18 +38,7 @@ export const getSnapshotCount: UMElasticsearchQueryFn = - counts.up > counts.down ? ['down', 'up'] : ['up', 'down']; - counts[leastCommonStatus] = await slowStatusCount(context, leastCommonStatus); - counts[mostCommonStatus] = counts.total - counts[leastCommonStatus]; - } + const counts = await statusCount(context); return { total: statusFilter ? counts[statusFilter] : counts.total, @@ -100,3 +46,139 @@ export const getSnapshotCount: UMElasticsearchQueryFn => { + const res = await context.search({ + index: INDEX_NAMES.HEARTBEAT, + body: statusCountBody(await context.dateAndCustomFilters()), + }); + + return res.aggregations.counts.value; +}; + +const statusCountBody = (filters: any): any => { + return { + size: 0, + query: { + bool: { + filter: [ + { + exists: { + field: 'summary', + }, + }, + filters, + ], + }, + }, + aggs: { + counts: { + scripted_metric: { + init_script: 'state.locStatus = new HashMap(); state.totalDocs = 0;', + map_script: ` + def loc = doc["observer.geo.name"].size() == 0 ? "" : doc["observer.geo.name"][0]; + + // One concern here is memory since we could build pretty gigantic maps. I've opted to + // stick to a simple map to reduce memory overhead. This means we do + // a little string parsing to treat these strings as records that stay lexicographically + // sortable (which is important later). + // We encode the ID and location as $id.len:$id$loc + String id = doc["monitor.id"][0]; + String idLenDelim = Integer.toHexString(id.length()) + ":" + id; + String idLoc = loc == null ? idLenDelim : idLenDelim + loc; + + String status = doc["summary.down"][0] > 0 ? "d" : "u"; + String timeAndStatus = doc["@timestamp"][0].toInstant().toEpochMilli().toString() + status; + state.locStatus[idLoc] = timeAndStatus; + state.totalDocs++; + `, + combine_script: ` + return state; + `, + reduce_script: ` + // Use a treemap since it's traversable in sorted order. + // This is important later. + TreeMap locStatus = new TreeMap(); + long totalDocs = 0; + int uniqueIds = 0; + for (state in states) { + totalDocs += state.totalDocs; + for (entry in state.locStatus.entrySet()) { + // Update the value for the given key if we have a more recent check from this location. + locStatus.merge(entry.getKey(), entry.getValue(), (a,b) -> a.compareTo(b) > 0 ? a : b) + } + } + + HashMap locTotals = new HashMap(); + int total = 0; + int down = 0; + String curId = ""; + boolean curIdDown = false; + // We now iterate through our tree map in order, which means records for a given ID + // always are encountered one after the other. This saves us having to make an intermediate + // map. + for (entry in locStatus.entrySet()) { + String idLoc = entry.getKey(); + String timeStatus = entry.getValue(); + + // Parse the length delimited id/location strings described in the map section + int colonIndex = idLoc.indexOf(":"); + int idEnd = Integer.parseInt(idLoc.substring(0, colonIndex), 16) + colonIndex + 1; + String id = idLoc.substring(colonIndex + 1, idEnd); + String loc = idLoc.substring(idEnd, idLoc.length()); + String status = timeStatus.substring(timeStatus.length() - 1); + + // Here we increment counters for the up/down key per location + // We also create a new hashmap in locTotals if we've never seen this location + // before. + locTotals.compute(loc, (k,v) -> { + HashMap res = v; + if (v == null) { + res = new HashMap(); + res.put('up', 0); + res.put('down', 0); + } + + if (status == 'u') { + res.up++; + } else { + res.down++; + } + + return res; + }); + + + // We've encountered a new ID + if (curId != id) { + total++; + curId = id; + if (status == "d") { + curIdDown = true; + down++; + } else { + curIdDown = false; + } + } else if (!curIdDown) { + if (status == "d") { + curIdDown = true; + down++; + } else { + curIdDown = false; + } + } + } + + Map result = new HashMap(); + result.total = total; + result.location_totals = locTotals; + result.up = total - down; + result.down = down; + result.totalDocs = totalDocs; + return result; + `, + }, + }, + }, + }; +}; diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts index b0d97837c770f..20fe59d149ae8 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -34,66 +34,69 @@ export default function({ getService }: FtrProviderContext) { let dateRange: { start: string; end: string }; [true, false].forEach(async (includeTimespan: boolean) => { - describe(`with timespans ${includeTimespan ? 'included' : 'missing'}`, async () => { - before(async () => { - const promises: Array> = []; - - // When includeTimespan is false we have to remove the values there. - let mogrify = (d: any) => d; - if ((includeTimespan = false)) { - mogrify = (d: any): any => { - d.monitor.delete('timespan'); + [true, false].forEach(async (includeObserver: boolean) => { + describe(`with timespans=${includeTimespan} and observer=${includeObserver}`, async () => { + before(async () => { + const promises: Array> = []; + + const mogrify = (d: any) => { + if (!includeTimespan) { + delete d.monitor.timespan; + } + if (!includeObserver) { + delete d.observer; + } return d; }; - } - - const makeMonitorChecks = async (monitorId: string, status: 'up' | 'down') => { - return makeChecksWithStatus( - getService('legacyEs'), - monitorId, - checksPerMonitor, - numIps, - scheduleEvery, - {}, - status, - mogrify - ); - }; - for (let i = 0; i < numUpMonitors; i++) { - promises.push(makeMonitorChecks(`up-${i}`, 'up')); - } - for (let i = 0; i < numDownMonitors; i++) { - promises.push(makeMonitorChecks(`down-${i}`, 'down')); - } + const makeMonitorChecks = async (monitorId: string, status: 'up' | 'down') => { + return makeChecksWithStatus( + getService('legacyEs'), + monitorId, + checksPerMonitor, + numIps, + scheduleEvery, + {}, + status, + mogrify + ); + }; - const allResults = await Promise.all(promises); - dateRange = getChecksDateRange(allResults); - }); + for (let i = 0; i < numUpMonitors; i++) { + promises.push(makeMonitorChecks(`up-${i}`, 'up')); + } + for (let i = 0; i < numDownMonitors; i++) { + promises.push(makeMonitorChecks(`down-${i}`, 'down')); + } - it('will count all statuses correctly', async () => { - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}` - ); + const allResults = await Promise.all(promises); + dateRange = getChecksDateRange(allResults); + }); - expectFixtureEql(apiResponse.body, 'snapshot'); - }); + it('will count all statuses correctly', async () => { + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}` + ); - it('will fetch a monitor snapshot filtered by down status', async () => { - const statusFilter = 'down'; - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}` - ); + expectFixtureEql(apiResponse.body, 'snapshot'); + }); - expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); - }); + it('will fetch a monitor snapshot filtered by down status', async () => { + const statusFilter = 'down'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}` + ); - it('will fetch a monitor snapshot filtered by up status', async () => { - const statusFilter = 'up'; - const apiResponse = await supertest.get( - `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}` - ); - expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); + }); + + it('will fetch a monitor snapshot filtered by up status', async () => { + const statusFilter = 'up'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + }); }); }); }); From a00ebc6f187ffbd920b0e2bf3f92483239ed0c0f Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 24 Feb 2020 18:44:52 -0500 Subject: [PATCH 070/113] [Uptime] Delete useless try...catch blocks (#58263) * Delete useless try...catch blocks. * Delete unneeded function. Co-authored-by: Elastic Machine --- .../plugins/uptime/public/hooks/use_telemetry.ts | 10 +++------- .../plugins/uptime/server/lib/helper/index.ts | 1 - .../uptime/server/lib/helper/parse_filter_query.ts | 13 ------------- .../server/lib/requests/get_ping_histogram.ts | 4 ++-- 4 files changed, 5 insertions(+), 23 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/helper/parse_filter_query.ts diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts index 15f276174e2cf..7eb18404decfd 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts @@ -22,13 +22,9 @@ const getApiPath = (page?: UptimePage) => { }; const logPageLoad = async (fetch: HttpHandler, page?: UptimePage) => { - try { - await fetch(getApiPath(page), { - method: 'POST', - }); - } catch (e) { - throw e; - } + await fetch(getApiPath(page), { + method: 'POST', + }); }; export const useUptimeTelemetry = (page?: UptimePage) => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts index 0dcbc0a424b5e..1607c36f1d1b7 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts @@ -7,6 +7,5 @@ export { getFilterClause } from './get_filter_clause'; export { parseRelativeDate } from './get_histogram_interval'; export { getHistogramIntervalFormatted } from './get_histogram_interval_formatted'; -export { parseFilterQuery } from './parse_filter_query'; export { assertCloseTo } from './assert_close_to'; export { objectValuesToArrays } from './object_to_array'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/parse_filter_query.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/parse_filter_query.ts deleted file mode 100644 index 4c73ec53af9b9..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/parse_filter_query.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const parseFilterQuery = (query?: string | null) => { - try { - return query ? JSON.parse(query) : null; - } catch { - return null; - } -}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 3b448dc31659b..7b8ca4708255c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { parseFilterQuery, getFilterClause } from '../helper'; +import { getFilterClause } from '../helper'; import { INDEX_NAMES, QUERY } from '../../../common/constants'; import { HistogramQueryResult } from './types'; import { HistogramResult } from '../../../common/types'; @@ -27,7 +27,7 @@ export const getPingHistogram: UMElasticsearchQueryFn< GetPingHistogramParams, HistogramResult > = async ({ callES, from, to, filters, monitorId, statusFilter }) => { - const boolFilters = parseFilterQuery(filters); + const boolFilters = filters ? JSON.parse(filters) : null; const additionalFilters = []; if (monitorId) { additionalFilters.push({ match: { 'monitor.id': monitorId } }); From 33334132eaacde693caa3f7b6a337309abd784bb Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 24 Feb 2020 17:07:36 -0700 Subject: [PATCH 071/113] [kbn/optimizer] disable parallelization in terser plugin (#58396) * [kbn/optimizer] disable parallelization in terer plugin * use more workers when building the dist --- .../kbn-optimizer/src/optimizer/optimizer_config.ts | 11 ++++++++++- packages/kbn-optimizer/src/worker/webpack.config.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index a258e1010fce3..1c8ae265bf6bb 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -25,6 +25,15 @@ import { Bundle, WorkerConfig } from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getBundles } from './get_bundles'; +function pickMaxWorkerCount(dist: boolean) { + // don't break if cpus() returns nothing, or an empty array + const cpuCount = Math.max(Os.cpus()?.length, 1); + // if we're buiding the dist then we can use more of the system's resources to get things done a little quicker + const maxWorkers = dist ? cpuCount - 1 : Math.ceil(cpuCount / 3); + // ensure we always have at least two workers + return Math.max(maxWorkers, 2); +} + interface Options { /** absolute path to root of the repo/build */ repoRoot: string; @@ -110,7 +119,7 @@ export class OptimizerConfig { const maxWorkerCount = process.env.KBN_OPTIMIZER_MAX_WORKERS ? parseInt(process.env.KBN_OPTIMIZER_MAX_WORKERS, 10) - : options.maxWorkerCount ?? Math.max(Math.ceil(Math.max(Os.cpus()?.length, 1) / 3), 2); + : options.maxWorkerCount ?? pickMaxWorkerCount(dist); if (typeof maxWorkerCount !== 'number' || !Number.isFinite(maxWorkerCount)) { throw new TypeError('worker count must be a number'); } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 22b927d4638d7..3c6ae78bc4d91 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -252,6 +252,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { cache: false, sourceMap: false, extractComments: false, + parallel: false, terserOptions: { compress: false, mangle: false, From 277b38079ee55a5ae088a3a4c6bacf127f5c8968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 25 Feb 2020 13:12:27 +0530 Subject: [PATCH 072/113] [Index management] Move to new platform "plugins" folder (#58109) --- x-pack/.i18nrc.json | 2 +- .../cross_cluster_replication/index.js | 7 +- .../components/detail_panel/detail_panel.js | 2 +- .../components/detail_panel/detail_panel.js | 7 +- .../public/extend_index_management/index.js | 9 ++- .../__jest__/extend_index_management.test.js | 6 +- .../index_lifecycle_management/plugin.ts | 8 +- .../public/legacy.ts | 4 +- .../components/policy_table/policy_table.js | 2 +- .../np_ready/extend_index_management/index.js | 3 +- .../legacy/plugins/index_management/index.ts | 29 +------ .../index_management/public/index.html | 3 - .../legacy/plugins/remote_clusters/index.ts | 4 +- x-pack/legacy/plugins/rollup/index.ts | 4 +- x-pack/legacy/plugins/rollup/public/legacy.ts | 2 - x-pack/legacy/plugins/rollup/public/plugin.ts | 20 +++-- x-pack/legacy/plugins/rollup/server/plugin.ts | 10 +-- .../rollup/server/rollup_data_enricher.ts | 7 +- x-pack/legacy/plugins/rollup/server/types.ts | 1 - .../client_integration/helpers/constants.ts | 0 .../helpers/home.helpers.ts | 6 +- .../helpers/http_requests.ts | 0 .../client_integration/helpers/index.ts | 2 +- .../helpers/setup_environment.tsx | 3 +- .../helpers/template_clone.helpers.ts | 4 +- .../helpers/template_create.helpers.ts | 4 +- .../helpers/template_edit.helpers.ts | 4 +- .../helpers/template_form.helpers.ts | 2 +- .../__jest__/client_integration/home.test.ts | 0 .../template_clone.test.tsx | 0 .../template_create.test.tsx | 0 .../client_integration/template_edit.test.tsx | 0 .../__snapshots__/index_table.test.js.snap | 0 .../__jest__/components/index_table.test.js | 4 +- .../__snapshots__/flatten_object.test.js.snap | 0 .../__jest__/lib/flatten_object.test.js | 0 .../plugins/index_management/__mocks__/ace.js | 0 .../__mocks__/ui/documentation_links.js | 0 .../index_management/__mocks__/ui/notify.js | 0 .../common/constants/api_base_path.ts | 0 .../common/constants/base_path.ts | 0 .../common/constants/index.ts | 0 .../common/constants/index_statuses.ts | 0 .../common/constants/invalid_characters.ts | 0 .../common/constants/plugin.ts | 2 +- .../common/constants/ui_metric.ts | 0 .../plugins/index_management/common/index.ts | 0 .../index_management/common/lib/index.ts | 0 .../common/lib/template_serialization.ts | 0 .../index_management/common/types/index.ts | 0 .../common/types/templates.ts | 0 x-pack/plugins/index_management/kibana.json | 15 ++++ .../public/application/app.tsx | 2 +- .../public/application/app_context.tsx | 4 +- .../public/application/components/index.ts | 0 .../client_integration/helpers/index.ts | 2 +- .../helpers/mappings_editor.helpers.ts | 2 +- .../mappings_editor.test.tsx | 0 .../components/mappings_editor/_index.scss | 0 .../mappings_editor/components/_index.scss | 0 .../mappings_editor/components/code_block.tsx | 0 .../configuration_form/configuration_form.tsx | 6 +- .../configuration_form_schema.tsx | 0 .../dynamic_mapping_section.tsx | 0 .../dynamic_mapping_section/index.ts | 0 .../components/configuration_form/index.ts | 0 .../meta_field_section/index.ts | 0 .../meta_field_section/meta_field_section.tsx | 0 .../configuration_form/routing_section.tsx | 0 .../source_field_section/index.ts | 0 .../source_field_section.tsx | 0 .../components/document_fields/_index.scss | 0 .../document_fields/document_fields.tsx | 11 ++- .../document_fields_header.tsx | 0 .../editor_toggle_controls.tsx | 0 .../field_parameters/analyzer_parameter.tsx | 0 .../analyzer_parameter_selects.tsx | 20 +++-- .../field_parameters/analyzers_parameter.tsx | 0 .../field_parameters/boost_parameter.tsx | 0 .../coerce_number_parameter.tsx | 0 .../coerce_shape_parameter.tsx | 0 .../field_parameters/copy_to_parameter.tsx | 0 .../field_parameters/doc_values_parameter.tsx | 0 .../field_parameters/dynamic_parameter.tsx | 0 .../eager_global_ordinals_parameter.tsx | 0 .../field_parameters/enabled_parameter.tsx | 0 .../fielddata_frequency_filter_absolute.tsx | 0 .../fielddata_frequency_filter_percentage.tsx | 0 .../field_parameters/fielddata_parameter.tsx | 0 .../field_parameters/format_parameter.tsx | 0 .../field_parameters/ignore_malformed.tsx | 0 .../ignore_z_value_parameter.tsx | 0 .../document_fields/field_parameters/index.ts | 0 .../field_parameters/index_parameter.tsx | 0 .../field_parameters/locale_parameter.tsx | 0 .../max_shingle_size_parameter.tsx | 0 .../field_parameters/name_parameter.tsx | 0 .../field_parameters/norms_parameter.tsx | 0 .../field_parameters/null_value_parameter.tsx | 0 .../orientation_parameter.tsx | 0 .../field_parameters/path_parameter.tsx | 0 .../field_parameters/relations_parameter.tsx | 0 .../field_parameters/similarity_parameter.tsx | 0 .../split_queries_on_whitespace_parameter.tsx | 0 .../field_parameters/store_parameter.tsx | 0 .../term_vector_parameter.tsx | 0 .../field_parameters/type_parameter.tsx | 0 .../fields/_field_list_item.scss | 0 .../document_fields/fields/_index.scss | 0 .../fields/create_field/create_field.tsx | 80 +++++++++++-------- .../fields/create_field/index.ts | 0 .../required_parameters_forms/alias_type.tsx | 0 .../dense_vector_type.tsx | 0 .../required_parameters_forms/index.ts | 0 .../scaled_float_type.tsx | 0 .../token_count_type.tsx | 0 .../fields/delete_field_provider.tsx | 0 .../edit_field/_edit_field_form_row.scss | 0 .../fields/edit_field/_index.scss | 0 .../advanced_parameters_section.tsx | 0 .../edit_field/basic_parameters_section.tsx | 0 .../fields/edit_field/edit_field.tsx | 0 .../edit_field/edit_field_container.tsx | 4 +- .../fields/edit_field/edit_field_form_row.tsx | 0 .../edit_field/edit_field_header_form.tsx | 0 .../edit_field/field_description_section.tsx | 0 .../fields/edit_field/index.ts | 0 .../edit_field/update_field_provider.tsx | 0 .../fields/field_types/alias_type.tsx | 0 .../fields/field_types/binary_type.tsx | 0 .../fields/field_types/boolean_type.tsx | 0 .../fields/field_types/completion_type.tsx | 0 .../fields/field_types/date_type.tsx | 0 .../fields/field_types/dense_vector_type.tsx | 0 .../fields/field_types/flattened_type.tsx | 0 .../fields/field_types/geo_point_type.tsx | 0 .../fields/field_types/geo_shape_type.tsx | 0 .../fields/field_types/index.ts | 0 .../fields/field_types/ip_type.tsx | 0 .../fields/field_types/join_type.tsx | 0 .../fields/field_types/keyword_type.tsx | 0 .../fields/field_types/nested_type.tsx | 0 .../fields/field_types/numeric_type.tsx | 0 .../fields/field_types/object_type.tsx | 0 .../fields/field_types/range_type.tsx | 0 .../fields/field_types/search_as_you_type.tsx | 0 .../fields/field_types/shape_type.tsx | 0 .../fields/field_types/text_type.tsx | 0 .../fields/field_types/token_count_type.tsx | 0 .../document_fields/fields/fields_list.tsx | 0 .../fields/fields_list_item.tsx | 0 .../fields/fields_list_item_container.tsx | 10 +-- .../document_fields/fields/index.ts | 0 .../modal_confirmation_delete_fields.tsx | 0 .../document_fields/fields_json_editor.tsx | 0 .../document_fields/fields_tree_editor.tsx | 12 +-- .../components/document_fields/index.ts | 0 .../document_fields/search_fields/index.ts | 0 .../search_fields/search_result.tsx | 0 .../search_fields/search_result_item.tsx | 0 .../components/fields_tree.tsx | 0 .../mappings_editor/components/index.ts | 0 .../components/load_mappings/index.ts | 0 .../load_mappings/load_from_json_button.tsx | 0 .../load_mappings_provider.test.tsx | 2 +- .../load_mappings/load_mappings_provider.tsx | 0 .../components/multiple_mappings_warning.tsx | 0 .../components/templates_form/index.ts | 0 .../templates_form/templates_form.tsx | 6 +- .../templates_form/templates_form_schema.ts | 0 .../mappings_editor/components/tree/index.ts | 0 .../mappings_editor/components/tree/tree.tsx | 0 .../components/tree/tree_item.tsx | 0 .../constants/data_types_definition.tsx | 0 .../constants/default_values.ts | 0 .../constants/field_options.tsx | 0 .../constants/field_options_i18n.ts | 0 .../mappings_editor/constants/index.ts | 0 .../constants/mappings_editor.ts | 0 .../constants/parameters_definition.tsx | 0 .../components/mappings_editor/index.ts | 0 .../index_settings_context.tsx | 0 .../mappings_editor/lib/error_reporter.ts | 0 .../lib/extract_mappings_definition.test.ts | 0 .../lib/extract_mappings_definition.ts | 0 .../components/mappings_editor/lib/index.ts | 0 .../lib/mappings_validator.test.ts | 0 .../mappings_editor/lib/mappings_validator.ts | 0 .../mappings_editor/lib/search_fields.test.ts | 0 .../mappings_editor/lib/search_fields.tsx | 0 .../mappings_editor/lib/serializers.ts | 0 .../mappings_editor/lib/utils.test.ts | 0 .../components/mappings_editor/lib/utils.ts | 0 .../mappings_editor/lib/validators.ts | 0 .../mappings_editor/mappings_editor.tsx | 2 +- .../mappings_editor/mappings_state.tsx | 4 +- .../components/mappings_editor/reducer.ts | 0 .../mappings_editor/shared_imports.ts | 8 +- .../components/mappings_editor/types.ts | 0 .../application/components/no_match/index.ts | 0 .../components/no_match/no_match.tsx | 0 .../components/page_error/index.ts | 0 .../page_error/page_error_forbidden.tsx | 0 .../application/components/section_error.tsx | 0 .../components/section_loading.tsx | 0 .../components/template_delete_modal.tsx | 0 .../components/template_form/index.ts | 0 .../components/template_form/steps/index.ts | 0 .../template_form/steps/step_aliases.tsx | 0 .../template_form/steps/step_logistics.tsx | 15 +--- .../template_form/steps/step_mappings.tsx | 0 .../template_form/steps/step_review.tsx | 2 +- .../template_form/steps/step_settings.tsx | 0 .../template_form/steps/use_json_step.ts | 14 ++-- .../template_form/template_form.tsx | 34 ++++---- .../template_form/template_form_schemas.tsx | 5 +- .../template_form/template_steps.tsx | 0 .../components/template_form/types.ts | 0 .../constants/detail_panel_tabs.ts | 0 .../public/application/constants/index.ts | 0 .../constants/refresh_intervals.ts | 0 .../public/application/index.tsx | 2 +- .../public/application/lib/ace.js | 6 +- .../public/application/lib/edit_settings.js | 0 .../public/application/lib/flatten_object.js | 0 .../application/lib/flatten_panel_tree.js | 0 .../application/lib/index_status_labels.js | 0 .../lib/manage_angular_lifecycle.ts | 0 .../public/application/lib/render_badges.js | 0 .../public/application/sections/home/home.tsx | 0 .../public/application/sections/home/index.ts | 0 .../detail_panel/detail_panel.container.js | 0 .../index_list/detail_panel/detail_panel.js | 0 .../edit_settings_json.container.js | 0 .../edit_settings_json/edit_settings_json.js | 0 .../detail_panel/edit_settings_json/index.js | 0 .../home/index_list/detail_panel/index.js | 0 .../detail_panel/show_json/index.js | 0 .../show_json/show_json.container.js | 0 .../detail_panel/show_json/show_json.js | 0 .../index_list/detail_panel/summary/index.js | 0 .../detail_panel/summary/summary.container.js | 0 .../detail_panel/summary/summary.js | 0 .../sections/home/index_list/index.ts | 0 .../index_actions_context_menu/index.js | 0 .../index_actions_context_menu.container.js | 0 .../index_actions_context_menu.js | 0 .../sections/home/index_list/index_list.d.ts | 0 .../sections/home/index_list/index_list.js | 0 .../home/index_list/index_table/index.js | 0 .../index_table/index_table.container.js | 0 .../index_list/index_table/index_table.js | 0 .../sections/home/template_list/index.ts | 0 .../template_list/template_details/index.ts | 0 .../template_details/tabs/index.ts | 0 .../template_details/tabs/tab_aliases.tsx | 0 .../template_details/tabs/tab_mappings.tsx | 0 .../template_details/tabs/tab_settings.tsx | 0 .../template_details/tabs/tab_summary.tsx | 0 .../template_details/template_details.tsx | 4 +- .../home/template_list/template_list.tsx | 6 +- .../template_list/template_table/index.ts | 0 .../template_table/template_table.tsx | 0 .../sections/template_clone/index.ts | 0 .../template_clone/template_clone.tsx | 4 +- .../sections/template_create/index.ts | 0 .../template_create/template_create.tsx | 0 .../sections/template_edit/index.ts | 0 .../sections/template_edit/template_edit.tsx | 4 +- .../public/application/services/api.ts | 4 +- .../application/services/breadcrumbs.ts | 2 +- .../application/services/documentation.ts | 2 +- .../application/services/health_to_color.ts | 0 .../public/application/services/http.ts | 2 +- .../public/application/services/index.ts | 2 +- .../public/application/services/navigation.ts | 0 .../application/services/notification.ts | 2 +- .../public/application/services/routing.ts | 0 .../public/application/services/sort_table.ts | 0 .../public/application/services/ui_metric.ts | 6 +- .../application/services/use_request.ts | 0 .../store/actions/clear_cache_indices.js | 0 .../store/actions/clear_row_status.js | 0 .../store/actions/close_indices.js | 0 .../store/actions/delete_indices.js | 0 .../application/store/actions/detail_panel.js | 0 .../store/actions/edit_index_settings.js | 0 .../store/actions/extension_action.js | 0 .../store/actions/flush_indices.js | 0 .../store/actions/forcemerge_indices.js | 0 .../store/actions/freeze_indices.js | 0 .../public/application/store/actions/index.js | 0 .../store/actions/load_index_data.js | 0 .../application/store/actions/load_indices.js | 0 .../application/store/actions/open_indices.js | 0 .../store/actions/refresh_indices.js | 0 .../store/actions/reload_indices.js | 0 .../application/store/actions/table_state.js | 0 .../store/actions/unfreeze_indices.js | 0 .../store/actions/update_index_settings.js | 0 .../public/application/store/index.ts | 0 .../store/reducers/detail_panel.js | 0 .../application/store/reducers/index.js | 0 .../store/reducers/index_management.js | 0 .../application/store/reducers/indices.js | 0 .../application/store/reducers/row_status.js | 0 .../application/store/reducers/table_state.js | 0 .../application/store/selectors/index.d.ts | 0 .../application/store/selectors/index.js | 0 .../public/application/store/store.d.ts | 0 .../public/application/store/store.js | 0 .../index_management/public/index.scss | 0 .../plugins/index_management/public/index.ts | 11 +-- .../plugins/index_management/public/mocks.ts | 0 .../plugins/index_management/public/plugin.ts | 10 +-- .../services/extensions_service.mock.ts | 0 .../public/services/extensions_service.ts | 0 .../index_management/public/services/index.ts | 0 .../index_management/public/shared_imports.ts | 33 ++++++++ .../plugins/index_management/public/types.ts | 0 .../index_management/server/config.ts} | 14 ++-- .../plugins/index_management/server/index.ts | 10 +++ .../server/lib/fetch_aliases.test.ts | 0 .../server/lib/fetch_aliases.ts | 0 .../server/lib/fetch_indices.ts | 0 .../server/lib/get_managed_templates.ts | 0 .../server/lib/is_es_error.ts | 0 .../plugins/index_management/server/plugin.ts | 4 +- .../server/routes/api/index.ts | 0 .../server/routes/api/indices/index.ts | 0 .../api/indices/register_clear_cache_route.ts | 0 .../api/indices/register_close_route.ts | 0 .../api/indices/register_delete_route.ts | 0 .../api/indices/register_flush_route.ts | 0 .../api/indices/register_forcemerge_route.ts | 0 .../api/indices/register_freeze_route.ts | 0 .../api/indices/register_indices_routes.ts | 0 .../routes/api/indices/register_list_route.ts | 0 .../routes/api/indices/register_open_route.ts | 0 .../api/indices/register_refresh_route.ts | 0 .../api/indices/register_reload_route.ts | 0 .../api/indices/register_unfreeze_route.ts | 0 .../server/routes/api/mapping/index.ts | 0 .../api/mapping/register_mapping_route.ts | 0 .../server/routes/api/settings/index.ts | 0 .../api/settings/register_load_route.ts | 0 .../api/settings/register_settings_routes.ts | 0 .../api/settings/register_update_route.ts | 0 .../server/routes/api/stats/index.ts | 0 .../routes/api/stats/register_stats_route.ts | 0 .../server/routes/api/templates/index.ts | 0 .../api/templates/register_create_route.ts | 0 .../api/templates/register_delete_route.ts | 0 .../api/templates/register_get_routes.ts | 0 .../api/templates/register_template_routes.ts | 0 .../api/templates/register_update_route.ts | 0 .../routes/api/templates/validate_schemas.ts | 0 .../index_management/server/routes/helpers.ts | 0 .../index_management/server/routes/index.ts | 0 .../index_management/server/services/index.ts | 0 .../server/services/index_data_enricher.ts | 0 .../server/services/license.ts | 5 +- .../plugins/index_management/server/types.ts | 2 +- .../index_management/test/fixtures/index.ts | 0 .../test/fixtures/template.ts | 2 +- 365 files changed, 297 insertions(+), 261 deletions(-) delete mode 100644 x-pack/legacy/plugins/index_management/public/index.html rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/constants.ts (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts (96%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/index.ts (95%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx (95%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts (85%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts (84%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts (85%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts (99%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/home.test.ts (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/template_clone.test.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/template_create.test.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/client_integration/template_edit.test.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/components/index_table.test.js (98%) rename x-pack/{legacy => }/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap (100%) rename x-pack/{legacy => }/plugins/index_management/__jest__/lib/flatten_object.test.js (100%) rename x-pack/{legacy => }/plugins/index_management/__mocks__/ace.js (100%) rename x-pack/{legacy => }/plugins/index_management/__mocks__/ui/documentation_links.js (100%) rename x-pack/{legacy => }/plugins/index_management/__mocks__/ui/notify.js (100%) rename x-pack/{legacy => }/plugins/index_management/common/constants/api_base_path.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/constants/base_path.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/constants/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/constants/index_statuses.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/constants/invalid_characters.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/constants/plugin.ts (86%) rename x-pack/{legacy => }/plugins/index_management/common/constants/ui_metric.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/lib/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/lib/template_serialization.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/types/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/common/types/templates.ts (100%) create mode 100644 x-pack/plugins/index_management/kibana.json rename x-pack/{legacy => }/plugins/index_management/public/application/app.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/app_context.tsx (90%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts (90%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts (85%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/_index.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/_index.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/code_block.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/_index.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx (91%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx (86%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/dynamic_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/enabled_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/relations_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_field_list_item.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_index.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx (83%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx (97%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/join_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/nested_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/object_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx (92%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_json_editor.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields_tree_editor.tsx (88%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/fields_tree.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/multiple_mappings_warning.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/templates_form/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/tree/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/tree/tree.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/components/tree/tree_item.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/field_options_i18n.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/mappings_editor.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/error_reporter.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.test.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.test.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.test.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/lib/validators.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/reducer.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts (72%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/mappings_editor/types.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/no_match/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/no_match/no_match.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/page_error/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/section_error.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/section_loading.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_delete_modal.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx (94%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/step_review.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts (83%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/template_form.tsx (92%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx (96%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/template_steps.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/components/template_form/types.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/constants/detail_panel_tabs.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/constants/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/constants/refresh_intervals.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/index.tsx (94%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/ace.js (94%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/edit_settings.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/flatten_object.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/flatten_panel_tree.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/index_status_labels.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/lib/render_badges.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/home.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.container.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.container.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.container.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.container.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.container.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_list.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_table/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_list.tsx (97%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_table/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/template_clone/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/template_clone/template_clone.tsx (96%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/template_create/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/template_create/template_create.tsx (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/template_edit/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/sections/template_edit/template_edit.tsx (96%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/api.ts (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/breadcrumbs.ts (96%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/documentation.ts (98%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/health_to_color.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/http.ts (88%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/index.ts (96%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/navigation.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/notification.ts (93%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/routing.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/sort_table.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/ui_metric.ts (90%) rename x-pack/{legacy => }/plugins/index_management/public/application/services/use_request.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/clear_cache_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/clear_row_status.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/close_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/delete_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/detail_panel.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/edit_index_settings.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/extension_action.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/flush_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/forcemerge_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/freeze_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/load_index_data.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/load_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/open_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/refresh_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/reload_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/table_state.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/unfreeze_indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/actions/update_index_settings.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/reducers/detail_panel.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/reducers/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/reducers/index_management.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/reducers/indices.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/reducers/row_status.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/reducers/table_state.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/selectors/index.d.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/selectors/index.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/store.d.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/application/store/store.js (100%) rename x-pack/{legacy => }/plugins/index_management/public/index.scss (100%) rename x-pack/{legacy => }/plugins/index_management/public/index.ts (66%) rename x-pack/{legacy => }/plugins/index_management/public/mocks.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/plugin.ts (90%) rename x-pack/{legacy => }/plugins/index_management/public/services/extensions_service.mock.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/services/extensions_service.ts (100%) rename x-pack/{legacy => }/plugins/index_management/public/services/index.ts (100%) create mode 100644 x-pack/plugins/index_management/public/shared_imports.ts rename x-pack/{legacy => }/plugins/index_management/public/types.ts (100%) rename x-pack/{legacy/plugins/index_management/public/shared_imports.ts => plugins/index_management/server/config.ts} (52%) rename x-pack/{legacy => }/plugins/index_management/server/index.ts (67%) rename x-pack/{legacy => }/plugins/index_management/server/lib/fetch_aliases.test.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/lib/fetch_aliases.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/lib/fetch_indices.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/lib/get_managed_templates.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/lib/is_es_error.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/plugin.ts (94%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_close_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_delete_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_flush_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_freeze_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_indices_routes.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_list_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_open_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_refresh_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_reload_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/mapping/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/settings/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/settings/register_load_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/settings/register_settings_routes.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/settings/register_update_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/stats/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/stats/register_stats_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/register_create_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/register_delete_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/register_get_routes.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/register_template_routes.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/register_update_route.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/api/templates/validate_schemas.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/helpers.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/routes/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/services/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/services/index_data_enricher.ts (100%) rename x-pack/{legacy => }/plugins/index_management/server/services/license.ts (89%) rename x-pack/{legacy => }/plugins/index_management/server/types.ts (92%) rename x-pack/{legacy => }/plugins/index_management/test/fixtures/index.ts (100%) rename x-pack/{legacy => }/plugins/index_management/test/fixtures/template.ts (88%) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f22f7e98d3b8a..b1cb9075434e8 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -16,7 +16,7 @@ "xpack.fileUpload": "legacy/plugins/file_upload", "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], "xpack.grokDebugger": "legacy/plugins/grokdebugger", - "xpack.idxMgmt": "legacy/plugins/index_management", + "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", diff --git a/x-pack/legacy/plugins/cross_cluster_replication/index.js b/x-pack/legacy/plugins/cross_cluster_replication/index.js index 1b5f42fc5107e..1b3aafcad5c0f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/index.js @@ -49,13 +49,12 @@ export function crossClusterReplication(kibana) { init: function initCcrPlugin(server) { registerLicenseChecker(server); registerRoutes(server); - if ( server.config().get('xpack.ccr.ui.enabled') && - server.plugins.index_management && - server.plugins.index_management.addIndexManagementDataEnricher + server.newPlatform.setup.plugins.indexManagement && + server.newPlatform.setup.plugins.indexManagement.indexDataEnricher ) { - server.plugins.index_management.addIndexManagementDataEnricher(ccrDataEnricher); + server.newPlatform.setup.plugins.indexManagement.indexDataEnricher.add(ccrDataEnricher); } }, }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js index 16ed0a7d695ad..7b31ffa5024b7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js @@ -7,7 +7,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getIndexListUri } from '../../../../../../../../index_management/public/application/services/navigation'; +import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; import { EuiButtonEmpty, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js index e3bda2e67097d..2ad118d28f38d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -7,8 +7,6 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getIndexListUri } from '../../../../../../../../index_management/public/application/services/navigation'; - import { EuiButton, EuiButtonEmpty, @@ -31,12 +29,11 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; - import 'brace/theme/textmate'; -import { ContextMenu } from '../context_menu'; - +import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; import { API_STATUS } from '../../../../../constants'; +import { ContextMenu } from '../context_menu'; export class DetailPanel extends Component { static propTypes = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js index 809a7c3e87b75..c44918c500849 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { extensionsService } from '../../../index_management/public'; +import { npSetup } from 'ui/new_platform'; import { get } from 'lodash'; const propertyPath = 'isFollowerIndex'; -export const followerBadgeExtension = { + +const followerBadgeExtension = { matchIndex: index => { return get(index, propertyPath); }, @@ -19,4 +20,6 @@ export const followerBadgeExtension = { filterExpression: 'isFollowerIndex:true', }; -extensionsService.addBadge(followerBadgeExtension); +if (npSetup.plugins.indexManagement) { + npSetup.plugins.indexManagement.extensionsService.addBadge(followerBadgeExtension); +} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js index bcbae7e093f46..d2619778617c3 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js @@ -27,8 +27,10 @@ initHttp(axios.create({ adapter: axiosXhrAdapter }), path => path); initUiMetric(() => () => {}); jest.mock('ui/new_platform'); -jest.mock('../../index_management/public', async () => { - const { indexManagementMock } = await import('../../index_management/public/mocks.ts'); +jest.mock('../../../../plugins/index_management/public', async () => { + const { indexManagementMock } = await import( + '../../../../plugins/index_management/public/mocks.ts' + ); return indexManagementMock.createSetup(); }); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts b/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts index 8d7f937039203..38d1bea45ce07 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts +++ b/x-pack/legacy/plugins/index_lifecycle_management/plugin.ts @@ -41,14 +41,14 @@ export class Plugin { registerPoliciesRoutes(server); registerTemplatesRoutes(server); - const serverPlugins = server.plugins as any; + const serverPlugins = server.newPlatform.setup.plugins as any; if ( server.config().get('xpack.ilm.ui.enabled') && - serverPlugins.index_management && - serverPlugins.index_management.addIndexManagementDataEnricher + serverPlugins.indexManagement && + serverPlugins.indexManagement.indexDataEnricher ) { - serverPlugins.index_management.addIndexManagementDataEnricher(indexLifecycleDataEnricher); + serverPlugins.indexManagement.indexDataEnricher.add(indexLifecycleDataEnricher); } } } diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts b/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts index 3c21a644c0f78..f7f924add2c51 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/legacy.ts @@ -20,7 +20,9 @@ import { addAllExtensions } from './np_ready/extend_index_management'; if (chrome.getInjected('ilmUiEnabled')) { // We have to initialize this outside of the NP lifecycle, otherwise these extensions won't // be available in Index Management unless the user visits ILM first. - addAllExtensions(); + if ((npSetup.plugins as any).indexManagement) { + addAllExtensions((npSetup.plugins as any).indexManagement.extensionsService); + } // This method handles the cleanup needed when route is scope is destroyed. It also prevents Angular // from destroying scope when route changes and both old route and new route are this same route. diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js index 83d32eae1097d..903161fe094fc 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/application/sections/policy_table/components/policy_table/policy_table.js @@ -37,7 +37,7 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; -import { getIndexListUri } from '../../../../../../../../index_management/public/application/services/navigation'; +import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; import { BASE_PATH } from '../../../../../../../common/constants'; import { UIM_EDIT_CLICK } from '../../../../constants'; import { getPolicyPath } from '../../../../services/navigation'; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js index 0e662b78b2c18..69658d31695bc 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/np_ready/extend_index_management/index.js @@ -9,7 +9,6 @@ import { get, every, any } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; -import { extensionsService } from '../../../../index_management/public'; import { init as initUiMetric } from '../application/services/ui_metric'; import { init as initNotification } from '../application/services/notification'; import { retryLifecycleForIndex } from '../application/services/api'; @@ -238,7 +237,7 @@ export const ilmFilterExtension = indices => { } }; -export const addAllExtensions = () => { +export const addAllExtensions = extensionsService => { extensionsService.addAction(retryLifecycleActionExtension); extensionsService.addAction(removeLifecyclePolicyActionExtension); extensionsService.addAction(addLifecyclePolicyActionExtension); diff --git a/x-pack/legacy/plugins/index_management/index.ts b/x-pack/legacy/plugins/index_management/index.ts index c92b38c0d94be..9eba98a526d2b 100644 --- a/x-pack/legacy/plugins/index_management/index.ts +++ b/x-pack/legacy/plugins/index_management/index.ts @@ -4,36 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; -import { Legacy } from 'kibana'; -import { PLUGIN } from './common/constants'; -import { plugin as initServerPlugin, Dependencies } from './server'; - -export type ServerFacade = Legacy.Server; - export function indexManagement(kibana: any) { return new kibana.Plugin({ - id: PLUGIN.id, + id: 'index_management', configPrefix: 'xpack.index_management', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/index_management'], - }, - - init(server: ServerFacade) { - const coreSetup = server.newPlatform.setup.core; - const coreInitializerContext = server.newPlatform.coreContext; - const pluginsSetup: Dependencies = { - licensing: server.newPlatform.setup.plugins.licensing as any, - }; - - const serverPlugin = initServerPlugin(coreInitializerContext as any); - const serverPublicApi = serverPlugin.setup(coreSetup, pluginsSetup); - - server.expose('addIndexManagementDataEnricher', serverPublicApi.indexDataEnricher.add); - }, }); } diff --git a/x-pack/legacy/plugins/index_management/public/index.html b/x-pack/legacy/plugins/index_management/public/index.html deleted file mode 100644 index 0e9ac6fa0bc97..0000000000000 --- a/x-pack/legacy/plugins/index_management/public/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/legacy/plugins/remote_clusters/index.ts b/x-pack/legacy/plugins/remote_clusters/index.ts index 37b2224f8d7c2..439cb818d8a56 100644 --- a/x-pack/legacy/plugins/remote_clusters/index.ts +++ b/x-pack/legacy/plugins/remote_clusters/index.ts @@ -5,6 +5,8 @@ */ import { resolve } from 'path'; +import { Legacy } from 'kibana'; + import { PLUGIN } from './common'; export function remoteClusters(kibana: any) { @@ -28,7 +30,7 @@ export function remoteClusters(kibana: any) { enabled: Joi.boolean().default(true), }).default(); }, - isEnabled(config: any) { + isEnabled(config: Legacy.KibanaConfig) { return ( config.get('xpack.remote_clusters.enabled') && config.get('xpack.index_management.enabled') ); diff --git a/x-pack/legacy/plugins/rollup/index.ts b/x-pack/legacy/plugins/rollup/index.ts index 7548af23b3aae..2c8363cc397f4 100644 --- a/x-pack/legacy/plugins/rollup/index.ts +++ b/x-pack/legacy/plugins/rollup/index.ts @@ -40,7 +40,7 @@ export function rollup(kibana: any) { }, init(server: any) { const { core: coreSetup, plugins } = server.newPlatform.setup; - const { usageCollection, metrics } = plugins; + const { usageCollection, metrics, indexManagement } = plugins; const rollupSetup = (plugins.rollup as unknown) as RollupSetup; @@ -54,11 +54,11 @@ export function rollup(kibana: any) { rollupPluginInstance.setup(coreSetup, { usageCollection, metrics, + indexManagement, __LEGACY: { plugins: { xpack_main: server.plugins.xpack_main, rollup: server.plugins[PLUGIN.ID], - index_management: server.plugins.index_management, }, }, }); diff --git a/x-pack/legacy/plugins/rollup/public/legacy.ts b/x-pack/legacy/plugins/rollup/public/legacy.ts index 64eb1f6436389..e3e663ac7b0f4 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy.ts @@ -10,7 +10,6 @@ import { aggTypeFieldFilters } from 'ui/agg_types'; import { addSearchStrategy } from '../../../../../src/plugins/data/public'; import { RollupPlugin } from './plugin'; import { setup as management } from '../../../../../src/legacy/core_plugins/management/public/legacy'; -import { extensionsService } from '../../index_management/public'; const plugin = new RollupPlugin(); @@ -20,7 +19,6 @@ export const setup = plugin.setup(npSetup.core, { aggTypeFilters, aggTypeFieldFilters, addSearchStrategy, - indexManagementExtensions: extensionsService, managementLegacy: management, }, }); diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts index 90d7e2d9d0191..a01383f4733ef 100644 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ b/x-pack/legacy/plugins/rollup/public/plugin.ts @@ -27,7 +27,7 @@ import { // @ts-ignore import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup } from '../../../../../src/plugins/management/public'; -import { IndexMgmtSetup } from '../../index_management/public'; +import { IndexMgmtSetup } from '../../../../plugins/index_management/public'; // @ts-ignore import { setEsBaseAndXPackBase, setHttp } from './crud_app/services'; import { setNotifications, setFatalErrors } from './kibana_services'; @@ -39,30 +39,28 @@ export interface RollupPluginSetupDependencies { aggTypeFieldFilters: AggTypeFieldFilters; addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void; managementLegacy: ManagementSetupLegacy; - indexManagementExtensions: IndexMgmtSetup['extensionsService']; }; home?: HomePublicPluginSetup; management: ManagementSetup; + indexManagement?: IndexMgmtSetup; } export class RollupPlugin implements Plugin { setup( core: CoreSetup, { - __LEGACY: { - aggTypeFilters, - aggTypeFieldFilters, - addSearchStrategy, - managementLegacy, - indexManagementExtensions, - }, + __LEGACY: { aggTypeFilters, aggTypeFieldFilters, addSearchStrategy, managementLegacy }, home, management, + indexManagement, }: RollupPluginSetupDependencies ) { setFatalErrors(core.fatalErrors); - indexManagementExtensions.addBadge(rollupBadgeExtension); - indexManagementExtensions.addToggle(rollupToggleExtension); + + if (indexManagement) { + indexManagement.extensionsService.addBadge(rollupBadgeExtension); + indexManagement.extensionsService.addToggle(rollupToggleExtension); + } const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts index 52b1e31af4eb2..090cb8a47377a 100644 --- a/x-pack/legacy/plugins/rollup/server/plugin.ts +++ b/x-pack/legacy/plugins/rollup/server/plugin.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; +import { IndexMgmtSetup } from '../../../../plugins/index_management/server'; import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; import { PLUGIN } from '../common'; import { ServerShim, RouteDependencies } from './types'; @@ -38,10 +39,12 @@ export class RollupsServerPlugin implements Plugin { __LEGACY: serverShim, usageCollection, metrics, + indexManagement, }: { __LEGACY: ServerShim; usageCollection?: UsageCollectionSetup; metrics?: VisTypeTimeseriesSetup; + indexManagement?: IndexMgmtSetup; } ) { const elasticsearch = await elasticsearchService.adminClient; @@ -76,11 +79,8 @@ export class RollupsServerPlugin implements Plugin { }); } - if ( - serverShim.plugins.index_management && - serverShim.plugins.index_management.addIndexManagementDataEnricher - ) { - serverShim.plugins.index_management.addIndexManagementDataEnricher(rollupDataEnricher); + if (indexManagement && indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(rollupDataEnricher); } if (metrics) { diff --git a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts b/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts index 7c5e160c54a31..ad621f2d9ba80 100644 --- a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts +++ b/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Index { - name: string; - [key: string]: unknown; -} +import { Index } from '../../../../plugins/index_management/server'; export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => { if (!indicesList || !indicesList.length) { - return indicesList; + return Promise.resolve(indicesList); } const params = { diff --git a/x-pack/legacy/plugins/rollup/server/types.ts b/x-pack/legacy/plugins/rollup/server/types.ts index 62a4841133cff..bcc6770e9b8ee 100644 --- a/x-pack/legacy/plugins/rollup/server/types.ts +++ b/x-pack/legacy/plugins/rollup/server/types.ts @@ -11,7 +11,6 @@ export interface ServerShim { plugins: { xpack_main: XPackMainPlugin; rollup: any; - index_management: any; }; } diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/constants.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts similarity index 96% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts index e5f0b25d89c3e..7e3e1fba9c44a 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts @@ -12,10 +12,10 @@ import { TestBedConfig, findTestSubject, nextTick, -} from '../../../../../../test_utils'; -import { IndexManagementHome } from '../../../public/application/sections/home'; +} from '../../../../../test_utils'; +import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { BASE_PATH } from '../../../common/constants'; -import { indexManagementStore } from '../../../public/application/store'; +import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { Template } from '../../../common/types'; import { WithAppDependencies, services } from './setup_environment'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts similarity index 95% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 6dce4453a67f9..66021b531919a 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -9,7 +9,7 @@ import { setup as templateCreateSetup } from './template_create.helpers'; import { setup as templateCloneSetup } from './template_clone.helpers'; import { setup as templateEditSetup } from './template_edit.helpers'; -export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils'; +export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils'; export { setupEnvironment } from './setup_environment'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx similarity index 95% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 0212efe1f322d..1eaf7efd17395 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ import React from 'react'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; @@ -10,7 +11,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { notificationServiceMock, docLinksServiceMock, -} from '../../../../../../../src/core/public/mocks'; +} from '../../../../../../src/core/public/mocks'; import { AppContextProvider } from '../../../public/application/app_context'; import { httpService } from '../../../public/application/services/http'; import { breadcrumbService } from '../../../public/application/services/breadcrumbs'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts similarity index 85% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts index cd1b67c08d934..36498b99ba143 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_clone.helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerTestBed, TestBedConfig } from '../../../../../../test_utils'; +import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; import { BASE_PATH } from '../../../common/constants'; -import { TemplateClone } from '../../../public/application/sections/template_clone'; +import { TemplateClone } from '../../../public/application/sections/template_clone'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { formSetup } from './template_form.helpers'; import { TEMPLATE_NAME } from './constants'; import { WithAppDependencies } from './setup_environment'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts similarity index 84% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts index 8e62bc25d6bd1..14a44968a93c3 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_create.helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerTestBed, TestBedConfig } from '../../../../../../test_utils'; +import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; import { BASE_PATH } from '../../../common/constants'; -import { TemplateCreate } from '../../../public/application/sections/template_create'; +import { TemplateCreate } from '../../../public/application/sections/template_create'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { formSetup, TestSubjects } from './template_form.helpers'; import { WithAppDependencies } from './setup_environment'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts similarity index 85% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts index cb1025234b48e..af5fa8b79ecad 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_edit.helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerTestBed, TestBedConfig } from '../../../../../../test_utils'; +import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; import { BASE_PATH } from '../../../common/constants'; -import { TemplateEdit } from '../../../public/application/sections/template_edit'; +import { TemplateEdit } from '../../../public/application/sections/template_edit'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { formSetup, TestSubjects } from './template_form.helpers'; import { TEMPLATE_NAME } from './constants'; import { WithAppDependencies } from './setup_environment'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts similarity index 99% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts rename to x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts index a7c87723b33fb..134c67c278b22 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/template_form.helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../../test_utils'; +import { TestBed, SetupFunc, UnwrapPromise } from '../../../../../test_utils'; import { Template } from '../../../common/types'; import { nextTick } from './index'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home.test.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts rename to x-pack/plugins/index_management/__jest__/client_integration/home.test.ts diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx rename to x-pack/plugins/index_management/__jest__/client_integration/template_edit.test.tsx diff --git a/x-pack/legacy/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap b/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap rename to x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap diff --git a/x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js similarity index 98% rename from x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js rename to x-pack/plugins/index_management/__jest__/components/index_table.test.js index 2b4fd89436458..15c3ef0b84562 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -20,13 +20,13 @@ import { setUiMetricService } from '../../public/application/services/api'; import { indexManagementStore } from '../../public/application/store'; import { setExtensionsService } from '../../public/application/store/selectors'; import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; -import { mountWithIntl } from '../../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; import { ExtensionsService } from '../../public/services'; import sinon from 'sinon'; import { findTestSubject } from '@elastic/eui/lib/test'; /* eslint-disable @kbn/eslint/no-restricted-paths */ -import { notificationServiceMock } from '../../../../../../src/core/public/notifications/notifications_service.mock'; +import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock'; jest.mock('ui/new_platform'); diff --git a/x-pack/legacy/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap b/x-pack/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap rename to x-pack/plugins/index_management/__jest__/lib/__snapshots__/flatten_object.test.js.snap diff --git a/x-pack/legacy/plugins/index_management/__jest__/lib/flatten_object.test.js b/x-pack/plugins/index_management/__jest__/lib/flatten_object.test.js similarity index 100% rename from x-pack/legacy/plugins/index_management/__jest__/lib/flatten_object.test.js rename to x-pack/plugins/index_management/__jest__/lib/flatten_object.test.js diff --git a/x-pack/legacy/plugins/index_management/__mocks__/ace.js b/x-pack/plugins/index_management/__mocks__/ace.js similarity index 100% rename from x-pack/legacy/plugins/index_management/__mocks__/ace.js rename to x-pack/plugins/index_management/__mocks__/ace.js diff --git a/x-pack/legacy/plugins/index_management/__mocks__/ui/documentation_links.js b/x-pack/plugins/index_management/__mocks__/ui/documentation_links.js similarity index 100% rename from x-pack/legacy/plugins/index_management/__mocks__/ui/documentation_links.js rename to x-pack/plugins/index_management/__mocks__/ui/documentation_links.js diff --git a/x-pack/legacy/plugins/index_management/__mocks__/ui/notify.js b/x-pack/plugins/index_management/__mocks__/ui/notify.js similarity index 100% rename from x-pack/legacy/plugins/index_management/__mocks__/ui/notify.js rename to x-pack/plugins/index_management/__mocks__/ui/notify.js diff --git a/x-pack/legacy/plugins/index_management/common/constants/api_base_path.ts b/x-pack/plugins/index_management/common/constants/api_base_path.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/constants/api_base_path.ts rename to x-pack/plugins/index_management/common/constants/api_base_path.ts diff --git a/x-pack/legacy/plugins/index_management/common/constants/base_path.ts b/x-pack/plugins/index_management/common/constants/base_path.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/constants/base_path.ts rename to x-pack/plugins/index_management/common/constants/base_path.ts diff --git a/x-pack/legacy/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/constants/index.ts rename to x-pack/plugins/index_management/common/constants/index.ts diff --git a/x-pack/legacy/plugins/index_management/common/constants/index_statuses.ts b/x-pack/plugins/index_management/common/constants/index_statuses.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/constants/index_statuses.ts rename to x-pack/plugins/index_management/common/constants/index_statuses.ts diff --git a/x-pack/legacy/plugins/index_management/common/constants/invalid_characters.ts b/x-pack/plugins/index_management/common/constants/invalid_characters.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/constants/invalid_characters.ts rename to x-pack/plugins/index_management/common/constants/invalid_characters.ts diff --git a/x-pack/legacy/plugins/index_management/common/constants/plugin.ts b/x-pack/plugins/index_management/common/constants/plugin.ts similarity index 86% rename from x-pack/legacy/plugins/index_management/common/constants/plugin.ts rename to x-pack/plugins/index_management/common/constants/plugin.ts index 2cd137f62d3db..e25f537edcf8d 100644 --- a/x-pack/legacy/plugins/index_management/common/constants/plugin.ts +++ b/x-pack/plugins/index_management/common/constants/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LicenseType } from '../../../../../plugins/licensing/common/types'; +import { LicenseType } from '../../../licensing/common/types'; const basicLicense: LicenseType = 'basic'; diff --git a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts b/x-pack/plugins/index_management/common/constants/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts rename to x-pack/plugins/index_management/common/constants/ui_metric.ts diff --git a/x-pack/legacy/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/index.ts rename to x-pack/plugins/index_management/common/index.ts diff --git a/x-pack/legacy/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/lib/index.ts rename to x-pack/plugins/index_management/common/lib/index.ts diff --git a/x-pack/legacy/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/lib/template_serialization.ts rename to x-pack/plugins/index_management/common/lib/template_serialization.ts diff --git a/x-pack/legacy/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/types/index.ts rename to x-pack/plugins/index_management/common/types/index.ts diff --git a/x-pack/legacy/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/common/types/templates.ts rename to x-pack/plugins/index_management/common/types/templates.ts diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json new file mode 100644 index 0000000000000..7387a042988c0 --- /dev/null +++ b/x-pack/plugins/index_management/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "indexManagement", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "home", + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": ["xpack", "index_management"] +} diff --git a/x-pack/legacy/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/app.tsx rename to x-pack/plugins/index_management/public/application/app.tsx index 3b475f3baba04..83997dd6ece18 100644 --- a/x-pack/legacy/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -16,7 +16,7 @@ import { useServices } from './app_context'; export const App = () => { const { uiMetricService } = useServices(); - useEffect(() => uiMetricService.trackMetric('loaded', UIM_APP_LOAD), []); + useEffect(() => uiMetricService.trackMetric('loaded', UIM_APP_LOAD), [uiMetricService]); return ( diff --git a/x-pack/legacy/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx similarity index 90% rename from x-pack/legacy/plugins/index_management/public/application/app_context.tsx rename to x-pack/plugins/index_management/public/application/app_context.tsx index 12e0d362a2930..2bb618ad8efce 100644 --- a/x-pack/legacy/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -5,9 +5,9 @@ */ import React, { createContext, useContext } from 'react'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart } from '../../../../../src/core/public'; -import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/public'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; import { IndexMgmtMetricsType } from '../types'; import { UiMetricService, NotificationService, HttpService } from './services'; import { ExtensionsService } from '../services'; diff --git a/x-pack/legacy/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/index.ts rename to x-pack/plugins/index_management/public/application/components/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts similarity index 90% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts index e3313bfba56fd..6d64cb73da4bd 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/index.ts @@ -10,7 +10,7 @@ export { getRandomString, findTestSubject, TestBed, -} from '../../../../../../../../../../test_utils'; +} from '../../../../../../../../../test_utils'; export const componentHelpers = { mappingsEditor: { setup: mappingsEditorSetup }, diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts similarity index 85% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts index dcee956130a29..acb416654023e 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerTestBed } from '../../../../../../../../../../test_utils'; +import { registerTestBed } from '../../../../../../../../../test_utils'; import { MappingsEditor } from '../../../mappings_editor'; export const setup = (props: any) => diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/_index.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/_index.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/_index.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/_index.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/_index.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/_index.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/_index.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/code_block.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/code_block.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/code_block.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/code_block.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 0c5c9e2a15b75..9b0b8420f9ea9 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -110,7 +110,7 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { }); }); return subscription.unsubscribe; - }, [form]); + }, [form, dispatch]); useEffect(() => { if (didMountRef.current) { @@ -121,14 +121,14 @@ export const ConfigurationForm = React.memo(({ defaultValue }: Props) => { // Avoid reseting the form on component mount. didMountRef.current = true; } - }, [defaultValue]); + }, [defaultValue, form]); useEffect(() => { return () => { // On unmount => save in the state a snapshot of the current form data. dispatch({ type: 'configuration.save' }); }; - }, []); + }, [dispatch]); return ( diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/dynamic_mapping_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/dynamic_mapping_section/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/meta_field_section/meta_field_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/routing_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/_index.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/_index.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/_index.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/_index.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx similarity index 91% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx index 378d669dee69c..400de4052afa4 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields.tsx @@ -24,7 +24,7 @@ export const DocumentFields = React.memo(() => { if (editorType === 'json') { return deNormalize(fields); } - }, [editorType]); + }, [editorType, fields]); const editor = editorType === 'json' ? ( @@ -41,9 +41,12 @@ export const DocumentFields = React.memo(() => { return ; }; - const onSearchChange = useCallback((value: string) => { - dispatch({ type: 'search:update', value }); - }, []); + const onSearchChange = useCallback( + (value: string) => { + dispatch({ type: 'search:update', value }); + }, + [dispatch] + ); const searchTerm = search.term.trim(); diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/document_fields_header.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/editor_toggle_controls.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx similarity index 86% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx index de3d70db31af4..a91231352c168 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzer_parameter_selects.tsx @@ -56,15 +56,21 @@ export const AnalyzerParameterSelects = ({ }); return subscription.unsubscribe; - }, [form]); + }, [form, onChange]); - const getSubOptionsMeta = (mainValue: string) => - mapOptionsToSubOptions !== undefined ? mapOptionsToSubOptions[mainValue] : undefined; + const getSubOptionsMeta = useCallback( + (mainValue: string) => + mapOptionsToSubOptions !== undefined ? mapOptionsToSubOptions[mainValue] : undefined, + [mapOptionsToSubOptions] + ); - const onMainValueChange = useCallback((mainValue: unknown) => { - const subOptionsMeta = getSubOptionsMeta(mainValue as string); - form.setFieldValue('sub', subOptionsMeta ? subOptionsMeta.options[0].value : undefined); - }, []); + const onMainValueChange = useCallback( + (mainValue: unknown) => { + const subOptionsMeta = getSubOptionsMeta(mainValue as string); + form.setFieldValue('sub', subOptionsMeta ? subOptionsMeta.options[0].value : undefined); + }, + [form, getSubOptionsMeta] + ); const renderSelect = (field: FieldHook, opts: Options) => { const isSuperSelect = areOptionsSuperSelect(opts); diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/analyzers_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/boost_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_number_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/coerce_shape_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/copy_to_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/doc_values_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/dynamic_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/dynamic_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/dynamic_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/dynamic_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/eager_global_ordinals_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/enabled_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/enabled_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/enabled_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/enabled_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_absolute.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_frequency_filter_percentage.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/fielddata_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/format_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_malformed.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/locale_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/name_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/norms_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/null_value_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/orientation_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/path_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/relations_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/relations_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/relations_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/relations_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/similarity_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/split_queries_on_whitespace_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/store_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/term_vector_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/type_parameter.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_field_list_item.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_field_list_item.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_field_list_item.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_field_list_item.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_index.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_index.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_index.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/_index.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx similarity index 83% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index 5f1b8c0df770e..60b025ce644ef 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -66,7 +66,7 @@ export const CreateField = React.memo(function CreateFieldComponent({ }); return subscription.unsubscribe; - }, [form]); + }, [form, dispatch]); const cancel = () => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); @@ -108,43 +108,53 @@ export const CreateField = React.memo(function CreateFieldComponent({ * * @param type The selected field type */ - const getSubTypeMeta = ( - type: MainType - ): { subTypeLabel?: string; subTypeOptions?: ComboBoxOption[] } => { - const typeDefinition = TYPE_DEFINITION[type]; - const hasSubTypes = typeDefinition !== undefined && typeDefinition.subTypes; - - let subTypeOptions = hasSubTypes - ? typeDefinition - .subTypes!.types.map(subType => TYPE_DEFINITION[subType]) - .map( - subType => ({ value: subType.value as SubType, label: subType.label } as ComboBoxOption) - ) - : undefined; - - if (hasSubTypes) { - if (isMultiField) { - // If it is a multi-field, we need to filter out non-allowed types - subTypeOptions = filterTypesForMultiField(subTypeOptions!); - } else if (isRootLevelField === false) { - subTypeOptions = filterTypesForNonRootFields(subTypeOptions!); + const getSubTypeMeta = useCallback( + ( + type: MainType + ): { + subTypeLabel?: string; + subTypeOptions?: ComboBoxOption[]; + } => { + const typeDefinition = TYPE_DEFINITION[type]; + const hasSubTypes = typeDefinition !== undefined && typeDefinition.subTypes; + + let subTypeOptions = hasSubTypes + ? typeDefinition + .subTypes!.types.map(subType => TYPE_DEFINITION[subType]) + .map( + subType => + ({ value: subType.value as SubType, label: subType.label } as ComboBoxOption) + ) + : undefined; + + if (hasSubTypes) { + if (isMultiField) { + // If it is a multi-field, we need to filter out non-allowed types + subTypeOptions = filterTypesForMultiField(subTypeOptions!); + } else if (isRootLevelField === false) { + subTypeOptions = filterTypesForNonRootFields(subTypeOptions!); + } } - } - return { - subTypeOptions, - subTypeLabel: hasSubTypes ? typeDefinition.subTypes!.label : undefined, - }; - }; + return { + subTypeOptions, + subTypeLabel: hasSubTypes ? typeDefinition.subTypes!.label : undefined, + }; + }, + [isMultiField, isRootLevelField] + ); - const onTypeChange = (nextType: ComboBoxOption[]) => { - form.setFieldValue('type', nextType); + const onTypeChange = useCallback( + (nextType: ComboBoxOption[]) => { + form.setFieldValue('type', nextType); - if (nextType.length) { - const { subTypeOptions } = getSubTypeMeta(nextType[0].value as MainType); - form.setFieldValue('subType', subTypeOptions ? [subTypeOptions[0]] : undefined); - } - }; + if (nextType.length) { + const { subTypeOptions } = getSubTypeMeta(nextType[0].value as MainType); + form.setFieldValue('subType', subTypeOptions ? [subTypeOptions[0]] : undefined); + } + }, + [form, getSubTypeMeta] + ); const renderFormFields = useCallback( ({ type }) => { @@ -208,7 +218,7 @@ export const CreateField = React.memo(function CreateFieldComponent({
); }, - [form, isMultiField] + [getSubTypeMeta, isMultiField, isRootLevelField, onTypeChange] ); const renderFormActions = () => ( diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/alias_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/dense_vector_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/scaled_float_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/required_parameters_forms/token_count_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/delete_field_provider.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_edit_field_form_row.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/_index.scss diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/advanced_parameters_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/basic_parameters_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx similarity index 97% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx index 284ae8803acb5..1f77584439568 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_container.tsx @@ -32,11 +32,11 @@ export const EditFieldContainer = React.memo(({ field, allFields }: Props) => { }); return subscription.unsubscribe; - }, [form]); + }, [form, dispatch]); const exitEdit = useCallback(() => { dispatch({ type: 'documentField.changeStatus', value: 'idle' }); - }, []); + }, [dispatch]); return ; }); diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_form_row.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/field_description_section.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/update_field_provider.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/alias_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/dense_vector_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_shape_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/ip_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/join_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/join_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/join_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/join_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/nested_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/nested_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/nested_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/nested_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/object_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/object_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/object_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/object_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/shape_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx similarity index 92% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx index cff2d294fead9..55093e606cfa1 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item_container.tsx @@ -23,7 +23,7 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop fields: { byId, maxNestedDepth }, } = useMappingsState(); - const getField = (id: string) => byId[id]; + const getField = useCallback((id: string) => byId[id], [byId]); const field: NormalizedField = getField(fieldId); const { childFields } = field; @@ -33,7 +33,7 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop const areActionButtonsVisible = status === 'idle'; const childFieldsArray = useMemo( () => (childFields !== undefined ? childFields.map(getField) : []), - [childFields] + [childFields, getField] ); const addField = useCallback(() => { @@ -41,18 +41,18 @@ export const FieldsListItemContainer = ({ fieldId, treeDepth, isLastItem }: Prop type: 'documentField.createField', value: fieldId, }); - }, [fieldId]); + }, [fieldId, dispatch]); const editField = useCallback(() => { dispatch({ type: 'documentField.editField', value: fieldId, }); - }, [fieldId]); + }, [fieldId, dispatch]); const toggleExpand = useCallback(() => { dispatch({ type: 'field.toggleExpand', value: { fieldId } }); - }, [fieldId]); + }, [fieldId, dispatch]); return ( { documentFields: { status, fieldToAddFieldTo }, } = useMappingsState(); - const getField = (fieldId: string) => byId[fieldId]; - const fields = useMemo(() => rootLevelFields.map(getField), [rootLevelFields]); + const getField = useCallback((fieldId: string) => byId[fieldId], [byId]); + const fields = useMemo(() => rootLevelFields.map(getField), [rootLevelFields, getField]); - const addField = () => { + const addField = useCallback(() => { dispatch({ type: 'documentField.createField' }); - }; + }, [dispatch]); useEffect(() => { /** @@ -32,7 +32,7 @@ export const DocumentFieldsTreeEditor = () => { if (status === 'idle' && fields.length === 0) { addField(); } - }, [fields, status]); + }, [addField, fields, status]); const renderCreateField = () => { // The "fieldToAddFieldTo" is undefined when adding to the top level "properties" object. diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/fields_tree.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/fields_tree.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/fields_tree.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/fields_tree.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx index a9433d3a7530f..f8e3eca7898d2 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx @@ -19,7 +19,7 @@ jest.mock('@elastic/eui', () => ({ ), })); -import { registerTestBed, nextTick, TestBed } from '../../../../../../../../../test_utils'; +import { registerTestBed, nextTick, TestBed } from '../../../../../../../../test_utils'; import { LoadMappingsProvider } from './load_mappings_provider'; const ComponentToTest = ({ onJson }: { onJson: () => void }) => ( diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/multiple_mappings_warning.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/multiple_mappings_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/multiple_mappings_warning.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/multiple_mappings_warning.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index 471217108ba6f..f32fcb3956e1c 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -69,7 +69,7 @@ export const TemplatesForm = React.memo(({ defaultValue }: Props) => { }); }); return subscription.unsubscribe; - }, [form]); + }, [form, dispatch]); useEffect(() => { if (didMountRef.current) { @@ -80,14 +80,14 @@ export const TemplatesForm = React.memo(({ defaultValue }: Props) => { // Avoid reseting the form on component mount. didMountRef.current = true; } - }, [defaultValue]); + }, [defaultValue, form]); useEffect(() => { return () => { // On unmount => save in the state a snapshot of the current form data. dispatch({ type: 'templates.save' }); }; - }, []); + }, [dispatch]); return ( <> diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form_schema.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/tree/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/tree/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/tree/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/tree/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/tree/tree.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/tree/tree.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/tree/tree.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/tree/tree.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/tree/tree_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/tree/tree_item.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/components/tree/tree_item.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/components/tree/tree_item.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/default_values.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/field_options_i18n.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options_i18n.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/field_options_i18n.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options_i18n.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/mappings_editor.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/mappings_editor.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/mappings_editor.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/mappings_editor.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/index_settings_context.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/error_reporter.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/error_reporter.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/error_reporter.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/error_reporter.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.test.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.test.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.test.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/extract_mappings_definition.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/index.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.test.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.test.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.test.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/mappings_validator.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.test.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.test.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.test.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/validators.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/validators.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/lib/validators.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/lib/validators.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index d79a023386e8d..b6345a7140e15 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -78,7 +78,7 @@ export const MappingsEditor = React.memo(({ onUpdate, defaultValue, indexSetting isValid: true, }); } - }, [multipleMappingsDeclared]); + }, [multipleMappingsDeclared, onUpdate, defaultValue]); const changeTab = async (tab: TabName, state: State) => { if (selectedTab === 'advanced') { diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx rename to x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx index 65a1aa2858d14..247bd183baddf 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx @@ -168,7 +168,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P }, isValid: state.isValid, }); - }, [state]); + }, [state, onUpdate]); useEffect(() => { /** @@ -187,7 +187,7 @@ export const MappingsState = React.memo(({ children, onUpdate, defaultValue }: P } else { didMountRef.current = true; } - }, [defaultValue]); + }, [defaultValue, parsedFieldsDefaultValue]); return ( diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/reducer.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts similarity index 72% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index e99d8840d57df..2979015c07455 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -24,7 +24,7 @@ export { VALIDATION_TYPES, ValidationFunc, ValidationFuncArg, -} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { CheckBoxField, @@ -39,14 +39,14 @@ export { ComboBoxField, ToggleField, JsonEditorField, -} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/components'; export { fieldFormatters, fieldValidators, -} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; export { JsonEditor, OnJsonEditorUpdateHandler, -} from '../../../../../../../../src/plugins/es_ui_shared/public'; +} from '../../../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/types.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/mappings_editor/types.ts rename to x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/no_match/index.ts b/x-pack/plugins/index_management/public/application/components/no_match/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/no_match/index.ts rename to x-pack/plugins/index_management/public/application/components/no_match/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/no_match/no_match.tsx b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/no_match/no_match.tsx rename to x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/page_error/index.ts b/x-pack/plugins/index_management/public/application/components/page_error/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/page_error/index.ts rename to x-pack/plugins/index_management/public/application/components/page_error/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx rename to x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/section_error.tsx b/x-pack/plugins/index_management/public/application/components/section_error.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/section_error.tsx rename to x-pack/plugins/index_management/public/application/components/section_error.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/section_loading.tsx b/x-pack/plugins/index_management/public/application/components/section_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/section_loading.tsx rename to x-pack/plugins/index_management/public/application/components/section_loading.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_delete_modal.tsx rename to x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/index.ts rename to x-pack/plugins/index_management/public/application/components/template_form/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/index.ts rename to x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx similarity index 94% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index dd8d49a569042..2f6e055b5d0c6 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -7,15 +7,8 @@ import React, { useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - useForm, - Form, - getUseField, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { - getFormRow, - Field, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; + +import { useForm, Form, getUseField, getFormRow, Field } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { StepProps } from '../types'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -80,11 +73,11 @@ export const StepLogistics: React.FunctionComponent = ({ useEffect(() => { onStepValidityChange(form.isValid); - }, [form.isValid]); + }, [form.isValid, onStepValidityChange]); useEffect(() => { setDataGetter(form.submit); - }, [form]); + }, [form.submit, setDataGetter]); const { name, indexPatterns, order, version } = fieldsMeta; diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_review.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 09172bf5cd0ca..09da43b83c3c5 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -20,7 +20,7 @@ import { EuiCodeBlock, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { serializers } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +import { serializers } from '../../../../shared_imports'; import { serializeTemplate } from '../../../../../common/lib/template_serialization'; import { Template } from '../../../../../common/types'; diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts similarity index 83% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts rename to x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts index ae16a2e9263ff..fbe479ea0cf23 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { isJSON } from '../../../../../../../../../src/plugins/es_ui_shared/static/validators/string'; +import { isJSON } from '../../../../shared_imports'; import { StepProps } from '../types'; interface Parameters { @@ -29,7 +29,7 @@ export const useJsonStep = ({ const [content, setContent] = useState(stringifyJson(defaultValue)); const [error, setError] = useState(null); - const validateContent = () => { + const validateContent = useCallback(() => { // We allow empty string as it will be converted to "{}"" const isValid = content.trim() === '' ? true : isJSON(content); if (!isValid) { @@ -42,20 +42,20 @@ export const useJsonStep = ({ setError(null); } return isValid; - }; + }, [content]); - const dataGetter = () => { + const dataGetter = useCallback(() => { const isValid = validateContent(); const value = isValid && content.trim() !== '' ? JSON.parse(content) : {}; const data = { [prop]: value }; return Promise.resolve({ isValid, data }); - }; + }, [content, validateContent, prop]); useEffect(() => { const isValid = validateContent(); onStepValidityChange(isValid); setDataGetter(dataGetter); - }, [content]); + }, [content, dataGetter, onStepValidityChange, setDataGetter, validateContent]); return { content, diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx similarity index 92% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/template_form.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 6a76e1d203b70..58be9b2c63365 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -3,7 +3,7 @@ * 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, { Fragment, useState, useRef } from 'react'; +import React, { Fragment, useState, useRef, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -14,7 +14,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { serializers } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +import { serializers } from '../../../shared_imports'; import { Template } from '../../../../common/types'; import { TemplateSteps } from './template_steps'; import { StepAliases, StepLogistics, StepMappings, StepSettings, StepReview } from './steps'; @@ -70,19 +70,25 @@ export const TemplateForm: React.FunctionComponent = ({ const CurrentStepComponent = stepComponentMap[currentStep]; const isStepValid = validation[currentStep].isValid; - const setStepDataGetter = (stepDataGetter: DataGetterFunc) => { - stepsDataGetters.current[currentStep] = stepDataGetter; - }; + const setStepDataGetter = useCallback( + (stepDataGetter: DataGetterFunc) => { + stepsDataGetters.current[currentStep] = stepDataGetter; + }, + [currentStep] + ); - const onStepValidityChange = (isValid: boolean | undefined) => { - setValidation(prev => ({ - ...prev, - [currentStep]: { - isValid, - errors: {}, - }, - })); - }; + const onStepValidityChange = useCallback( + (isValid: boolean | undefined) => { + setValidation(prev => ({ + ...prev, + [currentStep]: { + isValid, + errors: {}, + }, + })); + }, + [currentStep] + ); const validateAndGetDataFromCurrentStep = async () => { const validateAndGetData = stepsDataGetters.current[currentStep]; diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx similarity index 96% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index ed2616cc64e38..9ff73b71adf50 100644 --- a/x-pack/legacy/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -13,12 +13,9 @@ import { FIELD_TYPES, VALIDATION_TYPES, FieldConfig, -} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; - -import { fieldFormatters, fieldValidators, -} from '../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +} from '../../../shared_imports'; import { INVALID_INDEX_PATTERN_CHARS, diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/template_steps.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_steps.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/template_steps.tsx rename to x-pack/plugins/index_management/public/application/components/template_form/template_steps.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/components/template_form/types.ts b/x-pack/plugins/index_management/public/application/components/template_form/types.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/components/template_form/types.ts rename to x-pack/plugins/index_management/public/application/components/template_form/types.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/constants/detail_panel_tabs.ts b/x-pack/plugins/index_management/public/application/constants/detail_panel_tabs.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/constants/detail_panel_tabs.ts rename to x-pack/plugins/index_management/public/application/constants/detail_panel_tabs.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/constants/index.ts b/x-pack/plugins/index_management/public/application/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/constants/index.ts rename to x-pack/plugins/index_management/public/application/constants/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/constants/refresh_intervals.ts b/x-pack/plugins/index_management/public/application/constants/refresh_intervals.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/constants/refresh_intervals.ts rename to x-pack/plugins/index_management/public/application/constants/refresh_intervals.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx similarity index 94% rename from x-pack/legacy/plugins/index_management/public/application/index.tsx rename to x-pack/plugins/index_management/public/application/index.tsx index b9859903f1434..5850cb8d42f1a 100644 --- a/x-pack/legacy/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart } from '../../../../../src/core/public'; import { AppContextProvider, AppDependencies } from './app_context'; import { App } from './app'; diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/ace.js b/x-pack/plugins/index_management/public/application/lib/ace.js similarity index 94% rename from x-pack/legacy/plugins/index_management/public/application/lib/ace.js rename to x-pack/plugins/index_management/public/application/lib/ace.js index b9620dfbdb120..3b37c8fb8870e 100644 --- a/x-pack/legacy/plugins/index_management/public/application/lib/ace.js +++ b/x-pack/plugins/index_management/public/application/lib/ace.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import ace from 'ace'; +import brace from 'brace'; import 'brace/ext/language_tools'; const splitTokens = line => { @@ -43,14 +43,14 @@ const wordCompleter = words => { }; export const createAceEditor = (div, value, readOnly = true, autocompleteArray) => { - const editor = ace.edit(div); + const editor = brace.edit(div); editor.$blockScrolling = Infinity; editor.setValue(value, -1); const session = editor.getSession(); session.setUseWrapMode(true); session.setMode('ace/mode/json'); if (autocompleteArray) { - const languageTools = ace.acequire('ace/ext/language_tools'); + const languageTools = brace.acequire('ace/ext/language_tools'); const autocompleter = wordCompleter(autocompleteArray); languageTools.setCompleters([autocompleter]); } diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/edit_settings.js b/x-pack/plugins/index_management/public/application/lib/edit_settings.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/lib/edit_settings.js rename to x-pack/plugins/index_management/public/application/lib/edit_settings.js diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/flatten_object.js b/x-pack/plugins/index_management/public/application/lib/flatten_object.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/lib/flatten_object.js rename to x-pack/plugins/index_management/public/application/lib/flatten_object.js diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/flatten_panel_tree.js b/x-pack/plugins/index_management/public/application/lib/flatten_panel_tree.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/lib/flatten_panel_tree.js rename to x-pack/plugins/index_management/public/application/lib/flatten_panel_tree.js diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/index_status_labels.js b/x-pack/plugins/index_management/public/application/lib/index_status_labels.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/lib/index_status_labels.js rename to x-pack/plugins/index_management/public/application/lib/index_status_labels.js diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts b/x-pack/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts rename to x-pack/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/lib/render_badges.js b/x-pack/plugins/index_management/public/application/lib/render_badges.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/lib/render_badges.js rename to x-pack/plugins/index_management/public/application/lib/render_badges.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/home.tsx rename to x-pack/plugins/index_management/public/application/sections/home/home.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index.ts b/x-pack/plugins/index_management/public/application/sections/home/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index.ts rename to x-pack/plugins/index_management/public/application/sections/home/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.container.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.container.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.container.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.container.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.container.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/edit_settings_json/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.container.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.container.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.container.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/show_json/show_json.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.container.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.container.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.container.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index.ts b/x-pack/plugins/index_management/public/application/sections/home/index_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index.ts rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.container.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.container.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.container.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_list.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_list.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_table/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_table/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/index.ts rename to x-pack/plugins/index_management/public/application/sections/home/template_list/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/index.ts rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index ced8bd97e744b..9c31b0d650449 100644 --- a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -32,7 +32,7 @@ import { } from '../../../../../../common/constants'; import { Template } from '../../../../../../common/types'; import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; -import { loadIndexTemplate } from '../../../../services/api'; +import { useLoadIndexTemplate } from '../../../../services/api'; import { decodePath } from '../../../../services/routing'; import { SendRequestResponse } from '../../../../../shared_imports'; import { useServices } from '../../../../app_context'; @@ -103,7 +103,7 @@ export const TemplateDetails: React.FunctionComponent = ({ }) => { const { uiMetricService } = useServices(); const decodedTemplateName = decodePath(templateName); - const { error, data: templateDetails, isLoading } = loadIndexTemplate(decodedTemplateName); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(decodedTemplateName); // TS complains if we use destructuring here. Fixed in 3.6.0 (https://github.com/microsoft/TypeScript/pull/31711). const isManaged = templateDetails ? templateDetails.isManaged : undefined; const [templateToDelete, setTemplateToDelete] = useState>([]); diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx similarity index 97% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_list.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index 71c32e2e0177f..ffdb224f16271 100644 --- a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { SectionError, SectionLoading, Error } from '../../../components'; import { TemplateTable } from './template_table'; -import { loadIndexTemplates } from '../../../services/api'; +import { useLoadIndexTemplates } from '../../../services/api'; import { Template } from '../../../../../common/types'; import { useServices } from '../../../app_context'; import { @@ -40,7 +40,7 @@ export const TemplateList: React.FunctionComponent { const { uiMetricService } = useServices(); - const { error, isLoading, data: templates, sendRequest: reload } = loadIndexTemplates(); + const { error, isLoading, data: templates, sendRequest: reload } = useLoadIndexTemplates(); let content; @@ -68,7 +68,7 @@ export const TemplateList: React.FunctionComponent { uiMetricService.trackMetric('loaded', UIM_TEMPLATE_LIST_LOAD); - }, []); + }, [uiMetricService]); if (isLoading) { content = ( diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_table/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_table/index.ts rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx rename to x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/template_clone/index.ts b/x-pack/plugins/index_management/public/application/sections/template_clone/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/template_clone/index.ts rename to x-pack/plugins/index_management/public/application/sections/template_clone/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx similarity index 96% rename from x-pack/legacy/plugins/index_management/public/application/sections/template_clone/template_clone.tsx rename to x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 6659be5a2cf4e..cf6ca3c065777 100644 --- a/x-pack/legacy/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -11,7 +11,7 @@ import { TemplateForm, SectionLoading, SectionError, Error } from '../../compone import { breadcrumbService } from '../../services/breadcrumbs'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { Template } from '../../../../common/types'; -import { saveTemplate, loadIndexTemplate } from '../../services/api'; +import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; interface MatchParams { name: string; @@ -27,7 +27,7 @@ export const TemplateClone: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); - const { error: templateToCloneError, data: templateToClone, isLoading } = loadIndexTemplate( + const { error: templateToCloneError, data: templateToClone, isLoading } = useLoadIndexTemplate( decodedTemplateName ); diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/template_create/index.ts b/x-pack/plugins/index_management/public/application/sections/template_create/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/template_create/index.ts rename to x-pack/plugins/index_management/public/application/sections/template_create/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/template_create/template_create.tsx rename to x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/template_edit/index.ts b/x-pack/plugins/index_management/public/application/sections/template_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/sections/template_edit/index.ts rename to x-pack/plugins/index_management/public/application/sections/template_edit/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx similarity index 96% rename from x-pack/legacy/plugins/index_management/public/application/sections/template_edit/template_edit.tsx rename to x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 69e446528a68d..1e9d5f294de34 100644 --- a/x-pack/legacy/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -8,7 +8,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { breadcrumbService } from '../../services/breadcrumbs'; -import { loadIndexTemplate, updateTemplate } from '../../services/api'; +import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { decodePath, getTemplateDetailsLink } from '../../services/routing'; import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; import { Template } from '../../../../common/types'; @@ -27,7 +27,7 @@ export const TemplateEdit: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); - const { error, data: template, isLoading } = loadIndexTemplate(decodedTemplateName); + const { error, data: template, isLoading } = useLoadIndexTemplate(decodedTemplateName); useEffect(() => { breadcrumbService.setBreadcrumbs('templateEdit'); diff --git a/x-pack/legacy/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/services/api.ts rename to x-pack/plugins/index_management/public/application/services/api.ts index 642fd441b353a..88887f40972e4 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -200,7 +200,7 @@ export async function loadIndexData(type: string, indexName: string) { } } -export function loadIndexTemplates() { +export function useLoadIndexTemplates() { return useRequest({ path: `${API_BASE_PATH}/templates`, method: 'get', @@ -220,7 +220,7 @@ export async function deleteTemplates(names: Array) { return result; } -export function loadIndexTemplate(name: Template['name']) { +export function useLoadIndexTemplate(name: Template['name']) { return useRequest({ path: `${API_BASE_PATH}/templates/${encodeURIComponent(name)}`, method: 'get', diff --git a/x-pack/legacy/plugins/index_management/public/application/services/breadcrumbs.ts b/x-pack/plugins/index_management/public/application/services/breadcrumbs.ts similarity index 96% rename from x-pack/legacy/plugins/index_management/public/application/services/breadcrumbs.ts rename to x-pack/plugins/index_management/public/application/services/breadcrumbs.ts index 299491756560e..8128ccd545dce 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/index_management/public/application/services/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; import { BASE_PATH } from '../../../common/constants'; -import { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; diff --git a/x-pack/legacy/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts similarity index 98% rename from x-pack/legacy/plugins/index_management/public/application/services/documentation.ts rename to x-pack/plugins/index_management/public/application/services/documentation.ts index e0f261e586b1e..fdf07c43a0c8b 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DocLinksStart } from '../../../../../../../src/core/public'; +import { DocLinksStart } from '../../../../../../src/core/public'; import { DataType } from '../components/mappings_editor/types'; import { TYPE_DEFINITION } from '../components/mappings_editor/constants'; diff --git a/x-pack/legacy/plugins/index_management/public/application/services/health_to_color.ts b/x-pack/plugins/index_management/public/application/services/health_to_color.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/services/health_to_color.ts rename to x-pack/plugins/index_management/public/application/services/health_to_color.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/services/http.ts b/x-pack/plugins/index_management/public/application/services/http.ts similarity index 88% rename from x-pack/legacy/plugins/index_management/public/application/services/http.ts rename to x-pack/plugins/index_management/public/application/services/http.ts index a6973c263f00f..931e5fcd21898 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/http.ts +++ b/x-pack/plugins/index_management/public/application/services/http.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from '../../../../../../../src/core/public'; +import { HttpSetup } from '../../../../../../src/core/public'; export class HttpService { private client: any; diff --git a/x-pack/legacy/plugins/index_management/public/application/services/index.ts b/x-pack/plugins/index_management/public/application/services/index.ts similarity index 96% rename from x-pack/legacy/plugins/index_management/public/application/services/index.ts rename to x-pack/plugins/index_management/public/application/services/index.ts index 78ff8cb5c314a..2334d32adf131 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/index.ts +++ b/x-pack/plugins/index_management/public/application/services/index.ts @@ -21,7 +21,7 @@ export { loadIndexStats, loadIndexMapping, loadIndexData, - loadIndexTemplates, + useLoadIndexTemplates, } from './api'; export { healthToColor } from './health_to_color'; export { sortTable } from './sort_table'; diff --git a/x-pack/legacy/plugins/index_management/public/application/services/navigation.ts b/x-pack/plugins/index_management/public/application/services/navigation.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/services/navigation.ts rename to x-pack/plugins/index_management/public/application/services/navigation.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/services/notification.ts b/x-pack/plugins/index_management/public/application/services/notification.ts similarity index 93% rename from x-pack/legacy/plugins/index_management/public/application/services/notification.ts rename to x-pack/plugins/index_management/public/application/services/notification.ts index 0971ca77c004b..82b9de2272747 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/notification.ts +++ b/x-pack/plugins/index_management/public/application/services/notification.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NotificationsStart } from '../../../../../../../src/core/public'; +import { NotificationsStart } from '../../../../../../src/core/public'; export class NotificationService { private _toasts: any; diff --git a/x-pack/legacy/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/services/routing.ts rename to x-pack/plugins/index_management/public/application/services/routing.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/services/sort_table.ts b/x-pack/plugins/index_management/public/application/services/sort_table.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/services/sort_table.ts rename to x-pack/plugins/index_management/public/application/services/sort_table.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_management/public/application/services/ui_metric.ts similarity index 90% rename from x-pack/legacy/plugins/index_management/public/application/services/ui_metric.ts rename to x-pack/plugins/index_management/public/application/services/ui_metric.ts index 7c87ec9509085..73d2ef5aced86 100644 --- a/x-pack/legacy/plugins/index_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_management/public/application/services/ui_metric.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { UiStatsMetricType } from '@kbn/analytics'; -import { UsageCollectionSetup } from '../../../../../../../src/plugins/usage_collection/public'; + +import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/public'; import { IndexMgmtMetricsType } from '../../types'; let uiMetricService: UiMetricService; @@ -23,7 +24,8 @@ export class UiMetricService { private track(type: T, name: string) { if (!this.usageCollection) { - throw Error('UiMetricService not initialized.'); + // Usage collection might have been disabled in Kibana config. + return; } this.usageCollection.reportUiStats(this.appName, type as UiStatsMetricType, name); } diff --git a/x-pack/legacy/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/services/use_request.ts rename to x-pack/plugins/index_management/public/application/services/use_request.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/clear_cache_indices.js b/x-pack/plugins/index_management/public/application/store/actions/clear_cache_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/clear_cache_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/clear_cache_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/clear_row_status.js b/x-pack/plugins/index_management/public/application/store/actions/clear_row_status.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/clear_row_status.js rename to x-pack/plugins/index_management/public/application/store/actions/clear_row_status.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/close_indices.js b/x-pack/plugins/index_management/public/application/store/actions/close_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/close_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/close_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/delete_indices.js b/x-pack/plugins/index_management/public/application/store/actions/delete_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/delete_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/delete_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/detail_panel.js b/x-pack/plugins/index_management/public/application/store/actions/detail_panel.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/detail_panel.js rename to x-pack/plugins/index_management/public/application/store/actions/detail_panel.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/edit_index_settings.js b/x-pack/plugins/index_management/public/application/store/actions/edit_index_settings.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/edit_index_settings.js rename to x-pack/plugins/index_management/public/application/store/actions/edit_index_settings.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/extension_action.js b/x-pack/plugins/index_management/public/application/store/actions/extension_action.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/extension_action.js rename to x-pack/plugins/index_management/public/application/store/actions/extension_action.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/flush_indices.js b/x-pack/plugins/index_management/public/application/store/actions/flush_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/flush_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/flush_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/forcemerge_indices.js b/x-pack/plugins/index_management/public/application/store/actions/forcemerge_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/forcemerge_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/forcemerge_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/freeze_indices.js b/x-pack/plugins/index_management/public/application/store/actions/freeze_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/freeze_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/freeze_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/index.js b/x-pack/plugins/index_management/public/application/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/index.js rename to x-pack/plugins/index_management/public/application/store/actions/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/load_index_data.js b/x-pack/plugins/index_management/public/application/store/actions/load_index_data.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/load_index_data.js rename to x-pack/plugins/index_management/public/application/store/actions/load_index_data.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/load_indices.js b/x-pack/plugins/index_management/public/application/store/actions/load_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/load_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/load_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/open_indices.js b/x-pack/plugins/index_management/public/application/store/actions/open_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/open_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/open_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/refresh_indices.js b/x-pack/plugins/index_management/public/application/store/actions/refresh_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/refresh_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/refresh_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/reload_indices.js b/x-pack/plugins/index_management/public/application/store/actions/reload_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/reload_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/reload_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/table_state.js b/x-pack/plugins/index_management/public/application/store/actions/table_state.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/table_state.js rename to x-pack/plugins/index_management/public/application/store/actions/table_state.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/unfreeze_indices.js b/x-pack/plugins/index_management/public/application/store/actions/unfreeze_indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/unfreeze_indices.js rename to x-pack/plugins/index_management/public/application/store/actions/unfreeze_indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/actions/update_index_settings.js b/x-pack/plugins/index_management/public/application/store/actions/update_index_settings.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/actions/update_index_settings.js rename to x-pack/plugins/index_management/public/application/store/actions/update_index_settings.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/index.ts b/x-pack/plugins/index_management/public/application/store/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/index.ts rename to x-pack/plugins/index_management/public/application/store/index.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/store/reducers/detail_panel.js b/x-pack/plugins/index_management/public/application/store/reducers/detail_panel.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/reducers/detail_panel.js rename to x-pack/plugins/index_management/public/application/store/reducers/detail_panel.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/reducers/index.js b/x-pack/plugins/index_management/public/application/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/reducers/index.js rename to x-pack/plugins/index_management/public/application/store/reducers/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/reducers/index_management.js b/x-pack/plugins/index_management/public/application/store/reducers/index_management.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/reducers/index_management.js rename to x-pack/plugins/index_management/public/application/store/reducers/index_management.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/reducers/indices.js b/x-pack/plugins/index_management/public/application/store/reducers/indices.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/reducers/indices.js rename to x-pack/plugins/index_management/public/application/store/reducers/indices.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/reducers/row_status.js b/x-pack/plugins/index_management/public/application/store/reducers/row_status.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/reducers/row_status.js rename to x-pack/plugins/index_management/public/application/store/reducers/row_status.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/reducers/table_state.js b/x-pack/plugins/index_management/public/application/store/reducers/table_state.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/reducers/table_state.js rename to x-pack/plugins/index_management/public/application/store/reducers/table_state.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/selectors/index.d.ts b/x-pack/plugins/index_management/public/application/store/selectors/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/selectors/index.d.ts rename to x-pack/plugins/index_management/public/application/store/selectors/index.d.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/store/selectors/index.js b/x-pack/plugins/index_management/public/application/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/selectors/index.js rename to x-pack/plugins/index_management/public/application/store/selectors/index.js diff --git a/x-pack/legacy/plugins/index_management/public/application/store/store.d.ts b/x-pack/plugins/index_management/public/application/store/store.d.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/store.d.ts rename to x-pack/plugins/index_management/public/application/store/store.d.ts diff --git a/x-pack/legacy/plugins/index_management/public/application/store/store.js b/x-pack/plugins/index_management/public/application/store/store.js similarity index 100% rename from x-pack/legacy/plugins/index_management/public/application/store/store.js rename to x-pack/plugins/index_management/public/application/store/store.js diff --git a/x-pack/legacy/plugins/index_management/public/index.scss b/x-pack/plugins/index_management/public/index.scss similarity index 100% rename from x-pack/legacy/plugins/index_management/public/index.scss rename to x-pack/plugins/index_management/public/index.scss diff --git a/x-pack/legacy/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts similarity index 66% rename from x-pack/legacy/plugins/index_management/public/index.ts rename to x-pack/plugins/index_management/public/index.ts index 16e7bf21aee98..6bb921ef648f3 100644 --- a/x-pack/legacy/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -3,19 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { npSetup } from 'ui/new_platform'; - +import './index.scss'; import { IndexMgmtUIPlugin, IndexMgmtSetup } from './plugin'; /** @public */ -export { IndexMgmtSetup }; - export const plugin = () => { return new IndexMgmtUIPlugin(); }; -// Temp. To be removed after moving to the "plugins" folder - -const { extensionsService } = plugin().setup(npSetup.core, npSetup.plugins); +export { IndexMgmtSetup }; -export { extensionsService }; +export { getIndexListUri } from './application/services/navigation'; diff --git a/x-pack/legacy/plugins/index_management/public/mocks.ts b/x-pack/plugins/index_management/public/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/mocks.ts rename to x-pack/plugins/index_management/public/mocks.ts diff --git a/x-pack/legacy/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts similarity index 90% rename from x-pack/legacy/plugins/index_management/public/plugin.ts rename to x-pack/plugins/index_management/public/plugin.ts index 539324766cf95..c1b26fe3ca56b 100644 --- a/x-pack/legacy/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup } from '../../../../../src/core/public'; -import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; -import { ManagementSetup } from '../../../../../src/plugins/management/public'; -import { UIM_APP_NAME } from '../common/constants'; +import { CoreSetup } from '../../../../src/core/public'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { UIM_APP_NAME, PLUGIN } from '../common/constants'; import { AppDependencies } from './application'; import { httpService } from './application/services/http'; @@ -52,7 +52,7 @@ export class IndexMgmtUIPlugin { this.uiMetricService.setup(usageCollection); management.sections.getSection('elasticsearch')!.registerApp({ - id: 'index_management', + id: PLUGIN.id, title: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }), order: 1, mount: async ({ element, setBreadcrumbs }) => { diff --git a/x-pack/legacy/plugins/index_management/public/services/extensions_service.mock.ts b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/services/extensions_service.mock.ts rename to x-pack/plugins/index_management/public/services/extensions_service.mock.ts diff --git a/x-pack/legacy/plugins/index_management/public/services/extensions_service.ts b/x-pack/plugins/index_management/public/services/extensions_service.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/services/extensions_service.ts rename to x-pack/plugins/index_management/public/services/extensions_service.ts diff --git a/x-pack/legacy/plugins/index_management/public/services/index.ts b/x-pack/plugins/index_management/public/services/index.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/services/index.ts rename to x-pack/plugins/index_management/public/services/index.ts diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts new file mode 100644 index 0000000000000..cd2964df23d9b --- /dev/null +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -0,0 +1,33 @@ +/* + * 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 { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + sendRequest, + useRequest, +} from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; + +export { + FormSchema, + FIELD_TYPES, + VALIDATION_TYPES, + FieldConfig, + useForm, + Form, + getUseField, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + fieldFormatters, + fieldValidators, + serializers, +} from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { getFormRow, Field } from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { isJSON } from '../../../../src/plugins/es_ui_shared/static/validators/string'; diff --git a/x-pack/legacy/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/public/types.ts rename to x-pack/plugins/index_management/public/types.ts diff --git a/x-pack/legacy/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/server/config.ts similarity index 52% rename from x-pack/legacy/plugins/index_management/public/shared_imports.ts rename to x-pack/plugins/index_management/server/config.ts index cbc4dde7448ff..5f03575d3ff43 100644 --- a/x-pack/legacy/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/server/config.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - SendRequestConfig, - SendRequestResponse, - UseRequestConfig, - sendRequest, - useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type IndexManagementConfig = TypeOf; diff --git a/x-pack/legacy/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts similarity index 67% rename from x-pack/legacy/plugins/index_management/server/index.ts rename to x-pack/plugins/index_management/server/index.ts index 866b374740d3b..e4102711708cb 100644 --- a/x-pack/legacy/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -5,8 +5,18 @@ */ import { PluginInitializerContext } from 'src/core/server'; + import { IndexMgmtServerPlugin } from './plugin'; +import { configSchema } from './config'; export const plugin = (ctx: PluginInitializerContext) => new IndexMgmtServerPlugin(ctx); +export const config = { + schema: configSchema, +}; + +/** @public */ export { Dependencies } from './types'; +export { IndexMgmtSetup } from './plugin'; +export { Index } from './types'; +export { IndexManagementConfig } from './config'; diff --git a/x-pack/legacy/plugins/index_management/server/lib/fetch_aliases.test.ts b/x-pack/plugins/index_management/server/lib/fetch_aliases.test.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/server/lib/fetch_aliases.test.ts rename to x-pack/plugins/index_management/server/lib/fetch_aliases.test.ts diff --git a/x-pack/legacy/plugins/index_management/server/lib/fetch_aliases.ts b/x-pack/plugins/index_management/server/lib/fetch_aliases.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/server/lib/fetch_aliases.ts rename to x-pack/plugins/index_management/server/lib/fetch_aliases.ts diff --git a/x-pack/legacy/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/server/lib/fetch_indices.ts rename to x-pack/plugins/index_management/server/lib/fetch_indices.ts diff --git a/x-pack/legacy/plugins/index_management/server/lib/get_managed_templates.ts b/x-pack/plugins/index_management/server/lib/get_managed_templates.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/server/lib/get_managed_templates.ts rename to x-pack/plugins/index_management/server/lib/get_managed_templates.ts diff --git a/x-pack/legacy/plugins/index_management/server/lib/is_es_error.ts b/x-pack/plugins/index_management/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/index_management/server/lib/is_es_error.ts rename to x-pack/plugins/index_management/server/lib/is_es_error.ts diff --git a/x-pack/legacy/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts similarity index 94% rename from x-pack/legacy/plugins/index_management/server/plugin.ts rename to x-pack/plugins/index_management/server/plugin.ts index 95d27e1cf16ba..a0a9151cdb71f 100644 --- a/x-pack/legacy/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -24,8 +24,8 @@ export class IndexMgmtServerPlugin implements Plugin Date: Tue, 25 Feb 2020 09:05:22 +0100 Subject: [PATCH 073/113] [SIEM] Upgrades cypress to version 4.0.2 (#58400) * upgrades cypress to version 4.0.2 * fixes failing tests --- .../timeline_search_or_filter.spec.ts | 5 +- .../cypress/integration/url_state.spec.ts | 5 +- .../cypress/tasks/hosts/authentications.ts | 4 + .../cypress/tasks/hosts/uncommon_processes.ts | 4 + .../plugins/siem/cypress/tasks/timeline.ts | 5 +- .../components/fields_browser/header.test.tsx | 2 - x-pack/package.json | 2 +- yarn.lock | 301 ++++++++---------- 8 files changed, 159 insertions(+), 169 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts index c06fd69a558a4..f738ff792049a 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_search_or_filter.spec.ts @@ -24,6 +24,9 @@ describe('timeline search or filter KQL bar', () => { cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') - .should('be.above', 0); + .then(strCount => { + const intCount = +strCount; + cy.wrap(intCount).should('be.above', 0); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts index cabdb98fa5b67..11c0562eb3638 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/url_state.spec.ts @@ -236,7 +236,10 @@ describe('url state', () => { cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') - .should('be.above', 0); + .then(strCount => { + const intCount = +strCount; + cy.wrap(intCount).should('be.above', 0); + }); const bestTimelineName = 'The Best Timeline'; addNameToTimeline(bestTimelineName); diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts index f5f15150e8ac3..ce3767a340376 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/authentications.ts @@ -5,7 +5,11 @@ */ import { AUTHENTICATIONS_TABLE } from '../../screens/hosts/authentications'; +import { REFRESH_BUTTON } from '../../screens/siem_header'; export const waitForAuthenticationsToBeLoaded = () => { cy.get(AUTHENTICATIONS_TABLE).should('exist'); + cy.get(REFRESH_BUTTON) + .invoke('text') + .should('not.equal', 'Updating'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts index c44249acdd964..a28a7df07c3f8 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/uncommon_processes.ts @@ -5,7 +5,11 @@ */ import { UNCOMMON_PROCESSES_TABLE } from '../../screens/hosts/uncommon_processes'; +import { REFRESH_BUTTON } from '../../screens/siem_header'; export const waitForUncommonProcessesToBeLoaded = () => { cy.get(UNCOMMON_PROCESSES_TABLE).should('exist'); + cy.get(REFRESH_BUTTON) + .invoke('text') + .should('not.equal', 'Updating'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts index 76acdad990a7e..c218d5153356b 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline.ts @@ -65,7 +65,10 @@ export const populateTimeline = () => { executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT) .invoke('text') - .should('be.above', 0); + .then(strCount => { + const intCount = +strCount; + cy.wrap(intCount).should('be.above', 0); + }); }; export const uncheckTimestampToggleField = () => { diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx index 42689065354d0..2abc2fd1046e0 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx @@ -6,11 +6,9 @@ import { mount } from 'enzyme'; import React from 'react'; - import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; - import { Header } from './header'; const timelineId = 'test'; diff --git a/x-pack/package.json b/x-pack/package.json index 551e466893f93..f76b0182ea228 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -117,7 +117,7 @@ "cheerio": "0.22.0", "commander": "3.0.2", "copy-webpack-plugin": "^5.0.4", - "cypress": "^3.6.1", + "cypress": "^4.0.2", "cypress-multi-reporters": "^1.2.3", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", diff --git a/yarn.lock b/yarn.lock index 8ea23c17b8b8b..f46e869909e2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7023,13 +7023,6 @@ async@2.4.0: dependencies: lodash "^4.14.0" -async@2.6.1, async@^2.6.0, async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== - dependencies: - lodash "^4.17.10" - async@^2.0.0, async@^2.1.4: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" @@ -7037,6 +7030,13 @@ async@^2.0.0, async@^2.1.4: dependencies: lodash "^4.14.0" +async@^2.6.0, async@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== + dependencies: + lodash "^4.17.10" + async@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -7044,6 +7044,11 @@ async@^2.6.3: dependencies: lodash "^4.17.14" +async@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + async@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" @@ -7909,6 +7914,11 @@ bluebird@3.5.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== +bluebird@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + bluebird@^3.3.0, bluebird@^3.3.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -8536,12 +8546,10 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" -cachedir@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-1.3.0.tgz#5e01928bf2d95b5edd94b0942188246740e0dbc4" - integrity sha512-O1ji32oyON9laVPJL1IZ5bmwd2cB46VfpxkDequezH+15FDzzVddEyrGEeX4WusDSqKxdyFdDQDEG1yo1GoWkg== - dependencies: - os-homedir "^1.0.1" +cachedir@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== caching-transform@^3.0.2: version "3.0.2" @@ -8806,6 +8814,14 @@ chalk@2.4.2, chalk@^2.3.2, chalk@^2.4.2, chalk@~2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@3.0.0, chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -8835,14 +8851,6 @@ chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^5.2.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -9087,11 +9095,6 @@ ci-info@^1.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" integrity sha512-uTGIPNx/nSpBdsF6xnseRXLLtfr9VLqkz8ZqHXr3Y7b6SftyRxBGjwMtJj1OhNbmlc1wZzLNAlAcvyIiE8a6ZA== -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -9596,11 +9599,6 @@ commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.2: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== -commander@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== - commander@2.17.x, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -9611,6 +9609,11 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +commander@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" + integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== + commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -10646,41 +10649,42 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.6.1.tgz#4420957923879f60b7a5146ccbf81841a149b653" - integrity sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A== +cypress@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.0.2.tgz#ede194d7bc73fb449f8de553c9e1db4ca15309ef" + integrity sha512-WRzxOoSd+TxyXKa7Zi9orz3ii5VW7yhhVYstCU+EpOKfPan9x5Ww2Clucmy4H/W0GHUYAo7GYFZRD33ZCSNBQA== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/xvfb" "1.2.4" "@types/sizzle" "2.3.2" arch "2.1.1" - bluebird "3.5.0" - cachedir "1.3.0" - chalk "2.4.2" + bluebird "3.7.2" + cachedir "2.3.0" + chalk "3.0.0" check-more-types "2.24.0" - commander "2.15.1" + commander "4.1.0" common-tags "1.8.0" - debug "3.2.6" - execa "0.10.0" + debug "4.1.1" + eventemitter2 "4.1.2" + execa "3.3.0" executable "4.1.1" extract-zip "1.6.7" - fs-extra "5.0.0" - getos "3.1.1" - is-ci "1.2.1" + fs-extra "8.1.0" + getos "3.1.4" + is-ci "2.0.0" is-installed-globally "0.1.0" lazy-ass "1.6.0" - listr "0.12.0" + listr "0.14.3" lodash "4.17.15" - log-symbols "2.2.0" + log-symbols "3.0.0" minimist "1.2.0" moment "2.24.0" - ramda "0.24.1" + ramda "0.26.1" request "2.88.0" request-progress "3.0.0" - supports-color "5.5.0" + supports-color "7.1.0" tmp "0.1.0" - untildify "3.0.3" + untildify "4.0.0" url "0.11.0" yauzl "2.10.0" @@ -11065,7 +11069,7 @@ debug@4.1.0: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4.1.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -13028,6 +13032,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter2@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15" + integrity sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU= + eventemitter2@~0.4.13: version "0.4.14" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" @@ -13078,19 +13087,6 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== -execa@0.10.0, execa@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== - dependencies: - cross-spawn "^6.0.0" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@1.0.0, execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -13104,6 +13100,22 @@ execa@1.0.0, execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-3.3.0.tgz#7e348eef129a1937f21ecbbd53390942653522c1" + integrity sha512-j5Vit5WZR/cbHlqU97+qcnw9WHRCIL4V1SVe75VcHcD1JRBdt8fv0zw89b7CQHQdUHTt2VjuhcF5ibAgVOxqpg== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + p-finally "^2.0.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + execa@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-0.1.1.tgz#b09c2a9309bc0ef0501479472db3180f8d4c3edd" @@ -13113,6 +13125,19 @@ execa@^0.1.1: object-assign "^4.0.1" strip-eof "^1.0.0" +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== + dependencies: + cross-spawn "^6.0.0" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" @@ -14321,12 +14346,12 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= -fs-extra@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" - integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ== +fs-extra@8.1.0, fs-extra@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.2.0" jsonfile "^4.0.0" universalify "^0.1.0" @@ -14368,15 +14393,6 @@ fs-extra@^7.0.0, fs-extra@^7.0.1, fs-extra@~7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^8.0.1: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -14703,12 +14719,12 @@ getopts@^2.2.5: resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== -getos@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.1.tgz#967a813cceafee0156b0483f7cffa5b3eff029c5" - integrity sha512-oUP1rnEhAr97rkitiszGP9EgDVYnmchgFzfqRzSkgtfv7ai6tEi7Ko8GgjNXts7VLWEqrTWyhsOKLe5C5b/Zkg== +getos@3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.1.4.tgz#29cdf240ed10a70c049add7b6f8cb08c81876faf" + integrity sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw== dependencies: - async "2.6.1" + async "^3.1.0" getos@^3.1.0: version "3.1.0" @@ -17152,12 +17168,12 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== -is-ci@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== +is-ci@2.0.0, is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== dependencies: - ci-info "^1.5.0" + ci-info "^2.0.0" is-ci@^1.0.10: version "1.1.0" @@ -17166,13 +17182,6 @@ is-ci@^1.0.10: dependencies: ci-info "^1.0.0" -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -19254,20 +19263,6 @@ listr-update-renderer@0.5.0, listr-update-renderer@^0.5.0: log-update "^2.3.0" strip-ansi "^3.0.1" -listr-update-renderer@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" - integrity sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - strip-ansi "^3.0.1" - listr-update-renderer@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" @@ -19302,28 +19297,6 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" - integrity sha1-a84sD1YD+klYDqF81qAMwOX6RRo= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - figures "^1.7.0" - indent-string "^2.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.2.0" - listr-verbose-renderer "^0.4.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - ora "^0.2.3" - p-map "^1.1.1" - rxjs "^5.0.0-beta.11" - stream-to-observable "^0.1.0" - strip-ansi "^3.0.1" - listr@0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" @@ -19824,6 +19797,13 @@ log-symbols@2.2.0, log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" +log-symbols@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + log-symbols@^1.0.1, log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -23983,21 +23963,16 @@ railroad-diagrams@^1.0.0: resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= -ramda@0.24.1: - version "0.24.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857" - integrity sha1-w7d1UZfzW43DUCIoJixMkd22uFc= +ramda@0.26.1, ramda@^0.26, ramda@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== ramda@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= -ramda@^0.26, ramda@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== - randexp@0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" @@ -26375,7 +26350,7 @@ rxjs@6.5.2: dependencies: tslib "^1.9.0" -rxjs@^5.0.0-beta.11, rxjs@^5.5.2: +rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== @@ -27815,11 +27790,6 @@ stream-spigot@~2.1.2: dependencies: readable-stream "~1.1.0" -stream-to-observable@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" - integrity sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4= - streamroller@0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" @@ -28291,13 +28261,6 @@ supertest@^3.1.0: methods "~1.1.2" superagent "3.8.2" -supports-color@5.5.0, supports-color@^5.0.0, supports-color@^5.4.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" @@ -28312,6 +28275,13 @@ supports-color@6.1.0, supports-color@^6.0.0, supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@7.1.0, supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-color@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" @@ -28329,6 +28299,13 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" +supports-color@^5.0.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^5.2.0, supports-color@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" @@ -28343,13 +28320,6 @@ supports-color@^7.0.0: dependencies: has-flag "^4.0.0" -supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -30241,10 +30211,10 @@ unstated@^2.1.1: dependencies: create-react-context "^0.1.5" -untildify@3.0.3, untildify@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" - integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== +untildify@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== untildify@^2.0.0: version "2.1.0" @@ -30253,6 +30223,11 @@ untildify@^2.0.0: dependencies: os-homedir "^1.0.0" +untildify@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" + integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== + unzip-response@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" From a60b25f9ad45cbc8efb5f362d30996c0d49baa2b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 25 Feb 2020 09:05:53 +0100 Subject: [PATCH 074/113] fix short url in spaces (#58313) --- src/plugins/share/server/routes/goto.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 5c3a4e441099f..0c5b74915e58a 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -23,6 +23,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; import { getGotoPath } from '../../common/short_url_routes'; +import { modifyUrl } from '../../../../core/utils'; export const createGotoRoute = ({ router, @@ -49,9 +50,16 @@ export const createGotoRoute = ({ const uiSettings = context.core.uiSettings.client; const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); if (!stateStoreInSessionStorage) { + const basePath = http.basePath.get(request); + + const prependedUrl = modifyUrl(url, parts => { + if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { + parts.pathname = `${basePath}${parts.pathname}`; + } + }); return response.redirected({ headers: { - location: http.basePath.prepend(url), + location: prependedUrl, }, }); } From 1a96ff3c302c08ea049245a05c716ecd43ca0485 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 25 Feb 2020 11:00:10 +0100 Subject: [PATCH 075/113] [ML] Functional tests - stabilize typing during df analytics creation (#58227) This PR makes the typing in data frame analytics tests more robust. --- .../data_frame_analytics_creation.ts | 24 ++++++++++++------- x-pack/test/functional/services/ml.ts | 5 +++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index b4e455ebaa63f..96dc8993c3d35 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -6,10 +6,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommon } from './common'; -export function MachineLearningDataFrameAnalyticsCreationProvider({ - getService, -}: FtrProviderContext) { +export function MachineLearningDataFrameAnalyticsCreationProvider( + { getService }: FtrProviderContext, + mlCommon: MlCommon +) { const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const retry = getService('retry'); @@ -85,14 +87,14 @@ export function MachineLearningDataFrameAnalyticsCreationProvider({ }, async setJobId(jobId: string) { - await testSubjects.setValue('mlAnalyticsCreateJobFlyoutJobIdInput', jobId, { + await mlCommon.setValueWithChecks('mlAnalyticsCreateJobFlyoutJobIdInput', jobId, { clearWithKeyboard: true, }); await this.assertJobIdValue(jobId); }, async setJobDescription(jobDescription: string) { - await testSubjects.setValue('mlDFAnalyticsJobCreationJobDescription', jobDescription, { + await mlCommon.setValueWithChecks('mlDFAnalyticsJobCreationJobDescription', jobDescription, { clearWithKeyboard: true, }); await this.assertJobDescriptionValue(jobDescription); @@ -136,9 +138,13 @@ export function MachineLearningDataFrameAnalyticsCreationProvider({ }, async setDestIndex(destIndex: string) { - await testSubjects.setValue('mlAnalyticsCreateJobFlyoutDestinationIndexInput', destIndex, { - clearWithKeyboard: true, - }); + await mlCommon.setValueWithChecks( + 'mlAnalyticsCreateJobFlyoutDestinationIndexInput', + destIndex, + { + clearWithKeyboard: true, + } + ); await this.assertDestIndexValue(destIndex); }, @@ -248,7 +254,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider({ }, async setModelMemory(modelMemory: string) { - await testSubjects.setValue('mlAnalyticsCreateJobFlyoutModelMemoryInput', modelMemory, { + await mlCommon.setValueWithChecks('mlAnalyticsCreateJobFlyoutModelMemoryInput', modelMemory, { clearWithKeyboard: true, }); await this.assertModelMemoryValue(modelMemory); diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 2660a90662dec..354e0907375ca 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -42,7 +42,10 @@ export function MachineLearningProvider(context: FtrProviderContext) { const api = MachineLearningAPIProvider(context); const customUrls = MachineLearningCustomUrlsProvider(context); const dataFrameAnalytics = MachineLearningDataFrameAnalyticsProvider(context, api); - const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider(context); + const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider( + context, + common + ); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); const dataVisualizer = MachineLearningDataVisualizerProvider(context); const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context); From 737205fb9b0c5b27ff06331598802a2e57365ffb Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Tue, 25 Feb 2020 13:19:02 +0300 Subject: [PATCH 076/113] Move src/legacy/ui/public/notify/app_redirect to kibana_legacy (#58127) Co-authored-by: Elastic Machine --- .../core_plugins/kibana/public/kibana.js | 6 ++-- src/legacy/ui/public/notify/index.js | 1 - .../notify/app_redirect/app_redirect.test.ts} | 32 +++++++++++-------- .../notify/app_redirect/app_redirect.ts} | 9 +++--- .../public/notify/app_redirect/index.ts} | 0 .../kibana_legacy/public/notify/index.ts | 1 + .../dashboard_mode/public/dashboard_viewer.js | 6 ++-- .../plugins/graph/public/application.ts | 2 +- .../plugins/graph/public/legacy_imports.ts | 2 -- 9 files changed, 33 insertions(+), 26 deletions(-) rename src/{legacy/ui/public/notify/app_redirect/app_redirect.test.js => plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts} (75%) rename src/{legacy/ui/public/notify/app_redirect/app_redirect.js => plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts} (82%) rename src/{legacy/ui/public/notify/app_redirect/index.js => plugins/kibana_legacy/public/notify/app_redirect/index.ts} (100%) diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index d77fc780f4cc2..384c6bd80ec33 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -53,7 +53,7 @@ import './management'; import './dev_tools'; import 'ui/agg_response'; import 'ui/agg_types'; -import { showAppRedirectNotification } from 'ui/notify'; +import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; import 'leaflet'; import { localApplicationService } from './local_application_service'; @@ -68,4 +68,6 @@ routes.otherwise({ redirectTo: `/${config.defaultAppId || 'discover'}`, }); -uiModules.get('kibana').run(showAppRedirectNotification); +uiModules + .get('kibana') + .run($location => showAppRedirectNotification($location, npSetup.core.notifications.toasts)); diff --git a/src/legacy/ui/public/notify/index.js b/src/legacy/ui/public/notify/index.js index f7526f3b8f8fd..7ec6a394d7e88 100644 --- a/src/legacy/ui/public/notify/index.js +++ b/src/legacy/ui/public/notify/index.js @@ -19,5 +19,4 @@ export { fatalError, addFatalErrorCallback } from './fatal_error'; export { toastNotifications } from './toasts'; -export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect'; export { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/app_redirect/app_redirect.test.js b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts similarity index 75% rename from src/legacy/ui/public/notify/app_redirect/app_redirect.test.js rename to src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts index a23aabe6ad88e..efb1393ff0b16 100644 --- a/src/legacy/ui/public/notify/app_redirect/app_redirect.test.js +++ b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts @@ -17,17 +17,12 @@ * under the License. */ +import { ILocationService } from 'angular'; +import { ToastsStart } from '../../../../../core/public'; import { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect'; let isToastAdded = false; - -jest.mock('../toasts', () => ({ - toastNotifications: { - addDanger: () => { - isToastAdded = true; - }, - }, -})); +const toasts: ToastsStart = {} as ToastsStart; describe('addAppRedirectMessageToUrl', () => { test('adds a message to the URL', () => { @@ -39,20 +34,29 @@ describe('addAppRedirectMessageToUrl', () => { describe('showAppRedirectNotification', () => { beforeEach(() => { isToastAdded = false; + toasts.addDanger = (): any => { + isToastAdded = true; + }; }); test(`adds a toast when there's a message in the URL`, () => { - showAppRedirectNotification({ - search: () => ({ app_redirect_message: 'redirect message' }), - }); + showAppRedirectNotification( + { + search: () => ({ app_redirect_message: 'redirect message' }), + } as ILocationService, + toasts + ); expect(isToastAdded).toBe(true); }); test(`doesn't add a toast when there's no message in the URL`, () => { - showAppRedirectNotification({ - search: () => ({ app_redirect_message: '' }), - }); + showAppRedirectNotification( + { + search: () => ({ app_redirect_message: '' }), + } as ILocationService, + toasts + ); expect(isToastAdded).toBe(false); }); diff --git a/src/legacy/ui/public/notify/app_redirect/app_redirect.js b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts similarity index 82% rename from src/legacy/ui/public/notify/app_redirect/app_redirect.js rename to src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts index a92e5401e5e75..e79ab4b2fbc6d 100644 --- a/src/legacy/ui/public/notify/app_redirect/app_redirect.js +++ b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts @@ -17,12 +17,13 @@ * under the License. */ +import { ILocationService } from 'angular'; import { modifyUrl } from '../../../../../core/utils'; -import { toastNotifications } from '../toasts'; +import { ToastsStart } from '../../../../../core/public'; const APP_REDIRECT_MESSAGE_PARAM = 'app_redirect_message'; -export function addAppRedirectMessageToUrl(url, message) { +export function addAppRedirectMessageToUrl(url: string, message: string) { return modifyUrl(url, urlParts => { urlParts.hash = modifyUrl(urlParts.hash || '', hashParts => { hashParts.query[APP_REDIRECT_MESSAGE_PARAM] = message; @@ -32,7 +33,7 @@ export function addAppRedirectMessageToUrl(url, message) { // If an app needs to redirect, e.g. due to an expired license, it can surface a message via // the URL query params. -export function showAppRedirectNotification($location) { +export function showAppRedirectNotification($location: ILocationService, toasts: ToastsStart) { const queryString = $location.search(); if (!queryString[APP_REDIRECT_MESSAGE_PARAM]) { @@ -42,5 +43,5 @@ export function showAppRedirectNotification($location) { const message = queryString[APP_REDIRECT_MESSAGE_PARAM]; $location.search(APP_REDIRECT_MESSAGE_PARAM, null); - toastNotifications.addDanger(message); + toasts.addDanger(message); } diff --git a/src/legacy/ui/public/notify/app_redirect/index.js b/src/plugins/kibana_legacy/public/notify/app_redirect/index.ts similarity index 100% rename from src/legacy/ui/public/notify/app_redirect/index.js rename to src/plugins/kibana_legacy/public/notify/app_redirect/index.ts diff --git a/src/plugins/kibana_legacy/public/notify/index.ts b/src/plugins/kibana_legacy/public/notify/index.ts index 6aa4e36ab7227..b6f29876c2737 100644 --- a/src/plugins/kibana_legacy/public/notify/index.ts +++ b/src/plugins/kibana_legacy/public/notify/index.ts @@ -18,3 +18,4 @@ */ export * from './toasts'; export * from './lib'; +export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect'; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 8ca023aa90cf1..e0e49fe59daf4 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -37,7 +37,7 @@ import 'plugins/kibana/dashboard/legacy'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; -import { showAppRedirectNotification } from 'ui/notify'; +import { showAppRedirectNotification } from '../../../../../src/plugins/kibana_legacy/public'; import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard'; npStart.plugins.kibanaLegacy.dashboardConfig.turnHideWriteControlsOn(); @@ -51,7 +51,9 @@ chrome.setRootController('kibana', function() { npStart.core.chrome.navLinks.showOnly('kibana:dashboard'); }); -uiModules.get('kibana').run(showAppRedirectNotification); +uiModules + .get('kibana') + .run($location => showAppRedirectNotification($location, npStart.core.notifications.toasts)); /** * If there is a configured `kibana.defaultAppId`, and it is a dashboard ID, we'll diff --git a/x-pack/legacy/plugins/graph/public/application.ts b/x-pack/legacy/plugins/graph/public/application.ts index 80a797b7f0724..7bd18f841b478 100644 --- a/x-pack/legacy/plugins/graph/public/application.ts +++ b/x-pack/legacy/plugins/graph/public/application.ts @@ -24,7 +24,6 @@ import { configureAppAngularModule, createTopNavDirective, createTopNavHelper, - addAppRedirectMessageToUrl, } from './legacy_imports'; // @ts-ignore import { initGraphApp } from './app'; @@ -37,6 +36,7 @@ import { checkLicense } from '../../../../plugins/graph/common/check_license'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; import { createSavedWorkspacesLoader } from './services/persistence/saved_workspace_loader'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { addAppRedirectMessageToUrl } from '../../../../../src/plugins/kibana_legacy/public'; /** * These are dependencies of the Graph app besides the base dependencies diff --git a/x-pack/legacy/plugins/graph/public/legacy_imports.ts b/x-pack/legacy/plugins/graph/public/legacy_imports.ts index ac518d34551db..84fafdb580abe 100644 --- a/x-pack/legacy/plugins/graph/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/graph/public/legacy_imports.ts @@ -8,6 +8,4 @@ import 'ace'; // @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; -// @ts-ignore -export { addAppRedirectMessageToUrl } from 'ui/notify'; export { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; From 5910e83722ae3c211731f9671e6ceee438d75aca Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 25 Feb 2020 11:27:25 +0100 Subject: [PATCH 077/113] hide welcome screen for cloud (#58371) --- x-pack/test/functional/page_objects/security_page.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 3bcba7cbd1696..5889a374e443e 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -30,6 +30,11 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const rawDataTabLocator = 'a[id=rawdata-tab]'; await PageObjects.common.navigateToApp('login'); + + // ensure welcome screen won't be shown. This is relevant for environments which don't allow + // to use the yml setting, e.g. cloud + await browser.setLocalStorageItem('home:welcome:show', 'false'); + await testSubjects.setValue('loginUsername', username); await testSubjects.setValue('loginPassword', password); await testSubjects.click('loginSubmit'); From 506268c926d18f27133d93f419c0b594eca609aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 25 Feb 2020 10:31:35 +0000 Subject: [PATCH 078/113] [Telemetry] Separate the license retrieval from the stats in the usage collectors (#57332) * [Telemetry] Merge OSS and XPack usage collectors * Create X-Pack collector again * Separate the license retrieval from the stats * Fix telemetry tests with new fields * Use Promise.all to retrieve license and stats at the same time * Fix moment mock Co-authored-by: Elastic Machine --- .../telemetry/server/collection_manager.ts | 56 +- ...et_cluster_info.js => get_cluster_info.ts} | 24 +- .../telemetry_collection/get_local_license.ts | 90 ++ .../telemetry_collection/get_local_stats.ts | 26 +- .../server/telemetry_collection/index.ts | 1 - .../register_collection.ts | 2 + .../public/services/telemetry_service.test.ts | 10 + .../public/services/telemetry_service.ts | 5 +- .../get_all_stats.test.ts | 12 +- .../telemetry_collection/get_all_stats.ts | 41 +- .../telemetry_collection/get_es_stats.ts | 11 +- .../telemetry_collection/get_licenses.test.ts | 84 ++ .../telemetry_collection/get_licenses.ts | 84 ++ .../register_monitoring_collection.ts | 2 + .../get_stats_with_xpack.test.ts.snap | 118 ++ .../__tests__/get_xpack.js | 105 +- .../server/telemetry_collection/constants.ts | 1 + .../get_stats_with_xpack.test.ts | 113 ++ .../get_stats_with_xpack.ts | 28 +- .../server/telemetry_collection/get_xpack.js | 85 -- .../server/telemetry_collection/get_xpack.ts | 25 + .../register_xpack_collection.ts | 2 + .../apis/telemetry/fixtures/basiccluster.json | 7 + .../apis/telemetry/fixtures/multicluster.json | 1002 ++++++++++++++++- 24 files changed, 1671 insertions(+), 263 deletions(-) rename src/legacy/core_plugins/telemetry/server/telemetry_collection/{get_cluster_info.js => get_cluster_info.ts} (61%) create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts create mode 100644 x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap create mode 100644 x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts delete mode 100644 x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js create mode 100644 x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.ts diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 0394dea343adf..715ca56e290a2 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -20,6 +20,7 @@ import { encryptTelemetry } from './collectors'; import { CallCluster } from '../../elasticsearch'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; +import { ESLicense } from './telemetry_collection/get_local_license'; export type EncryptedStatsGetterConfig = { unencrypted: false } & { server: any; @@ -45,22 +46,38 @@ export interface StatsCollectionConfig { end: string | number; } +export interface BasicStatsPayload { + timestamp: string; + cluster_uuid: string; + cluster_name: string; + version: string; + cluster_stats: object; + collection?: string; + stack_stats: object; +} + export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise; -export type StatsGetter = ( +export type StatsGetter = ( + clustersDetails: ClusterDetails[], + config: StatsCollectionConfig +) => Promise; +export type LicenseGetter = ( clustersDetails: ClusterDetails[], config: StatsCollectionConfig -) => Promise; +) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>; -interface CollectionConfig { +interface CollectionConfig { title: string; priority: number; esCluster: string; - statsGetter: StatsGetter; + statsGetter: StatsGetter; clusterDetailsGetter: ClusterDetailsGetter; + licenseGetter: LicenseGetter; } interface Collection { statsGetter: StatsGetter; + licenseGetter: LicenseGetter; clusterDetailsGetter: ClusterDetailsGetter; esCluster: string; title: string; @@ -70,8 +87,15 @@ export class TelemetryCollectionManager { private usageGetterMethodPriority = -1; private collections: Collection[] = []; - public setCollection = (collectionConfig: CollectionConfig) => { - const { title, priority, esCluster, statsGetter, clusterDetailsGetter } = collectionConfig; + public setCollection = (collectionConfig: CollectionConfig) => { + const { + title, + priority, + esCluster, + statsGetter, + clusterDetailsGetter, + licenseGetter, + } = collectionConfig; if (typeof priority !== 'number') { throw new Error('priority must be set.'); @@ -88,10 +112,14 @@ export class TelemetryCollectionManager { throw Error('esCluster name must be set for the getCluster method.'); } if (!clusterDetailsGetter) { - throw Error('Cluser UUIds method is not set.'); + throw Error('Cluster UUIds method is not set.'); + } + if (!licenseGetter) { + throw Error('License getter method not set.'); } this.collections.unshift({ + licenseGetter, statsGetter, clusterDetailsGetter, esCluster, @@ -141,7 +169,19 @@ export class TelemetryCollectionManager { return; } - return await collection.statsGetter(clustersDetails, statsCollectionConfig); + const [stats, licenses] = await Promise.all([ + collection.statsGetter(clustersDetails, statsCollectionConfig), + collection.licenseGetter(clustersDetails, statsCollectionConfig), + ]); + + return stats.map(stat => { + const license = licenses[stat.cluster_uuid]; + return { + ...(license ? { license } : {}), + ...stat, + collectionSource: collection.title, + }; + }); }; public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => { diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts similarity index 61% rename from src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js rename to src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts index 2e4ed0b36ed26..67812457ed4ec 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts @@ -17,14 +17,32 @@ * under the License. */ +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +// This can be removed when the ES client improves the types +export interface ESClusterInfo { + cluster_uuid: string; + cluster_name: string; + version: { + number: string; + build_flavor: string; + build_type: string; + build_hash: string; + build_date: string; + build_snapshot?: boolean; + lucene_version: string; + minimum_wire_compatibility_version: string; + minimum_index_compatibility_version: string; + }; +} + /** * Get the cluster info from the connected cluster. * * This is the equivalent to GET / * * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch. */ -export function getClusterInfo(callCluster) { - return callCluster('info'); +export function getClusterInfo(callCluster: CallCluster) { + return callCluster('info'); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts new file mode 100644 index 0000000000000..589392ffb6095 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts @@ -0,0 +1,90 @@ +/* + * 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 { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LicenseGetter } from '../collection_manager'; + +// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html +export interface ESLicense { + status: string; + uid: string; + type: string; + issue_date: string; + issue_date_in_millis: number; + expiry_date: string; + expirty_date_in_millis: number; + max_nodes: number; + issued_to: string; + issuer: string; + start_date_in_millis: number; +} +let cachedLicense: ESLicense | undefined; + +function fetchLicense(callCluster: CallCluster, local: boolean) { + return callCluster<{ license: ESLicense }>('transport.request', { + method: 'GET', + path: '/_license', + query: { + local, + // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. + accept_enterprise: 'true', + }, + }); +} + +/** + * Get the cluster's license from the connected node. + * + * This is the equivalent of GET /_license?local=true . + * + * Like any X-Pack related API, X-Pack must installed for this to work. + */ +async function getLicenseFromLocalOrMaster(callCluster: CallCluster) { + // Fetching the local license is cheaper than getting it from the master and good enough + const { license } = await fetchLicense(callCluster, true).catch(async err => { + if (cachedLicense) { + try { + // Fallback to the master node's license info + const response = await fetchLicense(callCluster, false); + return response; + } catch (masterError) { + if (masterError.statusCode === 404) { + // If the master node does not have a license, we can assume there is no license + cachedLicense = undefined; + } else { + // Any other errors from the master node, throw and do not send any telemetry + throw err; + } + } + } + return { license: void 0 }; + }); + + if (license) { + cachedLicense = license; + } + return license; +} + +export const getLocalLicense: LicenseGetter = async (clustersDetails, { callCluster }) => { + const license = await getLicenseFromLocalOrMaster(callCluster); + + // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. + return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); +}; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 8adb6d237bee8..d99710deb1cbc 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -17,11 +17,8 @@ * under the License. */ -import { get, omit } from 'lodash'; -// @ts-ignore -import { getClusterInfo } from './get_cluster_info'; +import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; -// @ts-ignore import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { StatsGetter } from '../collection_manager'; @@ -33,20 +30,19 @@ import { StatsGetter } from '../collection_manager'; * @param {Object} clusterInfo Cluster info (GET /) * @param {Object} clusterStats Cluster stats (GET /_cluster/stats) * @param {Object} kibana The Kibana Usage stats - * @return {Object} A combined object containing the different responses. */ export function handleLocalStats( server: any, - clusterInfo: any, - clusterStats: any, + { cluster_name, cluster_uuid, version }: ESClusterInfo, + { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats ) { return { timestamp: new Date().toISOString(), - cluster_uuid: get(clusterInfo, 'cluster_uuid'), - cluster_name: get(clusterInfo, 'cluster_name'), - version: get(clusterInfo, 'version.number'), - cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), + cluster_uuid, + cluster_name, + version: version.number, + cluster_stats: clusterStats, collection: 'local', stack_stats: { kibana: handleKibanaStats(server, kibana), @@ -54,14 +50,12 @@ export function handleLocalStats( }; } +export type TelemetryLocalStats = ReturnType; + /** * Get statistics for all products joined by Elasticsearch cluster. - * - * @param {Object} server The Kibana server instance used to call ES as the internal user - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ -export const getLocalStats: StatsGetter = async (clustersDetails, config) => { +export const getLocalStats: StatsGetter = async (clustersDetails, config) => { const { server, callCluster, usageCollection } = config; return await Promise.all( diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts index 7f228dbc5e6f6..9ac94216c21bc 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts @@ -17,7 +17,6 @@ * under the License. */ -// @ts-ignore export { getLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts index faf8e9de79194..6580b47dba08e 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -39,6 +39,7 @@ import { telemetryCollectionManager } from '../collection_manager'; import { getLocalStats } from './get_local_stats'; import { getClusterUuids } from './get_cluster_stats'; +import { getLocalLicense } from './get_local_license'; export function registerCollection() { telemetryCollectionManager.setCollection({ @@ -47,5 +48,6 @@ export function registerCollection() { priority: 0, statsGetter: getLocalStats, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLocalLicense, }); } diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 0ebcd52f1423c..0a49b0ae3084e 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -26,9 +26,18 @@ const mockSubtract = jest.fn().mockImplementation(() => { }; }); +const mockClone = jest.fn().mockImplementation(() => { + return { + clone: mockClone, + subtract: mockSubtract, + toISOString: jest.fn(), + }; +}); + jest.mock('moment', () => { return jest.fn().mockImplementation(() => { return { + clone: mockClone, subtract: mockSubtract, toISOString: jest.fn(), }; @@ -43,6 +52,7 @@ describe('TelemetryService', () => { expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { body: JSON.stringify({ unencrypted: false, timeRange: {} }), }); + expect(mockClone).toBeCalled(); expect(mockSubtract).toBeCalledWith(20, 'minutes'); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index 073886e7d1327..cb91451bd8ef4 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -92,7 +92,10 @@ export class TelemetryService { body: JSON.stringify({ unencrypted, timeRange: { - min: now.subtract(20, 'minutes').toISOString(), + min: now + .clone() // Need to clone it to avoid mutation (and max being the same value) + .subtract(20, 'minutes') + .toISOString(), max: now.toISOString(), }, }), diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 470642f9dd8a3..dcc7924fe171a 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -5,7 +5,7 @@ */ import sinon from 'sinon'; -import { addStackStats, getAllStats, handleAllStats } from './get_all_stats'; +import { getStackStats, getAllStats, handleAllStats } from './get_all_stats'; import { ESClusterStats } from './get_es_stats'; import { KibanaStats } from './get_kibana_stats'; import { ClustersHighLevelStats } from './get_high_level_stats'; @@ -223,7 +223,8 @@ describe('get_all_stats', () => { beats: {}, }); - expect(clusters).toStrictEqual(expectedClusters); + const [a, b, c] = expectedClusters; + expect(clusters).toStrictEqual([a, b, { ...c, stack_stats: {} }]); }); it('handles no clusters response', () => { @@ -233,9 +234,8 @@ describe('get_all_stats', () => { }); }); - describe('addStackStats', () => { + describe('getStackStats', () => { it('searches for clusters', () => { - const cluster = { cluster_uuid: 'a' }; const stats = { a: { count: 2, @@ -250,9 +250,7 @@ describe('get_all_stats', () => { }, }; - addStackStats(cluster as ESClusterStats, stats, 'xyz'); - - expect((cluster as any).stack_stats.xyz).toStrictEqual(stats.a); + expect(getStackStats('a', stats, 'xyz')).toStrictEqual({ xyz: stats.a }); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index aa5e937387daf..a6ed5254dabd5 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -61,38 +61,31 @@ export function handleAllStats( } ) { return clusters.map(cluster => { - // if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats - addStackStats(cluster, kibana, KIBANA_SYSTEM_ID); - addStackStats(cluster, logstash, LOGSTASH_SYSTEM_ID); - addStackStats(cluster, beats, BEATS_SYSTEM_ID); - mergeXPackStats(cluster, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph + const stats = { + ...cluster, + stack_stats: { + ...cluster.stack_stats, + // if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats + ...getStackStats(cluster.cluster_uuid, kibana, KIBANA_SYSTEM_ID), + ...getStackStats(cluster.cluster_uuid, logstash, LOGSTASH_SYSTEM_ID), + ...getStackStats(cluster.cluster_uuid, beats, BEATS_SYSTEM_ID), + }, + }; - return cluster; + mergeXPackStats(stats, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph + + return stats; }); } -/** - * Add product data to the {@code cluster}, only if it exists for the current {@code cluster}. - * - * @param {Object} cluster The current Elasticsearch cluster stats - * @param {Object} allProductStats Product stats, keyed by Cluster UUID - * @param {String} product The product name being added (e.g., 'kibana' or 'logstash') - */ -export function addStackStats( - cluster: ESClusterStats & { stack_stats?: { [product: string]: K } }, +export function getStackStats( + clusterUuid: string, allProductStats: T, product: string ) { - const productStats = allProductStats[cluster.cluster_uuid]; - + const productStats = allProductStats[clusterUuid]; // Don't add it if they're not using (or configured to report stats) this product for this cluster - if (productStats) { - if (!cluster.stack_stats) { - cluster.stack_stats = {}; - } - - cluster.stack_stats[product] = productStats; - } + return productStats ? { [product]: productStats } : {}; } export function mergeXPackStats( diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index f0ae1163d3f52..2f2fffd3f0823 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -48,11 +48,6 @@ export function fetchElasticsearchStats( 'hits.hits._source.timestamp', 'hits.hits._source.cluster_name', 'hits.hits._source.version', - 'hits.hits._source.license.status', // license data only includes necessary fields to drive UI - 'hits.hits._source.license.type', - 'hits.hits._source.license.issue_date', - 'hits.hits._source.license.expiry_date', - 'hits.hits._source.license.expiry_date_in_millis', 'hits.hits._source.cluster_stats', 'hits.hits._source.stack_stats', ], @@ -79,7 +74,11 @@ export function fetchElasticsearchStats( export interface ESClusterStats { cluster_uuid: string; - type: 'cluster_stats'; + cluster_name: string; + timestamp: string; + version: string; + cluster_stats: object; + stack_stats?: object; } /** diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts new file mode 100644 index 0000000000000..bb8326ce0b63a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { getLicenses, handleLicenses, fetchLicenses } from './get_licenses'; + +describe('get_licenses', () => { + const callWith = sinon.stub(); + const size = 123; + const server = { + config: sinon.stub().returns({ + get: sinon + .stub() + .withArgs('xpack.monitoring.elasticsearch.index_pattern') + .returns('.monitoring-es-N-*') + .withArgs('xpack.monitoring.max_bucket_size') + .returns(size), + }), + }; + const response = { + hits: { + hits: [ + { _id: 'abc', _source: { cluster_uuid: 'abc', license: { type: 'basic' } } }, + { _id: 'xyz', _source: { cluster_uuid: 'xyz', license: { type: 'basic' } } }, + { _id: '123', _source: { cluster_uuid: '123' } }, + ], + }, + }; + const expectedClusters = response.hits.hits.map(hit => hit._source); + const clusterUuids = expectedClusters.map(cluster => ({ clusterUuid: cluster.cluster_uuid })); + const expectedLicenses = { + abc: { type: 'basic' }, + xyz: { type: 'basic' }, + '123': void 0, + }; + + describe('getLicenses', () => { + it('returns clusters', async () => { + callWith.withArgs('search').returns(Promise.resolve(response)); + + expect( + await getLicenses(clusterUuids, { server, callCluster: callWith } as any) + ).toStrictEqual(expectedLicenses); + }); + }); + + describe('fetchLicenses', () => { + it('searches for clusters', async () => { + callWith.returns(response); + + expect( + await fetchLicenses( + server, + callWith, + clusterUuids.map(({ clusterUuid }) => clusterUuid) + ) + ).toStrictEqual(response); + }); + }); + + describe('handleLicenses', () => { + // filterPath makes it easy to ignore anything unexpected because it will come back empty + it('handles unexpected response', () => { + const clusters = handleLicenses({} as any); + + expect(clusters).toStrictEqual({}); + }); + + it('handles valid response', () => { + const clusters = handleLicenses(response as any); + + expect(clusters).toStrictEqual(expectedLicenses); + }); + + it('handles no hits response', () => { + const clusters = handleLicenses({ hits: { hits: [] } } as any); + + expect(clusters).toStrictEqual({}); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts new file mode 100644 index 0000000000000..7364227e7dc92 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + StatsCollectionConfig, + LicenseGetter, +} from 'src/legacy/core_plugins/telemetry/server/collection_manager'; +import { SearchResponse } from 'elasticsearch'; +import { ESLicense } from 'src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; + +/** + * Get statistics for all selected Elasticsearch clusters. + */ +export const getLicenses: LicenseGetter = async (clustersDetails, { server, callCluster }) => { + const clusterUuids = clustersDetails.map(({ clusterUuid }) => clusterUuid); + const response = await fetchLicenses(server, callCluster, clusterUuids); + return handleLicenses(response); +}; + +/** + * Fetch the Elasticsearch stats. + * + * @param {Object} server The server instance + * @param {function} callCluster The callWithRequest or callWithInternalUser handler + * @param {Array} clusterUuids Cluster UUIDs to limit the request against + * + * Returns the response for the aggregations to fetch details for the product. + */ +export function fetchLicenses( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[] +) { + const config = server.config(); + const params = { + index: INDEX_PATTERN_ELASTICSEARCH, + size: config.get('monitoring.ui.max_bucket_size'), + ignoreUnavailable: true, + filterPath: ['hits.hits._source.cluster_uuid', 'hits.hits._source.license'], + body: { + query: { + bool: { + filter: [ + /* + * Note: Unlike most places, we don't care about the old _type: cluster_stats because it would NOT + * have the license in it (that used to be in the .monitoring-data-2 index in cluster_info) + */ + { term: { type: 'cluster_stats' } }, + { terms: { cluster_uuid: clusterUuids } }, + ], + }, + }, + collapse: { field: 'cluster_uuid' }, + sort: { timestamp: { order: 'desc' } }, + }, + }; + + return callCluster('search', params); +} + +export interface ESClusterStatsWithLicense { + cluster_uuid: string; + type: 'cluster_stats'; + license?: ESLicense; +} + +/** + * Extract the cluster stats for each cluster. + */ +export function handleLicenses(response: SearchResponse) { + const clusters = response.hits?.hits || []; + + return clusters.reduce( + (acc, { _source }) => ({ + ...acc, + [_source.cluster_uuid]: _source.license, + }), + {} + ); +} diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts index f0fda5229cb5c..0b14eb05f796f 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts @@ -7,6 +7,7 @@ import { telemetryCollectionManager } from '../../../../../../src/legacy/core_plugins/telemetry/server'; import { getAllStats } from './get_all_stats'; import { getClusterUuids } from './get_cluster_uuids'; +import { getLicenses } from './get_licenses'; export function registerMonitoringCollection() { telemetryCollectionManager.setCollection({ @@ -15,5 +16,6 @@ export function registerMonitoringCollection() { priority: 2, statsGetter: getAllStats, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLicenses, }); } diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap new file mode 100644 index 0000000000000..1a70504dc9391 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no license nor X-Pack telemetry) 1`] = ` +Array [ + Object { + "cluster_name": "test", + "cluster_stats": Object {}, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", + }, + ], + "platforms": Array [ + Object { + "count": 1, + "platform": "rocky", + }, + ], + }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], + }, + }, + "version": "8.0.0", + }, +] +`; + +exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + X-Pack) 1`] = ` +Array [ + Object { + "cluster_name": "test", + "cluster_stats": Object {}, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", + }, + ], + "platforms": Array [ + Object { + "count": 1, + "platform": "rocky", + }, + ], + }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], + }, + "xpack": Object {}, + }, + "version": "8.0.0", + }, +] +`; diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js index eca130b4d7465..eb03701fd195b 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js @@ -8,42 +8,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { TIMEOUT } from '../constants'; -import { getXPackLicense, getXPackUsage, getXPack, handleXPack } from '../get_xpack'; - -function mockGetXPackLicense(callCluster, license, req) { - callCluster - .withArgs(req, 'transport.request', { - method: 'GET', - path: '/_license', - query: { - local: 'true', - accept_enterprise: 'true', - }, - }) - .returns( - license.then( - response => ({ license: response }), - () => {} // Catch error so that we don't emit UnhandledPromiseRejectionWarning for tests with invalid license - ) - ); - - callCluster - .withArgs('transport.request', { - method: 'GET', - path: '/_license', - query: { - local: 'true', - accept_enterprise: 'true', - }, - }) - // conveniently wraps the passed in license object as { license: response }, like it really is - .returns( - license.then( - response => ({ license: response }), - () => {} // Catch error so that we don't emit UnhandledPromiseRejectionWarning for tests with invalid license - ) - ); -} +import { getXPackUsage } from '../get_xpack'; function mockGetXPackUsage(callCluster, usage, req) { callCluster @@ -67,31 +32,7 @@ function mockGetXPackUsage(callCluster, usage, req) { .returns(usage); } -/** - * Mock getXPack responses. - * - * @param {Function} callCluster Sinon function mock. - * @param {Promise} license Promised license response. - * @param {Promise} usage Promised usage response. - * @param {Object} usage reqeust object. - */ -export function mockGetXPack(callCluster, license, usage, req) { - mockGetXPackLicense(callCluster, license, req); - mockGetXPackUsage(callCluster, usage, req); -} - describe('get_xpack', () => { - describe('getXPackLicense', () => { - it('uses callCluster to get /_license API', async () => { - const response = { type: 'basic' }; - const callCluster = sinon.stub(); - - mockGetXPackLicense(callCluster, Promise.resolve(response)); - - expect(await getXPackLicense(callCluster)).to.eql(response); - }); - }); - describe('getXPackUsage', () => { it('uses callCluster to get /_xpack/usage API', () => { const response = Promise.resolve({}); @@ -102,48 +43,4 @@ describe('get_xpack', () => { expect(getXPackUsage(callCluster)).to.be(response); }); }); - - describe('handleXPack', () => { - it('uses data as expected', () => { - const license = { fake: 'data' }; - const usage = { also: 'fake', nested: { object: { data: [{ field: 1 }, { field: 2 }] } } }; - - expect(handleXPack(license, usage)).to.eql({ license, stack_stats: { xpack: usage } }); - }); - }); - - describe('getXPack', () => { - it('returns the formatted response object', async () => { - const license = { fancy: 'license' }; - const xpack = { also: 'fancy' }; - - const callCluster = sinon.stub(); - - mockGetXPack(callCluster, Promise.resolve(license), Promise.resolve(xpack)); - - const data = await getXPack(callCluster); - - expect(data).to.eql({ license, xpack }); - }); - - it('returns empty object upon license failure', async () => { - const callCluster = sinon.stub(); - - mockGetXPack(callCluster, Promise.reject(new Error()), Promise.resolve({ also: 'fancy' })); - - const data = await getXPack(callCluster); - - expect(data).to.eql({}); - }); - - it('returns empty object upon usage failure', async () => { - const callCluster = sinon.stub(); - - mockGetXPack(callCluster, Promise.resolve({ fancy: 'license' }), Promise.reject(new Error())); - - const data = await getXPack(callCluster); - - expect(data).to.eql({}); - }); - }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts index c89fbe416a0cc..b6f1aabab95c4 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts @@ -7,4 +7,5 @@ /** * The timeout used by each request, whenever a timeout can be specified. */ + export const TIMEOUT = '30s'; diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts new file mode 100644 index 0000000000000..b85cbd9661022 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { getStatsWithXpack } from './get_stats_with_xpack'; + +const kibana = { + kibana: { + great: 'googlymoogly', + versions: [{ version: '8675309', count: 1 }], + }, + kibana_stats: { + os: { + platform: 'rocky', + platformRelease: 'iv', + }, + }, + localization: { + locale: 'en', + labelsCount: 0, + integrities: {}, + }, + sun: { chances: 5 }, + clouds: { chances: 95 }, + rain: { chances: 2 }, + snow: { chances: 0 }, +}; + +const getMockServer = (getCluster = jest.fn()) => ({ + log(tags: string[], message: string) { + // eslint-disable-next-line no-console + console.log({ tags, message }); + }, + config() { + return { + get(item: string) { + switch (item) { + case 'pkg.version': + return '8675309-snapshot'; + default: + throw Error(`unexpected config.get('${item}') received.`); + } + }, + }; + }, + plugins: { + elasticsearch: { getCluster }, + }, +}); + +const mockUsageCollection = (kibanaUsage = kibana) => ({ + bulkFetch: () => kibanaUsage, + toObject: (data: any) => data, +}); + +describe('Telemetry Collection: Get Aggregated Stats', () => { + test('OSS-like telemetry (no license nor X-Pack telemetry)', async () => { + const callCluster = jest.fn(async (method: string, options: { path?: string }) => { + switch (method) { + case 'transport.request': + if (options.path === '/_license' || options.path === '/_xpack/usage') { + // eslint-disable-next-line no-throw-literal + throw { statusCode: 404 }; + } + return {}; + case 'info': + return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; + default: + return {}; + } + }); + const usageCollection = mockUsageCollection(); + const server = getMockServer(); + + const stats = await getStatsWithXpack([{ clusterUuid: '1234' }], { + callCluster, + usageCollection, + server, + } as any); + expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + }); + + test('X-Pack telemetry (license + X-Pack)', async () => { + const callCluster = jest.fn(async (method: string, options: { path?: string }) => { + switch (method) { + case 'transport.request': + if (options.path === '/_license') { + return { + license: { type: 'basic' }, + }; + } + if (options.path === '/_xpack/usage') { + return {}; + } + case 'info': + return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; + default: + return {}; + } + }); + const usageCollection = mockUsageCollection(); + const server = getMockServer(); + + const stats = await getStatsWithXpack([{ clusterUuid: '1234' }], { + callCluster, + usageCollection, + server, + } as any); + expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts index 41076d96231c9..ea7465f66f120 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts @@ -4,19 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import { getXPack } from './get_xpack'; -import { getLocalStats } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection'; import { StatsGetter } from '../../../../../../src/legacy/core_plugins/telemetry/server/collection_manager'; +import { + getLocalStats, + TelemetryLocalStats, +} from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats'; +import { getXPackUsage } from './get_xpack'; -export const getStatsWithXpack: StatsGetter = async function(clustersDetails, config) { +export type TelemetryAggregatedStats = TelemetryLocalStats & { + stack_stats: { xpack?: object }; +}; + +export const getStatsWithXpack: StatsGetter = async function( + clustersDetails, + config +) { const { callCluster } = config; const clustersLocalStats = await getLocalStats(clustersDetails, config); - const { license, xpack } = await getXPack(callCluster); + const xpack = await getXPackUsage(callCluster).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. return clustersLocalStats.map(localStats => { - localStats.license = license; - localStats.stack_stats.xpack = xpack; + if (xpack) { + return { + ...localStats, + stack_stats: { ...localStats.stack_stats, xpack }, + }; + } + return localStats; }); }; diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js deleted file mode 100644 index aaeb890981aa1..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js +++ /dev/null @@ -1,85 +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 { TIMEOUT } from './constants'; - -/** - * Get the cluster stats from the connected cluster. - * - * This is the equivalent of GET /_license?local=true . - * - * Like any X-Pack related API, X-Pack must installed for this to work. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch. - */ -export function getXPackLicense(callCluster) { - return callCluster('transport.request', { - method: 'GET', - path: '/_license', - query: { - // Fetching the local license is cheaper than getting it from the master and good enough - local: 'true', - // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. - accept_enterprise: 'true', - }, - }).then(({ license }) => license); -} - -/** - * Get the cluster stats from the connected cluster. - * - * This is the equivalent of GET /_xpack/usage?master_timeout=${TIMEOUT} - * - * Like any X-Pack related API, X-Pack must installed for this to work. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch equivalent to GET /_cluster/stats. - */ -export function getXPackUsage(callCluster) { - return callCluster('transport.request', { - method: 'GET', - path: '/_xpack/usage', - query: { - master_timeout: TIMEOUT, - }, - }); -} - -/** - * Combine the X-Pack responses into a single response as Monitoring does already. - * - * @param {Object} license The license returned from /_license - * @param {Object} usage The usage details returned from /_xpack/usage - * @return {Object} An object containing both the license and usage. - */ -export function handleXPack(license, usage) { - return { - license, - stack_stats: { - xpack: usage, - }, - }; -} - -/** - * Combine all X-Pack requests as a singular request that is ignored upon failure. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} - */ -export function getXPack(callCluster) { - return Promise.all([getXPackLicense(callCluster), getXPackUsage(callCluster)]) - .then(([license, xpack]) => { - return { - license, - xpack, - }; - }) - .catch(() => { - return {}; - }); -} diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.ts new file mode 100644 index 0000000000000..9b69540007e5f --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.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 { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { TIMEOUT } from './constants'; + +/** + * Get the cluster stats from the connected cluster. + * + * This is the equivalent of GET /_xpack/usage?master_timeout=${TIMEOUT} + * + * Like any X-Pack related API, X-Pack must installed for this to work. + */ +export function getXPackUsage(callCluster: CallCluster) { + return callCluster('transport.request', { + method: 'GET', + path: '/_xpack/usage', + query: { + master_timeout: TIMEOUT, + }, + }); +} diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts index 57faf2da90d09..04445d7bde7d7 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getLocalLicense } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license'; import { telemetryCollectionManager } from '../../../../../../src/legacy/core_plugins/telemetry/server'; import { getClusterUuids } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection'; import { getStatsWithXpack } from './get_stats_with_xpack'; @@ -15,5 +16,6 @@ export function registerMonitoringCollection() { priority: 1, statsGetter: getStatsWithXpack, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLocalLicense, }); } diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json index 5c3c8cfcab7a6..a0097f53ac93b 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json @@ -1,6 +1,7 @@ [ { "cluster_name": "docker-cluster", + "collectionSource": "monitoring", "cluster_stats": { "indices": { "completion": { @@ -161,6 +162,12 @@ }, "cluster_uuid": "ooEYzl3fSL222Y6eVm7SAA", "license": { + "uid": "79dc3adb-e85e-4cef-a985-9b74eb6c07c1", + "issue_date_in_millis": 1532383643540, + "issued_to": "docker-cluster", + "issuer": "elasticsearch", + "max_nodes": 1000, + "start_date_in_millis": -1, "issue_date": "2018-07-23T22:07:23.540Z", "status": "active", "type": "basic" diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json index 2e6a75ee75972..6cc9c55157b28 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json @@ -1 +1,1001 @@ -[{"cluster_uuid":"6d-9tDFTRe-qT5GoBytdlQ","timestamp":"2017-08-15T22:10:59.952Z","cluster_name":"clustertwo","version":"7.0.0-alpha1","license":{"status":"active","type":"basic","issue_date":"2014-09-29T00:00:00.000Z","expiry_date":"2030-08-29T23:59:59.999Z","expiry_date_in_millis":1914278399999},"cluster_stats":{"timestamp":1502835059952,"status":"green","indices":{"count":1,"shards":{"total":1,"primaries":1,"replication":0,"index":{"shards":{"min":1,"max":1,"avg":1},"primaries":{"min":1,"max":1,"avg":1},"replication":{"min":0,"max":0,"avg":0}}},"docs":{"count":8,"deleted":0},"store":{"size_in_bytes":34095},"fielddata":{"memory_size_in_bytes":0,"evictions":0},"query_cache":{"memory_size_in_bytes":0,"total_count":0,"hit_count":0,"miss_count":0,"cache_size":0,"cache_count":0,"evictions":0},"completion":{"size_in_bytes":0},"segments":{"count":8,"memory_in_bytes":13852,"terms_memory_in_bytes":10412,"stored_fields_memory_in_bytes":2496,"term_vectors_memory_in_bytes":0,"norms_memory_in_bytes":256,"points_memory_in_bytes":8,"doc_values_memory_in_bytes":680,"index_writer_memory_in_bytes":0,"version_map_memory_in_bytes":0,"fixed_bit_set_memory_in_bytes":0,"max_unsafe_auto_id_timestamp":-1,"file_sizes":{}}},"nodes":{"count":{"total":1,"data":1,"coordinating_only":0,"master":1,"ingest":1},"versions":["7.0.0-alpha1"],"os":{"available_processors":4,"allocated_processors":1,"names":[{"name":"Mac OS X","count":1}],"mem":{"total_in_bytes":17179869184,"free_in_bytes":183799808,"used_in_bytes":16996069376,"free_percent":1,"used_percent":99}},"process":{"cpu":{"percent":0},"open_file_descriptors":{"min":163,"max":163,"avg":163}},"jvm":{"max_uptime_in_millis":701043,"versions":[{"vm_version":"25.121-b13","count":1,"vm_vendor":"Oracle Corporation","version":"1.8.0_121","vm_name":"Java HotSpot(TM) 64-Bit Server VM"}],"mem":{"heap_used_in_bytes":204299464,"heap_max_in_bytes":628555776},"threads":40},"fs":{"total_in_bytes":499065712640,"free_in_bytes":200665341952,"available_in_bytes":200403197952},"plugins":[{"classname":"org.elasticsearch.xpack.XPackPlugin","name":"x-pack","description":"Elasticsearch Expanded Pack Plugin","version":"7.0.0-alpha1","has_native_controller":true}],"network_types":{"transport_types":{"security4":1},"http_types":{"security4":1}}}},"stack_stats":{"xpack":{"security":{"available":false,"enabled":true,"realms":{"file":{"available":false,"enabled":false},"ldap":{"available":false,"enabled":false},"native":{"available":false,"enabled":false},"active_directory":{"available":false,"enabled":false},"pki":{"available":false,"enabled":false}},"roles":{"native":{"size":1,"dls":false,"fls":false},"file":{"size":0,"dls":false,"fls":false}},"role_mapping":{"native":{"size":0,"enabled":0}},"ssl":{"http":{"enabled":false}},"audit":{"outputs":["logfile"],"enabled":false},"ipfilter":{"http":false,"transport":false},"anonymous":{"enabled":false}},"monitoring":{"available":true,"enabled":true,"enabled_exporters":{"http":1}},"watcher":{"available":false,"enabled":true,"execution":{"actions":{"_all":{"total":0,"total_time_in_ms":0}}}},"graph":{"available":false,"enabled":true},"ml":{"available":false,"enabled":true,"jobs":{"_all":{"count":0,"detectors":{"total":0,"min":0,"avg":0,"max":0},"model_size":{"total":0,"min":0,"avg":0,"max":0}}},"datafeeds":{"_all":{"count":0}}},"logstash":{"available":false,"enabled":true},"ccr":{"auto_follow_patterns_count":0,"available":true,"follower_indices_count":0,"enabled":true}}}},{"cluster_uuid":"lOF8kofiS_2DX58o9mXJ1Q","timestamp":"2017-08-15T22:10:54.610Z","cluster_name":"monitoring-one","version":"7.0.0-alpha1","license":{"status":"active","type":"trial","issue_date":"2017-08-15T21:58:28.997Z","expiry_date":"2017-09-14T21:58:28.997Z","expiry_date_in_millis":1505426308997},"cluster_stats":{"timestamp":1502835054610,"status":"yellow","indices":{"count":8,"shards":{"total":8,"primaries":8,"replication":0,"index":{"shards":{"min":1,"max":1,"avg":1},"primaries":{"min":1,"max":1,"avg":1},"replication":{"min":0,"max":0,"avg":0}}},"docs":{"count":3997,"deleted":69},"store":{"size_in_bytes":2647163},"fielddata":{"memory_size_in_bytes":2104,"evictions":0},"query_cache":{"memory_size_in_bytes":0,"total_count":0,"hit_count":0,"miss_count":0,"cache_size":0,"cache_count":0,"evictions":0},"completion":{"size_in_bytes":0},"segments":{"count":36,"memory_in_bytes":278961,"terms_memory_in_bytes":166031,"stored_fields_memory_in_bytes":11544,"term_vectors_memory_in_bytes":0,"norms_memory_in_bytes":6784,"points_memory_in_bytes":3250,"doc_values_memory_in_bytes":91352,"index_writer_memory_in_bytes":205347,"version_map_memory_in_bytes":26362,"fixed_bit_set_memory_in_bytes":992,"max_unsafe_auto_id_timestamp":-1,"file_sizes":{}}},"nodes":{"count":{"total":1,"data":1,"coordinating_only":0,"master":1,"ingest":1},"versions":["7.0.0-alpha1"],"os":{"available_processors":4,"allocated_processors":1,"names":[{"name":"Mac OS X","count":1}],"mem":{"total_in_bytes":17179869184,"free_in_bytes":86732800,"used_in_bytes":17093136384,"free_percent":1,"used_percent":99}},"process":{"cpu":{"percent":2},"open_file_descriptors":{"min":178,"max":178,"avg":178}},"jvm":{"max_uptime_in_millis":761002,"versions":[{"vm_version":"25.121-b13","count":1,"vm_vendor":"Oracle Corporation","version":"1.8.0_121","vm_name":"Java HotSpot(TM) 64-Bit Server VM"}],"mem":{"heap_used_in_bytes":133041176,"heap_max_in_bytes":628555776},"threads":42},"fs":{"total_in_bytes":499065712640,"free_in_bytes":200665792512,"available_in_bytes":200403648512},"plugins":[{"classname":"org.elasticsearch.xpack.XPackPlugin","name":"x-pack","description":"Elasticsearch Expanded Pack Plugin","version":"7.0.0-alpha1","has_native_controller":true}],"network_types":{"transport_types":{"security4":1},"http_types":{"security4":1}}}},"stack_stats":{"xpack":{"security":{"available":true,"enabled":true,"realms":{"file":{"name":["default_file"],"available":true,"size":[0],"enabled":true,"order":[2147483647]},"ldap":{"available":true,"enabled":false},"native":{"name":["default_native"],"available":true,"size":[2],"enabled":true,"order":[2147483647]},"active_directory":{"available":true,"enabled":false},"pki":{"available":true,"enabled":false}},"roles":{"native":{"size":1,"dls":false,"fls":false},"file":{"size":0,"dls":false,"fls":false}},"role_mapping":{"native":{"size":0,"enabled":0}},"ssl":{"http":{"enabled":false}},"audit":{"outputs":["logfile"],"enabled":false},"ipfilter":{"http":false,"transport":false},"anonymous":{"enabled":false}},"monitoring":{"available":true,"enabled":true,"enabled_exporters":{"local":1}},"watcher":{"available":true,"enabled":true,"execution":{"actions":{"index":{"total":14,"total_time_in_ms":158},"_all":{"total":110,"total_time_in_ms":2245},"email":{"total":14,"total_time_in_ms":3}}}},"graph":{"available":true,"enabled":true},"ml":{"available":true,"enabled":true,"jobs":{"_all":{"count":0,"detectors":{"total":0,"min":0,"avg":0,"max":0},"model_size":{"total":0,"min":0,"avg":0,"max":0}}},"datafeeds":{"_all":{"count":0}}},"logstash":{"available":true,"enabled":true},"ccr":{"auto_follow_patterns_count":0,"available":true,"follower_indices_count":0,"enabled":true}}}},{"cluster_uuid":"TkHOX_-1TzWwbROwQJU5IA","timestamp":"2017-08-15T22:10:52.642Z","cluster_name":"clusterone","version":"7.0.0-alpha1","license":{"status":"active","type":"trial","issue_date":"2017-08-15T21:58:47.135Z","expiry_date":"2017-09-14T21:58:47.135Z","expiry_date_in_millis":1505426327135},"cluster_stats":{"timestamp":1502835052641,"status":"green","indices":{"count":5,"shards":{"total":26,"primaries":13,"replication":1,"index":{"shards":{"min":2,"max":10,"avg":5.2},"primaries":{"min":1,"max":5,"avg":2.6},"replication":{"min":1,"max":1,"avg":1}}},"docs":{"count":150,"deleted":0},"store":{"size_in_bytes":4838464},"fielddata":{"memory_size_in_bytes":0,"evictions":0},"query_cache":{"memory_size_in_bytes":0,"total_count":0,"hit_count":0,"miss_count":0,"cache_size":0,"cache_count":0,"evictions":0},"completion":{"size_in_bytes":0},"segments":{"count":76,"memory_in_bytes":1907922,"terms_memory_in_bytes":1595112,"stored_fields_memory_in_bytes":23744,"term_vectors_memory_in_bytes":0,"norms_memory_in_bytes":197184,"points_memory_in_bytes":3818,"doc_values_memory_in_bytes":88064,"index_writer_memory_in_bytes":7006184,"version_map_memory_in_bytes":260,"fixed_bit_set_memory_in_bytes":0,"max_unsafe_auto_id_timestamp":1502834982386,"file_sizes":{}}},"nodes":{"count":{"total":2,"data":2,"coordinating_only":0,"master":2,"ingest":2},"versions":["7.0.0-alpha1"],"os":{"available_processors":8,"allocated_processors":2,"names":[{"name":"Mac OS X","count":2}],"mem":{"total_in_bytes":34359738368,"free_in_bytes":332099584,"used_in_bytes":34027638784,"free_percent":1,"used_percent":99}},"process":{"cpu":{"percent":2},"open_file_descriptors":{"min":218,"max":237,"avg":227}},"jvm":{"max_uptime_in_millis":741786,"versions":[{"vm_version":"25.121-b13","count":2,"vm_vendor":"Oracle Corporation","version":"1.8.0_121","vm_name":"Java HotSpot(TM) 64-Bit Server VM"}],"mem":{"heap_used_in_bytes":465621856,"heap_max_in_bytes":1257111552},"threads":92},"fs":{"total_in_bytes":499065712640,"free_in_bytes":200666353664,"available_in_bytes":200404209664},"plugins":[{"classname":"org.elasticsearch.xpack.XPackPlugin","name":"x-pack","description":"Elasticsearch Expanded Pack Plugin","version":"7.0.0-alpha1","has_native_controller":true}],"network_types":{"transport_types":{"security4":2},"http_types":{"security4":2}}}},"stack_stats":{"xpack":{"security":{"available":true,"enabled":true,"realms":{"file":{"name":["default_file"],"available":true,"size":[0],"enabled":true,"order":[2147483647]},"ldap":{"available":true,"enabled":false},"native":{"name":["default_native"],"available":true,"size":[1],"enabled":true,"order":[2147483647]},"active_directory":{"available":true,"enabled":false},"pki":{"available":true,"enabled":false}},"roles":{"native":{"size":1,"dls":false,"fls":false},"file":{"size":0,"dls":false,"fls":false}},"role_mapping":{"native":{"size":0,"enabled":0}},"ssl":{"http":{"enabled":false}},"audit":{"outputs":["logfile"],"enabled":false},"ipfilter":{"http":false,"transport":false},"anonymous":{"enabled":false}},"monitoring":{"available":true,"enabled":true,"enabled_exporters":{"http":1}},"watcher":{"available":true,"enabled":true,"execution":{"actions":{"_all":{"total":0,"total_time_in_ms":0}}}},"graph":{"available":true,"enabled":true,"graph_workspace":{"total":0}},"ml":{"available":true,"enabled":true,"jobs":{"_all":{"count":3,"detectors":{"total":3,"min":1,"avg":1,"max":1},"model_size":{"total":0,"min":0,"avg":0,"max":0}},"opened":{"count":1,"detectors":{"total":1,"min":1,"avg":1,"max":1},"model_size":{"total":0,"min":0,"avg":0,"max":0}},"closed":{"count":2,"detectors":{"total":2,"min":1,"avg":1,"max":1},"model_size":{"total":0,"min":0,"avg":0,"max":0}}},"datafeeds":{"_all":{"count":0}}},"logstash":{"available":true,"enabled":true},"ccr":{"auto_follow_patterns_count":0,"available":true,"follower_indices_count":0,"enabled":true}},"kibana":{"count":1,"versions":[{"version":"7.0.0-alpha1","count":1}],"os":{"platforms":[],"platformReleases":[],"distros":[],"distroReleases":[]},"dashboard":{"total":0},"visualization":{"total":0},"search":{"total":0},"index_pattern":{"total":0},"graph_workspace":{"total":0},"timelion_sheet":{"total":0},"indices":1,"plugins":{}},"logstash":{"count":1,"versions":[{"version":"7.0.0-alpha1","count":1}],"os":{"platforms":[],"platformReleases":[],"distros":[],"distroReleases":[]}}}}] +[ + { + "cluster_uuid": "6d-9tDFTRe-qT5GoBytdlQ", + "collectionSource": "monitoring", + "timestamp": "2017-08-15T22:10:59.952Z", + "cluster_name": "clustertwo", + "version": "7.0.0-alpha1", + "license": { + "status": "active", + "type": "basic", + "issue_date": "2014-09-29T00:00:00.000Z", + "expiry_date": "2030-08-29T23:59:59.999Z", + "expiry_date_in_millis": 1914278399999, + "issue_date_in_millis": 1411948800000, + "issued_to": "issuedTo", + "issuer": "issuer", + "max_nodes": 1, + "uid": "893361dc-9749-4997-93cb-802e3d7fa4a8", + "hkey": null + }, + "cluster_stats": { + "timestamp": 1502835059952, + "status": "green", + "indices": { + "count": 1, + "shards": { + "total": 1, + "primaries": 1, + "replication": 0, + "index": { + "shards": { + "min": 1, + "max": 1, + "avg": 1 + }, + "primaries": { + "min": 1, + "max": 1, + "avg": 1 + }, + "replication": { + "min": 0, + "max": 0, + "avg": 0 + } + } + }, + "docs": { + "count": 8, + "deleted": 0 + }, + "store": { + "size_in_bytes": 34095 + }, + "fielddata": { + "memory_size_in_bytes": 0, + "evictions": 0 + }, + "query_cache": { + "memory_size_in_bytes": 0, + "total_count": 0, + "hit_count": 0, + "miss_count": 0, + "cache_size": 0, + "cache_count": 0, + "evictions": 0 + }, + "completion": { + "size_in_bytes": 0 + }, + "segments": { + "count": 8, + "memory_in_bytes": 13852, + "terms_memory_in_bytes": 10412, + "stored_fields_memory_in_bytes": 2496, + "term_vectors_memory_in_bytes": 0, + "norms_memory_in_bytes": 256, + "points_memory_in_bytes": 8, + "doc_values_memory_in_bytes": 680, + "index_writer_memory_in_bytes": 0, + "version_map_memory_in_bytes": 0, + "fixed_bit_set_memory_in_bytes": 0, + "max_unsafe_auto_id_timestamp": -1, + "file_sizes": {} + } + }, + "nodes": { + "count": { + "total": 1, + "data": 1, + "coordinating_only": 0, + "master": 1, + "ingest": 1 + }, + "versions": [ + "7.0.0-alpha1" + ], + "os": { + "available_processors": 4, + "allocated_processors": 1, + "names": [ + { + "name": "Mac OS X", + "count": 1 + } + ], + "mem": { + "total_in_bytes": 17179869184, + "free_in_bytes": 183799808, + "used_in_bytes": 16996069376, + "free_percent": 1, + "used_percent": 99 + } + }, + "process": { + "cpu": { + "percent": 0 + }, + "open_file_descriptors": { + "min": 163, + "max": 163, + "avg": 163 + } + }, + "jvm": { + "max_uptime_in_millis": 701043, + "versions": [ + { + "vm_version": "25.121-b13", + "count": 1, + "vm_vendor": "Oracle Corporation", + "version": "1.8.0_121", + "vm_name": "Java HotSpot(TM) 64-Bit Server VM" + } + ], + "mem": { + "heap_used_in_bytes": 204299464, + "heap_max_in_bytes": 628555776 + }, + "threads": 40 + }, + "fs": { + "total_in_bytes": 499065712640, + "free_in_bytes": 200665341952, + "available_in_bytes": 200403197952 + }, + "plugins": [ + { + "classname": "org.elasticsearch.xpack.XPackPlugin", + "name": "x-pack", + "description": "Elasticsearch Expanded Pack Plugin", + "version": "7.0.0-alpha1", + "has_native_controller": true + } + ], + "network_types": { + "transport_types": { + "security4": 1 + }, + "http_types": { + "security4": 1 + } + } + } + }, + "stack_stats": { + "xpack": { + "security": { + "available": false, + "enabled": true, + "realms": { + "file": { + "available": false, + "enabled": false + }, + "ldap": { + "available": false, + "enabled": false + }, + "native": { + "available": false, + "enabled": false + }, + "active_directory": { + "available": false, + "enabled": false + }, + "pki": { + "available": false, + "enabled": false + } + }, + "roles": { + "native": { + "size": 1, + "dls": false, + "fls": false + }, + "file": { + "size": 0, + "dls": false, + "fls": false + } + }, + "role_mapping": { + "native": { + "size": 0, + "enabled": 0 + } + }, + "ssl": { + "http": { + "enabled": false + } + }, + "audit": { + "outputs": [ + "logfile" + ], + "enabled": false + }, + "ipfilter": { + "http": false, + "transport": false + }, + "anonymous": { + "enabled": false + } + }, + "monitoring": { + "available": true, + "enabled": true, + "enabled_exporters": { + "http": 1 + } + }, + "watcher": { + "available": false, + "enabled": true, + "execution": { + "actions": { + "_all": { + "total": 0, + "total_time_in_ms": 0 + } + } + } + }, + "graph": { + "available": false, + "enabled": true + }, + "ml": { + "available": false, + "enabled": true, + "jobs": { + "_all": { + "count": 0, + "detectors": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + } + }, + "datafeeds": { + "_all": { + "count": 0 + } + } + }, + "logstash": { + "available": false, + "enabled": true + }, + "ccr": { + "auto_follow_patterns_count": 0, + "available": true, + "follower_indices_count": 0, + "enabled": true + } + } + } + }, + { + "cluster_uuid": "lOF8kofiS_2DX58o9mXJ1Q", + "collectionSource": "monitoring", + "timestamp": "2017-08-15T22:10:54.610Z", + "cluster_name": "monitoring-one", + "version": "7.0.0-alpha1", + "license": { + "status": "active", + "type": "trial", + "issue_date": "2017-08-15T21:58:28.997Z", + "expiry_date": "2017-09-14T21:58:28.997Z", + "expiry_date_in_millis": 1505426308997, + "issue_date_in_millis": 1502834308997, + "issued_to": "monitoring-one", + "issuer": "elasticsearch", + "max_nodes": 1000, + "start_date_in_millis": -1, + "uid": "e5f6e897-0db5-4042-ad7c-6628ddc91691", + "hkey": null + }, + "cluster_stats": { + "timestamp": 1502835054610, + "status": "yellow", + "indices": { + "count": 8, + "shards": { + "total": 8, + "primaries": 8, + "replication": 0, + "index": { + "shards": { + "min": 1, + "max": 1, + "avg": 1 + }, + "primaries": { + "min": 1, + "max": 1, + "avg": 1 + }, + "replication": { + "min": 0, + "max": 0, + "avg": 0 + } + } + }, + "docs": { + "count": 3997, + "deleted": 69 + }, + "store": { + "size_in_bytes": 2647163 + }, + "fielddata": { + "memory_size_in_bytes": 2104, + "evictions": 0 + }, + "query_cache": { + "memory_size_in_bytes": 0, + "total_count": 0, + "hit_count": 0, + "miss_count": 0, + "cache_size": 0, + "cache_count": 0, + "evictions": 0 + }, + "completion": { + "size_in_bytes": 0 + }, + "segments": { + "count": 36, + "memory_in_bytes": 278961, + "terms_memory_in_bytes": 166031, + "stored_fields_memory_in_bytes": 11544, + "term_vectors_memory_in_bytes": 0, + "norms_memory_in_bytes": 6784, + "points_memory_in_bytes": 3250, + "doc_values_memory_in_bytes": 91352, + "index_writer_memory_in_bytes": 205347, + "version_map_memory_in_bytes": 26362, + "fixed_bit_set_memory_in_bytes": 992, + "max_unsafe_auto_id_timestamp": -1, + "file_sizes": {} + } + }, + "nodes": { + "count": { + "total": 1, + "data": 1, + "coordinating_only": 0, + "master": 1, + "ingest": 1 + }, + "versions": [ + "7.0.0-alpha1" + ], + "os": { + "available_processors": 4, + "allocated_processors": 1, + "names": [ + { + "name": "Mac OS X", + "count": 1 + } + ], + "mem": { + "total_in_bytes": 17179869184, + "free_in_bytes": 86732800, + "used_in_bytes": 17093136384, + "free_percent": 1, + "used_percent": 99 + } + }, + "process": { + "cpu": { + "percent": 2 + }, + "open_file_descriptors": { + "min": 178, + "max": 178, + "avg": 178 + } + }, + "jvm": { + "max_uptime_in_millis": 761002, + "versions": [ + { + "vm_version": "25.121-b13", + "count": 1, + "vm_vendor": "Oracle Corporation", + "version": "1.8.0_121", + "vm_name": "Java HotSpot(TM) 64-Bit Server VM" + } + ], + "mem": { + "heap_used_in_bytes": 133041176, + "heap_max_in_bytes": 628555776 + }, + "threads": 42 + }, + "fs": { + "total_in_bytes": 499065712640, + "free_in_bytes": 200665792512, + "available_in_bytes": 200403648512 + }, + "plugins": [ + { + "classname": "org.elasticsearch.xpack.XPackPlugin", + "name": "x-pack", + "description": "Elasticsearch Expanded Pack Plugin", + "version": "7.0.0-alpha1", + "has_native_controller": true + } + ], + "network_types": { + "transport_types": { + "security4": 1 + }, + "http_types": { + "security4": 1 + } + } + } + }, + "stack_stats": { + "xpack": { + "security": { + "available": true, + "enabled": true, + "realms": { + "file": { + "name": [ + "default_file" + ], + "available": true, + "size": [ + 0 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "ldap": { + "available": true, + "enabled": false + }, + "native": { + "name": [ + "default_native" + ], + "available": true, + "size": [ + 2 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "active_directory": { + "available": true, + "enabled": false + }, + "pki": { + "available": true, + "enabled": false + } + }, + "roles": { + "native": { + "size": 1, + "dls": false, + "fls": false + }, + "file": { + "size": 0, + "dls": false, + "fls": false + } + }, + "role_mapping": { + "native": { + "size": 0, + "enabled": 0 + } + }, + "ssl": { + "http": { + "enabled": false + } + }, + "audit": { + "outputs": [ + "logfile" + ], + "enabled": false + }, + "ipfilter": { + "http": false, + "transport": false + }, + "anonymous": { + "enabled": false + } + }, + "monitoring": { + "available": true, + "enabled": true, + "enabled_exporters": { + "local": 1 + } + }, + "watcher": { + "available": true, + "enabled": true, + "execution": { + "actions": { + "index": { + "total": 14, + "total_time_in_ms": 158 + }, + "_all": { + "total": 110, + "total_time_in_ms": 2245 + }, + "email": { + "total": 14, + "total_time_in_ms": 3 + } + } + } + }, + "graph": { + "available": true, + "enabled": true + }, + "ml": { + "available": true, + "enabled": true, + "jobs": { + "_all": { + "count": 0, + "detectors": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + } + }, + "datafeeds": { + "_all": { + "count": 0 + } + } + }, + "logstash": { + "available": true, + "enabled": true + }, + "ccr": { + "auto_follow_patterns_count": 0, + "available": true, + "follower_indices_count": 0, + "enabled": true + } + } + } + }, + { + "cluster_uuid": "TkHOX_-1TzWwbROwQJU5IA", + "collectionSource": "monitoring", + "timestamp": "2017-08-15T22:10:52.642Z", + "cluster_name": "clusterone", + "version": "7.0.0-alpha1", + "license": { + "status": "active", + "type": "trial", + "issue_date": "2017-08-15T21:58:47.135Z", + "expiry_date": "2017-09-14T21:58:47.135Z", + "expiry_date_in_millis": 1505426327135, + "issue_date_in_millis": 1502834327135, + "issued_to": "clusterone", + "issuer": "elasticsearch", + "max_nodes": 1000, + "start_date_in_millis": -1, + "uid": "e5e99511-0928-41a3-97b0-ec77fa5e3b4c", + "hkey": null + }, + "cluster_stats": { + "timestamp": 1502835052641, + "status": "green", + "indices": { + "count": 5, + "shards": { + "total": 26, + "primaries": 13, + "replication": 1, + "index": { + "shards": { + "min": 2, + "max": 10, + "avg": 5.2 + }, + "primaries": { + "min": 1, + "max": 5, + "avg": 2.6 + }, + "replication": { + "min": 1, + "max": 1, + "avg": 1 + } + } + }, + "docs": { + "count": 150, + "deleted": 0 + }, + "store": { + "size_in_bytes": 4838464 + }, + "fielddata": { + "memory_size_in_bytes": 0, + "evictions": 0 + }, + "query_cache": { + "memory_size_in_bytes": 0, + "total_count": 0, + "hit_count": 0, + "miss_count": 0, + "cache_size": 0, + "cache_count": 0, + "evictions": 0 + }, + "completion": { + "size_in_bytes": 0 + }, + "segments": { + "count": 76, + "memory_in_bytes": 1907922, + "terms_memory_in_bytes": 1595112, + "stored_fields_memory_in_bytes": 23744, + "term_vectors_memory_in_bytes": 0, + "norms_memory_in_bytes": 197184, + "points_memory_in_bytes": 3818, + "doc_values_memory_in_bytes": 88064, + "index_writer_memory_in_bytes": 7006184, + "version_map_memory_in_bytes": 260, + "fixed_bit_set_memory_in_bytes": 0, + "max_unsafe_auto_id_timestamp": 1502834982386, + "file_sizes": {} + } + }, + "nodes": { + "count": { + "total": 2, + "data": 2, + "coordinating_only": 0, + "master": 2, + "ingest": 2 + }, + "versions": [ + "7.0.0-alpha1" + ], + "os": { + "available_processors": 8, + "allocated_processors": 2, + "names": [ + { + "name": "Mac OS X", + "count": 2 + } + ], + "mem": { + "total_in_bytes": 34359738368, + "free_in_bytes": 332099584, + "used_in_bytes": 34027638784, + "free_percent": 1, + "used_percent": 99 + } + }, + "process": { + "cpu": { + "percent": 2 + }, + "open_file_descriptors": { + "min": 218, + "max": 237, + "avg": 227 + } + }, + "jvm": { + "max_uptime_in_millis": 741786, + "versions": [ + { + "vm_version": "25.121-b13", + "count": 2, + "vm_vendor": "Oracle Corporation", + "version": "1.8.0_121", + "vm_name": "Java HotSpot(TM) 64-Bit Server VM" + } + ], + "mem": { + "heap_used_in_bytes": 465621856, + "heap_max_in_bytes": 1257111552 + }, + "threads": 92 + }, + "fs": { + "total_in_bytes": 499065712640, + "free_in_bytes": 200666353664, + "available_in_bytes": 200404209664 + }, + "plugins": [ + { + "classname": "org.elasticsearch.xpack.XPackPlugin", + "name": "x-pack", + "description": "Elasticsearch Expanded Pack Plugin", + "version": "7.0.0-alpha1", + "has_native_controller": true + } + ], + "network_types": { + "transport_types": { + "security4": 2 + }, + "http_types": { + "security4": 2 + } + } + } + }, + "stack_stats": { + "xpack": { + "security": { + "available": true, + "enabled": true, + "realms": { + "file": { + "name": [ + "default_file" + ], + "available": true, + "size": [ + 0 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "ldap": { + "available": true, + "enabled": false + }, + "native": { + "name": [ + "default_native" + ], + "available": true, + "size": [ + 1 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "active_directory": { + "available": true, + "enabled": false + }, + "pki": { + "available": true, + "enabled": false + } + }, + "roles": { + "native": { + "size": 1, + "dls": false, + "fls": false + }, + "file": { + "size": 0, + "dls": false, + "fls": false + } + }, + "role_mapping": { + "native": { + "size": 0, + "enabled": 0 + } + }, + "ssl": { + "http": { + "enabled": false + } + }, + "audit": { + "outputs": [ + "logfile" + ], + "enabled": false + }, + "ipfilter": { + "http": false, + "transport": false + }, + "anonymous": { + "enabled": false + } + }, + "monitoring": { + "available": true, + "enabled": true, + "enabled_exporters": { + "http": 1 + } + }, + "watcher": { + "available": true, + "enabled": true, + "execution": { + "actions": { + "_all": { + "total": 0, + "total_time_in_ms": 0 + } + } + } + }, + "graph": { + "available": true, + "enabled": true, + "graph_workspace": { + "total": 0 + } + }, + "ml": { + "available": true, + "enabled": true, + "jobs": { + "_all": { + "count": 3, + "detectors": { + "total": 3, + "min": 1, + "avg": 1, + "max": 1 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + }, + "opened": { + "count": 1, + "detectors": { + "total": 1, + "min": 1, + "avg": 1, + "max": 1 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + }, + "closed": { + "count": 2, + "detectors": { + "total": 2, + "min": 1, + "avg": 1, + "max": 1 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + } + }, + "datafeeds": { + "_all": { + "count": 0 + } + } + }, + "logstash": { + "available": true, + "enabled": true + }, + "ccr": { + "auto_follow_patterns_count": 0, + "available": true, + "follower_indices_count": 0, + "enabled": true + } + }, + "kibana": { + "count": 1, + "versions": [ + { + "version": "7.0.0-alpha1", + "count": 1 + } + ], + "os": { + "platforms": [], + "platformReleases": [], + "distros": [], + "distroReleases": [] + }, + "dashboard": { + "total": 0 + }, + "visualization": { + "total": 0 + }, + "search": { + "total": 0 + }, + "index_pattern": { + "total": 0 + }, + "graph_workspace": { + "total": 0 + }, + "timelion_sheet": { + "total": 0 + }, + "indices": 1, + "plugins": {} + }, + "logstash": { + "count": 1, + "versions": [ + { + "version": "7.0.0-alpha1", + "count": 1 + } + ], + "os": { + "platforms": [], + "platformReleases": [], + "distros": [], + "distroReleases": [] + } + } + } + } +] From 900a8290fb9b544e958089adc03aeee34786b7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 25 Feb 2020 11:52:37 +0100 Subject: [PATCH 079/113] [Logs UI] Unskip and stabilitize log column configuration tests (#58392) This attempts to make the log column configuration tests more robust to inconsistent DOM and interaction timing. fixes #58059 --- .../apps/infra/logs_source_configuration.ts | 3 +- .../infra_source_configuration_form.ts | 30 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 1dfbe3526ce40..ecad5a40ec42e 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -15,8 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraLogs']); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/58059 - describe.skip('Logs Source Configuration', function() { + describe('Logs Source Configuration', function() { this.tags('smoke'); before(async () => { diff --git a/x-pack/test/functional/services/infra_source_configuration_form.ts b/x-pack/test/functional/services/infra_source_configuration_form.ts index ab61d5232fa1c..dbae6f00f75a2 100644 --- a/x-pack/test/functional/services/infra_source_configuration_form.ts +++ b/x-pack/test/functional/services/infra_source_configuration_form.ts @@ -36,25 +36,37 @@ export function InfraSourceConfigurationFormProvider({ getService }: FtrProvider return await testSubjects.find('~addLogColumnPopover'); }, async addTimestampLogColumn() { - await (await this.getAddLogColumnButton()).click(); + // try to open the popover + const popover = await retry.try(async () => { + await (await this.getAddLogColumnButton()).click(); + return this.getAddLogColumnPopover(); + }); + + // try to select the timestamp field await retry.try(async () => { - await ( - await testSubjects.findDescendant( - '~addTimestampLogColumn', - await this.getAddLogColumnPopover() - ) - ).click(); + await (await testSubjects.findDescendant('~addTimestampLogColumn', popover)).click(); }); + + // wait for timestamp panel to show up + await testSubjects.findDescendant('~systemLogColumnPanel:Timestamp', await this.getForm()); }, async addFieldLogColumn(fieldName: string) { - await (await this.getAddLogColumnButton()).click(); + // try to open the popover + const popover = await retry.try(async () => { + await (await this.getAddLogColumnButton()).click(); + return this.getAddLogColumnPopover(); + }); + + // try to select the given field await retry.try(async () => { - const popover = await this.getAddLogColumnPopover(); await (await testSubjects.findDescendant('~fieldSearchInput', popover)).type(fieldName); await ( await testSubjects.findDescendant(`~addFieldLogColumn:${fieldName}`, popover) ).click(); }); + + // wait for field panel to show up + await testSubjects.findDescendant(`~fieldLogColumnPanel:${fieldName}`, await this.getForm()); }, async getLogColumnPanels(): Promise { return await testSubjects.findAllDescendant('~logColumnPanel', await this.getForm()); From 31cc2015b73e1035acf409373890ffef7d475b60 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 25 Feb 2020 12:18:35 +0000 Subject: [PATCH 080/113] [ML] Use event.timezone instead of beat.timezone in file upload (#58447) This is because beat.timezone was renamed to event.timezone in elastic/beats#9458 The corresponding file structure finder change is elastic/elasticsearch#52720 --- .../components/import_view/importer/importer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js index 710fa49e64167..27899a58beed2 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js @@ -148,17 +148,17 @@ function populateFailures(error, failures, chunkCount) { } } -// The file structure endpoint sets the timezone to be {{ beat.timezone }} +// The file structure endpoint sets the timezone to be {{ event.timezone }} // as that's the variable Filebeat would send the client timezone in. // In this data import function the UI is effectively performing the role of Filebeat, // i.e. doing basic parsing, processing and conversion to JSON before forwarding to the ingest pipeline. // But it's not sending every single field that Filebeat would add, so the ingest pipeline -// cannot look for a beat.timezone variable in each input record. -// Therefore we need to replace {{ beat.timezone }} with the actual browser timezone +// cannot look for a event.timezone variable in each input record. +// Therefore we need to replace {{ event.timezone }} with the actual browser timezone function updatePipelineTimezone(ingestPipeline) { if (ingestPipeline !== undefined && ingestPipeline.processors && ingestPipeline.processors) { const dateProcessor = ingestPipeline.processors.find( - p => p.date !== undefined && p.date.timezone === '{{ beat.timezone }}' + p => p.date !== undefined && p.date.timezone === '{{ event.timezone }}' ); if (dateProcessor) { From 6fc6cb8b363b4a1ea7e07ce8d491874ca4fbb864 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 25 Feb 2020 13:22:29 +0100 Subject: [PATCH 081/113] [ML] Functional tests - adjust classification model memory (#58445) This PR increases the model memory setting in the classification creation test. --- .../data_frame_analytics/classification_creation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts index 798a04cae3740..1bcdeef394c00 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts @@ -36,7 +36,7 @@ export default function({ getService }: FtrProviderContext) { }, dependentVariable: 'y', trainingPercent: '20', - modelMemory: '105mb', + modelMemory: '200mb', createIndexPattern: true, expected: { row: { From db276d18945246421068e480c7dd5322487cb276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Tue, 25 Feb 2020 13:39:13 +0100 Subject: [PATCH 082/113] =?UTF-8?q?[Logs=20/=20Metrics=20UI]=20Remove=20pa?= =?UTF-8?q?th=20prefix=20from=20ViewSourceConfigur=E2=80=A6=20(#58238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the NP the routes have already the app name on them. Adding it to this button made the app name double /app/metrics/metrics/settings /app/logs/logs/settings The button only needs to go to the settings page within the app where it's being loaded, so we can safely drop the prefix. Closes #58233 --- .../public/components/source_configuration/index.ts | 5 +---- .../view_source_configuration_button.tsx | 9 +-------- .../public/pages/infrastructure/snapshot/index.tsx | 10 ++-------- .../pages/logs/stream/page_no_indices_content.tsx | 10 ++-------- .../public/pages/metrics/components/invalid_node.tsx | 10 ++-------- 5 files changed, 8 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/infra/public/components/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts index 4879a53ca329d..98825567cc204 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/index.ts @@ -5,7 +5,4 @@ */ export { SourceConfigurationSettings } from './source_configuration_settings'; -export { - ViewSourceConfigurationButton, - ViewSourceConfigurationButtonHrefBase, -} from './view_source_configuration_button'; +export { ViewSourceConfigurationButton } from './view_source_configuration_button'; diff --git a/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx b/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx index 9b584b2ef3bd0..9c3a40fb7ecf0 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx @@ -8,23 +8,16 @@ import { EuiButton } from '@elastic/eui'; import React from 'react'; import { Route } from 'react-router-dom'; -export enum ViewSourceConfigurationButtonHrefBase { - infrastructure = 'infrastructure', - logs = 'logs', -} - interface ViewSourceConfigurationButtonProps { 'data-test-subj'?: string; - hrefBase: ViewSourceConfigurationButtonHrefBase; children: React.ReactNode; } export const ViewSourceConfigurationButton = ({ 'data-test-subj': dataTestSubj, - hrefBase, children, }: ViewSourceConfigurationButtonProps) => { - const href = `/${hrefBase}/settings`; + const href = '/settings'; return ( {
{uiCapabilities?.infrastructure?.configureSource ? ( - + {i18n.translate('xpack.infra.configureSourceActionLabel', { defaultMessage: 'Change source configuration', })} diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index 1294007240027..739bad5689a96 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -10,10 +10,7 @@ import { identity } from 'fp-ts/lib/function'; import React from 'react'; import { NoIndices } from '../../../components/empty_states/no_indices'; -import { - ViewSourceConfigurationButton, - ViewSourceConfigurationButtonHrefBase, -} from '../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const LogsPageNoIndicesContent = () => { @@ -49,10 +46,7 @@ export const LogsPageNoIndicesContent = () => { {canConfigureSource ? ( - + {i18n.translate('xpack.infra.configureSourceActionLabel', { defaultMessage: 'Change source configuration', })} diff --git a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx index fde3b61de50b5..43f684cd5a585 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx @@ -10,10 +10,7 @@ import { identity } from 'fp-ts/lib/function'; import React from 'react'; import { euiStyled } from '../../../../../observability/public'; -import { - ViewSourceConfigurationButton, - ViewSourceConfigurationButtonHrefBase, -} from '../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface InvalidNodeErrorProps { @@ -59,10 +56,7 @@ export const InvalidNodeError: React.FunctionComponent = - + Date: Tue, 25 Feb 2020 07:18:04 -0600 Subject: [PATCH 083/113] [Uptime] Improve refresh handling when generating test data (#58285) When generating test data we refresh excessively, this can fill up the ES queues and break the tests if we run massive tests. I originally ran into this with https://github.com/elastic/kibana/pull/58078/ which I closed due to finding a better approach. While none of our current tests have the scale to expose this problem, we certainly will add tests that do later, so we should keep this change. Co-authored-by: Elastic Machine --- .../uptime/graphql/helpers/make_checks.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts index f89905f0da04f..4d3167b14b86f 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts @@ -13,7 +13,8 @@ export const makePing = async ( es: any, monitorId: string, fields: { [key: string]: any }, - mogrify: (doc: any) => any + mogrify: (doc: any) => any, + refresh: boolean = true ) => { const baseDoc = { tcp: { @@ -103,7 +104,7 @@ export const makePing = async ( await es.index({ index: INDEX_NAME, - refresh: true, + refresh, body: doc, }); @@ -115,7 +116,8 @@ export const makeCheck = async ( monitorId: string, numIps: number, fields: { [key: string]: any }, - mogrify: (doc: any) => any + mogrify: (doc: any) => any, + refresh: boolean = true ) => { const cgFields = { monitor: { @@ -137,11 +139,16 @@ export const makeCheck = async ( if (i === numIps - 1) { pingFields.summary = summary; } - const doc = await makePing(es, monitorId, pingFields, mogrify); + const doc = await makePing(es, monitorId, pingFields, mogrify, false); docs.push(doc); // @ts-ignore summary[doc.monitor.status]++; } + + if (refresh) { + es.indices.refresh(); + } + return docs; }; @@ -152,7 +159,8 @@ export const makeChecks = async ( numIps: number, every: number, // number of millis between checks fields: { [key: string]: any } = {}, - mogrify: (doc: any) => any = d => d + mogrify: (doc: any) => any = d => d, + refresh: boolean = true ) => { const checks = []; const oldestTime = new Date().getTime() - numChecks * every; @@ -169,7 +177,11 @@ export const makeChecks = async ( }, }, }); - checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify)); + checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify, false)); + } + + if (refresh) { + es.indices.refresh(); } return checks; @@ -183,19 +195,29 @@ export const makeChecksWithStatus = async ( every: number, fields: { [key: string]: any } = {}, status: 'up' | 'down', - mogrify: (doc: any) => any = d => d + mogrify: (doc: any) => any = d => d, + refresh: boolean = true ) => { const oppositeStatus = status === 'up' ? 'down' : 'up'; - return await makeChecks(es, monitorId, numChecks, numIps, every, fields, d => { - d.monitor.status = status; - if (d.summary) { - d.summary[status] += d.summary[oppositeStatus]; - d.summary[oppositeStatus] = 0; - } - - return mogrify(d); - }); + return await makeChecks( + es, + monitorId, + numChecks, + numIps, + every, + fields, + d => { + d.monitor.status = status; + if (d.summary) { + d.summary[status] += d.summary[oppositeStatus]; + d.summary[oppositeStatus] = 0; + } + + return mogrify(d); + }, + refresh + ); }; // Helper for processing a list of checks to find the time picker bounds. From b756cc3915fe62d405f006fd32bf8678f4c5e402 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 25 Feb 2020 13:39:50 +0000 Subject: [PATCH 084/113] [ML] Adding filebeat config to file dataviz (#58152) * [ML] Adding filebeat config to file dataviz * adding extra help text * removing commented out code * adding extra blank line to processors section * cleaning up types * moving hosts line out of function * typo in config text * updating config based on review * tiny refactor * translating paths text --- .../ml/common/types/file_datavisualizer.ts | 31 +++ .../plugins/ml/public/application/app.tsx | 4 + .../contexts/kibana/kibana_context.ts | 2 + .../filebeat_config_flyout/filebeat_config.ts | 71 +++++++ .../filebeat_config_flyout.tsx | 162 +++++++++++++++ .../filebeat_config_flyout/index.ts | 7 + .../components/import_view/import_view.js | 24 +++ .../import_view/importer/message_importer.js | 5 + .../results_links/{index.js => index.ts} | 0 .../components/results_links/results_links.js | 182 ----------------- .../results_links/results_links.tsx | 190 ++++++++++++++++++ .../application/util/dependency_cache.ts | 10 + x-pack/legacy/plugins/ml/public/legacy.ts | 4 +- x-pack/legacy/plugins/ml/public/plugin.ts | 3 +- .../file_data_visualizer.ts | 27 +-- 15 files changed, 513 insertions(+), 209 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts rename x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/{index.js => index.ts} (100%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js create mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx diff --git a/x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts new file mode 100644 index 0000000000000..bc03f82673a1f --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface FindFileStructureResponse { + charset: string; + has_header_row: boolean; + has_byte_order_marker: boolean; + format: string; + field_stats: { + [fieldName: string]: { + count: number; + cardinality: number; + top_hits: Array<{ count: number; value: any }>; + }; + }; + sample_start: string; + num_messages_analyzed: number; + mappings: { + [fieldName: string]: { + type: string; + }; + }; + quote: string; + delimiter: string; + need_client_timezone: boolean; + num_lines_analyzed: number; + column_names: string[]; +} diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx index 24cbfbfb346dd..add27193deb77 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.tsx +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -12,6 +12,7 @@ import 'ace'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { SecurityPluginSetup } from '../../../../../plugins/security/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { setDependencyCache, clearCache } from './util/dependency_cache'; @@ -20,6 +21,7 @@ import { MlRouter } from './routing'; export interface MlDependencies extends AppMountParameters { data: DataPublicPluginStart; + security: SecurityPluginSetup; __LEGACY: { XSRF: string; APP_URL: string; @@ -49,6 +51,7 @@ const App: FC = ({ coreStart, deps }) => { APP_URL: deps.__LEGACY.APP_URL, application: coreStart.application, http: coreStart.http, + security: deps.security, }); deps.onAppLeave(actions => { clearCache(); @@ -64,6 +67,7 @@ const App: FC = ({ coreStart, deps }) => { const services = { appName: 'ML', data: deps.data, + security: deps.security, ...coreStart, }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts index aaf539322809b..5fcd7c5473d3b 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -10,9 +10,11 @@ import { useKibana, KibanaReactContextValue, } from '../../../../../../../../src/plugins/kibana_react/public'; +import { SecurityPluginSetup } from '../../../../../../../plugins/security/public'; interface StartPlugins { data: DataPublicPluginStart; + security: SecurityPluginSetup; } export type StartServices = CoreStart & StartPlugins; // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts new file mode 100644 index 0000000000000..3344cdf991e6b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts @@ -0,0 +1,71 @@ +/* + * 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 { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; + +export function createFilebeatConfig( + index: string, + results: FindFileStructureResponse, + ingestPipelineId: string, + username: string | null +) { + return [ + 'filebeat.inputs:', + '- type: log', + ...getPaths(), + ...getEncoding(results), + ...getExcludeLines(results), + ...getMultiline(results), + '', + ...getProcessors(results), + 'output.elasticsearch:', + ' hosts: [""]', + ...getUserDetails(username), + ` index: "${index}"`, + ` pipeline: "${ingestPipelineId}"`, + '', + 'setup:', + ' template.enabled: false', + ' ilm.enabled: false', + ].join('\n'); +} + +function getPaths() { + const txt = i18n.translate('xpack.ml.fileDatavisualizer.fileBeatConfig.paths', { + defaultMessage: 'add path to your files here', + }); + return [' paths:', ` - '<${txt}>'`]; +} + +function getEncoding(results: any) { + return results.charset !== 'UTF-8' ? [` encoding: ${results.charset}`] : []; +} + +function getExcludeLines(results: any) { + return results.exclude_lines_pattern !== undefined + ? [` exclude_lines: ['${results.exclude_lines_pattern.replace(/'/g, "''")}']`] + : []; +} + +function getMultiline(results: any) { + return results.multiline_start_pattern !== undefined + ? [ + ' multiline:', + ` pattern: '${results.multiline_start_pattern.replace(/'/g, "''")}'`, + ' match: after', + ' negate: true', + ] + : []; +} + +function getProcessors(results: any) { + return results.need_client_timezone === true ? ['processors:', '- add_locale: ~', ''] : []; +} + +function getUserDetails(username: string | null) { + return username !== null ? [` username: "${username}"`, ' password: ""'] : []; +} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx new file mode 100644 index 0000000000000..30fc74acbabf4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx @@ -0,0 +1,162 @@ +/* + * 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. + */ +/* + * 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, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiCodeBlock, + EuiCode, + EuiCopy, +} from '@elastic/eui'; +import { createFilebeatConfig } from './filebeat_config'; +import { useMlKibana } from '../../../../contexts/kibana'; +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; + +export enum EDITOR_MODE { + HIDDEN, + READONLY, + EDITABLE, +} +interface Props { + index: string; + results: FindFileStructureResponse; + indexPatternId: string; + ingestPipelineId: string; + closeFlyout(): void; +} +export const FilebeatConfigFlyout: FC = ({ + index, + results, + indexPatternId, + ingestPipelineId, + closeFlyout, +}) => { + const [fileBeatConfig, setFileBeatConfig] = useState(''); + const [username, setUsername] = useState(null); + const { + services: { security }, + } = useMlKibana(); + + useEffect(() => { + security.authc.getCurrentUser().then(user => { + setUsername(user.username === undefined ? null : user.username); + }); + }, []); + + useEffect(() => { + const config = createFilebeatConfig(index, results, ingestPipelineId, username); + setFileBeatConfig(config); + }, [username]); + + return ( + + + + + + + + + + + + + + + + {copy => ( + + + + )} + + + + + + ); +}; + +const Contents: FC<{ + value: string; + index: string; + username: string | null; +}> = ({ value, index, username }) => { + return ( + + +
+ +
+
+ +

+ {index} }} + /> +

+

+ filebeat.yml }} + /> +

+ + + + {value} + + +

+ {username === null ? ( + {''}, + }} + /> + ) : ( + {username}, + password: {''}, + esUrl: {''}, + }} + /> + )} +

+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts new file mode 100644 index 0000000000000..9286b92c2ab97 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { FilebeatConfigFlyout } from './filebeat_config_flyout'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index beb5918e277ae..bdfc27099a185 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { importerFactory } from './importer'; import { ResultsLinks } from '../results_links'; +import { FilebeatConfigFlyout } from '../filebeat_config_flyout'; import { ImportProgress, IMPORT_STATUS } from '../import_progress'; import { ImportErrors } from '../import_errors'; import { ImportSummary } from '../import_summary'; @@ -64,6 +65,7 @@ const DEFAULT_STATE = { indexNameError: '', indexPatternNameError: '', timeFieldName: undefined, + isFilebeatFlyoutVisible: false, }; export class ImportView extends Component { @@ -384,6 +386,16 @@ export class ImportView extends Component { }); }; + showFilebeatFlyout = () => { + this.setState({ isFilebeatFlyoutVisible: true }); + this.props.hideBottomBar(); + }; + + closeFilebeatFlyout = () => { + this.setState({ isFilebeatFlyoutVisible: false }); + this.props.showBottomBar(); + }; + async loadIndexNames() { const indices = await ml.getIndices(); const indexNames = indices.map(i => i.name); @@ -424,6 +436,7 @@ export class ImportView extends Component { indexNameError, indexPatternNameError, timeFieldName, + isFilebeatFlyoutVisible, } = this.state; const createPipeline = pipelineString !== ''; @@ -549,7 +562,18 @@ export class ImportView extends Component { indexPatternId={indexPatternId} timeFieldName={timeFieldName} createIndexPattern={createIndexPattern} + showFilebeatFlyout={this.showFilebeatFlyout} /> + + {isFilebeatFlyoutVisible && ( + + )} )} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js index 840248817945a..c2d3ac69f0963 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js @@ -77,6 +77,11 @@ export class MessageImporter extends Importer { if (this.multilineStartRegex === null || line.match(this.multilineStartRegex) !== null) { this.addMessage(data, message); message = ''; + } else if (data.length === 0) { + // discard everything before the first line that is considered the first line of a message + // as it could be left over partial data from a spilt or rolled over log, + // or could be a blank line after the header in a csv file + return ''; } else { message += '\n'; } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js deleted file mode 100644 index aaebca2f58963..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js +++ /dev/null @@ -1,182 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React, { Component } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; - -import moment from 'moment'; - -import { ml } from '../../../../services/ml_api_service'; -import { isFullLicense } from '../../../../license/check_license'; -import { checkPermission } from '../../../../privilege/check_privilege'; -import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; -import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; - -const RECHECK_DELAY_MS = 3000; - -class ResultsLinksUI extends Component { - constructor(props) { - super(props); - - this.state = { - from: 'now-30m', - to: 'now', - }; - - this.recheckTimeout = null; - this.showCreateJobLink = true; - } - - componentDidMount() { - this.showCreateJobLink = checkPermission('canCreateJob') && mlNodesAvailable(); - // if this data has a time field, - // find the start and end times - if (this.props.timeFieldName !== undefined) { - this.updateTimeValues(); - } - } - - componentWillUnmount() { - clearTimeout(this.recheckTimeout); - } - - async updateTimeValues(recheck = true) { - const { index, timeFieldName } = this.props; - - const { from, to } = await getFullTimeRange(index, timeFieldName); - this.setState({ - from: from === null ? this.state.from : from, - to: to === null ? this.state.to : to, - }); - - // these links may have been drawn too quickly for the index to be ready - // to give us the correct start and end times. - // especially if the data was small. - // so if the start and end were null, try again in 3s - // the timeout is cleared when this component unmounts. just in case the user - // resets the form or navigates away within 3s - if (recheck && (from === null || to === null)) { - this.recheckTimeout = setTimeout(() => { - this.updateTimeValues(false); - }, RECHECK_DELAY_MS); - } - } - - render() { - const { index, indexPatternId, timeFieldName, createIndexPattern } = this.props; - - const { from, to } = this.state; - - const _g = - this.props.timeFieldName !== undefined - ? `&_g=(time:(from:'${from}',mode:quick,to:'${to}'))` - : ''; - - const { basePath } = this.props.kibana.services.http; - return ( - - {createIndexPattern && ( - - } - title={ - - } - description="" - href={`${basePath.get()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`} - /> - - )} - - {isFullLicense() === true && - timeFieldName !== undefined && - this.showCreateJobLink && - createIndexPattern && ( - - } - title={ - - } - description="" - href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`} - /> - - )} - - {createIndexPattern && ( - - } - title={ - - } - description="" - href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`} - /> - - )} - - - } - title={ - - } - description="" - href={`${basePath.get()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} - /> - - - - } - title={ - - } - description="" - href={`${basePath.get()}/app/kibana#/management/kibana/index_patterns/${ - createIndexPattern ? indexPatternId : '' - }`} - /> - - - ); - } -} - -export const ResultsLinks = withKibana(ResultsLinksUI); - -async function getFullTimeRange(index, timeFieldName) { - const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } }; - const resp = await ml.getTimeFieldRange({ - index, - timeFieldName, - query, - }); - - return { - from: moment(resp.start.epoch).toISOString(), - to: moment(resp.end.epoch).toISOString(), - }; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx new file mode 100644 index 0000000000000..debadba19051b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -0,0 +1,190 @@ +/* + * 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, useEffect } from 'react'; +import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; +import { ml } from '../../../../services/ml_api_service'; +import { isFullLicense } from '../../../../license/check_license'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; +import { useMlKibana } from '../../../../contexts/kibana'; + +const RECHECK_DELAY_MS = 3000; + +interface Props { + index: string; + indexPatternId: string; + timeFieldName?: string; + createIndexPattern: boolean; + showFilebeatFlyout(): void; +} + +export const ResultsLinks: FC = ({ + index, + indexPatternId, + timeFieldName, + createIndexPattern, + showFilebeatFlyout, +}) => { + const [duration, setDuration] = useState({ + from: 'now-30m', + to: 'now', + }); + const [showCreateJobLink, setShowCreateJobLink] = useState(false); + const [globalStateString, setGlobalStateString] = useState(''); + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); + + useEffect(() => { + setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable()); + updateTimeValues(); + }, []); + + useEffect(() => { + const _g = + timeFieldName !== undefined + ? `&_g=(time:(from:'${duration.from}',mode:quick,to:'${duration.to}'))` + : ''; + setGlobalStateString(_g); + }, [duration]); + + async function updateTimeValues(recheck = true) { + if (timeFieldName !== undefined) { + const { from, to } = await getFullTimeRange(index, timeFieldName); + setDuration({ + from: from === null ? duration.from : from, + to: to === null ? duration.to : to, + }); + + // these links may have been drawn too quickly for the index to be ready + // to give us the correct start and end times. + // especially if the data was small. + // so if the start and end were null, try again in 3s + if (recheck && (from === null || to === null)) { + setTimeout(() => { + updateTimeValues(false); + }, RECHECK_DELAY_MS); + } + } + } + + return ( + + {createIndexPattern && ( + + } + title={ + + } + description="" + href={`${basePath.get()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${globalStateString}`} + /> + + )} + + {isFullLicense() === true && + timeFieldName !== undefined && + showCreateJobLink && + createIndexPattern && ( + + } + title={ + + } + description="" + href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${globalStateString}`} + /> + + )} + + {createIndexPattern && ( + + } + title={ + + } + description="" + href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${globalStateString}`} + /> + + )} + + + } + title={ + + } + description="" + href={`${basePath.get()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} + /> + + + + } + title={ + + } + description="" + href={`${basePath.get()}/app/kibana#/management/kibana/index_patterns/${ + createIndexPattern ? indexPatternId : '' + }`} + /> + + + } + title={ + + } + description="" + onClick={showFilebeatFlyout} + /> + + + ); +}; + +async function getFullTimeRange(index: string, timeFieldName: string) { + const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } }; + const resp = await ml.getTimeFieldRange({ + index, + timeFieldName, + query, + }); + + return { + from: moment(resp.start.epoch).toISOString(), + to: moment(resp.end.epoch).toISOString(), + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts index 8857485a58644..f837d90dba8fe 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -20,6 +20,7 @@ import { ChromeRecentlyAccessed, IBasePath, } from 'kibana/public'; +import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; export interface DependencyCache { timefilter: TimefilterSetup | null; @@ -38,6 +39,7 @@ export interface DependencyCache { APP_URL: string | null; application: ApplicationStart | null; http: HttpStart | null; + security: SecurityPluginSetup | null; } const cache: DependencyCache = { @@ -57,6 +59,7 @@ const cache: DependencyCache = { APP_URL: null, application: null, http: null, + security: null, }; export function setDependencyCache(deps: Partial) { @@ -189,6 +192,13 @@ export function getHttp() { return cache.http; } +export function getSecurity() { + if (cache.security === null) { + throw new Error("security hasn't been initialized"); + } + return cache.security; +} + export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index bf431f0986d68..40a1afa06b5a6 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -6,14 +6,16 @@ import chrome from 'ui/chrome'; import { npSetup, npStart } from 'ui/new_platform'; - import { PluginInitializerContext } from 'src/core/public'; +import { SecurityPluginSetup } from '../../../../plugins/security/public'; + import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, { data: npStart.plugins.data, + security: ((npSetup.plugins as unknown) as { security: SecurityPluginSetup }).security, // security isn't in the PluginsSetup interface, but does exist __LEGACY: { XSRF: chrome.getXsrfToken(), // @ts-ignore getAppUrl is missing from chrome's definition diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index 79af300bce4ec..cb39b31a32b14 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -8,7 +8,7 @@ import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; import { MlDependencies } from './application/app'; export class MlPlugin implements Plugin { - setup(core: CoreSetup, { data, __LEGACY }: MlDependencies) { + setup(core: CoreSetup, { data, security, __LEGACY }: MlDependencies) { core.application.register({ id: 'ml', title: 'Machine learning', @@ -21,6 +21,7 @@ export class MlPlugin implements Plugin { onAppLeave: params.onAppLeave, data, __LEGACY, + security, }); }, }); diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index fd5b5221393fc..9f30f609c60b6 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'kibana/server'; +import { FindFileStructureResponse } from '../../../common/types/file_datavisualizer'; export type InputData = any[]; @@ -20,31 +21,7 @@ export type FormattedOverrides = InputOverrides & { }; export interface AnalysisResult { - results: { - charset: string; - has_header_row: boolean; - has_byte_order_marker: boolean; - format: string; - field_stats: { - [fieldName: string]: { - count: number; - cardinality: number; - top_hits: Array<{ count: number; value: any }>; - }; - }; - sample_start: string; - num_messages_analyzed: number; - mappings: { - [fieldName: string]: { - type: string; - }; - }; - quote: string; - delimiter: string; - need_client_timezone: boolean; - num_lines_analyzed: number; - column_names: string[]; - }; + results: FindFileStructureResponse; overrides?: FormattedOverrides; } From afb5d8fa5847cdb7e4f7d6d8742a73486815007f Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 25 Feb 2020 07:43:46 -0600 Subject: [PATCH 085/113] Fix service map popover transaction duration (#58422) It's already microseconds, so not converting it fixes it. Checked services to see if all metrics match now and they do! Fixes #55679 --- .../app/ServiceMap/Popover/ServiceMetricList.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 50ce918ea7037..3a6b4c5ebcaac 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -16,12 +16,7 @@ import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; -import { - asDuration, - asPercent, - toMicroseconds, - tpmUnit -} from '../../../../utils/formatters'; +import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; function LoadingSpinner() { return ( @@ -70,7 +65,7 @@ export function ServiceMetricList({ } ), description: isNumber(avgTransactionDuration) - ? asDuration(toMicroseconds(avgTransactionDuration, 'milliseconds')) + ? asDuration(avgTransactionDuration) : null }, { From db05fb6738cd906bf231a28d58d5c17d6efe201c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 25 Feb 2020 15:00:28 +0100 Subject: [PATCH 086/113] Don't mutate error message (#58452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 don't mutate error object in-place This avoids mutating error thrown by an expression function in-place. The error might not even be an object, in which case mutating it will throw. * test: 💍 capture in test that thrown error is not mutated Co-authored-by: Elastic Machine --- src/plugins/expressions/common/execution/execution.test.ts | 1 + src/plugins/expressions/common/execution/execution.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index f6ff9efca848b..4776204a8ab2f 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -630,6 +630,7 @@ describe('Execution', () => { }, }); expect(node2.debug?.rawError).toBeInstanceOf(Error); + expect(node2.debug?.rawError).toEqual(new Error('foo')); }); test('sets .debug object to expected shape', async () => { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 7e7df822724ae..272448870e817 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -230,8 +230,8 @@ export class Execution< input = output; } catch (rawError) { const timeEnd: number = this.params.debug ? performance.now() : 0; - rawError.message = `[${fnName}] > ${rawError.message}`; const error = createError(rawError) as ExpressionValueError; + error.error.message = `[${fnName}] > ${error.error.message}`; if (this.params.debug) { (link as ExpressionAstFunction).debug = { From 418c44a47ed7c5b6807e31aa4be24f4dbc9d6b5b Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 25 Feb 2020 15:00:50 +0100 Subject: [PATCH 087/113] [SIEM] Fix unnecessary re-renders on the Overview page (#56587) --- .../__snapshots__/barchart.test.tsx.snap | 2 - .../components/charts/areachart.test.tsx | 4 +- .../public/components/charts/areachart.tsx | 4 - .../components/charts/barchart.test.tsx | 11 +- .../siem/public/components/charts/common.tsx | 11 +- .../drag_and_drop/draggable_wrapper.tsx | 4 +- .../events_viewer/events_viewer.tsx | 10 +- .../public/components/events_viewer/index.tsx | 18 +- .../siem/public/components/flyout/index.tsx | 55 +++--- .../siem/public/components/inspect/index.tsx | 8 +- .../public/components/navigation/index.tsx | 7 +- .../hosts/authentications_table/index.tsx | 6 +- .../page/hosts/hosts_table/index.tsx | 1 + .../components/page/hosts/kpi_hosts/index.tsx | 1 + .../hosts/uncommon_process_table/index.tsx | 6 +- .../page/network/kpi_network/index.tsx | 1 + .../network/network_dns_table/columns.tsx | 3 +- .../page/network/network_dns_table/index.tsx | 10 +- .../page/network/network_http_table/index.tsx | 6 +- .../network_top_countries_table/index.tsx | 5 +- .../network_top_n_flow_table/index.tsx | 5 +- .../page/network/tls_table/index.tsx | 10 +- .../page/network/users_table/index.tsx | 13 +- .../overview/overview_host_stats/index.tsx | 2 - .../overview/overview_network_stats/index.tsx | 6 +- .../public/components/query_bar/index.tsx | 6 +- .../public/components/search_bar/index.tsx | 6 +- .../source_destination_ip.tsx | 5 +- .../__snapshots__/index.test.tsx.snap | 3 - .../__snapshots__/timeline.test.tsx.snap | 2 +- .../components/timeline/header/index.tsx | 8 +- .../siem/public/components/timeline/index.tsx | 12 +- .../components/timeline/query_bar/index.tsx | 5 +- .../timeline/search_or_filter/index.tsx | 21 ++- .../public/components/timeline/timeline.tsx | 4 - .../public/components/url_state/index.tsx | 4 +- .../components/url_state/use_url_state.tsx | 7 +- .../rules/fetch_index_patterns.tsx | 5 +- .../public/containers/global_time/index.tsx | 18 +- .../containers/query_template_paginated.tsx | 4 +- .../siem/public/containers/source/index.tsx | 1 + .../signals_histogram.tsx | 29 +-- .../rules/components/query_bar/index.tsx | 12 +- .../rules/components/rule_status/index.tsx | 4 +- .../components/step_about_rule/index.tsx | 4 +- .../components/step_define_rule/index.tsx | 13 +- .../components/step_schedule_rule/index.tsx | 4 +- .../pages/hosts/details/details_tabs.tsx | 45 ++--- .../siem/public/pages/hosts/hosts_tabs.tsx | 75 ++++---- .../network/navigation/network_routes.tsx | 171 +++++++++--------- x-pack/legacy/plugins/siem/public/routes.tsx | 8 +- .../siem/public/utils/kql/use_update_kql.tsx | 4 +- .../siem/public/utils/route/spy_routes.tsx | 3 +- x-pack/package.json | 1 + x-pack/tsconfig.json | 2 +- yarn.lock | 9 +- 56 files changed, 374 insertions(+), 330 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap index 12b9afb661da1..c330676e9219e 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap @@ -1,5 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BarChartBaseComponent render with customized configs should 2 render BarSeries 1`] = `[Function]`; - exports[`BarChartBaseComponent render with default configs if no customized configs given should 2 render BarSeries 1`] = `[Function]`; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx index 27f0222b96b77..3c2de28ae423c 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx @@ -331,7 +331,7 @@ describe('AreaChart', () => { }); it(`should render area chart`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(1); + expect(shallowWrapper.find('AreaChartBase')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); }); @@ -344,7 +344,7 @@ describe('AreaChart', () => { }); it(`should render a chart place holder`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(0); + expect(shallowWrapper.find('AreaChartBase')).toHaveLength(0); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1); }); } diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index b66cc77e30aad..f3b2b736ed87d 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -146,8 +146,4 @@ export const AreaChartComponent: React.FC = ({ areaChar ); }; -AreaChartComponent.displayName = 'AreaChartComponent'; - export const AreaChart = React.memo(AreaChartComponent); - -AreaChart.displayName = 'AreaChart'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index 0b6635b04d380..272c41833f368 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; -import { BarSeries, ScaleType, Axis } from '@elastic/charts'; +import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; jest.mock('../../lib/kibana'); @@ -139,7 +139,7 @@ describe('BarChartBaseComponent', () => { }); it('should render two bar series', () => { - expect(shallowWrapper.find('Chart')).toHaveLength(1); + expect(shallowWrapper.find(Chart)).toHaveLength(1); }); }); @@ -167,7 +167,6 @@ describe('BarChartBaseComponent', () => { }); it(`should ${mockBarChartData.length} render BarSeries`, () => { - expect(shallow).toMatchSnapshot(); expect(shallowWrapper.find(BarSeries)).toHaveLength(mockBarChartData.length); }); @@ -265,7 +264,7 @@ describe('BarChartBaseComponent', () => { }); it('should not render without height and width', () => { - expect(shallowWrapper.find('Chart')).toHaveLength(0); + expect(shallowWrapper.find(Chart)).toHaveLength(0); }); }); }); @@ -278,7 +277,7 @@ describe.each(chartDataSets)('BarChart with valid data [%o]', data => { }); it(`should render chart`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(1); + expect(shallowWrapper.find('BarChartBase')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); }); @@ -291,7 +290,7 @@ describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => { }); it(`should render a ChartPlaceHolder`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(0); + expect(shallowWrapper.find('BarChartBase')).toHaveLength(0); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index 03b412f575646..7377bcbe7050f 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -16,7 +16,9 @@ import { TickFormatter, Position, } from '@elastic/charts'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; + import { useUiSetting } from '../../lib/kibana'; import { DEFAULT_DARK_MODE } from '../../../common/constants'; @@ -54,7 +56,7 @@ export interface ChartSeriesData { color?: string | undefined; } -export const WrappedByAutoSizer = styled.div<{ height?: string }>` +const WrappedByAutoSizerComponent = styled.div<{ height?: string }>` ${style => ` height: ${style.height != null ? style.height : defaultChartHeight}; @@ -66,7 +68,9 @@ export const WrappedByAutoSizer = styled.div<{ height?: string }>` } `; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +WrappedByAutoSizerComponent.displayName = 'WrappedByAutoSizer'; + +export const WrappedByAutoSizer = React.memo(WrappedByAutoSizerComponent); export enum SeriesType { BAR = 'bar', @@ -96,8 +100,9 @@ const theme: PartialTheme = { export const useTheme = () => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const defaultTheme = isDarkMode ? DARK_THEME : LIGHT_THEME; + const themeValue = useMemo(() => mergeWithDefaultTheme(theme, defaultTheme), []); - return mergeWithDefaultTheme(theme, defaultTheme); + return themeValue; }; export const chartDefaultSettings = { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index cf958bfd75d3b..7d84403b87f8d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { createContext, useContext, useEffect } from 'react'; import { Draggable, @@ -14,6 +13,7 @@ import { } from 'react-beautiful-dnd'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { EuiPortal } from '@elastic/eui'; import { dragAndDropActions } from '../../store/drag_and_drop'; @@ -122,7 +122,7 @@ const DraggableWrapperComponent = React.memo( }, (prevProps, nextProps) => { return ( - isEqual(prevProps.dataProvider, nextProps.dataProvider) && + deepEqual(prevProps.dataProvider, nextProps.dataProvider) && prevProps.render !== nextProps.render && prevProps.truncate === nextProps.truncate ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index 2a4d08ea214bc..a913186d9ad3b 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -5,10 +5,10 @@ */ import { EuiPanel } from '@elastic/eui'; -import deepEqual from 'fast-deep-equal'; -import { getOr, isEmpty, isEqual, union } from 'lodash/fp'; +import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { BrowserFields } from '../../containers/source'; @@ -228,7 +228,7 @@ const EventsViewerComponent: React.FC = ({ export const EventsViewer = React.memo( EventsViewerComponent, (prevProps, nextProps) => - isEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && prevProps.dataProviders === nextProps.dataProviders && prevProps.deletedEventIds === nextProps.deletedEventIds && @@ -241,9 +241,9 @@ export const EventsViewer = React.memo( prevProps.itemsPerPage === nextProps.itemsPerPage && prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && prevProps.kqlMode === nextProps.kqlMode && - isEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.query, nextProps.query) && prevProps.start === nextProps.start && prevProps.sort === nextProps.sort && - isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && prevProps.utilityBar === nextProps.utilityBar ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 762ae8497dadb..9b31be40dd955 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { inputsActions, timelineActions } from '../../store/actions'; @@ -197,23 +197,23 @@ export const StatefulEventsViewer = connector( StatefulEventsViewerComponent, (prevProps, nextProps) => prevProps.id === nextProps.id && - isEqual(prevProps.columns, nextProps.columns) && - isEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && - isEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filters, nextProps.filters) && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && - isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && prevProps.kqlMode === nextProps.kqlMode && - isEqual(prevProps.query, nextProps.query) && - isEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && - isEqual(prevProps.pageFilters, nextProps.pageFilters) && + deepEqual(prevProps.pageFilters, nextProps.pageFilters) && prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && - isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && prevProps.utilityBar === nextProps.utilityBar ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index eb41773bb21c8..22fc9f27ce26c 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -6,7 +6,7 @@ import { EuiBadge } from '@elastic/eui'; import { defaultTo, getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; @@ -58,28 +58,39 @@ export const FlyoutComponent = React.memo( timelineId, usersViewing, width, - }) => ( - <> - - showTimeline({ id: timelineId, show: false })} + }) => { + const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ + showTimeline, + timelineId, + ]); + const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ + showTimeline, + timelineId, + ]); + + return ( + <> + + + {children} + + + - {children} - - - showTimeline({ id: timelineId, show: true })} - /> - - ) + onOpen={handleOpen} + /> + + ); + } ); FlyoutComponent.displayName = 'FlyoutComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 405a8f060948e..d6f8143745356 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; -import { getOr } from 'lodash/fp'; +import { getOr, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -162,7 +162,11 @@ const makeMapStateToProps = () => { const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); const mapStateToProps = (state: State, { inputId = 'global', queryId }: OwnProps) => { - return inputId === 'global' ? getGlobalQuery(state, queryId) : getTimelineQuery(state, queryId); + const props = + inputId === 'global' ? getGlobalQuery(state, queryId) : getTimelineQuery(state, queryId); + // refetch caused unnecessary component rerender and it was even not used + const propsWithoutRefetch = omit('refetch', props); + return propsWithoutRefetch; }; return mapStateToProps; }; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index e40cc887ab5ff..8a754cb47475f 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import isEqual from 'lodash/fp/isEqual'; -import deepEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; +import deepEqual from 'fast-deep-equal'; import { useKibana } from '../../lib/kibana'; import { RouteSpyState } from '../../utils/route/types'; @@ -81,8 +80,8 @@ export const SiemNavigationRedux = compose< (prevProps, nextProps) => prevProps.pathName === nextProps.pathName && prevProps.search === nextProps.search && - isEqual(prevProps.navTabs, nextProps.navTabs) && - isEqual(prevProps.urlState, nextProps.urlState) && + deepEqual(prevProps.navTabs, nextProps.navTabs) && + deepEqual(prevProps.urlState, nextProps.urlState) && deepEqual(prevProps.state, nextProps.state) ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx index 853ba7ae23414..678faff7654db 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx @@ -7,7 +7,7 @@ /* eslint-disable react/display-name */ import { has } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { hostsActions } from '../../../../store/hosts'; @@ -100,10 +100,12 @@ const AuthenticationTableComponent = React.memo( [type, updateTableActivePage] ); + const columns = useMemo(() => getAuthenticationColumnsCurated(type), [type]); + return ( ( [type, updateTableActivePage] ); + const columns = useMemo(() => getUncommonColumnsCurated(type), [type]); + return ( ]; -export const getNetworkDnsColumns = (type: networkModel.NetworkType): NetworkDnsColumns => [ +export const getNetworkDnsColumns = (): NetworkDnsColumns => [ { field: `node.${NetworkDnsFields.dnsName}`, name: i18n.REGISTERED_DOMAIN, diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx index f3fe98936a55d..c1dd96c5c96f9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { networkActions } from '../../../../store/actions'; import { @@ -93,7 +93,7 @@ export const NetworkDnsTableComponent = React.memo( field: criteria.sort.field.split('.')[1] as NetworkDnsFields, direction: criteria.sort.direction as Direction, }; - if (!isEqual(newDnsSortField, sort)) { + if (!deepEqual(newDnsSortField, sort)) { updateNetworkTable({ networkType: type, tableType, @@ -115,10 +115,12 @@ export const NetworkDnsTableComponent = React.memo( [type, updateNetworkTable, isPtrIncluded] ); + const columns = useMemo(() => getNetworkDnsColumns(), []); + return ( = ({ const sorting = { field: `node.${NetworkHttpFields.requestCount}`, direction: sort.direction }; + const columns = useMemo(() => getNetworkHttpColumns(tableType), [tableType]); + return ( = ({ field: field as NetworkTopTablesFields, direction: newSortDirection as Direction, }; - if (!isEqual(newTopNFlowSort, sort)) { + if (!deepEqual(newTopNFlowSort, sort)) { updateNetworkTable({ networkType: type, tableType, diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx index 77abae68b76bf..d1512699cc709 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { networkActions } from '../../../../store/network'; import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../../graphql/types'; @@ -91,7 +91,7 @@ const TlsTableComponent = React.memo( field: getSortFromString(splitField[splitField.length - 1]), direction: criteria.sort.direction as Direction, }; - if (!isEqual(newTlsSort, sort)) { + if (!deepEqual(newTlsSort, sort)) { updateNetworkTable({ networkType: type, tableType, @@ -103,10 +103,12 @@ const TlsTableComponent = React.memo( [sort, type, tableType, updateNetworkTable] ); + const columns = useMemo(() => getTlsColumns(tlsTableId), [tlsTableId]); + return ( ( field: getSortFromString(splitField[splitField.length - 1]), direction: criteria.sort.direction as Direction, }; - if (!isEqual(newUsersSort, sort)) { + if (!deepEqual(newUsersSort, sort)) { updateNetworkTable({ networkType: type, tableType, @@ -109,10 +109,15 @@ const UsersTableComponent = React.memo( [sort, type, updateNetworkTable] ); + const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [ + flowTarget, + usersTableId, + ]); + return ( = ({ data, loading ); }; -OverviewHostStatsComponent.displayName = 'OverviewHostStatsComponent'; - export const OverviewHostStats = React.memo(OverviewHostStatsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx index 260b1d6895140..ca947c29bc382 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx @@ -130,7 +130,7 @@ const AccordionContent = styled.div` margin-top: 8px; `; -export const OverviewNetworkStats = React.memo(({ data, loading }) => { +const OverviewNetworkStatsComponent: React.FC = ({ data, loading }) => { const allNetworkStats = getOverviewNetworkStats(data); const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0); @@ -190,6 +190,6 @@ export const OverviewNetworkStats = React.memo(({ data, lo })} ); -}); +}; -OverviewNetworkStats.displayName = 'OverviewNetworkStats'; +export const OverviewNetworkStats = React.memo(OverviewNetworkStatsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index 03a8143c89517..557d389aefee9 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import deepEqual from 'fast-deep-equal'; import { Filter, @@ -64,7 +64,7 @@ export const QueryBar = memo( const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !isEqual(payload.query, filterQuery)) { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { onSubmitQuery(payload.query); } }, @@ -73,7 +73,7 @@ export const QueryBar = memo( const onQueryChange = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !isEqual(payload.query, draftQuery)) { + if (payload.query != null && !deepEqual(payload.query, draftQuery)) { setDraftQuery(payload.query); onChangedQuery(payload.query); } diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index cb5729ad8e26e..2513004af84dd 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, isEqual, set } from 'lodash/fp'; +import { getOr, set } from 'lodash/fp'; import React, { memo, useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { FilterManager, IIndexPattern, TimeRange, Query, Filter } from 'src/plugins/data/public'; import { SavedQuery } from 'src/legacy/core_plugins/data/public'; @@ -60,7 +61,6 @@ const SearchBarComponent = memo( setSavedQuery, setSearchBarFilter, start, - timelineId, toStr, updateSearch, dataTestSubj, @@ -108,7 +108,7 @@ const SearchBarComponent = memo( updateSearchBar.start = payload.dateRange.from; } - if (payload.query != null && !isEqual(payload.query, filterQuery)) { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { isStateUpdated = true; updateSearchBar = set('query', payload.query, updateSearchBar); } diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx index 33159387214e4..b8192cce11e5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx @@ -5,8 +5,9 @@ */ import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, isEqual, uniqWith } from 'lodash/fp'; +import { isEmpty, uniqWith } from 'lodash/fp'; import React from 'react'; +import deepEqual from 'fast-deep-equal'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME, Port } from '../port'; @@ -115,7 +116,7 @@ const IpAdressesWithPorts = React.memo<{ return ( - {uniqWith(isEqual, ipPortPairs).map( + {uniqWith(deepEqual, ipPortPairs).map( ipPortPair => ipPortPair.ip != null && ( diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index 69596ba8f3325..ef077ece19f92 100644 --- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -94,7 +94,6 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = isInspected={false} loading={false} queryId="statItems" - refetch={null} selectedInspectIndex={0} setIsInspected={[Function]} title="KPI HOSTS" @@ -328,7 +327,6 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] = isInspected={false} loading={false} queryId="statItems" - refetch={null} selectedInspectIndex={0} setIsInspected={[Function]} title="KPI HOSTS" @@ -632,7 +630,6 @@ exports[`Stat Items Component rendering kpis with charts it renders the default isInspected={false} loading={false} queryId="statItems" - refetch={null} selectedInspectIndex={0} setIsInspected={[Function]} title="KPI UNIQUE_PRIVATE_IPS" diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 3fcd258b79147..372930ee3167d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -8,7 +8,7 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` justifyContent="flexStart" > - = ({ browserFields, id, indexPattern, @@ -60,7 +60,7 @@ export const TimelineHeaderComponent = ({ onToggleDataProviderExcluded, show, showCallOutUnauthorizedMsg, -}: Props) => ( +}) => ( {showCallOutUnauthorizedMsg && ( ); -TimelineHeaderComponent.displayName = 'TimelineHeaderComponent'; - export const TimelineHeader = React.memo(TimelineHeaderComponent); - -TimelineHeader.displayName = 'TimelineHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index d782d0366f041..0ce6bc16f1325 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { WithSource } from '../../containers/source'; import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; @@ -215,11 +215,11 @@ const StatefulTimelineComponent = React.memo( prevProps.show === nextProps.show && prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && prevProps.start === nextProps.start && - isEqual(prevProps.columns, nextProps.columns) && - isEqual(prevProps.dataProviders, nextProps.dataProviders) && - isEqual(prevProps.filters, nextProps.filters) && - isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - isEqual(prevProps.sort, nextProps.sort) + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.sort, nextProps.sort) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx index 96b8df6d8ada7..7f662cdb2f1b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual, isEmpty } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { memo, useCallback, useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern, @@ -127,7 +128,7 @@ export const QueryBarTimeline = memo( const filterWithoutDropArea = filterManager .getFilters() .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - if (!isEqual(filters, filterWithoutDropArea)) { + if (!deepEqual(filters, filterWithoutDropArea)) { filterManager.setFilters(filters); } }, [filters]); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index d1904fd5d9aac..87061bdbb5d02 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, isEqual } from 'lodash/fp'; +import { getOr } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; +import deepEqual from 'fast-deep-equal'; import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; @@ -152,15 +153,15 @@ const StatefulSearchOrFilterComponent = React.memo( prevProps.isRefreshPaused === nextProps.isRefreshPaused && prevProps.refreshInterval === nextProps.refreshInterval && prevProps.timelineId === nextProps.timelineId && - isEqual(prevProps.browserFields, nextProps.browserFields) && - isEqual(prevProps.dataProviders, nextProps.dataProviders) && - isEqual(prevProps.filters, nextProps.filters) && - isEqual(prevProps.filterQuery, nextProps.filterQuery) && - isEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && - isEqual(prevProps.indexPattern, nextProps.indexPattern) && - isEqual(prevProps.kqlMode, nextProps.kqlMode) && - isEqual(prevProps.savedQueryId, nextProps.savedQueryId) && - isEqual(prevProps.timelineId, nextProps.timelineId) + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filterQuery, nextProps.filterQuery) && + deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.kqlMode, nextProps.kqlMode) && + deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + deepEqual(prevProps.timelineId, nextProps.timelineId) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index c9ff0296a40e2..58bbbef328ddf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -229,8 +229,4 @@ export const TimelineComponent: React.FC = ({ ); }; -TimelineComponent.displayName = 'TimelineComponent'; - export const Timeline = React.memo(TimelineComponent); - -Timeline.displayName = 'Timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx index e656ec3496d8d..294e41a1faa7b 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React from 'react'; import { compose, Dispatch } from 'redux'; import { connect } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { timelineActions } from '../../store/actions'; import { RouteSpyState } from '../../utils/route/types'; @@ -39,7 +39,7 @@ export const UrlStateRedux = compose - prevProps.pathName === nextProps.pathName && isEqual(prevProps.urlState, nextProps.urlState) + prevProps.pathName === nextProps.pathName && deepEqual(prevProps.urlState, nextProps.urlState) ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx index deaf9bbf5011d..a7704e0e86970 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual, difference, isEmpty } from 'lodash/fp'; +import { difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; import { useKibana } from '../../lib/kibana'; import { useApolloClient } from '../../utils/apollo_context'; @@ -77,7 +78,7 @@ export const useUrlStateHooks = ({ const updatedUrlStateString = getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? newUrlStateString; - if (isInitializing || !isEqual(updatedUrlStateString, newUrlStateString)) { + if (isInitializing || !deepEqual(updatedUrlStateString, newUrlStateString)) { urlStateToUpdate = [ ...urlStateToUpdate, { @@ -157,7 +158,7 @@ export const useUrlStateHooks = ({ if (isInitializing && pageName != null && pageName !== '') { handleInitialize(type); setIsInitializing(false); - } else if (!isEqual(urlState, prevProps.urlState) && !isInitializing) { + } else if (!deepEqual(urlState, prevProps.urlState) && !isInitializing) { let mySearch = search; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { if ( diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index b7ad41b8ba1bb..06c4d1054bca4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, isEqual, get } from 'lodash/fp'; +import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { @@ -41,7 +42,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => const [, dispatchToaster] = useStateToaster(); useEffect(() => { - if (!isEqual(defaultIndices, indices)) { + if (!deepEqual(defaultIndices, indices)) { setIndices(defaultIndices); } }, [defaultIndices, indices]); diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx index caf597d02c835..4632e9aee3fdd 100644 --- a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -41,6 +41,17 @@ export const GlobalTimeComponent: React.FC = ({ }) => { const [isInitializing, setIsInitializing] = useState(true); + const setQuery = useCallback( + ({ id, inspect, loading, refetch }: SetQuery) => + setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), + [setGlobalQuery] + ); + + const deleteQuery = useCallback( + ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + [deleteOneQuery] + ); + useEffect(() => { if (isInitializing) { setIsInitializing(false); @@ -56,9 +67,8 @@ export const GlobalTimeComponent: React.FC = ({ isInitializing, from, to, - setQuery: ({ id, inspect, loading, refetch }: SetQuery) => - setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), - deleteQuery: ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + setQuery, + deleteQuery, })} ); diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx index 4d6ab757fdea7..db618f216d83e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx @@ -5,10 +5,10 @@ */ import { ApolloQueryResult, NetworkStatus } from 'apollo-client'; -import { isEqual } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; +import deepEqual from 'fast-deep-equal'; import { ESQuery } from '../../common/typed_json'; import { inputsModel } from '../store/model'; @@ -85,7 +85,7 @@ export class QueryTemplatePaginated< public isItAValidLoading(loading: boolean, variables: TVariables, networkStatus: NetworkStatus) { if ( !this.myLoading && - (!isEqual(variables, this.queryVariables) || networkStatus === NetworkStatus.refetch) && + (!deepEqual(variables, this.queryVariables) || networkStatus === NetworkStatus.refetch) && loading ) { this.myLoading = true; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index 0336e4a9a977b..e454421ca955d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -10,6 +10,7 @@ import { Query } from 'react-apollo'; import React, { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; + import { useUiSetting$ } from '../../lib/kibana'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx index 92f6740e4d767..2d9b1ee844b4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx @@ -12,9 +12,11 @@ import { HistogramBarSeries, Position, Settings, + ChartSizeArray, } from '@elastic/charts'; -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiProgress } from '@elastic/eui'; + import { useTheme } from '../../../../components/charts/common'; import { histogramDateTimeFormatter } from '../../../../components/utils'; import { HistogramData } from './types'; @@ -43,6 +45,14 @@ export const SignalsHistogram = React.memo( }) => { const theme = useTheme(); + const chartSize: ChartSizeArray = useMemo(() => ['100%', chartHeight], [chartHeight]); + const xAxisId = useMemo(() => getAxisId('signalsHistogramAxisX'), []); + const yAxisId = useMemo(() => getAxisId('signalsHistogramAxisY'), []); + const id = useMemo(() => getSpecId('signalsHistogram'), []); + const yAccessors = useMemo(() => ['y'], []); + const splitSeriesAccessors = useMemo(() => ['g'], []); + const tickFormat = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); + return ( <> {loading && ( @@ -54,7 +64,7 @@ export const SignalsHistogram = React.memo( /> )} - + ( theme={theme} /> - + - + @@ -84,4 +90,5 @@ export const SignalsHistogram = React.memo( ); } ); + SignalsHistogram.displayName = 'SignalsHistogram'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 88795f9195e68..fbe854c1ee346 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -5,10 +5,10 @@ */ import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { Filter, @@ -99,7 +99,7 @@ export const QueryBarDefineRule = ({ const newFilters = filterManager.getFilters(); const { filters } = field.value as FieldValueQueryBar; - if (!isEqual(filters, newFilters)) { + if (!deepEqual(filters, newFilters)) { field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); } } @@ -117,10 +117,10 @@ export const QueryBarDefineRule = ({ let isSubscribed = true; async function updateFilterQueryFromValue() { const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; - if (!isEqual(query, queryDraft)) { + if (!deepEqual(query, queryDraft)) { setQueryDraft(query); } - if (!isEqual(filters, filterManager.getFilters())) { + if (!deepEqual(filters, filterManager.getFilters())) { filterManager.setFilters(filters); } if ( @@ -148,7 +148,7 @@ export const QueryBarDefineRule = ({ const onSubmitQuery = useCallback( (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { const { query } = field.value as FieldValueQueryBar; - if (!isEqual(query, newQuery)) { + if (!deepEqual(query, newQuery)) { field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); } }, @@ -158,7 +158,7 @@ export const QueryBarDefineRule = ({ const onChangedQuery = useCallback( (newQuery: Query) => { const { query } = field.value as FieldValueQueryBar; - if (!isEqual(query, newQuery)) { + if (!deepEqual(query, newQuery)) { field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); } }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx index 2c9173cbeb694..ac457d7345c29 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx @@ -12,8 +12,8 @@ import { EuiLoadingSpinner, EuiText, } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules'; import { FormattedDate } from '../../../../../components/formatted_date'; @@ -43,7 +43,7 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); useEffect(() => { - if (!isEqual(currentStatus, ruleStatus?.current_status)) { + if (!deepEqual(currentStatus, ruleStatus?.current_status)) { setCurrentStatus(ruleStatus?.current_status ?? null); } }, [currentStatus, ruleStatus, setCurrentStatus]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 45da7d081333e..431d793d6e68a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -13,9 +13,9 @@ import { EuiSpacer, EuiButtonEmpty, } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { setFieldValue } from '../../helpers'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; @@ -103,7 +103,7 @@ const StepAboutRuleComponent: FC = ({ useEffect(() => { const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { const myDefaultValues = { ...defaultValues, isNew: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 920a9f2dfe56c..773eb44efb26c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -11,9 +11,10 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; -import { isEmpty, isEqual } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { FC, memo, useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; @@ -126,9 +127,9 @@ const StepDefineRuleComponent: FC = ({ useEffect(() => { if (indicesConfig != null && defaultValues != null) { const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); - if (!isEqual(myDefaultValues, myStepData)) { + if (!deepEqual(myDefaultValues, myStepData)) { setMyStepData(myDefaultValues); - setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); + setLocalUseIndicesConfig(deepEqual(myDefaultValues.index, indicesConfig)); setFieldValue(form, schema, myDefaultValues); } } @@ -212,13 +213,13 @@ const StepDefineRuleComponent: FC = ({ {({ index }) => { if (index != null) { - if (isEqual(index, indicesConfig) && !localUseIndicesConfig) { + if (deepEqual(index, indicesConfig) && !localUseIndicesConfig) { setLocalUseIndicesConfig(true); } - if (!isEqual(index, indicesConfig) && localUseIndicesConfig) { + if (!deepEqual(index, indicesConfig) && localUseIndicesConfig) { setLocalUseIndicesConfig(false); } - if (index != null && !isEmpty(index) && !isEqual(index, mylocalIndicesConfig)) { + if (index != null && !isEmpty(index) && !deepEqual(index, mylocalIndicesConfig)) { setMyLocalIndicesConfig(index); } } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index cfbb0a622c721..2e2c7e068dd85 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -5,8 +5,8 @@ */ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; @@ -62,7 +62,7 @@ const StepScheduleRuleComponent: FC = ({ useEffect(() => { const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { const myDefaultValues = { ...defaultValues, isNew: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx index 41eb620850a7f..f5efd9248029d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx @@ -73,32 +73,25 @@ export const HostDetailsTabs = React.memo( return ( - } - /> - } - /> - } - /> - ( - - )} - /> - } - /> - } - /> + + + + + + + + + + + + + + + + + + + ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx index 0b83710a13293..80c35e5563c1d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; import { HostsTabsProps } from './types'; @@ -22,7 +22,7 @@ import { } from './navigation'; import { HostAlertsQueryTabBody } from './navigation/alerts_query_tab_body'; -const HostsTabs = memo( +export const HostsTabs = memo( ({ deleteQuery, filterQuery, @@ -44,49 +44,48 @@ const HostsTabs = memo( startDate: from, type, indexPattern, - narrowDateRange: (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, + narrowDateRange: useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ), + updateDateRange: useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ), }; return ( - } - /> - } - /> - } - /> - ( - - )} - /> - } - /> - } - /> + + + + + + + + + + + + + + + + + + ); } ); HostsTabs.displayName = 'HostsTabs'; - -export { HostsTabs }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index 23a619db97ee4..b6b54b68ac06a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -23,77 +23,82 @@ import { TlsQueryTabBody } from './tls_query_tab_body'; import { Anomaly } from '../../../components/ml/types'; import { NetworkAlertsQueryTabBody } from './alerts_query_tab_body'; -export const NetworkRoutes = ({ - networkPagePath, - type, - to, - filterQuery, - isInitializing, - from, - indexPattern, - setQuery, - setAbsoluteRangeDatePicker, -}: NetworkRoutesProps) => { - const narrowDateRange = useCallback( - (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [setAbsoluteRangeDatePicker] - ); +export const NetworkRoutes = React.memo( + ({ + networkPagePath, + type, + to, + filterQuery, + isInitializing, + from, + indexPattern, + setQuery, + setAbsoluteRangeDatePicker, + }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + const updateDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); - const networkAnomaliesFilterQuery = { - bool: { - should: [ - { - exists: { - field: 'source.ip', + const networkAnomaliesFilterQuery = { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, }, - }, - { - exists: { - field: 'destination.ip', + { + exists: { + field: 'destination.ip', + }, }, - }, - ], - minimum_should_match: 1, - }, - }; + ], + minimum_should_match: 1, + }, + }; - const commonProps = { - startDate: from, - endDate: to, - skip: isInitializing, - type, - narrowDateRange, - setQuery, - filterQuery, - }; + const commonProps = { + startDate: from, + endDate: to, + skip: isInitializing, + type, + narrowDateRange, + setQuery, + filterQuery, + }; - const tabProps = { - ...commonProps, - indexPattern, - }; + const tabProps = { + ...commonProps, + indexPattern, + updateDateRange, + }; - const anomaliesProps = { - ...commonProps, - anomaliesFilterQuery: networkAnomaliesFilterQuery, - AnomaliesTableComponent: AnomaliesNetworkTable, - }; + const anomaliesProps = { + ...commonProps, + anomaliesFilterQuery: networkAnomaliesFilterQuery, + AnomaliesTableComponent: AnomaliesNetworkTable, + }; - return ( - - } - /> - ( + return ( + + + + + <> @@ -118,31 +123,25 @@ export const NetworkRoutes = ({ - )} - /> - } - /> - } - /> - ( + + + + + + + + - )} - /> - } - /> - - ); -}; + + + + + + ); + } +); NetworkRoutes.displayName = 'NetworkRoutes'; diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/legacy/plugins/siem/public/routes.tsx index cbb58a473e8ea..a989fa9873435 100644 --- a/x-pack/legacy/plugins/siem/public/routes.tsx +++ b/x-pack/legacy/plugins/siem/public/routes.tsx @@ -20,8 +20,12 @@ const PageRouterComponent: FC = ({ history }) => ( - } /> - } /> + + + + + + diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx index 213b881bd2084..af993588f7e0d 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import { Dispatch } from 'redux'; import { IIndexPattern } from 'src/plugins/data/public'; +import deepEqual from 'fast-deep-equal'; import { KueryFilterQuery } from '../../store'; import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../store/timeline/actions'; @@ -29,7 +29,7 @@ export const useUpdateKql = ({ timelineId, }: UseUpdateKqlProps): RefetchKql => { const updateKql: RefetchKql = (dispatch: Dispatch) => { - if (kueryFilterQueryDraft != null && !isEqual(kueryFilterQuery, kueryFilterQueryDraft)) { + if (kueryFilterQueryDraft != null && !deepEqual(kueryFilterQuery, kueryFilterQueryDraft)) { if (storeType === 'timelineType' && timelineId != null) { dispatch( dispatchApplyTimelineFilterQuery({ diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx index c88562abef6ae..ddee2359b28ba 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -5,7 +5,6 @@ */ import * as H from 'history'; -import { isEqual } from 'lodash/fp'; import { memo, useEffect, useState } from 'react'; import { withRouter } from 'react-router-dom'; import deepEqual from 'fast-deep-equal'; @@ -35,7 +34,7 @@ export const SpyRouteComponent = memo( } }, [search]); useEffect(() => { - if (pageName && !isEqual(route.pathName, pathname)) { + if (pageName && !deepEqual(route.pathName, pathname)) { if (isInitializing && detailName == null) { dispatch({ type: 'updateRouteWithOutSearch', diff --git a/x-pack/package.json b/x-pack/package.json index f76b0182ea228..b8fe0326903b6 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -106,6 +106,7 @@ "@types/uuid": "^3.4.4", "@types/xml-crypto": "^1.4.0", "@types/xml2js": "^0.4.5", + "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "axios": "^0.19.0", "babel-jest": "^24.9.0", diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 978271166cc05..723da7cef6a77 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -36,7 +36,7 @@ "x-pack/test_utils/*" ], "plugins/*": ["src/legacy/core_plugins/*/public/"], - "fixtures/*": ["src/fixtures/*"] + "fixtures/*": ["src/fixtures/*"], }, "types": [ "node", diff --git a/yarn.lock b/yarn.lock index f46e869909e2e..7906f363813b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5684,6 +5684,13 @@ text-table "^0.2.0" webpack-log "^1.1.2" +"@welldone-software/why-did-you-render@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-4.0.0.tgz#cc98c996f5a06ea55bd07dc99ba4b4d68af93332" + integrity sha512-PjqriZ8Ak9biP2+kOcIrg+NwsFwWVhGV03Hm+ns84YBCArn+hWBKM9rMBEU6e62I1qyrYF2/G9yktNpEmfWfJA== + dependencies: + lodash "^4" + "@wry/context@^0.4.0": version "0.4.1" resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.1.tgz#b3e23ca036035cbad0bd9711269352dd03a6fe3c" @@ -19767,7 +19774,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== From d339740c2de9f61d7140cd9bc983383f2843df67 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 25 Feb 2020 09:27:18 -0500 Subject: [PATCH 088/113] [Uptime] Migrate server to new platform (#53939) * Move uptime legacy server to plugins directory. * Re-add feature registration code. * Move feature registration back to kibana bootstrap file. * Delete obsolete test file. * Remove i18n call from untracked file. * Implement feedback. Co-authored-by: Elastic Machine --- .../plugins/uptime/common/constants/plugin.ts | 7 ++- x-pack/legacy/plugins/uptime/index.ts | 19 -------- .../plugins/uptime/scripts/graphql_schemas.ts | 15 ------ .../__tests__/find_potential_matches_test.ts | 5 -- x-pack/legacy/plugins/uptime/server/plugin.ts | 19 -------- x-pack/plugins/uptime/kibana.json | 9 ++++ .../uptime/server/graphql/constants.ts | 0 .../plugins/uptime/server/graphql/index.ts | 0 .../server/graphql/monitor_states/index.ts | 0 .../graphql/monitor_states/resolvers.ts | 6 +-- .../graphql/monitor_states/schema.gql.ts | 0 .../uptime/server/graphql/monitors/index.ts | 0 .../server/graphql/monitors/resolvers.ts | 10 +++- .../server/graphql/monitors/schema.gql.ts | 0 .../uptime/server/graphql/pings/index.ts | 0 .../uptime/server/graphql/pings/resolvers.ts | 7 ++- .../uptime/server/graphql/pings/schema.gql.ts | 0 .../plugins/uptime/server/graphql/types.ts | 0 .../__tests__/parse_literal.test.ts | 0 .../__tests__/parse_value.test.ts | 0 .../__tests__/serialize.test.ts | 0 .../graphql/unsigned_int_scalar/index.ts | 0 .../graphql/unsigned_int_scalar/resolvers.ts | 0 .../graphql/unsigned_int_scalar/schema.gql.ts | 0 .../plugins/uptime/server/index.ts | 6 ++- .../plugins/uptime/server/kibana.index.ts | 17 +++---- .../lib/adapters/framework/adapter_types.ts | 11 ++--- .../server/lib/adapters/framework/index.ts | 0 .../framework/kibana_framework_adapter.ts | 0 .../uptime/server/lib/adapters/index.ts | 0 .../kibana_telemetry_adapter.test.ts.snap | 0 .../kibana_telemetry_adapter.test.ts | 0 .../server/lib/adapters/telemetry/index.ts | 0 .../telemetry/kibana_telemetry_adapter.ts | 0 .../uptime/server/lib/compose/kibana.ts | 4 +- .../__snapshots__/license.test.ts.snap | 0 .../lib/domains/__tests__/license.test.ts | 2 +- .../uptime/server/lib/domains/index.ts | 0 .../uptime/server/lib/domains/license.ts | 2 +- .../assert_close_to.test.ts.snap | 0 .../get_filter_clause.test.ts.snap | 0 .../helper/__test__/assert_close_to.test.ts | 0 .../helper/__test__/get_filter_clause.test.ts | 0 .../__test__/get_histogram_interval.test.ts | 0 .../get_histogram_interval_formatted.test.ts | 0 .../__test__/parse_relative_date.test.ts | 0 .../server/lib/helper/assert_close_to.ts | 0 .../server/lib/helper/get_filter_clause.ts | 0 .../lib/helper/get_histogram_interval.ts | 2 +- .../get_histogram_interval_formatted.ts | 0 .../plugins/uptime/server/lib/helper/index.ts | 0 .../lib/helper/make_date_rate_filter.ts | 0 .../server/lib/helper/object_to_array.ts | 0 .../plugins/uptime/server/lib/lib.ts | 0 .../extract_filter_aggs_results.test.ts.snap | 0 .../generate_filter_aggs.test.ts.snap | 0 .../get_monitor_charts.test.ts.snap | 0 .../get_ping_histogram.test.ts.snap | 0 .../combine_range_with_filters.test.ts | 0 .../extract_filter_aggs_results.test.ts | 0 .../__tests__/generate_filter_aggs.test.ts | 0 .../__tests__/get_latest_monitor.test.ts | 0 .../__tests__/get_monitor_charts.test.ts | 0 .../__tests__/get_ping_histogram.test.ts | 0 .../lib/requests/__tests__/get_pings.test.ts | 0 .../__tests__/monitor_charts_mock.json | 0 .../lib/requests/generate_filter_aggs.ts | 0 .../server/lib/requests/get_filter_bar.ts | 4 +- .../server/lib/requests/get_index_pattern.ts | 4 +- .../server/lib/requests/get_index_status.ts | 4 +- .../server/lib/requests/get_latest_monitor.ts | 4 +- .../uptime/server/lib/requests/get_monitor.ts | 4 +- .../server/lib/requests/get_monitor_charts.ts | 7 ++- .../lib/requests/get_monitor_details.ts | 7 ++- .../lib/requests/get_monitor_locations.ts | 10 +++- .../server/lib/requests/get_monitor_states.ts | 8 +++- .../server/lib/requests/get_ping_histogram.ts | 4 +- .../uptime/server/lib/requests/get_pings.ts | 8 +++- .../lib/requests/get_snapshot_counts.ts | 7 ++- .../uptime/server/lib/requests/index.ts | 0 .../search/__tests__/fetch_page.test.ts | 2 +- .../__tests__/monitor_group_iterator.test.ts | 0 .../search/__tests__/query_context.test.ts | 5 +- .../requests/search/__tests__/test_helpers.ts | 5 +- .../requests/search/enrich_monitor_groups.ts | 4 +- .../server/lib/requests/search/fetch_chunk.ts | 0 .../server/lib/requests/search/fetch_page.ts | 8 +++- .../requests/search/find_potential_matches.ts | 4 +- .../server/lib/requests/search/index.ts | 0 .../requests/search/monitor_group_iterator.ts | 2 +- .../lib/requests/search/query_context.ts | 2 +- .../search/refine_potential_matches.ts | 4 +- .../server/lib/requests/search/types.ts | 5 +- .../uptime/server/lib/requests/types.ts | 7 ++- .../server/lib/requests/uptime_requests.ts | 11 +++-- x-pack/plugins/uptime/server/plugin.ts | 17 +++++++ .../server/rest_api/create_route_with_auth.ts | 0 .../plugins/uptime/server/rest_api/index.ts | 0 .../index_pattern/get_index_pattern.ts | 0 .../server/rest_api/index_pattern/index.ts | 0 .../uptime/server/rest_api/monitors/index.ts | 0 .../rest_api/monitors/monitor_locations.ts | 0 .../rest_api/monitors/monitors_details.ts | 0 .../uptime/server/rest_api/monitors/status.ts | 0 .../overview_filters/get_overview_filters.ts | 0 .../server/rest_api/overview_filters/index.ts | 0 .../uptime/server/rest_api/pings/get_all.ts | 48 +++++++++++++++++++ .../rest_api/pings/get_ping_histogram.ts | 0 .../uptime/server/rest_api/pings/get_pings.ts | 0 .../uptime/server/rest_api/pings/index.ts | 0 .../rest_api/snapshot/get_snapshot_count.ts | 0 .../uptime/server/rest_api/snapshot/index.ts | 0 .../uptime/server/rest_api/telemetry/index.ts | 0 .../rest_api/telemetry/log_monitor_page.ts | 0 .../rest_api/telemetry/log_overview_page.ts | 0 .../plugins/uptime/server/rest_api/types.ts | 0 .../server/rest_api/uptime_route_wrapper.ts | 0 .../plugins/uptime/server/uptime_server.ts | 0 .../apis/uptime/rest/ping_histogram.ts | 2 +- 119 files changed, 204 insertions(+), 134 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/plugin.ts create mode 100644 x-pack/plugins/uptime/kibana.json rename x-pack/{legacy => }/plugins/uptime/server/graphql/constants.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/monitor_states/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/monitor_states/resolvers.ts (89%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/monitor_states/schema.gql.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/monitors/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/monitors/resolvers.ts (71%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/monitors/schema.gql.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/pings/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/pings/resolvers.ts (84%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/pings/schema.gql.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/types.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/index.ts (59%) rename x-pack/{legacy => }/plugins/uptime/server/kibana.index.ts (82%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/framework/adapter_types.ts (85%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/framework/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/telemetry/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/compose/kibana.ts (80%) rename x-pack/{legacy => }/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/domains/__tests__/license.test.ts (93%) rename x-pack/{legacy => }/plugins/uptime/server/lib/domains/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/domains/license.ts (93%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/assert_close_to.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/get_filter_clause.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/get_histogram_interval.ts (95%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/make_date_rate_filter.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/helper/object_to_array.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/lib.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/generate_filter_aggs.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_filter_bar.ts (94%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_index_pattern.ts (92%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_index_status.ts (76%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_latest_monitor.ts (91%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_monitor.ts (87%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_monitor_charts.ts (96%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_monitor_details.ts (88%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_monitor_locations.ts (92%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_monitor_states.ts (89%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_ping_histogram.ts (93%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_pings.ts (93%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/get_snapshot_counts.ts (96%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts (96%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts (95%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts (88%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts (98%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/fetch_chunk.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/fetch_page.ts (96%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/find_potential_matches.ts (95%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts (98%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/query_context.ts (97%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts (96%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/search/types.ts (76%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/types.ts (89%) rename x-pack/{legacy => }/plugins/uptime/server/lib/requests/uptime_requests.ts (83%) create mode 100644 x-pack/plugins/uptime/server/plugin.ts rename x-pack/{legacy => }/plugins/uptime/server/rest_api/create_route_with_auth.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/index_pattern/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/monitors/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/monitors/monitor_locations.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/monitors/monitors_details.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/monitors/status.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/overview_filters/index.ts (100%) create mode 100644 x-pack/plugins/uptime/server/rest_api/pings/get_all.ts rename x-pack/{legacy => }/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/pings/get_pings.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/pings/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/snapshot/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/telemetry/index.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/types.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/rest_api/uptime_route_wrapper.ts (100%) rename x-pack/{legacy => }/plugins/uptime/server/uptime_server.ts (100%) diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts index f6fa569a50315..00781726941d5 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + export const PLUGIN = { APP_ROOT_ID: 'react-uptime-root', DESCRIPTION: 'Uptime monitoring', ID: 'uptime', - ROUTER_BASE_NAME: '/app/uptime#', LOCAL_STORAGE_KEY: 'xpack.uptime', + NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { + defaultMessage: 'Uptime', + }), + ROUTER_BASE_NAME: '/app/uptime#', TITLE: 'uptime', }; diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index cf7332f97d466..feecef5857895 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -6,9 +6,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; -import { PluginInitializerContext } from 'src/core/server'; import { PLUGIN } from './common/constants'; -import { KibanaServer, plugin } from './server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export const uptime = (kibana: any) => @@ -35,21 +33,4 @@ export const uptime = (kibana: any) => }, home: ['plugins/uptime/register_feature'], }, - init(server: KibanaServer) { - const initializerContext = {} as PluginInitializerContext; - const { savedObjects } = server; - const { xpack_main } = server.plugins; - const { usageCollection } = server.newPlatform.setup.plugins; - - plugin(initializerContext).setup( - { - route: server.newPlatform.setup.core.http.createRouter(), - }, - { - savedObjects, - usageCollection, - xpack: xpack_main, - } - ); - }, }); diff --git a/x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts b/x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts deleted file mode 100644 index c337cf098e48d..0000000000000 --- a/x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts +++ /dev/null @@ -1,15 +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 { buildSchemaFromTypeDefinitions } from 'graphql-tools'; -import { typeDefs } from '../server/graphql'; - -export const schemas = [...typeDefs]; - -// this default export is used to feed the combined types to the gql-gen tool -// which generates the corresponding typescript types -// eslint-disable-next-line import/no-default-export -export default buildSchemaFromTypeDefinitions(schemas); diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts deleted file mode 100644 index 41bc2aa258807..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts +++ /dev/null @@ -1,5 +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. - */ diff --git a/x-pack/legacy/plugins/uptime/server/plugin.ts b/x-pack/legacy/plugins/uptime/server/plugin.ts deleted file mode 100644 index acecce305e7cb..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/plugin.ts +++ /dev/null @@ -1,19 +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 { PluginInitializerContext } from 'src/core/server'; -import { initServerWithKibana } from './kibana.index'; -import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters/framework'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(); -} - -export class Plugin { - public setup(core: UptimeCoreSetup, plugins: UptimeCorePlugins) { - initServerWithKibana(core, plugins); - } -} diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json new file mode 100644 index 0000000000000..dd61716325afc --- /dev/null +++ b/x-pack/plugins/uptime/kibana.json @@ -0,0 +1,9 @@ +{ + "configPath": ["xpack"], + "id": "uptime", + "kibanaVersion": "kibana", + "requiredPlugins": ["features", "licensing", "usageCollection"], + "server": true, + "ui": false, + "version": "8.0.0" +} diff --git a/x-pack/legacy/plugins/uptime/server/graphql/constants.ts b/x-pack/plugins/uptime/server/graphql/constants.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/constants.ts rename to x-pack/plugins/uptime/server/graphql/constants.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/index.ts b/x-pack/plugins/uptime/server/graphql/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/index.ts rename to x-pack/plugins/uptime/server/graphql/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/index.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitor_states/index.ts rename to x-pack/plugins/uptime/server/graphql/monitor_states/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts index e2b076d570843..6ac42f7717259 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts @@ -6,13 +6,13 @@ import { CreateUMGraphQLResolvers, UMContext } from '../types'; import { UMServerLibs } from '../../lib/lib'; -import { UMResolver } from '../../../common/graphql/resolver_types'; +import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; import { GetMonitorStatesQueryArgs, MonitorSummaryResult, StatesIndexStatus, -} from '../../../common/graphql/types'; -import { CONTEXT_DEFAULTS } from '../../../common/constants/context_defaults'; +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants/context_defaults'; export type UMGetMonitorStatesResolver = UMResolver< MonitorSummaryResult | Promise, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitor_states/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/index.ts b/x-pack/plugins/uptime/server/graphql/monitors/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitors/index.ts rename to x-pack/plugins/uptime/server/graphql/monitors/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitors/resolvers.ts similarity index 71% rename from x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/monitors/resolvers.ts index 19f23fa1bb934..b39c5f42bfd75 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMResolver } from '../../../common/graphql/resolver_types'; -import { GetMonitorChartsDataQueryArgs, MonitorChart } from '../../../common/graphql/types'; +import { UMGqlRange } from '../../../../../legacy/plugins/uptime/common/domain_types'; +import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; +import { + GetMonitorChartsDataQueryArgs, + MonitorChart, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { CreateUMGraphQLResolvers, UMContext } from '../types'; +export type UMMonitorsResolver = UMResolver, any, UMGqlRange, UMContext>; + export type UMGetMonitorChartsResolver = UMResolver< any | Promise, any, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/monitors/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/monitors/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/index.ts b/x-pack/plugins/uptime/server/graphql/pings/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/pings/index.ts rename to x-pack/plugins/uptime/server/graphql/pings/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts similarity index 84% rename from x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/pings/resolvers.ts index de83a9ced16b2..b383fc5d5fb15 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMResolver } from '../../../common/graphql/resolver_types'; -import { AllPingsQueryArgs, PingResults } from '../../../common/graphql/types'; +import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; +import { + AllPingsQueryArgs, + PingResults, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { UMContext } from '../types'; import { CreateUMGraphQLResolvers } from '../types'; diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/pings/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/types.ts b/x-pack/plugins/uptime/server/graphql/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/types.ts rename to x-pack/plugins/uptime/server/graphql/types.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/index.ts b/x-pack/plugins/uptime/server/index.ts similarity index 59% rename from x-pack/legacy/plugins/uptime/server/index.ts rename to x-pack/plugins/uptime/server/index.ts index d063f0d8c2288..bec47fa9db4cf 100644 --- a/x-pack/legacy/plugins/uptime/server/index.ts +++ b/x-pack/plugins/uptime/server/index.ts @@ -4,5 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from '../../../../src/core/server'; +import { Plugin } from './plugin'; + export { initServerWithKibana, KibanaServer } from './kibana.index'; -export { plugin } from './plugin'; +export const plugin = (initializerContext: PluginInitializerContext) => + new Plugin(initializerContext); diff --git a/x-pack/legacy/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts similarity index 82% rename from x-pack/legacy/plugins/uptime/server/kibana.index.ts rename to x-pack/plugins/uptime/server/kibana.index.ts index 73fabc629946b..c7ac3a70c0494 100644 --- a/x-pack/legacy/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { Request, Server } from 'hapi'; -import { PLUGIN } from '../common/constants'; +import { PLUGIN } from '../../../legacy/plugins/uptime/common/constants'; import { KibanaTelemetryAdapter } from './lib/adapters/telemetry'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; @@ -25,17 +24,13 @@ export interface KibanaServer extends Server { } export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => { - const { usageCollection, xpack } = plugins; - const libs = compose(server, plugins); + const { features, usageCollection } = plugins; + const libs = compose(server); KibanaTelemetryAdapter.registerUsageCollector(usageCollection); - initUptimeServer(libs); - - xpack.registerFeature({ + features.registerFeature({ id: PLUGIN.ID, - name: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { - defaultMessage: 'Uptime', - }), + name: PLUGIN.NAME, navLinkId: PLUGIN.ID, icon: 'uptimeApp', app: ['uptime', 'kibana'], @@ -59,4 +54,6 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor }, }, }); + + initUptimeServer(libs); }; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts similarity index 85% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts rename to x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index fb2052bb4c87f..8dde6050d5d36 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -6,13 +6,9 @@ import { GraphQLSchema } from 'graphql'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { - SavedObjectsLegacyService, - IRouter, - CallAPIOptions, - SavedObjectsClientContract, -} from 'src/core/server'; +import { IRouter, CallAPIOptions, SavedObjectsClientContract } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; +import { PluginSetupContract } from '../../../../../features/server'; type APICaller = ( endpoint: string, @@ -34,9 +30,8 @@ export interface UptimeCoreSetup { } export interface UptimeCorePlugins { - savedObjects: SavedObjectsLegacyService; + features: PluginSetupContract; usageCollection: UsageCollectionSetup; - xpack: any; } export interface UMBackendFrameworkAdapter { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/index.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/framework/index.ts rename to x-pack/plugins/uptime/server/lib/adapters/framework/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts rename to x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts b/x-pack/plugins/uptime/server/lib/adapters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts rename to x-pack/plugins/uptime/server/lib/adapters/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/index.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/index.ts rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts b/x-pack/plugins/uptime/server/lib/compose/kibana.ts similarity index 80% rename from x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts rename to x-pack/plugins/uptime/server/lib/compose/kibana.ts index 875a5d9dc8c5c..edda5cb283323 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts +++ b/x-pack/plugins/uptime/server/lib/compose/kibana.ts @@ -8,9 +8,9 @@ import { UMKibanaBackendFrameworkAdapter } from '../adapters/framework'; import * as requests from '../requests'; import { licenseCheck } from '../domains'; import { UMDomainLibs, UMServerLibs } from '../lib'; -import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters/framework'; +import { UptimeCoreSetup } from '../adapters/framework'; -export function compose(server: UptimeCoreSetup, plugins: UptimeCorePlugins): UMServerLibs { +export function compose(server: UptimeCoreSetup): UMServerLibs { const framework = new UMKibanaBackendFrameworkAdapter(server); const domainLibs: UMDomainLibs = { diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap b/x-pack/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap rename to x-pack/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts b/x-pack/plugins/uptime/server/lib/domains/__tests__/license.test.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts rename to x-pack/plugins/uptime/server/lib/domains/__tests__/license.test.ts index 8c47b318da9bd..b842f55fc7579 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts +++ b/x-pack/plugins/uptime/server/lib/domains/__tests__/license.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILicense } from '../../../../../../../plugins/licensing/server'; +import { ILicense } from '../../../../../licensing/server'; import { licenseCheck } from '../license'; describe('license check', () => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/index.ts b/x-pack/plugins/uptime/server/lib/domains/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/domains/index.ts rename to x-pack/plugins/uptime/server/lib/domains/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts b/x-pack/plugins/uptime/server/lib/domains/license.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/domains/license.ts rename to x-pack/plugins/uptime/server/lib/domains/license.ts index b8b5722d79877..d272424379e48 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts +++ b/x-pack/plugins/uptime/server/lib/domains/license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILicense } from '../../../../../../plugins/licensing/server'; +import { ILicense } from '../../../../licensing/server'; export interface UMLicenseStatusResponse { statusCode: number; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap b/x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap rename to x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap b/x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap rename to x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts b/x-pack/plugins/uptime/server/lib/helper/assert_close_to.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts rename to x-pack/plugins/uptime/server/lib/helper/assert_close_to.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts b/x-pack/plugins/uptime/server/lib/helper/get_filter_clause.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts rename to x-pack/plugins/uptime/server/lib/helper/get_filter_clause.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts rename to x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts index 26515fb4b4c63..fb44f5727aab3 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -5,7 +5,7 @@ */ import DateMath from '@elastic/datemath'; -import { QUERY } from '../../../common/constants'; +import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; export const parseRelativeDate = (dateStr: string, options = {}) => { // We need this this parsing because if user selects This week or this date diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts rename to x-pack/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/plugins/uptime/server/lib/helper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/index.ts rename to x-pack/plugins/uptime/server/lib/helper/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/make_date_rate_filter.ts b/x-pack/plugins/uptime/server/lib/helper/make_date_rate_filter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/make_date_rate_filter.ts rename to x-pack/plugins/uptime/server/lib/helper/make_date_rate_filter.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts b/x-pack/plugins/uptime/server/lib/helper/object_to_array.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts rename to x-pack/plugins/uptime/server/lib/helper/object_to_array.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/lib.ts rename to x-pack/plugins/uptime/server/lib/lib.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json b/x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json rename to x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/generate_filter_aggs.ts b/x-pack/plugins/uptime/server/lib/requests/generate_filter_aggs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/generate_filter_aggs.ts rename to x-pack/plugins/uptime/server/lib/requests/generate_filter_aggs.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts rename to x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts index 79259afe2b9eb..affe205a46844 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -5,9 +5,9 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { OverviewFilters } from '../../../common/runtime_types'; +import { OverviewFilters } from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { generateFilterAggs } from './generate_filter_aggs'; -import { INDEX_NAMES } from '../../../common/constants'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetFilterBarParams { /** @param dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts similarity index 92% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts rename to x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 4b40f800b6779..1ba1eb62e8439 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -6,8 +6,8 @@ import { APICaller } from 'src/core/server'; import { UMElasticsearchQueryFn } from '../adapters'; -import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../../src/plugins/data/server'; -import { INDEX_NAMES } from '../../../common/constants'; +import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export const getUptimeIndexPattern: UMElasticsearchQueryFn = async callES => { const indexPatternsFetcher = new IndexPatternsFetcher((...rest: Parameters) => diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts similarity index 76% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_index_status.ts rename to x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index e801b05d057f4..95aa7eeef88e1 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { StatesIndexStatus } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES }) => { const { diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts similarity index 91% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts rename to x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index bfaee3f2bf7ee..2d549fce06884 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { Ping } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetLatestMonitorParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor.ts similarity index 87% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor.ts index 94175616f374e..20103042f19ab 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { Ping } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetMonitorParams { /** @member monitorId optional limit to monitorId */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_charts.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_charts.ts index b97cc7287e921..7dd17ef9aa80f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_charts.ts @@ -5,9 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES } from '../../../common/constants'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; import { getHistogramIntervalFormatted } from '../helper'; -import { MonitorChart, LocationDurationLine } from '../../../common/graphql/types'; +import { + MonitorChart, + LocationDurationLine, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; export interface GetMonitorChartsParams { /** @member monitorId ID value for the selected monitor */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index b516fde1ce844..eb3657e60a7bb 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -5,8 +5,11 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { + MonitorDetails, + MonitorError, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetMonitorDetailsParams { monitorId: string; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts similarity index 92% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index e1a0e14fe951d..328ef54c404d3 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -5,8 +5,14 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES, UNNAMED_LOCATION } from '../../../common/constants'; -import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; +import { + INDEX_NAMES, + UNNAMED_LOCATION, +} from '../../../../../legacy/plugins/uptime/common/constants'; +import { + MonitorLocations, + MonitorLocation, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; /** * Fetch data for the monitor page title. diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 32c82b1fa2098..5b02e2502a27e 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CONTEXT_DEFAULTS } from '../../../common/constants'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; import { fetchPage } from './search'; import { UMElasticsearchQueryFn } from '../adapters'; -import { MonitorSummary, SortOrder, CursorDirection } from '../../../common/graphql/types'; +import { + MonitorSummary, + SortOrder, + CursorDirection, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { QueryContext } from './search'; export interface CursorPagination { diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts rename to x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 7b8ca4708255c..339409b63a4f6 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,10 +5,10 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; +import { INDEX_NAMES, QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; -import { INDEX_NAMES, QUERY } from '../../../common/constants'; import { HistogramQueryResult } from './types'; -import { HistogramResult } from '../../../common/types'; +import { HistogramResult } from '../../../../../legacy/plugins/uptime/common/types'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts rename to x-pack/plugins/uptime/server/lib/requests/get_pings.ts index 381aca720dc1d..ddca27d782066 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -5,8 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters/framework'; -import { PingResults, Ping, HttpBody } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { + PingResults, + Ping, + HttpBody, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetPingsParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts rename to x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 8d84c0f4d6769..050e906f01c62 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -5,9 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Snapshot } from '../../../common/runtime_types'; +import { Snapshot } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { + CONTEXT_DEFAULTS, + INDEX_NAMES, +} from '../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './search'; -import { CONTEXT_DEFAULTS, INDEX_NAMES } from '../../../common/constants'; export interface GetSnapshotCountParams { dateRangeStart: string; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/index.ts rename to x-pack/plugins/uptime/server/lib/requests/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index d519a4e75463f..f542773f32796 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index 86506c2c4c044..ea81ec623e01c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -6,7 +6,10 @@ import { QueryContext } from '../query_context'; import { CursorPagination } from '../types'; -import { CursorDirection, SortOrder } from '../../../../../common/graphql/types'; +import { + CursorDirection, + SortOrder, +} from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; describe(QueryContext, () => { // 10 minute range diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index 98b192d14f91a..d96f8dc95aa72 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -5,7 +5,10 @@ */ import { CursorPagination } from '../types'; -import { CursorDirection, SortOrder } from '../../../../../common/graphql/types'; +import { + CursorDirection, + SortOrder, +} from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts rename to x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index e37c749e63566..9ad3928a3b1b2 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -7,14 +7,14 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; import { getHistogramIntervalFormatted } from '../../helper'; -import { INDEX_NAMES, STATES } from '../../../../common/constants'; +import { INDEX_NAMES, STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { MonitorSummary, SummaryHistogram, Check, CursorDirection, SortOrder, -} from '../../../../common/graphql/types'; +} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorEnricher } from './fetch_page'; export const enrichMonitorGroups: MonitorEnricher = async ( diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_chunk.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_chunk.ts rename to x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts rename to x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts index 6440850dc0ffc..62144dacbd377 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -7,8 +7,12 @@ import { flatten } from 'lodash'; import { CursorPagination } from './types'; import { QueryContext } from './query_context'; -import { QUERY } from '../../../../common/constants'; -import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/graphql/types'; +import { QUERY } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { + CursorDirection, + MonitorSummary, + SortOrder, +} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts rename to x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index fc0e35b279e0b..9b3b1186472be 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,8 +5,8 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../../common/constants'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts b/x-pack/plugins/uptime/server/lib/requests/search/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts rename to x-pack/plugins/uptime/server/lib/requests/search/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts rename to x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index ced557dbf62e0..267551907c5e8 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -6,7 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorGroups } from './fetch_page'; import { CursorPagination } from './types'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts rename to x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index f5b13c165d87d..c1f5d89ec1a38 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { APICaller } from 'src/core/server'; -import { INDEX_NAMES } from '../../../../common/constants'; +import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { CursorPagination } from './types'; import { parseRelativeDate } from '../../helper'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts rename to x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index a55301555c8bf..c55aff3e8c4cd 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { INDEX_NAMES } from '../../../../common/constants'; +import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts b/x-pack/plugins/uptime/server/lib/requests/search/types.ts similarity index 76% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts rename to x-pack/plugins/uptime/server/lib/requests/search/types.ts index dc6021a91146a..42c98ace6e8f5 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CursorDirection, SortOrder } from '../../../../common/graphql/types'; +import { + CursorDirection, + SortOrder, +} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; export interface CursorPagination { cursorKey?: any; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/types.ts b/x-pack/plugins/uptime/server/lib/requests/types.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/lib/requests/types.ts rename to x-pack/plugins/uptime/server/lib/requests/types.ts index e17eb546712a9..53a4e989e3789 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/types.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ping, PingResults } from '../../../common/graphql/types'; +import { Ping, PingResults } from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { UMElasticsearchQueryFn } from '../adapters'; -import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; +import { + GetPingHistogramParams, + HistogramResult, +} from '../../../../../legacy/plugins/uptime/common/types'; export interface GetAllParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts similarity index 83% rename from x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts rename to x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 73be850306202..8a411368c228f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -5,7 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping, MonitorChart, PingResults, StatesIndexStatus } from '../../../common/graphql/types'; +import { + Ping, + MonitorChart, + PingResults, + StatesIndexStatus, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { GetFilterBarParams, GetLatestMonitorParams, @@ -22,10 +27,10 @@ import { MonitorDetails, MonitorLocations, Snapshot, -} from '../../../common/runtime_types'; +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; -import { HistogramResult } from '../../../common/types'; +import { HistogramResult } from '../../../../../legacy/plugins/uptime/common/types'; type ESQ = UMElasticsearchQueryFn; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts new file mode 100644 index 0000000000000..e217b0e2f1ad8 --- /dev/null +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext, CoreStart, CoreSetup } from '../../../../src/core/server'; +import { initServerWithKibana } from './kibana.index'; +import { UptimeCorePlugins } from './lib/adapters'; + +export class Plugin { + constructor(_initializerContext: PluginInitializerContext) {} + public setup(core: CoreSetup, plugins: UptimeCorePlugins) { + initServerWithKibana({ route: core.http.createRouter() }, plugins); + } + public start(_core: CoreStart, _plugins: any) {} +} diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts rename to x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/index.ts rename to x-pack/plugins/uptime/server/rest_api/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts rename to x-pack/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/index.ts b/x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/index.ts rename to x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts b/x-pack/plugins/uptime/server/rest_api/monitors/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/status.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts rename to x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts rename to x-pack/plugins/uptime/server/rest_api/overview_filters/index.ts diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts new file mode 100644 index 0000000000000..21168edfc9744 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/pings', + validate: { + query: schema.object({ + dateRangeStart: schema.string(), + dateRangeEnd: schema.string(), + location: schema.maybe(schema.string()), + monitorId: schema.maybe(schema.string()), + size: schema.maybe(schema.number()), + sort: schema.maybe(schema.string()), + status: schema.maybe(schema.string()), + }), + }, + options: { + tags: ['access:uptime'], + }, + handler: async ({ callES }, _context, request, response): Promise => { + const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; + + const result = await libs.requests.getPings({ + callES, + dateRangeStart, + dateRangeEnd, + monitorId, + status, + sort, + size, + location, + }); + + return response.ok({ + body: { + ...result, + }, + }); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts rename to x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts rename to x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts b/x-pack/plugins/uptime/server/rest_api/pings/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts rename to x-pack/plugins/uptime/server/rest_api/pings/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts rename to x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts rename to x-pack/plugins/uptime/server/rest_api/snapshot/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/index.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/telemetry/index.ts rename to x-pack/plugins/uptime/server/rest_api/telemetry/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts rename to x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts rename to x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/types.ts rename to x-pack/plugins/uptime/server/rest_api/types.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/uptime_route_wrapper.ts rename to x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts diff --git a/x-pack/legacy/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/uptime_server.ts rename to x-pack/plugins/uptime/server/uptime_server.ts diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts index 429f50ec0aa5b..0982d5fef7cb4 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts @@ -6,7 +6,7 @@ import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { assertCloseTo } from '../../../../../legacy/plugins/uptime/server/lib/helper'; +import { assertCloseTo } from '../../../../../plugins/uptime/server/lib/helper'; export default function({ getService }: FtrProviderContext) { describe('pingHistogram', () => { From 899270a108c5e47f5602c0f5c3b67a8502e47173 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 25 Feb 2020 15:31:04 +0100 Subject: [PATCH 089/113] [Upgrade Assistant] Move out of legacy folder (#58034) * Create x-pack/plugins skeleton for upgrade assistant * Move public folder contents Move the public folder of Upgrade Assistant and migrate public to use HttpSetup (remove axios) * Include stylesheets in public * Move server side out of legacy Begin migration of Reindex worker to new platform Move imports around so that it satsifies new platform constraints like not importing server side code (even types) in client. * Updated the routes with new dependencies and removed server shim * Fix router unit tests * Fix server lib tests After changing function signatures for the reindex server factory (and others) all of the tests needed to be revisited and brought in line with the new APIs. Also used core/server mocks where appropriate * Clean up types issues * Fix setting credentials on request header * Removed the security plugin from Upgrade Assistant The security plugin is a potential future consumer of the Upgrade Assistant's deprecation feature and we would therefore not want to create a circular depedency here. We pull in the licensing plugin rather (as it is less likely that we will depend on that) and use it to determine whether security is available and enabled. * Migrate to config to new platform config xpack.upgrade_assistant.enabled * Remove unused types * Fix import issue * Move upgrade assistant back to Elasticsearch management section * Update dotfiles Added elasticsearch ui team as upgrade assistant code owner Updated i18nrc.json path * Alphabetical ordering in xpack/i18nrc.json * Implemented PR feedback Renamed callCluster -> callAsUser to be more consistent with platform naming. Added comment about why we are not using security plugin directly inside of Upgrade Assistant. Fixed long path imports and use 'src/core/..' throughout. Fixed import ordering. Renamed variables inside of telemetry lib. * Revert to longer import path In plugin.ts importing from 'kibana/server' or 'src/core/server' results in a module not found error. Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 2 + x-pack/.i18nrc.json | 2 +- .../legacy/plugins/upgrade_assistant/index.ts | 44 +--- .../plugins/upgrade_assistant/public/index.ts | 7 - .../upgrade_assistant/public/legacy.ts | 106 --------- .../public/np_ready/application/app.tsx | 40 ---- .../reindex/polling_service.test.mocks.ts | 24 -- .../public/np_ready/plugin.ts | 24 -- .../plugins/upgrade_assistant/server/index.ts | 7 - .../np_ready/lib/telemetry/es_ui_open_apis.ts | 47 ---- .../lib/telemetry/es_ui_reindex_apis.ts | 58 ----- .../server/np_ready/plugin.ts | 49 ---- .../server/np_ready/routes/cluster_checkup.ts | 46 ---- .../np_ready/routes/create_request_shim.ts | 16 -- .../np_ready/routes/deprecation_logging.ts | 57 ----- .../server/np_ready/routes/reindex_indices.ts | 207 ----------------- .../server/np_ready/types.ts | 29 --- .../upgrade_assistant/common/config.ts} | 10 +- .../plugins/upgrade_assistant/common/types.ts | 13 ++ .../upgrade_assistant/common/version.ts | 0 x-pack/plugins/upgrade_assistant/kibana.json | 9 + .../public/application/_index.scss} | 0 .../public/application/app.tsx | 44 ++++ .../public}/application/app_context.tsx | 1 - .../application/components/_index.scss | 0 .../application/components/error_banner.tsx | 0 .../components/latest_minor_banner.tsx | 2 +- .../application/components/tabs.test.tsx | 28 +-- .../public}/application/components/tabs.tsx | 9 +- .../__fixtures__/checkup_api_response.json | 0 .../__snapshots__/checkup_tab.test.tsx.snap | 0 .../__snapshots__/filter_bar.test.tsx.snap | 0 .../__snapshots__/group_by_bar.test.tsx.snap | 0 .../components/tabs/checkup/_index.scss | 0 .../tabs/checkup/checkup_tab.test.tsx | 0 .../components/tabs/checkup/checkup_tab.tsx | 2 +- .../components/tabs/checkup/constants.tsx | 0 .../components/tabs/checkup/controls.tsx | 0 .../tabs/checkup/deprecations/_cell.scss | 0 .../checkup/deprecations/_deprecations.scss | 0 .../tabs/checkup/deprecations/_index.scss | 0 .../tabs/checkup/deprecations/cell.tsx | 4 +- .../checkup/deprecations/count_summary.tsx | 2 +- .../checkup/deprecations/grouped.test.tsx | 2 +- .../tabs/checkup/deprecations/grouped.tsx | 2 +- .../tabs/checkup/deprecations/health.tsx | 0 .../tabs/checkup/deprecations/index.tsx | 0 .../checkup/deprecations/index_table.test.tsx | 0 .../tabs/checkup/deprecations/index_table.tsx | 4 +- .../tabs/checkup/deprecations/list.test.tsx | 2 +- .../tabs/checkup/deprecations/list.tsx | 2 +- .../checkup/deprecations/reindex/_button.scss | 0 .../checkup/deprecations/reindex/_index.scss | 0 .../checkup/deprecations/reindex/button.tsx | 7 +- .../checklist_step.test.tsx.snap | 0 .../__snapshots__/warning_step.test.tsx.snap | 0 .../deprecations/reindex/flyout/_index.scss | 0 .../reindex/flyout/_step_progress.scss | 0 .../reindex/flyout/_warnings_step.scss | 0 .../reindex/flyout/checklist_step.test.tsx | 2 +- .../reindex/flyout/checklist_step.tsx | 2 +- .../deprecations/reindex/flyout/container.tsx | 0 .../deprecations/reindex/flyout/index.tsx | 0 .../reindex/flyout/progress.test.tsx | 2 +- .../deprecations/reindex/flyout/progress.tsx | 2 +- .../reindex/flyout/step_progress.tsx | 0 .../reindex/flyout/warning_step.test.tsx | 2 +- .../reindex/flyout/warnings_step.tsx | 2 +- .../checkup/deprecations/reindex/index.tsx | 0 .../reindex/polling_service.test.ts | 67 ++---- .../deprecations/reindex/polling_service.ts | 29 +-- .../tabs/checkup/filter_bar.test.tsx | 0 .../components/tabs/checkup/filter_bar.tsx | 0 .../tabs/checkup/group_by_bar.test.tsx | 0 .../components/tabs/checkup/group_by_bar.tsx | 0 .../components/tabs/checkup/index.tsx | 0 .../components/tabs/overview/_index.scss | 0 .../components/tabs/overview/_steps.scss | 0 .../overview/deprecation_logging_toggle.tsx | 25 +- .../components/tabs/overview/index.tsx | 2 +- .../components/tabs/overview/steps.tsx | 6 +- .../public}/application/components/types.ts | 5 +- .../public/application/render_app.tsx | 20 ++ .../public}/application/utils.test.ts | 0 .../public}/application/utils.ts | 0 .../upgrade_assistant/public/index.scss | 1 + .../upgrade_assistant/public}/index.ts | 7 +- .../upgrade_assistant/public/plugin.ts | 48 ++++ .../plugins/upgrade_assistant/server/index.ts | 19 ++ .../lib/__fixtures__/fake_deprecations.json | 0 .../lib/__mocks__/es_version_precheck.ts | 0 .../es_migration_apis.test.ts.snap | 0 .../lib/es_deprecation_logging_apis.test.ts | 20 +- .../lib/es_deprecation_logging_apis.ts | 14 +- .../server}/lib/es_migration_apis.test.ts | 26 ++- .../server}/lib/es_migration_apis.ts | 27 +-- .../server}/lib/es_version_precheck.test.ts | 2 +- .../server}/lib/es_version_precheck.ts | 2 +- .../lib/reindexing/credential_store.test.ts | 2 +- .../lib/reindexing/credential_store.ts | 5 +- .../server}/lib/reindexing/index.ts | 0 .../lib/reindexing/index_settings.test.ts | 2 +- .../server}/lib/reindexing/index_settings.ts | 4 +- .../lib/reindexing/reindex_actions.test.ts | 4 +- .../server}/lib/reindexing/reindex_actions.ts | 9 +- .../lib/reindexing/reindex_service.test.ts | 93 ++++---- .../server}/lib/reindexing/reindex_service.ts | 70 +++--- .../server}/lib/reindexing/types.ts | 0 .../server}/lib/reindexing/worker.ts | 61 +++-- .../lib/telemetry/es_ui_open_apis.test.ts | 50 ++-- .../server/lib/telemetry/es_ui_open_apis.ts | 58 +++++ .../lib/telemetry/es_ui_reindex_apis.test.ts | 56 ++--- .../lib/telemetry/es_ui_reindex_apis.ts | 64 ++++++ .../server}/lib/telemetry/index.ts | 0 .../lib/telemetry/usage_collector.test.ts | 37 +-- .../server}/lib/telemetry/usage_collector.ts | 44 ++-- .../upgrade_assistant/server/plugin.ts | 125 ++++++++++ .../server}/routes/__mocks__/request.mock.ts | 2 +- .../server}/routes/__mocks__/routes.mock.ts | 16 +- .../server}/routes/cluster_checkup.test.ts | 44 ++-- .../server/routes/cluster_checkup.ts | 43 ++++ .../routes/deprecation_logging.test.ts | 52 +++-- .../server/routes/deprecation_logging.ts | 72 ++++++ .../server}/routes/reindex_indices.test.ts | 93 ++++---- .../server/routes/reindex_indices.ts | 217 ++++++++++++++++++ .../server}/routes/telemetry.test.ts | 58 +++-- .../server}/routes/telemetry.ts | 32 ++- .../plugins/upgrade_assistant/server/types.ts | 19 ++ .../upgrade_assistant/reindexing.js | 5 +- 129 files changed, 1234 insertions(+), 1343 deletions(-) delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/public/index.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/index.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts delete mode 100644 x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready/index.ts => plugins/upgrade_assistant/common/config.ts} (54%) rename x-pack/{legacy => }/plugins/upgrade_assistant/common/types.ts (88%) rename x-pack/{legacy => }/plugins/upgrade_assistant/common/version.ts (100%) create mode 100644 x-pack/plugins/upgrade_assistant/kibana.json rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss => plugins/upgrade_assistant/public/application/_index.scss} (100%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/app.tsx rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/app_context.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/_index.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/error_banner.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/latest_minor_banner.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs.test.tsx (82%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs.tsx (96%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/__fixtures__/checkup_api_response.json (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/_index.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/checkup_tab.test.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/checkup_tab.tsx (99%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/constants.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/controls.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/_cell.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/_deprecations.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/_index.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/cell.tsx (94%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/count_summary.tsx (93%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/grouped.test.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/grouped.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/health.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/index.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/index_table.test.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/index_table.tsx (96%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/list.test.tsx (96%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/list.tsx (97%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/_button.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/_index.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/button.tsx (97%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/_index.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/_step_progress.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/_warnings_step.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.test.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/checklist_step.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/container.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/index.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/progress.test.tsx (99%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/progress.tsx (99%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/step_progress.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx (95%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx (99%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/index.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts (63%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/deprecations/reindex/polling_service.ts (82%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/filter_bar.test.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/filter_bar.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/group_by_bar.test.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/group_by_bar.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/checkup/index.tsx (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/overview/_index.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/overview/_steps.scss (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/overview/deprecation_logging_toggle.tsx (86%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/overview/index.tsx (96%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/tabs/overview/steps.tsx (98%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/components/types.ts (89%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/render_app.tsx rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/utils.test.ts (100%) rename x-pack/{legacy/plugins/upgrade_assistant/public/np_ready => plugins/upgrade_assistant/public}/application/utils.ts (100%) create mode 100644 x-pack/plugins/upgrade_assistant/public/index.scss rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/public}/index.ts (62%) create mode 100644 x-pack/plugins/upgrade_assistant/public/plugin.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/index.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/__fixtures__/fake_deprecations.json (100%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/__mocks__/es_version_precheck.ts (100%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/__snapshots__/es_migration_apis.test.ts.snap (100%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/es_deprecation_logging_apis.test.ts (75%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/es_deprecation_logging_apis.ts (75%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/es_migration_apis.test.ts (73%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/es_migration_apis.ts (75%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/es_version_precheck.test.ts (98%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/es_version_precheck.ts (98%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/credential_store.test.ts (95%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/credential_store.ts (91%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/index.ts (100%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/index_settings.test.ts (99%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/index_settings.ts (97%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/reindex_actions.test.ts (99%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/reindex_actions.ts (97%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/reindex_service.test.ts (95%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/reindex_service.ts (90%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/types.ts (100%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/reindexing/worker.ts (76%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/telemetry/es_ui_open_apis.test.ts (51%) create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/telemetry/es_ui_reindex_apis.test.ts (52%) create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/telemetry/index.ts (100%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/telemetry/usage_collector.test.ts (78%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/lib/telemetry/usage_collector.ts (72%) create mode 100644 x-pack/plugins/upgrade_assistant/server/plugin.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/__mocks__/request.mock.ts (92%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/__mocks__/routes.mock.ts (72%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/cluster_checkup.test.ts (73%) create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/deprecation_logging.test.ts (56%) create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/reindex_indices.test.ts (80%) create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/telemetry.test.ts (79%) rename x-pack/{legacy/plugins/upgrade_assistant/server/np_ready => plugins/upgrade_assistant/server}/routes/telemetry.ts (66%) create mode 100644 x-pack/plugins/upgrade_assistant/server/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 51433f598ac16..b924c7a1a2c29 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -179,6 +179,8 @@ /x-pack/legacy/plugins/rollup/ @elastic/es-ui /x-pack/plugins/searchprofiler/ @elastic/es-ui /x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui +/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui +/x-pack/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/watcher/ @elastic/es-ui # Endpoint diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b1cb9075434e8..bb084b3bb72a1 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -40,7 +40,7 @@ "xpack.taskManager": "legacy/plugins/task_manager", "xpack.transform": "legacy/plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", - "xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant", + "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": "legacy/plugins/uptime", "xpack.watcher": "plugins/watcher" }, diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts index 3f98ff60a91ce..b5e8ce4750215 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/index.ts @@ -3,25 +3,14 @@ * 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 Joi from 'joi'; import { Legacy } from 'kibana'; -import { resolve } from 'path'; import mappings from './mappings.json'; -import { plugin } from './server/np_ready'; -import { CloudSetup } from '../../../plugins/cloud/server'; export function upgradeAssistant(kibana: any) { - const publicSrc = resolve(__dirname, 'public'); - const npSrc = resolve(publicSrc, 'np_ready'); - const config: Legacy.PluginSpecOptions = { id: 'upgrade_assistant', - configPrefix: 'xpack.upgrade_assistant', - require: ['elasticsearch'], uiExports: { // @ts-ignore - managementSections: ['plugins/upgrade_assistant'], savedObjectSchemas: { 'upgrade-assistant-reindex-operation': { isNamespaceAgnostic: true, @@ -30,41 +19,10 @@ export function upgradeAssistant(kibana: any) { isNamespaceAgnostic: true, }, }, - styleSheetPaths: resolve(npSrc, 'application/index.scss'), mappings, }, - publicDir: publicSrc, - - config() { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server: Legacy.Server) { - // Add server routes and initialize the plugin here - const instance = plugin({} as any); - const { usageCollection, cloud } = server.newPlatform.setup.plugins; - instance.setup(server.newPlatform.setup.core, { - usageCollection, - cloud: cloud as CloudSetup, - __LEGACY: { - // Legacy objects - events: server.events, - savedObjects: server.savedObjects, - - // Legacy functions - log: server.log.bind(server), - - // Legacy plugins - plugins: { - elasticsearch: server.plugins.elasticsearch, - xpack_main: server.plugins.xpack_main, - }, - }, - }); - }, + init() {}, }; return new kibana.Plugin(config); } diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/index.ts b/x-pack/legacy/plugins/upgrade_assistant/public/index.ts deleted file mode 100644 index d22b5d64b6b46..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './legacy'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts b/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts deleted file mode 100644 index b6bc6a14de224..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts +++ /dev/null @@ -1,106 +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 { ComponentType } from 'react'; -import { i18n } from '@kbn/i18n'; - -/* LEGACY IMPORTS */ -import { npSetup } from 'ui/new_platform'; -import { wrapInI18nContext } from 'ui/i18n'; -import { management } from 'ui/management'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import routes from 'ui/routes'; -import chrome from 'ui/chrome'; -/* LEGACY IMPORTS */ - -import { NEXT_MAJOR_VERSION } from '../common/version'; -import { plugin } from './np_ready'; -import { CloudSetup } from '../../../../plugins/cloud/public'; - -const BASE_PATH = `/management/elasticsearch/upgrade_assistant`; - -export interface LegacyAppMountParameters { - __LEGACY: { renderToElement: (RootComponent: ComponentType) => void }; -} - -export interface LegacyApp { - mount(ctx: any, params: LegacyAppMountParameters): void; -} - -export interface LegacyManagementPlugin { - sections: { - get( - name: string - ): { - registerApp(app: LegacyApp): void; - }; - }; -} - -// Based on /rfcs/text/0006_management_section_service.md -export interface LegacyPlugins { - cloud?: CloudSetup; - management: LegacyManagementPlugin; - __LEGACY: { - XSRF: string; - }; -} - -function startApp() { - routes.when(`${BASE_PATH}/:view?`, { - template: - '', - }); - const { cloud } = npSetup.plugins as any; - const legacyPluginsShim: LegacyPlugins = { - cloud: cloud as CloudSetup, - __LEGACY: { - XSRF: chrome.getXsrfToken(), - }, - management: { - sections: { - get(_: string) { - return { - registerApp(app) { - management.getSection('elasticsearch').register('upgrade_assistant', { - visible: true, - display: i18n.translate('xpack.upgradeAssistant.appTitle', { - defaultMessage: '{version} Upgrade Assistant', - values: { version: `${NEXT_MAJOR_VERSION}.0` }, - }), - order: 100, - url: `#${BASE_PATH}`, - }); - - app.mount( - {}, - { - __LEGACY: { - // While there is not an NP API for registering management section apps yet - renderToElement: RootComponent => { - uiModules - .get('kibana') - .directive('upgradeAssistant', (reactDirective: any) => { - return reactDirective(wrapInI18nContext(RootComponent)); - }); - }, - }, - } - ); - }, - }; - }, - }, - }, - }; - - const pluginInstance = plugin(); - - pluginInstance.setup(npSetup.core, legacyPluginsShim); -} - -startApp(); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx deleted file mode 100644 index 571967ab114c9..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx +++ /dev/null @@ -1,40 +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 { EuiPageHeader, EuiPageHeaderSection, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../common/version'; -import { UpgradeAssistantTabs } from './components/tabs'; -import { AppContextProvider, ContextValue, AppContext } from './app_context'; - -type AppDependencies = ContextValue; - -export const RootComponent = (deps: AppDependencies) => { - return ( - -
- - - -

- -

-
-
-
- - {({ http }) => } - -
-
- ); -}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts deleted file mode 100644 index dc7a758839fe5..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; - -export const mockClient = { - post: jest.fn().mockResolvedValue({ - lastCompletedStep: ReindexStep.created, - status: ReindexStatus.inProgress, - }), - get: jest.fn().mockResolvedValue({ - status: 200, - data: { - warnings: [], - reindexOp: null, - }, - }), -}; -jest.mock('axios', () => ({ - create: jest.fn().mockReturnValue(mockClient), -})); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts deleted file mode 100644 index ed85b988c25d6..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Plugin, CoreSetup } from 'src/core/public'; -import { RootComponent } from './application/app'; -import { LegacyPlugins } from '../legacy'; - -export class UpgradeAssistantUIPlugin implements Plugin { - async setup({ http }: CoreSetup, { cloud, management, __LEGACY: { XSRF } }: LegacyPlugins) { - const appRegistrar = management.sections.get('kibana'); - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - - return appRegistrar.registerApp({ - mount(__, { __LEGACY: { renderToElement } }) { - return renderToElement(() => RootComponent({ http, XSRF, isCloudEnabled })); - }, - }); - } - async start() {} - async stop() {} -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/index.ts deleted file mode 100644 index 8b0704283509d..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { plugin } from './np_ready'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts deleted file mode 100644 index b52b3b812b7f9..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts +++ /dev/null @@ -1,47 +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 { - UIOpen, - UIOpenOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../../common/types'; -import { RequestShim, ServerShim } from '../../types'; - -async function incrementUIOpenOptionCounter(server: ServerShim, uiOpenOptionCounter: UIOpenOption) { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_open.${uiOpenOptionCounter}` - ); -} - -export async function upsertUIOpenOption(server: ServerShim, req: RequestShim): Promise { - const { overview, cluster, indices } = req.payload as UIOpen; - - if (overview) { - await incrementUIOpenOptionCounter(server, 'overview'); - } - - if (cluster) { - await incrementUIOpenOptionCounter(server, 'cluster'); - } - - if (indices) { - await incrementUIOpenOptionCounter(server, 'indices'); - } - - return { - overview, - cluster, - indices, - }; -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts deleted file mode 100644 index 626d51b298e72..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts +++ /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 { - UIReindex, - UIReindexOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../../common/types'; -import { RequestShim, ServerShim } from '../../types'; - -async function incrementUIReindexOptionCounter( - server: ServerShim, - uiOpenOptionCounter: UIReindexOption -) { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.${uiOpenOptionCounter}` - ); -} - -export async function upsertUIReindexOption( - server: ServerShim, - req: RequestShim -): Promise { - const { close, open, start, stop } = req.payload as UIReindex; - - if (close) { - await incrementUIReindexOptionCounter(server, 'close'); - } - - if (open) { - await incrementUIReindexOptionCounter(server, 'open'); - } - - if (start) { - await incrementUIReindexOptionCounter(server, 'start'); - } - - if (stop) { - await incrementUIReindexOptionCounter(server, 'stop'); - } - - return { - close, - open, - start, - stop, - }; -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts deleted file mode 100644 index fae369fa59394..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts +++ /dev/null @@ -1,49 +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 { Plugin, CoreSetup, CoreStart } from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ServerShim, ServerShimWithRouter } from './types'; -import { credentialStoreFactory } from './lib/reindexing/credential_store'; -import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; -import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; -import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; -import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; -import { CloudSetup } from '../../../../../plugins/cloud/server'; -import { registerTelemetryRoutes } from './routes/telemetry'; - -interface PluginsSetup { - __LEGACY: ServerShim; - usageCollection: UsageCollectionSetup; - cloud?: CloudSetup; -} - -export class UpgradeAssistantServerPlugin implements Plugin { - setup({ http }: CoreSetup, { __LEGACY, usageCollection, cloud }: PluginsSetup) { - const router = http.createRouter(); - const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router }; - registerClusterCheckupRoutes(shimWithRouter, { cloud }); - registerDeprecationLoggingRoutes(shimWithRouter); - - // The ReindexWorker uses a map of request headers that contain the authentication credentials - // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not - // want to expose these credentials to any unauthenticated users. We also want to avoid any need - // to add a user for a special index just for upgrading. This in-memory cache allows us to - // process jobs without the browser staying on the page, but will require that jobs go into - // a paused state if no Kibana nodes have the required credentials. - const credentialStore = credentialStoreFactory(); - - const worker = registerReindexWorker(__LEGACY, credentialStore); - registerReindexIndicesRoutes(shimWithRouter, worker, credentialStore); - - // Bootstrap the needed routes and the collector for the telemetry - registerTelemetryRoutes(shimWithRouter); - registerUpgradeAssistantUsageCollector(usageCollection, __LEGACY); - } - - start(core: CoreStart, plugins: any) {} - - stop(): void {} -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts deleted file mode 100644 index 81cf690d813ad..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import { ServerShimWithRouter } from '../types'; -import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { CloudSetup } from '../../../../../../plugins/cloud/server'; -import { createRequestShim } from './create_request_shim'; - -interface PluginsSetup { - cloud?: CloudSetup; -} - -export function registerClusterCheckupRoutes( - server: ServerShimWithRouter, - pluginsSetup: PluginsSetup -) { - const { cloud } = pluginsSetup; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - - server.router.get( - { - path: '/api/upgrade_assistant/status', - validate: false, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - try { - return response.ok({ - body: await getUpgradeAssistantStatus(callWithRequest, reqShim, isCloudEnabled), - }); - } catch (e) { - if (e.status === 403) { - return response.forbidden(e.message); - } - - return response.internalError({ body: e }); - } - }) - ); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts deleted file mode 100644 index b1a5c8b72d0e0..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts +++ /dev/null @@ -1,16 +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 { KibanaRequest } from 'kibana/server'; -import { RequestShim } from '../types'; - -export const createRequestShim = (req: KibanaRequest): RequestShim => { - return { - headers: req.headers as Record, - payload: req.body || (req as any).payload, - params: req.params, - }; -}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts deleted file mode 100644 index 7e19ef3fb6047..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; - -import { - getDeprecationLoggingStatus, - setDeprecationLogging, -} from '../lib/es_deprecation_logging_apis'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { ServerShimWithRouter } from '../types'; -import { createRequestShim } from './create_request_shim'; - -export function registerDeprecationLoggingRoutes(server: ServerShimWithRouter) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - - server.router.get( - { - path: '/api/upgrade_assistant/deprecation_logging', - validate: false, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - try { - const result = await getDeprecationLoggingStatus(callWithRequest, reqShim); - return response.ok({ body: result }); - } catch (e) { - return response.internalError({ body: e }); - } - }) - ); - - server.router.put( - { - path: '/api/upgrade_assistant/deprecation_logging', - validate: { - body: schema.object({ - isEnabled: schema.boolean(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - try { - const { isEnabled } = reqShim.payload as { isEnabled: boolean }; - return response.ok({ - body: await setDeprecationLogging(callWithRequest, reqShim, isEnabled), - }); - } catch (e) { - return response.internalError({ body: e }); - } - }) - ); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts deleted file mode 100644 index c22f12316bd02..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsClientContract } from 'kibana/server'; -import { ReindexStatus } from '../../../common/types'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; -import { CredentialStore } from '../lib/reindexing/credential_store'; -import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; -import { ServerShim, ServerShimWithRouter } from '../types'; -import { createRequestShim } from './create_request_shim'; - -export function registerReindexWorker(server: ServerShim, credentialStore: CredentialStore) { - const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( - 'admin' - ); - const xpackInfo = server.plugins.xpack_main.info; - const savedObjectsRepository = server.savedObjects.getSavedObjectsRepository( - callWithInternalUser - ); - const savedObjectsClient = new server.savedObjects.SavedObjectsClient( - savedObjectsRepository - ) as SavedObjectsClientContract; - - // Cannot pass server.log directly because it's value changes during startup (?). - // Use this function to proxy through. - const log = (tags: string | string[], data?: string | object | (() => any), timestamp?: number) => - server.log(tags, data, timestamp); - - const worker = new ReindexWorker( - savedObjectsClient, - credentialStore, - callWithRequest, - callWithInternalUser, - xpackInfo, - log - ); - - // Wait for ES connection before starting the polling loop. - server.plugins.elasticsearch.waitUntilReady().then(() => { - worker.start(); - server.events.on('stop', () => worker.stop()); - }); - - return worker; -} - -export function registerReindexIndicesRoutes( - server: ServerShimWithRouter, - worker: ReindexWorker, - credentialStore: CredentialStore -) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const xpackInfo = server.plugins.xpack_main.info; - const BASE_PATH = '/api/upgrade_assistant/reindex'; - - // Start reindex for an index - server.router.post( - { - path: `${BASE_PATH}/{indexName}`, - validate: { - params: schema.object({ - indexName: schema.string(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - const { indexName } = reqShim.params; - const { client } = ctx.core.savedObjects; - const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - if (!(await reindexService.hasRequiredPrivileges(indexName))) { - return response.forbidden({ - body: `You do not have adequate privileges to reindex this index.`, - }); - } - - const existingOp = await reindexService.findReindexOperation(indexName); - - // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. - const reindexOp = - existingOp && existingOp.attributes.status === ReindexStatus.paused - ? await reindexService.resumeReindexOperation(indexName) - : await reindexService.createReindexOperation(indexName); - - // Add users credentials for the worker to use - credentialStore.set(reindexOp, reqShim.headers); - - // Kick the worker on this node to immediately pickup the new reindex operation. - worker.forceRefresh(); - - return response.ok({ body: reindexOp.attributes }); - } catch (e) { - return response.internalError({ body: e }); - } - }) - ); - - // Get status - server.router.get( - { - path: `${BASE_PATH}/{indexName}`, - validate: { - params: schema.object({ - indexName: schema.string(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - const { client } = ctx.core.savedObjects; - const { indexName } = reqShim.params; - const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); - const reindexOp = await reindexService.findReindexOperation(indexName); - // If the user doesn't have privileges than querying for warnings is going to fail. - const warnings = hasRequiredPrivileges - ? await reindexService.detectReindexWarnings(indexName) - : []; - const indexGroup = reindexService.getIndexGroup(indexName); - - return response.ok({ - body: { - reindexOp: reindexOp ? reindexOp.attributes : null, - warnings, - indexGroup, - hasRequiredPrivileges, - }, - }); - } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); - } - }) - ); - - // Cancel reindex - server.router.post( - { - path: `${BASE_PATH}/{indexName}/cancel`, - validate: { - params: schema.object({ - indexName: schema.string(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - const { indexName } = reqShim.params; - const { client } = ctx.core.savedObjects; - const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - await reindexService.cancelReindexing(indexName); - - return response.ok({ body: { acknowledged: true } }); - } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); - } - }) - ); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts deleted file mode 100644 index 77ba97529c32f..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Legacy } from 'kibana'; -import { IRouter } from 'src/core/server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; - -export interface ServerShim { - plugins: { - elasticsearch: ElasticsearchPlugin; - xpack_main: XPackMainPlugin; - }; - log: any; - events: any; - savedObjects: Legacy.SavedObjectsService; -} - -export interface ServerShimWithRouter extends ServerShim { - router: IRouter; -} - -export interface RequestShim { - headers: Record; - payload: any; - params: any; -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts b/x-pack/plugins/upgrade_assistant/common/config.ts similarity index 54% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts rename to x-pack/plugins/upgrade_assistant/common/config.ts index 020d6972f8280..8a13aedd5fdd8 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts +++ b/x-pack/plugins/upgrade_assistant/common/config.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpgradeAssistantUIPlugin } from './plugin'; +import { schema, TypeOf } from '@kbn/config-schema'; -export const plugin = () => { - return new UpgradeAssistantUIPlugin(); -}; +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type Config = TypeOf; diff --git a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts similarity index 88% rename from x-pack/legacy/plugins/upgrade_assistant/common/types.ts rename to x-pack/plugins/upgrade_assistant/common/types.ts index 0e65506bb584d..a0c12154988a1 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; export enum ReindexStep { @@ -114,3 +115,15 @@ export interface UpgradeAssistantTelemetry { export interface UpgradeAssistantTelemetrySavedObjectAttributes { [key: string]: any; } + +export interface EnrichedDeprecationInfo extends DeprecationInfo { + index?: string; + node?: string; + reindex?: boolean; +} + +export interface UpgradeAssistantStatus { + readyForUpgrade: boolean; + cluster: EnrichedDeprecationInfo[]; + indices: EnrichedDeprecationInfo[]; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/common/version.ts b/x-pack/plugins/upgrade_assistant/common/version.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/common/version.ts rename to x-pack/plugins/upgrade_assistant/common/version.ts diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json new file mode 100644 index 0000000000000..273036a653aeb --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "upgradeAssistant", + "version": "kibana", + "server": true, + "ui": true, + "configPath": ["xpack", "upgrade_assistant"], + "requiredPlugins": ["management", "licensing"], + "optionalPlugins": ["cloud", "usageCollection"] +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss b/x-pack/plugins/upgrade_assistant/public/application/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss rename to x-pack/plugins/upgrade_assistant/public/application/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx new file mode 100644 index 0000000000000..17eff71f1039b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -0,0 +1,44 @@ +/* + * 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 { I18nStart } from 'src/core/public'; +import { EuiPageHeader, EuiPageHeaderSection, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NEXT_MAJOR_VERSION } from '../../common/version'; +import { UpgradeAssistantTabs } from './components/tabs'; +import { AppContextProvider, ContextValue, AppContext } from './app_context'; + +export interface AppDependencies extends ContextValue { + i18n: I18nStart; +} + +export const RootComponent = ({ i18n, ...contexValue }: AppDependencies) => { + return ( + + +
+ + + +

+ +

+
+
+
+ + {({ http }) => } + +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx rename to x-pack/plugins/upgrade_assistant/public/application/app_context.tsx index a48a4efa3bbdf..1ae9dabd69481 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx @@ -9,7 +9,6 @@ import React, { createContext, useContext } from 'react'; export interface ContextValue { http: HttpSetup; isCloudEnabled: boolean; - XSRF: string; } export const AppContext = createContext({} as any); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx index 864df292fbffe..43d0364425cbb 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../common/version'; +import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../common/version'; export const LatestMinorBanner: React.FunctionComponent = () => ( ({ - get: jest.fn(), - create: jest.fn(), -})); - +import { httpServiceMock } from 'src/core/public/mocks'; import { UpgradeAssistantTabs } from './tabs'; import { LoadingState } from './types'; -import axios from 'axios'; import { OverviewTab } from './tabs/overview'; // Used to wait for promises to resolve and renders to finish before reading updates const promisesToResolve = () => new Promise(resolve => setTimeout(resolve, 0)); -const mockHttp = { - basePath: { - prepend: () => 'test', - }, - fetch() {}, -}; +const mockHttp = httpServiceMock.createSetupContract(); describe('UpgradeAssistantTabs', () => { test('renders loading state', async () => { - // @ts-ignore - axios.get.mockReturnValue( + mockHttp.get.mockReturnValue( new Promise(resolve => { /* never resolve */ }) ); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); // Should pass down loading status to child component expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Loading); }); test('successful data fetch', async () => { // @ts-ignore - axios.get.mockResolvedValue({ + mockHttp.get.mockResolvedValue({ data: { cluster: [], indices: [], }, }); const wrapper = mountWithIntl(); - expect(axios.get).toHaveBeenCalled(); + expect(mockHttp.get).toHaveBeenCalled(); await promisesToResolve(); wrapper.update(); // Should pass down success status to child component @@ -59,7 +47,7 @@ describe('UpgradeAssistantTabs', () => { test('network failure', async () => { // @ts-ignore - axios.get.mockRejectedValue(new Error(`oh no!`)); + mockHttp.get.mockRejectedValue(new Error(`oh no!`)); const wrapper = mountWithIntl(); await promisesToResolve(); wrapper.update(); @@ -69,7 +57,7 @@ describe('UpgradeAssistantTabs', () => { it('upgrade error', async () => { // @ts-ignore - axios.get.mockRejectedValue({ response: { status: 426 } }); + mockHttp.get.mockRejectedValue({ response: { status: 426 } }); const wrapper = mountWithIntl(); await promisesToResolve(); wrapper.update(); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 0b154fb20404d..43ec5554aaaee 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; import { findIndex, get, set } from 'lodash'; import React from 'react'; @@ -18,7 +17,7 @@ import { import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { HttpSetup } from 'src/core/public'; -import { UpgradeAssistantStatus } from '../../../../server/np_ready/lib/es_migration_apis'; +import { UpgradeAssistantStatus } from '../../../common/types'; import { LatestMinorBanner } from './latest_minor_banner'; import { CheckupTab } from './tabs/checkup'; import { OverviewTab } from './tabs/overview'; @@ -153,12 +152,10 @@ export class UpgradeAssistantTabsUI extends React.Component { private loadData = async () => { try { this.setState({ loadingState: LoadingState.Loading }); - const resp = await axios.get( - this.props.http.basePath.prepend('/api/upgrade_assistant/status') - ); + const resp = await this.props.http.get('/api/upgrade_assistant/status'); this.setState({ loadingState: LoadingState.Success, - checkupData: resp.data, + checkupData: resp, }); } catch (e) { if (get(e, 'response.status') === 426) { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/__fixtures__/checkup_api_response.json b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/__fixtures__/checkup_api_response.json similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/__fixtures__/checkup_api_response.json rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/__fixtures__/checkup_api_response.json diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx index 7e862a846290b..b047427174e08 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { LoadingErrorBanner } from '../../error_banner'; import { GroupByOption, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/constants.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_cell.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_cell.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_cell.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_cell.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_deprecations.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_deprecations.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx index 4bd2f7c4bf62c..879bb695ca60a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx @@ -79,9 +79,7 @@ export const DeprecationCell: FunctionComponent = ({ {reindexIndexName && ( - {({ http, XSRF }) => ( - - )} + {({ http }) => } )} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx similarity index 93% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx index a0e55dc55c865..7e5172a361a56 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FunctionComponent } from 'react'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; export const DeprecationCountSummary: FunctionComponent<{ deprecations: EnrichedDeprecationInfo[]; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx index 28f5f6894b78f..c6309fb57d786 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx @@ -11,7 +11,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { EuiBadge, EuiPagination } from '@elastic/eui'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx index 74f66b6c4fb35..8fa78639c39d3 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationCountSummary } from './count_summary'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx index 835affce59070..5506528a3ded0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx @@ -148,9 +148,7 @@ export class IndexDeprecationTableUI extends React.Component< render(indexDep: IndexDeprecationDetails) { return ( - {({ XSRF, http }) => ( - - )} + {({ http }) => } ); }, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx index 78ded73593464..a1e173737bab0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; import { DeprecationList } from './list'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx index 15a3d94974dcd..a46bc0d12fad4 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx @@ -7,7 +7,7 @@ import React, { FunctionComponent } from 'react'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; import { COLOR_MAP, LEVEL_MAP } from '../constants'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_button.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_button.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_button.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_button.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx index 2a28018a3ae81..30b46e0c15213 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -11,14 +11,13 @@ import { Subscription } from 'rxjs'; import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { HttpSetup } from 'src/core/public'; -import { ReindexStatus, UIReindexOption } from '../../../../../../../../common/types'; +import { ReindexStatus, UIReindexOption } from '../../../../../../../common/types'; import { LoadingState } from '../../../../types'; import { ReindexFlyout } from './flyout'; import { ReindexPollingService, ReindexState } from './polling_service'; interface ReindexButtonProps { indexName: string; - xsrf: string; http: HttpSetup; } @@ -154,8 +153,8 @@ export class ReindexButton extends React.Component { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx index 91e35c0bd7dc0..643dd2e9b6efc 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ReindexWarning } from '../../../../../../../../../common/types'; +import { ReindexWarning } from '../../../../../../../../common/types'; interface CheckedIds { [id: string]: boolean; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts similarity index 63% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index cb2a0856f0f2e..4228426d62159 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockClient } from './polling_service.test.mocks'; - -import { ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; import { httpServiceMock } from 'src/core/public/http/http_service.mock'; +const mockClient = httpServiceMock.createSetupContract(); + describe('ReindexPollingService', () => { beforeEach(() => { mockClient.post.mockReset(); @@ -18,18 +18,11 @@ describe('ReindexPollingService', () => { it('does not poll when reindexOp is null', async () => { mockClient.get.mockResolvedValueOnce({ - status: 200, - data: { - warnings: [], - reindexOp: null, - }, + warnings: [], + reindexOp: null, }); - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -39,22 +32,15 @@ describe('ReindexPollingService', () => { it('does not poll when first check is a 200 and status is failed', async () => { mockClient.get.mockResolvedValue({ - status: 200, - data: { - warnings: [], - reindexOp: { - lastCompletedStep: ReindexStep.created, - status: ReindexStatus.failed, - errorMessage: `Oh no!`, - }, + warnings: [], + reindexOp: { + lastCompletedStep: ReindexStep.created, + status: ReindexStatus.failed, + errorMessage: `Oh no!`, }, }); - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -65,21 +51,14 @@ describe('ReindexPollingService', () => { it('begins to poll when first check is a 200 and status is inProgress', async () => { mockClient.get.mockResolvedValue({ - status: 200, - data: { - warnings: [], - reindexOp: { - lastCompletedStep: ReindexStep.created, - status: ReindexStatus.inProgress, - }, + warnings: [], + reindexOp: { + lastCompletedStep: ReindexStep.created, + status: ReindexStatus.inProgress, }, }); - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -89,11 +68,7 @@ describe('ReindexPollingService', () => { describe('startReindex', () => { it('posts to endpoint', async () => { - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); await service.startReindex(); expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex'); @@ -102,11 +77,7 @@ describe('ReindexPollingService', () => { describe('cancelReindex', () => { it('posts to cancel endpoint', async () => { - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); await service.cancelReindex(); expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex/cancel'); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts similarity index 82% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts index 879fafe610982..6fe6a85905706 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import axios, { AxiosInstance } from 'axios'; - import { BehaviorSubject } from 'rxjs'; import { HttpSetup } from 'src/core/public'; @@ -14,7 +12,7 @@ import { ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../../../../../../common/types'; +} from '../../../../../../../common/types'; import { LoadingState } from '../../../../types'; const POLL_INTERVAL = 1000; @@ -45,24 +43,13 @@ interface StatusResponse { export class ReindexPollingService { public status$: BehaviorSubject; private pollTimeout?: NodeJS.Timeout; - private APIClient: AxiosInstance; - constructor(private indexName: string, private xsrf: string, private http: HttpSetup) { + constructor(private indexName: string, private http: HttpSetup) { this.status$ = new BehaviorSubject({ loadingState: LoadingState.Loading, errorMessage: null, reindexTaskPercComplete: null, }); - - this.APIClient = axios.create({ - headers: { - Accept: 'application/json', - credentials: 'same-origin', - 'Content-Type': 'application/json', - 'kbn-version': this.xsrf, - 'kbn-xsrf': this.xsrf, - }, - }); } public updateStatus = async () => { @@ -70,8 +57,8 @@ export class ReindexPollingService { this.stopPolling(); try { - const { data } = await this.APIClient.get( - this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}`) + const data = await this.http.get( + `/api/upgrade_assistant/reindex/${this.indexName}` ); this.updateWithResponse(data); @@ -107,8 +94,8 @@ export class ReindexPollingService { errorMessage: null, cancelLoadingState: undefined, }); - const { data } = await this.APIClient.post( - this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}`) + const data = await this.http.post( + `/api/upgrade_assistant/reindex/${this.indexName}` ); this.updateWithResponse({ reindexOp: data }); @@ -125,9 +112,7 @@ export class ReindexPollingService { cancelLoadingState: LoadingState.Loading, }); - await this.APIClient.post( - this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`) - ); + await this.http.post(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`); } catch (e) { this.status$.next({ ...this.status$.value, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_steps.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_steps.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_steps.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_steps.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx similarity index 86% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx index db37bc58904ec..0e6c79dc47b53 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; import React from 'react'; import { EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; @@ -15,7 +14,6 @@ import { HttpSetup } from 'src/core/public'; import { LoadingState } from '../../types'; interface DeprecationLoggingTabProps extends ReactIntl.InjectedIntlProps { - xsrf: string; http: HttpSetup; } @@ -88,12 +86,10 @@ export class DeprecationLoggingToggleUI extends React.Component< private loadData = async () => { try { this.setState({ loadingState: LoadingState.Loading }); - const resp = await axios.get( - this.props.http.basePath.prepend('/api/upgrade_assistant/deprecation_logging') - ); + const resp = await this.props.http.get('/api/upgrade_assistant/deprecation_logging'); this.setState({ loadingState: LoadingState.Success, - loggingEnabled: resp.data.isEnabled, + loggingEnabled: resp.isEnabled, }); } catch (e) { this.setState({ loadingState: LoadingState.Error }); @@ -102,26 +98,19 @@ export class DeprecationLoggingToggleUI extends React.Component< private toggleLogging = async () => { try { - const { http, xsrf } = this.props; // Optimistically toggle the UI const newEnabled = !this.state.loggingEnabled; this.setState({ loadingState: LoadingState.Loading, loggingEnabled: newEnabled }); - const resp = await axios.put( - http.basePath.prepend('/api/upgrade_assistant/deprecation_logging'), - { + const resp = await this.props.http.put('/api/upgrade_assistant/deprecation_logging', { + body: JSON.stringify({ isEnabled: newEnabled, - }, - { - headers: { - 'kbn-xsrf': xsrf, - }, - } - ); + }), + }); this.setState({ loadingState: LoadingState.Success, - loggingEnabled: resp.data.isEnabled, + loggingEnabled: resp.isEnabled, }); } catch (e) { this.setState({ loadingState: LoadingState.Error }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx index 284265bb31f14..aede377fa8d45 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { LoadingErrorBanner } from '../../error_banner'; import { LoadingState, UpgradeAssistantTabProps } from '../../types'; import { Steps } from './steps'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx index ccba51c73c136..85d275b080e13 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { UpgradeAssistantTabProps } from '../../types'; import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; import { useAppContext } from '../../../app_context'; @@ -104,7 +104,7 @@ export const StepsUI: FunctionComponent - + ), diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts similarity index 89% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/types.ts index 2d9a373f20b7e..86d1486543596 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts @@ -6,10 +6,7 @@ import React from 'react'; -import { - EnrichedDeprecationInfo, - UpgradeAssistantStatus, -} from '../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../../common/types'; export interface UpgradeAssistantTabProps { alertBanner?: React.ReactNode; diff --git a/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx b/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx new file mode 100644 index 0000000000000..97120cfc3333a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppDependencies, RootComponent } from './app'; + +interface BootDependencies extends AppDependencies { + element: HTMLElement; +} + +export const renderApp = (deps: BootDependencies) => { + const { element, ...appDependencies } = deps; + render(, element); + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts b/x-pack/plugins/upgrade_assistant/public/application/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts rename to x-pack/plugins/upgrade_assistant/public/application/utils.test.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts b/x-pack/plugins/upgrade_assistant/public/application/utils.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts rename to x-pack/plugins/upgrade_assistant/public/application/utils.ts diff --git a/x-pack/plugins/upgrade_assistant/public/index.scss b/x-pack/plugins/upgrade_assistant/public/index.scss new file mode 100644 index 0000000000000..9bd47b6473372 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/index.scss @@ -0,0 +1 @@ +@import './application/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts b/x-pack/plugins/upgrade_assistant/public/index.ts similarity index 62% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts rename to x-pack/plugins/upgrade_assistant/public/index.ts index cf1b78e1e3920..2b1860167ef5d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/index.ts @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; -import { UpgradeAssistantServerPlugin } from './plugin'; +import './index.scss'; +import { PluginInitializerContext } from 'src/core/public'; +import { UpgradeAssistantUIPlugin } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => { - return new UpgradeAssistantServerPlugin(); + return new UpgradeAssistantUIPlugin(ctx); }; diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts new file mode 100644 index 0000000000000..614221272dd5c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -0,0 +1,48 @@ +/* + * 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 { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; + +import { CloudSetup } from '../../cloud/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; + +import { NEXT_MAJOR_VERSION } from '../common/version'; +import { Config } from '../common/config'; + +import { renderApp } from './application/render_app'; + +interface Dependencies { + cloud: CloudSetup; + management: ManagementSetup; +} + +export class UpgradeAssistantUIPlugin implements Plugin { + constructor(private ctx: PluginInitializerContext) {} + setup({ http, getStartServices }: CoreSetup, { cloud, management }: Dependencies) { + const { enabled } = this.ctx.config.get(); + if (!enabled) { + return; + } + const appRegistrar = management.sections.getSection('elasticsearch')!; + const isCloudEnabled = Boolean(cloud?.isCloudEnabled); + + appRegistrar.registerApp({ + id: 'upgrade_assistant', + title: i18n.translate('xpack.upgradeAssistant.appTitle', { + defaultMessage: '{version} Upgrade Assistant', + values: { version: `${NEXT_MAJOR_VERSION}.0` }, + }), + order: 1000, + async mount({ element }) { + const [{ i18n: i18nDep }] = await getStartServices(); + return renderApp({ element, isCloudEnabled, http, i18n: i18nDep }); + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/upgrade_assistant/server/index.ts b/x-pack/plugins/upgrade_assistant/server/index.ts new file mode 100644 index 0000000000000..cab7eb613f74c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/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 { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { UpgradeAssistantServerPlugin } from './plugin'; +import { configSchema } from '../common/config'; + +export const plugin = (ctx: PluginInitializerContext) => { + return new UpgradeAssistantServerPlugin(ctx); +}; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + enabled: true, + }, +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__fixtures__/fake_deprecations.json b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__fixtures__/fake_deprecations.json rename to x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__mocks__/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/__mocks__/es_version_precheck.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__mocks__/es_version_precheck.ts rename to x-pack/plugins/upgrade_assistant/server/lib/__mocks__/es_version_precheck.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__snapshots__/es_migration_apis.test.ts.snap b/x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__snapshots__/es_migration_apis.test.ts.snap rename to x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts index 317e2a7554e03..862f64e232370 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { getDeprecationLoggingStatus, isDeprecationLoggingEnabled, @@ -12,9 +12,9 @@ import { describe('getDeprecationLoggingStatus', () => { it('calls cluster.getSettings', async () => { - const callWithRequest = jest.fn(); - await getDeprecationLoggingStatus(callWithRequest, {} as any); - expect(callWithRequest).toHaveBeenCalledWith({}, 'cluster.getSettings', { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + await getDeprecationLoggingStatus(dataClient); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.getSettings', { includeDefaults: true, }); }); @@ -23,9 +23,9 @@ describe('getDeprecationLoggingStatus', () => { describe('setDeprecationLogging', () => { describe('isEnabled = true', () => { it('calls cluster.putSettings with logger.deprecation = WARN', async () => { - const callWithRequest = jest.fn(); - await setDeprecationLogging(callWithRequest, {} as any, true); - expect(callWithRequest).toHaveBeenCalledWith({}, 'cluster.putSettings', { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + await setDeprecationLogging(dataClient, true); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { body: { transient: { 'logger.deprecation': 'WARN' } }, }); }); @@ -33,9 +33,9 @@ describe('setDeprecationLogging', () => { describe('isEnabled = false', () => { it('calls cluster.putSettings with logger.deprecation = ERROR', async () => { - const callWithRequest = jest.fn(); - await setDeprecationLogging(callWithRequest, {} as any, false); - expect(callWithRequest).toHaveBeenCalledWith({}, 'cluster.putSettings', { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + await setDeprecationLogging(dataClient, false); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { body: { transient: { 'logger.deprecation': 'ERROR' } }, }); }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts index 199d389408442..8f25533c0afb1 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts @@ -4,19 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; - -import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; -import { RequestShim } from '../types'; +import { IScopedClusterClient } from 'src/core/server'; interface DeprecationLoggingStatus { isEnabled: boolean; } export async function getDeprecationLoggingStatus( - callWithRequest: CallClusterWithRequest, - req: RequestShim + dataClient: IScopedClusterClient ): Promise { - const response = await callWithRequest(req, 'cluster.getSettings', { + const response = await dataClient.callAsCurrentUser('cluster.getSettings', { includeDefaults: true, }); @@ -26,11 +23,10 @@ export async function getDeprecationLoggingStatus( } export async function setDeprecationLogging( - callWithRequest: CallClusterWithRequest, - req: RequestShim, + dataClient: IScopedClusterClient, isEnabled: boolean ): Promise { - const response = await callWithRequest(req, 'cluster.putSettings', { + const response = await dataClient.callAsCurrentUser('cluster.putSettings', { body: { transient: { 'logger.deprecation': isEnabled ? 'WARN' : 'ERROR', diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts similarity index 73% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index a1d7049e4171f..4ab4227ba3e91 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -5,6 +5,7 @@ */ import _ from 'lodash'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { getUpgradeAssistantStatus } from './es_migration_apis'; import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; @@ -13,7 +14,8 @@ import fakeDeprecations from './__fixtures__/fake_deprecations.json'; describe('getUpgradeAssistantStatus', () => { let deprecationsResponse: DeprecationAPIResponse; - const callWithRequest = jest.fn().mockImplementation(async (req, api, { path }) => { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + (dataClient.callAsCurrentUser as jest.Mock).mockImplementation(async (api, { path }) => { if (path === '/_migration/deprecations') { return deprecationsResponse; } else if (api === 'indices.getMapping') { @@ -28,15 +30,15 @@ describe('getUpgradeAssistantStatus', () => { }); it('calls /_migration/deprecations', async () => { - await getUpgradeAssistantStatus(callWithRequest, {} as any, false); - expect(callWithRequest).toHaveBeenCalledWith({}, 'transport.request', { + await getUpgradeAssistantStatus(dataClient, false); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('transport.request', { path: '/_migration/deprecations', method: 'GET', }); }); it('returns the correct shape of data', async () => { - const resp = await getUpgradeAssistantStatus(callWithRequest, {} as any, false); + const resp = await getUpgradeAssistantStatus(dataClient, false); expect(resp).toMatchSnapshot(); }); @@ -48,9 +50,10 @@ describe('getUpgradeAssistantStatus', () => { index_settings: {}, }; - await expect( - getUpgradeAssistantStatus(callWithRequest, {} as any, false) - ).resolves.toHaveProperty('readyForUpgrade', false); + await expect(getUpgradeAssistantStatus(dataClient, false)).resolves.toHaveProperty( + 'readyForUpgrade', + false + ); }); it('returns readyForUpgrade === true when no critical issues found', async () => { @@ -61,9 +64,10 @@ describe('getUpgradeAssistantStatus', () => { index_settings: {}, }; - await expect( - getUpgradeAssistantStatus(callWithRequest, {} as any, false) - ).resolves.toHaveProperty('readyForUpgrade', true); + await expect(getUpgradeAssistantStatus(dataClient, false)).resolves.toHaveProperty( + 'readyForUpgrade', + true + ); }); it('filters out security realm deprecation on Cloud', async () => { @@ -80,7 +84,7 @@ describe('getUpgradeAssistantStatus', () => { index_settings: {}, }; - const result = await getUpgradeAssistantStatus(callWithRequest, {} as any, true); + const result = await getUpgradeAssistantStatus(dataClient, true); expect(result).toHaveProperty('readyForUpgrade', true); expect(result).toHaveProperty('cluster', []); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index b52c4c374266f..68f21c1fd93b5 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -4,32 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CallClusterWithRequest, - DeprecationAPIResponse, - DeprecationInfo, -} from 'src/legacy/core_plugins/elasticsearch'; - -import { RequestShim } from '../types'; - -export interface EnrichedDeprecationInfo extends DeprecationInfo { - index?: string; - node?: string; - reindex?: boolean; -} - -export interface UpgradeAssistantStatus { - readyForUpgrade: boolean; - cluster: EnrichedDeprecationInfo[]; - indices: EnrichedDeprecationInfo[]; -} +import { IScopedClusterClient } from 'src/core/server'; +import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; +import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../common/types'; export async function getUpgradeAssistantStatus( - callWithRequest: CallClusterWithRequest, - req: RequestShim, + dataClient: IScopedClusterClient, isCloudEnabled: boolean ): Promise { - const deprecations = await callWithRequest(req, 'transport.request', { + const deprecations = await dataClient.callAsCurrentUser('transport.request', { path: '/_migration/deprecations', method: 'GET', }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts index bbabe557df4d4..51cb776ef4e0b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts @@ -6,7 +6,7 @@ import { SemVer } from 'semver'; import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; -import { CURRENT_VERSION } from '../../../common/version'; +import { CURRENT_VERSION } from '../../common/version'; import { esVersionCheck, getAllNodeVersions, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts index 2fb3effe43793..e7636eea66479 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts @@ -13,7 +13,7 @@ import { RequestHandler, RequestHandlerContext, } from 'src/core/server'; -import { CURRENT_VERSION } from '../../../common/version'; +import { CURRENT_VERSION } from '../../common/version'; /** * Returns an array of all the unique Elasticsearch Node Versions in the Elasticsearch cluster. diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts index 06fa755472238..ce892df0de946 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReindexSavedObject } from '../../../../common/types'; +import { ReindexSavedObject } from '../../../common/types'; import { Credential, credentialStoreFactory } from './credential_store'; describe('credentialStore', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts similarity index 91% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts index a051d16b5779f..0958559910be6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts @@ -5,12 +5,11 @@ */ import { createHash } from 'crypto'; -import { Request } from 'hapi'; import stringify from 'json-stable-stringify'; -import { ReindexSavedObject } from '../../../../common/types'; +import { ReindexSavedObject } from '../../../common/types'; -export type Credential = Request['headers']; +export type Credential = Record; /** * An in-memory cache for user credentials to be used for reindexing operations. When looking up diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/index.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts index 7b346cc87edf6..9ec06b72f02e2 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; import { generateNewIndexName, getReindexWarnings, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts index 0b95bc628fbb4..f6dc471d0945d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts @@ -5,8 +5,8 @@ */ import { flow, omit } from 'lodash'; -import { ReindexWarning } from '../../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +import { ReindexWarning } from '../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; import { FlatSettings } from './types'; export interface ParsedIndexName { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 3fb855958a5d0..4569fdfa33a83 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -13,8 +13,8 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +} from '../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_actions'; describe('ReindexActions', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index 6683f80c8e779..2ae340f12d80c 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -6,8 +6,7 @@ import moment from 'moment'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsFindResponse, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsFindResponse, SavedObjectsClientContract, APICaller } from 'src/core/server'; import { IndexGroup, REINDEX_OP_TYPE, @@ -15,7 +14,7 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../../common/types'; +} from '../../../common/types'; import { generateNewIndexName } from './index_settings'; import { FlatSettings } from './types'; @@ -111,7 +110,7 @@ export interface ReindexActions { export const reindexActionsFactory = ( client: SavedObjectsClientContract, - callCluster: CallCluster + callAsUser: APICaller ): ReindexActions => { // ----- Internal functions const isLocked = (reindexOp: ReindexSavedObject) => { @@ -230,7 +229,7 @@ export const reindexActionsFactory = ( }, async getFlatSettings(indexName: string) { - const flatSettings = (await callCluster('transport.request', { + const flatSettings = (await callAsUser('transport.request', { path: `/${encodeURIComponent(indexName)}?flat_settings=true`, })) as { [indexName: string]: FlatSettings }; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index 9cd41c8cbe826..6c3b2c869dc7f 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -4,14 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject } from 'rxjs'; +import { Logger } from 'src/core/server'; +import { loggingServiceMock } from 'src/core/server/mocks'; + import { IndexGroup, ReindexOperation, ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +} from '../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; +import { licensingMock } from '../../../../licensing/server/mocks'; +import { LicensingPluginSetup } from '../../../../licensing/server'; + import { isMlIndex, isWatcherIndex, @@ -22,9 +29,9 @@ import { describe('reindexService', () => { let actions: jest.Mocked; let callCluster: jest.Mock; - let log: jest.Mock; - let xpackInfo: { feature: jest.Mocked }; + let log: Logger; let service: ReindexService; + let licensingPluginSetup: LicensingPluginSetup; const updateMockImpl = (reindexOp: ReindexSavedObject, attrs: Partial = {}) => Promise.resolve({ @@ -50,32 +57,24 @@ describe('reindexService', () => { runWhileIndexGroupLocked: jest.fn(async (group: string, f: any) => f({ attributes: {} })), }; callCluster = jest.fn(); - log = jest.fn(); - xpackInfo = { - feature: jest.fn(() => ({ - isAvailable() { - return true; - }, - isEnabled() { - return true; - }, - })), - }; - - service = reindexServiceFactory(callCluster as any, xpackInfo as any, actions, log); + log = loggingServiceMock.create().get(); + licensingPluginSetup = licensingMock.createSetup(); + licensingPluginSetup.license$ = new BehaviorSubject( + licensingMock.createLicense({ + features: { security: { isAvailable: true, isEnabled: true } }, + }) + ); + + service = reindexServiceFactory(callCluster as any, actions, log, licensingPluginSetup); }); describe('hasRequiredPrivileges', () => { it('returns true if security is disabled', async () => { - xpackInfo.feature.mockReturnValueOnce({ - isAvailable() { - return true; - }, - isEnabled() { - return false; - }, - }); - + licensingPluginSetup.license$ = new BehaviorSubject( + licensingMock.createLicense({ + features: { security: { isAvailable: true, isEnabled: false } }, + }) + ); const hasRequired = await service.hasRequiredPrivileges('anIndex'); expect(hasRequired).toBe(true); }); @@ -584,7 +583,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', @@ -599,7 +598,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', @@ -623,7 +622,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not stop ML jobs') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', @@ -645,7 +644,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Some nodes are not on minimum version') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); // Should not have called ML endpoint at all expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', @@ -698,7 +697,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_stop', method: 'POST', @@ -713,7 +712,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_stop', method: 'POST', @@ -735,7 +734,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not stop Watcher') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_watcher/_stop', method: 'POST', @@ -771,7 +770,7 @@ describe('reindexService', () => { ); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); it('fails if setting updates fail', async () => { @@ -782,7 +781,7 @@ describe('reindexService', () => { ); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -817,7 +816,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); it('fails if create index fails', async () => { @@ -829,7 +828,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); // Original index should have been set back to allow reads. expect(callCluster).toHaveBeenCalledWith('indices.putSettings', { @@ -874,7 +873,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.newIndexCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -931,7 +930,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -1014,7 +1013,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); it('fails if switching aliases fails', async () => { @@ -1023,7 +1022,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -1092,7 +1091,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', @@ -1108,7 +1107,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', @@ -1129,7 +1128,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not resume ML jobs') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', @@ -1196,7 +1195,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_start', method: 'POST', @@ -1212,7 +1211,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_start', method: 'POST', @@ -1233,7 +1232,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not start Watcher') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_watcher/_start', method: 'POST', diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts similarity index 90% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 0e6095f98b6ff..8f1df5b34372b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -5,17 +5,16 @@ */ import Boom from 'boom'; +import { APICaller, Logger } from 'src/core/server'; +import { first } from 'rxjs/operators'; -import { Server } from 'hapi'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackInfo } from '../../../../../xpack_main/server/lib/xpack_info'; import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../../common/types'; +} from '../../../common/types'; import { generateNewIndexName, getReindexWarnings, @@ -23,6 +22,7 @@ import { transformFlatSettings, } from './index_settings'; import { ReindexActions } from './reindex_actions'; +import { LicensingPluginSetup } from '../../../../licensing/server'; const VERSION_REGEX = new RegExp(/^([1-9]+)\.([0-9]+)\.([0-9]+)/); const ML_INDICES = ['.ml-state', '.ml-anomalies', '.ml-config']; @@ -97,10 +97,10 @@ export interface ReindexService { } export const reindexServiceFactory = ( - callCluster: CallCluster, - xpackInfo: XPackInfo, + callAsUser: APICaller, actions: ReindexActions, - log: Server['log'] + log: Logger, + licensing: LicensingPluginSetup ): ReindexService => { // ------ Utility functions @@ -114,7 +114,7 @@ export const reindexServiceFactory = ( await actions.runWhileIndexGroupLocked(IndexGroup.ml, async mlDoc => { await validateNodesMinimumVersion(6, 7); - const res = await callCluster('transport.request', { + const res = await callAsUser('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', }); @@ -134,7 +134,7 @@ export const reindexServiceFactory = ( await actions.decrementIndexGroupReindexes(IndexGroup.ml); await actions.runWhileIndexGroupLocked(IndexGroup.ml, async mlDoc => { if (mlDoc.attributes.runningReindexCount === 0) { - const res = await callCluster('transport.request', { + const res = await callAsUser('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', }); @@ -154,7 +154,7 @@ export const reindexServiceFactory = ( const stopWatcher = async () => { await actions.incrementIndexGroupReindexes(IndexGroup.watcher); await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async watcherDoc => { - const { acknowledged } = await callCluster('transport.request', { + const { acknowledged } = await callAsUser('transport.request', { path: '/_watcher/_stop', method: 'POST', }); @@ -174,7 +174,7 @@ export const reindexServiceFactory = ( await actions.decrementIndexGroupReindexes(IndexGroup.watcher); await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async watcherDoc => { if (watcherDoc.attributes.runningReindexCount === 0) { - const { acknowledged } = await callCluster('transport.request', { + const { acknowledged } = await callAsUser('transport.request', { path: '/_watcher/_start', method: 'POST', }); @@ -191,14 +191,14 @@ export const reindexServiceFactory = ( const cleanupChanges = async (reindexOp: ReindexSavedObject) => { // Cancel reindex task if it was started but not completed if (reindexOp.attributes.lastCompletedStep === ReindexStep.reindexStarted) { - await callCluster('tasks.cancel', { + await callAsUser('tasks.cancel', { taskId: reindexOp.attributes.reindexTaskId, }).catch(e => undefined); // Ignore any exceptions trying to cancel (it may have already completed). } // Set index back to writable if we ever got past this point. if (reindexOp.attributes.lastCompletedStep >= ReindexStep.readonly) { - await callCluster('indices.putSettings', { + await callAsUser('indices.putSettings', { index: reindexOp.attributes.indexName, body: { 'index.blocks.write': false }, }); @@ -208,7 +208,7 @@ export const reindexServiceFactory = ( reindexOp.attributes.lastCompletedStep >= ReindexStep.newIndexCreated && reindexOp.attributes.lastCompletedStep < ReindexStep.aliasCreated ) { - await callCluster('indices.delete', { index: reindexOp.attributes.newIndexName }); + await callAsUser('indices.delete', { index: reindexOp.attributes.newIndexName }); } // Resume consumers if we ever got past this point. @@ -222,7 +222,7 @@ export const reindexServiceFactory = ( // ------ Functions used to process the state machine const validateNodesMinimumVersion = async (minMajor: number, minMinor: number) => { - const nodesResponse = await callCluster('transport.request', { + const nodesResponse = await callAsUser('transport.request', { path: '/_nodes', method: 'GET', }); @@ -263,7 +263,7 @@ export const reindexServiceFactory = ( */ const setReadonly = async (reindexOp: ReindexSavedObject) => { const { indexName } = reindexOp.attributes; - const putReadonly = await callCluster('indices.putSettings', { + const putReadonly = await callAsUser('indices.putSettings', { index: indexName, body: { 'index.blocks.write': true }, }); @@ -289,7 +289,7 @@ export const reindexServiceFactory = ( const { settings, mappings } = transformFlatSettings(flatSettings); - const createIndex = await callCluster('indices.create', { + const createIndex = await callAsUser('indices.create', { index: newIndexName, body: { settings, @@ -313,7 +313,7 @@ export const reindexServiceFactory = ( const startReindexing = async (reindexOp: ReindexSavedObject) => { const { indexName } = reindexOp.attributes; - const startReindex = (await callCluster('reindex', { + const startReindex = (await callAsUser('reindex', { refresh: true, waitForCompletion: false, body: { @@ -337,7 +337,7 @@ export const reindexServiceFactory = ( const taskId = reindexOp.attributes.reindexTaskId; // Check reindexing task progress - const taskResponse = await callCluster('tasks.get', { + const taskResponse = await callAsUser('tasks.get', { taskId, waitForCompletion: false, }); @@ -358,7 +358,7 @@ export const reindexServiceFactory = ( reindexOp = await cleanupChanges(reindexOp); } else { // Check that it reindexed all documents - const { count } = await callCluster('count', { index: reindexOp.attributes.indexName }); + const { count } = await callAsUser('count', { index: reindexOp.attributes.indexName }); if (taskResponse.task.status.created < count) { // Include the entire task result in the error message. This should be guaranteed @@ -374,7 +374,7 @@ export const reindexServiceFactory = ( } // Delete the task from ES .tasks index - const deleteTaskResp = await callCluster('delete', { + const deleteTaskResp = await callAsUser('delete', { index: '.tasks', id: taskId, }); @@ -394,7 +394,7 @@ export const reindexServiceFactory = ( const { indexName, newIndexName } = reindexOp.attributes; const existingAliases = ( - await callCluster('indices.getAlias', { + await callAsUser('indices.getAlias', { index: indexName, }) )[indexName].aliases; @@ -403,7 +403,7 @@ export const reindexServiceFactory = ( add: { index: newIndexName, alias: aliasName, ...existingAliases[aliasName] }, })); - const aliasResponse = await callCluster('indices.updateAliases', { + const aliasResponse = await callAsUser('indices.updateAliases', { body: { actions: [ { add: { index: newIndexName, alias: indexName } }, @@ -443,9 +443,18 @@ export const reindexServiceFactory = ( return { async hasRequiredPrivileges(indexName: string) { + /** + * To avoid a circular dependency on Security we use a work around + * here to detect whether Security is available and enabled + * (i.e., via the licensing plugin). This enables Security to use + * functionality exposed through Upgrade Assistant. + */ + const license = await licensing.license$.pipe(first()).toPromise(); + + const securityFeature = license.getFeature('security'); + // If security is disabled or unavailable, return true. - const security = xpackInfo.feature('security'); - if (!security.isAvailable() || !security.isEnabled()) { + if (!securityFeature || !(securityFeature.isAvailable && securityFeature.isEnabled)) { return true; } @@ -482,7 +491,7 @@ export const reindexServiceFactory = ( body.cluster = [...body.cluster, 'manage_watcher']; } - const resp = await callCluster('transport.request', { + const resp = await callAsUser('transport.request', { path: '/_security/user/_has_privileges', method: 'POST', body, @@ -509,7 +518,7 @@ export const reindexServiceFactory = ( }, async createReindexOperation(indexName: string) { - const indexExists = await callCluster('indices.exists', { index: indexName }); + const indexExists = await callAsUser('indices.exists', { index: indexName }); if (!indexExists) { throw Boom.notFound(`Index ${indexName} does not exist in this cluster.`); } @@ -579,10 +588,7 @@ export const reindexServiceFactory = ( break; } } catch (e) { - log( - ['upgrade_assistant', 'error'], - `Reindexing step failed: ${e instanceof Error ? e.stack : e.toString()}` - ); + log.error(`Reindexing step failed: ${e instanceof Error ? e.stack : e.toString()}`); // Trap the exception and add the message to the object so the UI can display it. lockedReindexOp = await actions.updateReindexOp(lockedReindexOp, { @@ -647,7 +653,7 @@ export const reindexServiceFactory = ( throw new Error(`Reindex operation is not current waiting for reindex task to complete`); } - const resp = await callCluster('tasks.cancel', { + const resp = await callAsUser('tasks.cancel', { taskId: reindexOp.attributes.reindexTaskId, }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/types.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/types.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts similarity index 76% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts index 628a47be9f5e7..bad6db62efe41 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts @@ -3,23 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster, CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; -import { Request, Server } from 'src/legacy/server/kbn_server'; -import { SavedObjectsClientContract } from 'kibana/server'; - +import { IClusterClient, Logger, SavedObjectsClientContract, FakeRequest } from 'src/core/server'; import moment from 'moment'; -import { XPackInfo } from '../../../../../xpack_main/server/lib/xpack_info'; -import { ReindexSavedObject, ReindexStatus } from '../../../../common/types'; + +import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { CredentialStore } from './credential_store'; import { reindexActionsFactory } from './reindex_actions'; import { ReindexService, reindexServiceFactory } from './reindex_service'; +import { LicensingPluginSetup } from '../../../../licensing/server'; const POLL_INTERVAL = 30000; // If no nodes have been able to update this index in 2 minutes (due to missing credentials), set to paused. const PAUSE_WINDOW = POLL_INTERVAL * 4; -const LOG_TAGS = ['upgrade_assistant', 'reindex_worker']; - /** * A singleton worker that will coordinate two polling loops: * (1) A longer loop that polls for reindex operations that are in progress. If any are found, loop (2) is started. @@ -41,24 +37,27 @@ export class ReindexWorker { private timeout?: NodeJS.Timeout; private inProgressOps: ReindexSavedObject[] = []; private readonly reindexService: ReindexService; + private readonly log: Logger; constructor( private client: SavedObjectsClientContract, private credentialStore: CredentialStore, - private callWithRequest: CallClusterWithRequest, - private callWithInternalUser: CallCluster, - private xpackInfo: XPackInfo, - private readonly log: Server['log'] + private clusterClient: IClusterClient, + log: Logger, + private licensing: LicensingPluginSetup ) { + this.log = log.get('reindex_worker'); if (ReindexWorker.workerSingleton) { throw new Error(`More than one ReindexWorker cannot be created.`); } + const callAsInternalUser = this.clusterClient.callAsInternalUser.bind(this.clusterClient); + this.reindexService = reindexServiceFactory( - this.callWithInternalUser, - this.xpackInfo, - reindexActionsFactory(this.client, this.callWithInternalUser), - this.log + callAsInternalUser, + reindexActionsFactory(this.client, callAsInternalUser), + log, + this.licensing ); ReindexWorker.workerSingleton = this; @@ -68,7 +67,7 @@ export class ReindexWorker { * Begins loop (1) to begin checking for in progress reindex operations. */ public start = () => { - this.log(['debug', ...LOG_TAGS], `Starting worker...`); + this.log.debug('Starting worker...'); this.continuePolling = true; this.pollForOperations(); }; @@ -77,7 +76,7 @@ export class ReindexWorker { * Stops the worker from processing any further reindex operations. */ public stop = () => { - this.log(['debug', ...LOG_TAGS], `Stopping worker...`); + this.log.debug('Stopping worker...'); if (this.timeout) { clearTimeout(this.timeout); } @@ -107,7 +106,7 @@ export class ReindexWorker { this.updateOperationLoopRunning = true; while (this.inProgressOps.length > 0) { - this.log(['debug', ...LOG_TAGS], `Updating ${this.inProgressOps.length} reindex operations`); + this.log.debug(`Updating ${this.inProgressOps.length} reindex operations`); // Push each operation through the state machine and refresh. await Promise.all(this.inProgressOps.map(this.processNextStep)); @@ -118,7 +117,7 @@ export class ReindexWorker { }; private pollForOperations = async () => { - this.log(['debug', ...LOG_TAGS], `Polling for reindex operations`); + this.log.debug(`Polling for reindex operations`); await this.refresh(); @@ -131,7 +130,7 @@ export class ReindexWorker { try { this.inProgressOps = await this.reindexService.findAllByStatus(ReindexStatus.inProgress); } catch (e) { - this.log(['debug', ...LOG_TAGS], `Could not fetch riendex operations from Elasticsearch`); + this.log.debug(`Could not fetch reindex operations from Elasticsearch`); this.inProgressOps = []; } @@ -159,10 +158,13 @@ export class ReindexWorker { } // Setup a ReindexService specific to these credentials. - const fakeRequest = { headers: credential } as Request; - const callCluster = this.callWithRequest.bind(null, fakeRequest) as CallCluster; - const actions = reindexActionsFactory(this.client, callCluster); - const service = reindexServiceFactory(callCluster, this.xpackInfo, actions, this.log); + const fakeRequest: FakeRequest = { headers: credential }; + + const scopedClusterClient = this.clusterClient.asScoped(fakeRequest); + const callAsCurrentUser = scopedClusterClient.callAsCurrentUser.bind(scopedClusterClient); + const actions = reindexActionsFactory(this.client, callAsCurrentUser); + + const service = reindexServiceFactory(callAsCurrentUser, actions, this.log, this.licensing); reindexOp = await swallowExceptions(service.processNextStep, this.log)(reindexOp); // Update credential store with most recent state. @@ -176,18 +178,15 @@ export class ReindexWorker { */ const swallowExceptions = ( func: (reindexOp: ReindexSavedObject) => Promise, - log: Server['log'] + log: Logger ) => async (reindexOp: ReindexSavedObject) => { try { return await func(reindexOp); } catch (e) { if (reindexOp.attributes.locked) { - log(['debug', ...LOG_TAGS], `Skipping reindexOp with unexpired lock: ${reindexOp.id}`); + log.debug(`Skipping reindexOp with unexpired lock: ${reindexOp.id}`); } else { - log( - ['warning', ...LOG_TAGS], - `Error when trying to process reindexOp (${reindexOp.id}): ${e.toString()}` - ); + log.warn(`Error when trying to process reindexOp (${reindexOp.id}): ${e.toString()}`); } return reindexOp; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts similarity index 51% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts index 5f95f6e9fd555..703351c45ba5a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../../common/types'; import { upsertUIOpenOption } from './es_ui_open_apis'; /** @@ -13,54 +14,29 @@ import { upsertUIOpenOption } from './es_ui_open_apis'; * more thoroughly in the lib/telemetry tests. */ describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { - const mockIncrementCounter = jest.fn(); - const server = jest.fn().mockReturnValue({ - savedObjects: { - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - incrementCounter: mockIncrementCounter, - }; - }), - }, - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithInternalUser: {}, - }; - }, - }, - }, - }); - - const request = jest.fn().mockReturnValue({ - payload: { - overview: true, - cluster: true, - indices: true, - }, - }); - describe('Upsert UIOpen Option', () => { it('call saved objects internal repository with the correct info', async () => { - const serverMock = server(); - const incCounterSORepoFunc = serverMock.savedObjects.getSavedObjectsRepository() - .incrementCounter; + const internalRepo = savedObjectsRepositoryMock.create(); - await upsertUIOpenOption(serverMock, request()); + await upsertUIOpenOption({ + overview: true, + cluster: true, + indices: true, + savedObjects: { createInternalRepository: () => internalRepo } as any, + }); - expect(incCounterSORepoFunc).toHaveBeenCalledTimes(3); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3); + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_open.overview` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_open.cluster` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_open.indices` diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts new file mode 100644 index 0000000000000..64e9b0f217555 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts @@ -0,0 +1,58 @@ +/* + * 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 { SavedObjectsServiceStart } from 'src/core/server'; +import { + UIOpen, + UIOpenOption, + UPGRADE_ASSISTANT_DOC_ID, + UPGRADE_ASSISTANT_TYPE, +} from '../../../common/types'; + +interface IncrementUIOpenDependencies { + uiOpenOptionCounter: UIOpenOption; + savedObjects: SavedObjectsServiceStart; +} + +async function incrementUIOpenOptionCounter({ + savedObjects, + uiOpenOptionCounter, +}: IncrementUIOpenDependencies) { + const internalRepository = savedObjects.createInternalRepository(); + + await internalRepository.incrementCounter( + UPGRADE_ASSISTANT_TYPE, + UPGRADE_ASSISTANT_DOC_ID, + `ui_open.${uiOpenOptionCounter}` + ); +} + +type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; + +export async function upsertUIOpenOption({ + overview, + cluster, + indices, + savedObjects, +}: UpsertUIOpenOptionDependencies): Promise { + if (overview) { + await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' }); + } + + if (cluster) { + await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'cluster' }); + } + + if (indices) { + await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'indices' }); + } + + return { + overview, + cluster, + indices, + }; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts similarity index 52% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts index 3f2c80f7d6b75..31e4e3f07b5de 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../../common/types'; +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; import { upsertUIReindexOption } from './es_ui_reindex_apis'; /** @@ -13,60 +13,34 @@ import { upsertUIReindexOption } from './es_ui_reindex_apis'; * more thoroughly in the lib/telemetry tests. */ describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { - const mockIncrementCounter = jest.fn(); - const server = jest.fn().mockReturnValue({ - savedObjects: { - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - incrementCounter: mockIncrementCounter, - }; - }), - }, - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithInternalUser: {}, - }; - }, - }, - }, - }); - - const request = jest.fn().mockReturnValue({ - payload: { - close: true, - open: true, - start: true, - stop: true, - }, - }); - describe('Upsert UIReindex Option', () => { it('call saved objects internal repository with the correct info', async () => { - const serverMock = server(); - const incCounterSORepoFunc = serverMock.savedObjects.getSavedObjectsRepository() - .incrementCounter; - - await upsertUIReindexOption(serverMock, request()); + const internalRepo = savedObjectsRepositoryMock.create(); + await upsertUIReindexOption({ + close: true, + open: true, + start: true, + stop: true, + savedObjects: { createInternalRepository: () => internalRepo } as any, + }); - expect(incCounterSORepoFunc).toHaveBeenCalledTimes(4); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4); + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.close` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.open` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.start` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.stop` diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts new file mode 100644 index 0000000000000..0aaaf63196d67 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts @@ -0,0 +1,64 @@ +/* + * 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 { SavedObjectsServiceStart } from 'src/core/server'; +import { + UIReindex, + UIReindexOption, + UPGRADE_ASSISTANT_DOC_ID, + UPGRADE_ASSISTANT_TYPE, +} from '../../../common/types'; + +interface IncrementUIReindexOptionDependencies { + uiReindexOptionCounter: UIReindexOption; + savedObjects: SavedObjectsServiceStart; +} + +async function incrementUIReindexOptionCounter({ + savedObjects, + uiReindexOptionCounter, +}: IncrementUIReindexOptionDependencies) { + const internalRepository = savedObjects.createInternalRepository(); + + await internalRepository.incrementCounter( + UPGRADE_ASSISTANT_TYPE, + UPGRADE_ASSISTANT_DOC_ID, + `ui_reindex.${uiReindexOptionCounter}` + ); +} + +type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; + +export async function upsertUIReindexOption({ + start, + close, + open, + stop, + savedObjects, +}: UpsertUIReindexOptionDepencies): Promise { + if (close) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'close' }); + } + + if (open) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'open' }); + } + + if (start) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'start' }); + } + + if (stop) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'stop' }); + } + + return { + close, + open, + start, + stop, + }; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/index.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts similarity index 78% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 27a0eef0d16f6..a4833d9a3d7fe 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { registerUpgradeAssistantUsageCollector } from './usage_collector'; +import { IClusterClient } from 'src/core/server'; /** * Since these route callbacks are so thin, these serve simply as integration tests @@ -14,20 +15,31 @@ import { registerUpgradeAssistantUsageCollector } from './usage_collector'; describe('Upgrade Assistant Usage Collector', () => { let makeUsageCollectorStub: any; let registerStub: any; - let server: any; + let dependencies: any; let callClusterStub: any; let usageCollection: any; + let clusterClient: IClusterClient; beforeEach(() => { + clusterClient = elasticsearchServiceMock.createClusterClient(); + (clusterClient.callAsInternalUser as jest.Mock).mockResolvedValue({ + persistent: {}, + transient: { + logger: { + deprecation: 'WARN', + }, + }, + }); makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); usageCollection = { makeUsageCollector: makeUsageCollectorStub, registerCollector: registerStub, }; - server = jest.fn().mockReturnValue({ + dependencies = { + usageCollection, savedObjects: { - getSavedObjectsRepository: jest.fn().mockImplementation(() => { + createInternalRepository: jest.fn().mockImplementation(() => { return { get: () => { return { @@ -45,31 +57,26 @@ describe('Upgrade Assistant Usage Collector', () => { }; }), }, - }); - callClusterStub = jest.fn().mockResolvedValue({ - persistent: {}, - transient: { - logger: { - deprecation: 'WARN', - }, + elasticsearch: { + adminClient: clusterClient, }, - }); + }; }); describe('registerUpgradeAssistantUsageCollector', () => { it('should registerCollector', () => { - registerUpgradeAssistantUsageCollector(usageCollection, server()); + registerUpgradeAssistantUsageCollector(dependencies); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = upgrade-assistant', () => { - registerUpgradeAssistantUsageCollector(usageCollection, server()); + registerUpgradeAssistantUsageCollector(dependencies); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('upgrade-assistant-telemetry'); }); it('fetchUpgradeAssistantMetrics should return correct info', async () => { - registerUpgradeAssistantUsageCollector(usageCollection, server()); + registerUpgradeAssistantUsageCollector(dependencies); const upgradeAssistantStats = await makeUsageCollectorStub.mock.calls[0][0].fetch( callClusterStub ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts similarity index 72% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 1d24d190fa9f2..79d6e53c64ec0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -5,8 +5,12 @@ */ import { set } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsRepository } from 'src/core/server/saved_objects/service/lib/repository'; +import { + APICaller, + ElasticsearchServiceSetup, + ISavedObjectsRepository, + SavedObjectsServiceStart, +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { UPGRADE_ASSISTANT_DOC_ID, @@ -14,12 +18,11 @@ import { UpgradeAssistantTelemetry, UpgradeAssistantTelemetrySavedObject, UpgradeAssistantTelemetrySavedObjectAttributes, -} from '../../../../common/types'; -import { ServerShim } from '../../types'; +} from '../../../common/types'; import { isDeprecationLoggingEnabled } from '../es_deprecation_logging_apis'; async function getSavedObjectAttributesFromRepo( - savedObjectsRepository: SavedObjectsRepository, + savedObjectsRepository: ISavedObjectsRepository, docType: string, docID: string ) { @@ -35,9 +38,9 @@ async function getSavedObjectAttributesFromRepo( } } -async function getDeprecationLoggingStatusValue(callCluster: any): Promise { +async function getDeprecationLoggingStatusValue(callAsCurrentUser: APICaller): Promise { try { - const loggerDeprecationCallResult = await callCluster('cluster.getSettings', { + const loggerDeprecationCallResult = await callAsCurrentUser('cluster.getSettings', { includeDefaults: true, }); @@ -48,17 +51,17 @@ async function getDeprecationLoggingStatusValue(callCluster: any): Promise { - const { getSavedObjectsRepository } = server.savedObjects; - const savedObjectsRepository = getSavedObjectsRepository(callCluster); + const savedObjectsRepository = savedObjects.createInternalRepository(); const upgradeAssistantSOAttributes = await getSavedObjectAttributesFromRepo( savedObjectsRepository, UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID ); - const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callCluster); + const callAsInternalUser = adminClient.callAsInternalUser.bind(adminClient); + const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callAsInternalUser); const getTelemetrySavedObject = ( upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null @@ -103,14 +106,21 @@ export async function fetchUpgradeAssistantMetrics( }; } -export function registerUpgradeAssistantUsageCollector( - usageCollection: UsageCollectionSetup, - server: ServerShim -) { +interface Dependencies { + elasticsearch: ElasticsearchServiceSetup; + savedObjects: SavedObjectsServiceStart; + usageCollection: UsageCollectionSetup; +} + +export function registerUpgradeAssistantUsageCollector({ + elasticsearch, + usageCollection, + savedObjects, +}: Dependencies) { const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ type: UPGRADE_ASSISTANT_TYPE, isReady: () => true, - fetch: async (callCluster: any) => fetchUpgradeAssistantMetrics(callCluster, server), + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), }); usageCollection.registerCollector(upgradeAssistantUsageCollector); diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts new file mode 100644 index 0000000000000..6ccd073a9e020 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -0,0 +1,125 @@ +/* + * 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { + Plugin, + CoreSetup, + CoreStart, + PluginInitializerContext, + Logger, + ElasticsearchServiceSetup, + SavedObjectsClient, + SavedObjectsServiceStart, +} from '../../../../src/core/server'; + +import { CloudSetup } from '../../cloud/server'; +import { LicensingPluginSetup } from '../../licensing/server'; + +import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; +import { ReindexWorker } from './lib/reindexing'; +import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; +import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; +import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; +import { registerReindexIndicesRoutes, createReindexWorker } from './routes/reindex_indices'; +import { registerTelemetryRoutes } from './routes/telemetry'; +import { RouteDependencies } from './types'; + +interface PluginsSetup { + usageCollection: UsageCollectionSetup; + licensing: LicensingPluginSetup; + cloud?: CloudSetup; +} + +export class UpgradeAssistantServerPlugin implements Plugin { + private readonly logger: Logger; + private readonly credentialStore: CredentialStore; + + // Properties set at setup + private licensing?: LicensingPluginSetup; + private elasticSearchService?: ElasticsearchServiceSetup; + + // Properties set at start + private savedObjectsServiceStart?: SavedObjectsServiceStart; + private worker?: ReindexWorker; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.credentialStore = credentialStoreFactory(); + } + + private getWorker() { + if (!this.worker) { + throw new Error('Worker unavailable'); + } + return this.worker; + } + + setup( + { http, elasticsearch, getStartServices, capabilities }: CoreSetup, + { usageCollection, cloud, licensing }: PluginsSetup + ) { + this.elasticSearchService = elasticsearch; + this.licensing = licensing; + + const router = http.createRouter(); + + const dependencies: RouteDependencies = { + cloud, + router, + credentialStore: this.credentialStore, + log: this.logger, + getSavedObjectsService: () => { + if (!this.savedObjectsServiceStart) { + throw new Error('Saved Objects Start service not available'); + } + return this.savedObjectsServiceStart; + }, + licensing, + }; + + registerClusterCheckupRoutes(dependencies); + registerDeprecationLoggingRoutes(dependencies); + registerReindexIndicesRoutes(dependencies, this.getWorker.bind(this)); + // Bootstrap the needed routes and the collector for the telemetry + registerTelemetryRoutes(dependencies); + + if (usageCollection) { + getStartServices().then(([{ savedObjects }]) => { + registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects }); + }); + } + } + + start({ savedObjects }: CoreStart) { + this.savedObjectsServiceStart = savedObjects; + + // The ReindexWorker uses a map of request headers that contain the authentication credentials + // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not + // want to expose these credentials to any unauthenticated users. We also want to avoid any need + // to add a user for a special index just for upgrading. This in-memory cache allows us to + // process jobs without the browser staying on the page, but will require that jobs go into + // a paused state if no Kibana nodes have the required credentials. + + this.worker = createReindexWorker({ + credentialStore: this.credentialStore, + licensing: this.licensing!, + elasticsearchService: this.elasticSearchService!, + logger: this.logger, + savedObjects: new SavedObjectsClient( + this.savedObjectsServiceStart.createInternalRepository() + ), + }); + + this.worker.start(); + } + + stop(): void { + if (this.worker) { + this.worker.stop(); + } + } +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts similarity index 92% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts rename to x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts index d09a66dbb4326..fb68e188bb255 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts @@ -6,7 +6,7 @@ export const createRequestMock = (opts?: { headers?: any; params?: Record; - payload?: Record; + body?: Record; }) => { return Object.assign({ headers: {} }, opts || {}); }; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts similarity index 72% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts rename to x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts index 3769bc389123e..a8be171dccd98 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts @@ -3,7 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { RequestHandler, RequestHandlerContext } from 'src/core/server'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../src/core/server/mocks'; + +export const routeHandlerContextMock = ({ + core: { + elasticsearch: { + adminClient: elasticsearchServiceMock.createScopedClusterClient(), + dataClient: elasticsearchServiceMock.createScopedClusterClient(), + }, + savedObjects: { client: savedObjectsClientMock.create() }, + }, +} as unknown) as RequestHandlerContext; /** * Creates a very crude mock of the new platform router implementation. This enables use to test diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts similarity index 73% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts index 3fe2e1797182b..16f8001f8e1de 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; jest.mock('../lib/es_version_precheck', () => ({ @@ -24,32 +24,22 @@ import { registerClusterCheckupRoutes } from './cluster_checkup'; * more thoroughly in the es_migration_apis test. */ describe('cluster checkup API', () => { - afterEach(() => jest.clearAllMocks()); - let mockRouter: MockRouter; - let serverShim: any; - let ctxMock: any; - let mockPluginsSetup: any; + let routeDependencies: any; beforeEach(() => { mockRouter = createMockRouter(); - mockPluginsSetup = { + routeDependencies = { cloud: { isCloudEnabled: true, }, - }; - ctxMock = { - core: {}, - }; - serverShim = { router: mockRouter, - plugins: { - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - }, }; - registerClusterCheckupRoutes(serverShim, mockPluginsSetup); + registerClusterCheckupRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); }); describe('with cloud enabled', () => { @@ -62,11 +52,11 @@ describe('cluster checkup API', () => { nodes: [], }); - await serverShim.router.getHandler({ + await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); - expect(spy.mock.calls[0][2]).toBe(true); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + expect(spy.mock.calls[0][1]).toBe(true); }); }); @@ -77,10 +67,10 @@ describe('cluster checkup API', () => { indices: [], nodes: [], }); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(200); expect(JSON.stringify(resp.payload)).toMatchInlineSnapshot( @@ -93,10 +83,10 @@ describe('cluster checkup API', () => { e.status = 403; MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(e); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(403); }); @@ -104,10 +94,10 @@ describe('cluster checkup API', () => { it('returns an 500 error if it throws', async () => { MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(500); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts new file mode 100644 index 0000000000000..22a121ab78683 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerClusterCheckupRoutes({ cloud, router }: RouteDependencies) { + const isCloudEnabled = Boolean(cloud?.isCloudEnabled); + + router.get( + { + path: '/api/upgrade_assistant/status', + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + try { + return response.ok({ + body: await getUpgradeAssistantStatus(dataClient, isCloudEnabled), + }); + } catch (e) { + if (e.status === 403) { + return response.forbidden(e.message); + } + + return response.internalError({ body: e }); + } + } + ) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts similarity index 56% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index d663361956374..845a0238f7918 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -5,7 +5,7 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; jest.mock('../lib/es_version_precheck', () => ({ @@ -21,42 +21,42 @@ import { registerDeprecationLoggingRoutes } from './deprecation_logging'; */ describe('deprecation logging API', () => { let mockRouter: MockRouter; - let serverShim: any; - let callWithRequest: any; - const ctxMock: any = {}; + let routeDependencies: any; beforeEach(() => { mockRouter = createMockRouter(); - callWithRequest = jest.fn(); - serverShim = { + routeDependencies = { router: mockRouter, - plugins: { - elasticsearch: { - getCluster: () => ({ callWithRequest } as any), - } as any, - }, }; - registerDeprecationLoggingRoutes(serverShim); + registerDeprecationLoggingRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); }); describe('GET /api/upgrade_assistant/deprecation_logging', () => { it('returns isEnabled', async () => { - callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'WARN' } } }); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockResolvedValue({ + default: { logger: { deprecation: 'WARN' } }, + }); + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(200); expect(resp.payload).toEqual({ isEnabled: true }); }); it('returns an error if it throws', async () => { - callWithRequest.mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(500); }); @@ -64,21 +64,25 @@ describe('deprecation logging API', () => { describe('PUT /api/upgrade_assistant/deprecation_logging', () => { it('returns isEnabled', async () => { - callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'ERROR' } } }); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockResolvedValue({ + default: { logger: { deprecation: 'ERROR' } }, + }); + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.payload).toEqual({ isEnabled: false }); }); it('returns an error if it throws', async () => { - callWithRequest.mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, { body: { isEnabled: false } }, kibanaResponseFactory); + })(routeHandlerContextMock, { body: { isEnabled: false } }, kibanaResponseFactory); expect(resp.status).toEqual(500); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts new file mode 100644 index 0000000000000..739a789c95ce0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { + getDeprecationLoggingStatus, + setDeprecationLogging, +} from '../lib/es_deprecation_logging_apis'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) { + router.get( + { + path: '/api/upgrade_assistant/deprecation_logging', + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + try { + const result = await getDeprecationLoggingStatus(dataClient); + return response.ok({ body: result }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ) + ); + + router.put( + { + path: '/api/upgrade_assistant/deprecation_logging', + validate: { + body: schema.object({ + isEnabled: schema.boolean(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + try { + const { isEnabled } = request.body as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(dataClient, isEnabled), + }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts similarity index 80% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts index d520324239656..695bb6304cfdf 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { licensingMock } from '../../../licensing/server/mocks'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; const mockReindexService = { @@ -31,12 +31,7 @@ jest.mock('../lib/reindexing', () => { }; }); -import { - IndexGroup, - ReindexSavedObject, - ReindexStatus, - ReindexWarning, -} from '../../../common/types'; +import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexWarning } from '../../common/types'; import { credentialStoreFactory } from '../lib/reindexing/credential_store'; import { registerReindexIndicesRoutes } from './reindex_indices'; @@ -46,9 +41,8 @@ import { registerReindexIndicesRoutes } from './reindex_indices'; * more thoroughly in the es_migration_apis test. */ describe('reindex API', () => { - let serverShim: any; + let routeDependencies: any; let mockRouter: MockRouter; - let ctxMock: any; const credentialStore = credentialStoreFactory(); const worker = { @@ -57,24 +51,13 @@ describe('reindex API', () => { } as any; beforeEach(() => { - ctxMock = { - core: { - savedObjects: savedObjectsClientMock.create(), - }, - }; mockRouter = createMockRouter(); - serverShim = { + routeDependencies = { + credentialStore, router: mockRouter, - plugins: { - xpack_main: { - info: jest.fn(), - }, - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - }, + licensing: licensingMock.createSetup(), }; - registerReindexIndicesRoutes(serverShim, worker, credentialStore); + registerReindexIndicesRoutes(routeDependencies, () => worker); mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); mockReindexService.detectReindexWarnings.mockReset(); @@ -92,7 +75,9 @@ describe('reindex API', () => { credentialStore.clear(); }); - afterEach(() => jest.clearAllMocks()); + afterEach(() => { + jest.resetAllMocks(); + }); describe('GET /api/upgrade_assistant/reindex/{indexName}', () => { it('returns the attributes of the reindex operation and reindex warnings', async () => { @@ -101,10 +86,14 @@ describe('reindex API', () => { }); mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ReindexWarning.allField]); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'wowIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'wowIndex' } }), + kibanaResponseFactory + ); // It called into the service correctly expect(mockReindexService.findReindexOperation).toHaveBeenCalledWith('wowIndex'); @@ -121,10 +110,14 @@ describe('reindex API', () => { mockReindexService.findReindexOperation.mockResolvedValueOnce(null); mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'anIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'anIndex' } }), + kibanaResponseFactory + ); expect(resp.status).toEqual(200); const data = resp.payload; @@ -137,10 +130,14 @@ describe('reindex API', () => { mockReindexService.detectReindexWarnings.mockResolvedValueOnce([]); mockReindexService.getIndexGroup.mockReturnValue(IndexGroup.ml); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'anIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'anIndex' } }), + kibanaResponseFactory + ); expect(resp.status).toEqual(200); const data = resp.payload; @@ -154,10 +151,14 @@ describe('reindex API', () => { attributes: { indexName: 'theIndex' }, }); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'theIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'theIndex' } }), + kibanaResponseFactory + ); // It called create correctly expect(mockReindexService.createReindexOperation).toHaveBeenCalledWith('theIndex'); @@ -173,10 +174,14 @@ describe('reindex API', () => { attributes: { indexName: 'theIndex' }, }); - await serverShim.router.getHandler({ + await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'theIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'theIndex' } }), + kibanaResponseFactory + ); expect(worker.forceRefresh).toHaveBeenCalled(); }); @@ -187,11 +192,11 @@ describe('reindex API', () => { } as ReindexSavedObject; mockReindexService.createReindexOperation.mockResolvedValueOnce(reindexOp); - await serverShim.router.getHandler({ + await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ headers: { 'kbn-auth-x': 'HERE!', @@ -212,11 +217,11 @@ describe('reindex API', () => { attributes: { indexName: 'theIndex', status: ReindexStatus.inProgress }, }); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ params: { indexName: 'theIndex' }, }), @@ -235,11 +240,11 @@ describe('reindex API', () => { it('returns a 403 if required privileges fails', async () => { mockReindexService.hasRequiredPrivileges.mockResolvedValueOnce(false); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ params: { indexName: 'theIndex' }, }), @@ -254,11 +259,11 @@ describe('reindex API', () => { it('returns a 501', async () => { mockReindexService.cancelReindexing.mockResolvedValueOnce({}); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}/cancel', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ params: { indexName: 'cancelMe' }, }), diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts new file mode 100644 index 0000000000000..a910145474061 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { Logger, ElasticsearchServiceSetup, SavedObjectsClient } from 'src/core/server'; +import { ReindexStatus } from '../../common/types'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; +import { CredentialStore } from '../lib/reindexing/credential_store'; +import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; +import { RouteDependencies } from '../types'; +import { LicensingPluginSetup } from '../../../licensing/server'; + +interface CreateReindexWorker { + logger: Logger; + elasticsearchService: ElasticsearchServiceSetup; + credentialStore: CredentialStore; + savedObjects: SavedObjectsClient; + licensing: LicensingPluginSetup; +} + +export function createReindexWorker({ + logger, + elasticsearchService, + credentialStore, + savedObjects, + licensing, +}: CreateReindexWorker) { + const { adminClient } = elasticsearchService; + return new ReindexWorker(savedObjects, credentialStore, adminClient, logger, licensing); +} + +export function registerReindexIndicesRoutes( + { credentialStore, router, licensing, log }: RouteDependencies, + getWorker: () => ReindexWorker +) { + const BASE_PATH = '/api/upgrade_assistant/reindex'; + + // Start reindex for an index + router.post( + { + path: `${BASE_PATH}/{indexName}`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { indexName } = request.params as any; + const { client } = savedObjects; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + const reindexService = reindexServiceFactory( + callAsCurrentUser, + reindexActions, + log, + licensing + ); + + try { + if (!(await reindexService.hasRequiredPrivileges(indexName))) { + return response.forbidden({ + body: `You do not have adequate privileges to reindex this index.`, + }); + } + + const existingOp = await reindexService.findReindexOperation(indexName); + + // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. + const reindexOp = + existingOp && existingOp.attributes.status === ReindexStatus.paused + ? await reindexService.resumeReindexOperation(indexName) + : await reindexService.createReindexOperation(indexName); + + // Add users credentials for the worker to use + credentialStore.set(reindexOp, request.headers); + + // Kick the worker on this node to immediately pickup the new reindex operation. + getWorker().forceRefresh(); + + return response.ok({ body: reindexOp.attributes }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ) + ); + + // Get status + router.get( + { + path: `${BASE_PATH}/{indexName}`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { client } = savedObjects; + const { indexName } = request.params as any; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + const reindexService = reindexServiceFactory( + callAsCurrentUser, + reindexActions, + log, + licensing + ); + + try { + const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); + const reindexOp = await reindexService.findReindexOperation(indexName); + // If the user doesn't have privileges than querying for warnings is going to fail. + const warnings = hasRequiredPrivileges + ? await reindexService.detectReindexWarnings(indexName) + : []; + const indexGroup = reindexService.getIndexGroup(indexName); + + return response.ok({ + body: { + reindexOp: reindexOp ? reindexOp.attributes : null, + warnings, + indexGroup, + hasRequiredPrivileges, + }, + }); + } catch (e) { + if (!e.isBoom) { + return response.internalError({ body: e }); + } + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.statusCode, + }); + } + } + ) + ); + + // Cancel reindex + router.post( + { + path: `${BASE_PATH}/{indexName}/cancel`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { indexName } = request.params as any; + const { client } = savedObjects; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + const reindexService = reindexServiceFactory( + callAsCurrentUser, + reindexActions, + log, + licensing + ); + + try { + await reindexService.cancelReindexing(indexName); + + return response.ok({ body: { acknowledged: true } }); + } catch (e) { + if (!e.isBoom) { + return response.internalError({ body: e }); + } + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.statusCode, + }); + } + } + ) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts similarity index 79% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index 582c75e3701b6..b2b8ccf1ca57a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -5,7 +5,8 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ @@ -26,24 +27,15 @@ import { registerTelemetryRoutes } from './telemetry'; * more thoroughly in the lib/telemetry tests. */ describe('Upgrade Assistant Telemetry API', () => { - let serverShim: any; + let routeDependencies: any; let mockRouter: MockRouter; - let ctxMock: any; beforeEach(() => { - ctxMock = {}; mockRouter = createMockRouter(); - serverShim = { + routeDependencies = { + getSavedObjectsService: () => savedObjectsServiceMock.create(), router: mockRouter, - plugins: { - xpack_main: { - info: jest.fn(), - }, - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - }, }; - registerTelemetryRoutes(serverShim); + registerTelemetryRoutes(routeDependencies); }); afterEach(() => jest.clearAllMocks()); @@ -57,10 +49,14 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_open', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ body: returnPayload }), + kibanaResponseFactory + ); expect(resp.payload).toEqual(returnPayload); }); @@ -74,13 +70,13 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_open', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { overview: true, cluster: true, indices: true, @@ -95,13 +91,13 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_open', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { overview: false, }, }), @@ -123,13 +119,13 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { overview: false, }, }), @@ -149,13 +145,13 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { close: true, open: true, start: true, @@ -171,13 +167,13 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { start: false, }, }), diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts similarity index 66% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts rename to x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index f08c49809033d..900a5e64c55c3 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -7,11 +7,10 @@ import { schema } from '@kbn/config-schema'; import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { ServerShimWithRouter } from '../types'; -import { createRequestShim } from './create_request_shim'; +import { RouteDependencies } from '../types'; -export function registerTelemetryRoutes(server: ServerShimWithRouter) { - server.router.put( +export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { + router.put( { path: '/api/upgrade_assistant/telemetry/ui_open', validate: { @@ -23,16 +22,23 @@ export function registerTelemetryRoutes(server: ServerShimWithRouter) { }, }, async (ctx, request, response) => { - const reqShim = createRequestShim(request); + const { cluster, indices, overview } = request.body; try { - return response.ok({ body: await upsertUIOpenOption(server, reqShim) }); + return response.ok({ + body: await upsertUIOpenOption({ + savedObjects: getSavedObjectsService(), + cluster, + indices, + overview, + }), + }); } catch (e) { return response.internalError({ body: e }); } } ); - server.router.put( + router.put( { path: '/api/upgrade_assistant/telemetry/ui_reindex', validate: { @@ -45,9 +51,17 @@ export function registerTelemetryRoutes(server: ServerShimWithRouter) { }, }, async (ctx, request, response) => { - const reqShim = createRequestShim(request); + const { close, open, start, stop } = request.body; try { - return response.ok({ body: await upsertUIReindexOption(server, reqShim) }); + return response.ok({ + body: await upsertUIReindexOption({ + savedObjects: getSavedObjectsService(), + close, + open, + start, + stop, + }), + }); } catch (e) { return response.internalError({ body: e }); } diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts new file mode 100644 index 0000000000000..3f3beadd2f333 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/types.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 { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server'; +import { CloudSetup } from '../../cloud/server'; +import { CredentialStore } from './lib/reindexing/credential_store'; +import { LicensingPluginSetup } from '../../licensing/server'; + +export interface RouteDependencies { + router: IRouter; + credentialStore: CredentialStore; + log: Logger; + getSavedObjectsService: () => SavedObjectsServiceStart; + licensing: LicensingPluginSetup; + cloud?: CloudSetup; +} diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js index 9fc8078162847..38fc1f0c6356f 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js @@ -6,10 +6,7 @@ import expect from '@kbn/expect'; -import { - ReindexStatus, - REINDEX_OP_TYPE, -} from '../../../legacy/plugins/upgrade_assistant/common/types'; +import { ReindexStatus, REINDEX_OP_TYPE } from '../../../plugins/upgrade_assistant/common/types'; export default function({ getService }) { const supertest = getService('supertest'); From 10bf90f9f57112c078db973b196974277404f745 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 25 Feb 2020 09:34:53 -0500 Subject: [PATCH 090/113] [SIEM] [Detection Engine] Increase unit test coverage (#58274) * updates read privileges route tests * fixes test for 404 when alertsClient is not present, adds new test for catching errors at the route level for prepackaged rules route * fixes test for happy path on create rules bulk route (missing mock resulted in 200, but was still throwing an error), adds tests for covering case where a rule with a matching rule id is found to have already existed, adds a test covering the case where createRules throws an error and that error is caught in the catch block, and appears in the response * adds more jest functions to beforeEach to ensure mockImplementation state used by spyOn is not carried over between tests, increases test coverage for create rules route * updates unit test coverage for delete rules route and bulk delete * increase unit test coverage for find_rules_route * add test case to get_prepackaged_rules where findRules throws an error * adds unit test for missing alertsClient in patch_rules_bulk route * adds unit test coverage for transform error and pathRules throwing an error on patch rules route * adds unit test coverage for rule not found, transform error, and readRules throws an error on read_rules_route * adds unit test coverage for update rules (bulk) routes * increases unit test coverage for open close signals route * updates coverage for signals query route * adds unit tests for rules status route, updates utils test coverage. Removed unreachable code. * updates test coverage, removes usage of pipes from ndjson to stream conversion and returns arrays of tranformers to be used in createPromiseFromStreams so that unhandled promise rejections can be handled appropriately. * fixes type errors * fix bug on transform when rulestatus saved objects array is empty because we are no longer passing in the current status, we are passing in the whole saved object. * adds unit test for when readRules throws an error inside of get export by object ids * adds unit tests for catching errors on read rules and fixes property undefined bug in catch block * removes unused function from utils * adds the 'else' clause back to the getTupleDuplicateErrorsAndUniqueRules function and adds a test case for when an attempt to import rules with missing ruleIds is made, and the expected outcome. --- .../routes/__mocks__/request_responses.ts | 43 ++- .../routes/__mocks__/utils.ts | 37 ++ .../privileges/read_privileges_route.test.ts | 12 + .../rules/add_prepackaged_rules_route.test.ts | 23 +- .../rules/create_rules_bulk_route.test.ts | 49 ++- .../routes/rules/create_rules_route.test.ts | 48 ++- .../rules/delete_rules_bulk_route.test.ts | 10 + .../routes/rules/delete_rules_bulk_route.ts | 2 +- .../routes/rules/delete_rules_route.test.ts | 33 ++ .../routes/rules/find_rules_route.test.ts | 49 ++- .../rules/find_rules_status_route.test.ts | 96 +++++ .../routes/rules/find_rules_status_route.ts | 77 ++-- ...get_prepackaged_rules_status_route.test.ts | 17 + .../routes/rules/import_rules_route.test.ts | 38 +- .../routes/rules/import_rules_route.ts | 332 +++++++++--------- .../routes/rules/patch_rules_bulk.test.ts | 9 + .../routes/rules/patch_rules_route.test.ts | 34 ++ .../routes/rules/read_rules_route.test.ts | 41 ++- .../routes/rules/update_rules_bulk.test.ts | 4 +- .../routes/rules/update_rules_route.test.ts | 30 ++ .../routes/rules/utils.test.ts | 44 ++- .../detection_engine/routes/rules/utils.ts | 6 +- .../routes/signals/open_close_signals.test.ts | 14 + .../signals/open_close_signals_route.ts | 16 +- .../signals/query_signals_route.test.ts | 13 + .../routes/signals/query_signals_route.ts | 16 +- .../lib/detection_engine/routes/utils.test.ts | 12 + .../lib/detection_engine/routes/utils.ts | 9 + .../create_rules_stream_from_ndjson.test.ts | 64 +++- .../rules/create_rules_stream_from_ndjson.ts | 22 +- .../rules/get_export_by_object_ids.test.ts | 23 ++ .../detection_engine/rules/read_rules.test.ts | 73 ++++ .../lib/detection_engine/rules/read_rules.ts | 2 +- .../lib/detection_engine/rules/types.ts | 4 - .../security_and_spaces/tests/import_rules.ts | 11 + 35 files changed, 1038 insertions(+), 275 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 1578c71dddc6a..2b50011cf4dff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -427,13 +427,54 @@ export const getMockPrivileges = () => ({ has_encryption_key: true, }); -export const getFindResultStatus = (): SavedObjectsFindResponse => ({ +export const getFindResultStatusEmpty = (): SavedObjectsFindResponse => ({ page: 1, per_page: 1, total: 0, saved_objects: [], }); +export const getFindResultStatus = (): SavedObjectsFindResponse => ({ + page: 1, + per_page: 6, + total: 2, + saved_objects: [ + { + type: 'my-type', + id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:26:49.783Z', + status: 'succeeded', + lastFailureAt: null, + lastSuccessAt: '2020-02-18T15:26:49.783Z', + lastFailureMessage: null, + lastSuccessMessage: 'succeeded', + }, + references: [], + updated_at: '2020-02-18T15:26:51.333Z', + version: 'WzQ2LDFd', + }, + { + type: 'my-type', + id: '91246bd0-5261-11ea-9650-33b954270f67', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:15:58.806Z', + status: 'failed', + lastFailureAt: '2020-02-18T15:15:58.806Z', + lastSuccessAt: '2020-02-13T20:31:59.855Z', + lastFailureMessage: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + lastSuccessMessage: 'succeeded', + }, + references: [], + updated_at: '2020-02-18T15:15:58.860Z', + version: 'WzMyLDFd', + }, + ], +}); + export const getIndexName = () => 'index-name'; export const getEmptyIndex = (): { _shards: Partial } => ({ _shards: { total: 0 }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index f8c8e1f231ffa..32226e38a1f7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -26,6 +26,20 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial = query: 'user.name: root or user.name: admin', }); +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRuleWithId = (id = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + id, + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', +}); + /** * Given an array of rule_id strings this will return a ndjson buffer which is useful * for testing uploads. @@ -51,3 +65,26 @@ export const getSimpleRuleAsMultipartContent = (ruleIds: string[], isNdjson = tr return Buffer.from(resultingPayload); }; + +/** + * Given an array of rule_id strings this will return a ndjson buffer which is useful + * for testing uploads. + * @param count Number of rules to generate + * @param isNdjson Boolean to determine file extension + */ +export const getSimpleRuleAsMultipartContentNoRuleId = (count: number, isNdjson = true): Buffer => { + const arrayOfRules = Array(count).fill(JSON.stringify(getSimpleRuleWithId())); + const stringOfRules = arrayOfRules.join('\r\n'); + + const resultingPayload = + `--${TEST_BOUNDARY}\r\n` + + `Content-Disposition: form-data; name="file"; filename="rules.${ + isNdjson ? 'ndjson' : 'json' + }\r\n` + + 'Content-Type: application/octet-stream\r\n' + + '\r\n' + + `${stringOfRules}\r\n` + + `--${TEST_BOUNDARY}--\r\n`; + + return Buffer.from(resultingPayload); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 308ee95a77e20..3c31658c61d6e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -5,6 +5,7 @@ */ import { readPrivilegesRoute } from './read_privileges_route'; +import * as readPrivileges from '../../privileges/read_privileges'; import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; @@ -38,5 +39,16 @@ describe('read_privileges', () => { const { payload } = await inject(getPrivilegeRequest()); expect(JSON.parse(payload)).toEqual(getMockPrivileges()); }); + + test('returns 500 when bad response from readPrivileges', async () => { + jest.spyOn(readPrivileges, 'readPrivileges').mockImplementation(() => { + throw new Error('Test error'); + }); + const { payload } = await inject(getPrivilegeRequest()); + expect(JSON.parse(payload)).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index e018ed4cc22ff..e6a93fdadcfca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -6,7 +6,6 @@ import { omit } from 'lodash/fp'; -import { createRulesRoute } from './create_rules_route'; import { getFindResult, getResult, @@ -17,6 +16,7 @@ import { getNonEmptyIndex, } from '../__mocks__/request_responses'; import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; +import * as updatePrepackagedRules from '../../rules/update_prepacked_rules'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -54,7 +54,8 @@ describe('add_prepackaged_rules_route', () => { beforeEach(() => { jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); getClients = clientsServiceMock.createGetScoped(); @@ -78,9 +79,7 @@ describe('add_prepackaged_rules_route', () => { test('returns 404 if alertClient is not available on the route', async () => { getClients.mockResolvedValue(omit('alertsClient', clients)); - const { inject, route } = createMockServer(); - createRulesRoute(route, config, getClients); - const { statusCode } = await inject(addPrepackagedRulesRequest()); + const { statusCode } = await server.inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(404); }); }); @@ -126,5 +125,19 @@ describe('add_prepackaged_rules_route', () => { rules_updated: 1, }); }); + test('catches errors if payloads cause errors to be thrown', async () => { + jest.spyOn(updatePrepackagedRules, 'updatePrepackagedRules').mockImplementation(() => { + throw new Error('Test error'); + }); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(addPrepackagedRulesRequest()); + expect(JSON.parse(payload)).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 664d27a7572ad..931623ea6652c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -14,12 +14,15 @@ import { typicalPayload, getReadBulkRequest, getEmptyIndex, + getNonEmptyIndex, } from '../__mocks__/request_responses'; import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { BulkError } from '../utils'; import { OutputRuleAlertRest } from '../../types'; +import * as createRules from '../../rules/create_rules'; +import * as readRules from '../../rules/read_rules'; describe('create_rules_bulk', () => { let server = createMockServer(); @@ -29,10 +32,14 @@ describe('create_rules_bulk', () => { beforeEach(() => { jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + getClients.mockResolvedValue(clients); createRulesBulkRoute(server.route, config, getClients); @@ -44,8 +51,12 @@ describe('create_rules_bulk', () => { clients.alertsClient.get.mockResolvedValue(getResult()); clients.actionsClient.create.mockResolvedValue(createActionResult()); clients.alertsClient.create.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getReadBulkRequest()); + jest.spyOn(createRules, 'createRules').mockImplementation(async () => { + return getResult(); + }); + const { payload, statusCode } = await server.inject(getReadBulkRequest()); expect(statusCode).toBe(200); + expect(JSON.parse(payload).error).toBeUndefined(); }); test('returns 404 if alertClient is not available on the route', async () => { @@ -149,6 +160,24 @@ describe('create_rules_bulk', () => { expect(output.some(item => item.error?.status_code === 409)).toBeTruthy(); }); + test('returns 409 if duplicate rule_ids found in rule saved objects', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + return getResult(); + }); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array> = JSON.parse(payload); + expect(output.some(item => item.error?.status_code === 409)).toBeTruthy(); + }); + test('returns one error object in response when duplicate rule_ids found in request payload', async () => { clients.alertsClient.find.mockResolvedValue(getFindResult()); clients.alertsClient.get.mockResolvedValue(getResult()); @@ -163,4 +192,22 @@ describe('create_rules_bulk', () => { const output: Array> = JSON.parse(payload); expect(output.length).toBe(1); }); + + test('catches error if createRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest.spyOn(createRules, 'createRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array> = JSON.parse(payload); + expect(output[0].error.message).toBe('Test error'); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 4f28771db8ed7..5ad43e70f2651 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -9,6 +9,9 @@ import { omit } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesRoute } from './create_rules_route'; +import * as createRules from '../../rules/create_rules'; +import * as readRules from '../../rules/read_rules'; +import * as utils from './utils'; import { getFindResult, @@ -29,8 +32,12 @@ describe('create_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); getClients = clientsServiceMock.createGetScoped(); @@ -130,5 +137,44 @@ describe('create_rules', () => { const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); + + test('catches error if createRules throws error', async () => { + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(createRules, 'createRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getCreateRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); + + test('catches error if transform returns null', async () => { + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getCreateRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('returns 409 if duplicate rule_ids found in rule saved objects', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + return getResult(); + }); + const { payload } = await server.inject(getCreateRequest()); + const output = JSON.parse(payload); + expect(output.status_code).toEqual(409); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 855bf7f634c26..fb44f96d76859 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -48,6 +48,16 @@ describe('delete_rules', () => { expect(statusCode).toBe(200); }); + test('resturns 200 when deleting a single rule and related rule status', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue(true); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(200); + }); + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => { clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 6438318cb43db..aabf3e513bfea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -58,7 +58,7 @@ export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.S ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - return transformOrBulkError(idOrRuleIdOrUnknown, rule); + return transformOrBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index a0a6f61223279..57c7c85976619 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -7,6 +7,8 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; import { deleteRulesRoute } from './delete_rules_route'; +import * as utils from './utils'; +import * as deleteRules from '../../rules/delete_rules'; import { getFindResult, @@ -25,7 +27,12 @@ describe('delete_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); @@ -73,6 +80,32 @@ describe('delete_rules', () => { const { statusCode } = await inject(getDeleteRequest()); expect(statusCode).toBe(404); }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getDeleteRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if deleteRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); + jest.spyOn(deleteRules, 'deleteRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getDeleteRequestById()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 5b75f17164acf..019424ea2420a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -6,13 +6,22 @@ import { omit } from 'lodash/fp'; +import { + getFindResult, + getResult, + getFindResultWithSingleHit, + getFindResultStatus, + getFindRequest, +} from '../__mocks__/request_responses'; import { createMockServer } from '../__mocks__'; import { clientsServiceMock } from '../__mocks__/clients_service_mock'; +import * as utils from './utils'; +import * as findRules from '../../rules/find_rules'; + import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('find_rules', () => { @@ -21,7 +30,12 @@ describe('find_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); @@ -33,15 +47,10 @@ describe('find_rules', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResult()); - clients.actionsClient.find.mockResolvedValue({ - page: 1, - perPage: 1, - total: 0, - data: [], - }); + test('returns 200 when finding a single rule with a valid alertsClient', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getFindRequest()); expect(statusCode).toBe(200); }); @@ -53,6 +62,28 @@ describe('find_rules', () => { const { statusCode } = await inject(getFindRequest()); expect(statusCode).toBe(404); }); + + test('catches error when transformation fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transformFindAlerts').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getFindRequest()); + expect(statusCode).toBe(500); + expect(JSON.parse(payload).message).toBe('unknown data type, error transforming alert'); + }); + + test('catch error when findRules function throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(findRules, 'findRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getFindRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts new file mode 100644 index 0000000000000..00411c550fa2e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { omit } from 'lodash/fp'; + +import { getFindResultStatus } from '../__mocks__/request_responses'; +import { createMockServer } from '../__mocks__'; +import { clientsServiceMock } from '../__mocks__/clients_service_mock'; + +import { findRulesStatusesRoute } from './find_rules_status_route'; +import { ServerInjectOptions } from 'hapi'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; + +describe('find_statuses', () => { + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + findRulesStatusesRoute(server.route, getClients); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when finding a single rule status with a valid alertsClient', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + getClients.mockResolvedValue(omit('alertsClient', clients)); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + }); + + test('catch error when savedObjectsClient find function throws error', async () => { + clients.savedObjectsClient.find.mockImplementation(async () => { + throw new Error('Test error'); + }); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { payload, statusCode } = await server.inject(request); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); + }); + + describe('validation', () => { + test('returns 400 if id is given instead of ids', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?id=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 200 if the set of optional query parameters are given', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index fe8742ff0b60c..c496c7b7ce59c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import { snakeCase } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { LegacyServices, LegacyRequest } from '../../../../types'; @@ -18,17 +17,7 @@ import { IRuleStatusAttributes, } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const convertToSnakeCase = >(obj: T): Partial | null => { - if (!obj) { - return null; - } - return Object.keys(obj).reduce((acc, item) => { - const newKey = snakeCase(item); - return { ...acc, [newKey]: obj[item] }; - }, {}); -}; +import { transformError, convertToSnakeCase } from '../utils'; export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', @@ -57,33 +46,43 @@ export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.S "anotherAlertId": ... } */ - const statuses = await query.ids.reduce>(async (acc, id) => { - const lastFiveErrorsForId = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 6, - sortField: 'statusDate', - sortOrder: 'desc', - search: id, - searchFields: ['alertId'], - }); - const accumulated = await acc; - const currentStatus = convertToSnakeCase( - lastFiveErrorsForId.saved_objects[0]?.attributes - ); - const failures = lastFiveErrorsForId.saved_objects - .slice(1) - .map(errorItem => convertToSnakeCase(errorItem.attributes)); - return { - ...accumulated, - [id]: { - current_status: currentStatus, - failures, - }, - }; - }, Promise.resolve({})); - return statuses; + try { + const statuses = await query.ids.reduce>(async (acc, id) => { + const lastFiveErrorsForId = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 6, + sortField: 'statusDate', + sortOrder: 'desc', + search: id, + searchFields: ['alertId'], + }); + const accumulated = await acc; + const currentStatus = convertToSnakeCase( + lastFiveErrorsForId.saved_objects[0]?.attributes + ); + const failures = lastFiveErrorsForId.saved_objects + .slice(1) + .map(errorItem => convertToSnakeCase(errorItem.attributes)); + return { + ...accumulated, + [id]: { + current_status: currentStatus, + failures, + }, + }; + }, Promise.resolve({})); + return statuses; + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index 8f27910a7e5e2..99157b4d15360 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash/fp'; import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; +import * as findRules from '../../rules/find_rules'; import { getFindResult, @@ -47,7 +48,12 @@ describe('get_prepackaged_rule_status_route', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); @@ -76,6 +82,17 @@ describe('get_prepackaged_rule_status_route', () => { const { statusCode } = await inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(404); }); + + test('catch error when findRules function throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + jest.spyOn(findRules, 'findRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getPrepackagedRulesStatusRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('payload', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index b1dd08f8ca371..c8b77e505b5d7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -8,6 +8,7 @@ import { omit } from 'lodash/fp'; import { getSimpleRuleAsMultipartContent, + getSimpleRuleAsMultipartContentNoRuleId, TEST_BOUNDARY, UNPARSABLE_LINE, getSimpleRule, @@ -25,6 +26,7 @@ import { import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { importRulesRoute } from './import_rules_route'; import { DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; +import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; describe('import_rules_route', () => { let server = createMockServer(); @@ -33,8 +35,12 @@ describe('import_rules_route', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); config = () => ({ @@ -94,6 +100,21 @@ describe('import_rules_route', () => { const { statusCode } = await inject(getImportRulesRequest(requestPayload)); expect(statusCode).toEqual(404); }); + + test('returns error if createPromiseFromStreams throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest + .spyOn(createRulesStreamFromNdJson, 'createRulesStreamFromNdJson') + .mockImplementation(() => { + throw new Error('Test error'); + }); + const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']); + const { payload, statusCode } = await server.inject(getImportRulesRequest(requestPayload)); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { @@ -306,6 +327,21 @@ describe('import_rules_route', () => { expect(statusCode).toEqual(200); }); + test('returns 200 with errors if all rules are missing rule_ids and import fails on validation', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + + const requestPayload = getSimpleRuleAsMultipartContentNoRuleId(2); + const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload)); + const parsed: ImportSuccessError = JSON.parse(payload); + + expect(parsed.success).toEqual(false); + expect(parsed.errors[0].error.message).toEqual( + 'child "rule_id" fails because ["rule_id" is required]' + ); + expect(parsed.errors[0].error.status_code).toEqual(400); + expect(statusCode).toEqual(200); + }); + test('returns 200 with reported conflict if error parsing rule', async () => { const multipartPayload = `--${TEST_BOUNDARY}\r\n` + diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index f438e0120f96a..a9358a47f25fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -7,6 +7,7 @@ import Hapi from 'hapi'; import { chunk, isEmpty } from 'lodash/fp'; import { extname } from 'path'; +import { Readable } from 'stream'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -15,7 +16,7 @@ import { createRules } from '../../rules/create_rules'; import { ImportRulesRequest } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; -import { getIndex, createBulkErrorObject, ImportRuleResponse } from '../utils'; +import { getIndex, transformError, createBulkErrorObject, ImportRuleResponse } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; @@ -74,179 +75,192 @@ export const createImportRulesRoute = ( } const objectLimit = config().get('savedObjects.maxImportExportSize'); - const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); - const parsedObjects = await createPromiseFromStreams([readStream]); - const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules( - parsedObjects, - request.query.overwrite - ); + try { + const readStream = createRulesStreamFromNdJson(objectLimit); + const parsedObjects = await createPromiseFromStreams([ + request.payload.file as Readable, + ...readStream, + ]); + const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules( + parsedObjects, + request.query.overwrite + ); - const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); - let importRuleResponse: ImportRuleResponse[] = []; + const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); + let importRuleResponse: ImportRuleResponse[] = []; - while (chunkParseObjects.length) { - const batchParseObjects = chunkParseObjects.shift() ?? []; - const newImportRuleResponse = await Promise.all( - batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise( - async (resolve, reject) => { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } - const { - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = parsedRule; - try { - const finalIndex = getIndex(spacesClient.getSpaceId, config); - const indexExists = await getIndexExists( - clusterClient.callAsCurrentUser, - finalIndex - ); - if (!indexExists) { + while (chunkParseObjects.length) { + const batchParseObjects = chunkParseObjects.shift() ?? []; + const newImportRuleResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedRule) => { + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId resolve( createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + statusCode: 400, + message: parsedRule.message, }) ); + return null; } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - await createRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null && request.query.overwrite) { - await patchRules({ - alertsClient, - actionsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null) { + const { + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + } = parsedRule; + try { + const finalIndex = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + finalIndex + ); + if (!indexExists) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }) + ); + } + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await patchRules({ + alertsClient, + actionsClient, + savedObjectsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }) + ); + } + } catch (err) { resolve( createBulkErrorObject({ ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, + statusCode: 400, + message: err.message, }) ); } - } catch (err) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: 400, - message: err.message, - }) - ); } - } - ); - return [...accum, importsWorkerPromise]; - }, []) - ); - importRuleResponse = [ - ...duplicateIdErrors, - ...importRuleResponse, - ...newImportRuleResponse, - ]; - } + ); + return [...accum, importsWorkerPromise]; + }, []) + ); + importRuleResponse = [ + ...duplicateIdErrors, + ...importRuleResponse, + ...newImportRuleResponse, + ]; + } - const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); - return { - success: errorsResp.length === 0, - success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, - errors: errorsResp, - }; + const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); + return { + success: errorsResp.length === 0, + success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, + errors: errorsResp, + }; + } catch (exc) { + const error = transformError(exc); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts index 02af4135b534f..1cfb4ae81ab85 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -15,6 +15,7 @@ import { typicalPayload, getFindResultWithSingleHit, getPatchBulkRequest, + getFindResultStatus, } from '../__mocks__/request_responses'; import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -43,6 +44,7 @@ describe('patch_rules_bulk', () => { clients.alertsClient.get.mockResolvedValue(getResult()); clients.actionsClient.update.mockResolvedValue(updateActionResult()); clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); @@ -56,6 +58,13 @@ describe('patch_rules_bulk', () => { expect(statusCode).toBe(200); }); + test('returns 404 as a response when missing alertsClient', async () => { + getClients.mockResolvedValue(omit('alertsClient', clients)); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + const { statusCode } = await server.inject(getPatchBulkRequest()); + expect(statusCode).toBe(404); + }); + test('returns 404 within the payload when updating a single rule that does not exist', async () => { clients.alertsClient.find.mockResolvedValue(getFindResult()); clients.alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index cc84b08fdef11..04cd3a026562f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -7,6 +7,8 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; import { patchRulesRoute } from './patch_rules_route'; +import * as utils from './utils'; +import * as patchRules from '../../rules/patch_rules'; import { getFindResult, @@ -26,7 +28,13 @@ describe('patch_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); @@ -63,6 +71,32 @@ describe('patch_rules', () => { const { statusCode } = await inject(getPatchRequest()); expect(statusCode).toBe(404); }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getPatchRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if patchRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(patchRules, 'patchRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getPatchRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 7c4653af97f21..0366d3648e1ea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -7,6 +7,9 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; +import * as utils from './utils'; +import * as readRules from '../../rules/read_rules'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { readRulesRoute } from './read_rules_route'; import { @@ -24,8 +27,12 @@ describe('read_signals', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); @@ -50,6 +57,38 @@ describe('read_signals', () => { const { statusCode } = await inject(getReadRequest()); expect(statusCode).toBe(404); }); + + test('returns error if readRules returns null', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(readRules, 'readRules').mockResolvedValue(null); + const { payload, statusCode } = await server.inject(getReadRequest()); + expect(JSON.parse(payload).message).toBe('rule_id: "rule-1" not found'); + expect(statusCode).toBe(404); + }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getReadRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if readRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getReadRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 9ff7ebc37aab1..32a633799ad44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -75,9 +75,9 @@ describe('update_rules_bulk', () => { test('returns 404 if alertClient is not available on the route', async () => { getClients.mockResolvedValue(omit('alertsClient', clients)); - const { route, inject } = createMockServer(); + const { route } = createMockServer(); updateRulesRoute(route, config, getClients); - const { statusCode } = await inject(getUpdateBulkRequest()); + const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 7cadfa94467a7..c3a92ed9a61ae 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -7,6 +7,9 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; +import * as utils from './utils'; +import * as updateRules from '../../rules/update_rules'; + import { updateRulesRoute } from './update_rules_route'; import { getFindResult, @@ -27,7 +30,12 @@ describe('update_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); @@ -66,6 +74,28 @@ describe('update_rules', () => { const { statusCode } = await inject(getUpdateRequest()); expect(statusCode).toBe(404); }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getUpdateRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if readRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(updateRules, 'updateRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getUpdateRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 593c55bcae9f2..b5b687c284b25 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -1251,9 +1251,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); const isInstanceOfError = output[0] instanceof Error; @@ -1272,9 +1273,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); @@ -1290,6 +1292,30 @@ describe('utils', () => { ]); }); + test('returns tuple of duplicate conflict error and single rule when rules with matching ids passed in and `overwrite` is false', async () => { + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + const rule2 = getSimpleRule('rule-1'); + delete rule2.rule_id; + const ndJsonStream = new Readable({ + read() { + this.push(`${JSON.stringify(rule)}\n`); + this.push(`${JSON.stringify(rule2)}\n`); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); + const isInstanceOfError = output[0] instanceof Error; + + expect(isInstanceOfError).toEqual(true); + expect(errors).toEqual([]); + }); + test('returns tuple of empty duplicate errors array and single rule when rules with matching rule-ids passed in and `overwrite` is true', async () => { const rule = getSimpleRule('rule-1'); const rule2 = getSimpleRule('rule-1'); @@ -1300,9 +1326,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, true); @@ -1320,9 +1347,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); const isInstanceOfError = output[0] instanceof Error; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 198cdbfb9771d..ab80e1570c31c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -180,9 +180,7 @@ export const transform = ( if (!ruleStatus && isAlertType(alert)) { return transformAlertToRule(alert); } - if (isAlertType(alert) && isRuleStatusFindType(ruleStatus)) { - return transformAlertToRule(alert, ruleStatus.saved_objects[0]); - } else if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { + if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { return transformAlertToRule(alert, ruleStatus); } else { return null; @@ -195,7 +193,7 @@ export const transformOrBulkError = ( ruleStatus?: unknown ): Partial | BulkError => { if (isAlertType(alert)) { - if (isRuleStatusFindType(ruleStatus)) { + if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { return transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus); } else { return transformAlertToRule(alert); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 3e7ed4de6d8c6..7086c62f81711 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -25,7 +25,12 @@ describe('set signal status', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); server = createMockServer(); @@ -50,6 +55,15 @@ describe('set signal status', () => { const { statusCode } = await server.inject(getSetSignalStatusByQueryRequest()); expect(statusCode).toBe(200); }); + + test('catches error if callAsCurrentUser throws error', async () => { + clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getSetSignalStatusByQueryRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index dd3b8d3c99e0c..ee3fd349a26ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -28,7 +28,7 @@ export const setSignalsStatusRouteDef = ( payload: setSignalsStatusSchema, }, }, - async handler(request: SignalsStatusRequest) { + async handler(request: SignalsStatusRequest, headers) { const { signal_ids: signalIds, query, status } = request.payload; const { clusterClient, spacesClient } = await getClients(request); const index = getIndex(spacesClient.getSpaceId, config); @@ -45,7 +45,7 @@ export const setSignalsStatusRouteDef = ( }; } try { - return clusterClient.callAsCurrentUser('updateByQuery', { + const updateByQueryResponse = await clusterClient.callAsCurrentUser('updateByQuery', { index, body: { script: { @@ -56,9 +56,15 @@ export const setSignalsStatusRouteDef = ( }, ignoreUnavailable: true, }); - } catch (exc) { - // error while getting or updating signal with id: id in signal index .siem-signals - return transformError(exc); + return updateByQueryResponse; + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 9439adfcec3cb..210ac9f3d7b01 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -127,5 +127,18 @@ describe('query for signal', () => { const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); + test('catches error if deleteRules throws error', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }, + }; + clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(request); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index adb6e5f32921a..7636329ecc306 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -28,21 +28,27 @@ export const querySignalsRouteDef = ( payload: querySignalsSchema, }, }, - async handler(request: SignalsQueryRequest) { + async handler(request: SignalsQueryRequest, headers) { const { query, aggs, _source, track_total_hits, size } = request.payload; const { clusterClient, spacesClient } = await getClients(request); const index = getIndex(spacesClient.getSpaceId, config); try { - return clusterClient.callAsCurrentUser('search', { + const searchSignalsIndexResult = await clusterClient.callAsCurrentUser('search', { index, body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); - } catch (exc) { - // error while getting or updating signal with id: id in signal index .siem-signals - return transformError(exc); + return searchSignalsIndexResult; + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 957ddd4ee6caa..3148083b4db26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -15,6 +15,7 @@ import { ImportSuccessError, createImportErrorObject, transformImportError, + convertToSnakeCase, } from './utils'; import { createMockConfig } from './__mocks__'; @@ -312,4 +313,15 @@ describe('utils', () => { expect(index).toEqual('mockSignalsIndex-myspace'); }); }); + + describe('convertToSnakeCase', () => { + it('converts camelCase to snakeCase', () => { + const values = { myTestCamelCaseKey: 'something' }; + expect(convertToSnakeCase(values)).toEqual({ my_test_camel_case_key: 'something' }); + }); + it('returns empty object when object is empty', () => { + const values = {}; + expect(convertToSnakeCase(values)).toEqual({}); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 55832ab67dc6b..36e1a814d8ec2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { snakeCase } from 'lodash/fp'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { LegacyServices } from '../../../types'; @@ -211,3 +212,11 @@ export const getIndex = (getSpaceId: () => string, config: LegacyServices['confi return `${signalsIndex}-${spaceId}`; }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const convertToSnakeCase = >(obj: T): Partial | null => { + return Object.keys(obj).reduce((acc, item) => { + const newKey = snakeCase(item); + return { ...acc, [newKey]: obj[item] }; + }, {}); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index d4b7c252e3e38..b1dc62f6fc90f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -5,12 +5,10 @@ */ import { Readable } from 'stream'; import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; -import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams'; +import { createPromiseFromStreams } from 'src/legacy/utils/streams'; import { ImportRuleAlertRest } from '../types'; -const readStreamToCompletion = (stream: Readable) => { - return createPromiseFromStreams([stream, createConcatStream([])]); -}; +type PromiseFromStreams = ImportRuleAlertRest | Error; export const getOutputSample = (): Partial => ({ rule_id: 'rule-1', @@ -43,8 +41,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); expect(result).toEqual([ { rule_id: 'rule-1', @@ -95,6 +96,22 @@ describe('create_rules_stream_from_ndjson', () => { ]); }); + test('returns error when ndjson stream is larger than limit', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push(getSampleAsNdjson(sample2)); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1); + await expect( + createPromiseFromStreams([ndJsonStream, ...rulesObjectsStream]) + ).rejects.toThrowError("Can't import more than 1 rules"); + }); + test('skips empty lines', async () => { const sample1 = getOutputSample(); const sample2 = getOutputSample(); @@ -108,8 +125,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); expect(result).toEqual([ { rule_id: 'rule-1', @@ -172,8 +192,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); expect(result).toEqual([ { rule_id: 'rule-1', @@ -236,8 +259,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); const resultOrError = result as Error[]; expect(resultOrError[0]).toEqual({ rule_id: 'rule-1', @@ -300,8 +326,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); const resultOrError = result as TypeError[]; expect(resultOrError[0]).toEqual({ rule_id: 'rule-1', @@ -366,8 +395,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); const resultOrError = result as TypeError[]; expect(resultOrError[1] instanceof TypeError).toEqual(true); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 6d58171a3245d..ae0dfa20852aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Readable, Transform } from 'stream'; +import { Transform } from 'stream'; import { has, isString } from 'lodash/fp'; import { ImportRuleAlertRest } from '../types'; import { @@ -74,15 +74,13 @@ export const createLimitStream = (limit: number): Transform => { * Inspiration and the pattern of code followed is from: * saved_objects/lib/create_saved_objects_stream_from_ndjson.ts */ -export const createRulesStreamFromNdJson = ( - ndJsonStream: Readable, - ruleLimit: number -): Transform => { - return ndJsonStream - .pipe(createSplitStream('\n')) - .pipe(parseNdjsonStrings()) - .pipe(filterExportedCounts()) - .pipe(validateRules()) - .pipe(createLimitStream(ruleLimit)) - .pipe(createConcatStream([])); +export const createRulesStreamFromNdJson = (ruleLimit: number) => { + return [ + createSplitStream('\n'), + parseNdjsonStrings(), + filterExportedCounts(), + validateRules(), + createLimitStream(ruleLimit), + createConcatStream([]), + ]; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 98f5df4852530..44d3013263c65 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -10,9 +10,15 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; +import * as readRules from './read_rules'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; describe('get_export_by_object_ids', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); describe('getExportByObjectIds', () => { test('it exports object ids into an expected string with new line characters', async () => { const alertsClient = alertsClientMock.create(); @@ -119,6 +125,23 @@ describe('get_export_by_object_ids', () => { expect(exports).toEqual(expected); }); + test('it returns error when readRules throws error', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getRulesFromObjects(alertsClient, objects); + const expected: RulesErrors = { + exportedCount: 0, + missingRules: [{ rule_id: objects[0].rule_id }], + rules: [], + }; + expect(exports).toEqual(expected); + }); + test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => { const alertsClient = alertsClientMock.create(); const result = getResult(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 45507a69f50c2..aa1cce6f15238 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -8,7 +8,23 @@ import { readRules } from './read_rules'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; +class TestError extends Error { + constructor() { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(); + + this.name = 'CustomError'; + this.output = { statusCode: 404 }; + } + public output: { statusCode: number }; +} + describe('read_rules', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); describe('readRules', () => { test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { const alertsClient = alertsClientMock.create(); @@ -21,6 +37,49 @@ describe('read_rules', () => { }); expect(rule).toEqual(getResult()); }); + test('should return null if saved object found by alerts client given id is not alert type', async () => { + const alertsClient = alertsClientMock.create(); + const { alertTypeId, ...rest } = getResult(); + // @ts-ignore + alertsClient.get.mockImplementation(() => rest); + + const rule = await readRules({ + alertsClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + expect(rule).toEqual(null); + }); + + test('should return error if alerts client throws 404 error on get', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockImplementation(() => { + throw new TestError(); + }); + + const rule = await readRules({ + alertsClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + expect(rule).toEqual(null); + }); + + test('should return error if alerts client throws error on get', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockImplementation(() => { + throw new Error('Test error'); + }); + try { + await readRules({ + alertsClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + } catch (exc) { + expect(exc.message).toEqual('Test error'); + } + }); test('should return the output from alertsClient if id is set but ruleId is null', async () => { const alertsClient = alertsClientMock.create(); @@ -47,6 +106,20 @@ describe('read_rules', () => { expect(rule).toEqual(getResult()); }); + test('should return null if the output from alertsClient with ruleId set is empty', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + // @ts-ignore + alertsClient.find.mockResolvedValue({ data: [] }); + + const rule = await readRules({ + alertsClient, + id: undefined, + ruleId: 'rule-1', + }); + expect(rule).toEqual(null); + }); + test('should return the output from alertsClient if id is null but ruleId is set', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index cbe6dbda8449f..94e4e6357a4a0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -31,7 +31,7 @@ export const readRules = async ({ return null; } } catch (err) { - if (err.output.statusCode === 404) { + if (err?.output?.statusCode === 404) { return null; } else { // throw non-404 as they would be 500 or other internal errors diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index fa22765c143e1..3d95e9868a1d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -197,10 +197,6 @@ export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isRuleStatusAttributes = (obj: unknown): obj is IRuleStatusAttributes => { - return get('lastSuccessMessage', obj) != null; -}; - export const isRuleStatusSavedObjectType = ( obj: unknown ): obj is SavedObject => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index 79a1e667e5458..a1cb60483c332 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -74,6 +74,17 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should report that it failed to import a thousand and one (10001) simple rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(new Array(10001).fill('rule-1')), 'rules.ndjson') + .query() + .expect(500); + + expect(body).to.eql({ message: "Can't import more than 10000 rules", status_code: 500 }); + }); + it('should be able to read an imported rule back out correctly', async () => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) From b17aa3551387bbe752b86950fb2d8b0ee600a629 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 25 Feb 2020 15:28:41 +0000 Subject: [PATCH 091/113] [Logs / Metrics UI] Handle index.scss (#58219) * Ensure our index.scss file is handled --- .../plugins/infra/public/apps/start_app.tsx | 9 ++---- x-pack/plugins/infra/public/index.scss | 29 +------------------ 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index 66e699abd22b4..a797e4c9d4ba7 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -26,6 +26,7 @@ import { KibanaContextProvider, } from '../../../../../src/plugins/kibana_react/public'; import { AppRouter } from '../routers'; +import '../index.scss'; export const CONTAINER_CLASSNAME = 'infra-container-element'; @@ -74,12 +75,8 @@ export async function startApp( ); - // Ensure the element we're handed from application mounting takes up - // the full size it can, so that our inner application styles work as - // expected. - element.style.height = '100%'; - element.style.display = 'flex'; - element.style.overflowY = 'hidden'; // Prevent having scroll within a container having scroll. It messes up with drag-n-drop elements + // Ensure the element we're handed from application mounting is assigned a class + // for our index.scss styles to apply to. element.className += ` ${CONTAINER_CLASSNAME}`; ReactDOM.render(, element); diff --git a/x-pack/plugins/infra/public/index.scss b/x-pack/plugins/infra/public/index.scss index afee4ab8b0389..05e045c1bd53b 100644 --- a/x-pack/plugins/infra/public/index.scss +++ b/x-pack/plugins/infra/public/index.scss @@ -3,22 +3,13 @@ /* Infra plugin styles */ -// Prefix all styles with "inf" to avoid conflicts. -// Examples -// infChart -// infChart__legend -// infChart__legend--small -// infChart__legend-isLoading - -.infReactRoot { +.infra-container-element { background-color: $euiColorEmptyShade; position: absolute; top: 0; left: 0; bottom: 0; right: 0; - // display: flex; - // flex-direction: column; align-items: flex-start; flex: 1 0 auto; overflow-x: hidden; @@ -27,21 +18,3 @@ flex-direction: column; } -// This is a temporary workaround for https://github.com/elastic/kibana/issues/39808 -// A real fix will most likely depend on changes in elastic-charts. - -.infrastructureChart .echTooltip { - max-width: 90vw; -} - -.infrastructureChart .echTooltip__label { - overflow-x: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.metricsExplorerTitleAnchor { - white-space: nowrap; - text-overflow: ellipsis; - display: inline; -} From 21d98d787e4f8c0b20be89d9c12829ba6b3aeefd Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 25 Feb 2020 17:10:18 +0100 Subject: [PATCH 092/113] skips 'ResizeObserver loop limit exceeded' error (#58465) --- x-pack/legacy/plugins/siem/cypress/support/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/legacy/plugins/siem/cypress/support/index.js b/x-pack/legacy/plugins/siem/cypress/support/index.js index 9b86c2ffa94c6..37fa920a8bc31 100644 --- a/x-pack/legacy/plugins/siem/cypress/support/index.js +++ b/x-pack/legacy/plugins/siem/cypress/support/index.js @@ -26,5 +26,11 @@ Cypress.Cookies.defaults({ whitelist: 'sid', }); +Cypress.on('uncaught:exception', err => { + if (err.message.includes('ResizeObserver loop limit exceeded')) { + return false; + } +}); + // Alternatively you can use CommonJS syntax: // require('./commands') From 712c0851a7d5b34009d948485eda797ed85d71fa Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 25 Feb 2020 17:15:31 +0100 Subject: [PATCH 093/113] [SIEM] Deletes duplicated tests (#58453) * deletes duplicated test * deletes duplicated fields browser test --- .../plugins/siem/cypress/integration/events_viewer.spec.ts | 7 ------- .../siem/cypress/integration/fields_browser.spec.ts | 7 ------- 2 files changed, 14 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts index e44c8f4459ba9..446db89ec09dc 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts @@ -8,7 +8,6 @@ import { FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, - FIELDS_BROWSER_TITLE, } from '../screens/fields_browser'; import { HEADER_SUBTITLE, @@ -61,12 +60,6 @@ describe('Events Viewer', () => { cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); }); - it('renders the fields browser with the expected title when the Events Viewer Fields Browser button is clicked', () => { - cy.get(FIELDS_BROWSER_TITLE) - .invoke('text') - .should('eq', 'Customize Columns'); - }); - it('displays the `default ECS` category (by default)', () => { cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) .invoke('text') diff --git a/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts index 095fc30356fd4..8dddd97f2d830 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts @@ -15,7 +15,6 @@ import { FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, - FIELDS_BROWSER_TITLE, } from '../screens/fields_browser'; import { @@ -58,12 +57,6 @@ describe('Fields Browser', () => { clearFieldsBrowser(); }); - it('renders the fields browser with the expected title when the Fields button is clicked', () => { - cy.get(FIELDS_BROWSER_TITLE) - .invoke('text') - .should('eq', 'Customize Columns'); - }); - it('displays the `default ECS` category (by default)', () => { cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) .invoke('text') From c1dede78fbbee6ca1c97fb9c5f31d72f39584566 Mon Sep 17 00:00:00 2001 From: liza-mae Date: Tue, 25 Feb 2020 09:21:30 -0700 Subject: [PATCH 094/113] Add a way to disable certificate verification for FTR. (#58386) * Add a way to disable certificate verification for FTR. * Remove other options Co-authored-by: Elastic Machine --- test/functional/services/remote/webdriver.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 1bd6358749e11..382543822d8ac 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -44,6 +44,7 @@ import { Browsers } from './browsers'; const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string; const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string; const remoteDebug: string = process.env.TEST_REMOTE_DEBUG as string; +const certValidation: string = process.env.NODE_TLS_REJECT_UNAUTHORIZED as string; const SECOND = 1000; const MINUTE = 60 * SECOND; const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit']; @@ -98,6 +99,9 @@ async function attemptToCreateCommand( // See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md chromeOptions.push('headless', 'disable-gpu'); } + if (certValidation === '0') { + chromeOptions.push('ignore-certificate-errors'); + } if (remoteDebug === '1') { // Visit chrome://inspect in chrome to remotely view/debug chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222'); From a714300155fee3854777f4478fcf766bbc68c892 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 25 Feb 2020 17:48:56 +0100 Subject: [PATCH 095/113] =?UTF-8?q?fix:=20=F0=9F=90=9B=20make=20performanc?= =?UTF-8?q?e.now=20work=20on=20server=20(#58360)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 make performance.now work on server * fix: 🐛 change check for Node environment * fix: 🐛 simplify now() fn, remove require('perf_hohoks') --- .../expressions/common/execution/execution.ts | 8 +++--- src/plugins/kibana_utils/common/index.ts | 1 + src/plugins/kibana_utils/common/now.ts | 25 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/plugins/kibana_utils/common/now.ts diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 272448870e817..f70a32f2f09c1 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -21,7 +21,7 @@ import { keys, last, mapValues, reduce, zipObject } from 'lodash'; import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; -import { Defer } from '../../../kibana_utils/common'; +import { Defer, now } from '../../../kibana_utils/common'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -211,11 +211,11 @@ export class Execution< // actually have a `then` function which would be treated as a `Promise`. const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs); args = resolvedArgs; - timeStart = this.params.debug ? performance.now() : 0; + timeStart = this.params.debug ? now() : 0; const output = await this.invokeFunction(fn, input, resolvedArgs); if (this.params.debug) { - const timeEnd: number = performance.now(); + const timeEnd: number = now(); (link as ExpressionAstFunction).debug = { success: true, fn, @@ -229,7 +229,7 @@ export class Execution< if (getType(output) === 'error') return output; input = output; } catch (rawError) { - const timeEnd: number = this.params.debug ? performance.now() : 0; + const timeEnd: number = this.params.debug ? now() : 0; const error = createError(rawError) as ExpressionValueError; error.error.message = `[${fnName}] > ${error.error.message}`; diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 3b07674315dce..50120edc0c056 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -25,3 +25,4 @@ export * from './typed_json'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; export { url } from './url'; +export { now } from './now'; diff --git a/src/plugins/kibana_utils/common/now.ts b/src/plugins/kibana_utils/common/now.ts new file mode 100644 index 0000000000000..e6c6d74552622 --- /dev/null +++ b/src/plugins/kibana_utils/common/now.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. + */ + +/** + * Function that returns number in milliseconds since some undefined point in + * time. Use this function for performance measurements. + */ +export const now: () => number = + typeof performance === 'object' ? performance.now.bind(performance) : Date.now; From c8f18208ab697ea823f7029ee6ae48f87d5fa5fe Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 25 Feb 2020 17:56:02 +0100 Subject: [PATCH 096/113] Embeddable action storage (#58438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 stub out EmbeddableActionStorage class * test: 💍 add embeddable event storage .create() tests * feat: 🎸 add EmbeddableActionStorage.update() method * feat: 🎸 add .actionStorage to Embeddable class * feat: 🎸 add EmbeddableActionStorage.remove() method * feat: 🎸 add EmbeddableActionStorage.read() method * feat: 🎸 add .list() and .count() to EmbeddableActionStorage --- .../public/lib/embeddables/embeddable.tsx | 6 + .../embeddable_action_storage.test.ts | 536 ++++++++++++++++++ .../embeddables/embeddable_action_storage.ts | 126 ++++ .../public/lib/embeddables/i_embeddable.ts | 5 + 4 files changed, 673 insertions(+) create mode 100644 src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts create mode 100644 src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 534e1cae3f62d..a1b332bb65617 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -22,6 +22,7 @@ import { Adapters } from '../types'; import { IContainer } from '../containers'; import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; import { ViewMode } from '../types'; +import { EmbeddableActionStorage } from './embeddable_action_storage'; function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title; @@ -49,6 +50,11 @@ export abstract class Embeddable< // TODO: Rename to destroyed. private destoyed: boolean = false; + private __actionStorage?: EmbeddableActionStorage; + public get actionStorage(): EmbeddableActionStorage { + return this.__actionStorage || (this.__actionStorage = new EmbeddableActionStorage(this)); + } + constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { this.id = input.id; this.output = { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts new file mode 100644 index 0000000000000..56facc37fc666 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts @@ -0,0 +1,536 @@ +/* + * 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 { Embeddable } from './embeddable'; +import { EmbeddableInput } from './i_embeddable'; +import { ViewMode } from '../types'; +import { EmbeddableActionStorage, SerializedEvent } from './embeddable_action_storage'; +import { of } from '../../../../kibana_utils/common'; + +class TestEmbeddable extends Embeddable { + public readonly type = 'test'; + constructor() { + super({ id: 'test', viewMode: ViewMode.VIEW }, {}); + } + reload() {} +} + +describe('EmbeddableActionStorage', () => { + describe('.create()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.create).toBe('function'); + }); + + test('can add event to embeddable', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([]); + + await storage.create(event); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event]); + }); + + test('can create multiple events', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([]); + + await storage.create(event1); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event1]); + + await storage.create(event2); + await storage.create(event3); + + const events3 = embeddable.getInput().events || []; + expect(events3).toEqual([event1, event2, event3]); + }); + + test('throws when creating an event with the same ID', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const [, error] = await of(storage.create(event)); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[EEXIST]: Event with [eventId = EVENT_ID] already exists on [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + }); + + describe('.update()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.update).toBe('function'); + }); + + test('can update an existing event', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: { + name: 'foo', + } as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: { + name: 'bar', + } as any, + }; + + await storage.create(event1); + await storage.update(event2); + + const events = embeddable.getInput().events || []; + expect(events).toEqual([event2]); + }); + + test('updates event in place of the old event', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: { + name: 'foo', + } as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: { + name: 'bar', + } as any, + }; + const event22: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: { + name: 'baz', + } as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID', + action: { + name: 'qux', + } as any, + }; + + await storage.create(event1); + await storage.create(event2); + await storage.create(event3); + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([event1, event2, event3]); + + await storage.update(event22); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event1, event22, event3]); + + await storage.update(event2); + + const events3 = embeddable.getInput().events || []; + expect(events3).toEqual([event1, event2, event3]); + }); + + test('throws when updating event, but storage is empty', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + const [, error] = await of(storage.update(event)); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID] could not be updated as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('throws when updating event with ID that is not stored', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event1); + const [, error] = await of(storage.update(event2)); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID2] could not be updated as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + }); + + describe('.remove()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.remove).toBe('function'); + }); + + test('can remove existing event', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + await storage.remove(event.eventId); + + const events = embeddable.getInput().events || []; + expect(events).toEqual([]); + }); + + test('removes correct events in a list of events', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: { + name: 'foo', + } as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: { + name: 'bar', + } as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID', + action: { + name: 'qux', + } as any, + }; + + await storage.create(event1); + await storage.create(event2); + await storage.create(event3); + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([event1, event2, event3]); + + await storage.remove(event2.eventId); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event1, event3]); + + await storage.remove(event3.eventId); + + const events3 = embeddable.getInput().events || []; + expect(events3).toEqual([event1]); + + await storage.remove(event1.eventId); + + const events4 = embeddable.getInput().events || []; + expect(events4).toEqual([]); + }); + + test('throws when removing an event from an empty storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const [, error] = await of(storage.remove('EVENT_ID')); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID] could not be removed as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('throws when removing with ID that does not exist in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const [, error] = await of(storage.remove('WRONG_ID')); + await storage.remove(event.eventId); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = WRONG_ID] could not be removed as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + }); + + describe('.read()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.read).toBe('function'); + }); + + test('can read an existing event out of storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const event2 = await storage.read(event.eventId); + + expect(event2).toEqual(event); + }); + + test('throws when reading from empty storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const [, error] = await of(storage.read('EVENT_ID')); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID] could not be found in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('throws when reading event with ID not existing in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const [, error] = await of(storage.read('WRONG_ID')); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = WRONG_ID] could not be found in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('returns correct event when multiple events are stored', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID2', + action: {} as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID3', + action: {} as any, + }; + + await storage.create(event1); + await storage.create(event2); + await storage.create(event3); + + const event12 = await storage.read(event1.eventId); + const event22 = await storage.read(event2.eventId); + const event32 = await storage.read(event3.eventId); + + expect(event12).toEqual(event1); + expect(event22).toEqual(event2); + expect(event32).toEqual(event3); + + expect(event12).not.toEqual(event2); + }); + }); + + describe('.count()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.count).toBe('function'); + }); + + test('returns 0 when storage is empty', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const count = await storage.count(); + + expect(count).toBe(0); + }); + + test('returns correct number of events in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + expect(await storage.count()).toBe(0); + + await storage.create({ + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }); + + expect(await storage.count()).toBe(1); + + await storage.create({ + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }); + + expect(await storage.count()).toBe(2); + + await storage.remove('EVENT_ID1'); + + expect(await storage.count()).toBe(1); + + await storage.remove('EVENT_ID2'); + + expect(await storage.count()).toBe(0); + }); + }); + + describe('.list()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.list).toBe('function'); + }); + + test('returns empty array when storage is empty', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const list = await storage.list(); + + expect(list).toEqual([]); + }); + + test('returns correct list of events in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }; + + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }; + + expect(await storage.list()).toEqual([]); + + await storage.create(event1); + + expect(await storage.list()).toEqual([event1]); + + await storage.create(event2); + + expect(await storage.list()).toEqual([event1, event2]); + + await storage.remove('EVENT_ID1'); + + expect(await storage.list()).toEqual([event2]); + + await storage.remove('EVENT_ID2'); + + expect(await storage.list()).toEqual([]); + }); + }); +}); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts new file mode 100644 index 0000000000000..520f92840c5f9 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.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 { Embeddable } from '..'; + +/** + * Below two interfaces are here temporarily, they will move to `ui_actions` + * plugin once #58216 is merged. + */ +export interface SerializedEvent { + eventId: string; + triggerId: string; + action: unknown; +} +export interface ActionStorage { + create(event: SerializedEvent): Promise; + update(event: SerializedEvent): Promise; + remove(eventId: string): Promise; + read(eventId: string): Promise; + count(): Promise; + list(): Promise; +} + +export class EmbeddableActionStorage implements ActionStorage { + constructor(private readonly embbeddable: Embeddable) {} + + async create(event: SerializedEvent) { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const exists = !!events.find(({ eventId }) => eventId === event.eventId); + + if (exists) { + throw new Error( + `[EEXIST]: Event with [eventId = ${event.eventId}] already exists on ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + this.embbeddable.updateInput({ + ...input, + events: [...events, event], + }); + } + + async update(event: SerializedEvent) { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const index = events.findIndex(({ eventId }) => eventId === event.eventId); + + if (index === -1) { + throw new Error( + `[ENOENT]: Event with [eventId = ${event.eventId}] could not be ` + + `updated as it does not exist in ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + this.embbeddable.updateInput({ + ...input, + events: [...events.slice(0, index), event, ...events.slice(index + 1)], + }); + } + + async remove(eventId: string) { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const index = events.findIndex(event => eventId === event.eventId); + + if (index === -1) { + throw new Error( + `[ENOENT]: Event with [eventId = ${eventId}] could not be ` + + `removed as it does not exist in ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + this.embbeddable.updateInput({ + ...input, + events: [...events.slice(0, index), ...events.slice(index + 1)], + }); + } + + async read(eventId: string): Promise { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const event = events.find(ev => eventId === ev.eventId); + + if (!event) { + throw new Error( + `[ENOENT]: Event with [eventId = ${eventId}] could not be found in ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + return event; + } + + private __list() { + const input = this.embbeddable.getInput(); + return (input.events || []) as SerializedEvent[]; + } + + async count(): Promise { + return this.__list().length; + } + + async list(): Promise { + return this.__list(); + } +} diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 46cffab879684..62121cb0f23dd 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -30,6 +30,11 @@ export interface EmbeddableInput { hidePanelTitles?: boolean; isEmptyState?: boolean; + /** + * Reserved key for `ui_actions` events. + */ + events?: unknown; + /** * List of action IDs that this embeddable should not render. */ From bd40f557a5e4a7cbcabe1c6f6a5cc7d4d5f52320 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 25 Feb 2020 17:57:40 +0100 Subject: [PATCH 097/113] Embeddable action storage (#58438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 stub out EmbeddableActionStorage class * test: 💍 add embeddable event storage .create() tests * feat: 🎸 add EmbeddableActionStorage.update() method * feat: 🎸 add .actionStorage to Embeddable class * feat: 🎸 add EmbeddableActionStorage.remove() method * feat: 🎸 add EmbeddableActionStorage.read() method * feat: 🎸 add .list() and .count() to EmbeddableActionStorage From 120f03dc06f7f4a64e911454d6774c1d651acef1 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 25 Feb 2020 18:02:25 +0100 Subject: [PATCH 098/113] Add component (#58335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 use .story for Storybook and standartize dir struct * feat: 🎸 add component * feat: 🎸 add component * refactor: 💡 improve FlyoutCreateDrilldownAction * test: 💍 add tests * docs: ✏️ add @todo for * feat: 🎸 make name editable in * test: 💍 add name field tests * chore: 🤖 change drilldown translation keys --- .../index.tsx | 22 ++-- .../drilldowns/public/actions/index.ts | 2 +- ...ples.tsx => drilldown_hello_bar.story.tsx} | 2 +- .../drilldown_hello_bar.tsx | 26 +++++ .../components/drilldown_hello_bar/index.tsx | 22 +--- ...xamples.tsx => drilldown_picker.story.tsx} | 2 +- .../drilldown_picker/drilldown_picker.tsx | 21 ++++ .../components/drilldown_picker/index.tsx | 16 +-- .../flyout_create_drilldown.story.tsx | 24 +++++ .../flyout_create_drilldown.tsx | 34 ++++++ .../flyout_create_drilldown/i18n.ts | 14 +++ .../flyout_create_drilldown/index.ts | 7 ++ .../flyout_frame/flyout_frame.story.tsx | 39 +++++++ .../flyout_frame/flyout_frame.test.tsx | 102 ++++++++++++++++++ .../components/flyout_frame/flyout_frame.tsx | 71 ++++++++++++ .../i18n.ts} | 8 +- .../public/components/flyout_frame/index.tsx | 7 ++ .../form_create_drilldown.story.tsx | 34 ++++++ .../form_create_drilldown.test.tsx | 68 ++++++++++++ .../form_create_drilldown.tsx | 52 +++++++++ .../components/form_create_drilldown/i18n.ts | 6 +- .../form_create_drilldown/index.tsx | 25 +---- .../public/service/drilldown_service.ts | 8 +- .../plugins/drilldowns/scripts/storybook.js | 2 +- 24 files changed, 528 insertions(+), 86 deletions(-) rename x-pack/plugins/drilldowns/public/actions/{open_flyout_add_drilldown => flyout_create_drilldown}/index.tsx (61%) rename x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/{__examples__/drilldown_hello_bar.examples.tsx => drilldown_hello_bar.story.tsx} (91%) create mode 100644 x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx rename x-pack/plugins/drilldowns/public/components/drilldown_picker/{__examples__/drilldown_picker.examples.tsx => drilldown_picker.story.tsx} (91%) create mode 100644 x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx rename x-pack/plugins/drilldowns/public/components/{form_create_drilldown/__examples__/form_create_drilldown.examples.tsx => flyout_frame/i18n.ts} (51%) create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx diff --git a/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx similarity index 61% rename from x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx rename to x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx index 06f134b10a4b7..0b9f54f51f61e 100644 --- a/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx @@ -10,11 +10,11 @@ import { CoreStart } from 'src/core/public'; import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { FormCreateDrilldown } from '../../components/form_create_drilldown'; +import { FlyoutCreateDrilldown } from '../../components/flyout_create_drilldown'; export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; -interface ActionContext { +export interface FlyoutCreateDrilldownActionContext { embeddable: IEmbeddable; } @@ -22,29 +22,31 @@ export interface OpenFlyoutAddDrilldownParams { overlays: () => Promise; } -export class OpenFlyoutAddDrilldown implements Action { +export class FlyoutCreateDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; - public order = 100; + public order = 5; constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {} public getDisplayName() { - return i18n.translate('xpack.drilldowns.panel.openFlyoutAddDrilldown.displayName', { - defaultMessage: 'Add drilldown', + return i18n.translate('xpack.drilldowns.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Create Drilldown', }); } public getIconType() { - return 'empty'; + return 'plusInCircle'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible({ embeddable }: FlyoutCreateDrilldownActionContext) { return true; } - public async execute({ embeddable }: ActionContext) { + public async execute(context: FlyoutCreateDrilldownActionContext) { const overlays = await this.params.overlays(); - overlays.openFlyout(toMountPoint()); + const handle = overlays.openFlyout( + toMountPoint( handle.close()} />) + ); } } diff --git a/x-pack/plugins/drilldowns/public/actions/index.ts b/x-pack/plugins/drilldowns/public/actions/index.ts index c0ca7fac22049..ce235043b4ef6 100644 --- a/x-pack/plugins/drilldowns/public/actions/index.ts +++ b/x-pack/plugins/drilldowns/public/actions/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './open_flyout_add_drilldown'; +export * from './flyout_create_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx similarity index 91% rename from x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx rename to x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx index afa82f5e74c16..7a9e19342f27c 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; -import { DrilldownHelloBar } from '..'; +import { DrilldownHelloBar } from '.'; storiesOf('components/DrilldownHelloBar', module).add('default', () => { return ; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx new file mode 100644 index 0000000000000..1ef714f7b86e2 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx @@ -0,0 +1,26 @@ +/* + * 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'; + +export interface DrilldownHelloBarProps { + docsLink?: string; +} + +/** + * @todo https://github.com/elastic/kibana/issues/55311 + */ +export const DrilldownHelloBar: React.FC = ({ docsLink }) => { + return ( +
+

+ Drilldowns provide the ability to define a new behavior when interacting with a panel. You + can add multiple options or simply override the default filtering behavior. +

+ View docs +
+ ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx index 895a100df3ac5..f28c8cfa3a059 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx @@ -4,24 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -export interface DrilldownHelloBarProps { - docsLink?: string; -} - -export const DrilldownHelloBar: React.FC = ({ docsLink }) => { - return ( -
-

- Drilldowns provide the ability to define a new behavior when interacting with a panel. You - can add multiple options or simply override the default filtering behavior. -

- View docs - -
- ); -}; +export * from './drilldown_hello_bar'; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx similarity index 91% rename from x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx rename to x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx index dfdd9627ab5cd..5627a5d6f4522 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; -import { DrilldownPicker } from '..'; +import { DrilldownPicker } from '.'; storiesOf('components/DrilldownPicker', module).add('default', () => { return ; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx new file mode 100644 index 0000000000000..3748fc666c81c --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx @@ -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 React from 'react'; + +// eslint-disable-next-line +export interface DrilldownPickerProps {} + +export const DrilldownPicker: React.FC = () => { + return ( + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx index 3748fc666c81c..3be289fe6d46e 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx @@ -4,18 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -// eslint-disable-next-line -export interface DrilldownPickerProps {} - -export const DrilldownPicker: React.FC = () => { - return ( - - ); -}; +export * from './drilldown_picker'; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx new file mode 100644 index 0000000000000..4f024b7d9cd6a --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FlyoutCreateDrilldown } from '.'; + +storiesOf('components/FlyoutCreateDrilldown', module) + .add('default', () => { + return ; + }) + .add('open in flyout', () => { + return ( + + + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx new file mode 100644 index 0000000000000..b45ac9197c7e0 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { FormCreateDrilldown } from '../form_create_drilldown'; +import { FlyoutFrame } from '../flyout_frame'; +import { txtCreateDrilldown } from './i18n'; +import { FlyoutCreateDrilldownActionContext } from '../../actions'; + +export interface FlyoutCreateDrilldownProps { + context: FlyoutCreateDrilldownActionContext; + onClose?: () => void; +} + +export const FlyoutCreateDrilldown: React.FC = ({ + context, + onClose, +}) => { + const footer = ( + {}} fill> + {txtCreateDrilldown} + + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts new file mode 100644 index 0000000000000..ceabc6d3a9aa5 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts @@ -0,0 +1,14 @@ +/* + * 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 txtCreateDrilldown = i18n.translate( + 'xpack.drilldowns.components.FlyoutCreateDrilldown.CreateDrilldown', + { + defaultMessage: 'Create drilldown', + } +); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts new file mode 100644 index 0000000000000..ce235043b4ef6 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './flyout_create_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx new file mode 100644 index 0000000000000..2715637f6392f --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx @@ -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. + */ + +/* eslint-disable no-console */ + +import * as React from 'react'; +import { EuiFlyout, EuiButton } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FlyoutFrame } from '.'; + +storiesOf('components/FlyoutFrame', module) + .add('default', () => { + return test; + }) + .add('with title', () => { + return test; + }) + .add('with onClose', () => { + return console.log('onClose')}>test; + }) + .add('custom footer', () => { + return click me!}>test; + }) + .add('open in flyout', () => { + return ( + + Save} + onClose={() => console.log('onClose')} + > + test + + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx new file mode 100644 index 0000000000000..b5fb52fcf5c18 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { render as renderTestingLibrary, fireEvent } from '@testing-library/react'; +import { FlyoutFrame } from '.'; + +describe('', () => { + test('renders without crashing', () => { + const div = document.createElement('div'); + render(, div); + }); + + describe('[title=]', () => { + test('renders title in

tag', () => { + const div = document.createElement('div'); + render(, div); + + const title = div.querySelector('h1'); + expect(title?.textContent).toBe('foobar'); + }); + + test('title can be any react node', () => { + const div = document.createElement('div'); + render( + + foo bar + + } + />, + div + ); + + const title = div.querySelector('h1'); + expect(title?.innerHTML).toBe('foo bar'); + }); + }); + + describe('[footer=]', () => { + test('if [footer=] prop not provided, does not render footer', () => { + const div = document.createElement('div'); + render(, div); + + const footer = div.querySelector('[data-test-subj="flyoutFooter"]'); + expect(footer).toBe(null); + }); + + test('can render anything in footer', () => { + const div = document.createElement('div'); + render( + + a b + + } + />, + div + ); + + const footer = div.querySelector('[data-test-subj="flyoutFooter"]'); + expect(footer?.innerHTML).toBe('a b'); + }); + }); + + describe('[onClose=]', () => { + test('does not render close button if "onClose" prop is missing', () => { + const div = document.createElement('div'); + render(, div); + + const closeButton = div.querySelector('[data-test-subj="flyoutCloseButton"]'); + expect(closeButton).toBe(null); + }); + + test('renders close button if "onClose" prop is provided', () => { + const div = document.createElement('div'); + render( {}} />, div); + + const closeButton = div.querySelector('[data-test-subj="flyoutCloseButton"]'); + expect(closeButton).not.toBe(null); + }); + + test('calls onClose prop when close button clicked', async () => { + const onClose = jest.fn(); + const el = renderTestingLibrary(); + + const closeButton = el.queryByText('Close'); + + expect(onClose).toHaveBeenCalledTimes(0); + + fireEvent.click(closeButton!); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx new file mode 100644 index 0000000000000..2945cfd739482 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx @@ -0,0 +1,71 @@ +/* + * 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 { + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; +import { txtClose } from './i18n'; + +export interface FlyoutFrameProps { + title?: React.ReactNode; + footer?: React.ReactNode; + onClose?: () => void; +} + +/** + * @todo This component can be moved to `kibana_react`. + */ +export const FlyoutFrame: React.FC = ({ + title = '', + footer, + onClose, + children, +}) => { + const headerFragment = title && ( + + +

{title}

+
+
+ ); + + const footerFragment = (onClose || footer) && ( + + + + {onClose && ( + + {txtClose} + + )} + + + {footer} + + + + ); + + return ( + <> + {headerFragment} + {children} + {footerFragment} + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts similarity index 51% rename from x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx rename to x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts index 34f6932b41dac..257d7d36dbee1 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; -import { FormCreateDrilldown } from '..'; +import { i18n } from '@kbn/i18n'; -storiesOf('components/FormCreateDrilldown', module).add('default', () => { - return ; +export const txtClose = i18n.translate('xpack.drilldowns.components.FlyoutFrame.Close', { + defaultMessage: 'Close', }); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx new file mode 100644 index 0000000000000..040b4b6b5e243 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx @@ -0,0 +1,7 @@ +/* + * 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 * from './flyout_frame'; diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx new file mode 100644 index 0000000000000..e7e1d67473e8c --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FormCreateDrilldown } from '.'; + +const DemoEditName: React.FC = () => { + const [name, setName] = React.useState(''); + + return ; +}; + +storiesOf('components/FormCreateDrilldown', module) + .add('default', () => { + return ; + }) + .add('[name=foobar]', () => { + return ; + }) + .add('can edit name', () => ) + .add('open in flyout', () => { + return ( + + + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx new file mode 100644 index 0000000000000..6691966e47e64 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { FormCreateDrilldown } from '.'; +import { render as renderTestingLibrary, fireEvent } from '@testing-library/react'; +import { txtNameOfDrilldown } from './i18n'; + +describe('', () => { + test('renders without crashing', () => { + const div = document.createElement('div'); + render( {}} />, div); + }); + + describe('[name=]', () => { + test('if name not provided, uses to empty string', () => { + const div = document.createElement('div'); + + render(, div); + + const input = div.querySelector( + '[data-test-subj="dynamicActionNameInput"]' + ) as HTMLInputElement; + + expect(input?.value).toBe(''); + }); + + test('can set name input field value', () => { + const div = document.createElement('div'); + + render(, div); + + const input = div.querySelector( + '[data-test-subj="dynamicActionNameInput"]' + ) as HTMLInputElement; + + expect(input?.value).toBe('foo'); + + render(, div); + + expect(input?.value).toBe('bar'); + }); + + test('fires onNameChange callback on name change', () => { + const onNameChange = jest.fn(); + const utils = renderTestingLibrary( + + ); + const input = utils.getByLabelText(txtNameOfDrilldown); + + expect(onNameChange).toHaveBeenCalledTimes(0); + + fireEvent.change(input, { target: { value: 'qux' } }); + + expect(onNameChange).toHaveBeenCalledTimes(1); + expect(onNameChange).toHaveBeenCalledWith('qux'); + + fireEvent.change(input, { target: { value: 'quxx' } }); + + expect(onNameChange).toHaveBeenCalledTimes(2); + expect(onNameChange).toHaveBeenCalledWith('quxx'); + }); + }); +}); diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx new file mode 100644 index 0000000000000..4422de604092b --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { DrilldownHelloBar } from '../drilldown_hello_bar'; +import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n'; +import { DrilldownPicker } from '../drilldown_picker'; + +const noop = () => {}; + +export interface FormCreateDrilldownProps { + name?: string; + onNameChange?: (name: string) => void; +} + +export const FormCreateDrilldown: React.FC = ({ + name = '', + onNameChange = noop, +}) => { + const nameFragment = ( + + onNameChange(event.target.value)} + data-test-subj="dynamicActionNameInput" + /> + + ); + + const triggerPicker =
Trigger Picker will be here
; + const actionPicker = ( + + + + ); + + return ( + <> + + {nameFragment} + {triggerPicker} + {actionPicker} + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts index 922131ba4b901..4c0e287935edd 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts @@ -7,21 +7,21 @@ import { i18n } from '@kbn/i18n'; export const txtNameOfDrilldown = i18n.translate( - 'xpack.drilldowns.components.form_create_drilldown.nameOfDrilldown', + 'xpack.drilldowns.components.FormCreateDrilldown.nameOfDrilldown', { defaultMessage: 'Name of drilldown', } ); export const txtUntitledDrilldown = i18n.translate( - 'xpack.drilldowns.components.form_create_drilldown.untitledDrilldown', + 'xpack.drilldowns.components.FormCreateDrilldown.untitledDrilldown', { defaultMessage: 'Untitled drilldown', } ); export const txtDrilldownAction = i18n.translate( - 'xpack.drilldowns.components.form_create_drilldown.drilldownAction', + 'xpack.drilldowns.components.FormCreateDrilldown.drilldownAction', { defaultMessage: 'Drilldown action', } diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx index 40cd4cf2b210b..c2c5a7e435b39 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx @@ -4,27 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { DrilldownHelloBar } from '../drilldown_hello_bar'; -import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n'; -import { DrilldownPicker } from '../drilldown_picker'; - -// eslint-disable-next-line -export interface FormCreateDrilldownProps {} - -export const FormCreateDrilldown: React.FC = () => { - return ( -
- - - - - - - - - -
- ); -}; +export * from './form_create_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts index f22f452181648..b1310ba003ff7 100644 --- a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts +++ b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts @@ -5,17 +5,17 @@ */ import { CoreSetup } from 'src/core/public'; -import { OpenFlyoutAddDrilldown } from '../actions/open_flyout_add_drilldown'; +import { FlyoutCreateDrilldownAction } from '../actions'; import { DrilldownsSetupDependencies } from '../plugin'; export class DrilldownService { bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) { - const actionOpenFlyoutAddDrilldown = new OpenFlyoutAddDrilldown({ + const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays: async () => (await core.getStartServices())[0].overlays, }); - uiActions.registerAction(actionOpenFlyoutAddDrilldown); - uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionOpenFlyoutAddDrilldown.id); + uiActions.registerAction(actionFlyoutCreateDrilldown); + uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionFlyoutCreateDrilldown.id); } /** diff --git a/x-pack/plugins/drilldowns/scripts/storybook.js b/x-pack/plugins/drilldowns/scripts/storybook.js index 9b0f57746e584..2bfd0eb1a8f19 100644 --- a/x-pack/plugins/drilldowns/scripts/storybook.js +++ b/x-pack/plugins/drilldowns/scripts/storybook.js @@ -9,5 +9,5 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ name: 'drilldowns', - storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.examples.tsx')], + storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], }); From fac57c1cca66dc3aeccb535ccf99f5dba7b76535 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 25 Feb 2020 18:25:11 +0100 Subject: [PATCH 099/113] [Visualize] Improve linked saved search functional test (#58339) * Remove redundant sleep * convert to TypeScript --- ..._linked_saved_searches.js => _linked_saved_searches.ts} | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) rename test/functional/apps/visualize/{_linked_saved_searches.js => _linked_saved_searches.ts} (95%) diff --git a/test/functional/apps/visualize/_linked_saved_searches.js b/test/functional/apps/visualize/_linked_saved_searches.ts similarity index 95% rename from test/functional/apps/visualize/_linked_saved_searches.js rename to test/functional/apps/visualize/_linked_saved_searches.ts index 37ec3f06f2ecd..345987a803394 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.js +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -18,8 +18,9 @@ */ import expect from '@kbn/expect'; - -export default function({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const retry = getService('retry'); const PageObjects = getPageObjects([ @@ -40,8 +41,6 @@ export default function({ getService, getPageObjects }) { await filterBar.addFilter('extension.raw', 'is', 'jpg'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.saveSearch(savedSearchName); - // TODO: Remove this once https://github.com/elastic/kibana/issues/19750 is properly resolved - await PageObjects.common.sleep(500); }); it('should create a visualization from a saved search', async () => { From 93556a9bdddc671cabe218973b86ee8d7ecb5273 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 25 Feb 2020 18:31:24 +0100 Subject: [PATCH 100/113] Visualizations plugin np_ready (#57983) Partially addresses #50897 updates visualizations plugin makes visualizations/saved_visualizations np_ready makes visualizations/embeddable np_ready remove DefaultVisEditor dependency from visualizations import AggConfigs directly instead of ui/public Clean up imports from root. use relative imports Remove bind directive import --- src/legacy/core_plugins/data/public/index.ts | 2 + .../public/visualize/kibana_services.ts | 2 + .../kibana/public/visualize/legacy_imports.ts | 3 +- .../np_ready/editor/visualization_editor.js | 2 +- .../kibana/public/visualize/plugin.ts | 4 +- .../public/default_editor.tsx | 6 +- .../public/default_editor_controller.tsx | 2 +- .../public/embeddable/query_geohash_bounds.ts | 103 ------------------ .../visualizations/public/index.scss | 2 - .../visualizations/public/legacy_imports.ts | 8 +- .../public/np_ready/kibana.json | 5 +- .../public/np_ready/public/_index.scss | 1 + .../public}/embeddable/_embeddables.scss | 0 .../public}/embeddable/_index.scss | 0 .../embeddable/_visualize_lab_disabled.scss | 0 .../public}/embeddable/constants.ts | 0 .../embeddable/disabled_lab_embeddable.tsx | 2 +- .../embeddable/disabled_lab_visualization.tsx | 0 .../public}/embeddable/get_index_pattern.ts | 6 +- .../{ => np_ready/public}/embeddable/index.ts | 1 + .../embeddable/visualize_embeddable.ts | 47 ++++---- .../visualize_embeddable_factory.tsx | 37 +++---- .../expressions/visualization_function.ts | 7 +- .../public/np_ready/public/index.ts | 16 ++- .../public/np_ready/public/legacy.ts | 4 +- .../__tests__/vis_types/base_vis_type.js | 2 +- .../__tests__/vis_types/react_vis_type.js | 2 +- .../public/legacy/build_pipeline.test.ts | 5 - .../np_ready/public/legacy/build_pipeline.ts | 16 +-- .../public/np_ready/public/mocks.ts | 7 +- .../public/np_ready/public/plugin.ts | 62 ++++++----- .../saved_visualizations/_saved_vis.ts | 16 ++- .../find_list_items.test.ts} | 67 +++++++----- .../saved_visualizations/find_list_items.ts} | 33 ++++-- .../public}/saved_visualizations/index.ts | 0 .../saved_visualization_references.test.ts | 10 +- .../saved_visualization_references.ts | 4 +- .../saved_visualizations.ts | 11 +- .../public/np_ready/public/services.ts | 23 +++- .../public/np_ready/public/types.ts | 38 +++++++ .../public/np_ready/public/vis.ts | 2 +- .../public/np_ready/public/vis_impl.d.ts | 2 +- .../{types => vis_types}/base_vis_type.js | 4 +- .../public/{types => vis_types}/index.ts | 0 .../{types => vis_types}/react_vis_type.js | 0 .../{types => vis_types}/types_service.ts | 0 .../vis_type_alias_registry.ts | 2 +- .../public/wizard/new_vis_modal.test.tsx | 4 +- .../np_ready/public/wizard/new_vis_modal.tsx | 4 +- .../search_selection/search_selection.tsx | 4 +- .../wizard/type_selection/new_vis_help.tsx | 2 +- .../wizard/type_selection/type_selection.tsx | 2 +- .../save_modal/saved_object_save_modal.tsx | 6 +- .../expression_types/embeddable_types.ts | 2 +- .../functions/common/saved_visualization.ts | 2 +- .../embeddable_input_to_expression.test.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 58 files changed, 279 insertions(+), 316 deletions(-) delete mode 100644 src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/_embeddables.scss (100%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/_index.scss (100%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/_visualize_lab_disabled.scss (100%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/constants.ts (100%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/disabled_lab_embeddable.tsx (94%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/disabled_lab_visualization.tsx (100%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/get_index_pattern.ts (90%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/index.ts (92%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/visualize_embeddable.ts (90%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/embeddable/visualize_embeddable_factory.tsx (85%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/saved_visualizations/_saved_vis.ts (90%) rename src/legacy/core_plugins/visualizations/public/{saved_visualizations/find_list_items.test.js => np_ready/public/saved_visualizations/find_list_items.test.ts} (77%) rename src/legacy/core_plugins/visualizations/public/{saved_visualizations/find_list_items.js => np_ready/public/saved_visualizations/find_list_items.ts} (64%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/saved_visualizations/index.ts (100%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/saved_visualizations/saved_visualization_references.test.ts (96%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/saved_visualizations/saved_visualization_references.ts (95%) rename src/legacy/core_plugins/visualizations/public/{ => np_ready/public}/saved_visualizations/saved_visualizations.ts (91%) create mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts rename src/legacy/core_plugins/visualizations/public/np_ready/public/{types => vis_types}/base_vis_type.js (95%) rename src/legacy/core_plugins/visualizations/public/np_ready/public/{types => vis_types}/index.ts (100%) rename src/legacy/core_plugins/visualizations/public/np_ready/public/{types => vis_types}/react_vis_type.js (100%) rename src/legacy/core_plugins/visualizations/public/np_ready/public/{types => vis_types}/types_service.ts (100%) rename src/legacy/core_plugins/visualizations/public/np_ready/public/{types => vis_types}/vis_type_alias_registry.ts (97%) diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index ce46f534141f4..8cde5d0a1fc11 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -54,6 +54,7 @@ export * from '../common'; export { FilterStateManager } from './filter/filter_manager'; export { // agg_types TODO need to group these under a namespace or prefix + AggConfigs, AggParamType, AggTypeFilters, // TODO convert to interface aggTypeFilters, @@ -66,6 +67,7 @@ export { convertIPRangeToString, intervalOptions, // only used in Discover isDateHistogramBucketAggConfig, + setBounds, isStringType, isType, isValidInterval, diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 096877d5824c4..cfd12b3283459 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,6 +36,7 @@ import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; +import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; @@ -60,6 +61,7 @@ export interface VisualizeKibanaServices { usageCollection?: UsageCollectionSetup; I18nContext: I18nStart['Context']; setActiveUrl: (newUrl: string) => void; + DefaultVisualizationEditor: typeof DefaultEditorController; } let services: VisualizeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 8b1bb0fda8c84..d9565938c838d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -43,8 +43,7 @@ export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { wrapInI18nContext } from 'ui/i18n'; export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; -export { VisSavedObject } from '../../../visualizations/public/embeddable/visualize_embeddable'; -export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; +export { VisSavedObject, VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/'; export { configureAppAngularModule, ensureDefaultIndexPattern, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index c40a10115ae4e..65c25b8cf705d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -30,7 +30,7 @@ export function initVisEditorDirective(app, deps) { appState: '=', }, link: function($scope, element) { - const Editor = $scope.savedObj.vis.type.editor; + const Editor = $scope.savedObj.vis.type.editor || deps.DefaultVisualizationEditor; const editor = new Editor(element[0], $scope.savedObj); $scope.renderFunction = () => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 22804685db3cc..9f2283d29c203 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -50,6 +50,7 @@ import { HomePublicPluginSetup, } from '../../../../../plugins/home/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; +import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -139,7 +140,7 @@ export class VisualizePlugin implements Plugin { localStorage: new Storage(localStorage), navigation, savedObjectsClient, - savedVisualizations: visualizations.getSavedVisualizationsLoader(), + savedVisualizations: visualizations.savedVisualizationsLoader, savedQueryService: dataStart.query.savedQueries, share, toastNotifications: coreStart.notifications.toasts, @@ -150,6 +151,7 @@ export class VisualizePlugin implements Plugin { usageCollection, I18nContext: coreStart.i18n.Context, setActiveUrl, + DefaultVisualizationEditor: DefaultEditorController, }; setServices(deps); diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx index 32ea71c0bc005..7eee54006f684 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx @@ -20,8 +20,10 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EditorRenderProps } from '../../kibana/public/visualize/np_ready/types'; -import { VisualizeEmbeddable } from '../../visualizations/public/embeddable'; -import { VisualizeEmbeddableFactory } from '../../visualizations/public/embeddable/visualize_embeddable_factory'; +import { + VisualizeEmbeddableContract as VisualizeEmbeddable, + VisualizeEmbeddableFactoryContract as VisualizeEmbeddableFactory, +} from '../../visualizations/public/'; import { PanelsContainer, Panel } from '../../../../plugins/kibana_react/public'; import './vis_type_agg_filter'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx index d3090d277aef9..db910604eddd1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; -import { VisSavedObject } from 'src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable'; +import { VisSavedObject } from 'src/legacy/core_plugins/visualizations/public/'; import { Storage } from '../../../../plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; import { DefaultEditor } from './default_editor'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts b/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts deleted file mode 100644 index f37bc858efab0..0000000000000 --- a/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { toastNotifications } from 'ui/notify'; - -import { IAggConfig } from 'ui/agg_types'; -import { timefilter } from 'ui/timefilter'; -import { Vis } from '../np_ready/public'; -import { Filter, Query, SearchSource, ISearchSource } from '../../../../../plugins/data/public'; - -interface QueryGeohashBoundsParams { - filters?: Filter[]; - query?: Query; - searchSource?: ISearchSource; -} - -/** - * Coordinate map visualization needs to be able to query for the latest geohash - * bounds when a user clicks the "fit to data" map icon, which requires knowing - * about global filters & queries. This logic has been extracted here so we can - * keep `searchSource` out of the vis, but ultimately we need to design a - * long-term solution for situations like this. - * - * TODO: Remove this as a part of elastic/kibana#30593 - */ -export async function queryGeohashBounds(vis: Vis, params: QueryGeohashBoundsParams) { - const agg = vis.getAggConfig().aggs.find((a: IAggConfig) => { - return get(a, 'type.dslName') === 'geohash_grid'; - }); - - if (agg) { - const searchSource = params.searchSource - ? params.searchSource.createChild() - : new SearchSource(); - searchSource.setField('size', 0); - searchSource.setField('aggs', () => { - const geoBoundsAgg = vis.getAggConfig().createAggConfig( - { - type: 'geo_bounds', - enabled: true, - params: { - field: agg.getField(), - }, - schema: 'metric', - }, - { - addToAggConfigs: false, - } - ); - return { - '1': geoBoundsAgg.toDsl(), - }; - }); - - const { filters, query } = params; - if (filters) { - searchSource.setField('filter', () => { - const activeFilters = [...filters]; - const indexPattern = agg.getIndexPattern(); - const useTimeFilter = !!indexPattern.timeFieldName; - if (useTimeFilter) { - const filter = timefilter.createFilter(indexPattern); - if (filter) activeFilters.push((filter as any) as Filter); - } - return activeFilters; - }); - } - if (query) { - searchSource.setField('query', query); - } - - try { - const esResp = await searchSource.fetch(); - return get(esResp, 'aggregations.1.bounds'); - } catch (error) { - toastNotifications.addDanger({ - title: i18n.translate('visualizations.queryGeohashBounds.unableToGetBoundErrorTitle', { - defaultMessage: 'Unable to get bounds', - }), - text: `${error.message}`, - }); - return; - } - } -} diff --git a/src/legacy/core_plugins/visualizations/public/index.scss b/src/legacy/core_plugins/visualizations/public/index.scss index 748945eabd331..238f58fbfa295 100644 --- a/src/legacy/core_plugins/visualizations/public/index.scss +++ b/src/legacy/core_plugins/visualizations/public/index.scss @@ -1,4 +1,2 @@ @import 'src/legacy/ui/public/styles/styling_constants'; - -@import './embeddable/index'; @import './np_ready/public/index'; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index 5cff588d951b0..223c130df3505 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -24,9 +24,5 @@ export { IAggConfigs, isDateHistogramBucketAggConfig, setBounds, -} from '../../../ui/public/agg_types'; -export { createFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; -export { I18nContext } from '../../../ui/public/i18n'; -import chrome from '../../../ui/public/chrome'; -export { chrome as legacyChrome }; -import '../../../ui/public/directives/bind'; +} from '../../data/public'; +export { createSavedSearchesLoader } from '../../kibana/public/discover/saved_searches/'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json b/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json index 888edde44a261..d4f9bd327d6ac 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json +++ b/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json @@ -3,8 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [ - "data", - "search" - ] + "requiredPlugins": ["data", "search", "expressions", "uiActions"] } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss index d87b6b004a511..eada763b63c4d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss @@ -1 +1,2 @@ @import 'wizard/index'; +@import 'embeddable/index'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/_embeddables.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_embeddables.scss similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/_embeddables.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_embeddables.scss diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_index.scss similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/_index.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_index.scss diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_visualize_lab_disabled.scss similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_visualize_lab_disabled.scss diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/constants.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/constants.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants.ts diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_embeddable.tsx similarity index 94% rename from src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_embeddable.tsx index f9dfd5d2b98f4..fbb2eba3afe79 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_embeddable.tsx @@ -19,7 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Embeddable, EmbeddableOutput } from '../../../../../plugins/embeddable/public'; +import { Embeddable, EmbeddableOutput } from '../../../../../../../plugins/embeddable/public'; import { DisabledLabVisualization } from './disabled_lab_visualization'; import { VisualizeInput } from './visualize_embeddable'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_visualization.tsx similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_visualization.tsx diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/get_index_pattern.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/get_index_pattern.ts index cfb2960cfbb7c..51d839275fd27 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/get_index_pattern.ts @@ -17,13 +17,13 @@ * under the License. */ -import { VisSavedObject } from './visualize_embeddable'; +import { VisSavedObject } from '../types'; import { indexPatterns, IIndexPattern, IndexPatternAttributes, -} from '../../../../../plugins/data/public'; -import { getUISettings, getSavedObjects } from '../np_ready/public/services'; +} from '../../../../../../../plugins/data/public'; +import { getUISettings, getSavedObjects } from '../services'; export async function getIndexPattern( savedVis: VisSavedObject diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/index.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/embeddable/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/index.ts index d7c0205891ec5..a1cd31eebef20 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/index.ts @@ -18,4 +18,5 @@ */ export { DisabledLabEmbeddable } from './disabled_lab_embeddable'; export { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable'; +export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; export { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts index 32bbae13b79b8..2537caa01cd46 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts @@ -18,13 +18,8 @@ */ import _, { get } from 'lodash'; -import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; -import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; -import { npStart } from 'ui/new_platform'; -import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; -import { EmbeddableVisTriggerContext } from 'src/plugins/embeddable/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -32,9 +27,8 @@ import { Query, esFilters, Filter, - ISearchSource, TimefilterContract, -} from '../../../../../plugins/data/public'; +} from '../../../../../../../plugins/data/public'; import { EmbeddableInput, EmbeddableOutput, @@ -42,27 +36,21 @@ import { Container, selectRangeTrigger, valueClickTrigger, -} from '../../../../../plugins/embeddable/public'; -import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; -import { SavedObject } from '../../../../../plugins/saved_objects/public'; -import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; -import { Vis } from '../np_ready/public'; + EmbeddableVisTriggerContext, +} from '../../../../../../../plugins/embeddable/public'; +import { dispatchRenderComplete } from '../../../../../../../plugins/kibana_utils/public'; +import { + IExpressionLoaderParams, + ExpressionsStart, +} from '../../../../../../../plugins/expressions/public'; +import { buildPipeline } from '../legacy/build_pipeline'; +import { Vis } from '../vis'; +import { getExpressions, getUiActions } from '../services'; +import { PersistedState } from '../../../legacy_imports'; +import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; -export interface VisSavedObject extends SavedObject { - vis: Vis; - description?: string; - searchSource: ISearchSource; - title: string; - uiStateJSON?: string; - destroy: () => void; - savedSearchRefName?: string; - savedSearchId?: string; - savedSearch?: SavedSearch; - visState: any; -} - export interface VisualizeEmbeddableConfiguration { savedVisualization: VisSavedObject; indexPatterns?: IIndexPattern[]; @@ -90,7 +78,7 @@ export interface VisualizeOutput extends EmbeddableOutput { visTypeName: string; } -type ExpressionLoader = InstanceType; +type ExpressionLoader = InstanceType; export class VisualizeEmbeddable extends Embeddable { private handler?: ExpressionLoader; @@ -281,7 +269,8 @@ export class VisualizeEmbeddable extends Embeddable { @@ -309,7 +298,9 @@ export class VisualizeEmbeddable extends Embeddable { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - constructor( - private timefilter: TimefilterContract, - private getSavedVisualizationsLoader: () => SavedVisualizations - ) { + constructor() { super({ savedObjectMetaData: { name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), @@ -101,7 +98,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< input: Partial & { id: string }, parent?: Container ): Promise { - const savedVisualizations = this.getSavedVisualizationsLoader(); + const savedVisualizations = getSavedVisualizationsLoader(); try { const visId = savedObject.id as string; @@ -118,7 +115,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< const indexPattern = await getIndexPattern(savedObject); const indexPatterns = indexPattern ? [indexPattern] : []; return new VisualizeEmbeddable( - this.timefilter, + getTimeFilter(), { savedVisualization: savedObject, indexPatterns, @@ -141,7 +138,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< input: Partial & { id: string }, parent?: Container ): Promise { - const savedVisualizations = this.getSavedVisualizationsLoader(); + const savedVisualizations = getSavedVisualizationsLoader(); try { const savedObject = await savedVisualizations.get(savedObjectId); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts index f1c1677a60f26..4ac0931c5d865 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts @@ -19,8 +19,11 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { VisResponseValue } from 'src/plugins/visualizations/public'; -import { ExpressionFunctionDefinition, Render } from 'src/plugins/expressions/public'; +import { VisResponseValue } from '../../../../../../../plugins/visualizations/public'; +import { + ExpressionFunctionDefinition, + Render, +} from '../../../../../../../plugins/expressions/public'; import { PersistedState } from '../../../legacy_imports'; import { getTypes, getIndexPatterns, getFilterManager } from '../services'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 3c4a1c1449d47..34ffb698e5f8c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -29,22 +29,28 @@ * either types, or static code. */ -import { PluginInitializerContext } from 'src/core/public'; +import { PublicContract } from '@kbn/utility-types'; +import { PluginInitializerContext } from '../../../../../../core/public'; import { VisualizationsPlugin, VisualizationsSetup, VisualizationsStart } from './plugin'; /** @public */ export { VisualizationsSetup, VisualizationsStart }; /** @public types */ -export { VisTypeAlias, VisType } from './types'; +export { VisTypeAlias, VisType } from './vis_types'; +export { VisSavedObject } from './types'; +export { Vis, VisParams, VisState } from './vis'; +import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable'; +export type VisualizeEmbeddableFactoryContract = PublicContract; +export type VisualizeEmbeddableContract = PublicContract; export function plugin(initializerContext: PluginInitializerContext) { return new VisualizationsPlugin(initializerContext); } /** @public static code */ -export { Vis, VisParams, VisState } from './vis'; -export { TypesService } from './types/types_service'; +export { TypesService } from './vis_types/types_service'; +export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable'; export { Status } from './legacy/update_status'; export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/build_pipeline'; @@ -52,4 +58,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; +export { createSavedVisLoader } from './saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts index 41b23b276e88d..57c686b6e9cb0 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts @@ -17,12 +17,12 @@ * under the License. */ -import { PluginInitializerContext } from 'src/core/public'; - /* eslint-disable @kbn/eslint/no-restricted-paths */ import { npSetup, npStart } from 'ui/new_platform'; /* eslint-enable @kbn/eslint/no-restricted-paths */ +import { PluginInitializerContext } from '../../../../../../core/public'; + import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js index b8aa33d0a5abe..9c1dfd9780255 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { BaseVisType } from '../../../types/base_vis_type'; +import { BaseVisType } from '../../../vis_types/base_vis_type'; describe('Base Vis Type', function() { beforeEach(ngMock.module('kibana')); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js index c85557ea1b0b0..2474a58870424 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { ReactVisType } from '../../../types/react_vis_type'; +import { ReactVisType } from '../../../vis_types/react_vis_type'; describe('React Vis Type', function() { const visConfig = { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts index f73dc3e19d0ef..1adf6fd23f5a5 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts @@ -31,11 +31,6 @@ import { IAggConfig } from '../../../legacy_imports'; import { searchSourceMock } from '../../../legacy_mocks'; jest.mock('ui/new_platform'); -jest.mock('ui/agg_types', () => ({ - setBounds: () => {}, - dateHistogramBucketAgg: () => {}, - isDateHistogramBucketAggConfig: () => true, -})); describe('visualize loader pipeline helpers: build pipeline', () => { describe('prepareJson', () => { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index 025eef834ca86..155213b4103b0 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -18,17 +18,11 @@ */ import { cloneDeep, get } from 'lodash'; -// @ts-ignore import moment from 'moment'; -import { SerializedFieldFormat } from 'src/plugins/expressions/public'; -import { ISearchSource } from 'src/plugins/data/public'; -import { - IAggConfig, - setBounds, - isDateHistogramBucketAggConfig, - createFormat, -} from '../../../legacy_imports'; -import { Vis, VisParams } from '..'; +import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/public'; +import { fieldFormats, ISearchSource } from '../../../../../../../plugins/data/public'; +import { IAggConfig, setBounds, isDateHistogramBucketAggConfig } from '../../../legacy_imports'; +import { Vis, VisParams } from '../types'; interface SchemaConfigParams { precision?: number; @@ -102,7 +96,7 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => { 'max_bucket', ].includes(agg.type.name); - const format = createFormat( + const format = fieldFormats.serialize( hasSubAgg ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) : agg diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index 9fb87cadb2983..b3dd22f62f81f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -23,7 +23,7 @@ jest.mock('ui/vis/vis_factory'); jest.mock('ui/registry/vis_types'); jest.mock('./types/vis_type_alias_registry'); -import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext } from '../../../../../../core/public'; import { VisualizationsSetup, VisualizationsStart } from './'; import { VisualizationsPlugin } from './plugin'; import { coreMock } from '../../../../../../core/public/mocks'; @@ -31,6 +31,7 @@ import { embeddablePluginMock } from '../../../../../../plugins/embeddable/publi import { expressionsPluginMock } from '../../../../../../plugins/expressions/public/mocks'; import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../../../../plugins/usage_collection/public/mocks'; +import { uiActionsPluginMock } from '../../../../../../plugins/ui_actions/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ types: { @@ -47,7 +48,7 @@ const createStartContract = (): VisualizationsStart => ({ all: jest.fn(), getAliases: jest.fn(), }, - getSavedVisualizationsLoader: jest.fn(), + savedVisualizationsLoader: {} as any, showNewVisModal: jest.fn(), Vis: jest.fn(), }); @@ -64,6 +65,8 @@ const createInstance = async () => { const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), + expressions: expressionsPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); return { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 20bed59faad88..e1d87d414d398 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -17,8 +17,13 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { TypesService, TypesSetup, TypesStart } from './types'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, +} from '../../../../../../core/public'; +import { TypesService, TypesSetup, TypesStart } from './vis_types'; import { setUISettings, setTypes, @@ -29,10 +34,13 @@ import { setSavedObjects, setUsageCollector, setFilterManager, + setExpressions, + setUiActions, + setSavedVisualizationsLoader, + setTimeFilter, } from './services'; -import { VisualizeEmbeddableFactory } from '../../embeddable/visualize_embeddable_factory'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../embeddable'; -import { ExpressionsSetup } from '../../../../../../plugins/expressions/public'; +import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../../plugins/expressions/public'; import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; @@ -41,13 +49,11 @@ import { DataPublicPluginStart, } from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; -import { - createSavedVisLoader, - SavedObjectKibanaServicesWithVisualizations, -} from '../../saved_visualizations'; -import { SavedVisualizations } from '../../../../kibana/public/visualize/np_ready/types'; +import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; import { VisImpl, VisImplConstructor } from './vis_impl'; import { showNewVisModal } from './wizard'; +import { UiActionsStart } from '../../../../../../plugins/ui_actions/public'; + /** * Interface for this plugin's returned setup/start contracts. * @@ -59,9 +65,9 @@ export interface VisualizationsSetup { export interface VisualizationsStart { types: TypesStart; - getSavedVisualizationsLoader: () => SavedVisualizations; - showNewVisModal: typeof showNewVisModal; + savedVisualizationsLoader: SavedVisualizationsLoader; Vis: VisImplConstructor; + showNewVisModal: typeof showNewVisModal; } export interface VisualizationsSetupDeps { @@ -73,6 +79,8 @@ export interface VisualizationsSetupDeps { export interface VisualizationsStartDeps { data: DataPublicPluginStart; + expressions: ExpressionsStart; + uiActions: UiActionsStart; } /** @@ -92,8 +100,6 @@ export class VisualizationsPlugin VisualizationsStartDeps > { private readonly types: TypesService = new TypesService(); - private savedVisualizations?: SavedVisualizations; - private savedVisualizationDependencies?: SavedObjectKibanaServicesWithVisualizations; constructor(initializerContext: PluginInitializerContext) {} @@ -107,10 +113,7 @@ export class VisualizationsPlugin expressions.registerFunction(visualizationFunction); expressions.registerRenderer(visualizationRenderer); - const embeddableFactory = new VisualizeEmbeddableFactory( - data.query.timefilter.timefilter, - this.getSavedVisualizationsLoader - ); + const embeddableFactory = new VisualizeEmbeddableFactory(); embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); return { @@ -118,7 +121,10 @@ export class VisualizationsPlugin }; } - public start(core: CoreStart, { data }: VisualizationsStartDeps): VisualizationsStart { + public start( + core: CoreStart, + { data, expressions, uiActions }: VisualizationsStartDeps + ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); setTypes(types); @@ -127,31 +133,27 @@ export class VisualizationsPlugin setSavedObjects(core.savedObjects); setIndexPatterns(data.indexPatterns); setFilterManager(data.query.filterManager); - - this.savedVisualizationDependencies = { + setExpressions(expressions); + setUiActions(uiActions); + setTimeFilter(data.query.timefilter.timefilter); + const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, chrome: core.chrome, overlays: core.overlays, visualizationTypes: types, - }; + }); + setSavedVisualizationsLoader(savedVisualizationsLoader); return { types, - getSavedVisualizationsLoader: () => this.getSavedVisualizationsLoader(), showNewVisModal, Vis: VisImpl, + savedVisualizationsLoader, }; } public stop() { this.types.stop(); } - - private getSavedVisualizationsLoader = () => { - if (!this.savedVisualizations) { - this.savedVisualizations = createSavedVisLoader(this.savedVisualizationDependencies!); - } - return this.savedVisualizations; - }; } diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/_saved_vis.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts index f4548da375216..f3539b3564c56 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts @@ -28,15 +28,13 @@ import { createSavedObjectClass, SavedObject, SavedObjectKibanaServices, -} from '../../../../../plugins/saved_objects/public'; -import { updateOldState } from '../index'; +} from '../../../../../../../plugins/saved_objects/public'; +import { updateOldState } from '../../../index'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern } from '../../../../../plugins/data/public'; -import { VisSavedObject } from '../embeddable/visualize_embeddable'; - -import { createSavedSearchesLoader } from '../../../kibana/public/discover'; -import { VisualizeConstants } from '../../../kibana/public/visualize'; -import { VisImpl } from '../np_ready/public/vis_impl'; +import { IIndexPattern } from '../../../../../../../plugins/data/public'; +import { VisSavedObject } from '../types'; +import { VisImpl } from '../vis_impl'; +import { createSavedSearchesLoader } from '../../../legacy_imports'; async function _afterEsResp(savedVis: VisSavedObject, services: any) { await _getLinkedSavedSearch(savedVis, services); @@ -138,7 +136,7 @@ export function createSavedVisClass(services: SavedObjectKibanaServices) { }); this.showInRecentlyAccessed = true; this.getFullPath = () => { - return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; + return `/app/kibana#/visualize/edit/${this.id}`; }; } } diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.test.ts similarity index 77% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.test.ts index ed0f6dc429ef4..d1def09978dbb 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.test.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.test.ts @@ -18,19 +18,24 @@ */ import { findListItems } from './find_list_items'; +import { coreMock } from '../../../../../../../core/public/mocks'; +import { SavedObjectsClientContract } from '../../../../../../../core/public'; +import { VisTypeAlias } from '../vis_types'; describe('saved_visualizations', () => { function testProps() { + const savedObjects = coreMock.createStart().savedObjects.client as jest.Mocked< + SavedObjectsClientContract + >; + (savedObjects.find as jest.Mock).mockImplementation(() => ({ + total: 0, + savedObjects: [], + })); return { visTypes: [], search: '', size: 10, - savedObjectsClient: { - find: jest.fn(async () => ({ - total: 0, - savedObjects: [], - })), - }, + savedObjectsClient: savedObjects, mapSavedObjectApiHits: jest.fn(), }; } @@ -60,7 +65,7 @@ describe('saved_visualizations', () => { searchFields: ['baz', 'bing'], }, }, - }, + } as VisTypeAlias, ], }; const { find } = props.savedObjectsClient; @@ -86,7 +91,7 @@ describe('saved_visualizations', () => { searchFields: ['baz', 'bing', 'barfield'], }, }, - }, + } as VisTypeAlias, { appExtensions: { visualizations: { @@ -94,7 +99,7 @@ describe('saved_visualizations', () => { searchFields: ['baz', 'bing', 'foofield'], }, }, - }, + } as VisTypeAlias, ], }; const { find } = props.savedObjectsClient; @@ -128,24 +133,11 @@ describe('saved_visualizations', () => { it('uses type-specific toListItem function, if available', async () => { const props = { ...testProps(), - savedObjectsClient: { - find: jest.fn(async () => ({ - total: 2, - savedObjects: [ - { - id: 'lotr', - type: 'wizard', - attributes: { label: 'Gandalf' }, - }, - { - id: 'wat', - type: 'visualization', - attributes: { title: 'WATEVER' }, - }, - ], - })), - }, - mapSavedObjectApiHits(savedObject) { + mapSavedObjectApiHits(savedObject: { + id: string; + type: string; + attributes: { title: string }; + }) { return { id: savedObject.id, title: `DEFAULT ${savedObject.attributes.title}`, @@ -159,14 +151,31 @@ describe('saved_visualizations', () => { toListItem(savedObject) { return { id: savedObject.id, - title: `${savedObject.attributes.label} THE GRAY`, + title: `${(savedObject.attributes as { label: string }).label} THE GRAY`, }; }, }, }, - }, + } as VisTypeAlias, ], }; + + (props.savedObjectsClient.find as jest.Mock).mockImplementationOnce(async () => ({ + total: 2, + savedObjects: [ + { + id: 'lotr', + type: 'wizard', + attributes: { label: 'Gandalf' }, + }, + { + id: 'wat', + type: 'visualization', + attributes: { title: 'WATEVER' }, + }, + ], + })); + const items = await findListItems(props); expect(items).toEqual({ total: 2, diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.ts similarity index 64% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.ts index a7fcee67adf72..02db90a762e89 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.ts @@ -18,6 +18,13 @@ */ import _ from 'lodash'; +import { + SavedObjectAttributes, + SavedObjectsClientContract, +} from '../../../../../../../core/public'; +import { SavedObjectLoader } from '../../../../../../../plugins/saved_objects/public'; +import { VisTypeAlias } from '../vis_types'; +import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry'; /** * Search for visualizations and convert them into a list display-friendly format. @@ -28,34 +35,42 @@ export async function findListItems({ size, savedObjectsClient, mapSavedObjectApiHits, +}: { + search: string; + size: number; + visTypes: VisTypeAlias[]; + savedObjectsClient: SavedObjectsClientContract; + mapSavedObjectApiHits: SavedObjectLoader['mapSavedObjectApiHits']; }) { - const extensions = _.compact( - visTypes.map(v => v.appExtensions && v.appExtensions.visualizations) - ); + const extensions = visTypes + .map(v => v.appExtensions?.visualizations) + .filter(Boolean) as VisualizationsAppExtension[]; const extensionByType = extensions.reduce((acc, m) => { - return m.docTypes.reduce((_acc, type) => { + return m!.docTypes.reduce((_acc, type) => { acc[type] = m; return acc; }, acc); - }, {}); - const searchOption = (field, ...defaults) => + }, {} as { [visType: string]: VisualizationsAppExtension }); + const searchOption = (field: string, ...defaults: string[]) => _(extensions) .pluck(field) .concat(defaults) .compact() .flatten() .uniq() - .value(); + .value() as string[]; const searchOptions = { type: searchOption('docTypes', 'visualization'), searchFields: searchOption('searchFields', 'title^3', 'description'), search: search ? `${search}*` : undefined, perPage: size, page: 1, - defaultSearchOperator: 'AND', + defaultSearchOperator: 'AND' as 'AND', }; - const { total, savedObjects } = await savedObjectsClient.find(searchOptions); + const { total, savedObjects } = await savedObjectsClient.find( + searchOptions + ); return { total, diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/index.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/index.ts diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.test.ts similarity index 96% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.test.ts index 6549b317d1634..98af6d99025c2 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.test.ts @@ -18,7 +18,7 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; -import { VisSavedObject } from '../embeddable/visualize_embeddable'; +import { VisSavedObject, VisState } from '../types'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { @@ -128,7 +128,7 @@ Object { id: '1', title: 'test', savedSearchRefName: 'search_0', - visState: { + visState: ({ params: { controls: [ { @@ -140,7 +140,7 @@ Object { }, ], }, - }, + } as unknown) as VisState, } as VisSavedObject; const references = [ { @@ -192,7 +192,7 @@ Object { const context = { id: '1', title: 'test', - visState: { + visState: ({ params: { controls: [ { @@ -201,7 +201,7 @@ Object { }, ], }, - }, + } as unknown) as VisState, } as VisSavedObject; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find index pattern reference \\"control_0_index_pattern\\""` diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.ts similarity index 95% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.ts index 330f5e2dacd10..b995d340d44d9 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { VisSavedObject } from '../embeddable/visualize_embeddable'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../../../../core/public'; +import { VisSavedObject } from '../types'; export function extractReferences({ attributes, diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualizations.ts similarity index 91% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualizations.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualizations.ts index 7d0d6a10ff66f..fc0f77d54059c 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualizations.ts @@ -19,18 +19,15 @@ import { SavedObjectLoader, SavedObjectKibanaServices, -} from '../../../../../plugins/saved_objects/public'; - -// @ts-ignore +} from '../../../../../../../plugins/saved_objects/public'; import { findListItems } from './find_list_items'; import { createSavedVisClass } from './_saved_vis'; -import { createVisualizeEditUrl } from '../../../kibana/public/visualize'; -import { TypesStart } from '../np_ready/public/types'; +import { TypesStart } from '../vis_types'; export interface SavedObjectKibanaServicesWithVisualizations extends SavedObjectKibanaServices { visualizationTypes: TypesStart; } - +export type SavedVisualizationsLoader = ReturnType; export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisualizations) { const { savedObjectsClient, visualizationTypes } = services; @@ -59,7 +56,7 @@ export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisu source.icon = source.type.icon; source.image = source.type.image; source.typeTitle = source.type.title; - source.editUrl = `#${createVisualizeEditUrl(id)}`; + source.editUrl = `#/visualize/edit/${id}`; return source; }; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts index 433c5c7b6df0d..a977a4b452bf7 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts @@ -23,11 +23,18 @@ import { I18nStart, IUiSettingsClient, SavedObjectsStart, -} from 'src/core/public'; -import { TypesStart } from './types'; +} from '../../../../../../core/public'; +import { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../../../../plugins/kibana_utils/public'; -import { FilterManager, IndexPatternsContract } from '../../../../../../plugins/data/public'; +import { + FilterManager, + IndexPatternsContract, + TimefilterContract, +} from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; +import { ExpressionsStart } from '../../../../../../plugins/expressions/public'; +import { UiActionsStart } from '../../../../../../plugins/ui_actions/public'; +import { SavedVisualizationsLoader } from './saved_visualizations'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -47,6 +54,8 @@ export const [getFilterManager, setFilterManager] = createGetterSetter('TimeFilter'); + export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' ); @@ -54,3 +63,11 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'UsageCollection' ); + +export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); + +export const [getUiActions, setUiActions] = createGetterSetter('UiActions'); + +export const [getSavedVisualizationsLoader, setSavedVisualizationsLoader] = createGetterSetter< + SavedVisualizationsLoader +>('SavedVisualisationsLoader'); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts new file mode 100644 index 0000000000000..d2ca4ffb92eb2 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts @@ -0,0 +1,38 @@ +/* + * 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 { SavedObject } from '../../../../../../plugins/saved_objects/public'; +import { Vis, VisState, VisParams, VisualizationController } from './vis'; +import { ISearchSource } from '../../../../../../plugins/data/public/'; +import { SavedSearch } from '../../../../kibana/public/discover/np_ready/types'; + +export { Vis, VisState, VisParams, VisualizationController }; + +export interface VisSavedObject extends SavedObject { + vis: Vis; + description?: string; + searchSource: ISearchSource; + title: string; + uiStateJSON?: string; + destroy: () => void; + savedSearchRefName?: string; + savedSearchId?: string; + savedSearch?: SavedSearch; + visState: VisState; +} diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts index 19375e25a9fb7..990f27dca7556 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts @@ -17,7 +17,7 @@ * under the License. */ -import { VisType } from './types'; +import { VisType } from './vis_types'; import { IAggConfigs } from '../../legacy_imports'; import { Status } from './legacy/update_status'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts index f9b7db5c02d93..62b68082e21f8 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts @@ -18,7 +18,7 @@ */ import { Vis, VisState, VisParams } from './vis'; -import { VisType } from './types'; +import { VisType } from './vis_types'; import { IIndexPattern } from '../../../../../../plugins/data/common'; type InitVisStateType = diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/base_vis_type.js similarity index 95% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/base_vis_type.js index 351acc48e2676..50ff74cfe9dd3 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/base_vis_type.js @@ -19,8 +19,6 @@ import _ from 'lodash'; -import { DefaultEditorController } from '../../../../../vis_default_editor/public'; - export class BaseVisType { constructor(opts = {}) { if (!opts.name) { @@ -47,7 +45,7 @@ export class BaseVisType { }, requestHandler: 'courier', // select one from registry or pass a function responseHandler: 'none', - editor: DefaultEditorController, + editor: null, // no default is provided editorConfig: { collections: {}, // collections used for configuration (list of positions, ...) }, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/index.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/index.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/react_vis_type.js similarity index 100% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/react_vis_type.js diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/vis_type_alias_registry.ts similarity index 97% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/vis_type_alias_registry.ts index 97f4798c296d4..12b02ee9e6b32 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/vis_type_alias_registry.ts @@ -27,7 +27,7 @@ interface VisualizationListItem { typeTitle: string; } -interface VisualizationsAppExtension { +export interface VisualizationsAppExtension { docTypes: string[]; searchFields?: string[]; toListItem: (savedObject: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx index 58f7bc49d1cdb..2712019e42609 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx @@ -19,9 +19,9 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TypesStart, VisType } from '../types'; +import { TypesStart, VisType } from '../vis_types'; import { NewVisModal } from './new_vis_modal'; -import { SavedObjectsStart } from 'kibana/public'; +import { SavedObjectsStart } from '../../../../../../../core/public'; describe('NewVisModal', () => { const { location } = window; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx index b39e8e8926707..7c10001eddb50 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx @@ -23,10 +23,10 @@ import { EuiModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; -import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; +import { IUiSettingsClient, SavedObjectsStart } from '../../../../../../../core/public'; import { SearchSelection } from './search_selection'; import { TypeSelection } from './type_selection'; -import { TypesStart, VisType, VisTypeAlias } from '../types'; +import { TypesStart, VisType, VisTypeAlias } from '../vis_types'; import { UsageCollectionSetup } from '../../../../../../../plugins/usage_collection/public'; interface TypeSelectionProps { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx index 9b3b8a6425e52..f8eb191dd5f92 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx @@ -21,10 +21,10 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui' import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; +import { IUiSettingsClient, SavedObjectsStart } from '../../../../../../../../core/public'; import { SavedObjectFinderUi } from '../../../../../../../../plugins/saved_objects/public'; -import { VisType } from '../../types'; +import { VisType } from '../../vis_types'; interface SearchSelectionProps { onSearchSelected: (searchId: string, searchType: string) => void; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx index 5068f43952c4e..e84314853ba4c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { EuiText, EuiButton } from '@elastic/eui'; import { VisTypeAliasListEntry } from './type_selection'; -import { VisTypeAlias } from '../../types'; +import { VisTypeAlias } from '../../vis_types'; interface Props { promotedTypes: VisTypeAliasListEntry[]; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx index 574f5b3cccc99..81dcecfee2613 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx @@ -40,7 +40,7 @@ import { VisTypeAlias } from '../../../../../../visualizations/public'; import { NewVisHelp } from './new_vis_help'; import { VisHelpText } from './vis_help_text'; import { VisTypeIcon } from './vis_type_icon'; -import { VisType, TypesStart } from '../../types'; +import { VisType, TypesStart } from '../../vis_types'; export interface VisTypeListEntry extends VisType { highlighted: boolean; diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index da70d9fe89525..275a9da96a2c4 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -39,7 +39,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../legacy/core_plugins/visualizations/public/embeddable/constants'; + +// TODO: can't import from '../../../../legacy/core_plugins/visualizations/public/' directly, +// because yarn build:types fails after trying to emit type declarations for whole visualizations plugin +// Bunch of errors like this: 'Return type of exported function has or is using private name 'SavedVis'' +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants'; export interface OnSaveProps { newTitle: string; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts index cc92d864282e7..d9e841092be56 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts @@ -6,7 +6,7 @@ // @ts-ignore import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/visualizations/public/embeddable/constants'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/visualizations/public'; import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants'; export const EmbeddableTypes: { map: string; search: string; visualization: string } = { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts index 6ac0d84bc9a73..0315a1f480911 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -5,7 +5,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; -import { VisualizeInput } from 'src/legacy/core_plugins/visualizations/public/embeddable'; +import { VisualizeInput } from 'src/legacy/core_plugins/visualizations/public'; import { EmbeddableTypes, EmbeddableExpressionType, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts index 93d747537c34c..8694c0e2c7f9f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('ui/new_platform'); import { embeddableInputToExpression } from './embeddable_input_to_expression'; import { SavedMapInput } from '../../functions/common/saved_map'; import { EmbeddableTypes } from '../../expression_types'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc97a96d4fcba..dc8d6343007b8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2872,7 +2872,6 @@ "visualizations.newVisWizard.title": "新規ビジュアライゼーション", "visualizations.newVisWizard.visTypeAliasDescription": "Visualize外でKibanaアプリケーションを開きます。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibanaアプリケーション", - "visualizations.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした", "visDefaultEditor.aggSelect.aggregationLabel": "集約", "visDefaultEditor.aggSelect.helpLinkLabel": "{aggTitle} のヘルプ", "visDefaultEditor.aggSelect.noCompatibleAggsDescription": "インデックスパターン{indexPatternTitle}には集約可能なフィールドが含まれていません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2532cdb0c4d07..f6ef0140e385e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2873,7 +2873,6 @@ "visualizations.newVisWizard.title": "新建可视化", "visualizations.newVisWizard.visTypeAliasDescription": "打开 Visualize 外部的 Kibana 应用程序。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibana 应用程序", - "visualizations.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界", "visDefaultEditor.aggSelect.aggregationLabel": "聚合", "visDefaultEditor.aggSelect.helpLinkLabel": "{aggTitle} 帮助", "visDefaultEditor.aggSelect.noCompatibleAggsDescription": "索引模式“{indexPatternTitle}”不包含任何聚合。", From d50031e7215ea66eb1b7e2ee20b2897ae3b9a5b7 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 25 Feb 2020 18:02:40 +0000 Subject: [PATCH 101/113] [ML] Global calendars (#57890) * [ML] Global calendars * updating snapshots * changes based on review * larger spacer * updating jest snapshot --- .../plugins/ml/common/constants/calendars.ts | 7 ++ .../calendars/calendars_selection.tsx | 5 +- .../__snapshots__/new_calendar.test.js.snap | 2 + .../__snapshots__/calendar_form.test.js.snap | 56 ++---------- .../edit/calendar_form/calendar_form.js | 85 ++++++++++++------- .../settings/calendars/edit/new_calendar.js | 42 ++++++--- .../settings/calendars/edit/utils.js | 17 ++-- .../table/__snapshots__/table.test.js.snap | 1 + .../settings/calendars/list/table/table.js | 14 +++ .../server/models/calendar/event_manager.ts | 4 +- .../ml/server/models/job_service/groups.js | 18 ++-- 11 files changed, 148 insertions(+), 103 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/common/constants/calendars.ts diff --git a/x-pack/legacy/plugins/ml/common/constants/calendars.ts b/x-pack/legacy/plugins/ml/common/constants/calendars.ts new file mode 100644 index 0000000000000..1a56257ca1304 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/calendars.ts @@ -0,0 +1,7 @@ +/* + * 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 GLOBAL_CALENDAR = '_all'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 919972186761a..1e7327552623e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -23,6 +23,7 @@ import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; import { ml } from '../../../../../../../../../services/ml_api_service'; import { Calendar } from '../../../../../../../../../../../common/types/calendars'; +import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; export const CalendarsSelection: FC = () => { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); @@ -35,7 +36,9 @@ export const CalendarsSelection: FC = () => { async function loadCalendars() { setIsLoading(true); - const calendars = await ml.calendars(); + const calendars = (await ml.calendars()).filter( + c => c.job_ids.includes(GLOBAL_CALENDAR) === false + ); setOptions(calendars.map(c => ({ label: c.calendar_id, value: c }))); setSelectedOptions(selectedCalendars.map(c => ({ label: c.calendar_id, value: c }))); setIsLoading(false); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap index 2f5eb596a157b..21f505cff9aec 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap @@ -22,6 +22,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` eventsList={Array []} groupIds={Array []} isEdit={false} + isGlobalCalendar={false} isNewCalendarIdValid={true} jobIds={Array []} onCalendarIdChange={[Function]} @@ -30,6 +31,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` onDescriptionChange={[Function]} onEdit={[Function]} onEventDelete={[Function]} + onGlobalCalendarChange={[Function]} onGroupSelection={[Function]} onJobSelection={[Function]} saving={false} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index 0e7db62e44b51..acce01f1994db 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -84,59 +84,19 @@ exports[`CalendarForm Renders calendar form 1`] = ` value="" /> - - } - labelType="label" - > - - - + } - labelType="label" - > - - + name="switch" + /> diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js index fffcdf4c516f8..62daced72ceb2 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js @@ -18,6 +18,7 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiSwitch, } from '@elastic/eui'; import { EventsTable } from '../events_table'; @@ -68,6 +69,8 @@ export const CalendarForm = ({ selectedGroupOptions, selectedJobOptions, showNewEventModal, + isGlobalCalendar, + onGlobalCalendarChange, }) => { const msg = i18n.translate('xpack.ml.calendarsEdit.calendarForm.allowedCharactersDescription', { defaultMessage: @@ -81,7 +84,9 @@ export const CalendarForm = ({ return ( - {!isEdit && ( + {isEdit === true ? ( + + ) : (

@@ -128,39 +133,59 @@ export const CalendarForm = ({ )} - {isEdit && } - - } - > - - - + + } - > - - + checked={isGlobalCalendar} + onChange={onGlobalCalendarChange} + /> + + {isGlobalCalendar === false && ( + <> + + + + } + > + + + + + } + > + + + + )} @@ -240,4 +265,6 @@ CalendarForm.propTypes = { selectedGroupOptions: PropTypes.array.isRequired, selectedJobOptions: PropTypes.array.isRequired, showNewEventModal: PropTypes.func.isRequired, + isGlobalCalendar: PropTypes.bool.isRequired, + onGlobalCalendarChange: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 935e67ec05eff..815d1565d5bc4 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -19,6 +19,7 @@ import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars'; class NewCalendarUI extends Component { static propTypes = { @@ -46,6 +47,7 @@ class NewCalendarUI extends Component { events: [], saving: false, selectedCalendar: undefined, + isGlobalCalendar: false, }; } @@ -65,6 +67,7 @@ class NewCalendarUI extends Component { let eventsList = []; let selectedCalendar; let formCalendarId = ''; + let isGlobalCalendar = false; // Editing existing calendar. if (this.props.calendarId !== undefined) { @@ -74,13 +77,17 @@ class NewCalendarUI extends Component { formCalendarId = selectedCalendar.calendar_id; eventsList = selectedCalendar.events; - selectedCalendar.job_ids.forEach(id => { - if (jobIds.find(jobId => jobId === id)) { - selectedJobOptions.push({ label: id }); - } else if (groupIds.find(groupId => groupId === id)) { - selectedGroupOptions.push({ label: id }); - } - }); + if (selectedCalendar.job_ids.includes(GLOBAL_CALENDAR)) { + isGlobalCalendar = true; + } else { + selectedCalendar.job_ids.forEach(id => { + if (jobIds.find(jobId => jobId === id)) { + selectedJobOptions.push({ label: id }); + } else if (groupIds.find(groupId => groupId === id)) { + selectedGroupOptions.push({ label: id }); + } + }); + } } } @@ -96,6 +103,7 @@ class NewCalendarUI extends Component { selectedJobOptions, selectedGroupOptions, selectedCalendar, + isGlobalCalendar, }); } catch (error) { console.log(error); @@ -181,10 +189,15 @@ class NewCalendarUI extends Component { events, selectedGroupOptions, selectedJobOptions, + isGlobalCalendar, } = this.state; - const jobIds = selectedJobOptions.map(option => option.label); - const groupIds = selectedGroupOptions.map(option => option.label); + const allIds = isGlobalCalendar + ? [GLOBAL_CALENDAR] + : [ + ...selectedJobOptions.map(option => option.label), + ...selectedGroupOptions.map(option => option.label), + ]; // Reduce events to fields expected by api const eventsToSave = events.map(event => ({ @@ -198,7 +211,7 @@ class NewCalendarUI extends Component { calendarId: formCalendarId, description, events: eventsToSave, - job_ids: [...jobIds, ...groupIds], + job_ids: allIds, }; return calendar; @@ -214,6 +227,12 @@ class NewCalendarUI extends Component { })); }; + onGlobalCalendarChange = ({ currentTarget }) => { + this.setState({ + isGlobalCalendar: currentTarget.checked, + }); + }; + onJobSelection = selectedJobOptions => { this.setState({ selectedJobOptions, @@ -295,6 +314,7 @@ class NewCalendarUI extends Component { selectedCalendar, selectedJobOptions, selectedGroupOptions, + isGlobalCalendar, } = this.state; let modal = ''; @@ -351,6 +371,8 @@ class NewCalendarUI extends Component { selectedJobOptions={selectedJobOptions} onCreateGroupOption={this.onCreateGroupOption} showNewEventModal={this.showNewEventModal} + isGlobalCalendar={isGlobalCalendar} + onGlobalCalendarChange={this.onGlobalCalendarChange} /> {modal} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js index e4ab6677accf5..efc54c181fdc1 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js @@ -73,14 +73,17 @@ function getCalendars() { export function getCalendarSettingsData() { return new Promise(async (resolve, reject) => { try { - const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]); + const [jobIds, groupIds, calendars] = await Promise.all([ + getJobIds(), + getGroupIds(), + getCalendars(), + ]); - const formattedData = { - jobIds: data[0], - groupIds: data[1], - calendars: data[2], - }; - resolve(formattedData); + resolve({ + jobIds, + groupIds, + calendars, + }); } catch (error) { console.log(error); reject(error); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index ff74c592b2b0f..14b65a04ce599 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -16,6 +16,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` Object { "field": "job_ids_string", "name": "Jobs", + "render": [Function], "sortable": true, "truncateText": true, }, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index bd1dafcd6c0aa..be41eabd5ae2d 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -12,6 +12,8 @@ import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { GLOBAL_CALENDAR } from '../../../../../../common/constants/calendars'; + export const CalendarsListTable = ({ calendarsList, onDeleteClick, @@ -52,6 +54,18 @@ export const CalendarsListTable = ({ }), sortable: true, truncateText: true, + render: jobList => { + return jobList === GLOBAL_CALENDAR ? ( + + + + ) : ( + jobList + ); + }, }, { field: 'events_length', diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts index 19f2eda466179..488839f68b3fe 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts @@ -6,6 +6,8 @@ import Boom from 'boom'; +import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; + export interface CalendarEvent { calendar_id?: string; event_id?: string; @@ -32,7 +34,7 @@ export class EventManager { // jobId is optional async getAllEvents(jobId?: string) { - const calendarId = '_all'; + const calendarId = GLOBAL_CALENDAR; try { const resp = await this._client('ml.events', { calendarId, diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js index 91f82f04a9a0c..6fbc071ef9854 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js @@ -5,6 +5,7 @@ */ import { CalendarManager } from '../calendar'; +import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export function groupsProvider(callWithRequest) { const calMngr = new CalendarManager(callWithRequest); @@ -12,11 +13,13 @@ export function groupsProvider(callWithRequest) { async function getAllGroups() { const groups = {}; const jobIds = {}; - const [JOBS, CALENDARS] = [0, 1]; - const results = await Promise.all([callWithRequest('ml.jobs'), calMngr.getAllCalendars()]); + const [{ jobs }, calendars] = await Promise.all([ + callWithRequest('ml.jobs'), + calMngr.getAllCalendars(), + ]); - if (results[JOBS] && results[JOBS].jobs) { - results[JOBS].jobs.forEach(job => { + if (jobs) { + jobs.forEach(job => { jobIds[job.job_id] = null; if (job.groups !== undefined) { job.groups.forEach(g => { @@ -33,10 +36,11 @@ export function groupsProvider(callWithRequest) { } }); } - if (results[CALENDARS]) { - results[CALENDARS].forEach(cal => { + if (calendars) { + calendars.forEach(cal => { cal.job_ids.forEach(jId => { - if (jobIds[jId] === undefined) { + // don't include _all in the calendar groups list + if (jId !== GLOBAL_CALENDAR && jobIds[jId] === undefined) { if (groups[jId] === undefined) { groups[jId] = { id: jId, From 30fb4eb6945a463aeec1211d04854735c3c75281 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 25 Feb 2020 11:50:54 -0700 Subject: [PATCH 102/113] [Maps] remove duplicated pagintation text in locked tooltips (#58383) --- .../__snapshots__/tooltip_header.test.js.snap | 38 ------------------- .../map/features_tooltip/tooltip_header.js | 2 +- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap index 486a830d21b65..b5fe334f8415e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap @@ -17,25 +17,6 @@ exports[`TooltipHeader multiple features, multiple layers: locked should show pa pageCount={3} /> - - - - - @@ -139,25 +120,6 @@ exports[`TooltipHeader multiple features, single layer: locked should show pagin pageCount={2} /> - - - - - 1) { + if (!isLocked && filteredFeatures.length > 1) { headerItems.push( From d9e3d744ba83554139c0c4383b2167a577577b29 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 25 Feb 2020 14:21:25 -0500 Subject: [PATCH 103/113] [Maps] Add EPSG-code to docs (#58378) --- docs/setup/settings.asciidoc | 2 +- .../file_upload/public/components/json_index_file_picker.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c1f06aff722b5..2d4d00b730109 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -231,7 +231,7 @@ This setting does not impact <> and <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the -https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system] +https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] and only include polygons. If the file is hosted on a separate domain from Kibana, the server needs to be CORS-enabled so Kibana can download the file. The following example shows a valid regionmap configuration. diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js index 0ee4f76ebf9d0..67086883a9a32 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js @@ -268,6 +268,10 @@ export class JsonIndexFilePicker extends Component { maxFileSize: bytesToSize(MAX_FILE_SIZE), }} /> +
+ {i18n.translate('xpack.fileUpload.jsonIndexFilePicker.coordinateSystemAccepted', { + defaultMessage: 'Coordinates must be in EPSG:4326 coordinate reference system.', + })}{' '} ) } From 7b4c809fc7b29ffead03daac0363d3701d2f016a Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 25 Feb 2020 13:51:02 -0700 Subject: [PATCH 104/113] Abort cancelled search requests to Elasticsearch (#56788) * Update abort controller library * Bootstrap * Abort when the request is aborted * Add utility and update value suggestions route * Remove bad merge * Revert switching abort controller libraries * Revert package.json in lib * Move to previous abort controller * Fix test to use fake timers to run debounced handlers * Fix loading bar not going away when cancelling * Add test for loading count * Fix test * Fix failing test Co-authored-by: Elastic Machine --- .../search/sync_search_strategy.test.ts | 52 ++++++++++++++++--- .../public/search/sync_search_strategy.ts | 21 ++++---- .../autocomplete/value_suggestions_route.ts | 4 +- .../lib/get_request_aborted_signal.test.ts | 45 ++++++++++++++++ .../server/lib/get_request_aborted_signal.ts | 33 ++++++++++++ src/plugins/data/server/lib/index.ts | 20 +++++++ src/plugins/data/server/search/routes.ts | 5 +- 7 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 src/plugins/data/server/lib/get_request_aborted_signal.test.ts create mode 100644 src/plugins/data/server/lib/get_request_aborted_signal.ts create mode 100644 src/plugins/data/server/lib/index.ts diff --git a/src/plugins/data/public/search/sync_search_strategy.test.ts b/src/plugins/data/public/search/sync_search_strategy.test.ts index 9378e5833f8d7..31a1adfa01c75 100644 --- a/src/plugins/data/public/search/sync_search_strategy.test.ts +++ b/src/plugins/data/public/search/sync_search_strategy.test.ts @@ -35,12 +35,9 @@ describe('Sync search strategy', () => { core: mockCoreStart, getSearchStrategy: jest.fn(), }); - syncSearch.search( - { - serverStrategy: SYNC_SEARCH_STRATEGY, - }, - {} - ); + const request = { serverStrategy: SYNC_SEARCH_STRATEGY }; + syncSearch.search(request, {}); + expect(mockCoreStart.http.fetch.mock.calls[0][0]).toEqual({ path: `/internal/search/${SYNC_SEARCH_STRATEGY}`, body: JSON.stringify({ @@ -50,4 +47,47 @@ describe('Sync search strategy', () => { signal: undefined, }); }); + + it('increments and decrements loading count on success', async () => { + const expectedLoadingCountValues = [0, 1, 0]; + const receivedLoadingCountValues: number[] = []; + + mockCoreStart.http.fetch.mockResolvedValueOnce('response'); + + const syncSearch = syncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn(), + }); + const request = { serverStrategy: SYNC_SEARCH_STRATEGY }; + + const loadingCount$ = mockCoreStart.http.addLoadingCountSource.mock.calls[0][0]; + loadingCount$.subscribe(value => receivedLoadingCountValues.push(value)); + + await syncSearch.search(request, {}).toPromise(); + + expect(receivedLoadingCountValues).toEqual(expectedLoadingCountValues); + }); + + it('increments and decrements loading count on failure', async () => { + expect.assertions(1); + const expectedLoadingCountValues = [0, 1, 0]; + const receivedLoadingCountValues: number[] = []; + + mockCoreStart.http.fetch.mockRejectedValueOnce('error'); + + const syncSearch = syncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn(), + }); + const request = { serverStrategy: SYNC_SEARCH_STRATEGY }; + + const loadingCount$ = mockCoreStart.http.addLoadingCountSource.mock.calls[0][0]; + loadingCount$.subscribe(value => receivedLoadingCountValues.push(value)); + + try { + await syncSearch.search(request, {}).toPromise(); + } catch (e) { + expect(receivedLoadingCountValues).toEqual(expectedLoadingCountValues); + } + }); }); diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index c2cc180af546e..860ce593ae217 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -18,7 +18,8 @@ */ import { BehaviorSubject, from } from 'rxjs'; -import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../common/search'; +import { finalize } from 'rxjs/operators'; +import { IKibanaSearchRequest } from '../../common/search'; import { ISearch, ISearchOptions } from './i_search'; import { TSearchStrategyProvider, ISearchStrategy, ISearchContext } from './types'; @@ -40,16 +41,14 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { loadingCount$.next(loadingCount$.getValue() + 1); - const response: Promise = context.core.http.fetch({ - path: `/internal/search/${request.serverStrategy}`, - method: 'POST', - body: JSON.stringify(request), - signal: options.signal, - }); - - response.then(() => loadingCount$.next(loadingCount$.getValue() - 1)); - - return from(response); + return from( + context.core.http.fetch({ + path: `/internal/search/${request.serverStrategy}`, + method: 'POST', + body: JSON.stringify(request), + signal: options.signal, + }) + ).pipe(finalize(() => loadingCount$.next(loadingCount$.getValue() - 1))); }; return { search }; diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index f032890e98901..02a5e0921fe4f 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -23,6 +23,7 @@ import { IRouter } from 'kibana/server'; import { IFieldType, Filter } from '../index'; import { findIndexPatternById, getFieldByName } from '../index_patterns'; +import { getRequestAbortedSignal } from '../lib'; export function registerValueSuggestionsRoute(router: IRouter) { router.post( @@ -50,6 +51,7 @@ export function registerValueSuggestionsRoute(router: IRouter) { const { field: fieldName, query, boolFilter } = request.body; const { index } = request.params; const { dataClient } = context.core.elasticsearch; + const signal = getRequestAbortedSignal(request.events.aborted$); const autocompleteSearchOptions = { timeout: await uiSettings.get('kibana.autocompleteTimeout'), @@ -62,7 +64,7 @@ export function registerValueSuggestionsRoute(router: IRouter) { const body = await getBody(autocompleteSearchOptions, field || fieldName, query, boolFilter); try { - const result = await dataClient.callAsCurrentUser('search', { index, body }); + const result = await dataClient.callAsCurrentUser('search', { index, body }, { signal }); const buckets: any[] = get(result, 'aggregations.suggestions.buckets') || diff --git a/src/plugins/data/server/lib/get_request_aborted_signal.test.ts b/src/plugins/data/server/lib/get_request_aborted_signal.test.ts new file mode 100644 index 0000000000000..3c1e20dbcb158 --- /dev/null +++ b/src/plugins/data/server/lib/get_request_aborted_signal.test.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 { Subject } from 'rxjs'; +import { getRequestAbortedSignal } from './get_request_aborted_signal'; + +describe('abortableRequestHandler', () => { + jest.useFakeTimers(); + + it('should call abort if disconnected', () => { + const abortedSubject = new Subject(); + const aborted$ = abortedSubject.asObservable(); + const onAborted = jest.fn(); + + const signal = getRequestAbortedSignal(aborted$); + signal.addEventListener('abort', onAborted); + + // Shouldn't be aborted or call onAborted prior to disconnecting + expect(signal.aborted).toBe(false); + expect(onAborted).not.toBeCalled(); + + abortedSubject.next(); + jest.runAllTimers(); + + // Should be aborted and call onAborted after disconnecting + expect(signal.aborted).toBe(true); + expect(onAborted).toBeCalled(); + }); +}); diff --git a/src/plugins/data/server/lib/get_request_aborted_signal.ts b/src/plugins/data/server/lib/get_request_aborted_signal.ts new file mode 100644 index 0000000000000..d1541f1df9384 --- /dev/null +++ b/src/plugins/data/server/lib/get_request_aborted_signal.ts @@ -0,0 +1,33 @@ +/* + * 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'; +// @ts-ignore not typed +import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; + +/** + * A simple utility function that returns an `AbortSignal` corresponding to an `AbortController` + * which aborts when the given request is aborted. + * @param aborted$ The observable of abort events (usually `request.events.aborted$`) + */ +export function getRequestAbortedSignal(aborted$: Observable): AbortSignal { + const controller = new AbortController(); + aborted$.subscribe(() => controller.abort()); + return controller.signal; +} diff --git a/src/plugins/data/server/lib/index.ts b/src/plugins/data/server/lib/index.ts new file mode 100644 index 0000000000000..a2af456846e14 --- /dev/null +++ b/src/plugins/data/server/lib/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 { getRequestAbortedSignal } from './get_request_aborted_signal'; diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 6f726771c41b2..11879d14931ae 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -19,6 +19,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../../../core/server'; +import { getRequestAbortedSignal } from '../lib'; export function registerSearchRoute(router: IRouter): void { router.post( @@ -35,8 +36,10 @@ export function registerSearchRoute(router: IRouter): void { async (context, request, res) => { const searchRequest = request.body; const strategy = request.params.strategy; + const signal = getRequestAbortedSignal(request.events.aborted$); + try { - const response = await context.search!.search(searchRequest, {}, strategy); + const response = await context.search!.search(searchRequest, { signal }, strategy); return res.ok({ body: response }); } catch (err) { return res.customError({ From ca5fb7fb01256861cca9d897e628f11a17bc68fb Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 25 Feb 2020 15:56:28 -0500 Subject: [PATCH 105/113] Use links instead of click handlers when switching spaces (#57730) * use links instead of click handlers when switching spaces * remove unused imports * remove use of deprecated injectedMetadata service * simplify SpacesManager constructor Co-authored-by: Elastic Machine --- x-pack/plugins/spaces/common/index.ts | 2 +- .../nav_control/components/spaces_menu.tsx | 6 ++--- .../spaces/public/nav_control/nav_control.tsx | 1 + .../nav_control/nav_control_popover.test.tsx | 2 ++ .../nav_control/nav_control_popover.tsx | 7 ++--- x-pack/plugins/spaces/public/plugin.tsx | 3 +-- .../components/space_card.test.tsx | 27 ++++++++++++++----- .../space_selector/components/space_card.tsx | 9 ++++--- .../components/space_cards.test.tsx | 2 +- .../space_selector/components/space_cards.tsx | 12 +++------ .../space_selector/space_selector.test.tsx | 8 ++++-- .../public/space_selector/space_selector.tsx | 7 ++--- .../space_selector/space_selector_app.tsx | 5 +++- .../spaces_manager/spaces_manager.mock.ts | 1 - .../spaces_manager/spaces_manager.test.ts | 12 ++++----- .../public/spaces_manager/spaces_manager.ts | 12 ++++----- 16 files changed, 62 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index c1f0f8bd3ece4..703722fcf8f52 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -5,5 +5,5 @@ */ export { isReservedSpace } from './is_reserved_space'; -export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD } from './constants'; +export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 59656333f865e..cdfbc9bbe2683 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -14,7 +14,7 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; import { Capabilities, ApplicationStart } from 'src/core/public'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common/constants'; +import { addSpaceIdToPath, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from '../../../common'; import { Space } from '../../../common/model/space'; import { ManageSpacesButton } from './manage_spaces_button'; import { SpaceAvatar } from '../../space_avatar'; @@ -23,7 +23,7 @@ interface Props { id: string; spaces: Space[]; isLoading: boolean; - onSelectSpace: (space: Space) => void; + serverBasePath: string; onManageSpacesClick: () => void; intl: InjectedIntl; capabilities: Capabilities; @@ -184,7 +184,7 @@ class SpacesMenuUI extends Component { diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control.tsx index 53d7038cec4d5..4d7af256822c8 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control.tsx @@ -23,6 +23,7 @@ export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreSta { const wrapper = shallow( { const wrapper = mountWithIntl( { id={popoutContentId} spaces={this.state.spaces} isLoading={this.state.loading} - onSelectSpace={this.onSelectSpace} + serverBasePath={this.props.serverBasePath} onManageSpacesClick={this.toggleSpaceSelector} capabilities={this.props.capabilities} navigateToApp={this.props.navigateToApp} @@ -175,8 +176,4 @@ export class NavControlPopover extends Component { showSpaceSelector: false, }); }; - - private onSelectSpace = (space: Space) => { - this.props.spacesManager.changeSelectedSpace(space); - }; } diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 73dae84c1873b..44215ec538002 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -43,8 +43,7 @@ export class SpacesPlugin implements Plugin { const space = { @@ -16,21 +17,33 @@ test('it renders without crashing', () => { disabledFeatures: [], }; - shallow(); + shallow(); }); -test('it is clickable', () => { +test('links to the indicated space', () => { const space = { - id: '', + id: 'some-space', name: 'space name', description: 'space description', disabledFeatures: [], }; - const clickHandler = jest.fn(); + const wrapper = mount(); + expect(wrapper.find(EuiCard).props()).toMatchObject({ + href: '/server-base-path/s/some-space/spaces/enter', + }); +}); - const wrapper = mount(); - wrapper.find('button').simulate('click'); +test('links to the default space too', () => { + const space = { + id: 'default', + name: 'default space', + description: 'space description', + disabledFeatures: [], + }; - expect(clickHandler).toHaveBeenCalledTimes(1); + const wrapper = mount(); + expect(wrapper.find(EuiCard).props()).toMatchObject({ + href: '/server-base-path/spaces/enter', + }); }); diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx index f898ba87c60bd..7a056427fb166 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx @@ -6,15 +6,16 @@ import { EuiCard } from '@elastic/eui'; import React from 'react'; -import { Space } from '../../../common/model/space'; +import { addSpaceIdToPath, ENTER_SPACE_PATH } from '../../../common'; import { SpaceAvatar } from '../../space_avatar'; +import { Space } from '../..'; interface Props { space: Space; - onClick: () => void; + serverBasePath: string; } export const SpaceCard = (props: Props) => { - const { space, onClick } = props; + const { serverBasePath, space } = props; return ( { icon={renderSpaceAvatar(space)} title={space.name} description={renderSpaceDescription(space)} - onClick={onClick} + href={addSpaceIdToPath(serverBasePath, space.id, ENTER_SPACE_PATH)} /> ); }; diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx index 07b85114e3c8f..8de22bcbe235f 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx @@ -16,5 +16,5 @@ test('it renders without crashing', () => { disabledFeatures: [], }; - shallow(); + shallow(); }); diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx index 7c4084d36b21d..b480cf524304f 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx @@ -11,7 +11,7 @@ import { SpaceCard } from './space_card'; interface Props { spaces: Space[]; - onSpaceSelect: (space: Space) => void; + serverBasePath: string; } export class SpaceCards extends Component { @@ -25,15 +25,9 @@ export class SpaceCards extends Component { ); } - public renderSpace = (space: Space) => ( + private renderSpace = (space: Space) => ( - + ); - - public createSpaceClickHandler = (space: Space) => { - return () => { - this.props.onSpaceSelect(space); - }; - }; } diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx index c8173de1661be..112a8a9a0a685 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx @@ -18,7 +18,9 @@ function getSpacesManager(spaces: Space[] = []) { test('it renders without crashing', () => { const spacesManager = getSpacesManager(); - const component = shallowWithIntl(); + const component = shallowWithIntl( + + ); expect(component).toMatchSnapshot(); }); @@ -34,7 +36,9 @@ test('it queries for spaces when loaded', () => { const spacesManager = getSpacesManager(spaces); - shallowWithIntl(); + shallowWithIntl( + + ); return Promise.resolve().then(() => { expect(spacesManager.getSpaces).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx index b63de399b95c9..9289784b9f95f 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx @@ -30,6 +30,7 @@ import { SpacesManager } from '../spaces_manager'; interface Props { spacesManager: SpacesManager; + serverBasePath: string; } interface State { @@ -129,7 +130,7 @@ export class SpaceSelector extends Component { {this.state.loading && } {!this.state.loading && ( - + )} {!this.state.loading && filteredSpaces.length === 0 && ( @@ -179,10 +180,6 @@ export class SpaceSelector extends Component { searchTerm: searchTerm.trim().toLowerCase(), }); }; - - public onSelectSpace = (space: Space) => { - this.props.spacesManager.changeSelectedSpace(space); - }; } export const renderSpaceSelectorApp = (i18nStart: CoreStart['i18n'], el: Element, props: Props) => { diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx index 29ead8d34994d..6fab1767e4b6d 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx @@ -29,7 +29,10 @@ export const spaceSelectorApp = Object.freeze({ getStartServices(), import('./space_selector'), ]); - return renderSpaceSelectorApp(coreStart.i18n, params.element, { spacesManager }); + return renderSpaceSelectorApp(coreStart.i18n, params.element, { + spacesManager, + serverBasePath: coreStart.http.basePath.serverBasePath, + }); }, }); }, diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 56879af33916f..6186ac7fd93be 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -20,7 +20,6 @@ function createSpacesManagerMock() { copySavedObjects: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), - changeSelectedSpace: jest.fn(), } as unknown) as jest.Mocked; } diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts index f89bfc32a69e6..508669361c23f 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts @@ -12,14 +12,14 @@ describe('SpacesManager', () => { describe('#constructor', () => { it('attempts to retrieve the active space', () => { const coreStart = coreMock.createStart(); - new SpacesManager('/server-base-path', coreStart.http); + new SpacesManager(coreStart.http); expect(coreStart.http.get).toHaveBeenCalledWith('/internal/spaces/_active_space'); }); it('does not retrieve the active space if on an anonymous path', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); - new SpacesManager('/server-base-path', coreStart.http); + new SpacesManager(coreStart.http); expect(coreStart.http.get).not.toHaveBeenCalled(); }); }); @@ -31,7 +31,7 @@ describe('SpacesManager', () => { id: 'my-space', name: 'my space', }); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(coreStart.http.get).toHaveBeenCalledWith('/internal/spaces/_active_space'); await nextTick(); @@ -47,7 +47,7 @@ describe('SpacesManager', () => { it('throws if on an anonymous path', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(coreStart.http.get).not.toHaveBeenCalled(); expect(() => spacesManager.getActiveSpace()).toThrowErrorMatchingInlineSnapshot( @@ -67,7 +67,7 @@ describe('SpacesManager', () => { name: 'my other space', }); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(coreStart.http.get).toHaveBeenCalledWith('/internal/spaces/_active_space'); await nextTick(); @@ -95,7 +95,7 @@ describe('SpacesManager', () => { name: 'my space', }); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(() => spacesManager.getActiveSpace({ forceRefresh: true }) diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index e151dcd4f9368..cc3f51b1850de 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -9,16 +9,18 @@ import { HttpSetup } from 'src/core/public'; import { SavedObjectsManagementRecord } from 'src/legacy/core_plugins/management/public'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; -import { ENTER_SPACE_PATH } from '../../common/constants'; import { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; -import { addSpaceIdToPath } from '../../common'; export class SpacesManager { private activeSpace$: BehaviorSubject = new BehaviorSubject(null); + private readonly serverBasePath: string; + public readonly onActiveSpaceChange$: Observable; - constructor(private readonly serverBasePath: string, private readonly http: HttpSetup) { + constructor(private readonly http: HttpSetup) { + this.serverBasePath = http.basePath.serverBasePath; + this.onActiveSpaceChange$ = this.activeSpace$ .asObservable() .pipe(skipWhile((v: Space | null) => v == null)) as Observable; @@ -99,10 +101,6 @@ export class SpacesManager { }); } - public async changeSelectedSpace(space: Space) { - window.location.href = addSpaceIdToPath(this.serverBasePath, space.id, ENTER_SPACE_PATH); - } - public redirectToSpaceSelector() { window.location.href = `${this.serverBasePath}/spaces/space_selector`; } From 0e2363615d62e0e27cf8bcce053a5ea80b6acf2e Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 Feb 2020 13:59:04 -0700 Subject: [PATCH 106/113] Fix flaky jest test (#58402) * Revert "Temporarily removes kbn-optimizer cache key tests (#58318)" This reverts commit e64eff0a3d5fde205256ab74b731a63766822f9a. * [kbn-optmizer] avoid mocking fs exports * overwrite ciGroup script to support jest in flaky testing job * limit jest workers to 3 so that concurrent runners have space to operate * Revert "limit jest workers to 3 so that concurrent runners have space to operate" This reverts commit 1a2f882f6d0b37366b0accc6dbb8bc2b42a5fc3f. * Revert "overwrite ciGroup script to support jest in flaky testing job" This reverts commit 548db6172213858691c3b93dc77cb8fd48d4fc3c. --- .../src/optimizer/cache_keys.test.ts | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 packages/kbn-optimizer/src/optimizer/cache_keys.test.ts diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts new file mode 100644 index 0000000000000..7750f9145667e --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -0,0 +1,208 @@ +/* + * 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 Path from 'path'; + +import jestDiff from 'jest-diff'; +import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; +import { OptimizerConfig } from './optimizer_config'; + +jest.mock('./get_changes.ts', () => ({ + getChanges: async () => + new Map([ + ['/foo/bar/a', 'modified'], + ['/foo/bar/b', 'modified'], + ['/foo/bar/c', 'deleted'], + ]), +})); + +jest.mock('./get_mtimes.ts', () => ({ + getMtimes: async (paths: string[]) => new Map(paths.map(path => [path, 12345])), +})); + +jest.mock('execa'); + +jest.mock('fs', () => { + const realFs = jest.requireActual('fs'); + return { + ...realFs, + readFile: jest.fn(realFs.readFile), + }; +}); + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { + expect(cmd).toBe('git'); + expect(args).toEqual([ + 'log', + '-n', + '1', + '--pretty=format:%H', + '--', + expect.stringContaining('kbn-optimizer'), + ]); + expect(opts).toEqual({ + cwd: REPO_ROOT, + }); + + return { + stdout: '', + }; +}); + +describe('getOptimizerCacheKey()', () => { + it('uses latest commit, bootstrap cache, and changed files to create unique value', async () => { + jest + .requireMock('fs') + .readFile.mockImplementation( + (path: string, enc: string, cb: (err: null, file: string) => void) => { + expect(path).toBe( + Path.resolve(REPO_ROOT, 'packages/kbn-optimizer/target/.bootstrap-cache') + ); + expect(enc).toBe('utf8'); + cb(null, ''); + } + ); + + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + }); + + await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` + Object { + "bootstrap": "", + "deletedPaths": Array [ + "/foo/bar/c", + ], + "lastCommit": "", + "modifiedTimes": Object { + "/foo/bar/a": 12345, + "/foo/bar/b": 12345, + }, + "workerConfig": Object { + "browserslistEnv": "dev", + "cache": true, + "dist": false, + "optimizerCacheKey": "♻", + "profileWebpack": false, + "repoRoot": , + "watch": false, + }, + } + `); + }); +}); + +describe('diffCacheKey()', () => { + it('returns undefined if values are equal', () => { + expect(diffCacheKey('1', '1')).toBe(undefined); + expect(diffCacheKey(1, 1)).toBe(undefined); + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { a: 'b' }])).toBe(undefined); + expect( + diffCacheKey( + { + a: '1', + b: '2', + }, + { + b: '2', + a: '1', + } + ) + ).toBe(undefined); + }); + + it('returns a diff if the values are different', () => { + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { b: 'a' }])).toMatchInlineSnapshot(` + "- Expected + + Received + +  Array [ +  \\"1\\", +  \\"2\\", +  Object { + - \\"a\\": \\"b\\", + + \\"b\\": \\"a\\", +  }, +  ]" + `); + expect( + diffCacheKey( + { + a: '1', + b: '1', + }, + { + b: '2', + a: '2', + } + ) + ).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": \\"1\\", + - \\"b\\": \\"1\\", + + \\"a\\": \\"2\\", + + \\"b\\": \\"2\\", +  }" + `); + }); +}); + +describe('reformatJestDiff()', () => { + it('reformats large jestDiff output to focus on the changed lines', () => { + const diff = jestDiff( + { + a: ['1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1', '1', '1', '1', '1', '1'], + }, + { + b: ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1'], + } + ); + + expect(reformatJestDiff(diff)).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": Array [ + + \\"b\\": Array [ +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + - \\"2\\", +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + + \\"2\\", +  \\"1\\", +  \\"1\\", +  ..." + `); + }); +}); From 2501919a8a191cc127973b3789bf588229e2071c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 25 Feb 2020 15:33:49 -0600 Subject: [PATCH 107/113] Remove support of HEAD request in signals index route (#58489) Until these are officially supported in new platform, it's best to not rely on this behavior. We can achieve roughly the same functionality with a GET request. Modifies the existing script accordingly, in case anyone's still using it. Co-authored-by: Elastic Machine --- .../routes/index/read_index_route.ts | 25 ++++++------------- .../scripts/signal_index_exists.sh | 4 +-- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 41be42f7c0fe1..26a6c790ceef9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -36,25 +36,14 @@ export const createReadIndexRoute = ( const indexExists = await getIndexExists(callCluster, index); if (indexExists) { - // head request is used for if you want to get if the index exists - // or not and it will return a content-length: 0 along with either a 200 or 404 - // depending on if the index exists or not. - if (request.method.toLowerCase() === 'head') { - return headers.response().code(200); - } else { - return headers.response({ name: index }).code(200); - } + return headers.response({ name: index }).code(200); } else { - if (request.method.toLowerCase() === 'head') { - return headers.response().code(404); - } else { - return headers - .response({ - message: 'index for this space does not exist', - status_code: 404, - }) - .code(404); - } + return headers + .response({ + message: 'index for this space does not exist', + status_code: 404, + }) + .code(404); } } catch (err) { const error = transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh index b4a494a102b54..ec3e0595c7b08 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./signal_index_exists.sh -curl -s -k --head \ +curl -s -k -f \ -H 'Content-Type: application/json' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index + ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index > /dev/null From 87f6cd115252e4fa6914c1fc767ab58650c3e253 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Tue, 25 Feb 2020 16:40:06 -0600 Subject: [PATCH 108/113] [Uptime] Show only total in snapshot heading (#58376) This fixes a number of design issues. Fixes https://github.com/elastic/kibana/issues/58372 --- .../__snapshots__/snapshot.test.tsx.snap | 1 - .../snapshot_heading.test.tsx.snap | 12 ++++-- .../__tests__/snapshot_heading.test.tsx | 6 +-- .../__snapshots__/chart_wrapper.test.tsx.snap | 2 - .../charts/__tests__/chart_wrapper.test.tsx | 8 ++-- .../public/components/functional/snapshot.tsx | 7 ++-- .../functional/snapshot_heading.tsx | 39 ++++++------------- .../translations/translations/ja-JP.json | 3 -- .../translations/translations/zh-CN.json | 3 -- 9 files changed, 31 insertions(+), 50 deletions(-) diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap index ebbd8a4ac56a8..db41dfb0b04c4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Snapshot component renders without errors 1`] = ` loading={false} >

- All monitors are up + 23 + + Monitors

`; @@ -15,7 +17,9 @@ exports[`SnapshotHeading renders custom heading for no monitors 1`] = ` size="s" >

- No monitors found + 0 + + Monitors

`; @@ -25,7 +29,9 @@ exports[`SnapshotHeading renders standard heading for valid counts 1`] = ` size="s" >

- 3/17 monitors are down + 17 + + Monitors

`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx index 5ddef3d0aabd1..70d082b26d653 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx @@ -10,17 +10,17 @@ import { SnapshotHeading } from '../snapshot_heading'; describe('SnapshotHeading', () => { it('renders custom heading for no down monitors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); it('renders standard heading for valid counts', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); it('renders custom heading for no monitors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap index c1b5970f6456c..71690432fd01b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap @@ -114,7 +114,6 @@ exports[`ChartWrapper component renders the component with loading false 1`] = ` } > { it('renders the component with loading false', () => { const component = shallowWithIntl( - + @@ -29,7 +29,7 @@ describe('ChartWrapper component', () => { it('renders the component with loading true', () => { const component = shallowWithIntl( - + @@ -40,7 +40,7 @@ describe('ChartWrapper component', () => { it('mounts the component with loading true or false', async () => { const component = mount( - + @@ -62,7 +62,7 @@ describe('ChartWrapper component', () => { it('mounts the component with chart when loading true or false', async () => { const component = mount( - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 8531cd1a3cc83..999ade9dccdd9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -6,7 +6,6 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; -import { get } from 'lodash'; import { DonutChart } from './charts'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; @@ -28,11 +27,11 @@ interface SnapshotComponentProps { */ export const SnapshotComponent: React.FC = ({ count, height, loading }) => ( - (count, 'down', 0)} total={get(count, 'total', 0)} /> + (count, 'up', 0)} - down={get(count, 'down', 0)} + up={count.up} + down={count.down} height={SNAPSHOT_CHART_HEIGHT} width={SNAPSHOT_CHART_WIDTH} /> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx index 85d1294d4b064..308d6e19241c2 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx @@ -8,32 +8,17 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -interface Props { - down: number; - total: number; -} +export const SnapshotHeading = ({ total }: { total: number }) => { + const monitorsText = + total === 1 + ? i18n.translate('xpack.uptime.snapshot.monitor', { defaultMessage: 'Monitor' }) + : i18n.translate('xpack.uptime.snapshot.monitors', { defaultMessage: 'Monitors' }); -const getMessage = (down: number, total: number): string => { - if (down === 0 && total > 0) { - return i18n.translate('xpack.uptime.snapshot.zeroDownMessage', { - defaultMessage: 'All monitors are up', - }); - } else if (down === 0 && total === 0) { - return i18n.translate('xpack.uptime.snapshot.noMonitorMessage', { - defaultMessage: 'No monitors found', - }); - } - return i18n.translate('xpack.uptime.snapshot.downCountsMessage', { - defaultMessage: '{down}/{total} monitors are down', - values: { - down, - total, - }, - }); + return ( + +

+ {total} {monitorsText} +

+
+ ); }; - -export const SnapshotHeading = ({ down, total }: Props) => ( - -

{getMessage(down, total)}

-
-); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc8d6343007b8..4a627d48c3cf0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12781,12 +12781,9 @@ "xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "ダウン", "xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "アップ", "xpack.uptime.pluginDescription": "アップタイム監視", - "xpack.uptime.snapshot.downCountsMessage": "{down}/{total} 個のモニターがダウンしています", "xpack.uptime.snapshot.noDataDescription": "申し訳ございませんが、ヒストグラムに利用可能なデータがありません", "xpack.uptime.snapshot.noDataTitle": "ヒストグラムデータがありません", - "xpack.uptime.snapshot.noMonitorMessage": "モニターが見つかりません", "xpack.uptime.snapshot.pingsOverTimeTitle": "一定時間のピング", - "xpack.uptime.snapshot.zeroDownMessage": "すべてのモニターが起動しています", "xpack.uptime.snapshotHistogram.description": "{startTime} から {endTime} までの期間のアップタイムステータスを表示する棒グラフです。", "xpack.uptime.snapshotHistogram.downMonitorsId": "ダウンモニター", "xpack.uptime.snapshotHistogram.series.downLabel": "ダウン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f6ef0140e385e..e0ef4a7a1ebdb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12781,12 +12781,9 @@ "xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "关闭", "xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "运行", "xpack.uptime.pluginDescription": "运行时间监测", - "xpack.uptime.snapshot.downCountsMessage": "{down}/{total} 个监测已关闭", "xpack.uptime.snapshot.noDataDescription": "抱歉,没有可用于该直方图的数据", "xpack.uptime.snapshot.noDataTitle": "没有可用的直方图数据", - "xpack.uptime.snapshot.noMonitorMessage": "未找到任何监测", "xpack.uptime.snapshot.pingsOverTimeTitle": "时移 Ping 数", - "xpack.uptime.snapshot.zeroDownMessage": "所有监测已启动", "xpack.uptime.snapshotHistogram.description": "显示从 {startTime} 到 {endTime} 的运行时间时移状态的条形图。", "xpack.uptime.snapshotHistogram.downMonitorsId": "已关闭监测", "xpack.uptime.snapshotHistogram.series.downLabel": "关闭", From ef80ebf1bb4b9b399b0fcb852e305c2f0db3c853 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 25 Feb 2020 15:57:40 -0700 Subject: [PATCH 109/113] [Search service] Add async search strategy (#53057) * Add async search strategy * Add async search * Fix async strategy and add tests * Move types to separate file * Revert changes to demo search * Update demo search strategy to use async * Add cancellation to search strategies * Add tests * Simplify async search strategy * Move loadingCount to search strategy * Update abort controller library * Bootstrap * Abort when the request is aborted * Add utility and update value suggestions route * Fix bad merge conflict * Update tests * Move to data_enhanced plugin * Remove bad merge * Revert switching abort controller libraries * Revert package.json in lib * Move to previous abort controller * Fix test to use fake timers to run debounced handlers * Revert changes to example plugin * Fix loading bar not going away when cancelling * Call getSearchStrategy instead of passing directly * Add async demo search strategy * Fix error with setting state * Update how aborting works * Fix type checks * Add test for loading count * Attempt to fix broken example test * Revert changes to test * Fix test * Update name to camelCase * Fix failing test * Don't require data_enhanced in example plugin Co-authored-by: Elastic Machine --- examples/demo_search/common/index.ts | 9 ++ examples/demo_search/kibana.json | 2 +- .../public/async_demo_search_strategy.ts | 69 ++++++++++ .../public/demo_search_strategy.ts | 2 +- examples/demo_search/public/plugin.ts | 17 ++- .../server/async_demo_search_strategy.ts | 60 +++++++++ examples/demo_search/server/plugin.ts | 17 ++- examples/search_explorer/kibana.json | 2 +- examples/search_explorer/package.json | 2 +- .../search_explorer/public/application.tsx | 6 + .../public/async_demo_strategy.tsx | 123 +++++++++++++++++ examples/search_explorer/public/do_search.tsx | 4 +- src/plugins/data/common/search/types.ts | 6 - .../search/es_search/es_search_strategy.ts | 10 +- src/plugins/data/public/search/index.ts | 2 +- .../data/server/search/create_api.test.ts | 2 +- src/plugins/data/server/search/create_api.ts | 11 +- .../search/i_route_handler_search_context.ts | 3 +- src/plugins/data/server/search/i_search.ts | 7 + .../data/server/search/i_search_strategy.ts | 3 +- src/plugins/data/server/search/routes.ts | 33 ++++- x-pack/plugins/data_enhanced/public/index.ts | 2 + x-pack/plugins/data_enhanced/public/plugin.ts | 8 +- .../search/async_search_strategy.test.ts | 125 ++++++++++++++++++ .../public/search/async_search_strategy.ts | 75 +++++++++++ .../data_enhanced/public/search/index.ts | 8 ++ .../data_enhanced/public/search/types.ts | 21 +++ 27 files changed, 601 insertions(+), 28 deletions(-) create mode 100644 examples/demo_search/public/async_demo_search_strategy.ts create mode 100644 examples/demo_search/server/async_demo_search_strategy.ts create mode 100644 examples/search_explorer/public/async_demo_strategy.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/types.ts diff --git a/examples/demo_search/common/index.ts b/examples/demo_search/common/index.ts index 6587ee96ef61b..8ea8d6186ee82 100644 --- a/examples/demo_search/common/index.ts +++ b/examples/demo_search/common/index.ts @@ -20,6 +20,7 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public'; export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY'; +export const ASYNC_DEMO_SEARCH_STRATEGY = 'ASYNC_DEMO_SEARCH_STRATEGY'; export interface IDemoRequest extends IKibanaSearchRequest { mood: string | 'sad' | 'happy'; @@ -29,3 +30,11 @@ export interface IDemoRequest extends IKibanaSearchRequest { export interface IDemoResponse extends IKibanaSearchResponse { greeting: string; } + +export interface IAsyncDemoRequest extends IKibanaSearchRequest { + fibonacciNumbers: number; +} + +export interface IAsyncDemoResponse extends IKibanaSearchResponse { + fibonacciSequence: number[]; +} diff --git a/examples/demo_search/kibana.json b/examples/demo_search/kibana.json index 0603706b07d1f..cb73274ed23f7 100644 --- a/examples/demo_search/kibana.json +++ b/examples/demo_search/kibana.json @@ -2,7 +2,7 @@ "id": "demoSearch", "version": "0.0.1", "kibanaVersion": "kibana", - "configPath": ["demo_search"], + "configPath": ["demoSearch"], "server": true, "ui": true, "requiredPlugins": ["data"], diff --git a/examples/demo_search/public/async_demo_search_strategy.ts b/examples/demo_search/public/async_demo_search_strategy.ts new file mode 100644 index 0000000000000..7a3f33ce05a75 --- /dev/null +++ b/examples/demo_search/public/async_demo_search_strategy.ts @@ -0,0 +1,69 @@ +/* + * 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, + TSearchStrategyProvider, + ISearchStrategy, +} from '../../../src/plugins/data/public'; + +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoResponse } from '../common'; +import { ASYNC_SEARCH_STRATEGY } from '../../../x-pack/plugins/data_enhanced/public'; + +/** + * This demo search strategy provider simply provides a shortcut for calling the DEMO_ASYNC_SEARCH_STRATEGY + * on the server side, without users having to pass it in explicitly, and it takes advantage of the + * already registered ASYNC_SEARCH_STRATEGY that exists on the client. + * + * so instead of callers having to do: + * + * ``` + * search( + * { ...request, serverStrategy: DEMO_ASYNC_SEARCH_STRATEGY }, + * options, + * ASYNC_SEARCH_STRATEGY + * ) as Observable, + *``` + + * They can instead just do + * + * ``` + * search(request, options, DEMO_ASYNC_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 asyncDemoClientSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext +): ISearchStrategy => { + const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY); + const { search } = asyncStrategyProvider(context); + return { + search: (request, options) => { + return search( + { ...request, serverStrategy: ASYNC_DEMO_SEARCH_STRATEGY }, + options + ) as Observable; + }, + }; +}; diff --git a/examples/demo_search/public/demo_search_strategy.ts b/examples/demo_search/public/demo_search_strategy.ts index cb2480c8a5f19..8dc2779a8544c 100644 --- a/examples/demo_search/public/demo_search_strategy.ts +++ b/examples/demo_search/public/demo_search_strategy.ts @@ -43,7 +43,7 @@ import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; * ``` * 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. diff --git a/examples/demo_search/public/plugin.ts b/examples/demo_search/public/plugin.ts index 62c912716e627..a2539cc7a21c5 100644 --- a/examples/demo_search/public/plugin.ts +++ b/examples/demo_search/public/plugin.ts @@ -19,9 +19,16 @@ import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; import { Plugin, CoreSetup } from '../../../src/core/public'; -import { DEMO_SEARCH_STRATEGY } from '../common'; +import { + DEMO_SEARCH_STRATEGY, + IDemoRequest, + IDemoResponse, + ASYNC_DEMO_SEARCH_STRATEGY, + IAsyncDemoRequest, + IAsyncDemoResponse, +} from '../common'; import { demoClientSearchStrategyProvider } from './demo_search_strategy'; -import { IDemoRequest, IDemoResponse } from '../common'; +import { asyncDemoClientSearchStrategyProvider } from './async_demo_search_strategy'; interface DemoDataSearchSetupDependencies { data: DataPublicPluginSetup; @@ -39,10 +46,12 @@ interface DemoDataSearchSetupDependencies { declare module '../../../src/plugins/data/public' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest; } export interface IResponseTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoResponse; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse; } } @@ -52,6 +61,10 @@ export class DemoDataPlugin implements Plugin { DEMO_SEARCH_STRATEGY, demoClientSearchStrategyProvider ); + deps.data.search.registerSearchStrategyProvider( + ASYNC_DEMO_SEARCH_STRATEGY, + asyncDemoClientSearchStrategyProvider + ); } public start() {} diff --git a/examples/demo_search/server/async_demo_search_strategy.ts b/examples/demo_search/server/async_demo_search_strategy.ts new file mode 100644 index 0000000000000..d2d40891a5d93 --- /dev/null +++ b/examples/demo_search/server/async_demo_search_strategy.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 { TSearchStrategyProvider } from '../../../src/plugins/data/server'; +import { ASYNC_DEMO_SEARCH_STRATEGY } from '../common'; + +function getFibonacciSequence(n = 0) { + const beginning = [0, 1].slice(0, n); + return Array(Math.max(0, n)) + .fill(null) + .reduce((sequence, value, i) => { + if (i < 2) return sequence; + return [...sequence, sequence[i - 1] + sequence[i - 2]]; + }, beginning); +} + +const generateId = (() => { + let id = 0; + return () => `${id++}`; +})(); + +const loadedMap = new Map(); +const totalMap = new Map(); + +export const asyncDemoSearchStrategyProvider: TSearchStrategyProvider = () => { + return { + search: async request => { + const id = request.id ?? generateId(); + + const loaded = (loadedMap.get(id) ?? 0) + 1; + loadedMap.set(id, loaded); + + const total = request.fibonacciNumbers ?? totalMap.get(id); + totalMap.set(id, total); + + const fibonacciSequence = getFibonacciSequence(loaded); + return { id, total, loaded, fibonacciSequence }; + }, + cancel: async id => { + loadedMap.delete(id); + totalMap.delete(id); + }, + }; +}; diff --git a/examples/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts index 653aa217717fa..49fbae43e3aa2 100644 --- a/examples/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -20,7 +20,15 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; import { demoSearchStrategyProvider } from './demo_search_strategy'; -import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common'; +import { + DEMO_SEARCH_STRATEGY, + IDemoRequest, + IDemoResponse, + ASYNC_DEMO_SEARCH_STRATEGY, + IAsyncDemoRequest, + IAsyncDemoResponse, +} from '../common'; +import { asyncDemoSearchStrategyProvider } from './async_demo_search_strategy'; interface IDemoSearchExplorerDeps { data: DataPluginSetup; @@ -38,10 +46,12 @@ interface IDemoSearchExplorerDeps { declare module '../../../src/plugins/data/server' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest; } export interface IResponseTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoResponse; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse; } } @@ -54,6 +64,11 @@ export class DemoDataPlugin implements Plugin id: 'demoSearch', component: , }, + { + title: 'Async demo search strategy', + id: 'asyncDemoSearch', + component: , + }, ]; const routes = pages.map((page, i) => ( diff --git a/examples/search_explorer/public/async_demo_strategy.tsx b/examples/search_explorer/public/async_demo_strategy.tsx new file mode 100644 index 0000000000000..40ddcc1f48fe7 --- /dev/null +++ b/examples/search_explorer/public/async_demo_strategy.tsx @@ -0,0 +1,123 @@ +/* + * 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 { + EuiPageContentBody, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, +} from '@elastic/eui'; +import { ISearchGeneric } from '../../../src/plugins/data/public'; +import { DoSearch } from './do_search'; +import { GuideSection } from './guide_section'; + +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoRequest } from '../../demo_search/common'; + +// @ts-ignore +import demoStrategyServerProvider from '!!raw-loader!./../../demo_search/server/async_demo_search_strategy'; +// @ts-ignore +import demoStrategyPublicProvider from '!!raw-loader!./../../demo_search/public/async_demo_search_strategy'; +// @ts-ignore +import demoStrategyServerPlugin from '!!raw-loader!./../../demo_search/server/plugin'; +// @ts-ignore +import demoStrategyPublicPlugin from '!!raw-loader!./../../demo_search/public/plugin'; + +interface Props { + search: ISearchGeneric; +} + +interface State { + searching: boolean; + fibonacciNumbers: number; + changes: boolean; + error?: any; +} + +export class AsyncDemoStrategy extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + searching: false, + changes: false, + fibonacciNumbers: 5, + }; + } + + renderDemo = () => { + const request: IAsyncDemoRequest = { + fibonacciNumbers: this.state.fibonacciNumbers, + }; + return ( + + + + + this.setState({ fibonacciNumbers: parseFloat(e.target.value) })} + /> + + + + + this.props.search(request, { signal }, ASYNC_DEMO_SEARCH_STRATEGY) + } + /> + + ); + }; + + render() { + return ( + + + + ); + } +} diff --git a/examples/search_explorer/public/do_search.tsx b/examples/search_explorer/public/do_search.tsx index f279b9fcd6e23..a6b6b9b57db4a 100644 --- a/examples/search_explorer/public/do_search.tsx +++ b/examples/search_explorer/public/do_search.tsx @@ -118,8 +118,8 @@ ${requestStr} Response: = ( context: ISearchContext ): ISearchStrategy => { + const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY); + const { search } = syncStrategyProvider(context); return { search: (request, options) => { if (typeof request.params.preference === 'undefined') { @@ -33,11 +35,9 @@ export const esSearchStrategyProvider: TSearchStrategyProvider; + return search({ ...request, serverStrategy: ES_SEARCH_STRATEGY }, options) as Observable< + IEsSearchResponse + >; }, }; }; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 762c278a05640..853dbd09e1f93 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -35,7 +35,7 @@ export { export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; -export { SYNC_SEARCH_STRATEGY } from './sync_search_strategy'; +export { ISyncSearchRequest, SYNC_SEARCH_STRATEGY } from './sync_search_strategy'; export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts index cc13269e1aa21..99e48056ef857 100644 --- a/src/plugins/data/server/search/create_api.test.ts +++ b/src/plugins/data/server/search/create_api.test.ts @@ -25,7 +25,7 @@ import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; // let mockCoreSetup: MockedKeys; -const mockDefaultSearch = jest.fn(() => Promise.resolve({ percentComplete: 0 })); +const mockDefaultSearch = jest.fn(() => Promise.resolve({ total: 100, loaded: 0 })); const mockDefaultSearchStrategyProvider = jest.fn(() => Promise.resolve({ search: mockDefaultSearch, diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index e1613103ac399..798a4b82caaef 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -31,7 +31,7 @@ export function createApi({ }) { const api: IRouteHandlerSearchContext = { search: async (request, options, strategyName) => { - const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; + const name = strategyName ?? DEFAULT_SEARCH_STRATEGY; const strategyProvider = searchStrategies[name]; if (!strategyProvider) { throw new Error(`No strategy found for ${strategyName}`); @@ -40,6 +40,15 @@ export function createApi({ const strategy = await strategyProvider(caller, api.search); return strategy.search(request, options); }, + cancel: async (id, strategyName) => { + const name = strategyName ?? DEFAULT_SEARCH_STRATEGY; + const strategyProvider = searchStrategies[name]; + if (!strategyProvider) { + throw new Error(`No strategy found for ${strategyName}`); + } + const strategy = await strategyProvider(caller, api.search); + return strategy.cancel && strategy.cancel(id); + }, }; return api; } 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 index 8a44738a1dcfa..89862781b826e 100644 --- a/src/plugins/data/server/search/i_route_handler_search_context.ts +++ b/src/plugins/data/server/search/i_route_handler_search_context.ts @@ -17,8 +17,9 @@ * under the License. */ -import { ISearchGeneric } from './i_search'; +import { ISearchGeneric, ICancelGeneric } from './i_search'; export interface IRouteHandlerSearchContext { search: ISearchGeneric; + cancel: ICancelGeneric; } diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index 0a35734574153..ea014c5e136d9 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -42,7 +42,14 @@ export type ISearchGeneric = Promise; +export type ICancelGeneric = ( + id: string, + strategy?: T +) => Promise; + export type ISearch = ( request: IRequestTypesMap[T], options?: ISearchOptions ) => Promise; + +export type ICancel = (id: string) => Promise; diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts index d00dd552c9e95..4cfc9608383a9 100644 --- a/src/plugins/data/server/search/i_search_strategy.ts +++ b/src/plugins/data/server/search/i_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; -import { ISearch, ISearchGeneric } from './i_search'; +import { ISearch, ICancel, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { ISearchContext } from './i_search_context'; @@ -28,6 +28,7 @@ import { ISearchContext } from './i_search_context'; */ export interface ISearchStrategy { search: ISearch; + cancel?: ICancel; } /** diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 11879d14931ae..2b8c4b95ee022 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -35,7 +35,7 @@ export function registerSearchRoute(router: IRouter): void { }, async (context, request, res) => { const searchRequest = request.body; - const strategy = request.params.strategy; + const { strategy } = request.params; const signal = getRequestAbortedSignal(request.events.aborted$); try { @@ -54,4 +54,35 @@ export function registerSearchRoute(router: IRouter): void { } } ); + + router.delete( + { + path: '/internal/search/{strategy}/{id}', + validate: { + params: schema.object({ + strategy: schema.string(), + id: schema.string(), + }), + + query: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, res) => { + const { strategy, id } = request.params; + try { + await context.search!.cancel(id, strategy); + return res.ok(); + } catch (err) { + return res.customError({ + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + error: err.body.error, + }, + }, + }); + } + } + ); } diff --git a/x-pack/plugins/data_enhanced/public/index.ts b/x-pack/plugins/data_enhanced/public/index.ts index 93b6b7a957182..927716aae9780 100644 --- a/x-pack/plugins/data_enhanced/public/index.ts +++ b/x-pack/plugins/data_enhanced/public/index.ts @@ -9,3 +9,5 @@ import { DataEnhancedPlugin, DataEnhancedSetup, DataEnhancedStart } from './plug export const plugin = () => new DataEnhancedPlugin(); export { DataEnhancedSetup, DataEnhancedStart }; + +export { ASYNC_SEARCH_STRATEGY, IAsyncSearchRequest, IAsyncSearchOptions } from './search'; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 14b5382bc85aa..4fe27d400f45f 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -8,6 +8,7 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; +import { ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider } from './search'; export interface DataEnhancedSetupDependencies { data: DataPublicPluginSetup; @@ -20,11 +21,14 @@ export type DataEnhancedSetup = ReturnType; export type DataEnhancedStart = ReturnType; export class DataEnhancedPlugin implements Plugin { - public setup(core: CoreSetup, plugins: DataEnhancedSetupDependencies) { - plugins.data.autocomplete.addQuerySuggestionProvider( + constructor() {} + + public setup(core: CoreSetup, { data }: DataEnhancedSetupDependencies) { + data.autocomplete.addQuerySuggestionProvider( KUERY_LANGUAGE_NAME, setupKqlQuerySuggestionProvider(core) ); + data.search.registerSearchStrategyProvider(ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider); } public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts new file mode 100644 index 0000000000000..95f2c9e477064 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { of } from 'rxjs'; +import { AbortController } from 'abort-controller'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { asyncSearchStrategyProvider } from './async_search_strategy'; +import { IAsyncSearchOptions } from './types'; +import { CoreStart } from 'kibana/public'; + +describe('Async search strategy', () => { + let mockCoreStart: MockedKeys; + const mockSearch = jest.fn(); + const mockRequest = { params: {}, serverStrategy: 'foo' }; + const mockOptions: IAsyncSearchOptions = { pollInterval: 0 }; + + beforeEach(() => { + mockCoreStart = coreMock.createStart(); + mockSearch.mockReset(); + }); + + it('only sends one request if the first response is complete', async () => { + mockSearch.mockReturnValueOnce(of({ id: 1, total: 1, loaded: 1 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest); + expect(mockSearch.mock.calls[0][1]).toEqual({}); + expect(mockSearch).toBeCalledTimes(1); + }); + + it('stops polling when the response is complete', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch).toBeCalledTimes(2); + }); + + it('only sends the ID and server strategy after the first request', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch).toBeCalledTimes(2); + expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest); + expect(mockSearch.mock.calls[1][0]).toEqual({ id: 1, serverStrategy: 'foo' }); + }); + + it('sends a DELETE request and stops polling when the signal is aborted', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + const abortController = new AbortController(); + const options = { ...mockOptions, signal: abortController.signal }; + + const promise = asyncSearch.search(mockRequest, options).toPromise(); + abortController.abort(); + + try { + await promise; + } catch (e) { + expect(e.name).toBe('AbortError'); + expect(mockSearch).toBeCalledTimes(1); + expect(mockCoreStart.http.delete).toBeCalled(); + } + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts new file mode 100644 index 0000000000000..fa5d677a53b2a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts @@ -0,0 +1,75 @@ +/* + * 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 { EMPTY, fromEvent, NEVER, Observable, throwError, timer } from 'rxjs'; +import { mergeMap, expand, takeUntil } from 'rxjs/operators'; +import { + IKibanaSearchResponse, + ISearchContext, + ISearchStrategy, + SYNC_SEARCH_STRATEGY, + TSearchStrategyProvider, +} from '../../../../../src/plugins/data/public'; +import { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; + +export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY'; + +declare module '../../../../../src/plugins/data/public' { + export interface IRequestTypesMap { + [ASYNC_SEARCH_STRATEGY]: IAsyncSearchRequest; + } +} + +export const asyncSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext +): ISearchStrategy => { + const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY); + const { search } = syncStrategyProvider(context); + return { + search: ( + request: IAsyncSearchRequest, + { pollInterval = 1000, ...options }: IAsyncSearchOptions = {} + ): Observable => { + const { serverStrategy } = request; + let id: string | undefined = request.id; + + const aborted$ = options.signal + ? fromEvent(options.signal, 'abort').pipe( + mergeMap(() => { + // If we haven't received the response to the initial request, including the ID, then + // we don't need to send a follow-up request to delete this search. Otherwise, we + // send the follow-up request to delete this search, then throw an abort error. + if (id !== undefined) { + context.core.http.delete(`/internal/search/${request.serverStrategy}/${id}`); + } + + const error = new Error('Aborted'); + error.name = 'AbortError'; + return throwError(error); + }) + ) + : NEVER; + + return search(request, options).pipe( + expand(response => { + // If the response indicates it is complete, stop polling and complete the observable + if ((response.loaded ?? 0) >= (response.total ?? 0)) return EMPTY; + + id = response.id; + + // Delay by the given poll interval + return timer(pollInterval).pipe( + // Send future requests using just the ID from the response + mergeMap(() => { + return search({ id, serverStrategy }, options); + }) + ); + }), + takeUntil(aborted$) + ); + }, + }; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/index.ts b/x-pack/plugins/data_enhanced/public/search/index.ts new file mode 100644 index 0000000000000..a7729aeea5647 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider } from './async_search_strategy'; +export { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; diff --git a/x-pack/plugins/data_enhanced/public/search/types.ts b/x-pack/plugins/data_enhanced/public/search/types.ts new file mode 100644 index 0000000000000..edaaf1b22654d --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/types.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 { ISearchOptions, ISyncSearchRequest } from '../../../../../src/plugins/data/public'; + +export interface IAsyncSearchRequest extends ISyncSearchRequest { + /** + * The ID received from the response from the initial request + */ + id?: string; +} + +export interface IAsyncSearchOptions extends ISearchOptions { + /** + * The number of milliseconds to wait between receiving a response and sending another request + */ + pollInterval?: number; +} From 29439789abc05d05f7a12305397f23469a3499d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 25 Feb 2020 18:18:27 -0500 Subject: [PATCH 110/113] Add migration support to the event log (#58010) * Initial work * Add missing file * Add tests where missing * Add kibana version to esNames * Share ILM policy across versions --- .../server/es/cluster_client_adapter.test.ts | 1 + .../event_log/server/es/cluster_client_adapter.ts | 7 +++++-- .../plugins/event_log/server/es/documents.test.ts | 3 +-- x-pack/plugins/event_log/server/es/documents.ts | 5 +---- x-pack/plugins/event_log/server/es/init.ts | 8 +++++++- x-pack/plugins/event_log/server/es/names.mock.ts | 7 ++++--- x-pack/plugins/event_log/server/es/names.test.ts | 12 +++++++++--- x-pack/plugins/event_log/server/es/names.ts | 14 ++++++++++---- 8 files changed, 38 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index ecefd4bfa271e..b61196439ee4f 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -173,6 +173,7 @@ describe('createIndex', () => { await clusterClientAdapter.createIndex('foo'); expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', { index: 'foo', + body: {}, }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index c74eeacc9bb19..d585fd4f539b5 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -94,9 +94,12 @@ export class ClusterClientAdapter { return result as boolean; } - public async createIndex(name: string): Promise { + public async createIndex(name: string, body: any = {}): Promise { try { - await this.callEs('indices.create', { index: name }); + await this.callEs('indices.create', { + index: name, + body, + }); } catch (err) { if (err.body?.error?.type !== 'resource_already_exists_exception') { throw new Error(`error creating initial index: ${err.message}`); diff --git a/x-pack/plugins/event_log/server/es/documents.test.ts b/x-pack/plugins/event_log/server/es/documents.test.ts index 7edca4b3943a6..c08d0ac978bc9 100644 --- a/x-pack/plugins/event_log/server/es/documents.test.ts +++ b/x-pack/plugins/event_log/server/es/documents.test.ts @@ -22,8 +22,7 @@ describe('getIndexTemplate()', () => { test('returns the correct details of the index template', () => { const indexTemplate = getIndexTemplate(esNames); - expect(indexTemplate.index_patterns).toEqual([esNames.indexPattern]); - expect(indexTemplate.aliases[esNames.alias]).toEqual({}); + expect(indexTemplate.index_patterns).toEqual([esNames.indexPatternWithVersion]); expect(indexTemplate.settings.number_of_shards).toBeGreaterThanOrEqual(0); expect(indexTemplate.settings.number_of_replicas).toBeGreaterThanOrEqual(0); expect(indexTemplate.settings['index.lifecycle.name']).toBe(esNames.ilmPolicy); diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index 09dd7383c4c5e..982454e671008 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -10,10 +10,7 @@ import mappings from '../../generated/mappings.json'; // returns the body of an index template used in an ES indices.putTemplate call export function getIndexTemplate(esNames: EsNames) { const indexTemplateBody: any = { - index_patterns: [esNames.indexPattern], - aliases: { - [esNames.alias]: {}, - }, + index_patterns: [esNames.indexPatternWithVersion], settings: { number_of_shards: 1, number_of_replicas: 1, diff --git a/x-pack/plugins/event_log/server/es/init.ts b/x-pack/plugins/event_log/server/es/init.ts index 7094277f7aa9f..c67d6541ce002 100644 --- a/x-pack/plugins/event_log/server/es/init.ts +++ b/x-pack/plugins/event_log/server/es/init.ts @@ -62,7 +62,13 @@ class EsInitializationSteps { async createInitialIndexIfNotExists(): Promise { const exists = await this.esContext.esAdapter.doesAliasExist(this.esContext.esNames.alias); if (!exists) { - await this.esContext.esAdapter.createIndex(this.esContext.esNames.initialIndex); + await this.esContext.esAdapter.createIndex(this.esContext.esNames.initialIndex, { + aliases: { + [this.esContext.esNames.alias]: { + is_write_index: true, + }, + }, + }); } } } diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 7b013a0d263da..268421235b4b2 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -9,11 +9,12 @@ import { EsNames } from './names'; const createNamesMock = () => { const mock: jest.Mocked = { base: '.kibana', - alias: '.kibana-event-log', + alias: '.kibana-event-log-8.0.0', ilmPolicy: '.kibana-event-log-policy', indexPattern: '.kibana-event-log-*', - initialIndex: '.kibana-event-log-000001', - indexTemplate: '.kibana-event-log-template', + indexPatternWithVersion: '.kibana-event-log-8.0.0-*', + initialIndex: '.kibana-event-log-8.0.0-000001', + indexTemplate: '.kibana-event-log-8.0.0-template', }; return mock; }; diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index d88c4212df91c..baefd756bb1ed 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -6,15 +6,21 @@ import { getEsNames } from './names'; +jest.mock('../lib/../../../../package.json', () => ({ + version: '1.2.3', +})); + describe('getEsNames()', () => { test('works as expected', () => { const base = 'XYZ'; + const version = '1.2.3'; const esNames = getEsNames(base); expect(esNames.base).toEqual(base); - expect(esNames.alias).toEqual(`${base}-event-log`); + expect(esNames.alias).toEqual(`${base}-event-log-${version}`); expect(esNames.ilmPolicy).toEqual(`${base}-event-log-policy`); expect(esNames.indexPattern).toEqual(`${base}-event-log-*`); - expect(esNames.initialIndex).toEqual(`${base}-event-log-000001`); - expect(esNames.indexTemplate).toEqual(`${base}-event-log-template`); + expect(esNames.indexPatternWithVersion).toEqual(`${base}-event-log-${version}-*`); + expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`); + expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`); }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index be737d23625f1..d55d02a16fc9a 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -4,25 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -const EVENT_LOG_NAME_SUFFIX = '-event-log'; +import xPackage from '../../../../package.json'; + +const EVENT_LOG_NAME_SUFFIX = `-event-log`; +const EVENT_LOG_VERSION_SUFFIX = `-${xPackage.version}`; export interface EsNames { base: string; alias: string; ilmPolicy: string; indexPattern: string; + indexPatternWithVersion: string; initialIndex: string; indexTemplate: string; } export function getEsNames(baseName: string): EsNames { const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; + const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; return { base: baseName, - alias: eventLogName, + alias: eventLogNameWithVersion, ilmPolicy: `${eventLogName}-policy`, indexPattern: `${eventLogName}-*`, - initialIndex: `${eventLogName}-000001`, - indexTemplate: `${eventLogName}-template`, + indexPatternWithVersion: `${eventLogNameWithVersion}-*`, + initialIndex: `${eventLogNameWithVersion}-000001`, + indexTemplate: `${eventLogNameWithVersion}-template`, }; } From 24b8ecf74d559ce1226d730ed961d32d5196ef47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 25 Feb 2020 21:47:53 -0500 Subject: [PATCH 111/113] Add localization to missing action types (#58554) --- .../actions/server/builtin_action_types/server_log.ts | 8 ++++---- .../server/builtin_action_types/servicenow.test.ts | 2 +- .../actions/server/builtin_action_types/servicenow.ts | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index 62406cfaf66e1..01355f2a34f92 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -12,8 +12,6 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { withoutControlCharacters } from './lib/string_utils'; -const ACTION_NAME = 'Server log'; - // params definition export type ActionParamsType = TypeOf; @@ -37,7 +35,9 @@ const ParamsSchema = schema.object({ export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.server-log', - name: ACTION_NAME, + name: i18n.translate('xpack.actions.builtin.serverLogTitle', { + defaultMessage: 'Server log', + }), validate: { params: ParamsSchema, }, @@ -56,7 +56,7 @@ async function executor( const sanitizedMessage = withoutControlCharacters(params.message); try { - logger[params.level](`${ACTION_NAME}: ${sanitizedMessage}`); + logger[params.level](`Server log: ${sanitizedMessage}`); } catch (err) { const message = i18n.translate('xpack.actions.builtin.serverLog.errorLoggingErrorMessage', { defaultMessage: 'error logging message', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts index a6c43f48fa803..9ae96cb23a5c3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts @@ -49,7 +49,7 @@ beforeAll(() => { describe('get()', () => { test('should return correct action type', () => { expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual('servicenow'); + expect(actionType.name).toEqual('ServiceNow'); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts index 2d5c18207def3..0ad435281eba4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts @@ -89,7 +89,9 @@ export function getActionType({ }): ActionType { return { id: '.servicenow', - name: 'servicenow', + name: i18n.translate('xpack.actions.builtin.servicenowTitle', { + defaultMessage: 'ServiceNow', + }), validate: { config: schema.object(ConfigSchemaProps, { validate: curry(validateConfig)(configurationUtilities), From b56eead4ac7d2ccc5657f51a2be665cbc6118bec Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 25 Feb 2020 21:59:44 -0500 Subject: [PATCH 112/113] [SIEM] [Detections] Refactor the all rules page (#58428) * Refactor the all rules page to be easier to test * review with Garrett * bring back utility bar under condition * fix bugs tags and allow switch to show loading when enable/disable rule * fix rules selection when trigerring new rules * fix imports/exports can only use rule_id as learned today * review I --- .../detection_engine/rules/use_rules.test.tsx | 99 ++++---- .../detection_engine/rules/use_rules.tsx | 50 ++-- .../detection_engine/rules/use_tags.test.tsx | 8 +- .../detection_engine/rules/use_tags.tsx | 9 +- .../rules/all/__mocks__/mock.ts | 101 -------- .../detection_engine/rules/all/actions.tsx | 51 ++-- .../rules/all/batch_actions.tsx | 74 +++--- .../detection_engine/rules/all/columns.tsx | 92 +++++--- .../rules/all/helpers.test.tsx | 18 +- .../detection_engine/rules/all/helpers.ts | 33 +-- .../detection_engine/rules/all/index.tsx | 220 +++++++++--------- .../detection_engine/rules/all/reducer.ts | 160 ++++++------- .../rules_table_filters.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 1 + .../rule_actions_overflow/index.tsx | 10 +- .../components/rule_downloader/index.tsx | 14 +- .../pages/detection_engine/rules/index.tsx | 18 +- .../pages/detection_engine/rules/types.ts | 19 -- .../routes/rules/create_rules_bulk_route.ts | 4 +- .../routes/rules/utils.test.ts | 34 ++- .../detection_engine/routes/rules/utils.ts | 15 +- 21 files changed, 461 insertions(+), 575 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx index b369d3a50730d..242d715e20f77 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx @@ -5,9 +5,8 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { useRules, ReturnRules } from './use_rules'; +import { useRules, UseRules, ReturnRules } from './use_rules'; import * as api from './api'; -import { PaginationOptions, FilterOptions } from '.'; jest.mock('./api'); @@ -17,55 +16,40 @@ describe('useRules', () => { }); test('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(props => - useRules( - { + const { result, waitForNextUpdate } = renderHook(props => + useRules({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: '', sortField: 'created_at', sortOrder: 'desc', - } - ) + }, + }) ); await waitForNextUpdate(); - expect(result.current).toEqual([ - true, - { - data: [], - page: 1, - perPage: 20, - total: 0, - }, - null, - ]); + expect(result.current).toEqual([true, null, result.current[2]]); }); }); test('fetch rules', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(() => - useRules( - { + const { result, waitForNextUpdate } = renderHook(() => + useRules({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: '', sortField: 'created_at', sortOrder: 'desc', - } - ) + }, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -148,22 +132,19 @@ describe('useRules', () => { test('re-fetch rules', async () => { const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); await act(async () => { - const { result, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(id => - useRules( - { + const { result, waitForNextUpdate } = renderHook(id => + useRules({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: '', sortField: 'created_at', sortOrder: 'desc', - } - ) + }, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -178,37 +159,37 @@ describe('useRules', () => { test('fetch rules if props changes', async () => { const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); await act(async () => { - const { rerender, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(args => useRules(args[0], args[1]), { - initialProps: [ - { - page: 1, - perPage: 10, - total: 100, - }, - { - filter: '', - sortField: 'created_at', - sortOrder: 'desc', + const { rerender, waitForNextUpdate } = renderHook( + args => useRules(args), + { + initialProps: { + pagination: { + page: 1, + perPage: 10, + total: 100, + }, + filterOptions: { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + }, }, - ], - }); + } + ); await waitForNextUpdate(); await waitForNextUpdate(); - rerender([ - { + rerender({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: 'hello world', sortField: 'created_at', sortOrder: 'desc', }, - ]); + }); await waitForNextUpdate(); expect(spyOnfetchRules).toHaveBeenCalledTimes(2); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index 301a68dc6f445..d05d59d15802d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -4,16 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; -import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; +import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from './types'; import { useStateToaster } from '../../../components/toasters'; import { fetchRules } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -type Func = () => void; -export type ReturnRules = [boolean, FetchRulesResponse, Func | null]; +export type ReturnRules = [ + boolean, + FetchRulesResponse | null, + (refreshPrePackagedRule?: boolean) => void +]; + +export interface UseRules { + pagination: PaginationOptions; + filterOptions: FilterOptions; + refetchPrePackagedRulesStatus?: () => void; + dispatchRulesInReducer?: (rules: Rule[]) => void; +} /** * Hook for using the list of Rules from the Detection Engine API @@ -21,17 +32,14 @@ export type ReturnRules = [boolean, FetchRulesResponse, Func | null]; * @param pagination desired pagination options (e.g. page/perPage) * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) */ -export const useRules = ( - pagination: PaginationOptions, - filterOptions: FilterOptions -): ReturnRules => { - const [rules, setRules] = useState({ - page: 1, - perPage: 20, - total: 0, - data: [], - }); - const reFetchRules = useRef(null); +export const useRules = ({ + pagination, + filterOptions, + refetchPrePackagedRulesStatus, + dispatchRulesInReducer, +}: UseRules): ReturnRules => { + const [rules, setRules] = useState(null); + const reFetchRules = useRef<(refreshPrePackagedRule?: boolean) => void>(noop); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -50,10 +58,16 @@ export const useRules = ( if (isSubscribed) { setRules(fetchRulesResult); + if (dispatchRulesInReducer != null) { + dispatchRulesInReducer(fetchRulesResult.data); + } } } catch (error) { if (isSubscribed) { errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + if (dispatchRulesInReducer != null) { + dispatchRulesInReducer([]); + } } } if (isSubscribed) { @@ -62,7 +76,12 @@ export const useRules = ( } fetchData(); - reFetchRules.current = fetchData.bind(null, true); + reFetchRules.current = (refreshPrePackagedRule: boolean = false) => { + fetchData(true); + if (refreshPrePackagedRule && refetchPrePackagedRulesStatus != null) { + refetchPrePackagedRulesStatus(); + } + }; return () => { isSubscribed = false; abortCtrl.abort(); @@ -76,6 +95,7 @@ export const useRules = ( filterOptions.tags?.sort().join(), filterOptions.showCustomRules, filterOptions.showElasticRules, + refetchPrePackagedRulesStatus, ]); return [loading, rules, reFetchRules.current]; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx index 4a796efa5b0cb..68f54b35754f6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx @@ -14,7 +14,7 @@ describe('useTags', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useTags()); await waitForNextUpdate(); - expect(result.current).toEqual([true, []]); + expect(result.current).toEqual([true, [], result.current[2]]); }); }); @@ -23,7 +23,11 @@ describe('useTags', () => { const { result, waitForNextUpdate } = renderHook(() => useTags()); await waitForNextUpdate(); await waitForNextUpdate(); - expect(result.current).toEqual([false, ['elastic', 'love', 'quality', 'code']]); + expect(result.current).toEqual([ + false, + ['elastic', 'love', 'quality', 'code'], + result.current[2], + ]); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx index 196d4b1420561..5985200fa16ec 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { noop } from 'lodash/fp'; +import { useEffect, useState, useRef } from 'react'; import { useStateToaster } from '../../../components/toasters'; import { fetchTags } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -export type ReturnTags = [boolean, string[]]; +export type ReturnTags = [boolean, string[], () => void]; /** * Hook for using the list of Tags from the Detection Engine API @@ -20,6 +21,7 @@ export const useTags = (): ReturnTags => { const [tags, setTags] = useState([]); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); + const reFetchTags = useRef<() => void>(noop); useEffect(() => { let isSubscribed = true; @@ -46,6 +48,7 @@ export const useTags = (): ReturnTags => { } fetchData(); + reFetchTags.current = fetchData; return () => { isSubscribed = false; @@ -53,5 +56,5 @@ export const useTags = (): ReturnTags => { }; }, []); - return [loading, tags]; + return [loading, tags, reFetchTags.current]; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 980575f1470a5..e2287e5eeeb3f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -5,7 +5,6 @@ */ import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; -import { TableData } from '../../types'; export const mockRule = (id: string): Rule => ({ created_at: '2020-01-10T21:11:45.839Z', @@ -50,103 +49,3 @@ export const mockRules: Rule[] = [ mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), ]; -export const mockTableData: TableData[] = [ - { - activate: true, - id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61', - immutable: false, - isLoading: false, - risk_score: 21, - rule: { - href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61', - name: 'Home Grown!', - }, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - severity: 'low', - sourceRule: { - created_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61', - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { from: '0m' }, - name: 'Home Grown!', - output_index: '.siem-signals-default', - query: '', - references: [], - risk_score: 21, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - saved_id: "Garrett's IP", - severity: 'low', - tags: [], - threat: [], - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - to: 'now', - type: 'saved_query', - updated_at: '2020-01-10T21:11:45.839Z', - updated_by: 'elastic', - version: 1, - }, - status: null, - statusDate: null, - tags: [], - }, - { - activate: true, - id: '63f06f34-c181-4b2d-af35-f2ace572a1ee', - immutable: false, - isLoading: false, - risk_score: 21, - rule: { - href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee', - name: 'Home Grown!', - }, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - severity: 'low', - sourceRule: { - created_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id: '63f06f34-c181-4b2d-af35-f2ace572a1ee', - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { from: '0m' }, - name: 'Home Grown!', - output_index: '.siem-signals-default', - query: '', - references: [], - risk_score: 21, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - saved_id: "Garrett's IP", - severity: 'low', - tags: [], - threat: [], - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - to: 'now', - type: 'saved_query', - updated_at: '2020-01-10T21:11:45.839Z', - updated_by: 'elastic', - version: 1, - }, - status: null, - statusDate: null, - tags: [], - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index 6212c2067384d..a17fd34d1c344 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -32,53 +32,58 @@ export const editRuleAction = (rule: Rule, history: H.History) => { export const duplicateRulesAction = async ( rules: Rule[], + ruleIds: string[], dispatch: React.Dispatch, dispatchToaster: Dispatch ) => { try { - const ruleIds = rules.map(r => r.id); - dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true }); - const duplicatedRules = await duplicateRules({ rules }); - dispatch({ type: 'refresh' }); - displaySuccessToast( - i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length), - dispatchToaster - ); + dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' }); + const response = await duplicateRules({ rules }); + const { errors } = bucketRulesResponse(response); + if (errors.length > 0) { + displayErrorToast( + i18n.DUPLICATE_RULE_ERROR, + errors.map(e => e.error.message), + dispatchToaster + ); + } else { + displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); + } + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } catch (e) { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster); } }; -export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch) => { - dispatch({ type: 'setExportPayload', exportPayload: rules }); +export const exportRulesAction = (exportRuleId: string[], dispatch: React.Dispatch) => { + dispatch({ type: 'exportRuleIds', ids: exportRuleId }); }; export const deleteRulesAction = async ( - ids: string[], + ruleIds: string[], dispatch: React.Dispatch, dispatchToaster: Dispatch, onRuleDeleted?: () => void ) => { try { - dispatch({ type: 'loading', isLoading: true }); - - const response = await deleteRules({ ids }); + dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'delete' }); + const response = await deleteRules({ ids: ruleIds }); const { errors } = bucketRulesResponse(response); - - dispatch({ type: 'refresh' }); + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); if (errors.length > 0) { displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length), + i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), errors.map(e => e.error.message), dispatchToaster ); - } else { - // FP: See https://github.com/typescript-eslint/typescript-eslint/issues/1138#issuecomment-566929566 - onRuleDeleted?.(); // eslint-disable-line no-unused-expressions + } else if (onRuleDeleted) { + onRuleDeleted(); } } catch (e) { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length), + i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), [e.message], dispatchToaster ); @@ -96,7 +101,7 @@ export const enableRulesAction = async ( : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length); try { - dispatch({ type: 'updateLoading', ids, isLoading: true }); + dispatch({ type: 'loadingRuleIds', ids, actionType: enabled ? 'enable' : 'disable' }); const response = await enableRules({ ids, enabled }); const { rules, errors } = bucketRulesResponse(response); @@ -125,6 +130,6 @@ export const enableRulesAction = async ( } } catch (e) { displayErrorToast(errorTitle, [e.message], dispatchToaster); - dispatch({ type: 'updateLoading', ids, isLoading: false }); + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx index 8a10d4f7100b9..a0942d7f6534a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx @@ -6,9 +6,7 @@ import { EuiContextMenuItem } from '@elastic/eui'; import React, { Dispatch } from 'react'; -import * as H from 'history'; import * as i18n from '../translations'; -import { TableData } from '../types'; import { Action } from './reducer'; import { deleteRulesAction, @@ -17,18 +15,37 @@ import { exportRulesAction, } from './actions'; import { ActionToaster } from '../../../../components/toasters'; +import { Rule } from '../../../../containers/detection_engine/rules'; -export const getBatchItems = ( - selectedState: TableData[], - dispatch: Dispatch, - dispatchToaster: Dispatch, - history: H.History, - closePopover: () => void -) => { - const containsEnabled = selectedState.some(v => v.activate); - const containsDisabled = selectedState.some(v => !v.activate); - const containsLoading = selectedState.some(v => v.isLoading); - const containsImmutable = selectedState.some(v => v.immutable); +interface GetBatchItems { + closePopover: () => void; + dispatch: Dispatch; + dispatchToaster: Dispatch; + loadingRuleIds: string[]; + reFetchRules: (refreshPrePackagedRule?: boolean) => void; + rules: Rule[]; + selectedRuleIds: string[]; +} + +export const getBatchItems = ({ + closePopover, + dispatch, + dispatchToaster, + loadingRuleIds, + reFetchRules, + rules, + selectedRuleIds, +}: GetBatchItems) => { + const containsEnabled = selectedRuleIds.some( + id => rules.find(r => r.id === id)?.enabled ?? false + ); + const containsDisabled = selectedRuleIds.some( + id => !rules.find(r => r.id === id)?.enabled ?? false + ); + const containsLoading = selectedRuleIds.some(id => loadingRuleIds.includes(id)); + const containsImmutable = selectedRuleIds.some( + id => rules.find(r => r.id === id)?.immutable ?? false + ); return [ { closePopover(); - const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id); + const deactivatedIds = selectedRuleIds.filter( + id => !rules.find(r => r.id === id)?.enabled ?? false + ); await enableRulesAction(deactivatedIds, true, dispatch, dispatchToaster); }} > @@ -49,7 +68,9 @@ export const getBatchItems = ( disabled={containsLoading || !containsEnabled} onClick={async () => { closePopover(); - const activatedIds = selectedState.filter(s => s.activate).map(s => s.id); + const activatedIds = selectedRuleIds.filter( + id => rules.find(r => r.id === id)?.enabled ?? false + ); await enableRulesAction(activatedIds, false, dispatch, dispatchToaster); }} > @@ -58,11 +79,11 @@ export const getBatchItems = ( { + disabled={containsImmutable || containsLoading || selectedRuleIds.length === 0} + onClick={() => { closePopover(); - await exportRulesAction( - selectedState.map(s => s.sourceRule), + exportRulesAction( + rules.filter(r => selectedRuleIds.includes(r.id)).map(r => r.rule_id), dispatch ); }} @@ -72,14 +93,16 @@ export const getBatchItems = ( { closePopover(); await duplicateRulesAction( - selectedState.map(s => s.sourceRule), + rules.filter(r => selectedRuleIds.includes(r.id)), + selectedRuleIds, dispatch, dispatchToaster ); + reFetchRules(true); }} > {i18n.BATCH_ACTION_DUPLICATE_SELECTED} @@ -88,14 +111,11 @@ export const getBatchItems = ( key={i18n.BATCH_ACTION_DELETE_SELECTED} icon="trash" title={containsImmutable ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE : undefined} - disabled={containsLoading || selectedState.length === 0} + disabled={containsLoading || selectedRuleIds.length === 0} onClick={async () => { closePopover(); - await deleteRulesAction( - selectedState.map(({ sourceRule: { id } }) => id), - dispatch, - dispatchToaster - ); + await deleteRulesAction(selectedRuleIds, dispatch, dispatchToaster); + reFetchRules(true); }} > {i18n.BATCH_ACTION_DELETE_SELECTED} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index d648854368c28..ff104f09d68ef 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -15,72 +15,92 @@ import { } from '@elastic/eui'; import * as H from 'history'; import React, { Dispatch } from 'react'; + +import { Rule } from '../../../../containers/detection_engine/rules'; import { getEmptyTagValue } from '../../../../components/empty_value'; +import { FormattedDate } from '../../../../components/formatted_date'; +import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine'; +import { ActionToaster } from '../../../../components/toasters'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import { getStatusColor } from '../components/rule_status/helpers'; +import { RuleSwitch } from '../components/rule_switch'; +import { SeverityBadge } from '../components/severity_badge'; +import * as i18n from '../translations'; import { deleteRulesAction, duplicateRulesAction, editRuleAction, exportRulesAction, } from './actions'; - import { Action } from './reducer'; -import { TableData } from '../types'; -import * as i18n from '../translations'; -import { FormattedDate } from '../../../../components/formatted_date'; -import { RuleSwitch } from '../components/rule_switch'; -import { SeverityBadge } from '../components/severity_badge'; -import { ActionToaster } from '../../../../components/toasters'; -import { getStatusColor } from '../components/rule_status/helpers'; -import { TruncatableText } from '../../../../components/truncatable_text'; const getActions = ( dispatch: React.Dispatch, dispatchToaster: Dispatch, - history: H.History + history: H.History, + reFetchRules: (refreshPrePackagedRule?: boolean) => void ) => [ { description: i18n.EDIT_RULE_SETTINGS, icon: 'visControls', name: i18n.EDIT_RULE_SETTINGS, - onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history), - enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable, + onClick: (rowItem: Rule) => editRuleAction(rowItem, history), + enabled: (rowItem: Rule) => !rowItem.immutable, }, { description: i18n.DUPLICATE_RULE, icon: 'copy', name: i18n.DUPLICATE_RULE, - onClick: (rowItem: TableData) => - duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster), + onClick: (rowItem: Rule) => { + duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); + reFetchRules(true); + }, }, { description: i18n.EXPORT_RULE, icon: 'exportAction', name: i18n.EXPORT_RULE, - onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch), - enabled: (rowItem: TableData) => !rowItem.immutable, + onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), + enabled: (rowItem: Rule) => !rowItem.immutable, }, { description: i18n.DELETE_RULE, icon: 'trash', name: i18n.DELETE_RULE, - onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster), + onClick: (rowItem: Rule) => { + deleteRulesAction([rowItem.id], dispatch, dispatchToaster); + reFetchRules(true); + }, }, ]; -type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; +type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; + +interface GetColumns { + dispatch: React.Dispatch; + dispatchToaster: Dispatch; + history: H.History; + hasNoPermissions: boolean; + loadingRuleIds: string[]; + reFetchRules: (refreshPrePackagedRule?: boolean) => void; +} // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const getColumns = ( - dispatch: React.Dispatch, - dispatchToaster: Dispatch, - history: H.History, - hasNoPermissions: boolean -): RulesColumns[] => { +export const getColumns = ({ + dispatch, + dispatchToaster, + history, + hasNoPermissions, + loadingRuleIds, + reFetchRules, +}: GetColumns): RulesColumns[] => { const cols: RulesColumns[] = [ { - field: 'rule', + field: 'name', name: i18n.COLUMN_RULE, - render: (value: TableData['rule']) => {value.name}, + render: (value: Rule['name'], item: Rule) => ( + {value} + ), truncateText: true, width: '24%', }, @@ -93,14 +113,14 @@ export const getColumns = ( { field: 'severity', name: i18n.COLUMN_SEVERITY, - render: (value: TableData['severity']) => , + render: (value: Rule['severity']) => , truncateText: true, width: '16%', }, { - field: 'statusDate', + field: 'status_date', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: TableData['statusDate']) => { + render: (value: Rule['status_date']) => { return value == null ? ( getEmptyTagValue() ) : ( @@ -114,7 +134,7 @@ export const getColumns = ( { field: 'status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: TableData['status']) => { + render: (value: Rule['status']) => { return ( <> @@ -129,7 +149,7 @@ export const getColumns = ( { field: 'tags', name: i18n.COLUMN_TAGS, - render: (value: TableData['tags']) => ( + render: (value: Rule['tags']) => ( {value.map((tag, i) => ( @@ -145,13 +165,13 @@ export const getColumns = ( align: 'center', field: 'activate', name: i18n.COLUMN_ACTIVATE, - render: (value: TableData['activate'], item: TableData) => ( + render: (value: Rule['enabled'], item: Rule) => ( ), sortable: true, @@ -160,9 +180,9 @@ export const getColumns = ( ]; const actions: RulesColumns[] = [ { - actions: getActions(dispatch, dispatchToaster, history), + actions: getActions(dispatch, dispatchToaster, history, reFetchRules), width: '40px', - } as EuiTableActionsColumnType, + } as EuiTableActionsColumnType, ]; return hasNoPermissions ? cols : [...cols, ...actions]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx index e925161444e42..c60933733587d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { bucketRulesResponse, formatRules } from './helpers'; -import { mockRule, mockRuleError, mockRules, mockTableData } from './__mocks__/mock'; +import { bucketRulesResponse } from './helpers'; +import { mockRule, mockRuleError } from './__mocks__/mock'; import uuid from 'uuid'; import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; @@ -15,20 +15,6 @@ describe('AllRulesTable Helpers', () => { const mockRuleError1: Readonly = mockRuleError(uuid.v4()); const mockRuleError2: Readonly = mockRuleError(uuid.v4()); - describe('formatRules', () => { - test('formats rules with no selection', () => { - const formattedRules = formatRules(mockRules); - expect(formattedRules).toEqual(mockTableData); - }); - - test('formats rules with selection', () => { - const mockTableDataWithSelected = [...mockTableData]; - mockTableDataWithSelected[0].isLoading = true; - const formattedRules = formatRules(mockRules, [mockRules[0].id]); - expect(formattedRules).toEqual(mockTableDataWithSelected); - }); - }); - describe('bucketRulesResponse', () => { test('buckets empty response', () => { const bucketedResponse = bucketRulesResponse([]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 9a523536694d9..5ce26144a4d9c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -9,32 +9,6 @@ import { RuleError, RuleResponseBuckets, } from '../../../../containers/detection_engine/rules'; -import { TableData } from '../types'; - -/** - * Formats rules into the correct format for the AllRulesTable - * - * @param rules as returned from the Rules API - * @param selectedIds ids of the currently selected rules - */ -export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] => - rules.map(rule => ({ - id: rule.id, - immutable: rule.immutable, - rule_id: rule.rule_id, - rule: { - href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`, - name: rule.name, - }, - risk_score: rule.risk_score, - severity: rule.severity, - tags: rule.tags ?? [], - activate: rule.enabled, - status: rule.status ?? null, - statusDate: rule.status_date ?? null, - sourceRule: rule, - isLoading: selectedIds?.includes(rule.id) ?? false, - })); /** * Separates rules/errors from bulk rules API response (create/update/delete) @@ -52,14 +26,11 @@ export const bucketRulesResponse = (response: Array) => ); export const showRulesTable = ({ - isInitialLoad, rulesCustomInstalled, rulesInstalled, }: { - isInitialLoad: boolean; rulesCustomInstalled: number | null; rulesInstalled: number | null; }) => - !isInitialLoad && - ((rulesCustomInstalled != null && rulesCustomInstalled > 0) || - (rulesInstalled != null && rulesInstalled > 0)); + (rulesCustomInstalled != null && rulesCustomInstalled > 0) || + (rulesInstalled != null && rulesInstalled > 0); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index b304d77f2e276..79fec526faf48 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -11,15 +11,16 @@ import { EuiLoadingContent, EuiSpacer, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; import uuid from 'uuid'; import { useRules, CreatePreBuiltRules, FilterOptions, + Rule, } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import { @@ -36,35 +37,39 @@ import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_em import { RuleDownloader } from '../components/rule_downloader'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; -import { EuiBasicTableOnChange, TableData } from '../types'; +import { EuiBasicTableOnChange } from '../types'; import { getBatchItems } from './batch_actions'; import { getColumns } from './columns'; import { showRulesTable } from './helpers'; import { allRulesReducer, State } from './reducer'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; +// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way +// after few hours of fight with typescript !!!! I lost :( +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; + const initialState: State = { - isLoading: true, - rules: [], - tableData: [], - selectedItems: [], - refreshToggle: true, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, + exportRuleIds: [], filterOptions: { filter: '', sortField: 'enabled', sortOrder: 'desc', }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + rules: [], + selectedRuleIds: [], }; interface AllRulesProps { createPrePackagedRules: CreatePreBuiltRules | null; hasNoPermissions: boolean; - importCompleteToggle: boolean; loading: boolean; loadingCreatePrePackagedRules: boolean; refetchPrePackagedRulesStatus: () => void; @@ -72,7 +77,7 @@ interface AllRulesProps { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: () => void) => void; + setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; } /** @@ -87,7 +92,6 @@ export const AllRules = React.memo( ({ createPrePackagedRules, hasNoPermissions, - importCompleteToggle, loading, loadingCreatePrePackagedRules, refetchPrePackagedRulesStatus, @@ -97,24 +101,36 @@ export const AllRules = React.memo( rulesNotUpdated, setRefreshRulesData, }) => { + const [initLoading, setInitLoading] = useState(true); + const tableRef = useRef(); const [ { - exportPayload, + exportRuleIds, filterOptions, - isLoading, - refreshToggle, - selectedItems, - tableData, + loadingRuleIds, + loadingRulesAction, pagination, + rules, + selectedRuleIds, }, dispatch, - ] = useReducer(allRulesReducer, initialState); + ] = useReducer(allRulesReducer(tableRef), initialState); const history = useHistory(); - const [oldRefreshToggle, setOldRefreshToggle] = useState(refreshToggle); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const [isGlobalLoading, setIsGlobalLoad] = useState(false); const [, dispatchToaster] = useStateToaster(); - const [isLoadingRules, rulesData, reFetchRulesData] = useRules(pagination, filterOptions); + + const setRules = useCallback((newRules: Rule[]) => { + dispatch({ + type: 'setRules', + rules: newRules, + }); + }, []); + + const [isLoadingRules, , reFetchRulesData] = useRules({ + pagination, + filterOptions, + refetchPrePackagedRulesStatus, + dispatchRulesInReducer: setRules, + }); const prePackagedRuleStatus = getPrePackagedRuleStatus( rulesInstalled, @@ -125,10 +141,18 @@ export const AllRules = React.memo( const getBatchItemsPopoverContent = useCallback( (closePopover: () => void) => ( ), - [selectedItems, dispatch, dispatchToaster, history] + [dispatch, dispatchToaster, loadingRuleIds, reFetchRulesData, rules, selectedRuleIds] ); const tableOnChangeCallback = useCallback( @@ -146,46 +170,19 @@ export const AllRules = React.memo( ); const columns = useMemo(() => { - return getColumns(dispatch, dispatchToaster, history, hasNoPermissions); - }, [dispatch, dispatchToaster, history]); - - useEffect(() => { - dispatch({ type: 'loading', isLoading: isLoadingRules }); - }, [isLoadingRules]); - - useEffect(() => { - if (!isLoadingRules && !loading && isInitialLoad) { - setIsInitialLoad(false); - } - }, [isInitialLoad, isLoadingRules, loading]); - - useEffect(() => { - if (!isGlobalLoading && (isLoadingRules || isLoading)) { - setIsGlobalLoad(true); - } else if (isGlobalLoading && !isLoadingRules && !isLoading) { - setIsGlobalLoad(false); - } - }, [setIsGlobalLoad, isGlobalLoading, isLoadingRules, isLoading]); - - useEffect(() => { - if (!isInitialLoad) { - dispatch({ type: 'refresh' }); - } - }, [importCompleteToggle]); - - useEffect(() => { - if (!isInitialLoad && reFetchRulesData != null && oldRefreshToggle !== refreshToggle) { - setOldRefreshToggle(refreshToggle); - reFetchRulesData(); - refetchPrePackagedRulesStatus(); - } - }, [ - isInitialLoad, - refreshToggle, - oldRefreshToggle, - reFetchRulesData, - refetchPrePackagedRulesStatus, - ]); + return getColumns({ + dispatch, + dispatchToaster, + history, + hasNoPermissions, + loadingRuleIds: + loadingRulesAction != null && + (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') + ? loadingRuleIds + : [], + reFetchRules: reFetchRulesData, + }); + }, [dispatch, dispatchToaster, history, loadingRuleIds, loadingRulesAction, reFetchRulesData]); useEffect(() => { if (reFetchRulesData != null) { @@ -194,31 +191,25 @@ export const AllRules = React.memo( }, [reFetchRulesData, setRefreshRulesData]); useEffect(() => { - dispatch({ - type: 'updateRules', - rules: rulesData.data, - pagination: { - page: rulesData.page, - perPage: rulesData.perPage, - total: rulesData.total, - }, - }); - }, [rulesData]); + if (initLoading && !loading && !isLoadingRules) { + setInitLoading(false); + } + }, [initLoading, loading, isLoadingRules]); const handleCreatePrePackagedRules = useCallback(async () => { - if (createPrePackagedRules != null) { + if (createPrePackagedRules != null && reFetchRulesData != null) { await createPrePackagedRules(); - dispatch({ type: 'refresh' }); + reFetchRulesData(true); } - }, [createPrePackagedRules]); + }, [createPrePackagedRules, reFetchRulesData]); const euiBasicTableSelectionProps = useMemo( () => ({ - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), + selectable: (item: Rule) => !loadingRuleIds.includes(item.id), + onSelectionChange: (selected: Rule[]) => + dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }), }), - [] + [loadingRuleIds] ); const onFilterChangedCallback = useCallback((newFilterOptions: Partial) => { @@ -237,12 +228,25 @@ export const AllRules = React.memo( ); }, []); + const isLoadingAnActionOnRule = useMemo(() => { + if ( + loadingRuleIds.length > 0 && + (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') + ) { + return false; + } else if (loadingRuleIds.length > 0) { + return true; + } + return false; + }, [loadingRuleIds, loadingRulesAction]); + return ( <> { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); dispatchToaster({ type: 'addToaster', toast: { @@ -256,22 +260,17 @@ export const AllRules = React.memo( /> - + <> - {((rulesCustomInstalled && rulesCustomInstalled > 0) || - (rulesInstalled != null && rulesInstalled > 0)) && ( - - - - )} - {isInitialLoad && ( - - )} - {isGlobalLoading && !isEmpty(tableData) && !isInitialLoad && ( + + + + + {(loading || isLoadingRules || isLoadingAnActionOnRule) && !initLoading && ( )} {rulesCustomInstalled != null && @@ -283,7 +282,10 @@ export const AllRules = React.memo( userHasNoPermissions={hasNoPermissions} /> )} - {showRulesTable({ isInitialLoad, rulesCustomInstalled, rulesInstalled }) && ( + {initLoading && ( + + )} + {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( <> @@ -292,7 +294,7 @@ export const AllRules = React.memo( - {i18n.SELECTED_RULES(selectedItems.length)} + {i18n.SELECTED_RULES(selectedRuleIds.length)} {!hasNoPermissions && ( ( )} dispatch({ type: 'refresh' })} + onClick={() => reFetchRulesData(true)} > {i18n.REFRESH} - - ( totalItemCount: pagination.total, pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], }} - sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + ref={tableRef} + sorting={{ sort: { field: 'enabled', direction: filterOptions.sortOrder } }} selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts index 3634a16cbf6ac..54da43efd66d9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts @@ -4,34 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBasicTable } from '@elastic/eui'; import { FilterOptions, PaginationOptions, Rule, } from '../../../../containers/detection_engine/rules'; -import { TableData } from '../types'; -import { formatRules } from './helpers'; +type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null; export interface State { - isLoading: boolean; - rules: Rule[]; - selectedItems: TableData[]; - pagination: PaginationOptions; + exportRuleIds: string[]; filterOptions: FilterOptions; - refreshToggle: boolean; - tableData: TableData[]; - exportPayload?: Rule[]; + loadingRuleIds: string[]; + loadingRulesAction: LoadingRuleAction; + pagination: PaginationOptions; + rules: Rule[]; + selectedRuleIds: string[]; } export type Action = - | { type: 'refresh' } - | { type: 'loading'; isLoading: boolean } - | { type: 'deleteRules'; rules: Rule[] } - | { type: 'duplicate'; rule: Rule } - | { type: 'setExportPayload'; exportPayload?: Rule[] } - | { type: 'setSelected'; selectedItems: TableData[] } - | { type: 'updateLoading'; ids: string[]; isLoading: boolean } - | { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions } + | { type: 'exportRuleIds'; ids: string[] } + | { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction } + | { type: 'selectedRuleIds'; ids: string[] } + | { type: 'setRules'; rules: Rule[] } + | { type: 'updateRules'; rules: Rule[] } | { type: 'updatePagination'; pagination: Partial } | { type: 'updateFilterOptions'; @@ -40,54 +36,71 @@ export type Action = } | { type: 'failure' }; -export const allRulesReducer = (state: State, action: Action): State => { +export const allRulesReducer = ( + tableRef: React.MutableRefObject | undefined> +) => (state: State, action: Action): State => { switch (action.type) { - case 'refresh': { + case 'exportRuleIds': { return { ...state, - refreshToggle: !state.refreshToggle, + loadingRuleIds: action.ids, + loadingRulesAction: 'export', + exportRuleIds: action.ids, }; } - case 'updateRules': { - // If pagination included, this was a hard refresh - if (action.pagination) { - return { - ...state, - rules: action.rules, - pagination: action.pagination, - tableData: formatRules(action.rules), - }; + case 'loadingRuleIds': { + return { + ...state, + loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids], + loadingRulesAction: action.actionType, + }; + } + case 'selectedRuleIds': { + return { + ...state, + selectedRuleIds: action.ids, + }; + } + case 'setRules': { + if ( + tableRef != null && + tableRef.current != null && + tableRef.current.changeSelection != null + ) { + tableRef.current.changeSelection([]); } - const ruleIds = state.rules.map(r => r.rule_id); - const updatedRules = action.rules.reverse().reduce((rules, updatedRule) => { - let newRules = rules; - if (ruleIds.includes(updatedRule.rule_id)) { - newRules = newRules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)); - } else { - newRules = [...newRules, updatedRule]; - } - return newRules; - }, state.rules); - - // Update enabled on selectedItems so that batch actions show correct available actions - const updatedRuleIdToState = action.rules.reduce>( - (acc, r) => ({ ...acc, [r.id]: r.enabled }), - {} - ); - const updatedSelectedItems = state.selectedItems.map(selectedItem => - Object.keys(updatedRuleIdToState).includes(selectedItem.id) - ? { ...selectedItem, activate: updatedRuleIdToState[selectedItem.id] } - : selectedItem - ); - return { ...state, - rules: updatedRules, - tableData: formatRules(updatedRules), - selectedItems: updatedSelectedItems, + rules: action.rules, + selectedRuleIds: [], + loadingRuleIds: [], + loadingRulesAction: null, }; } + case 'updateRules': { + if (state.rules != null) { + const ruleIds = state.rules.map(r => r.id); + const updatedRules = action.rules.reduce((rules, updatedRule) => { + let newRules = rules; + if (ruleIds.includes(updatedRule.id)) { + newRules = newRules.map(r => (updatedRule.id === r.id ? updatedRule : r)); + } else { + newRules = [...newRules, updatedRule]; + } + return newRules; + }, state.rules); + const updatedRuleIds = action.rules.map(r => r.id); + const newLoadingRuleIds = state.loadingRuleIds.filter(id => !updatedRuleIds.includes(id)); + return { + ...state, + rules: updatedRules, + loadingRuleIds: newLoadingRuleIds, + loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction, + }; + } + return state; + } case 'updatePagination': { return { ...state, @@ -110,51 +123,12 @@ export const allRulesReducer = (state: State, action: Action): State => { }, }; } - case 'deleteRules': { - const deletedRuleIds = action.rules.map(r => r.rule_id); - const updatedRules = state.rules.reduce( - (rules, rule) => (deletedRuleIds.includes(rule.rule_id) ? rules : [...rules, rule]), - [] - ); - return { - ...state, - rules: updatedRules, - tableData: formatRules(updatedRules), - refreshToggle: !state.refreshToggle, - }; - } - case 'setSelected': { - return { - ...state, - selectedItems: action.selectedItems, - }; - } - case 'updateLoading': { - return { - ...state, - rules: state.rules, - tableData: formatRules(state.rules, action.ids), - }; - } - case 'loading': { - return { - ...state, - isLoading: action.isLoading, - }; - } case 'failure': { return { ...state, - isLoading: false, rules: [], }; } - case 'setExportPayload': { - return { - ...state, - exportPayload: [...(action.exportPayload ?? [])], - }; - } default: return state; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index fa4f6a874ca5e..ddb8894c206b5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -41,7 +41,11 @@ const RulesTableFiltersComponent = ({ const [selectedTags, setSelectedTags] = useState([]); const [showCustomRules, setShowCustomRules] = useState(false); const [showElasticRules, setShowElasticRules] = useState(false); - const [isLoadingTags, tags] = useTags(); + const [isLoadingTags, tags, reFetchTags] = useTags(); + + useEffect(() => { + reFetchTags(); + }, [rulesCustomInstalled, rulesInstalled]); // Propagate filter changes to parent useEffect(() => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap index 9bd2fab23ac99..9355d0ae2cccb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -58,6 +58,7 @@ exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` `; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx index b52c10881cf42..7c8926c2064c7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx @@ -48,7 +48,7 @@ const RuleActionsOverflowComponent = ({ userHasNoPermissions, }: RuleActionsOverflowComponentProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [rulesToExport, setRulesToExport] = useState(undefined); + const [rulesToExport, setRulesToExport] = useState([]); const history = useHistory(); const [, dispatchToaster] = useStateToaster(); @@ -66,7 +66,7 @@ const RuleActionsOverflowComponent = ({ disabled={userHasNoPermissions} onClick={async () => { setIsPopoverOpen(false); - await duplicateRulesAction([rule], noop, dispatchToaster); + await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster); }} > {i18nActions.DUPLICATE_RULE} @@ -75,9 +75,9 @@ const RuleActionsOverflowComponent = ({ key={i18nActions.EXPORT_RULE} icon="indexEdit" disabled={userHasNoPermissions || rule.immutable} - onClick={async () => { + onClick={() => { setIsPopoverOpen(false); - setRulesToExport([rule]); + setRulesToExport([rule.id]); }} > {i18nActions.EXPORT_RULE} @@ -131,7 +131,7 @@ const RuleActionsOverflowComponent = ({ { displaySuccessToast( i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx index b41265adea6b1..5d3086051a6e2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useRef } from 'react'; import styled from 'styled-components'; import { isFunction } from 'lodash/fp'; -import { exportRules, Rule } from '../../../../../containers/detection_engine/rules'; +import { exportRules } from '../../../../../containers/detection_engine/rules'; import { displayErrorToast, useStateToaster } from '../../../../../components/toasters'; import * as i18n from './translations'; @@ -17,7 +17,7 @@ const InvisibleAnchor = styled.a` export interface RuleDownloaderProps { filename: string; - rules?: Rule[]; + ruleIds?: string[]; onExportComplete: (exportCount: number) => void; } @@ -30,7 +30,7 @@ export interface RuleDownloaderProps { */ export const RuleDownloaderComponent = ({ filename, - rules, + ruleIds, onExportComplete, }: RuleDownloaderProps) => { const anchorRef = useRef(null); @@ -41,10 +41,10 @@ export const RuleDownloaderComponent = ({ const abortCtrl = new AbortController(); async function exportData() { - if (anchorRef && anchorRef.current && rules != null) { + if (anchorRef && anchorRef.current && ruleIds != null && ruleIds.length > 0) { try { const exportResponse = await exportRules({ - ruleIds: rules.map(r => r.rule_id), + ruleIds, signal: abortCtrl.signal, }); @@ -61,7 +61,7 @@ export const RuleDownloaderComponent = ({ window.URL.revokeObjectURL(objectURL); } - onExportComplete(rules.length); + onExportComplete(ruleIds.length); } } catch (error) { if (isSubscribed) { @@ -77,7 +77,7 @@ export const RuleDownloaderComponent = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [rules]); + }, [ruleIds]); return ; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 0c53ad19a3574..20185c2eda816 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -26,11 +26,10 @@ import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/u import { getPrePackagedRuleStatus, redirectToDetections } from './helpers'; import * as i18n from './translations'; -type Func = () => void; +type Func = (refreshPrePackagedRule?: boolean) => void; const RulesPageComponent: React.FC = () => { const [showImportModal, setShowImportModal] = useState(false); - const [importCompleteToggle, setImportCompleteToggle] = useState(false); const refreshRulesData = useRef(null); const { loading, @@ -67,14 +66,18 @@ const RulesPageComponent: React.FC = () => { const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; + const handleRefreshRules = useCallback(async () => { + if (refreshRulesData.current != null) { + refreshRulesData.current(true); + } + }, [refreshRulesData]); + const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); - if (refreshRulesData.current != null) { - refreshRulesData.current(); - } + handleRefreshRules(); } - }, [createPrePackagedRules, refreshRulesData]); + }, [createPrePackagedRules, handleRefreshRules]); const handleRefetchPrePackagedRulesStatus = useCallback(() => { if (refetchPrePackagedRulesStatus != null) { @@ -96,7 +99,7 @@ const RulesPageComponent: React.FC = () => { setShowImportModal(false)} - importComplete={() => setImportCompleteToggle(!importCompleteToggle)} + importComplete={handleRefreshRules} /> { loading={loading || prePackagedRuleLoading} loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} hasNoPermissions={userHasNoPermissions} - importCompleteToggle={importCompleteToggle} refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus} rulesCustomInstalled={rulesCustomInstalled} rulesInstalled={rulesInstalled} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 55eb45fb5ed9d..b2650dcc2b77e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -5,7 +5,6 @@ */ import { Filter } from '../../../../../../../../src/plugins/data/common'; -import { Rule } from '../../../containers/detection_engine/rules'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from '../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; @@ -23,24 +22,6 @@ export interface EuiBasicTableOnChange { sort?: EuiBasicTableSortTypes; } -export interface TableData { - id: string; - immutable: boolean; - rule_id: string; - rule: { - href: string; - name: string; - }; - risk_score: number; - severity: string; - tags: string[]; - activate: boolean; - isLoading: boolean; - sourceRule: Rule; - status?: string | null; - statusDate?: string | null; -} - export enum RuleStep { defineRule = 'define-rule', aboutRule = 'about-rule', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 51b7b132fc794..08a0589389966 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import { countBy } from 'lodash/fp'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -45,8 +44,7 @@ export const createCreateRulesBulkRoute = ( } const ruleDefinitions = request.payload; - const mappedDuplicates = countBy('rule_id', ruleDefinitions); - const dupes = getDuplicates(mappedDuplicates); + const dupes = getDuplicates(ruleDefinitions, 'rule_id'); const rules = await Promise.all( ruleDefinitions diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index b5b687c284b25..5fac3f79f359c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -20,7 +20,7 @@ import { } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types'; +import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { sampleRule } from '../../signals/__mocks__/es_results'; import { getSimpleRule } from '../__mocks__/utils'; @@ -1222,20 +1222,32 @@ describe('utils', () => { describe('getDuplicates', () => { test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { - const output = getDuplicates({ - value1: 1, - value2: 2, - value3: 2, - }); + const output = getDuplicates( + [ + { rule_id: 'value1' }, + { rule_id: 'value2' }, + { rule_id: 'value2' }, + { rule_id: 'value3' }, + { rule_id: 'value3' }, + {}, + {}, + ] as RuleAlertParamsRest[], + 'rule_id' + ); const expected = ['value2', 'value3']; expect(output).toEqual(expected); }); test('returns null when given a map of no duplicates', () => { - const output = getDuplicates({ - value1: 1, - value2: 1, - value3: 1, - }); + const output = getDuplicates( + [ + { rule_id: 'value1' }, + { rule_id: 'value2' }, + { rule_id: 'value3' }, + {}, + {}, + ] as RuleAlertParamsRest[], + 'rule_id' + ); const expected: string[] = []; expect(output).toEqual(expected); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index ab80e1570c31c..7004bf2088ef2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pickBy } from 'lodash/fp'; -import { Dictionary } from 'lodash'; +import { pickBy, countBy } from 'lodash/fp'; import { SavedObject } from 'kibana/server'; import uuid from 'uuid'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; @@ -18,7 +17,7 @@ import { isRuleStatusFindTypes, isRuleStatusSavedObjectType, } from '../../rules/types'; -import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types'; +import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; import { createBulkErrorObject, BulkError, @@ -224,10 +223,14 @@ export const transformOrImportError = ( } }; -export const getDuplicates = (lodashDict: Dictionary): string[] => { - const hasDuplicates = Object.values(lodashDict).some(i => i > 1); +export const getDuplicates = (ruleDefinitions: RuleAlertParamsRest[], by: 'rule_id'): string[] => { + const mappedDuplicates = countBy( + by, + ruleDefinitions.filter(r => r[by] != null) + ); + const hasDuplicates = Object.values(mappedDuplicates).some(i => i > 1); if (hasDuplicates) { - return Object.keys(lodashDict).filter(key => lodashDict[key] > 1); + return Object.keys(mappedDuplicates).filter(key => mappedDuplicates[key] > 1); } return []; }; From 0e0f114d03f0becb9bbc5b93cbed217f2663efbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 26 Feb 2020 07:40:06 +0100 Subject: [PATCH 113/113] [APM] Improve debug output (#58467) --- .../apm/server/lib/helpers/es_client.ts | 96 +++++++++++-------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index 86eb1dba507f0..c22084dbb7168 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -12,8 +12,9 @@ import { IndicesCreateParams, DeleteDocumentResponse } from 'elasticsearch'; -import { cloneDeep, isString, merge, uniqueId } from 'lodash'; +import { cloneDeep, isString, merge } from 'lodash'; import { KibanaRequest } from 'src/core/server'; +import chalk from 'chalk'; import { ESSearchRequest, ESSearchResponse @@ -126,6 +127,10 @@ interface ClientCreateOptions { export type ESClient = ReturnType; +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + export function getESClient( context: APMRequestHandlerContext, request: KibanaRequest, @@ -136,25 +141,49 @@ export function getESClient( callAsInternalUser } = context.core.elasticsearch.dataClient; - const callMethod = clientAsInternalUser - ? callAsInternalUser - : callAsCurrentUser; + async function callEs(operationName: string, params: Record) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = clientAsInternalUser + ? await callAsInternalUser(operationName, params) + : await callAsCurrentUser(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (context.params.query._debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; - const debug = context.params.query._debug; + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); - function withTime( - fn: (log: typeof console.log) => Promise - ): Promise { - const log = console.log.bind(console, uniqueId()); - if (!debug) { - return fn(log); + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); } - const time = process.hrtime(); - return fn(log).then(data => { - const now = process.hrtime(time); - log(`took: ${Math.round(now[0] * 1000 + now[1] / 1e6)}ms`); - return data; - }); + + if (esError) { + throw esError; + } + + return res; } return { @@ -171,40 +200,25 @@ export function getESClient( apmOptions ); - return withTime(log => { - if (context.params.query._debug) { - log(`--DEBUG ES QUERY--`); - log( - `${request.url.pathname} ${JSON.stringify(context.params.query)}` - ); - log(`GET ${nextParams.index}/_search`); - log(JSON.stringify(nextParams.body, null, 2)); - } - - return (callMethod('search', nextParams) as unknown) as Promise< - ESSearchResponse - >; - }); + return callEs('search', nextParams); }, index: (params: APMIndexDocumentParams) => { - return withTime(() => callMethod('index', params)); + return callEs('index', params); }, delete: (params: IndicesDeleteParams): Promise => { - return withTime(() => callMethod('delete', params)); + return callEs('delete', params); }, indicesCreate: (params: IndicesCreateParams) => { - return withTime(() => callMethod('indices.create', params)); + return callEs('indices.create', params); }, hasPrivileges: ( params: IndexPrivilegesParams ): Promise => { - return withTime(() => - callMethod('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params - }) - ); + return callEs('transport.request', { + method: 'POST', + path: '/_security/user/_has_privileges', + body: params + }); } }; }