diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 69300c949fb..792d5195c4c 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -47,7 +47,7 @@ import { IUiSettingsClient } from '../ui_settings'; import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; -import { WorkspaceStart } from '../workspace'; +import { WorkspacesStart } from '../workspace'; /** * Accessibility status of an application. @@ -345,8 +345,8 @@ export interface AppMountContext { injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; - /** {@link WorkspaceService} */ - workspaces: WorkspaceStart; + /** {@link WorkspacesService} */ + workspaces: WorkspacesStart; }; } diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index bdd4dda1b09..0133047a293 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -48,7 +48,7 @@ import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; -import { Branding, WorkspaceStart } from '../'; +import { Branding, WorkspacesStart } from '../'; import { getLogos } from '../../common'; import type { Logos } from '../../common/types'; @@ -96,7 +96,7 @@ interface StartDeps { injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; } /** @internal */ diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 812772b92ef..af0bb4d4e28 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -42,7 +42,7 @@ import { groupBy, sortBy } from 'lodash'; import React, { useRef } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; -import { WorkspaceStart } from 'opensearch-dashboards/public'; +import { WorkspacesStart } from 'opensearch-dashboards/public'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; import { InternalApplicationStart } from '../../../application'; @@ -132,7 +132,7 @@ interface Props { navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; logos: Logos; - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; } export function CollapsibleNav({ diff --git a/src/core/public/chrome/ui/header/collapsible_nav_header.tsx b/src/core/public/chrome/ui/header/collapsible_nav_header.tsx index 8ef24a1937e..b947a149fa1 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_header.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_header.tsx @@ -6,10 +6,10 @@ import { i18n } from '@osd/i18n'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiCollapsibleNavGroup } from '@elastic/eui'; -import { WorkspaceStart } from '../../../../public'; +import { WorkspacesStart } from '../../../../public'; interface Props { - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; } export function CollapsibleNavHeader({ workspaces }: Props) { diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 36560bc7576..6c11374dd1c 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -44,7 +44,7 @@ import classnames from 'classnames'; import React, { createRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { WorkspaceStart } from 'opensearch-dashboards/public'; +import { WorkspacesStart } from 'opensearch-dashboards/public'; import { LoadingIndicator } from '../'; import { ChromeBadge, @@ -94,7 +94,7 @@ export interface HeaderProps { branding: ChromeBranding; logos: Logos; survey: string | undefined; - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; } export function Header({ diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index da09de341bc..4025361e150 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -42,6 +42,7 @@ import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { renderingServiceMock } from './rendering/rendering_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; +import { workspacesServiceMock } from './workspace/workspaces_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); @@ -145,3 +146,11 @@ export const CoreAppConstructor = jest.fn().mockImplementation(() => MockCoreApp jest.doMock('./core_app', () => ({ CoreApp: CoreAppConstructor, })); + +export const MockWorkspacesService = workspacesServiceMock.create(); +export const WorkspacesServiceConstructor = jest + .fn() + .mockImplementation(() => MockWorkspacesService); +jest.doMock('./workspace', () => ({ + WorkspacesService: WorkspacesServiceConstructor, +})); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index b3acd696bc3..81dfa97b9af 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -55,6 +55,8 @@ import { MockIntegrationsService, CoreAppConstructor, MockCoreApp, + WorkspacesServiceConstructor, + MockWorkspacesService, } from './core_system.test.mocks'; import { CoreSystem } from './core_system'; @@ -99,6 +101,7 @@ describe('constructor', () => { expect(RenderingServiceConstructor).toHaveBeenCalledTimes(1); expect(IntegrationsServiceConstructor).toHaveBeenCalledTimes(1); expect(CoreAppConstructor).toHaveBeenCalledTimes(1); + expect(WorkspacesServiceConstructor).toHaveBeenCalledTimes(1); }); it('passes injectedMetadata param to InjectedMetadataService', () => { @@ -223,6 +226,11 @@ describe('#setup()', () => { expect(MockIntegrationsService.setup).toHaveBeenCalledTimes(1); }); + it('calls workspaces#setup()', async () => { + await setupCore(); + expect(MockWorkspacesService.setup).toHaveBeenCalledTimes(1); + }); + it('calls coreApp#setup()', async () => { await setupCore(); expect(MockCoreApp.setup).toHaveBeenCalledTimes(1); @@ -310,6 +318,15 @@ describe('#start()', () => { expect(MockIntegrationsService.start).toHaveBeenCalledTimes(1); }); + it('calls workspaces#start()', async () => { + await startCore(); + expect(MockWorkspacesService.start).toHaveBeenCalledTimes(1); + expect(MockWorkspacesService.start).toHaveBeenCalledWith({ + application: expect.any(Object), + http: expect.any(Object), + }); + }); + it('calls coreApp#start()', async () => { await startCore(); expect(MockCoreApp.start).toHaveBeenCalledTimes(1); @@ -364,6 +381,14 @@ describe('#stop()', () => { expect(MockIntegrationsService.stop).toHaveBeenCalled(); }); + it('calls workspaces.stop()', () => { + const coreSystem = createCoreSystem(); + + expect(MockWorkspacesService.stop).not.toHaveBeenCalled(); + coreSystem.stop(); + expect(MockWorkspacesService.stop).toHaveBeenCalled(); + }); + it('calls coreApp.stop()', () => { const coreSystem = createCoreSystem(); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index b1fb35483da..92da7ea0e01 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -54,7 +54,7 @@ import { ContextService } from './context'; import { IntegrationsService } from './integrations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; -import { WorkspaceService } from './workspace'; +import { WorkspacesService } from './workspace'; interface Params { rootDomElement: HTMLElement; @@ -111,7 +111,7 @@ export class CoreSystem { private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; - private readonly workspaces: WorkspaceService; + private readonly workspaces: WorkspacesService; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -140,7 +140,7 @@ export class CoreSystem { this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); - this.workspaces = new WorkspaceService(); + this.workspaces = new WorkspacesService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; @@ -312,6 +312,7 @@ export class CoreSystem { this.chrome.stop(); this.i18n.stop(); this.application.stop(); + this.workspaces.stop(); this.rootDomElement.textContent = ''; } } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 00dd418cb9d..152b52870bb 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -87,7 +87,7 @@ import { HandlerParameters, } from './context'; import { Branding } from '../types'; -import { WorkspaceStart, WorkspaceSetup } from './workspace'; +import { WorkspacesStart, WorkspacesSetup } from './workspace'; export type { Logos } from '../common'; export { PackageInfo, EnvironmentMode } from '../server/types'; @@ -241,8 +241,8 @@ export interface CoreSetup; - /** {@link WorkspaceSetup} */ - workspaces: WorkspaceSetup; + /** {@link WorkspacesSetup} */ + workspaces: WorkspacesSetup; } /** @@ -297,8 +297,8 @@ export interface CoreStart { getInjectedVar: (name: string, defaultValue?: any) => unknown; getBranding: () => Branding; }; - /** {@link WorkspaceStart} */ - workspaces: WorkspaceStart; + /** {@link WorkspacesStart} */ + workspaces: WorkspacesStart; } export { @@ -349,9 +349,9 @@ export { export { __osdBootstrap__ } from './osd_bootstrap'; export { - WorkspaceStart, - WorkspaceSetup, - WorkspaceService, + WorkspacesStart, + WorkspacesSetup, + WorkspacesService, WorkspaceObservables, } from './workspace'; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 99d9f5a517f..3acc71424b9 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -87,7 +87,7 @@ function createCoreSetupMock({ getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, getBranding: injectedMetadataServiceMock.createSetupContract().getBranding, }, - workspaces: workspacesServiceMock, + workspaces: workspacesServiceMock.createSetupContract(), }; return mock; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 7e8c96c1f9d..f26626ed1ca 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -109,7 +109,7 @@ describe('PluginsService', () => { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), - workspaces: workspacesServiceMock.createSetupContractMock(), + workspaces: workspacesServiceMock.createSetupContract(), }; mockSetupContext = { ...mockSetupDeps, diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index d83bb2c9090..4ef6aaae7fd 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ export { - WorkspaceStart, - WorkspaceService, - WorkspaceSetup, + WorkspacesStart, + WorkspacesService, + WorkspacesSetup, WorkspaceObservables, } from './workspaces_service'; diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index 3c35315aa85..3b1408b0304 100644 --- a/src/core/public/workspace/workspaces_service.mock.ts +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -4,6 +4,9 @@ */ import { BehaviorSubject } from 'rxjs'; +import type { PublicMethodsOf } from '@osd/utility-types'; + +import { WorkspacesService } from './workspaces_service'; import { WorkspaceAttribute } from '..'; const currentWorkspaceId$ = new BehaviorSubject(''); @@ -30,7 +33,15 @@ const createWorkspacesStartContractMock = () => ({ renderWorkspaceMenu: jest.fn(), }); +export type WorkspacesServiceContract = PublicMethodsOf; +const createMock = (): jest.Mocked => ({ + setup: jest.fn().mockReturnValue(createWorkspacesSetupContractMock()), + start: jest.fn().mockReturnValue(createWorkspacesStartContractMock()), + stop: jest.fn(), +}); + export const workspacesServiceMock = { - createSetupContractMock: createWorkspacesSetupContractMock, + create: createMock, + createSetupContract: createWorkspacesSetupContractMock, createStartContract: createWorkspacesStartContractMock, }; diff --git a/src/core/public/workspace/workspaces_service.test.ts b/src/core/public/workspace/workspaces_service.test.ts new file mode 100644 index 00000000000..b2d84152da8 --- /dev/null +++ b/src/core/public/workspace/workspaces_service.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { httpServiceMock } from '../http/http_service.mock'; +import { applicationServiceMock } from '../application/application_service.mock'; +import { WorkspacesService, WorkspacesSetup, WorkspacesStart } from './workspaces_service'; + +describe('WorkspacesService', () => { + let workspaces: WorkspacesService; + let workspacesSetup: WorkspacesSetup; + let workspacesStart: WorkspacesStart; + + beforeEach(() => { + workspaces = new WorkspacesService(); + workspacesSetup = workspaces.setup(); + workspacesStart = workspaces.start({ + http: httpServiceMock.createStartContract(), + application: applicationServiceMock.createInternalStartContract(), + }); + }); + + afterEach(() => { + workspaces.stop(); + }); + + it('workspace initialized$ state is false by default', () => { + expect(workspacesStart.initialized$.value).toBe(false); + }); + + it('workspace is not enabled by default', () => { + expect(workspacesStart.workspaceEnabled$.value).toBe(false); + }); + + it('currentWorkspace is not set by default', () => { + expect(workspacesStart.currentWorkspace$.value).toBe(null); + expect(workspacesStart.currentWorkspaceId$.value).toBe(''); + }); + + it('workspaceList$ is empty by default', () => { + expect(workspacesStart.workspaceList$.value.length).toBe(0); + }); + + it('should call menu render function', () => { + const renderFn = jest.fn(); + workspacesSetup.registerWorkspaceMenuRender(renderFn); + workspacesStart.renderWorkspaceMenu(); + expect(renderFn).toHaveBeenCalled(); + }); + + it('should return null if NO menu render function was registered', () => { + expect(workspacesStart.renderWorkspaceMenu()).toBe(null); + }); + + it('the current workspace should also updated after changing current workspace id', () => { + expect(workspacesStart.currentWorkspace$.value).toBe(null); + + workspacesStart.initialized$.next(true); + workspacesStart.workspaceList$.next([ + { id: 'workspace-1', name: 'workspace 1' }, + { id: 'workspace-2', name: 'workspace 2' }, + ]); + workspacesStart.currentWorkspaceId$.next('workspace-1'); + + expect(workspacesStart.currentWorkspace$.value).toEqual({ + id: 'workspace-1', + name: 'workspace 1', + }); + + workspacesStart.currentWorkspaceId$.next(''); + expect(workspacesStart.currentWorkspace$.value).toEqual(null); + }); + + it('should return error if the specified workspace id cannot be found', () => { + expect(workspacesStart.currentWorkspace$.hasError).toBe(false); + workspacesStart.initialized$.next(true); + workspacesStart.workspaceList$.next([ + { id: 'workspace-1', name: 'workspace 1' }, + { id: 'workspace-2', name: 'workspace 2' }, + ]); + workspacesStart.currentWorkspaceId$.next('workspace-3'); + expect(workspacesStart.currentWorkspace$.hasError).toBe(true); + }); + + it('should stop all observables when workspace service stopped', () => { + workspaces.stop(); + expect(workspacesStart.currentWorkspaceId$.isStopped).toBe(true); + expect(workspacesStart.currentWorkspace$.isStopped).toBe(true); + expect(workspacesStart.workspaceList$.isStopped).toBe(true); + expect(workspacesStart.workspaceEnabled$.isStopped).toBe(true); + expect(workspacesStart.initialized$.isStopped).toBe(true); + }); +}); diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index b05d1af58b0..9b8f3cabade 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -8,7 +8,7 @@ import { isEqual } from 'lodash'; import { CoreService, WorkspaceAttribute } from '../../types'; import { InternalApplicationStart } from '../application'; -import { HttpSetup } from '../http'; +import { HttpStart } from '../http'; type WorkspaceMenuRenderFn = ({ basePath, @@ -16,7 +16,7 @@ type WorkspaceMenuRenderFn = ({ observables, }: { getUrlForApp: InternalApplicationStart['getUrlForApp']; - basePath: HttpSetup['basePath']; + basePath: HttpStart['basePath']; observables: WorkspaceObservables; }) => JSX.Element | null; @@ -36,15 +36,15 @@ enum WORKSPACE_ERROR_REASON_MAP { /** * @public */ -export interface WorkspaceSetup extends WorkspaceObservables { +export interface WorkspacesSetup extends WorkspaceObservables { registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => void; } -export interface WorkspaceStart extends WorkspaceObservables { +export interface WorkspacesStart extends WorkspaceObservables { renderWorkspaceMenu: () => JSX.Element | null; } -export class WorkspaceService implements CoreService { +export class WorkspacesService implements CoreService { private currentWorkspaceId$ = new BehaviorSubject(''); private workspaceList$ = new BehaviorSubject([]); private currentWorkspace$ = new BehaviorSubject(null); @@ -81,7 +81,7 @@ export class WorkspaceService implements CoreService HomePluginBranding; }; dataSource?: DataSourcePluginStart; - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; } let services: HomeOpenSearchDashboardsServices | null = null; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx index 81013ef2575..5a3a7e6f57f 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx @@ -35,7 +35,7 @@ import { HttpSetup, NotificationsStart, WorkspaceAttribute, - WorkspaceStart, + WorkspacesStart, } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { SavedObjectWithMetadata } from '../../../../common'; @@ -54,7 +54,7 @@ export interface ShowDuplicateModalProps { targetWorkspace: string ) => Promise; http: HttpSetup; - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; duplicateMode: DuplicateMode; notifications: NotificationsStart; selectedSavedObjects: SavedObjectWithMetadata[]; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 161ec229500..cf34e77a5db 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -123,7 +123,7 @@ describe('SavedObjectsTable', () => { notifications = notificationServiceMock.createStartContract(); savedObjects = savedObjectsServiceMock.createStartContract(); search = dataPluginMock.createStartContract().search; - workspaces = workspacesServiceMock.createSetupContractMock(); + workspaces = workspacesServiceMock.createStartContract(); const applications = applicationServiceMock.createStartContract(); applications.capabilities = { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 2ddfabf0501..8ab6f54432f 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -61,7 +61,7 @@ import { FormattedMessage } from '@osd/i18n/react'; import { SavedObjectsClientContract, SavedObjectsFindOptions, - WorkspaceStart, + WorkspacesStart, HttpStart, OverlayStart, NotificationsStart, @@ -111,7 +111,7 @@ export interface SavedObjectsTableProps { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; http: HttpStart; - workspaces: WorkspaceStart; + workspaces: WorkspacesStart; search: DataPublicPluginStart['search']; overlays: OverlayStart; notifications: NotificationsStart; diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts index fbd783a1ba7..6f33df05953 100644 --- a/src/plugins/workspace/public/workspace_client.ts +++ b/src/plugins/workspace/public/workspace_client.ts @@ -7,7 +7,7 @@ import { HttpFetchOptions, HttpSetup, WorkspaceAttribute, - WorkspaceSetup, + WorkspacesSetup, } from '../../../core/public'; import { WorkspacePermissionMode } from '../../../core/public'; @@ -55,9 +55,9 @@ interface WorkspaceFindOptions { */ export class WorkspaceClient { private http: HttpSetup; - private workspaces: WorkspaceSetup; + private workspaces: WorkspacesSetup; - constructor(http: HttpSetup, workspaces: WorkspaceSetup) { + constructor(http: HttpSetup, workspaces: WorkspacesSetup) { this.http = http; this.workspaces = workspaces; }