diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts index 971a43d06d05..d4490b60901b 100644 --- a/src/core/public/application/capabilities/capabilities_service.mock.ts +++ b/src/core/public/application/capabilities/capabilities_service.mock.ts @@ -37,6 +37,7 @@ const createStartContractMock = (): jest.Mocked => ({ catalogue: {}, management: {}, navLinks: {}, + workspaces: {}, }), }); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 7398aad65009..4744ab34cfd3 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -47,6 +47,7 @@ import { IUiSettingsClient } from '../ui_settings'; import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; +import { WorkspacesStart } from '../workspace'; /** * Accessibility status of an application. @@ -334,6 +335,8 @@ export interface AppMountContext { injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; + /** {@link WorkspacesService} */ + workspaces: WorkspacesStart; }; } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 4e17b9d7e5f5..7ec470c74e03 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -66,6 +66,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentActionMenu$": BehaviorSubject { "_isScalar": false, @@ -6757,6 +6758,7 @@ exports[`Header renders condensed header 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentActionMenu$": BehaviorSubject { "_isScalar": false, diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index da09de341bc4..4025361e150d 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 b3acd696bc3d..24de9ea6b9ea 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,11 @@ describe('#start()', () => { expect(MockIntegrationsService.start).toHaveBeenCalledTimes(1); }); + it('calls workspaces#start()', async () => { + await startCore(); + expect(MockWorkspacesService.start).toHaveBeenCalledTimes(1); + }); + it('calls coreApp#start()', async () => { await startCore(); expect(MockCoreApp.start).toHaveBeenCalledTimes(1); @@ -364,6 +377,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 8fe5a36ebb55..a9dba002e44c 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -54,6 +54,7 @@ import { ContextService } from './context'; import { IntegrationsService } from './integrations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; +import { WorkspacesService } from './workspace'; interface Params { rootDomElement: HTMLElement; @@ -110,6 +111,7 @@ export class CoreSystem { private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; + private readonly workspaces: WorkspacesService; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -138,6 +140,7 @@ export class CoreSystem { this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); + this.workspaces = new WorkspacesService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; @@ -160,6 +163,7 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); + const workspaces = this.workspaces.setup(); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ @@ -176,6 +180,7 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, + workspaces, }; // Services that do not expose contracts at setup @@ -220,6 +225,7 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, overlays }); + const workspaces = this.workspaces.start(); const chrome = await this.chrome.start({ application, docLinks, @@ -242,6 +248,7 @@ export class CoreSystem { overlays, savedObjects, uiSettings, + workspaces, })); const core: InternalCoreStart = { @@ -256,6 +263,7 @@ export class CoreSystem { overlays, uiSettings, fatalErrors, + workspaces, }; await this.plugins.start(core); @@ -303,6 +311,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 03ef6b6392f9..084e3c246b13 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -87,6 +87,7 @@ import { HandlerParameters, } from './context'; import { Branding } from '../types'; +import { WorkspacesStart, WorkspacesSetup } from './workspace'; export type { Logos } from '../common'; export { PackageInfo, EnvironmentMode } from '../server/types'; @@ -102,6 +103,7 @@ export { StringValidation, StringValidationRegex, StringValidationRegexString, + WorkspaceAttribute, } from '../types'; export { @@ -239,6 +241,8 @@ export interface CoreSetup; + /** {@link WorkspacesSetup} */ + workspaces: WorkspacesSetup; } /** @@ -293,6 +297,8 @@ export interface CoreStart { getInjectedVar: (name: string, defaultValue?: any) => unknown; getBranding: () => Branding; }; + /** {@link WorkspacesStart} */ + workspaces: WorkspacesStart; } export { @@ -341,3 +347,5 @@ export { }; export { __osdBootstrap__ } from './osd_bootstrap'; + +export { WorkspacesStart, WorkspacesSetup } from './workspace'; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e863d627c801..3acc71424b91 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -47,6 +47,7 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; +import { workspacesServiceMock } from './workspace/workspaces_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -60,6 +61,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; +export { workspacesServiceMock } from './workspace/workspaces_service.mock'; function createCoreSetupMock({ basePath = '', @@ -85,6 +87,7 @@ function createCoreSetupMock({ getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, getBranding: injectedMetadataServiceMock.createSetupContract().getBranding, }, + workspaces: workspacesServiceMock.createSetupContract(), }; return mock; @@ -106,6 +109,7 @@ function createCoreStartMock({ basePath = '' } = {}) { getBranding: injectedMetadataServiceMock.createStartContract().getBranding, }, fatalErrors: fatalErrorsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 42c40e91183f..87738fc7e57a 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -121,6 +121,7 @@ export function createPluginSetupContext< getBranding: deps.injectedMetadata.getBranding, }, getStartServices: () => plugin.startDependencies, + workspaces: deps.workspaces, }; } @@ -168,5 +169,6 @@ export function createPluginStartContext< getBranding: deps.injectedMetadata.getBranding, }, fatalErrors: deps.fatalErrors, + workspaces: deps.workspaces, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 616133a84a32..acca058fe657 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -58,6 +58,7 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; +import { workspacesServiceMock } from '../workspace/workspaces_service.mock'; export let mockPluginInitializers: Map; @@ -108,6 +109,7 @@ describe('PluginsService', () => { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + workspaces: workspacesServiceMock.createSetupContract(), }; mockSetupContext = { ...mockSetupDeps, @@ -127,6 +129,7 @@ describe('PluginsService', () => { uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts new file mode 100644 index 000000000000..4b9b2c86f649 --- /dev/null +++ b/src/core/public/workspace/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service'; diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts new file mode 100644 index 000000000000..ae56c035eb3a --- /dev/null +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import type { PublicMethodsOf } from '@osd/utility-types'; + +import { WorkspacesService } from './workspaces_service'; +import { WorkspaceAttribute } from '..'; + +const currentWorkspaceId$ = new BehaviorSubject(''); +const workspaceList$ = new BehaviorSubject([]); +const currentWorkspace$ = new BehaviorSubject(null); +const initialized$ = new BehaviorSubject(false); + +const createWorkspacesSetupContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + initialized$, +}); + +const createWorkspacesStartContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + initialized$, +}); + +export type WorkspacesServiceContract = PublicMethodsOf; +const createMock = (): jest.Mocked => ({ + setup: jest.fn().mockReturnValue(createWorkspacesSetupContractMock()), + start: jest.fn().mockReturnValue(createWorkspacesStartContractMock()), + stop: jest.fn(), +}); + +export const workspacesServiceMock = { + 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 000000000000..4eca97ef2ed2 --- /dev/null +++ b/src/core/public/workspace/workspaces_service.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WorkspacesService, WorkspacesStart } from './workspaces_service'; + +describe('WorkspacesService', () => { + let workspaces: WorkspacesService; + let workspacesStart: WorkspacesStart; + + beforeEach(() => { + workspaces = new WorkspacesService(); + workspacesStart = workspaces.start(); + }); + + afterEach(() => { + workspaces.stop(); + }); + + it('workspace initialized$ state is false by default', () => { + expect(workspacesStart.initialized$.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('currentWorkspace is updated when currentWorkspaceId changes', () => { + 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.initialized$.isStopped).toBe(true); + }); +}); diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts new file mode 100644 index 000000000000..cc19b3c79229 --- /dev/null +++ b/src/core/public/workspace/workspaces_service.ts @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { isEqual } from 'lodash'; + +import { CoreService, WorkspaceAttribute } from '../../types'; + +type WorkspaceObject = WorkspaceAttribute & { readonly?: boolean }; + +interface WorkspaceObservables { + /** + * Indicates the current activated workspace id, the value should be changed every time + * when switching to a different workspace + */ + currentWorkspaceId$: BehaviorSubject; + + /** + * The workspace that is derived from `currentWorkspaceId` and `workspaceList`, if + * `currentWorkspaceId` cannot be found from `workspaceList`, it will return an error + * + * This value MUST NOT set manually from outside of WorkspacesService + */ + currentWorkspace$: BehaviorSubject; + + /** + * The list of available workspaces. This workspace list should be set by whoever + * implemented the workspace functionalities, core workspace module should not be + * aware of how to populate the workspace list. + */ + workspaceList$: BehaviorSubject; + + /** + * This is a flag which indicates the WorkspacesService module is initialized and ready + * for consuming by others. For example, the `workspaceList` has been set, etc + */ + initialized$: BehaviorSubject; +} + +enum WORKSPACE_ERROR { + WORKSPACE_IS_STALE = 'WORKSPACE_IS_STALE', +} + +export type WorkspacesSetup = WorkspaceObservables; +export type WorkspacesStart = WorkspaceObservables; + +export class WorkspacesService implements CoreService { + private currentWorkspaceId$ = new BehaviorSubject(''); + private workspaceList$ = new BehaviorSubject([]); + private currentWorkspace$ = new BehaviorSubject(null); + private initialized$ = new BehaviorSubject(false); + + constructor() { + combineLatest([this.initialized$, this.workspaceList$, this.currentWorkspaceId$]).subscribe( + ([workspaceInitialized, workspaceList, currentWorkspaceId]) => { + if (workspaceInitialized) { + const currentWorkspace = workspaceList.find((w) => w && w.id === currentWorkspaceId); + + /** + * Do a simple idempotent verification here + */ + if (!isEqual(currentWorkspace, this.currentWorkspace$.getValue())) { + this.currentWorkspace$.next(currentWorkspace ?? null); + } + + if (currentWorkspaceId && !currentWorkspace?.id) { + /** + * Current workspace is stale + */ + this.currentWorkspaceId$.error({ + reason: WORKSPACE_ERROR.WORKSPACE_IS_STALE, + }); + this.currentWorkspace$.error({ + reason: WORKSPACE_ERROR.WORKSPACE_IS_STALE, + }); + } + } + } + ); + } + + public setup(): WorkspacesSetup { + return { + currentWorkspaceId$: this.currentWorkspaceId$, + currentWorkspace$: this.currentWorkspace$, + workspaceList$: this.workspaceList$, + initialized$: this.initialized$, + }; + } + + public start(): WorkspacesStart { + return { + currentWorkspaceId$: this.currentWorkspaceId$, + currentWorkspace$: this.currentWorkspace$, + workspaceList$: this.workspaceList$, + initialized$: this.initialized$, + }; + } + + public async stop() { + this.currentWorkspace$.unsubscribe(); + this.currentWorkspaceId$.unsubscribe(); + this.workspaceList$.unsubscribe(); + this.initialized$.unsubscribe(); + } +} diff --git a/src/core/server/capabilities/capabilities_service.mock.ts b/src/core/server/capabilities/capabilities_service.mock.ts index fb0785aac947..f8be8f22b70c 100644 --- a/src/core/server/capabilities/capabilities_service.mock.ts +++ b/src/core/server/capabilities/capabilities_service.mock.ts @@ -52,6 +52,7 @@ const createCapabilitiesMock = (): Capabilities => { navLinks: {}, management: {}, catalogue: {}, + workspaces: {}, }; }; diff --git a/src/core/server/capabilities/capabilities_service.test.ts b/src/core/server/capabilities/capabilities_service.test.ts index 80e22fdb6721..be28130afd4e 100644 --- a/src/core/server/capabilities/capabilities_service.test.ts +++ b/src/core/server/capabilities/capabilities_service.test.ts @@ -72,6 +72,7 @@ describe('CapabilitiesService', () => { "navLinks": Object { "myLink": true, }, + "workspaces": Object {}, } `); }); @@ -107,6 +108,7 @@ describe('CapabilitiesService', () => { "B": true, "C": true, }, + "workspaces": Object {}, } `); }); @@ -134,6 +136,7 @@ describe('CapabilitiesService', () => { }, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, } `); }); @@ -192,6 +195,7 @@ describe('CapabilitiesService', () => { "b": true, "c": false, }, + "workspaces": Object {}, } `); }); diff --git a/src/core/server/capabilities/capabilities_service.ts b/src/core/server/capabilities/capabilities_service.ts index b92166427271..5153cd5ded1d 100644 --- a/src/core/server/capabilities/capabilities_service.ts +++ b/src/core/server/capabilities/capabilities_service.ts @@ -123,6 +123,7 @@ const defaultCapabilities: Capabilities = { navLinks: {}, management: {}, catalogue: {}, + workspaces: {}, }; /** @internal */ diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts index b60a067982e0..3cab62478cbc 100644 --- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts +++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts @@ -79,6 +79,7 @@ describe('CapabilitiesService', () => { "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, } `); }); @@ -101,6 +102,7 @@ describe('CapabilitiesService', () => { }, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, } `); }); diff --git a/src/core/server/capabilities/resolve_capabilities.test.ts b/src/core/server/capabilities/resolve_capabilities.test.ts index 25968d858e7a..88e2aa7a4f7c 100644 --- a/src/core/server/capabilities/resolve_capabilities.test.ts +++ b/src/core/server/capabilities/resolve_capabilities.test.ts @@ -42,6 +42,7 @@ describe('resolveCapabilities', () => { navLinks: {}, catalogue: {}, management: {}, + workspaces: {}, }; request = httpServerMock.createOpenSearchDashboardsRequest(); }); @@ -75,6 +76,7 @@ describe('resolveCapabilities', () => { }, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, } `); }); diff --git a/src/core/types/capabilities.ts b/src/core/types/capabilities.ts index d964cd75b997..454f997de8f1 100644 --- a/src/core/types/capabilities.ts +++ b/src/core/types/capabilities.ts @@ -47,6 +47,9 @@ export interface Capabilities { /** Catalogue capabilities. Catalogue entries drive the visibility of the OpenSearch Dashboards homepage options. */ catalogue: Record; + /** Workspaces capabilities. */ + workspaces: Record; + /** Custom capabilities, registered by plugins. undefined if the key does not exist */ [key: string]: Record> | undefined; } diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 5a84d7a883e8..f65d683f6bdc 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -39,4 +39,5 @@ export * from './ui_settings'; export * from './saved_objects'; export * from './serializable'; export * from './custom_branding'; +export * from './workspace'; export * from './cross_compatibility'; diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts new file mode 100644 index 000000000000..c95b993edc74 --- /dev/null +++ b/src/core/types/workspace.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface WorkspaceAttribute { + id: string; + name: string; + description?: string; + features?: string[]; + color?: string; + icon?: string; + reserved?: boolean; +} diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index 848106a7fba3..c9ffe147e5f8 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -104,6 +104,7 @@ exports[`dashboard listing hideWriteControls 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -994,6 +995,44 @@ exports[`dashboard listing hideWriteControls 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -1197,6 +1236,7 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -2087,6 +2127,44 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -2351,6 +2429,7 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -3241,6 +3320,44 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -3505,6 +3622,7 @@ exports[`dashboard listing renders table rows 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -4395,6 +4513,44 @@ exports[`dashboard listing renders table rows 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -4659,6 +4815,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -5549,6 +5706,44 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index b0f1abee686a..1954051c9474 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -104,6 +104,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -860,6 +861,44 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -1022,6 +1061,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -1778,6 +1818,44 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -1940,6 +2018,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -2696,6 +2775,44 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -2858,6 +2975,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -3614,6 +3732,44 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -3776,6 +3932,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -4532,6 +4689,44 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -4694,6 +4889,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -5450,6 +5646,44 @@ exports[`Dashboard top nav render with all components 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } >