From ebc645108b6fabb9c5918322a8e06ab6e026212a Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Fri, 14 Jul 2023 14:57:50 +0800 Subject: [PATCH] allow user to turn on/off workspace from advance settings (#46) return 404 if accessing a workspace path when workspace is disabled --------- Signed-off-by: Yulong Ruan --- src/core/public/chrome/chrome_service.tsx | 4 +- src/core/public/core_app/core_app.ts | 2 - src/core/public/core_system.ts | 6 +-- src/core/public/http/types.ts | 5 ++ .../ui_settings/ui_settings_service.mock.ts | 9 ++-- .../public/workspace/workspaces_client.ts | 8 +-- .../public/workspace/workspaces_service.ts | 9 +++- src/core/server/server.ts | 1 + .../server/ui_settings/settings/index.test.ts | 2 + src/core/server/ui_settings/settings/index.ts | 2 + .../server/ui_settings/settings/workspace.ts | 25 +++++++++ .../server/workspaces/workspaces_service.ts | 29 +++++++--- .../workspace_dropdown_list.tsx | 17 +++--- src/plugins/workspace/public/mount.tsx | 19 +++++-- src/plugins/workspace/public/plugin.ts | 53 +++++++++++++------ 15 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 src/core/server/ui_settings/settings/workspace.ts diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 3826864d7895..f6380ce3a850 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -183,7 +183,7 @@ export class ChromeService { }); const getWorkspaceUrl = (id: string) => { - return workspaces?.formatUrlWithWorkspaceId( + return workspaces.formatUrlWithWorkspaceId( application.getUrlForApp(WORKSPACE_APP_ID, { path: '/', absolute: true, @@ -195,7 +195,7 @@ export class ChromeService { const exitWorkspace = async () => { let result; try { - result = await workspaces?.client.exitWorkspace(); + result = await workspaces.client.exitWorkspace(); } catch (error) { notifications?.toasts.addDanger({ title: i18n.translate('workspace.exit.failed', { diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index c4d359d58dc1..fcbcc5de5655 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -42,14 +42,12 @@ import type { IUiSettingsClient } from '../ui_settings'; import type { InjectedMetadataSetup } from '../injected_metadata'; import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; import { renderApp as renderStatusApp } from './status'; -import { WorkspacesSetup } from '../workspace'; interface SetupDeps { application: InternalApplicationSetup; http: HttpSetup; injectedMetadata: InjectedMetadataSetup; notifications: NotificationsSetup; - workspaces: WorkspacesSetup; } interface StartDeps { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9512560112f7..1e756ddcf8c9 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -163,14 +163,14 @@ 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 = await this.workspaces.setup({ http }); + const workspaces = await this.workspaces.setup({ http, uiSettings }); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ pluginDependencies: new Map([...pluginDependencies]), }); const application = this.application.setup({ context, http }); - this.coreApp.setup({ application, http, injectedMetadata, notifications, workspaces }); + this.coreApp.setup({ application, http, injectedMetadata, notifications }); const core: InternalCoreSetup = { application, @@ -204,7 +204,6 @@ export class CoreSystem { const uiSettings = await this.uiSettings.start(); const docLinks = this.docLinks.start({ injectedMetadata }); const http = await this.http.start(); - const workspaces = await this.workspaces.start(); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const fatalErrors = await this.fatalErrors.start(); @@ -226,6 +225,7 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, overlays }); + const workspaces = await this.workspaces.start(); const chrome = await this.chrome.start({ application, docLinks, diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 9a466a1519c7..4c81dbdd7a5f 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -97,6 +97,11 @@ export interface IBasePath { */ get: () => string; + /** + * Gets the `basePath + */ + getBasePath: () => string; + /** * Prepends `path` with the basePath + workspace. */ diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index 8458c86d6774..2d9cead9682b 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -33,7 +33,7 @@ import type { PublicMethodsOf } from '@osd/utility-types'; import { UiSettingsService } from './'; import { IUiSettingsClient } from './types'; -const createSetupContractMock = () => { +const createUiSettingsClientMock = () => { const setupContract: jest.Mocked = { getAll: jest.fn(), get: jest.fn(), @@ -66,12 +66,13 @@ const createMock = () => { stop: jest.fn(), }; - mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.setup.mockReturnValue(createUiSettingsClientMock()); + mocked.start.mockReturnValue(createUiSettingsClientMock()); return mocked; }; export const uiSettingsServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, - createStartContract: createSetupContractMock, + createSetupContract: createUiSettingsClientMock, + createStartContract: createUiSettingsClientMock, }; diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index f37fd89ae249..ac909b62deeb 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -70,10 +70,12 @@ export class WorkspacesClient { } } ); + } - /** - * Initialize workspace list - */ + /** + * Initialize workspace list + */ + init() { this.updateWorkspaceListAndNotify(); } diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index 7d30ac52f49f..cb82f3c44406 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -6,6 +6,7 @@ import { CoreService } from 'src/core/types'; import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; import type { WorkspaceAttribute } from '../../server/types'; import { HttpSetup } from '../http'; +import { IUiSettingsClient } from '../ui_settings'; /** * @public @@ -26,8 +27,14 @@ export class WorkspacesService implements CoreService this.formatUrlWithWorkspaceId(url, id), diff --git a/src/core/server/server.ts b/src/core/server/server.ts index f80b90ba6baa..dbaee6c12400 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -263,6 +263,7 @@ export class Server { }); await this.workspaces.start({ savedObjects: savedObjectsStart, + uiSettings: uiSettingsStart, }); this.coreStart = { diff --git a/src/core/server/ui_settings/settings/index.test.ts b/src/core/server/ui_settings/settings/index.test.ts index f71f852eb3ce..03564ce7e9b2 100644 --- a/src/core/server/ui_settings/settings/index.test.ts +++ b/src/core/server/ui_settings/settings/index.test.ts @@ -36,6 +36,7 @@ import { getNotificationsSettings } from './notifications'; import { getThemeSettings } from './theme'; import { getCoreSettings } from './index'; import { getStateSettings } from './state'; +import { getWorkspaceSettings } from './workspace'; describe('getCoreSettings', () => { it('should not have setting overlaps', () => { @@ -48,6 +49,7 @@ describe('getCoreSettings', () => { getNotificationsSettings(), getThemeSettings(), getStateSettings(), + getWorkspaceSettings(), ].reduce((sum, settings) => sum + Object.keys(settings).length, 0); expect(coreSettingsLength).toBe(summedLength); diff --git a/src/core/server/ui_settings/settings/index.ts b/src/core/server/ui_settings/settings/index.ts index b284744fc818..cea335117af8 100644 --- a/src/core/server/ui_settings/settings/index.ts +++ b/src/core/server/ui_settings/settings/index.ts @@ -36,6 +36,7 @@ import { getNavigationSettings } from './navigation'; import { getNotificationsSettings } from './notifications'; import { getThemeSettings } from './theme'; import { getStateSettings } from './state'; +import { getWorkspaceSettings } from './workspace'; export const getCoreSettings = (): Record => { return { @@ -46,5 +47,6 @@ export const getCoreSettings = (): Record => { ...getNotificationsSettings(), ...getThemeSettings(), ...getStateSettings(), + ...getWorkspaceSettings(), }; }; diff --git a/src/core/server/ui_settings/settings/workspace.ts b/src/core/server/ui_settings/settings/workspace.ts new file mode 100644 index 000000000000..3eb9e33b681c --- /dev/null +++ b/src/core/server/ui_settings/settings/workspace.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { i18n } from '@osd/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getWorkspaceSettings = (): Record => { + return { + 'workspace:enabled': { + name: i18n.translate('core.ui_settings.params.workspace.enableWorkspaceTitle', { + defaultMessage: 'Enable Workspace', + }), + value: false, + requiresPageReload: true, + description: i18n.translate('core.ui_settings.params.workspace.enableWorkspaceTitle', { + defaultMessage: 'Enable or disable OpenSearch Dashboards Workspace', + }), + category: ['workspace'], + schema: schema.boolean(), + }, + }; +}; diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 887cf46af86a..b25d1e9e1025 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -15,6 +15,7 @@ import { import { IWorkspaceDBImpl } from './types'; import { WorkspacesClientWithSavedObject } from './workspaces_client'; import { WorkspacePermissionControl } from './workspace_permission_control'; +import { UiSettingsServiceStart } from '../ui_settings/types'; export interface WorkspacesServiceSetup { client: IWorkspaceDBImpl; @@ -37,6 +38,7 @@ export type InternalWorkspacesServiceStart = WorkspacesServiceStart; /** @internal */ export interface WorkspacesStartDeps { savedObjects: InternalSavedObjectsServiceStart; + uiSettings: UiSettingsServiceStart; } export class WorkspacesService @@ -44,7 +46,7 @@ export class WorkspacesService private logger: Logger; private client?: IWorkspaceDBImpl; private permissionControl?: WorkspacePermissionControl; - + private startDeps?: WorkspacesStartDeps; constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('workspaces-service'); } @@ -52,15 +54,29 @@ export class WorkspacesService private proxyWorkspaceTrafficToRealHandler(setupDeps: WorkspacesSetupDeps) { /** * Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to - * {basePath}{osdPath*} + * {basePath}{osdPath*} when workspace is enabled + * + * Return HTTP 404 if accessing {basePath}/w/{workspaceId} when workspace is disabled */ - setupDeps.http.registerOnPreRouting((request, response, toolkit) => { + setupDeps.http.registerOnPreRouting(async (request, response, toolkit) => { const regexp = /\/w\/([^\/]*)/; const matchedResult = request.url.pathname.match(regexp); + if (matchedResult) { - const requestUrl = new URL(request.url.toString()); - requestUrl.pathname = requestUrl.pathname.replace(regexp, ''); - return toolkit.rewriteUrl(requestUrl.toString()); + if (this.startDeps) { + const savedObjectsClient = this.startDeps.savedObjects.getScopedClient(request); + const uiSettingsClient = this.startDeps.uiSettings.asScopedToClient(savedObjectsClient); + const workspacesEnabled = await uiSettingsClient.get('workspace:enabled'); + + if (workspacesEnabled) { + const requestUrl = new URL(request.url.toString()); + requestUrl.pathname = requestUrl.pathname.replace(regexp, ''); + return toolkit.rewriteUrl(requestUrl.toString()); + } else { + // If workspace was disable, return HTTP 404 + return response.notFound(); + } + } } return toolkit.next(); }); @@ -90,6 +106,7 @@ export class WorkspacesService } public async start(deps: WorkspacesStartDeps): Promise { + this.startDeps = deps; this.logger.debug('Starting SavedObjects service'); return { 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 3dd50bb5886f..a53e39cf1647 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 @@ -7,14 +7,15 @@ 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 { ApplicationStart, WorkspaceAttribute, WorkspacesStart } from '../../../../../core/public'; import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; import { switchWorkspace } from '../../components/utils/workspace'; type WorkspaceOption = EuiComboBoxOptionOption; interface WorkspaceDropdownListProps { - coreStart: CoreStart; + workspaces: WorkspacesStart; + application: ApplicationStart; } function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { @@ -27,10 +28,8 @@ export function getErrorMessage(err: any) { } export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { - const { coreStart } = props; - - const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); - const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null); + const workspaceList = useObservable(props.workspaces.client.workspaceList$, []); + const currentWorkspace = useObservable(props.workspaces.client.currentWorkspace$, null); const [loading, setLoading] = useState(false); const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]); @@ -58,14 +57,14 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { /** switch the workspace */ setLoading(true); const id = workspaceOption[0].key!; - switchWorkspace(coreStart, id); + switchWorkspace({ workspaces: props.workspaces, application: props.application }, id); setLoading(false); }, - [coreStart] + [props.application, props.workspaces] ); const onCreateWorkspaceClick = () => { - coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); + props.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); }; useEffect(() => { diff --git a/src/plugins/workspace/public/mount.tsx b/src/plugins/workspace/public/mount.tsx index c4ca29479d23..dc2ff1de8c1e 100644 --- a/src/plugins/workspace/public/mount.tsx +++ b/src/plugins/workspace/public/mount.tsx @@ -5,14 +5,25 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart } from '../../../core/public'; +import { ApplicationStart, ChromeStart, WorkspacesStart } from '../../../core/public'; import { WorkspaceDropdownList } from './containers/workspace_dropdown_list'; -export const mountDropdownList = (core: CoreStart) => { - core.chrome.navControls.registerLeft({ +export const mountDropdownList = ({ + application, + workspaces, + chrome, +}: { + application: ApplicationStart; + workspaces: WorkspacesStart; + chrome: ChromeStart; +}) => { + chrome.navControls.registerLeft({ order: 0, mount: (element) => { - ReactDOM.render(, element); + ReactDOM.render( + , + element + ); return () => { ReactDOM.unmountComponentAtNode(element); }; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 31d996f3b341..c221e892aa58 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -14,9 +14,12 @@ import { import { WORKSPACE_APP_ID } from '../common/constants'; import { mountDropdownList } from './mount'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; +import type { Subscription } from 'rxjs'; export class WorkspacesPlugin implements Plugin<{}, {}> { - private core?: CoreSetup; + private coreSetup?: CoreSetup; + private coreStart?: CoreStart; + private currentWorkspaceSubscription?: Subscription; private getWorkspaceIdFromURL(): string | null { return getWorkspaceIdFromUrl(window.location.href); } @@ -25,20 +28,25 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { /** * Patch workspace id into path */ - newUrl.pathname = this.core?.http.basePath.remove(newUrl.pathname) || ''; + newUrl.pathname = this.coreSetup?.http.basePath.remove(newUrl.pathname) || ''; if (workspaceId) { - newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}/w/${workspaceId}${ + newUrl.pathname = `${this.coreSetup?.http.basePath.serverBasePath || ''}/w/${workspaceId}${ newUrl.pathname }`; } else { - newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}${newUrl.pathname}`; + newUrl.pathname = `${this.coreSetup?.http.basePath.serverBasePath || ''}${newUrl.pathname}`; } return newUrl.toString(); }; public async setup(core: CoreSetup) { - this.core = core; - this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); + // If workspace feature is disabled, it will not load the workspace plugin + if (core.uiSettings.get('workspace:enabled') === false) { + return {}; + } + + this.coreSetup = core; + core.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); /** * Retrieve workspace id from url */ @@ -78,19 +86,34 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { 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); - }); + private _changeSavedObjectCurrentWorkspace() { + if (this.coreStart) { + return this.coreStart.workspaces.client.currentWorkspaceId$.subscribe( + (currentWorkspaceId) => { + this.coreStart?.savedObjects.client.setCurrentWorkspace(currentWorkspaceId); + } + ); } } public start(core: CoreStart) { - mountDropdownList(core); - this._changeSavedObjectCurrentWorkspace(); + // If workspace feature is disabled, it will not load the workspace plugin + if (core.uiSettings.get('workspace:enabled') === false) { + return {}; + } + + this.coreStart = core; + + mountDropdownList({ + application: core.application, + workspaces: core.workspaces, + chrome: core.chrome, + }); + this.currentWorkspaceSubscription = this._changeSavedObjectCurrentWorkspace(); return {}; } + + public stop() { + this.currentWorkspaceSubscription?.unsubscribe(); + } }