From ba6a4ab36b29b34f7863663ac44a0bfca789b612 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 19 Sep 2023 17:21:04 +0800 Subject: [PATCH 01/28] feat: add core workspace module (#145) The core workspace module(WorkspaceService) is a foundational component that enables the implementation of workspace features within OSD plugins. The purpose of the core workspace module is to provide a framework for workspace implementations. This module does not implement specific workspace functionality but provides the essential infrastructure for plugins to extend and customize workspace features, it maintains a shared workspace state(observables) across the entire application to ensure a consistent and up-to-date view of workspace-related information to all parts of the application. --------- Signed-off-by: Yulong Ruan --- src/core/public/application/types.ts | 3 + src/core/public/core_system.ts | 8 + src/core/public/index.ts | 13 + src/core/public/mocks.ts | 4 + src/core/public/plugins/plugin_context.ts | 2 + .../public/plugins/plugins_service.test.ts | 3 + src/core/public/workspace/index.ts | 10 + .../workspace/workspaces_service.mock.ts | 36 +++ .../public/workspace/workspaces_service.ts | 134 ++++++++ .../dashboard_listing.test.tsx.snap | 240 +++++++++++++++ .../dashboard_top_nav.test.tsx.snap | 288 ++++++++++++++++++ 11 files changed, 741 insertions(+) create mode 100644 src/core/public/workspace/index.ts create mode 100644 src/core/public/workspace/workspaces_service.mock.ts create mode 100644 src/core/public/workspace/workspaces_service.ts 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/core_system.ts b/src/core/public/core_system.ts index 8fe5a36ebb55..d7de8b4595d5 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({ application, http }); 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); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 03ef6b6392f9..14ab91e1cb13 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,10 @@ export { }; export { __osdBootstrap__ } from './osd_bootstrap'; + +export { + WorkspacesStart, + WorkspacesSetup, + WorkspacesService, + WorkspaceObservables, +} from './workspace'; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e863d627c801..722070d5a9ea 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.createSetupContractMock(), }; 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 b2cf4e8880cf..7e8c96c1f9d0 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.createSetupContractMock(), }; 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..4ef6aaae7fd4 --- /dev/null +++ b/src/core/public/workspace/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export { + 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 new file mode 100644 index 000000000000..3c35315aa850 --- /dev/null +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { WorkspaceAttribute } from '..'; + +const currentWorkspaceId$ = new BehaviorSubject(''); +const workspaceList$ = new BehaviorSubject([]); +const currentWorkspace$ = new BehaviorSubject(null); +const initialized$ = new BehaviorSubject(false); +const workspaceEnabled$ = new BehaviorSubject(false); + +const createWorkspacesSetupContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + initialized$, + workspaceEnabled$, + registerWorkspaceMenuRender: jest.fn(), +}); + +const createWorkspacesStartContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + initialized$, + workspaceEnabled$, + renderWorkspaceMenu: jest.fn(), +}); + +export const workspacesServiceMock = { + createSetupContractMock: createWorkspacesSetupContractMock, + createStartContract: createWorkspacesStartContractMock, +}; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts new file mode 100644 index 000000000000..39519ccdddbe --- /dev/null +++ b/src/core/public/workspace/workspaces_service.ts @@ -0,0 +1,134 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { isEqual } from 'lodash'; + +import { CoreService, WorkspaceAttribute } from '../../types'; +import { InternalApplicationStart } from '../application'; +import { HttpSetup } from '../http'; + +type WorkspaceMenuRenderFn = ({ + basePath, + getUrlForApp, + observables, +}: { + getUrlForApp: InternalApplicationStart['getUrlForApp']; + basePath: HttpSetup['basePath']; + observables: WorkspaceObservables; +}) => JSX.Element | null; + +type WorkspaceObject = WorkspaceAttribute & { readonly?: boolean }; + +export interface WorkspaceObservables { + currentWorkspaceId$: BehaviorSubject; + currentWorkspace$: BehaviorSubject; + workspaceList$: BehaviorSubject; + workspaceEnabled$: BehaviorSubject; + initialized$: BehaviorSubject; +} + +enum WORKSPACE_ERROR { + WORKSPACE_STALED = 'WORKSPACE_STALED', +} + +/** + * @public + */ +export interface WorkspacesSetup extends WorkspaceObservables { + registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => void; +} + +export interface WorkspacesStart extends WorkspaceObservables { + renderWorkspaceMenu: () => JSX.Element | null; +} + +export class WorkspacesService implements CoreService { + private currentWorkspaceId$ = new BehaviorSubject(''); + private workspaceList$ = new BehaviorSubject([]); + private currentWorkspace$ = new BehaviorSubject(null); + private initialized$ = new BehaviorSubject(false); + private workspaceEnabled$ = new BehaviorSubject(false); + private _renderWorkspaceMenu: WorkspaceMenuRenderFn | null = null; + + 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 staled + */ + this.currentWorkspaceId$.error({ + reason: WORKSPACE_ERROR.WORKSPACE_STALED, + }); + this.currentWorkspace$.error({ + reason: WORKSPACE_ERROR.WORKSPACE_STALED, + }); + } + } + } + ); + } + + public setup(): WorkspacesSetup { + return { + currentWorkspaceId$: this.currentWorkspaceId$, + currentWorkspace$: this.currentWorkspace$, + workspaceList$: this.workspaceList$, + initialized$: this.initialized$, + workspaceEnabled$: this.workspaceEnabled$, + registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => + (this._renderWorkspaceMenu = render), + }; + } + + public start({ + http, + application, + }: { + application: InternalApplicationStart; + http: HttpSetup; + }): WorkspacesStart { + const observables = { + currentWorkspaceId$: this.currentWorkspaceId$, + currentWorkspace$: this.currentWorkspace$, + workspaceList$: this.workspaceList$, + initialized$: this.initialized$, + workspaceEnabled$: this.workspaceEnabled$, + }; + return { + ...observables, + renderWorkspaceMenu: () => { + if (this._renderWorkspaceMenu) { + return this._renderWorkspaceMenu({ + basePath: http.basePath, + getUrlForApp: application.getUrlForApp, + observables, + }); + } + return null; + }, + }; + } + + public async stop() { + this.currentWorkspace$.unsubscribe(); + this.currentWorkspaceId$.unsubscribe(); + this.workspaceList$.unsubscribe(); + this.workspaceEnabled$.unsubscribe(); + this.initialized$.unsubscribe(); + this._renderWorkspaceMenu = null; + } +} 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 1b3c486615c8..d3d0fe94fb68 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 @@ -993,6 +993,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -2085,6 +2133,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -3238,6 +3334,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -4391,6 +4535,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -5544,6 +5736,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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 101e0e520304..0c48ffdc474a 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 @@ -859,6 +859,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -1776,6 +1824,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -2693,6 +2789,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -3610,6 +3754,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -4527,6 +4719,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > @@ -5444,6 +5684,54 @@ 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, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": 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, + }, + }, } } > From 5d1e1fcf96304e05af9158bb74619c27dc5723d1 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Fri, 22 Sep 2023 17:29:52 +0800 Subject: [PATCH 02/28] add unit tests for workspace core service (#191) Signed-off-by: Yulong Ruan --- src/core/public/core_system.test.mocks.ts | 9 ++ src/core/public/core_system.test.ts | 25 +++++ src/core/public/core_system.ts | 1 + src/core/public/mocks.ts | 2 +- .../public/plugins/plugins_service.test.ts | 2 +- .../workspace/workspaces_service.mock.ts | 13 ++- .../workspace/workspaces_service.test.ts | 94 +++++++++++++++++++ .../public/workspace/workspaces_service.ts | 6 +- 8 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 src/core/public/workspace/workspaces_service.test.ts 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..81dfa97b9afa 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 d7de8b4595d5..0a64e0f4fa9a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -311,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/mocks.ts b/src/core/public/mocks.ts index 722070d5a9ea..3acc71424b91 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.createSetupContractMock(), + 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 7e8c96c1f9d0..f26626ed1ca3 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/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index 3c35315aa850..3b1408b03045 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 000000000000..b2d84152da83 --- /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 39519ccdddbe..b5c8ff4cdba1 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; @@ -99,7 +99,7 @@ export class WorkspacesService implements CoreService Date: Fri, 22 Sep 2023 18:51:57 +0800 Subject: [PATCH 03/28] add missing type definition Signed-off-by: Yulong Ruan --- src/core/types/index.ts | 1 + src/core/types/workspace.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/core/types/workspace.ts diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 9f620273e3b2..4afe9c537f75 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -39,3 +39,4 @@ export * from './ui_settings'; export * from './saved_objects'; export * from './serializable'; export * from './custom_branding'; +export * from './workspace'; diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts new file mode 100644 index 000000000000..e99744183cac --- /dev/null +++ b/src/core/types/workspace.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface WorkspaceAttribute { + id: string; + name: string; + description?: string; + features?: string[]; + color?: string; + icon?: string; + defaultVISTheme?: string; + reserved?: boolean; +} From 26f38d65067e2396ba11d0c06ef73b1e8ec752b2 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Mon, 25 Sep 2023 17:18:09 +0800 Subject: [PATCH 04/28] add changelog for workspace service module Signed-off-by: Yulong Ruan --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 020ce3583eba..09bf2d826999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Theme] Use themes' definitions to render the initial view ([#4936](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4936)) - [Theme] Make `next` theme the default ([#4854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4854)) - [Discover] Update embeddable for saved searches ([#5081](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5081)) +- [Workspace] Add core workspace service module to enable the implementation of workspace features within OSD plugins ([#5092](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5092)) ### 🐛 Bug Fixes @@ -797,4 +798,4 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 🔩 Tests -- Update caniuse to fix failed integration tests ([#2322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2322)) +- Update caniuse to fix failed integration tests ([#2322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2322)) \ No newline at end of file From 088e165420ba974da814b6b9835685058ba55db2 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 26 Sep 2023 17:51:14 +0800 Subject: [PATCH 05/28] remove unnecessary workspace menu register Signed-off-by: Yulong Ruan --- src/core/public/core_system.test.ts | 4 -- src/core/public/core_system.ts | 2 +- .../workspace/workspaces_service.mock.ts | 2 - .../workspace/workspaces_service.test.ts | 22 +------- .../public/workspace/workspaces_service.ts | 51 ++----------------- .../dashboard_listing.test.tsx.snap | 5 -- .../dashboard_top_nav.test.tsx.snap | 6 --- 7 files changed, 7 insertions(+), 85 deletions(-) diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 81dfa97b9afa..24de9ea6b9ea 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -321,10 +321,6 @@ describe('#start()', () => { 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 () => { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 0a64e0f4fa9a..a9dba002e44c 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -225,7 +225,7 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, overlays }); - const workspaces = this.workspaces.start({ application, http }); + const workspaces = this.workspaces.start(); const chrome = await this.chrome.start({ application, docLinks, diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index 3b1408b03045..a8d2a91bd3d1 100644 --- a/src/core/public/workspace/workspaces_service.mock.ts +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -21,7 +21,6 @@ const createWorkspacesSetupContractMock = () => ({ currentWorkspace$, initialized$, workspaceEnabled$, - registerWorkspaceMenuRender: jest.fn(), }); const createWorkspacesStartContractMock = () => ({ @@ -30,7 +29,6 @@ const createWorkspacesStartContractMock = () => ({ currentWorkspace$, initialized$, workspaceEnabled$, - renderWorkspaceMenu: jest.fn(), }); export type WorkspacesServiceContract = PublicMethodsOf; diff --git a/src/core/public/workspace/workspaces_service.test.ts b/src/core/public/workspace/workspaces_service.test.ts index b2d84152da83..b8f9b17ae0c8 100644 --- a/src/core/public/workspace/workspaces_service.test.ts +++ b/src/core/public/workspace/workspaces_service.test.ts @@ -3,22 +3,15 @@ * 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'; +import { WorkspacesService, 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(), - }); + workspacesStart = workspaces.start(); }); afterEach(() => { @@ -42,17 +35,6 @@ describe('WorkspacesService', () => { 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); diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index b5c8ff4cdba1..f3d1400ce709 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -7,18 +7,6 @@ import { BehaviorSubject, combineLatest } from 'rxjs'; import { isEqual } from 'lodash'; import { CoreService, WorkspaceAttribute } from '../../types'; -import { InternalApplicationStart } from '../application'; -import { HttpStart } from '../http'; - -type WorkspaceMenuRenderFn = ({ - basePath, - getUrlForApp, - observables, -}: { - getUrlForApp: InternalApplicationStart['getUrlForApp']; - basePath: HttpStart['basePath']; - observables: WorkspaceObservables; -}) => JSX.Element | null; type WorkspaceObject = WorkspaceAttribute & { readonly?: boolean }; @@ -34,16 +22,8 @@ enum WORKSPACE_ERROR { WORKSPACE_STALED = 'WORKSPACE_STALED', } -/** - * @public - */ -export interface WorkspacesSetup extends WorkspaceObservables { - registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => void; -} - -export interface WorkspacesStart extends WorkspaceObservables { - renderWorkspaceMenu: () => JSX.Element | null; -} +export type WorkspacesSetup = WorkspaceObservables; +export type WorkspacesStart = WorkspaceObservables; export class WorkspacesService implements CoreService { private currentWorkspaceId$ = new BehaviorSubject(''); @@ -51,7 +31,6 @@ export class WorkspacesService implements CoreService(null); private initialized$ = new BehaviorSubject(false); private workspaceEnabled$ = new BehaviorSubject(false); - private _renderWorkspaceMenu: WorkspaceMenuRenderFn | null = null; constructor() { combineLatest([this.initialized$, this.workspaceList$, this.currentWorkspaceId$]).subscribe( @@ -89,38 +68,17 @@ export class WorkspacesService implements CoreService - (this._renderWorkspaceMenu = render), }; } - public start({ - http, - application, - }: { - application: InternalApplicationStart; - http: HttpStart; - }): WorkspacesStart { - const observables = { + public start(): WorkspacesStart { + return { currentWorkspaceId$: this.currentWorkspaceId$, currentWorkspace$: this.currentWorkspace$, workspaceList$: this.workspaceList$, initialized$: this.initialized$, workspaceEnabled$: this.workspaceEnabled$, }; - return { - ...observables, - renderWorkspaceMenu: () => { - if (this._renderWorkspaceMenu) { - return this._renderWorkspaceMenu({ - basePath: http.basePath, - getUrlForApp: application.getUrlForApp, - observables, - }); - } - return null; - }, - }; } public async stop() { @@ -129,6 +87,5 @@ export class WorkspacesService implements CoreService Date: Wed, 27 Sep 2023 09:26:15 +0800 Subject: [PATCH 06/28] Update test description per comment Co-authored-by: Miki Signed-off-by: Yulong Ruan Signed-off-by: Yulong Ruan --- src/core/public/workspace/workspaces_service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/workspace/workspaces_service.test.ts b/src/core/public/workspace/workspaces_service.test.ts index b8f9b17ae0c8..3bedc734d92c 100644 --- a/src/core/public/workspace/workspaces_service.test.ts +++ b/src/core/public/workspace/workspaces_service.test.ts @@ -35,7 +35,7 @@ describe('WorkspacesService', () => { expect(workspacesStart.workspaceList$.value.length).toBe(0); }); - it('the current workspace should also updated after changing current workspace id', () => { + it('currentWorkspace is updated when currentWorkspaceId changes', () => { expect(workspacesStart.currentWorkspace$.value).toBe(null); workspacesStart.initialized$.next(true); From 3bbeaa7c3647920dbf35cf7c3111a22ac222161a Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 28 Sep 2023 16:15:45 +0800 Subject: [PATCH 07/28] remove unnecessary workspace enabled flag from core workspace module Signed-off-by: Yulong Ruan --- .../workspace/workspaces_service.mock.ts | 3 -- .../workspace/workspaces_service.test.ts | 5 -- .../public/workspace/workspaces_service.ts | 5 -- src/core/types/capabilities.ts | 3 ++ .../dashboard_listing.test.tsx.snap | 45 ---------------- .../dashboard_top_nav.test.tsx.snap | 54 ------------------- 6 files changed, 3 insertions(+), 112 deletions(-) diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index a8d2a91bd3d1..ae56c035eb3a 100644 --- a/src/core/public/workspace/workspaces_service.mock.ts +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -13,14 +13,12 @@ const currentWorkspaceId$ = new BehaviorSubject(''); const workspaceList$ = new BehaviorSubject([]); const currentWorkspace$ = new BehaviorSubject(null); const initialized$ = new BehaviorSubject(false); -const workspaceEnabled$ = new BehaviorSubject(false); const createWorkspacesSetupContractMock = () => ({ currentWorkspaceId$, workspaceList$, currentWorkspace$, initialized$, - workspaceEnabled$, }); const createWorkspacesStartContractMock = () => ({ @@ -28,7 +26,6 @@ const createWorkspacesStartContractMock = () => ({ workspaceList$, currentWorkspace$, initialized$, - workspaceEnabled$, }); export type WorkspacesServiceContract = PublicMethodsOf; diff --git a/src/core/public/workspace/workspaces_service.test.ts b/src/core/public/workspace/workspaces_service.test.ts index 3bedc734d92c..4eca97ef2ed2 100644 --- a/src/core/public/workspace/workspaces_service.test.ts +++ b/src/core/public/workspace/workspaces_service.test.ts @@ -22,10 +22,6 @@ describe('WorkspacesService', () => { 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(''); @@ -70,7 +66,6 @@ describe('WorkspacesService', () => { 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 f3d1400ce709..f4cad977deef 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -14,7 +14,6 @@ export interface WorkspaceObservables { currentWorkspaceId$: BehaviorSubject; currentWorkspace$: BehaviorSubject; workspaceList$: BehaviorSubject; - workspaceEnabled$: BehaviorSubject; initialized$: BehaviorSubject; } @@ -30,7 +29,6 @@ export class WorkspacesService implements CoreService([]); private currentWorkspace$ = new BehaviorSubject(null); private initialized$ = new BehaviorSubject(false); - private workspaceEnabled$ = new BehaviorSubject(false); constructor() { combineLatest([this.initialized$, this.workspaceList$, this.currentWorkspaceId$]).subscribe( @@ -67,7 +65,6 @@ export class WorkspacesService implements CoreService; + /** Workspaces capabilities. */ + workspaces: Record; + /** Custom capabilities, registered by plugins. undefined if the key does not exist */ [key: string]: Record> | undefined; } 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 5d32f6a74c78..2a0173850a4d 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 @@ -1021,15 +1021,6 @@ exports[`dashboard listing hideWriteControls 1`] = ` "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -2160,15 +2151,6 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -3360,15 +3342,6 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -4560,15 +4533,6 @@ exports[`dashboard listing renders table rows 1`] = ` "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -5760,15 +5724,6 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], 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 0b877eb199c6..7a241fe344f5 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 @@ -887,15 +887,6 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -1851,15 +1842,6 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -2815,15 +2797,6 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -3779,15 +3752,6 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -4743,15 +4707,6 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -5707,15 +5662,6 @@ exports[`Dashboard top nav render with all components 1`] = ` "observers": Array [], "thrownError": null, }, - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, "workspaceList$": BehaviorSubject { "_isScalar": false, "_value": Array [], From 7f57bd2881f78a705df5e36fdb1962eddb866e12 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 28 Sep 2023 18:42:30 +0800 Subject: [PATCH 08/28] fix tests and add comments Signed-off-by: Yulong Ruan --- .../capabilities/capabilities_service.mock.ts | 1 + src/core/public/index.ts | 7 +----- src/core/public/workspace/index.ts | 8 ++----- .../public/workspace/workspaces_service.ts | 23 ++++++++++++++++++- .../capabilities/capabilities_service.mock.ts | 1 + .../capabilities/capabilities_service.ts | 1 + .../capabilities/resolve_capabilities.test.ts | 1 + 7 files changed, 29 insertions(+), 13 deletions(-) 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/index.ts b/src/core/public/index.ts index 14ab91e1cb13..2967b45f1d75 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -348,9 +348,4 @@ export { export { __osdBootstrap__ } from './osd_bootstrap'; -export { - WorkspacesStart, - WorkspacesSetup, - WorkspacesService, - WorkspaceObservables, -} from './workspace'; +export { WorkspacesStart, WorkspacesSetup, WorkspacesService } from './workspace'; diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index 4ef6aaae7fd4..4b9b2c86f649 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -2,9 +2,5 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -export { - WorkspacesStart, - WorkspacesService, - WorkspacesSetup, - WorkspaceObservables, -} from './workspaces_service'; + +export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service'; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index f4cad977deef..a7c62a76bec2 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -10,10 +10,31 @@ import { CoreService, WorkspaceAttribute } from '../../types'; type WorkspaceObject = WorkspaceAttribute & { readonly?: boolean }; -export interface WorkspaceObservables { +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 + * the workspace functionalities + */ 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; } 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.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/resolve_capabilities.test.ts b/src/core/server/capabilities/resolve_capabilities.test.ts index 25968d858e7a..b307929d2671 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(); }); From 22d5b30eeb56e9d355f6e0764143ae1aba08ac02 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 28 Sep 2023 22:38:52 +0800 Subject: [PATCH 09/28] update failed snapshot Signed-off-by: Yulong Ruan --- .../chrome/ui/header/__snapshots__/header.test.tsx.snap | 2 ++ src/core/server/capabilities/capabilities_service.test.ts | 4 ++++ src/core/server/capabilities/resolve_capabilities.test.ts | 1 + .../__snapshots__/dashboard_listing.test.tsx.snap | 5 +++++ .../__snapshots__/dashboard_top_nav.test.tsx.snap | 6 ++++++ 5 files changed, 18 insertions(+) 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 b9da5ac37dbe..d5903379b2eb 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, @@ -6756,6 +6757,7 @@ exports[`Header renders condensed header 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentActionMenu$": BehaviorSubject { "_isScalar": false, 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/resolve_capabilities.test.ts b/src/core/server/capabilities/resolve_capabilities.test.ts index b307929d2671..88e2aa7a4f7c 100644 --- a/src/core/server/capabilities/resolve_capabilities.test.ts +++ b/src/core/server/capabilities/resolve_capabilities.test.ts @@ -76,6 +76,7 @@ describe('resolveCapabilities', () => { }, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, } `); }); 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 2a0173850a4d..28265faad578 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, @@ -1234,6 +1235,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, @@ -2425,6 +2427,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, @@ -3616,6 +3619,7 @@ exports[`dashboard listing renders table rows 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, @@ -4807,6 +4811,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, 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 7a241fe344f5..7cb635c2f2c0 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, @@ -1059,6 +1060,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, @@ -2014,6 +2016,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, @@ -2969,6 +2972,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, @@ -3924,6 +3928,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, @@ -4879,6 +4884,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, From 8267820e93e61712f589633c8ddd2dc2abfd547e Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Thu, 28 Sep 2023 23:35:56 +0800 Subject: [PATCH 10/28] update failed snapshots Signed-off-by: Yulong Ruan --- .../capabilities/integration_tests/capabilities_service.test.ts | 2 ++ 1 file changed, 2 insertions(+) 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 {}, } `); }); From 8126c448a6225c902480e44bbc3e83c25e8b01d0 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 3 Oct 2023 11:19:47 -0700 Subject: [PATCH 11/28] [Discover] A bunch of navigation fixes (#5168) * Discover: Fixes state persistence after nav * Fixed breadcrumbs and navigation * fixes mobile view --------- Signed-off-by: Ashwin P Chandran --- .../public/application/components/sidebar/discover_field.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.scss b/src/plugins/discover/public/application/components/sidebar/discover_field.scss index 39cacdcd0c97..dbf42180acc2 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.scss @@ -17,4 +17,8 @@ &:focus &__actionButton { opacity: 1; } + + @include ouiBreakpoint("xs", "s", "m") { + opacity: 1; + } } From 3e4e3cc5c1c94642aa2681de33290fa84ce47379 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Thu, 29 Jun 2023 14:32:55 +0800 Subject: [PATCH 12/28] feat: make url stateful Signed-off-by: SuZhoue-Joe --- src/plugins/workspace/common/constants.ts | 15 ++ .../public/components/workspace_overview.tsx | 51 ++++++ .../workspace_updater/workspace_updater.tsx | 171 ++++++++++++++++++ src/plugins/workspace/public/plugin.ts | 126 +++++++++++++ 4 files changed, 363 insertions(+) create mode 100644 src/plugins/workspace/common/constants.ts create mode 100644 src/plugins/workspace/public/components/workspace_overview.tsx create mode 100644 src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx create mode 100644 src/plugins/workspace/public/plugin.ts diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts new file mode 100644 index 000000000000..4ec961e3578e --- /dev/null +++ b/src/plugins/workspace/common/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const WORKSPACE_APP_ID = 'workspace'; +export const WORKSPACE_APP_NAME = 'Workspace'; + +export const PATHS = { + create: '/create', + overview: '/overview', + update: '/update', +}; +export const WORKSPACE_OP_TYPE_CREATE = 'create'; +export const WORKSPACE_OP_TYPE_UPDATE = 'update'; diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx new file mode 100644 index 000000000000..e04856f25de1 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { useObservable } from 'react-use'; +import { of } from 'rxjs'; +import { i18n } from '@osd/i18n'; +import { ApplicationStart, WORKSPACE_ID_QUERYSTRING_NAME } from '../../../../core/public'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { PATHS } from '../../common/constants'; +import { WORKSPACE_APP_ID } from '../../common/constants'; + +export const WorkspaceOverview = () => { + const { + services: { workspaces, application, notifications }, + } = useOpenSearchDashboards<{ application: ApplicationStart }>(); + + const currentWorkspace = useObservable( + workspaces ? workspaces.client.currentWorkspace$ : of(null) + ); + + const onUpdateWorkspaceClick = () => { + if (!currentWorkspace || !currentWorkspace.id) { + notifications?.toasts.addDanger({ + title: i18n.translate('Cannot find current workspace', { + defaultMessage: 'Cannot update workspace', + }), + }); + return; + } + application.navigateToApp(WORKSPACE_APP_ID, { + path: PATHS.update + '?' + WORKSPACE_ID_QUERYSTRING_NAME + '=' + currentWorkspace.id, + }); + }; + + return ( + <> + + + +

Workspace

+
+ + {JSON.stringify(currentWorkspace)} +
+ + ); +}; diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx new file mode 100644 index 000000000000..7678ba532653 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -0,0 +1,171 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageContent, + EuiButton, + EuiPanel, +} from '@elastic/eui'; +import { useObservable } from 'react-use'; +import { i18n } from '@osd/i18n'; +import { of } from 'rxjs'; + +import { WorkspaceAttribute } from 'opensearch-dashboards/public'; +import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; + +import { PATHS } from '../../../common/constants'; +import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; +import { WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; +import { ApplicationStart, WORKSPACE_ID_QUERYSTRING_NAME } from '../../../../../core/public'; +import { DeleteWorkspaceModal } from '../delete_workspace_modal'; + +export const WorkspaceUpdater = () => { + const { + services: { application, workspaces, notifications }, + } = useOpenSearchDashboards<{ application: ApplicationStart }>(); + + const currentWorkspace = useObservable( + workspaces ? workspaces.client.currentWorkspace$ : of(null) + ); + + const excludedAttribute = 'id'; + const { [excludedAttribute]: removedProperty, ...otherAttributes } = + currentWorkspace || ({} as WorkspaceAttribute); + + const [deleteWorkspaceModalVisible, setDeleteWorkspaceModalVisible] = useState(false); + const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState< + Omit + >(otherAttributes); + + useEffect(() => { + const { id, ...others } = currentWorkspace || ({} as WorkspaceAttribute); + setCurrentWorkspaceFormData(others); + }, [workspaces, currentWorkspace, excludedAttribute]); + + const handleWorkspaceFormSubmit = useCallback( + async (data: WorkspaceFormData) => { + let result; + if (!currentWorkspace) { + notifications?.toasts.addDanger({ + title: i18n.translate('Cannot find current workspace', { + defaultMessage: 'Cannot update workspace', + }), + }); + return; + } + try { + result = await workspaces?.client.update(currentWorkspace?.id, data); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.update.failed', { + defaultMessage: 'Failed to update workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return; + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.update.success', { + defaultMessage: 'Update workspace successfully', + }), + }); + await application.navigateToApp(WORKSPACE_APP_ID, { + path: PATHS.overview + '?' + WORKSPACE_ID_QUERYSTRING_NAME + '=' + currentWorkspace.id, + }); + return; + } + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.update.failed', { + defaultMessage: 'Failed to update workspace', + }), + text: result?.error, + }); + }, + [notifications?.toasts, workspaces, currentWorkspace, application] + ); + + if (!currentWorkspaceFormData.name) { + return null; + } + const deleteWorkspace = async () => { + if (currentWorkspace?.id) { + let result; + try { + result = await workspaces?.client.delete(currentWorkspace?.id); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return setDeleteWorkspaceModalVisible(false); + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.delete.success', { + defaultMessage: 'Delete workspace successfully', + }), + }); + } else { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: result?.error, + }); + } + } + setDeleteWorkspaceModalVisible(false); + await application.navigateToApp('home'); + }; + + return ( + + + setDeleteWorkspaceModalVisible(true)}> + Delete + , + ]} + /> + + {deleteWorkspaceModalVisible && ( + + setDeleteWorkspaceModalVisible(false)} + selectedItems={currentWorkspace?.name ? [currentWorkspace.name] : []} + /> + + )} + {application && ( + + )} + + + + ); +}; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts new file mode 100644 index 000000000000..69e7afaa8078 --- /dev/null +++ b/src/plugins/workspace/public/plugin.ts @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { combineLatest } from 'rxjs'; +import { i18n } from '@osd/i18n'; +import { + CoreSetup, + CoreStart, + Plugin, + AppMountParameters, + AppNavLinkStatus, +} from '../../../core/public'; +import { WORKSPACE_APP_ID, PATHS } from '../common/constants'; +import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; +import { mountDropdownList } from './mount'; + +export class WorkspacesPlugin implements Plugin<{}, {}> { + private core?: CoreSetup; + private getWorkpsaceIdFromQueryString(): string | null { + const searchParams = new URLSearchParams(window.location.search); + return searchParams.get(WORKSPACE_ID_QUERYSTRING_NAME); + } + private async getWorkpsaceId(): Promise { + if (this.getWorkpsaceIdFromQueryString()) { + return this.getWorkpsaceIdFromQueryString() || ''; + } + + const currentWorkspaceIdResp = await this.core?.workspaces.client.getCurrentWorkspaceId(); + if (currentWorkspaceIdResp?.success && currentWorkspaceIdResp?.result) { + return currentWorkspaceIdResp.result; + } + + return ''; + } + private async listenToApplicationChange(): Promise { + const startService = await this.core?.getStartServices(); + if (startService) { + combineLatest([ + this.core?.workspaces.client.currentWorkspaceId$, + startService[0].application.currentAppId$, + ]).subscribe(async ([]) => { + const newUrl = new URL(window.location.href); + /** + * Patch workspace id into querystring + */ + const currentWorkspaceId = await this.getWorkpsaceId(); + if (currentWorkspaceId) { + newUrl.searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); + } else { + newUrl.searchParams.delete(WORKSPACE_ID_QUERYSTRING_NAME); + } + history.replaceState(history.state, '', newUrl.toString()); + }); + } + } + public async setup(core: CoreSetup) { + this.core = core; + /** + * Retrive workspace id from url or sessionstorage + * url > sessionstorage + */ + const workspaceId = await this.getWorkpsaceId(); + + if (workspaceId) { + const result = await core.workspaces.client.enterWorkspace(workspaceId); + if (!result.success) { + core.fatalErrors.add( + result.error || + i18n.translate('workspace.error.setup', { + defaultMessage: 'Workspace init failed', + }) + ); + } + } + + /** + * listen to application change and patch workspace id + */ + this.listenToApplicationChange(); + + core.application.register({ + id: WORKSPACE_APP_ID, + title: i18n.translate('workspace.settings.title', { + defaultMessage: 'Workspace', + }), + // order: 6010, + navLinkStatus: AppNavLinkStatus.hidden, + // updater$: this.appUpdater, + async mount(params: AppMountParameters) { + const { renderApp } = await import('./application'); + const [coreStart] = await core.getStartServices(); + const services = { + ...coreStart, + }; + + return renderApp(params, services); + }, + }); + + return {}; + } + + private async _changeSavedObjectCurrentWorkspace() { + const startServices = await this.core?.getStartServices(); + if (startServices) { + const coreStart = startServices[0]; + coreStart.workspaces.client.currentWorkspaceId$.subscribe((currentWorkspaceId) => { + coreStart.savedObjects.client.setCurrentWorkspace(currentWorkspaceId); + }); + } + } + + public start(core: CoreStart) { + mountDropdownList(core); + + core.chrome.setCustomNavLink({ + title: i18n.translate('workspace.nav.title', { defaultMessage: 'Workspace Overview' }), + baseUrl: core.http.basePath.get(), + href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.update }), + }); + this._changeSavedObjectCurrentWorkspace(); + return {}; + } +} From 70793dd7454809c70fba613ae942eba4ee228d08 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Thu, 29 Jun 2023 15:38:42 +0800 Subject: [PATCH 13/28] feat: optimize code Signed-off-by: SuZhoue-Joe --- src/plugins/workspace/public/plugin.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 69e7afaa8078..c44d37e071d9 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -58,10 +58,9 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { public async setup(core: CoreSetup) { this.core = core; /** - * Retrive workspace id from url or sessionstorage - * url > sessionstorage + * Retrive workspace id from url */ - const workspaceId = await this.getWorkpsaceId(); + const workspaceId = this.getWorkpsaceIdFromQueryString(); if (workspaceId) { const result = await core.workspaces.client.enterWorkspace(workspaceId); @@ -76,7 +75,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } /** - * listen to application change and patch workspace id + * listen to application change and patch workspace id in querystring */ this.listenToApplicationChange(); From 9871b30aeb785021940e58bbdccc31f32b65c4aa Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Thu, 29 Jun 2023 15:39:48 +0800 Subject: [PATCH 14/28] feat: remove useless change Signed-off-by: SuZhoue-Joe --- .../public/components/workspace_updater/workspace_updater.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index 7678ba532653..eb26cbf5aed3 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -88,7 +88,7 @@ export const WorkspaceUpdater = () => { text: result?.error, }); }, - [notifications?.toasts, workspaces, currentWorkspace, application] + [notifications?.toasts, workspaces?.client, currentWorkspace, application] ); if (!currentWorkspaceFormData.name) { From 3d1dfa3a4ecccfa59dfe414d374b7710205cd40c Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 3 Jul 2023 11:49:12 +0800 Subject: [PATCH 15/28] feat: optimize url listener Signed-off-by: SuZhoue-Joe --- .../public/components/utils/hash_url.ts | 42 ++++++++++ src/plugins/workspace/public/plugin.ts | 84 ++++++++++++++----- 2 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 src/plugins/workspace/public/components/utils/hash_url.ts diff --git a/src/plugins/workspace/public/components/utils/hash_url.ts b/src/plugins/workspace/public/components/utils/hash_url.ts new file mode 100644 index 000000000000..4c20cb0e90fc --- /dev/null +++ b/src/plugins/workspace/public/components/utils/hash_url.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This class extends URL and provides a hashSearchParams + * for convinience, and OSD is using an unstandard querystring + * in which the querystring won't be encoded. Thus we need to implement + * a map for CRUD of the search params instead of using URLSearchParams + */ +export class HashURL extends URL { + public get hashSearchParams(): Map { + const reg = /\?(.*)/; + const matchedResult = this.hash.match(reg); + const queryMap = new Map(); + const queryString = matchedResult ? matchedResult[1] : ''; + const params = queryString.split('&'); + for (const param of params) { + const [key, value] = param.split('='); + if (key && value) { + queryMap.set(key, value); + } + } + return queryMap; + } + public set hashSearchParams(searchParams: Map) { + const params: string[] = []; + + searchParams.forEach((value, key) => { + params.push(`${key}=${value}`); + }); + + const tempSearchValue = params.join('&'); + const tempHash = `${this.hash.replace(/^#/, '').replace(/\?.*/, '')}${ + tempSearchValue ? '?' : '' + }${tempSearchValue}`; + if (tempHash) { + this.hash = tempHash; + } + } +} diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index c44d37e071d9..6e3708a82250 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { combineLatest } from 'rxjs'; +import { BehaviorSubject, combineLatest } from 'rxjs'; import { i18n } from '@osd/i18n'; +import { debounce } from 'lodash'; import { CoreSetup, CoreStart, @@ -15,16 +16,18 @@ import { import { WORKSPACE_APP_ID, PATHS } from '../common/constants'; import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; import { mountDropdownList } from './mount'; +import { HashURL } from './components/utils/hash_url'; export class WorkspacesPlugin implements Plugin<{}, {}> { private core?: CoreSetup; - private getWorkpsaceIdFromQueryString(): string | null { - const searchParams = new URLSearchParams(window.location.search); - return searchParams.get(WORKSPACE_ID_QUERYSTRING_NAME); + private URLChange$ = new BehaviorSubject(''); + private getWorkpsaceIdFromURL(): string | null { + const hashUrl = new HashURL(window.location.href); + return hashUrl.hashSearchParams.get(WORKSPACE_ID_QUERYSTRING_NAME) || null; } private async getWorkpsaceId(): Promise { - if (this.getWorkpsaceIdFromQueryString()) { - return this.getWorkpsaceIdFromQueryString() || ''; + if (this.getWorkpsaceIdFromURL()) { + return this.getWorkpsaceIdFromURL() || ''; } const currentWorkspaceIdResp = await this.core?.workspaces.client.getCurrentWorkspaceId(); @@ -34,24 +37,56 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return ''; } + private async getPatchedUrl(url: string) { + const newUrl = new HashURL(url, window.location.href); + /** + * Patch workspace id into hash + */ + const currentWorkspaceId = await this.getWorkpsaceId(); + const searchParams = newUrl.hashSearchParams; + if (currentWorkspaceId) { + searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); + } else { + searchParams.delete(WORKSPACE_ID_QUERYSTRING_NAME); + } + + newUrl.hashSearchParams = searchParams; + + return newUrl.toString(); + } + private async listenToHashChange(): Promise { + window.addEventListener( + 'hashchange', + debounce(async (e) => { + if (this.shouldPatchUrl()) { + this.URLChange$.next(await this.getPatchedUrl(window.location.href)); + } + }, 150) + ); + } + private shouldPatchUrl(): boolean { + const currentWorkspaceId = this.core?.workspaces.client.currentWorkspaceId$.getValue(); + const workspaceIdFromURL = this.getWorkpsaceIdFromURL(); + if (!currentWorkspaceId && !workspaceIdFromURL) { + return false; + } + + if (currentWorkspaceId === workspaceIdFromURL) { + return false; + } + + return true; + } private async listenToApplicationChange(): Promise { const startService = await this.core?.getStartServices(); if (startService) { combineLatest([ this.core?.workspaces.client.currentWorkspaceId$, startService[0].application.currentAppId$, - ]).subscribe(async ([]) => { - const newUrl = new URL(window.location.href); - /** - * Patch workspace id into querystring - */ - const currentWorkspaceId = await this.getWorkpsaceId(); - if (currentWorkspaceId) { - newUrl.searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); - } else { - newUrl.searchParams.delete(WORKSPACE_ID_QUERYSTRING_NAME); + ]).subscribe(async ([currentWorkspaceId]) => { + if (this.shouldPatchUrl()) { + this.URLChange$.next(await this.getPatchedUrl(window.location.href)); } - history.replaceState(history.state, '', newUrl.toString()); }); } } @@ -60,7 +95,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { /** * Retrive workspace id from url */ - const workspaceId = this.getWorkpsaceIdFromQueryString(); + const workspaceId = this.getWorkpsaceIdFromURL(); if (workspaceId) { const result = await core.workspaces.client.enterWorkspace(workspaceId); @@ -75,10 +110,21 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } /** - * listen to application change and patch workspace id in querystring + * listen to application change and patch workspace id in hash */ this.listenToApplicationChange(); + /** + * listen to application internal hash change and patch workspace id in hash + */ + this.listenToHashChange(); + + this.URLChange$.subscribe( + debounce(async (url) => { + history.replaceState(history.state, '', url); + }, 500) + ); + core.application.register({ id: WORKSPACE_APP_ID, title: i18n.translate('workspace.settings.title', { From 2220febeac3accdf33e4ae28bae899b7df4b44d6 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 3 Jul 2023 12:34:25 +0800 Subject: [PATCH 16/28] feat: make formatUrlWithWorkspaceId extensible Signed-off-by: SuZhoue-Joe --- src/core/public/workspace/consts.ts | 10 ++++++++++ src/plugins/workspace/common/constants.ts | 2 ++ src/plugins/workspace/public/plugin.ts | 24 ++++++++++++++++++----- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 src/core/public/workspace/consts.ts diff --git a/src/core/public/workspace/consts.ts b/src/core/public/workspace/consts.ts new file mode 100644 index 000000000000..b02fa29f1013 --- /dev/null +++ b/src/core/public/workspace/consts.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +export enum WORKSPACE_ERROR_REASON_MAP { + WORKSPACE_STALED = 'WORKSPACE_STALED', +} diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 4ec961e3578e..7f79942a7d81 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -13,3 +13,5 @@ export const PATHS = { }; export const WORKSPACE_OP_TYPE_CREATE = 'create'; export const WORKSPACE_OP_TYPE_UPDATE = 'update'; + +export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 6e3708a82250..f2c12dd4d7b8 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -13,8 +13,7 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID, PATHS } from '../common/constants'; -import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; +import { WORKSPACE_APP_ID, PATHS, WORKSPACE_ID_QUERYSTRING_NAME } from '../common/constants'; import { mountDropdownList } from './mount'; import { HashURL } from './components/utils/hash_url'; @@ -37,12 +36,18 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return ''; } - private async getPatchedUrl(url: string) { + private getPatchedUrl = async ( + url: string, + workspaceId?: string, + options?: { + jumpable?: boolean; + } + ) => { const newUrl = new HashURL(url, window.location.href); /** * Patch workspace id into hash */ - const currentWorkspaceId = await this.getWorkpsaceId(); + const currentWorkspaceId = workspaceId || (await this.getWorkpsaceId()); const searchParams = newUrl.hashSearchParams; if (currentWorkspaceId) { searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); @@ -50,10 +55,18 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { searchParams.delete(WORKSPACE_ID_QUERYSTRING_NAME); } + if (options?.jumpable && currentWorkspaceId) { + /** + * When in hash, window.location.href won't make browser to reload + * append a querystring. + */ + newUrl.searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); + } + newUrl.hashSearchParams = searchParams; return newUrl.toString(); - } + }; private async listenToHashChange(): Promise { window.addEventListener( 'hashchange', @@ -92,6 +105,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } public async setup(core: CoreSetup) { this.core = core; + this.core?.workspaces.setFormatUrlWithWorkspaceId(this.getPatchedUrl); /** * Retrive workspace id from url */ From 643ebe125401dcea179f176690bdbf3feff8cb60 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 3 Jul 2023 12:35:34 +0800 Subject: [PATCH 17/28] feat: modify to related components Signed-off-by: SuZhoue-Joe --- .../workspace_creator/workspace_creator.tsx | 84 ++++++++++++++ .../public/components/workspace_overview.tsx | 19 +--- .../workspace_updater/workspace_updater.tsx | 15 ++- .../workspace_dropdown_list.tsx | 105 ++++++++++++++++++ 4 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx create mode 100644 src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx new file mode 100644 index 000000000000..b6d04fa3c7ba --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -0,0 +1,84 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; + +import { WorkspaceForm, WorkspaceFormData } from './workspace_form'; +import { PATHS, WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_CREATE } from '../../../common/constants'; + +export const WorkspaceCreator = () => { + const { + services: { application, workspaces, notifications }, + } = useOpenSearchDashboards(); + + const handleWorkspaceFormSubmit = useCallback( + async (data: WorkspaceFormData) => { + let result; + try { + result = await workspaces?.client.create(data); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.create.failed', { + defaultMessage: 'Failed to create workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return; + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.create.success', { + defaultMessage: 'Create workspace successfully', + }), + }); + if (application && workspaces) { + window.location.href = await workspaces.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.overview, + absolute: true, + }), + result.result.id + ); + } + return; + } + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.create.failed', { + defaultMessage: 'Failed to create workspace', + }), + text: result?.error, + }); + }, + [notifications?.toasts, workspaces, application] + ); + + return ( + + + + + {application && ( + + )} + + + + ); +}; diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx index e04856f25de1..55de87d20b66 100644 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -7,11 +7,8 @@ import React, { useState } from 'react'; import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { useObservable } from 'react-use'; import { of } from 'rxjs'; -import { i18n } from '@osd/i18n'; -import { ApplicationStart, WORKSPACE_ID_QUERYSTRING_NAME } from '../../../../core/public'; +import { ApplicationStart } from '../../../../core/public'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { PATHS } from '../../common/constants'; -import { WORKSPACE_APP_ID } from '../../common/constants'; export const WorkspaceOverview = () => { const { @@ -22,20 +19,6 @@ export const WorkspaceOverview = () => { workspaces ? workspaces.client.currentWorkspace$ : of(null) ); - const onUpdateWorkspaceClick = () => { - if (!currentWorkspace || !currentWorkspace.id) { - notifications?.toasts.addDanger({ - title: i18n.translate('Cannot find current workspace', { - defaultMessage: 'Cannot update workspace', - }), - }); - return; - } - application.navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.update + '?' + WORKSPACE_ID_QUERYSTRING_NAME + '=' + currentWorkspace.id, - }); - }; - return ( <> diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index eb26cbf5aed3..fa0b9e4912ad 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -22,7 +22,7 @@ import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearc import { PATHS } from '../../../common/constants'; import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; import { WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; -import { ApplicationStart, WORKSPACE_ID_QUERYSTRING_NAME } from '../../../../../core/public'; +import { ApplicationStart } from '../../../../../core/public'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; export const WorkspaceUpdater = () => { @@ -76,9 +76,14 @@ export const WorkspaceUpdater = () => { defaultMessage: 'Update workspace successfully', }), }); - await application.navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.overview + '?' + WORKSPACE_ID_QUERYSTRING_NAME + '=' + currentWorkspace.id, - }); + window.location.href = + (await workspaces?.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.overview, + absolute: true, + }), + currentWorkspace.id + )) || ''; return; } notifications?.toasts.addDanger({ @@ -88,7 +93,7 @@ export const WorkspaceUpdater = () => { text: result?.error, }); }, - [notifications?.toasts, workspaces?.client, currentWorkspace, application] + [notifications?.toasts, workspaces, currentWorkspace, application] ); if (!currentWorkspaceFormData.name) { diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx new file mode 100644 index 000000000000..a06f476ef01c --- /dev/null +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -0,0 +1,105 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useCallback, useMemo, useEffect } from 'react'; + +import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; +import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; +import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; + +type WorkspaceOption = EuiComboBoxOptionOption; + +interface WorkspaceDropdownListProps { + coreStart: CoreStart; +} + +function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { + return { label: workspace.name, key: workspace.id, value: workspace }; +} + +export function getErrorMessage(err: any) { + if (err && err.message) return err.message; + return ''; +} + +export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { + const { coreStart } = props; + + const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); + const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null); + + const [loading, setLoading] = useState(false); + const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]); + + const currentWorkspaceOption = useMemo(() => { + if (!currentWorkspace) { + return []; + } else { + return [workspaceToOption(currentWorkspace)]; + } + }, [currentWorkspace]); + const allWorkspaceOptions = useMemo(() => { + return workspaceList.map(workspaceToOption); + }, [workspaceList]); + + const onSearchChange = useCallback( + (searchValue: string) => { + setWorkspaceOptions(allWorkspaceOptions.filter((item) => item.label.includes(searchValue))); + }, + [allWorkspaceOptions] + ); + + const onChange = useCallback( + async (workspaceOption: WorkspaceOption[]) => { + /** switch the workspace */ + setLoading(true); + const id = workspaceOption[0].key!; + const newUrl = await coreStart.workspaces?.formatUrlWithWorkspaceId( + coreStart.application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.update, + absolute: true, + }), + id, + { + jumpable: true, + } + ); + if (newUrl) { + window.location.href = newUrl; + } + setLoading(false); + }, + [coreStart.workspaces, coreStart.application] + ); + + const onCreateWorkspaceClick = () => { + coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); + }; + + useEffect(() => { + onSearchChange(''); + }, [onSearchChange]); + + return ( + <> + + Create workspace + + } + /> + + ); +} From 7ee4201ebcedbc427f709482230de72ecb4b08d0 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 3 Jul 2023 14:08:39 +0800 Subject: [PATCH 18/28] feat: modify the async format to be sync function Signed-off-by: SuZhoue-Joe --- .../workspace_updater/workspace_updater.tsx | 4 ++-- .../workspace_dropdown_list.tsx | 4 ++-- src/plugins/workspace/public/plugin.ts | 18 +++++++++++------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index fa0b9e4912ad..a3dc973ee095 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -77,13 +77,13 @@ export const WorkspaceUpdater = () => { }), }); window.location.href = - (await workspaces?.formatUrlWithWorkspaceId( + workspaces?.formatUrlWithWorkspaceId( application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.overview, absolute: true, }), currentWorkspace.id - )) || ''; + ) || ''; return; } notifications?.toasts.addDanger({ diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index a06f476ef01c..b64c7bc34c16 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -53,11 +53,11 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { ); const onChange = useCallback( - async (workspaceOption: WorkspaceOption[]) => { + (workspaceOption: WorkspaceOption[]) => { /** switch the workspace */ setLoading(true); const id = workspaceOption[0].key!; - const newUrl = await coreStart.workspaces?.formatUrlWithWorkspaceId( + const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId( coreStart.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.update, absolute: true, diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index f2c12dd4d7b8..86023a0ff5f4 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -36,9 +36,9 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return ''; } - private getPatchedUrl = async ( + private getPatchedUrl = ( url: string, - workspaceId?: string, + workspaceId: string, options?: { jumpable?: boolean; } @@ -47,7 +47,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { /** * Patch workspace id into hash */ - const currentWorkspaceId = workspaceId || (await this.getWorkpsaceId()); + const currentWorkspaceId = workspaceId; const searchParams = newUrl.hashSearchParams; if (currentWorkspaceId) { searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); @@ -72,7 +72,8 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { 'hashchange', debounce(async (e) => { if (this.shouldPatchUrl()) { - this.URLChange$.next(await this.getPatchedUrl(window.location.href)); + const workspaceId = await this.getWorkpsaceId(); + this.URLChange$.next(this.getPatchedUrl(window.location.href, workspaceId)); } }, 150) ); @@ -96,16 +97,19 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { combineLatest([ this.core?.workspaces.client.currentWorkspaceId$, startService[0].application.currentAppId$, - ]).subscribe(async ([currentWorkspaceId]) => { + ]).subscribe(async ([]) => { if (this.shouldPatchUrl()) { - this.URLChange$.next(await this.getPatchedUrl(window.location.href)); + const currentWorkspaceId = await this.getWorkpsaceId(); + this.URLChange$.next(this.getPatchedUrl(window.location.href, currentWorkspaceId)); } }); } } public async setup(core: CoreSetup) { this.core = core; - this.core?.workspaces.setFormatUrlWithWorkspaceId(this.getPatchedUrl); + this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id, options) => + this.getPatchedUrl(url, id, options) + ); /** * Retrive workspace id from url */ From ed3e23b799ef687f8c3953b09b301c65d4e46dfd Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 3 Jul 2023 14:09:52 +0800 Subject: [PATCH 19/28] feat: modify the async format to be sync function Signed-off-by: SuZhoue-Joe --- .../public/components/workspace_creator/workspace_creator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index b6d04fa3c7ba..bbea2d2aa0a2 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -38,7 +38,7 @@ export const WorkspaceCreator = () => { }), }); if (application && workspaces) { - window.location.href = await workspaces.formatUrlWithWorkspaceId( + window.location.href = workspaces.formatUrlWithWorkspaceId( application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.overview, absolute: true, From 73d595c206e9aa5144fb795a41478039111db628 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Sat, 7 Oct 2023 14:37:43 +0800 Subject: [PATCH 20/28] feat: some update Signed-off-by: SuZhou-Joe --- src/plugins/workspace/public/plugin.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 86023a0ff5f4..cbf4c90d14c0 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -14,7 +14,6 @@ import { AppNavLinkStatus, } from '../../../core/public'; import { WORKSPACE_APP_ID, PATHS, WORKSPACE_ID_QUERYSTRING_NAME } from '../common/constants'; -import { mountDropdownList } from './mount'; import { HashURL } from './components/utils/hash_url'; export class WorkspacesPlugin implements Plugin<{}, {}> { @@ -176,8 +175,6 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } public start(core: CoreStart) { - mountDropdownList(core); - core.chrome.setCustomNavLink({ title: i18n.translate('workspace.nav.title', { defaultMessage: 'Workspace Overview' }), baseUrl: core.http.basePath.get(), From 05fda74c423410419d515999db2ae6aed02e4f20 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Sat, 7 Oct 2023 15:07:48 +0800 Subject: [PATCH 21/28] feat: remove useless code Signed-off-by: SuZhou-Joe --- src/plugins/workspace/public/plugin.ts | 65 ++++---------------------- 1 file changed, 8 insertions(+), 57 deletions(-) diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index cbf4c90d14c0..87dbfec83e99 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -6,13 +6,7 @@ import { BehaviorSubject, combineLatest } from 'rxjs'; import { i18n } from '@osd/i18n'; import { debounce } from 'lodash'; -import { - CoreSetup, - CoreStart, - Plugin, - AppMountParameters, - AppNavLinkStatus, -} from '../../../core/public'; +import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { WORKSPACE_APP_ID, PATHS, WORKSPACE_ID_QUERYSTRING_NAME } from '../common/constants'; import { HashURL } from './components/utils/hash_url'; @@ -28,12 +22,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return this.getWorkpsaceIdFromURL() || ''; } - const currentWorkspaceIdResp = await this.core?.workspaces.client.getCurrentWorkspaceId(); - if (currentWorkspaceIdResp?.success && currentWorkspaceIdResp?.result) { - return currentWorkspaceIdResp.result; - } - - return ''; + return (await this.core?.workspaces.currentWorkspaceId$.getValue()) || ''; } private getPatchedUrl = ( url: string, @@ -78,7 +67,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { ); } private shouldPatchUrl(): boolean { - const currentWorkspaceId = this.core?.workspaces.client.currentWorkspaceId$.getValue(); + const currentWorkspaceId = this.core?.workspaces.currentWorkspaceId$.getValue(); const workspaceIdFromURL = this.getWorkpsaceIdFromURL(); if (!currentWorkspaceId && !workspaceIdFromURL) { return false; @@ -94,7 +83,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { const startService = await this.core?.getStartServices(); if (startService) { combineLatest([ - this.core?.workspaces.client.currentWorkspaceId$, + this.core?.workspaces.currentWorkspaceId$, startService[0].application.currentAppId$, ]).subscribe(async ([]) => { if (this.shouldPatchUrl()) { @@ -106,24 +95,16 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } public async setup(core: CoreSetup) { this.core = core; - this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id, options) => - this.getPatchedUrl(url, id, options) - ); /** * Retrive workspace id from url */ const workspaceId = this.getWorkpsaceIdFromURL(); if (workspaceId) { - const result = await core.workspaces.client.enterWorkspace(workspaceId); - if (!result.success) { - core.fatalErrors.add( - result.error || - i18n.translate('workspace.error.setup', { - defaultMessage: 'Workspace init failed', - }) - ); - } + /** + * Enter a workspace + */ + this.core.workspaces.currentWorkspaceId$.next(workspaceId); } /** @@ -142,45 +123,15 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { }, 500) ); - core.application.register({ - id: WORKSPACE_APP_ID, - title: i18n.translate('workspace.settings.title', { - defaultMessage: 'Workspace', - }), - // order: 6010, - navLinkStatus: AppNavLinkStatus.hidden, - // updater$: this.appUpdater, - async mount(params: AppMountParameters) { - const { renderApp } = await import('./application'); - const [coreStart] = await core.getStartServices(); - const services = { - ...coreStart, - }; - - return renderApp(params, services); - }, - }); - return {}; } - private async _changeSavedObjectCurrentWorkspace() { - const startServices = await this.core?.getStartServices(); - if (startServices) { - const coreStart = startServices[0]; - coreStart.workspaces.client.currentWorkspaceId$.subscribe((currentWorkspaceId) => { - coreStart.savedObjects.client.setCurrentWorkspace(currentWorkspaceId); - }); - } - } - public start(core: CoreStart) { core.chrome.setCustomNavLink({ title: i18n.translate('workspace.nav.title', { defaultMessage: 'Workspace Overview' }), baseUrl: core.http.basePath.get(), href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.update }), }); - this._changeSavedObjectCurrentWorkspace(); return {}; } } From 3ba70da0bcaa68c3d517977b0b77650f345ffcfd Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 11:11:01 +0800 Subject: [PATCH 22/28] feat: remove useless code Signed-off-by: SuZhou-Joe --- .../components/sidebar/discover_field.scss | 4 - src/plugins/workspace/common/constants.ts | 11 -- .../workspace_creator/workspace_creator.tsx | 84 --------- .../public/components/workspace_overview.tsx | 34 ---- .../workspace_updater/workspace_updater.tsx | 176 ------------------ .../workspace_dropdown_list.tsx | 105 ----------- 6 files changed, 414 deletions(-) delete mode 100644 src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx delete mode 100644 src/plugins/workspace/public/components/workspace_overview.tsx delete mode 100644 src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx delete mode 100644 src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.scss b/src/plugins/discover/public/application/components/sidebar/discover_field.scss index 665a2846bd5d..e869707ebcd3 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.scss @@ -18,8 +18,4 @@ .dscSidebarField__actionButton:focus { opacity: 1; } - - @include ouiBreakpoint("xs", "s", "m") { - opacity: 1; - } } diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index e6d1b39eae43..cd506d86c8ca 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -3,16 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export const WORKSPACE_APP_ID = 'workspace'; -export const WORKSPACE_APP_NAME = 'Workspace'; - -export const PATHS = { - create: '/create', - overview: '/overview', - update: '/update', -}; -export const WORKSPACE_OP_TYPE_CREATE = 'create'; -export const WORKSPACE_OP_TYPE_UPDATE = 'update'; - export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx deleted file mode 100644 index bbea2d2aa0a2..000000000000 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback } from 'react'; -import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; -import { i18n } from '@osd/i18n'; - -import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; - -import { WorkspaceForm, WorkspaceFormData } from './workspace_form'; -import { PATHS, WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_CREATE } from '../../../common/constants'; - -export const WorkspaceCreator = () => { - const { - services: { application, workspaces, notifications }, - } = useOpenSearchDashboards(); - - const handleWorkspaceFormSubmit = useCallback( - async (data: WorkspaceFormData) => { - let result; - try { - result = await workspaces?.client.create(data); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.create.failed', { - defaultMessage: 'Failed to create workspace', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - return; - } - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.create.success', { - defaultMessage: 'Create workspace successfully', - }), - }); - if (application && workspaces) { - window.location.href = workspaces.formatUrlWithWorkspaceId( - application.getUrlForApp(WORKSPACE_APP_ID, { - path: PATHS.overview, - absolute: true, - }), - result.result.id - ); - } - return; - } - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.create.failed', { - defaultMessage: 'Failed to create workspace', - }), - text: result?.error, - }); - }, - [notifications?.toasts, workspaces, application] - ); - - return ( - - - - - {application && ( - - )} - - - - ); -}; diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx deleted file mode 100644 index 55de87d20b66..000000000000 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from 'react'; -import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { useObservable } from 'react-use'; -import { of } from 'rxjs'; -import { ApplicationStart } from '../../../../core/public'; -import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; - -export const WorkspaceOverview = () => { - const { - services: { workspaces, application, notifications }, - } = useOpenSearchDashboards<{ application: ApplicationStart }>(); - - const currentWorkspace = useObservable( - workspaces ? workspaces.client.currentWorkspace$ : of(null) - ); - - return ( - <> - - - -

Workspace

-
- - {JSON.stringify(currentWorkspace)} -
- - ); -}; diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx deleted file mode 100644 index a3dc973ee095..000000000000 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useState } from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageContent, - EuiButton, - EuiPanel, -} from '@elastic/eui'; -import { useObservable } from 'react-use'; -import { i18n } from '@osd/i18n'; -import { of } from 'rxjs'; - -import { WorkspaceAttribute } from 'opensearch-dashboards/public'; -import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; - -import { PATHS } from '../../../common/constants'; -import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; -import { WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; -import { ApplicationStart } from '../../../../../core/public'; -import { DeleteWorkspaceModal } from '../delete_workspace_modal'; - -export const WorkspaceUpdater = () => { - const { - services: { application, workspaces, notifications }, - } = useOpenSearchDashboards<{ application: ApplicationStart }>(); - - const currentWorkspace = useObservable( - workspaces ? workspaces.client.currentWorkspace$ : of(null) - ); - - const excludedAttribute = 'id'; - const { [excludedAttribute]: removedProperty, ...otherAttributes } = - currentWorkspace || ({} as WorkspaceAttribute); - - const [deleteWorkspaceModalVisible, setDeleteWorkspaceModalVisible] = useState(false); - const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState< - Omit - >(otherAttributes); - - useEffect(() => { - const { id, ...others } = currentWorkspace || ({} as WorkspaceAttribute); - setCurrentWorkspaceFormData(others); - }, [workspaces, currentWorkspace, excludedAttribute]); - - const handleWorkspaceFormSubmit = useCallback( - async (data: WorkspaceFormData) => { - let result; - if (!currentWorkspace) { - notifications?.toasts.addDanger({ - title: i18n.translate('Cannot find current workspace', { - defaultMessage: 'Cannot update workspace', - }), - }); - return; - } - try { - result = await workspaces?.client.update(currentWorkspace?.id, data); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.update.failed', { - defaultMessage: 'Failed to update workspace', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - return; - } - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.update.success', { - defaultMessage: 'Update workspace successfully', - }), - }); - window.location.href = - workspaces?.formatUrlWithWorkspaceId( - application.getUrlForApp(WORKSPACE_APP_ID, { - path: PATHS.overview, - absolute: true, - }), - currentWorkspace.id - ) || ''; - return; - } - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.update.failed', { - defaultMessage: 'Failed to update workspace', - }), - text: result?.error, - }); - }, - [notifications?.toasts, workspaces, currentWorkspace, application] - ); - - if (!currentWorkspaceFormData.name) { - return null; - } - const deleteWorkspace = async () => { - if (currentWorkspace?.id) { - let result; - try { - result = await workspaces?.client.delete(currentWorkspace?.id); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.delete.failed', { - defaultMessage: 'Failed to delete workspace', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - return setDeleteWorkspaceModalVisible(false); - } - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.delete.success', { - defaultMessage: 'Delete workspace successfully', - }), - }); - } else { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.delete.failed', { - defaultMessage: 'Failed to delete workspace', - }), - text: result?.error, - }); - } - } - setDeleteWorkspaceModalVisible(false); - await application.navigateToApp('home'); - }; - - return ( - - - setDeleteWorkspaceModalVisible(true)}> - Delete - , - ]} - /> - - {deleteWorkspaceModalVisible && ( - - setDeleteWorkspaceModalVisible(false)} - selectedItems={currentWorkspace?.name ? [currentWorkspace.name] : []} - /> - - )} - {application && ( - - )} - - - - ); -}; diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx deleted file mode 100644 index b64c7bc34c16..000000000000 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState, useCallback, useMemo, useEffect } from 'react'; - -import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import useObservable from 'react-use/lib/useObservable'; -import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; -import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; - -type WorkspaceOption = EuiComboBoxOptionOption; - -interface WorkspaceDropdownListProps { - coreStart: CoreStart; -} - -function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { - return { label: workspace.name, key: workspace.id, value: workspace }; -} - -export function getErrorMessage(err: any) { - if (err && err.message) return err.message; - return ''; -} - -export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { - const { coreStart } = props; - - const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); - const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null); - - const [loading, setLoading] = useState(false); - const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]); - - const currentWorkspaceOption = useMemo(() => { - if (!currentWorkspace) { - return []; - } else { - return [workspaceToOption(currentWorkspace)]; - } - }, [currentWorkspace]); - const allWorkspaceOptions = useMemo(() => { - return workspaceList.map(workspaceToOption); - }, [workspaceList]); - - const onSearchChange = useCallback( - (searchValue: string) => { - setWorkspaceOptions(allWorkspaceOptions.filter((item) => item.label.includes(searchValue))); - }, - [allWorkspaceOptions] - ); - - const onChange = useCallback( - (workspaceOption: WorkspaceOption[]) => { - /** switch the workspace */ - setLoading(true); - const id = workspaceOption[0].key!; - const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId( - coreStart.application.getUrlForApp(WORKSPACE_APP_ID, { - path: PATHS.update, - absolute: true, - }), - id, - { - jumpable: true, - } - ); - if (newUrl) { - window.location.href = newUrl; - } - setLoading(false); - }, - [coreStart.workspaces, coreStart.application] - ); - - const onCreateWorkspaceClick = () => { - coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); - }; - - useEffect(() => { - onSearchChange(''); - }, [onSearchChange]); - - return ( - <> - - Create workspace - - } - /> - - ); -} From 66df5e8f63d6044f5da10db446cf179268cdf927 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 11:14:07 +0800 Subject: [PATCH 23/28] feat: some update Signed-off-by: SuZhou-Joe --- src/plugins/workspace/public/plugin.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index b65654bcdc17..9f60c6d446ff 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -128,4 +128,6 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { public start() { return {}; } + + public stop() {} } From 803dd11ed33c663f81ceb5e08820d36b69799188 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 11:21:03 +0800 Subject: [PATCH 24/28] feat: remove useless code Signed-off-by: SuZhou-Joe --- src/core/public/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 2bae33cdb236..4e889ff82e6a 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -351,4 +351,4 @@ export { export { __osdBootstrap__ } from './osd_bootstrap'; -export { WorkspacesStart, WorkspacesSetup, WorkspacesService } from './workspace'; +export { WorkspacesStart, WorkspacesSetup } from './workspace'; From bc1a81b410611fa0e0d4a733fba324d180b34785 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 11:21:35 +0800 Subject: [PATCH 25/28] feat: remove useless code Signed-off-by: SuZhou-Joe --- src/core/public/workspace/consts.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/core/public/workspace/consts.ts diff --git a/src/core/public/workspace/consts.ts b/src/core/public/workspace/consts.ts deleted file mode 100644 index b02fa29f1013..000000000000 --- a/src/core/public/workspace/consts.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const WORKSPACES_API_BASE_URL = '/api/workspaces'; - -export enum WORKSPACE_ERROR_REASON_MAP { - WORKSPACE_STALED = 'WORKSPACE_STALED', -} From 38a0c8e4ec091d747f5ba215308b78565ecbaf79 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 12:58:52 +0800 Subject: [PATCH 26/28] feat: enable url in hash Signed-off-by: SuZhou-Joe --- src/plugins/workspace/common/constants.ts | 2 +- .../workspace/opensearch_dashboards.json | 5 +- src/plugins/workspace/public/plugin.ts | 60 +++++-------------- src/plugins/workspace/public/utils.ts | 11 ++++ 4 files changed, 31 insertions(+), 47 deletions(-) create mode 100644 src/plugins/workspace/public/utils.ts diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index cd506d86c8ca..e5cace442a83 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; +export const WORKSPACE_ID_STATE_KEY = '_w'; export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index 40a7eb5c3f9f..176bf5c2d132 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -2,9 +2,10 @@ "id": "workspace", "version": "opensearchDashboards", "server": true, - "ui": false, + "ui": true, "requiredPlugins": [ - "savedObjects" + "savedObjects", + "opensearchDashboardsUtils" ], "optionalPlugins": [], "requiredBundles": [] diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 9f60c6d446ff..29c69bf5511f 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -6,15 +6,15 @@ import { BehaviorSubject, combineLatest } from 'rxjs'; import { debounce } from 'lodash'; import { CoreSetup, Plugin } from '../../../core/public'; -import { WORKSPACE_ID_QUERYSTRING_NAME } from '../common/constants'; -import { HashURL } from './components/utils/hash_url'; +import { getStateFromOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { formatUrlWithWorkspaceId } from './utils'; +import { WORKSPACE_ID_STATE_KEY } from '../common/constants'; -export class WorkspacesPlugin implements Plugin<{}, {}> { +export class WorkspacePlugin implements Plugin<{}, {}> { private core?: CoreSetup; private URLChange$ = new BehaviorSubject(''); private getWorkpsaceIdFromURL(): string | null { - const hashUrl = new HashURL(window.location.href); - return hashUrl.hashSearchParams.get(WORKSPACE_ID_QUERYSTRING_NAME) || null; + return getStateFromOsdUrl(WORKSPACE_ID_STATE_KEY); } private async getWorkpsaceId(): Promise { if (this.getWorkpsaceIdFromURL()) { @@ -23,47 +23,16 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return (await this.core?.workspaces.currentWorkspaceId$.getValue()) || ''; } - private getPatchedUrl = ( - url: string, - workspaceId: string, - options?: { - jumpable?: boolean; - } - ) => { - const newUrl = new HashURL(url, window.location.href); - /** - * Patch workspace id into hash - */ - const currentWorkspaceId = workspaceId; - const searchParams = newUrl.hashSearchParams; - if (currentWorkspaceId) { - searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); - } else { - searchParams.delete(WORKSPACE_ID_QUERYSTRING_NAME); - } - - if (options?.jumpable && currentWorkspaceId) { - /** - * When in hash, window.location.href won't make browser to reload - * append a querystring. - */ - newUrl.searchParams.set(WORKSPACE_ID_QUERYSTRING_NAME, currentWorkspaceId); - } - - newUrl.hashSearchParams = searchParams; - - return newUrl.toString(); + private getPatchedUrl = (url: string, workspaceId: string) => { + return formatUrlWithWorkspaceId(url, workspaceId); }; private async listenToHashChange(): Promise { - window.addEventListener( - 'hashchange', - debounce(async (e) => { - if (this.shouldPatchUrl()) { - const workspaceId = await this.getWorkpsaceId(); - this.URLChange$.next(this.getPatchedUrl(window.location.href, workspaceId)); - } - }, 150) - ); + window.addEventListener('hashchange', async () => { + if (this.shouldPatchUrl()) { + const workspaceId = await this.getWorkpsaceId(); + this.URLChange$.next(this.getPatchedUrl(window.location.href, workspaceId)); + } + }); } private shouldPatchUrl(): boolean { const currentWorkspaceId = this.core?.workspaces.currentWorkspaceId$.getValue(); @@ -116,6 +85,9 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { */ this.listenToHashChange(); + /** + * All the URLChange will flush in this subscriber + */ this.URLChange$.subscribe( debounce(async (url) => { history.replaceState(history.state, '', url); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts new file mode 100644 index 000000000000..4443e450a3cd --- /dev/null +++ b/src/plugins/workspace/public/utils.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { setStateToOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { WORKSPACE_ID_STATE_KEY } from '../common/constants'; + +export const formatUrlWithWorkspaceId = (url: string, workspaceId: string) => { + return setStateToOsdUrl(WORKSPACE_ID_STATE_KEY, workspaceId, undefined, url); +}; From e1bbc94bb6477d4790cea6f47f9b4f96d714db05 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 13:25:56 +0800 Subject: [PATCH 27/28] feat: remove useless code Signed-off-by: SuZhou-Joe --- .../public/components/utils/hash_url.ts | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 src/plugins/workspace/public/components/utils/hash_url.ts diff --git a/src/plugins/workspace/public/components/utils/hash_url.ts b/src/plugins/workspace/public/components/utils/hash_url.ts deleted file mode 100644 index 4c20cb0e90fc..000000000000 --- a/src/plugins/workspace/public/components/utils/hash_url.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * This class extends URL and provides a hashSearchParams - * for convinience, and OSD is using an unstandard querystring - * in which the querystring won't be encoded. Thus we need to implement - * a map for CRUD of the search params instead of using URLSearchParams - */ -export class HashURL extends URL { - public get hashSearchParams(): Map { - const reg = /\?(.*)/; - const matchedResult = this.hash.match(reg); - const queryMap = new Map(); - const queryString = matchedResult ? matchedResult[1] : ''; - const params = queryString.split('&'); - for (const param of params) { - const [key, value] = param.split('='); - if (key && value) { - queryMap.set(key, value); - } - } - return queryMap; - } - public set hashSearchParams(searchParams: Map) { - const params: string[] = []; - - searchParams.forEach((value, key) => { - params.push(`${key}=${value}`); - }); - - const tempSearchValue = params.join('&'); - const tempHash = `${this.hash.replace(/^#/, '').replace(/\?.*/, '')}${ - tempSearchValue ? '?' : '' - }${tempSearchValue}`; - if (tempHash) { - this.hash = tempHash; - } - } -} From 476812d604017cd0cddef9607ae90968238a926c Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 29 Feb 2024 13:37:46 +0800 Subject: [PATCH 28/28] feat: remove useless code Signed-off-by: SuZhou-Joe --- src/plugins/workspace/public/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 29c69bf5511f..1fb4cd0f4046 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -10,7 +10,7 @@ import { getStateFromOsdUrl } from '../../opensearch_dashboards_utils/public'; import { formatUrlWithWorkspaceId } from './utils'; import { WORKSPACE_ID_STATE_KEY } from '../common/constants'; -export class WorkspacePlugin implements Plugin<{}, {}> { +export class WorkspacePlugin implements Plugin<{}, {}, {}> { private core?: CoreSetup; private URLChange$ = new BehaviorSubject(''); private getWorkpsaceIdFromURL(): string | null {