From fcdaed954eefc53601c53a6b6cc6c3fc843c7fa2 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 16 Dec 2019 08:31:07 -0500 Subject: [PATCH] Spaces - Client NP Migration, Phase 1 (#40856) * shimming NP for spaces client-side plugin * refresh active space in nav control when updated * fix advanced settings screen * allow npStart from unauthed routes * use NP for deriving space management url * remove security's usage of SpacesManager * remove usages of ui/capabilities * fix tests * implement NP plugin interface * remove hack in favor of convention in migration guide * shim feature catalogue registration * streamline nav control, and handle async loading more gracefully * adding opaqueId * fixes from merge * fix merge from master * fixing merge from master * move _active_space route to NP * moving to the NP feature catalogue registry * moving setup to setup phase * optimizing active space retrieval * reverting test isolation change * Apply suggestions from code review Co-Authored-By: Aleh Zasypkin * removing unnecessary PluginInitializerContext * updating advanced settings subtitle * using NP anonymousPaths service * additional nav_control_popover cleanup * additional cleanup * testing out onActiveSpaceChange$ property * make the linter happy * make the type checker happy * fixing types * fix merge from master * spaces LP init should run on all pages, not just the kibana app * address nits * fix infra/logs, and the spaces disabled scenario * fix typescript errors * revert changes to infra plugin * reintroducing activeSpace injected var for legacy plugins * fixing react deprecation warning and unhandled promise rejection * restore activeSpace default var * spaces does not need to check its own enabled status * fix from merge Co-authored-by: Elastic Machine --- .../infra/public/utils/use_kibana_space_id.ts | 1 + .../views/management/edit_role/index.js | 3 +- x-pack/legacy/plugins/spaces/index.ts | 15 ++-- .../public/__mocks__/ui_capabilities.ts | 24 ----- .../components/manage_spaces_button.test.tsx | 45 +++++----- .../components/manage_spaces_button.tsx | 9 +- ...e.ts => create_feature_catalogue_entry.ts} | 9 +- x-pack/legacy/plugins/spaces/public/index.ts | 10 +++ x-pack/legacy/plugins/spaces/public/legacy.ts | 18 ++++ .../plugins/spaces/public/lib/constants.ts | 5 +- .../copy_saved_objects_to_space_action.tsx | 4 +- .../spaces/public/lib/spaces_manager.mock.ts | 7 +- .../spaces/public/lib/spaces_manager.ts | 62 +++++++------ .../legacy/plugins/spaces/public/plugin.tsx | 40 +++++++++ .../confirm_delete_modal.test.tsx.snap | 22 ----- .../advanced_settings_subtitle.test.tsx.snap | 28 ------ .../advanced_settings_subtitle.test.tsx | 18 +++- .../advanced_settings_subtitle.tsx | 54 ++++++----- .../advanced_settings_title.test.tsx.snap | 45 ---------- .../advanced_settings_title.test.tsx | 15 +++- .../advanced_settings_title.tsx | 48 ++++++---- .../components/confirm_delete_modal.test.tsx | 15 +--- .../components/confirm_delete_modal.tsx | 23 +++-- .../copy_to_space_flyout.test.tsx | 13 +-- .../copy_to_space_flyout.tsx | 21 ++--- .../edit_space/delete_spaces_button.test.tsx | 7 -- .../edit_space/delete_spaces_button.tsx | 9 +- .../enabled_features.test.tsx | 8 +- .../enabled_features/enabled_features.tsx | 6 +- .../edit_space/manage_space_page.test.tsx | 51 ++++++----- .../edit_space/manage_space_page.tsx | 42 ++++----- .../spaces/public/views/management/index.tsx | 31 ++++--- .../public/views/management/page_routes.tsx | 42 ++++----- .../spaces_grid/spaces_grid_page.tsx | 17 ++-- .../spaces_grid/spaces_grid_pages.test.tsx | 21 +++-- .../nav_control_popover.test.tsx.snap | 41 +++++---- .../spaces_description.test.tsx.snap | 10 +++ .../components/spaces_description.test.tsx | 16 +++- .../components/spaces_description.tsx | 3 + .../components/spaces_header_nav_button.tsx | 25 ------ .../nav_control/components/spaces_menu.tsx | 30 +++++-- .../spaces/public/views/nav_control/index.ts | 4 +- .../public/views/nav_control/nav_control.tsx | 89 ++++++------------- .../nav_control/nav_control_popover.test.tsx | 45 +++++----- .../views/nav_control/nav_control_popover.tsx | 87 ++++++++++-------- .../space_selector.test.tsx.snap | 16 +--- .../public/views/space_selector/index.tsx | 17 ++-- .../space_selector/space_selector.test.tsx | 36 +------- .../views/space_selector/space_selector.tsx | 20 ++--- .../server/lib/toggle_ui_capabilities.test.ts | 30 +++---- .../server/lib/toggle_ui_capabilities.ts | 24 ++--- x-pack/plugins/spaces/server/plugin.ts | 8 ++ .../api/internal/get_active_space.test.ts | 59 ++++++++++++ .../routes/api/internal/get_active_space.ts | 28 ++++++ .../server/routes/api/internal/index.ts | 18 ++++ .../apis/spaces/get_active_space.ts | 64 +++++++++++++ .../test/api_integration/apis/spaces/index.ts | 1 + 57 files changed, 792 insertions(+), 667 deletions(-) delete mode 100644 x-pack/legacy/plugins/spaces/public/__mocks__/ui_capabilities.ts rename x-pack/legacy/plugins/spaces/public/{register_feature.ts => create_feature_catalogue_entry.ts} (81%) create mode 100644 x-pack/legacy/plugins/spaces/public/index.ts create mode 100644 x-pack/legacy/plugins/spaces/public/legacy.ts create mode 100644 x-pack/legacy/plugins/spaces/public/plugin.tsx delete mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx create mode 100644 x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/internal/index.ts create mode 100644 x-pack/test/api_integration/apis/spaces/get_active_space.ts diff --git a/x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts b/x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts index 4642f7fd26f21f..52c896feef1a22 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts @@ -11,6 +11,7 @@ import * as rt from 'io-ts'; import { useKibanaInjectedVar } from './use_kibana_injected_var'; export const useKibanaSpaceId = (): string => { + // NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform. const activeSpace = useKibanaInjectedVar('activeSpace'); return pipe( diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js index 8b54f50c4beebc..09c612526918fe 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js @@ -15,7 +15,6 @@ import 'plugins/security/services/shield_user'; import 'plugins/security/services/shield_role'; import 'plugins/security/services/shield_indices'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { SpacesManager } from '../../../../../spaces/public/lib'; import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls'; import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs'; @@ -79,7 +78,7 @@ const routeDefinition = action => ({ }, spaces(spacesEnabled) { if (spacesEnabled) { - return new SpacesManager().getSpaces(); + return kfetch({ method: 'get', pathname: '/api/spaces/space' }); } return []; }, diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index b20ddacc7e527c..0083847cfb441b 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -48,7 +48,6 @@ export const spaces = (kibana: Record) => }, uiExports: { - chromeNavControls: ['plugins/spaces/views/nav_control'], styleSheetPaths: resolve(__dirname, 'public/index.scss'), managementSections: ['plugins/spaces/views/management'], apps: [ @@ -60,7 +59,7 @@ export const spaces = (kibana: Record) => hidden: true, }, ], - hacks: [], + hacks: ['plugins/spaces/legacy'], mappings, migrations: { space: { @@ -73,19 +72,21 @@ export const spaces = (kibana: Record) => hidden: true, }, }, - home: ['plugins/spaces/register_feature'], - injectDefaultVars(server: any) { + home: [], + injectDefaultVars(server: Server) { return { - spaces: [], - activeSpace: null, serverBasePath: server.config().get('server.basePath'), + activeSpace: null, }; }, async replaceInjectedVars( vars: Record, request: Legacy.Request, - server: Record + server: Server ) { + // NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform. + // Known usages: + // - x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup; if (!spacesPlugin) { throw new Error('New Platform XPack Spaces plugin is not available.'); diff --git a/x-pack/legacy/plugins/spaces/public/__mocks__/ui_capabilities.ts b/x-pack/legacy/plugins/spaces/public/__mocks__/ui_capabilities.ts deleted file mode 100644 index 532f83b27a3698..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/__mocks__/ui_capabilities.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('ui/capabilities', () => ({ - capabilities: { - get: jest.fn().mockReturnValue({ - navLinks: {}, - management: {}, - catalogue: {}, - spaces: { - manage: true, - }, - }), - }, -})); - -import { capabilities, UICapabilities } from 'ui/capabilities'; - -export function setMockCapabilities(mockCapabilities: UICapabilities) { - ((capabilities.get as unknown) as jest.Mock).mockReturnValue(mockCapabilities); -} diff --git a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx index 00e306fe3d691a..2dc6ae919c0187 100644 --- a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.test.tsx @@ -4,37 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import { setMockCapabilities } from '../__mocks__/ui_capabilities'; import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ManageSpacesButton } from './manage_spaces_button'; describe('ManageSpacesButton', () => { it('renders as expected', () => { - setMockCapabilities({ - navLinks: {}, - management: {}, - catalogue: {}, - spaces: { - manage: true, - }, - }); - - const component = ; + const component = ( + + ); expect(shallowWithIntl(component)).toMatchSnapshot(); }); it(`doesn't render if user profile forbids managing spaces`, () => { - setMockCapabilities({ - navLinks: {}, - management: {}, - catalogue: {}, - spaces: { - manage: false, - }, - }); - - const component = ; + const component = ( + + ); expect(shallowWithIntl(component)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx b/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx index ef7dba02f8fad1..91a0803c20bc9e 100644 --- a/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/components/manage_spaces_button.tsx @@ -7,8 +7,8 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, CSSProperties } from 'react'; -import { capabilities } from 'ui/capabilities'; -import { MANAGE_SPACES_URL } from '../lib/constants'; +import { Capabilities } from 'src/core/public'; +import { getManageSpacesUrl } from '../lib/constants'; interface Props { isDisabled?: boolean; @@ -16,11 +16,12 @@ interface Props { size?: 's' | 'm'; style?: CSSProperties; onClick?: () => void; + capabilities: Capabilities; } export class ManageSpacesButton extends Component { public render() { - if (!capabilities.get().spaces.manage) { + if (!this.props.capabilities.spaces.manage) { return null; } @@ -44,6 +45,6 @@ export class ManageSpacesButton extends Component { if (this.props.onClick) { this.props.onClick(); } - window.location.replace(MANAGE_SPACES_URL); + window.location.replace(getManageSpacesUrl()); }; } diff --git a/x-pack/legacy/plugins/spaces/public/register_feature.ts b/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts similarity index 81% rename from x-pack/legacy/plugins/spaces/public/register_feature.ts rename to x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts index c8ebfd8db5686f..1f41bb89d77076 100644 --- a/x-pack/legacy/plugins/spaces/public/register_feature.ts +++ b/x-pack/legacy/plugins/spaces/public/create_feature_catalogue_entry.ts @@ -6,13 +6,12 @@ import { i18n } from '@kbn/i18n'; import { + FeatureCatalogueEntry, FeatureCatalogueCategory, - FeatureCatalogueRegistryProvider, - // @ts-ignore -} from 'ui/registry/feature_catalogue'; +} from '../../../../../src/plugins/home/public'; import { getSpacesFeatureDescription } from './lib/constants'; -FeatureCatalogueRegistryProvider.register(() => { +export const createSpacesFeatureCatalogueEntry = (): FeatureCatalogueEntry => { return { id: 'spaces', title: i18n.translate('xpack.spaces.spacesTitle', { @@ -24,4 +23,4 @@ FeatureCatalogueRegistryProvider.register(() => { showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, }; -}); +}; diff --git a/x-pack/legacy/plugins/spaces/public/index.ts b/x-pack/legacy/plugins/spaces/public/index.ts new file mode 100644 index 00000000000000..9233aae9fb12fb --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SpacesPlugin } from './plugin'; + +export const plugin = () => { + return new SpacesPlugin(); +}; diff --git a/x-pack/legacy/plugins/spaces/public/legacy.ts b/x-pack/legacy/plugins/spaces/public/legacy.ts new file mode 100644 index 00000000000000..99419206093e93 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/legacy.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npSetup, npStart } from 'ui/new_platform'; +import { plugin } from '.'; +import { SpacesPlugin, PluginsSetup } from './plugin'; + +const spacesPlugin: SpacesPlugin = plugin(); + +const plugins: PluginsSetup = { + home: npSetup.plugins.home, +}; + +export const setup = spacesPlugin.setup(npSetup.core, plugins); +export const start = spacesPlugin.start(npStart.core); diff --git a/x-pack/legacy/plugins/spaces/public/lib/constants.ts b/x-pack/legacy/plugins/spaces/public/lib/constants.ts index 7752ce1b6bc78f..94799f6f2b5d8e 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/constants.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/constants.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; +import { npSetup } from 'ui/new_platform'; let spacesFeatureDescription: string; @@ -20,4 +20,5 @@ export const getSpacesFeatureDescription = () => { return spacesFeatureDescription; }; -export const MANAGE_SPACES_URL = chrome.addBasePath(`/app/kibana#/management/spaces/list`); +export const getManageSpacesUrl = () => + npSetup.core.http.basePath.prepend(`/app/kibana#/management/spaces/list`); diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx index e0db5f360f0f6d..3b0fffa38e7856 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx @@ -11,7 +11,6 @@ import { SavedObjectsManagementRecord, } from '../../../../../../../src/legacy/core_plugins/management/public'; import { CopySavedObjectsToSpaceFlyout } from '../../views/management/components/copy_saved_objects_to_space'; -import { Space } from '../../../common/model/space'; import { SpacesManager } from '../spaces_manager'; export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction { @@ -31,7 +30,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem }, }; - constructor(private readonly spacesManager: SpacesManager, private readonly activeSpace: Space) { + constructor(private readonly spacesManager: SpacesManager) { super(); } @@ -44,7 +43,6 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem onClose={this.onClose} savedObject={this.record} spacesManager={this.spacesManager} - activeSpace={this.activeSpace} toastNotifications={toastNotifications} /> ); diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts index 4d7a9251228e8d..69c6f7a452fdd0 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.mock.ts @@ -4,18 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { of, Observable } from 'rxjs'; +import { Space } from '../../common/model/space'; + function createSpacesManagerMock() { return { + onActiveSpaceChange$: (of(undefined) as unknown) as Observable, getSpaces: jest.fn().mockResolvedValue([]), getSpace: jest.fn().mockResolvedValue(undefined), + getActiveSpace: jest.fn().mockResolvedValue(undefined), createSpace: jest.fn().mockResolvedValue(undefined), updateSpace: jest.fn().mockResolvedValue(undefined), deleteSpace: jest.fn().mockResolvedValue(undefined), copySavedObjects: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), - requestRefresh: jest.fn(), - on: jest.fn(), }; } diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts index 67d34960ed98e3..4fff1ddba08b2b 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EventEmitter } from 'events'; -import { kfetch } from 'ui/kfetch'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { skipWhile } from 'rxjs/operators'; +import { HttpSetup } from 'src/core/public'; import { SavedObjectsManagementRecord } from '../../../../../../src/legacy/core_plugins/management/public'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; @@ -12,43 +13,57 @@ import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/t import { ENTER_SPACE_PATH } from '../../common/constants'; import { addSpaceIdToPath } from '../../../../../plugins/spaces/common'; -export class SpacesManager extends EventEmitter { - constructor(private readonly serverBasePath: string) { - super(); +export class SpacesManager { + private activeSpace$: BehaviorSubject = new BehaviorSubject(null); + + public readonly onActiveSpaceChange$: Observable; + + constructor(private readonly serverBasePath: string, private readonly http: HttpSetup) { + this.onActiveSpaceChange$ = this.activeSpace$ + .asObservable() + .pipe(skipWhile((v: Space | null) => v == null)) as Observable; + + this.refreshActiveSpace(); } public async getSpaces(purpose?: GetSpacePurpose): Promise { - return await kfetch({ pathname: '/api/spaces/space', query: { purpose } }); + return await this.http.get('/api/spaces/space', { query: { purpose } }); } public async getSpace(id: string): Promise { - return await kfetch({ pathname: `/api/spaces/space/${encodeURIComponent(id)}` }); + return await this.http.get(`/api/spaces/space/${encodeURIComponent(id)}`); + } + + public getActiveSpace({ forceRefresh = false } = {}) { + if (!forceRefresh && this.activeSpace$.value) { + return Promise.resolve(this.activeSpace$.value); + } + return this.http.get('/internal/spaces/_active_space') as Promise; } public async createSpace(space: Space) { - return await kfetch({ - pathname: `/api/spaces/space`, - method: 'POST', + await this.http.post(`/api/spaces/space`, { body: JSON.stringify(space), }); } public async updateSpace(space: Space) { - return await kfetch({ - pathname: `/api/spaces/space/${encodeURIComponent(space.id)}`, + await this.http.put(`/api/spaces/space/${encodeURIComponent(space.id)}`, { query: { overwrite: true, }, - method: 'PUT', body: JSON.stringify(space), }); + + const activeSpaceId = (await this.getActiveSpace()).id; + + if (space.id === activeSpaceId) { + this.refreshActiveSpace(); + } } public async deleteSpace(space: Space) { - return await kfetch({ - pathname: `/api/spaces/space/${encodeURIComponent(space.id)}`, - method: 'DELETE', - }); + await this.http.delete(`/api/spaces/space/${encodeURIComponent(space.id)}`); } public async copySavedObjects( @@ -57,9 +72,7 @@ export class SpacesManager extends EventEmitter { includeReferences: boolean, overwrite: boolean ): Promise { - return await kfetch({ - pathname: `/api/spaces/_copy_saved_objects`, - method: 'POST', + return this.http.post('/api/spaces/_copy_saved_objects', { body: JSON.stringify({ objects, spaces, @@ -74,9 +87,7 @@ export class SpacesManager extends EventEmitter { retries: unknown, includeReferences: boolean ): Promise { - return await kfetch({ - pathname: `/api/spaces/_resolve_copy_saved_objects_errors`, - method: 'POST', + return this.http.post(`/api/spaces/_resolve_copy_saved_objects_errors`, { body: JSON.stringify({ objects, includeReferences, @@ -93,7 +104,8 @@ export class SpacesManager extends EventEmitter { window.location.href = `${this.serverBasePath}/spaces/space_selector`; } - public async requestRefresh() { - this.emit('request_refresh'); + private async refreshActiveSpace() { + const activeSpace = await this.getActiveSpace({ forceRefresh: true }); + this.activeSpace$.next(activeSpace); } } diff --git a/x-pack/legacy/plugins/spaces/public/plugin.tsx b/x-pack/legacy/plugins/spaces/public/plugin.tsx new file mode 100644 index 00000000000000..4e070c3cee3df5 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/public/plugin.tsx @@ -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 { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { HomePublicPluginSetup } from 'src/plugins/home/public'; +import { SpacesManager } from './lib'; +import { initSpacesNavControl } from './views/nav_control'; +import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; + +export interface SpacesPluginStart { + spacesManager: SpacesManager | null; +} + +export interface PluginsSetup { + home?: HomePublicPluginSetup; +} + +export class SpacesPlugin implements Plugin { + private spacesManager: SpacesManager | null = null; + + public async start(core: CoreStart) { + const serverBasePath = core.injectedMetadata.getInjectedVar('serverBasePath') as string; + + this.spacesManager = new SpacesManager(serverBasePath, core.http); + initSpacesNavControl(this.spacesManager, core); + + return { + spacesManager: this.spacesManager, + }; + } + + public async setup(core: CoreSetup, plugins: PluginsSetup) { + if (plugins.home) { + plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); + } + } +} diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap index 82f94f2346ae7a..db9913ea7f0724 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -65,28 +65,6 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` value="" /> - - - - - ( - - My Space - - ) - , - } - } - /> - - diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap deleted file mode 100644 index 6c29a309c36b0b..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/__snapshots__/advanced_settings_subtitle.test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AdvancedSettingsSubtitle renders as expected 1`] = ` - - - - - My Space - , - } - } - /> -

- } - /> -
-`; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx index 43804b9ba44fcb..49f5233db44e2b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx @@ -4,16 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; +import { EuiCallOut } from '@elastic/eui'; describe('AdvancedSettingsSubtitle', () => { - it('renders as expected', () => { + it('renders as expected', async () => { const space = { id: 'my-space', name: 'My Space', disabledFeatures: [], }; - expect(shallowWithIntl()).toMatchSnapshot(); + + const wrapper = mountWithIntl( + Promise.resolve(space)} /> + ); + + // Wait for active space to resolve before requesting the component to update + await Promise.resolve(); + await Promise.resolve(); + + wrapper.update(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); }); }); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx index 901bce019012f3..433f8a8ccf0a20 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx @@ -6,30 +6,40 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Fragment } from 'react'; +import React, { Fragment, useState, useEffect } from 'react'; import { Space } from '../../../../../common/model/space'; interface Props { - space: Space; + getActiveSpace: () => Promise; } -export const AdvancedSettingsSubtitle = (props: Props) => ( - - - - {props.space.name}, - }} - /> -

- } - /> -
-); +export const AdvancedSettingsSubtitle = (props: Props) => { + const [activeSpace, setActiveSpace] = useState(null); + + useEffect(() => { + props.getActiveSpace().then(space => setActiveSpace(space)); + }, [props]); + + if (!activeSpace) return null; + + return ( + + + + {activeSpace.name}, + }} + /> +

+ } + /> +
+ ); +}; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap deleted file mode 100644 index eba4a06a8e8aad..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/__snapshots__/advanced_settings_title.test.tsx.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AdvancedSettingsTitle renders as expected 1`] = ` - - - - - - -

- -

-
-
-
-`; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx index 3a91bcb6019ee8..7f2b6eee62c45e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.test.tsx @@ -4,16 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { AdvancedSettingsTitle } from './advanced_settings_title'; +import { SpaceAvatar } from '../../../../components'; describe('AdvancedSettingsTitle', () => { - it('renders as expected', () => { + it('renders without crashing', async () => { const space = { id: 'my-space', name: 'My Space', disabledFeatures: [], }; - expect(shallowWithIntl()).toMatchSnapshot(); + + const wrapper = mountWithIntl( + Promise.resolve(space)} /> + ); + + await Promise.resolve(); + await Promise.resolve(); + wrapper.update(); + expect(wrapper.find(SpaceAvatar)).toHaveLength(1); }); }); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx index 9ba38a12f436ac..af6fa42cce07be 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/advanced_settings_title/advanced_settings_title.tsx @@ -6,28 +6,38 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Space } from '../../../../../common/model/space'; import { SpaceAvatar } from '../../../../components'; interface Props { - space: Space; + getActiveSpace: () => Promise; } -export const AdvancedSettingsTitle = (props: Props) => ( - - - - - - -

- -

-
-
-
-); +export const AdvancedSettingsTitle = (props: Props) => { + const [activeSpace, setActiveSpace] = useState(null); + + useEffect(() => { + props.getActiveSpace().then(space => setActiveSpace(space)); + }, [props]); + + if (!activeSpace) return null; + + return ( + + + + + + +

+ +

+
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx index 3c3fa502a917d3..f0ab2c99ac2e20 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/confirm_delete_modal.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { SpacesNavState } from '../../nav_control'; import { ConfirmDeleteModal } from './confirm_delete_modal'; import { spacesManagerMock } from '../../../lib/mocks'; import { SpacesManager } from '../../../lib'; @@ -20,11 +19,7 @@ describe('ConfirmDeleteModal', () => { }; const spacesManager = spacesManagerMock.create(); - - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; + spacesManager.getActiveSpace.mockResolvedValue(space); const onCancel = jest.fn(); const onConfirm = jest.fn(); @@ -34,7 +29,6 @@ describe('ConfirmDeleteModal', () => { { }; const spacesManager = spacesManagerMock.create(); - - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; + spacesManager.getActiveSpace.mockResolvedValue(space); const onCancel = jest.fn(); const onConfirm = jest.fn(); @@ -64,7 +54,6 @@ describe('ConfirmDeleteModal', () => { void; onConfirm: () => void; intl: InjectedIntl; @@ -42,6 +40,7 @@ interface State { confirmSpaceName: string; error: boolean | null; deleteInProgress: boolean; + isDeletingCurrentSpace: boolean; } class ConfirmDeleteModalUI extends Component { @@ -49,13 +48,23 @@ class ConfirmDeleteModalUI extends Component { confirmSpaceName: '', error: null, deleteInProgress: false, + isDeletingCurrentSpace: false, }; + public componentDidMount() { + isCurrentSpace(this.props.space, this.props.spacesManager).then(result => { + this.setState({ + isDeletingCurrentSpace: result, + }); + }); + } + public render() { - const { space, spacesNavState, onCancel, intl } = this.props; + const { space, onCancel, intl } = this.props; + const { isDeletingCurrentSpace } = this.state; let warning = null; - if (isDeletingCurrentSpace(space, spacesNavState)) { + if (isDeletingCurrentSpace) { const name = ( ({space.name}) @@ -186,7 +195,7 @@ class ConfirmDeleteModalUI extends Component { private onConfirm = async () => { if (this.state.confirmSpaceName === this.props.space.name) { - const needsRedirect = isDeletingCurrentSpace(this.props.space, this.props.spacesNavState); + const needsRedirect = this.state.isDeletingCurrentSpace; const spacesManager = this.props.spacesManager; this.setState({ @@ -210,8 +219,8 @@ class ConfirmDeleteModalUI extends Component { }; } -function isDeletingCurrentSpace(space: Space, spacesNavState: SpacesNavState) { - return space.id === spacesNavState.getActiveSpace().id; +async function isCurrentSpace(space: Space, spacesManager: SpacesManager) { + return space.id === (await spacesManager.getActiveSpace()).id; } export const ConfirmDeleteModal = injectI18n(ConfirmDeleteModalUI); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx index c30792b23e3ac8..590c0edc0073ba 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx @@ -33,6 +33,13 @@ const setup = async (opts: SetupOpts = {}) => { const onClose = jest.fn(); const mockSpacesManager = spacesManagerMock.create(); + + mockSpacesManager.getActiveSpace.mockResolvedValue({ + id: 'my-active-space', + name: 'my active space', + disabledFeatures: [], + }); + mockSpacesManager.getSpaces.mockResolvedValue( opts.mockSpaces || [ { @@ -79,11 +86,6 @@ const setup = async (opts: SetupOpts = {}) => { @@ -92,6 +94,7 @@ const setup = async (opts: SetupOpts = {}) => { if (!opts.returnBeforeSpacesLoad) { // Wait for spaces manager to complete and flyout to rerender await Promise.resolve(); + await Promise.resolve(); wrapper.update(); } diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx index 1de5a10977f838..5a43e5878ab83f 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.tsx @@ -38,7 +38,6 @@ interface Props { onClose: () => void; savedObject: SavedObjectsManagementRecord; spacesManager: SpacesManager; - activeSpace: Space; toastNotifications: ToastNotifications; } @@ -57,12 +56,13 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { } ); useEffect(() => { - spacesManager - .getSpaces('copySavedObjectsIntoSpace') - .then(response => { + const getSpaces = spacesManager.getSpaces('copySavedObjectsIntoSpace'); + const getActiveSpace = spacesManager.getActiveSpace(); + Promise.all([getSpaces, getActiveSpace]) + .then(([allSpaces, activeSpace]) => { setSpacesState({ isLoading: false, - spaces: response, + spaces: allSpaces.filter(space => space.id !== activeSpace.id), }); }) .catch(e => { @@ -73,7 +73,6 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { }); }); }, [spacesManager, toastNotifications]); - const eligibleSpaces = spaces.filter(space => space.id !== props.activeSpace.id); const [copyInProgress, setCopyInProgress] = useState(false); const [conflictResolutionInProgress, setConflictResolutionInProgress] = useState(false); @@ -159,7 +158,7 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { } // Step 1a: assets loaded, but no spaces are available for copy. - if (eligibleSpaces.length === 0) { + if (spaces.length === 0) { return ( { // Step 2: Copy has not been initiated yet; User must fill out form to continue. if (!copyInProgress) { return ( - + ); } @@ -200,7 +195,7 @@ export const CopySavedObjectsToSpaceFlyout = (props: Props) => { copyInProgress={copyInProgress} conflictResolutionInProgress={conflictResolutionInProgress} copyResult={copyResult} - spaces={eligibleSpaces} + spaces={spaces} copyOptions={copyOptions} retries={retries} onRetriesChange={onRetriesChange} diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx index 24296bf0fa7633..e7c7dfc5eb1b05 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { SpacesNavState } from '../../nav_control'; import { DeleteSpacesButton } from './delete_spaces_button'; import { spacesManagerMock } from '../../../lib/mocks'; import { SpacesManager } from '../../../lib'; @@ -21,16 +20,10 @@ describe('DeleteSpacesButton', () => { it('renders as expected', () => { const spacesManager = spacesManagerMock.create(); - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; - const wrapper = shallowWithIntl( diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx index 7f3dd0aea485ec..216dd7c41f124b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/delete_spaces_button.tsx @@ -6,7 +6,6 @@ import { EuiButton, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { SpacesNavState } from 'plugins/spaces/views/nav_control'; import React, { Component, Fragment } from 'react'; // @ts-ignore import { toastNotifications } from 'ui/notify'; @@ -18,7 +17,6 @@ interface Props { style?: 'button' | 'icon'; space: Space; spacesManager: SpacesManager; - spacesNavState: SpacesNavState; onDelete: () => void; intl: InjectedIntl; } @@ -81,12 +79,11 @@ class DeleteSpacesButtonUI extends Component { return null; } - const { spacesNavState, spacesManager } = this.props; + const { spacesManager } = this.props; return ( { this.setState({ @@ -99,7 +96,7 @@ class DeleteSpacesButtonUI extends Component { }; public deleteSpaces = async () => { - const { spacesManager, space, spacesNavState, intl } = this.props; + const { spacesManager, space, intl } = this.props; try { await spacesManager.deleteSpace(space); @@ -139,8 +136,6 @@ class DeleteSpacesButtonUI extends Component { if (this.props.onDelete) { this.props.onDelete(); } - - spacesNavState.refreshSpacesList(); }; } diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx index a69a8f47263e6d..8f82e6d413350f 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx @@ -33,7 +33,7 @@ const space: Space = { disabledFeatures: ['feature-1', 'feature-2'], }; -const uiCapabilities = { +const capabilities = { navLinks: {}, management: {}, catalogue: {}, @@ -49,7 +49,7 @@ describe('EnabledFeatures', () => { @@ -64,7 +64,7 @@ describe('EnabledFeatures', () => { @@ -99,7 +99,7 @@ describe('EnabledFeatures', () => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx index b1f3e8c43de9c9..628be759b7c5ce 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment, ReactNode } from 'react'; -import { UICapabilities } from 'ui/capabilities'; +import { Capabilities } from 'src/core/public'; import { Feature } from '../../../../../../../../plugins/features/public'; import { Space } from '../../../../../common/model/space'; import { getEnabledFeatures } from '../../lib/feature_utils'; @@ -17,7 +17,7 @@ import { FeatureTable } from './feature_table'; interface Props { space: Partial; features: Feature[]; - uiCapabilities: UICapabilities; + capabilities: Capabilities; intl: InjectedIntl; onChange: (space: Partial) => void; } @@ -130,7 +130,7 @@ export class EnabledFeatures extends Component { defaultMessage="The feature is hidden in the UI, but is not disabled." />

- {this.props.uiCapabilities.spaces.manage && ( + {this.props.capabilities.spaces.manage && (

({ kfetch: () => Promise.resolve([{ id: 'feature-1', name: 'feature 1' }]), })); -import '../../../__mocks__/ui_capabilities'; import '../../../__mocks__/xpack_info'; import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { SpacesNavState } from '../../nav_control'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; import { ManageSpacePage } from './manage_space_page'; import { SectionPanel } from './section_panel'; @@ -29,17 +27,18 @@ describe('ManageSpacePage', () => { it('allows a space to be created', async () => { const spacesManager = spacesManagerMock.create(); spacesManager.createSpace = jest.fn(spacesManager.createSpace); - - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; + spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( ); @@ -75,17 +74,19 @@ describe('ManageSpacePage', () => { initials: 'AB', disabledFeatures: [], }); + spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; const wrapper = mountWithIntl( ); @@ -121,17 +122,19 @@ describe('ManageSpacePage', () => { initials: 'AB', disabledFeatures: [], }); + spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; const wrapper = mountWithIntl( ); @@ -176,17 +179,19 @@ describe('ManageSpacePage', () => { initials: 'AB', disabledFeatures: [], }); + spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); - const spacesNavState: SpacesNavState = { - getActiveSpace: () => space, - refreshSpacesList: jest.fn(), - }; const wrapper = mountWithIntl( ); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx index dfd60f7c193c1c..a5d60d1a731ba2 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx @@ -16,12 +16,11 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; -import { SpacesNavState } from 'plugins/spaces/views/nav_control'; import React, { Component, Fragment } from 'react'; -import { capabilities } from 'ui/capabilities'; import { Breadcrumb } from 'ui/chrome'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; +import { Capabilities } from 'src/core/public'; import { Feature } from '../../../../../../../plugins/features/public'; import { isReservedSpace } from '../../../../common'; import { Space } from '../../../../common/model/space'; @@ -39,9 +38,9 @@ import { ReservedSpaceBadge } from './reserved_space_badge'; interface Props { spacesManager: SpacesManager; spaceId?: string; - spacesNavState: SpacesNavState; intl: InjectedIntl; setBreadcrumbs?: (breadcrumbs: Breadcrumb[]) => void; + capabilities: Capabilities; } interface State { @@ -73,7 +72,7 @@ class ManageSpacePageUI extends Component { } public async componentDidMount() { - if (!capabilities.get().spaces.manage) { + if (!this.props.capabilities.spaces.manage) { return; } @@ -139,7 +138,7 @@ class ManageSpacePageUI extends Component { ); public getForm = () => { - if (!capabilities.get().spaces.manage) { + if (!this.props.capabilities.spaces.manage) { return ; } @@ -173,7 +172,7 @@ class ManageSpacePageUI extends Component { @@ -269,7 +268,6 @@ class ManageSpacePageUI extends Component { data-test-subj="delete-space-button" space={this.state.space as Space} spacesManager={this.props.spacesManager} - spacesNavState={this.props.spacesNavState} onDelete={this.backToSpacesList} /> @@ -298,27 +296,30 @@ class ManageSpacePageUI extends Component { } if (this.editingExistingSpace()) { - const { spacesNavState } = this.props; + const { spacesManager } = this.props; const originalSpace: Space = this.state.originalSpace as Space; const space: Space = this.state.space as Space; - const editingActiveSpace = spacesNavState.getActiveSpace().id === originalSpace.id; + spacesManager.getActiveSpace().then(activeSpace => { + const editingActiveSpace = activeSpace.id === originalSpace.id; - const haveDisabledFeaturesChanged = - space.disabledFeatures.length !== originalSpace.disabledFeatures.length || - _.difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0; + const haveDisabledFeaturesChanged = + space.disabledFeatures.length !== originalSpace.disabledFeatures.length || + _.difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0; - if (editingActiveSpace && haveDisabledFeaturesChanged) { - this.setState({ - showAlteringActiveSpaceDialog: true, - }); + if (editingActiveSpace && haveDisabledFeaturesChanged) { + this.setState({ + showAlteringActiveSpaceDialog: true, + }); - return; - } + return; + } + this.performSave(); + }); + } else { + this.performSave(); } - - this.performSave(); }; private performSave = (requireRefresh = false) => { @@ -358,7 +359,6 @@ class ManageSpacePageUI extends Component { action .then(() => { - this.props.spacesNavState.refreshSpacesList(); toastNotifications.addSuccess( intl.formatMessage( { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx index f659154c910f19..bf33273c614d65 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx @@ -15,16 +15,16 @@ import { // @ts-ignore import routes from 'ui/routes'; import { setup as managementSetup } from '../../../../../../../src/legacy/core_plugins/management/public/legacy'; -import { SpacesManager } from '../../lib'; import { AdvancedSettingsSubtitle } from './components/advanced_settings_subtitle'; import { AdvancedSettingsTitle } from './components/advanced_settings_title'; +import { start as spacesNPStart } from '../../legacy'; import { CopyToSpaceSavedObjectsManagementAction } from '../../lib/copy_saved_objects_to_space'; const MANAGE_SPACES_KEY = 'spaces'; routes.defaults(/\/management/, { resolve: { - spacesManagementSection(activeSpace: any, serverBasePath: string) { + spacesManagementSection() { function getKibanaSection() { return management.getSection('kibana'); } @@ -48,21 +48,24 @@ routes.defaults(/\/management/, { } // Customize Saved Objects Management - const action = new CopyToSpaceSavedObjectsManagementAction( - new SpacesManager(serverBasePath), - activeSpace.space - ); - // This route resolve function executes any time the management screen is loaded, and we want to ensure - // that this action is only registered once. - if (!managementSetup.savedObjects.registry.has(action.id)) { - managementSetup.savedObjects.registry.register(action); - } + spacesNPStart.then(({ spacesManager }) => { + const action = new CopyToSpaceSavedObjectsManagementAction(spacesManager!); + // This route resolve function executes any time the management screen is loaded, and we want to ensure + // that this action is only registered once. + if (!managementSetup.savedObjects.registry.has(action.id)) { + managementSetup.savedObjects.registry.register(action); + } + }); + + const getActiveSpace = async () => { + const { spacesManager } = await spacesNPStart; + return spacesManager!.getActiveSpace(); + }; - // Customize Advanced Settings - const PageTitle = () => ; + const PageTitle = () => ; registerSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle, true); - const SubTitle = () => ; + const SubTitle = () => ; registerSettingsComponent(PAGE_SUBTITLE_COMPONENT, SubTitle, true); } diff --git a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx index 1f0afc706c3f0e..d8fd0298df2fc3 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx @@ -5,31 +5,36 @@ */ // @ts-ignore import template from 'plugins/spaces/views/management/template.html'; -import { SpacesNavState } from 'plugins/spaces/views/nav_control'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nContext } from 'ui/i18n'; // @ts-ignore import routes from 'ui/routes'; -import { SpacesManager } from '../../lib/spaces_manager'; +import { npStart } from 'ui/new_platform'; import { ManageSpacePage } from './edit_space'; import { getCreateBreadcrumbs, getEditBreadcrumbs, getListBreadcrumbs } from './lib'; import { SpacesGridPage } from './spaces_grid'; + +import { start as spacesNPStart } from '../../legacy'; + const reactRootNodeId = 'manageSpacesReactRoot'; routes.when('/management/spaces/list', { template, k7Breadcrumbs: getListBreadcrumbs, requireUICapability: 'management.kibana.spaces', - controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) { + controller($scope: any) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); - const spacesManager = new SpacesManager(serverBasePath); + const { spacesManager } = await spacesNPStart; render( - + , domNode ); @@ -48,15 +53,18 @@ routes.when('/management/spaces/create', { template, k7Breadcrumbs: getCreateBreadcrumbs, requireUICapability: 'management.kibana.spaces', - controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) { + controller($scope: any) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); - const spacesManager = new SpacesManager(serverBasePath); + const { spacesManager } = await spacesNPStart; render( - + , domNode ); @@ -79,29 +87,21 @@ routes.when('/management/spaces/edit/:spaceId', { template, k7Breadcrumbs: () => getEditBreadcrumbs(), requireUICapability: 'management.kibana.spaces', - controller( - $scope: any, - $route: any, - chrome: any, - spacesNavState: SpacesNavState, - serverBasePath: string - ) { + controller($scope: any, $route: any) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); const { spaceId } = $route.current.params; - const spacesManager = new SpacesManager(serverBasePath); + const { spacesManager } = await spacesNPStart; render( { - chrome.breadcrumbs.set(breadcrumbs); - }} + spacesManager={spacesManager!} + setBreadcrumbs={npStart.core.chrome.setBreadcrumbs} + capabilities={npStart.core.application.capabilities} /> , domNode diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx index c6ad2e36740d95..9fa03b1a9b74ae 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx @@ -19,10 +19,9 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { capabilities } from 'ui/capabilities'; import { kfetch } from 'ui/kfetch'; -// @ts-ignore import { toastNotifications } from 'ui/notify'; +import { Capabilities } from 'src/core/public'; import { Feature } from '../../../../../../../plugins/features/public'; import { isReservedSpace } from '../../../../common'; import { DEFAULT_SPACE_ID } from '../../../../common/constants'; @@ -30,7 +29,6 @@ import { Space } from '../../../../common/model/space'; import { SpaceAvatar } from '../../../components'; import { getSpacesFeatureDescription } from '../../../lib/constants'; import { SpacesManager } from '../../../lib/spaces_manager'; -import { SpacesNavState } from '../../nav_control'; import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; import { SecureSpaceMessage } from '../components/secure_space_message'; import { UnauthorizedPrompt } from '../components/unauthorized_prompt'; @@ -38,8 +36,8 @@ import { getEnabledFeatures } from '../lib/feature_utils'; interface Props { spacesManager: SpacesManager; - spacesNavState: SpacesNavState; intl: InjectedIntl; + capabilities: Capabilities; } interface State { @@ -65,7 +63,7 @@ class SpacesGridPageUI extends Component { } public componentDidMount() { - if (capabilities.get().spaces.manage) { + if (this.props.capabilities.spaces.manage) { this.loadGrid(); } } @@ -83,7 +81,7 @@ class SpacesGridPageUI extends Component { public getPageContent() { const { intl } = this.props; - if (!capabilities.get().spaces.manage) { + if (!this.props.capabilities.spaces.manage) { return ; } @@ -159,12 +157,11 @@ class SpacesGridPageUI extends Component { return null; } - const { spacesNavState, spacesManager } = this.props; + const { spacesManager } = this.props; return ( { this.setState({ @@ -178,7 +175,7 @@ class SpacesGridPageUI extends Component { public deleteSpace = async () => { const { intl } = this.props; - const { spacesManager, spacesNavState } = this.props; + const { spacesManager } = this.props; const space = this.state.selectedSpace; @@ -221,8 +218,6 @@ class SpacesGridPageUI extends Component { ); toastNotifications.addSuccess(message); - - spacesNavState.refreshSpacesList(); }; public loadGrid = async () => { diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx index 346cb8b7074fa7..4add607707b248 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_pages.test.tsx @@ -6,14 +6,12 @@ jest.mock('ui/kfetch', () => ({ kfetch: () => Promise.resolve([]), })); -import '../../../__mocks__/ui_capabilities'; import '../../../__mocks__/xpack_info'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { SpaceAvatar } from '../../../components'; import { spacesManagerMock } from '../../../lib/mocks'; import { SpacesManager } from '../../../lib'; -import { SpacesNavState } from '../../nav_control'; import { SpacesGridPage } from './spaces_grid_page'; const spaces = [ @@ -38,11 +36,6 @@ const spaces = [ }, ]; -const spacesNavState: SpacesNavState = { - getActiveSpace: () => spaces[0], - refreshSpacesList: jest.fn(), -}; - const spacesManager = spacesManagerMock.create(); spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces); @@ -52,8 +45,13 @@ describe('SpacesGridPage', () => { shallowWithIntl( ) ).toMatchSnapshot(); @@ -63,8 +61,13 @@ describe('SpacesGridPage', () => { const wrapper = mountWithIntl( ); diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap index 4e829bd481152f..5cad4e794cfda6 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/__snapshots__/nav_control_popover.test.tsx.snap @@ -4,25 +4,18 @@ exports[`NavControlPopover renders without crashing 1`] = ` - } - linkTitle="foo" - spaceSelectorShown={false} - toggleSpaceSelector={[Function]} - /> + + + } closePopover={[Function]} data-test-subj="spacesNavSelector" @@ -36,6 +29,16 @@ exports[`NavControlPopover renders without crashing 1`] = ` withTitle={true} > diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap index 8cce2a376746af..079dab701cc1dd 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap @@ -19,6 +19,16 @@ exports[`SpacesDescription renders without crashing 1`] = ` key="manageSpacesButton" > { it('renders without crashing', () => { - expect(shallow()).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx index c9bc0891df8c82..043fc656a571e8 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_description.tsx @@ -6,11 +6,13 @@ import { EuiContextMenuPanel, EuiText } from '@elastic/eui'; import React, { FC } from 'react'; +import { Capabilities } from 'src/core/public'; import { ManageSpacesButton } from '../../../components'; import { getSpacesFeatureDescription } from '../../../lib/constants'; interface Props { onManageSpacesClick: () => void; + capabilities: Capabilities; } export const SpacesDescription: FC = (props: Props) => { @@ -29,6 +31,7 @@ export const SpacesDescription: FC = (props: Props) => { size="s" style={{ width: `100%` }} onClick={props.onManageSpacesClick} + capabilities={props.capabilities} /> diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx deleted file mode 100644 index d5c693df58b284..00000000000000 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_header_nav_button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - // @ts-ignore - EuiHeaderSectionItemButton, -} from '@elastic/eui'; -import React from 'react'; -import { ButtonProps } from '../types'; - -export const SpacesHeaderNavButton: React.FC = props => ( - - {props.linkIcon} - -); diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx index 76a47ca738627e..9a26f6802abdf5 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiContextMenuItem, EuiContextMenuPanel, EuiFieldSearch, EuiText } from '@elastic/eui'; +import { + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFieldSearch, + EuiText, + EuiLoadingContent, +} from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component } from 'react'; +import React, { Component, ReactElement } from 'react'; +import { Capabilities } from 'src/core/public'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants'; import { Space } from '../../../../common/model/space'; import { ManageSpacesButton, SpaceAvatar } from '../../../components'; interface Props { spaces: Space[]; + isLoading: boolean; onSelectSpace: (space: Space) => void; onManageSpacesClick: () => void; intl: InjectedIntl; + capabilities: Capabilities; } interface State { @@ -30,10 +39,12 @@ class SpacesMenuUI extends Component { }; public render() { - const { intl } = this.props; + const { intl, isLoading } = this.props; const { searchTerm } = this.state; - const items = this.getVisibleSpaces(searchTerm).map(this.renderSpaceMenuItem); + const items = isLoading + ? [1, 2, 3].map(this.renderPlaceholderMenuItem) + : this.getVisibleSpaces(searchTerm).map(this.renderSpaceMenuItem); const panelProps = { className: 'spcMenu', @@ -76,7 +87,7 @@ class SpacesMenuUI extends Component { return filteredSpaces; }; - private renderSpacesListPanel = (items: JSX.Element[], searchTerm: string) => { + private renderSpacesListPanel = (items: ReactElement[], searchTerm: string) => { if (items.length === 0) { return ( @@ -151,6 +162,7 @@ class SpacesMenuUI extends Component { className="spcMenu__manageButton" size="s" onClick={this.props.onManageSpacesClick} + capabilities={this.props.capabilities} /> ); }; @@ -175,6 +187,14 @@ class SpacesMenuUI extends Component { ); }; + + private renderPlaceholderMenuItem = (key: string | number): JSX.Element => { + return ( + + + + ); + }; } export const SpacesMenu = injectI18n(SpacesMenuUI); diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts b/x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts index 541c79a8fd4a36..649aeee9eab9ed 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './nav_control'; - -export { SpacesNavState } from './nav_control'; +export { initSpacesNavControl } from './nav_control'; diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx index bac95bbf22099e..0df077e0d2da04 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx @@ -5,69 +5,34 @@ */ import { SpacesManager } from 'plugins/spaces/lib/spaces_manager'; -// @ts-ignore -import template from 'plugins/spaces/views/nav_control/nav_control.html'; -import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_popover'; -// @ts-ignore -import { Path } from 'plugins/xpack_main/services/path'; import React from 'react'; import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { - chromeHeaderNavControlsRegistry, - NavControlSide, -} from 'ui/registry/chrome_header_nav_controls'; -// @ts-ignore -import { Space } from '../../../common/model/space'; -import { SpacesHeaderNavButton } from './components/spaces_header_nav_button'; - -const module = uiModules.get('spaces_nav', ['kibana']); - -export interface SpacesNavState { - getActiveSpace: () => Space; - refreshSpacesList: () => void; -} - -let spacesManager: SpacesManager; - -module.service('spacesNavState', (activeSpace: any) => { - return { - getActiveSpace: () => { - return activeSpace.space; - }, - refreshSpacesList: () => { - if (spacesManager) { - spacesManager.requestRefresh(); +import { CoreStart } from 'src/core/public'; +import { NavControlPopover } from './nav_control_popover'; + +export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreStart) { + const I18nContext = core.i18n.Context; + core.chrome.navControls.registerLeft({ + order: 1000, + mount(targetDomElement: HTMLElement) { + if (core.http.anonymousPaths.isAnonymous(window.location.pathname)) { + return () => null; } - }, - } as SpacesNavState; -}); - -chromeHeaderNavControlsRegistry.register((chrome: any, activeSpace: any) => ({ - name: 'spaces', - order: 1000, - side: NavControlSide.Left, - render(el: HTMLElement) { - if (Path.isUnauthenticated()) { - return; - } - - const serverBasePath = chrome.getInjected('serverBasePath'); - spacesManager = new SpacesManager(serverBasePath); - - ReactDOM.render( - - - , - el - ); - }, -})); + ReactDOM.render( + + + , + targetDomElement + ); + + return () => { + ReactDOM.unmountComponentAtNode(targetDomElement); + }; + }, + }); +} diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx index c0d04342a69c87..a04f28242f984a 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx @@ -4,40 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import * as Rx from 'rxjs'; +import { shallow } from 'enzyme'; import React from 'react'; import { SpaceAvatar } from '../../components'; import { spacesManagerMock } from '../../lib/mocks'; import { SpacesManager } from '../../lib'; -import { SpacesHeaderNavButton } from './components/spaces_header_nav_button'; import { NavControlPopover } from './nav_control_popover'; +import { EuiHeaderSectionItemButton } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; describe('NavControlPopover', () => { it('renders without crashing', () => { - const activeSpace = { - space: { id: '', name: 'foo', disabledFeatures: [] }, - valid: true, - }; - const spacesManager = spacesManagerMock.create(); const wrapper = shallow( ); expect(wrapper).toMatchSnapshot(); }); it('renders a SpaceAvatar with the active space', async () => { - const activeSpace = { - space: { id: 'foo-space', name: 'foo', disabledFeatures: [] }, - valid: true, - }; - const spacesManager = spacesManagerMock.create(); spacesManager.getSpaces = jest.fn().mockResolvedValue([ { @@ -51,23 +42,27 @@ describe('NavControlPopover', () => { disabledFeatures: [], }, ]); + spacesManager.onActiveSpaceChange$ = Rx.of({ + id: 'foo-space', + name: 'foo', + disabledFeatures: [], + }); - const wrapper = mount( + const wrapper = mountWithIntl( ); - return new Promise(resolve => { - setTimeout(() => { - expect(wrapper.state().spaces).toHaveLength(2); - wrapper.update(); - expect(wrapper.find(SpaceAvatar)).toHaveLength(1); - resolve(); - }, 20); - }); + wrapper.find(EuiHeaderSectionItemButton).simulate('click'); + + // Wait for `getSpaces` promise to resolve + await Promise.resolve(); + await Promise.resolve(); + wrapper.update(); + + expect(wrapper.find(SpaceAvatar)).toHaveLength(3); }); }); diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx index f0e365c27b8e74..98ce64715f3253 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control_popover.tsx @@ -4,24 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAvatar, EuiPopover, PopoverAnchorPosition } from '@elastic/eui'; +import { + EuiPopover, + PopoverAnchorPosition, + EuiLoadingSpinner, + EuiHeaderSectionItemButton, +} from '@elastic/eui'; import React, { Component } from 'react'; +import { Capabilities } from 'src/core/public'; +import { Subscription } from 'rxjs'; import { Space } from '../../../common/model/space'; import { SpaceAvatar } from '../../components'; import { SpacesManager } from '../../lib/spaces_manager'; import { SpacesDescription } from './components/spaces_description'; import { SpacesMenu } from './components/spaces_menu'; -import { ButtonProps } from './types'; interface Props { spacesManager: SpacesManager; - activeSpace: { - valid: boolean; - error?: string; - space: Space; - }; anchorPosition: PopoverAnchorPosition; - buttonClass: React.ComponentType; + capabilities: Capabilities; } interface State { @@ -32,23 +33,31 @@ interface State { } export class NavControlPopover extends Component { + private activeSpace$?: Subscription; + constructor(props: Props) { super(props); this.state = { showSpaceSelector: false, loading: false, - activeSpace: props.activeSpace.space, + activeSpace: null, spaces: [], }; } - public componentDidMount() { - this.loadSpaces(); + public componentWillMount() { + this.activeSpace$ = this.props.spacesManager.onActiveSpaceChange$.subscribe({ + next: activeSpace => { + this.setState({ + activeSpace, + }); + }, + }); + } - if (this.props.spacesManager) { - this.props.spacesManager.on('request_refresh', () => { - this.loadSpaces(); - }); + public componentWillUnmount() { + if (this.activeSpace$) { + this.activeSpace$.unsubscribe(); } } @@ -59,20 +68,26 @@ export class NavControlPopover extends Component { } let element: React.ReactNode; - if (this.state.spaces.length < 2) { - element = ; + if (!this.state.loading && this.state.spaces.length < 2) { + element = ( + + ); } else { element = ( ); } return ( - // @ts-ignore repositionOnScroll doesn't exist on EuiPopover { } private async loadSpaces() { - const { spacesManager, activeSpace } = this.props; + const { spacesManager } = this.props; + + if (this.state.loading) { + return; + } this.setState({ loading: true, @@ -99,16 +118,8 @@ export class NavControlPopover extends Component { const spaces = await spacesManager.getSpaces(); - // Update the active space definition, if it changed since the last load operation - let activeSpaceEntry: Space | null = activeSpace.space; - - if (activeSpace.valid) { - activeSpaceEntry = spaces.find(space => space.id === this.props.activeSpace.space.id) || null; - } - this.setState({ spaces, - activeSpace: activeSpaceEntry, loading: false, }); } @@ -117,10 +128,7 @@ export class NavControlPopover extends Component { const { activeSpace } = this.state; if (!activeSpace) { - return this.getButton( - , - 'error' - ); + return this.getButton(, 'loading'); } return this.getButton( @@ -130,14 +138,17 @@ export class NavControlPopover extends Component { }; private getButton = (linkIcon: JSX.Element, linkTitle: string) => { - const Button = this.props.buttonClass; return ( -