From 30739263c75c9c904ba84482058be12843a257f8 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 14 Mar 2024 12:41:56 +0800 Subject: [PATCH] [Workspace] Add workspace id in basePath (#6060) * [Workspace]Add workspace id in basePath (#212) * feat: enable workspace id in basePath Signed-off-by: SuZhou-Joe * feat: add unit test Signed-off-by: SuZhou-Joe * feat: remove useless test object id Signed-off-by: SuZhou-Joe * feat: add unit test Signed-off-by: SuZhou-Joe * feat: add unit test Signed-off-by: SuZhou-Joe * feat: update snapshot Signed-off-by: SuZhou-Joe * feat: move formatUrlWithWorkspaceId to core/public/utils Signed-off-by: SuZhou-Joe * feat: remove useless variable Signed-off-by: SuZhou-Joe * feat: remove useless variable Signed-off-by: SuZhou-Joe * feat: optimization Signed-off-by: SuZhou-Joe * feat: optimization Signed-off-by: SuZhou-Joe * feat: optimization Signed-off-by: SuZhou-Joe * feat: move workspace/utils to core Signed-off-by: SuZhou-Joe * feat: move workspace/utils to core Signed-off-by: SuZhou-Joe * feat: update comment Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: update unit test Signed-off-by: SuZhou-Joe * feat: optimization Signed-off-by: SuZhou-Joe * feat: add space under license Signed-off-by: SuZhou-Joe * fix: unit test Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe * feat: add CHANGELOG Signed-off-by: SuZhou-Joe * feat: add feature flag check Signed-off-by: SuZhou-Joe * feat: make the pr smaller Signed-off-by: SuZhou-Joe * feat: optimize with a more strict check Signed-off-by: SuZhou-Joe * fix: unit test error Signed-off-by: SuZhou-Joe * feat: remove useless code Signed-off-by: SuZhou-Joe * feat: add a unit test case Signed-off-by: SuZhou-Joe * feat: better merge Signed-off-by: SuZhou-Joe * feat: rename the workspaceBasePath to clientBasePath Signed-off-by: SuZhou-Joe * fix: snapshot Signed-off-by: SuZhou-Joe * feat: rename withoutWorkspace to withoutClientBasePath Signed-off-by: SuZhou-Joe * Revert "feat: add feature flag check" This reverts commit 64b364545164ac3da2e43189ec060ad72f4a559b. Signed-off-by: SuZhou-Joe * Revert "fix: unit test error" This reverts commit 80bed72a6c66dacde6f9f1591f7c348a0a5aeb4f. Signed-off-by: SuZhou-Joe * feat: optimize comment and test cases description Signed-off-by: SuZhou-Joe * feat: optimize comment Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- CHANGELOG.md | 1 + .../collapsible_nav.test.tsx.snap | 16 +++++++ .../header/__snapshots__/header.test.tsx.snap | 8 ++++ src/core/public/http/base_path.test.ts | 47 +++++++++++++++++++ src/core/public/http/base_path.ts | 28 +++++++---- src/core/public/http/http_service.mock.ts | 10 ++-- src/core/public/http/http_service.test.ts | 26 ++++++++++ src/core/public/http/http_service.ts | 10 +++- src/core/public/http/types.ts | 28 +++++++++-- src/core/public/index.ts | 4 +- src/core/public/utils/index.ts | 6 +++ src/core/server/utils/index.ts | 1 + src/core/utils/constants.ts | 2 + src/core/utils/index.ts | 3 +- src/core/utils/workspace.test.ts | 40 ++++++++++++++++ src/core/utils/workspace.ts | 42 +++++++++++++++++ .../dashboard_listing.test.tsx.snap | 10 ++++ .../dashboard_top_nav.test.tsx.snap | 12 +++++ .../dashboard_empty_screen.test.tsx.snap | 6 +++ .../saved_objects_table.test.tsx.snap | 2 + .../__snapshots__/flyout.test.tsx.snap | 2 + ...telemetry_management_section.test.tsx.snap | 2 + .../workspace/opensearch_dashboards.json | 2 +- src/plugins/workspace/public/plugin.test.ts | 18 +++++++ src/plugins/workspace/public/plugin.ts | 14 ++++++ src/plugins/workspace/server/plugin.ts | 18 +++++++ 26 files changed, 335 insertions(+), 23 deletions(-) create mode 100644 src/core/utils/workspace.test.ts create mode 100644 src/core/utils/workspace.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ad681aaf4a..5dcadda9fb67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) - [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) - [Multiple Datasource] Add component to show single selected data source in read only mode ([#6125](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6125)) +- [Workspace] Add workspace id in basePath ([#6060](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6060)) ### 🐛 Bug Fixes diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 7b4e3ba472dc..62f00bee2c74 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -54,7 +54,9 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -2005,7 +2007,9 @@ exports[`CollapsibleNav renders the default nav 1`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -2307,7 +2311,9 @@ exports[`CollapsibleNav renders the default nav 2`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -2610,7 +2616,9 @@ exports[`CollapsibleNav renders the default nav 3`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -3204,7 +3212,9 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -4319,7 +4329,9 @@ exports[`CollapsibleNav with custom branding renders the nav bar in default mode basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -5433,7 +5445,9 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -6540,7 +6554,9 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", 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 7ec470c74e03..8d244a212d1f 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 @@ -242,7 +242,9 @@ exports[`Header handles visibility and lock changes 1`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -5873,7 +5875,9 @@ exports[`Header handles visibility and lock changes 1`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -6934,7 +6938,9 @@ exports[`Header renders condensed header 1`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", @@ -11346,7 +11352,9 @@ exports[`Header renders condensed header 1`] = ` basePath={ BasePath { "basePath": "/test", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "/test", diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts index 27cfa9bf0581..921ec13e6db2 100644 --- a/src/core/public/http/base_path.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -110,4 +110,51 @@ describe('BasePath', () => { expect(new BasePath('/foo/bar', '/foo').serverBasePath).toEqual('/foo'); }); }); + + describe('clientBasePath', () => { + it('get with clientBasePath provided when construct', () => { + expect(new BasePath('/foo/bar', '/foo/bar', '/client_base_path').get()).toEqual( + '/foo/bar/client_base_path' + ); + }); + + it('getBasePath with clientBasePath provided when construct', () => { + expect(new BasePath('/foo/bar', '/foo/bar', '/client_base_path').getBasePath()).toEqual( + '/foo/bar' + ); + }); + + it('prepend with clientBasePath provided when construct', () => { + expect(new BasePath('/foo/bar', '/foo/bar', '/client_base_path').prepend('/prepend')).toEqual( + '/foo/bar/client_base_path/prepend' + ); + }); + + it('construct with clientBasePath provided but calls prepend with withoutClientBasePath is true', () => { + expect( + new BasePath('/foo/bar', '/foo/bar', '/client_base_path').prepend('/prepend', { + withoutClientBasePath: true, + }) + ).toEqual('/foo/bar/prepend'); + }); + + it('remove with clientBasePath provided when construct', () => { + expect( + new BasePath('/foo/bar', '/foo/bar', '/client_base_path').remove( + '/foo/bar/client_base_path/remove' + ) + ).toEqual('/remove'); + }); + + it('construct with clientBasePath provided but calls remove with withoutClientBasePath is true', () => { + expect( + new BasePath('/foo/bar', '/foo/bar', '/client_base_path').remove( + '/foo/bar/client_base_path/remove', + { + withoutClientBasePath: true, + } + ) + ).toEqual('/client_base_path/remove'); + }); + }); }); diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index b31504676dba..c88602c35b9d 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -29,37 +29,47 @@ */ import { modifyUrl } from '@osd/std'; +import type { PrependOptions } from './types'; export class BasePath { constructor( private readonly basePath: string = '', - public readonly serverBasePath: string = basePath + public readonly serverBasePath: string = basePath, + private readonly clientBasePath: string = '' ) {} public get = () => { + return `${this.basePath}${this.clientBasePath}`; + }; + + public getBasePath = () => { return this.basePath; }; - public prepend = (path: string): string => { - if (!this.basePath) return path; + public prepend = (path: string, prependOptions?: PrependOptions): string => { + const { withoutClientBasePath } = prependOptions || {}; + const basePath = withoutClientBasePath ? this.basePath : this.get(); + if (!basePath) return path; return modifyUrl(path, (parts) => { if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { - parts.pathname = `${this.basePath}${parts.pathname}`; + parts.pathname = `${basePath}${parts.pathname}`; } }); }; - public remove = (path: string): string => { - if (!this.basePath) { + public remove = (path: string, prependOptions?: PrependOptions): string => { + const { withoutClientBasePath } = prependOptions || {}; + const basePath = withoutClientBasePath ? this.basePath : this.get(); + if (!basePath) { return path; } - if (path === this.basePath) { + if (path === basePath) { return '/'; } - if (path.startsWith(`${this.basePath}/`)) { - return path.slice(this.basePath.length); + if (path.startsWith(`${basePath}/`)) { + return path.slice(basePath.length); } return path; diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 8c10d10017e5..b34b4d1cfa88 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -39,7 +39,7 @@ export type HttpSetupMock = jest.Mocked & { anonymousPaths: jest.Mocked; }; -const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ +const createServiceMock = ({ basePath = '', clientBasePath = '' } = {}): HttpSetupMock => ({ fetch: jest.fn(), get: jest.fn(), head: jest.fn(), @@ -48,7 +48,7 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ patch: jest.fn(), delete: jest.fn(), options: jest.fn(), - basePath: new BasePath(basePath), + basePath: new BasePath(basePath, undefined, clientBasePath), anonymousPaths: { register: jest.fn(), isAnonymous: jest.fn(), @@ -58,14 +58,14 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ intercept: jest.fn(), }); -const createMock = ({ basePath = '' } = {}) => { +const createMock = ({ basePath = '', clientBasePath = '' } = {}) => { const mocked: jest.Mocked> = { setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; - mocked.setup.mockReturnValue(createServiceMock({ basePath })); - mocked.start.mockReturnValue(createServiceMock({ basePath })); + mocked.setup.mockReturnValue(createServiceMock({ basePath, clientBasePath })); + mocked.start.mockReturnValue(createServiceMock({ basePath, clientBasePath })); return mocked; }; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index e60e506dfc0a..5671064e4c52 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -74,6 +74,32 @@ describe('#setup()', () => { // We don't verify that this Observable comes from Fetch#getLoadingCount$() to avoid complex mocking expect(loadingServiceSetup.addLoadingCountSource).toHaveBeenCalledWith(expect.any(Observable)); }); + + it('setup basePath without workspaceId provided in window.location.href', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const httpService = new HttpService(); + const setupResult = httpService.setup({ fatalErrors, injectedMetadata }); + expect(setupResult.basePath.get()).toEqual(''); + }); + + it('setup basePath with workspaceId provided in window.location.href', () => { + const windowSpy = jest.spyOn(window, 'window', 'get'); + windowSpy.mockImplementation( + () => + ({ + location: { + href: 'http://localhost/w/workspaceId/app', + }, + } as any) + ); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const httpService = new HttpService(); + const setupResult = httpService.setup({ fatalErrors, injectedMetadata }); + expect(setupResult.basePath.get()).toEqual('/w/workspaceId'); + windowSpy.mockRestore(); + }); }); describe('#stop()', () => { diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index f26323f261aa..6832703c7925 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -36,6 +36,8 @@ import { AnonymousPathsService } from './anonymous_paths_service'; import { LoadingCountService } from './loading_count_service'; import { Fetch } from './fetch'; import { CoreService } from '../../types'; +import { getWorkspaceIdFromUrl } from '../utils'; +import { WORKSPACE_PATH_PREFIX } from '../../utils/constants'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; @@ -50,9 +52,15 @@ export class HttpService implements CoreService { public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { const opensearchDashboardsVersion = injectedMetadata.getOpenSearchDashboardsVersion(); + let clientBasePath = ''; + const workspaceId = getWorkspaceIdFromUrl(window.location.href); + if (workspaceId) { + clientBasePath = `${WORKSPACE_PATH_PREFIX}/${workspaceId}`; + } const basePath = new BasePath( injectedMetadata.getBasePath(), - injectedMetadata.getServerBasePath() + injectedMetadata.getServerBasePath(), + clientBasePath ); const fetchService = new Fetch({ basePath, opensearchDashboardsVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index f2573a6badd5..6e93e1cee94a 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -87,25 +87,43 @@ export interface HttpSetup { */ export type HttpStart = HttpSetup; +/** + * prepend options + * + * withoutClientBasePath option will prepend a relative url with serverBasePath only. + * For now, clientBasePath is consist of: + * workspacePath, which has the pattern of /w/{workspaceId}. + * + * In the future, clientBasePath may have other parts but keep `withoutClientBasePath` for now to not over-design the interface, + */ +export interface PrependOptions { + withoutClientBasePath?: boolean; +} + /** * APIs for manipulating the basePath on URL segments. * @public */ export interface IBasePath { /** - * Gets the `basePath` string. + * Gets the `basePath + clientBasePath` string. */ get: () => string; /** - * Prepends `path` with the basePath. + * Gets the `basePath` string + */ + getBasePath: () => string; + + /** + * Prepends `path` with the basePath + clientBasePath. */ - prepend: (url: string) => string; + prepend: (url: string, prependOptions?: PrependOptions) => string; /** - * Removes the prepended basePath from the `path`. + * Removes the prepended basePath + clientBasePath from the `path`. */ - remove: (url: string) => string; + remove: (url: string, prependOptions?: PrependOptions) => string; /** * Returns the server's root basePath as configured, without any namespace prefix. diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 4e889ff82e6a..4140603ff6f7 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -351,4 +351,6 @@ export { export { __osdBootstrap__ } from './osd_bootstrap'; -export { WorkspacesStart, WorkspacesSetup } from './workspace'; +export { WorkspacesStart, WorkspacesSetup, WorkspacesService } from './workspace'; + +export { WORKSPACE_TYPE } from '../utils'; diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index 7676b9482aac..c0c6f2582e9c 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -31,3 +31,9 @@ export { shareWeakReplay } from './share_weak_replay'; export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; +export { + WORKSPACE_PATH_PREFIX, + WORKSPACE_TYPE, + formatUrlWithWorkspaceId, + getWorkspaceIdFromUrl, +} from '../../utils'; diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts index d2c9e0086ad7..42b01e72b0d1 100644 --- a/src/core/server/utils/index.ts +++ b/src/core/server/utils/index.ts @@ -32,3 +32,4 @@ export * from './crypto'; export * from './from_root'; export * from './package_json'; export * from './streams'; +export { getWorkspaceIdFromUrl, cleanWorkspaceId } from '../../utils'; diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index 73c2d6010846..ecc1b7e863c4 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -4,3 +4,5 @@ */ export const WORKSPACE_TYPE = 'workspace'; + +export const WORKSPACE_PATH_PREFIX = '/w'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index af4f9a17ae58..a83f85a8fce0 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -37,4 +37,5 @@ export { IContextProvider, } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; -export { WORKSPACE_TYPE } from './constants'; +export { WORKSPACE_PATH_PREFIX, WORKSPACE_TYPE } from './constants'; +export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } from './workspace'; diff --git a/src/core/utils/workspace.test.ts b/src/core/utils/workspace.test.ts new file mode 100644 index 000000000000..a852ddcc5190 --- /dev/null +++ b/src/core/utils/workspace.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId } from './workspace'; +import { httpServiceMock } from '../public/mocks'; + +describe('#getWorkspaceIdFromUrl', () => { + it('return workspace when there is a match', () => { + expect(getWorkspaceIdFromUrl('http://localhost/w/foo')).toEqual('foo'); + }); + + it('return empty when there is not a match', () => { + expect(getWorkspaceIdFromUrl('http://localhost/w2/foo')).toEqual(''); + }); + + it('return workspace when there is a match with basePath provided', () => { + expect(getWorkspaceIdFromUrl('http://localhost/basepath/w/foo', '/basepath')).toEqual('foo'); + }); + + it('return empty when there is a match without basePath but basePath provided', () => { + expect(getWorkspaceIdFromUrl('http://localhost/w/foo', '/w')).toEqual(''); + }); +}); + +describe('#formatUrlWithWorkspaceId', () => { + const basePathWithoutClientBasePath = httpServiceMock.createSetupContract().basePath; + it('return url with workspace prefix when format with a id provided', () => { + expect( + formatUrlWithWorkspaceId('/app/dashboard', 'foo', basePathWithoutClientBasePath) + ).toEqual('http://localhost/w/foo/app/dashboard'); + }); + + it('return url without workspace prefix when format without a id', () => { + expect( + formatUrlWithWorkspaceId('/w/foo/app/dashboard', '', basePathWithoutClientBasePath) + ).toEqual('http://localhost/app/dashboard'); + }); +}); diff --git a/src/core/utils/workspace.ts b/src/core/utils/workspace.ts new file mode 100644 index 000000000000..c383967483a8 --- /dev/null +++ b/src/core/utils/workspace.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WORKSPACE_PATH_PREFIX } from './constants'; +import { IBasePath } from '../public'; + +export const getWorkspaceIdFromUrl = (url: string, basePath?: string): string => { + const regexp = new RegExp(`^${basePath || ''}\/w\/([^\/]*)`); + const urlObject = new URL(url); + const matchedResult = urlObject.pathname.match(regexp); + if (matchedResult) { + return matchedResult[1]; + } + + return ''; +}; + +export const cleanWorkspaceId = (path: string) => { + return path.replace(/^\/w\/([^\/]*)/, ''); +}; + +export const formatUrlWithWorkspaceId = (url: string, workspaceId: string, basePath: IBasePath) => { + const newUrl = new URL(url, window.location.href); + /** + * Patch workspace id into path + */ + newUrl.pathname = basePath.remove(newUrl.pathname); + + if (workspaceId) { + newUrl.pathname = `${WORKSPACE_PATH_PREFIX}/${workspaceId}${newUrl.pathname}`; + } else { + newUrl.pathname = cleanWorkspaceId(newUrl.pathname); + } + + newUrl.pathname = basePath.prepend(newUrl.pathname, { + withoutClientBasePath: true, + }); + + return newUrl.toString(); +}; 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 dfd9d3d3e378..16916b9a41ad 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 @@ -856,7 +856,9 @@ exports[`dashboard listing hideWriteControls 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -1989,7 +1991,9 @@ exports[`dashboard listing render table listing with initial filters from URL 1` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -3183,7 +3187,9 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -4377,7 +4383,9 @@ exports[`dashboard listing renders table rows 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -5571,7 +5579,9 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", 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 54b40858b4f1..5a6af05750c4 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 @@ -748,7 +748,9 @@ exports[`Dashboard top nav render in embed mode 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -1706,7 +1708,9 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -2664,7 +2668,9 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -3622,7 +3628,9 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -4580,7 +4588,9 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -5538,7 +5548,9 @@ exports[`Dashboard top nav render with all components 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", diff --git a/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap index 04120e429393..c2c83ff6f356 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -11,7 +11,9 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -378,7 +380,9 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", @@ -755,7 +759,9 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index d18762f4912f..1183c4cccd68 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -260,7 +260,9 @@ exports[`SavedObjectsTable should render normally 1`] = ` basePath={ BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 14fe1fbabd88..d4a33e4a0569 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -168,7 +168,9 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 5d71bc774cff..2761ce16fea3 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -313,7 +313,9 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO }, "basePath": BasePath { "basePath": "", + "clientBasePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], "remove": [Function], "serverBasePath": "", diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index 40a7eb5c3f9f..f34106ab4fed 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -2,7 +2,7 @@ "id": "workspace", "version": "opensearchDashboards", "server": true, - "ui": false, + "ui": true, "requiredPlugins": [ "savedObjects" ], diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 8c869415aede..e54a20552329 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -30,4 +30,22 @@ describe('Workspace plugin', () => { coreStart.workspaces.currentWorkspaceId$.next('foo'); expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); }); + + it('#setup when workspace id is in url and enterWorkspace return error', async () => { + const windowSpy = jest.spyOn(window, 'window', 'get'); + windowSpy.mockImplementation( + () => + ({ + location: { + href: 'http://localhost/w/workspaceId/app', + }, + } as any) + ); + const setupMock = getSetupMock(); + + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock); + expect(setupMock.workspaces.currentWorkspaceId$.getValue()).toEqual('workspaceId'); + windowSpy.mockRestore(); + }); }); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 6f604bcf5678..8a69d597c84b 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -5,6 +5,7 @@ import type { Subscription } from 'rxjs'; import { Plugin, CoreStart, CoreSetup } from '../../../core/public'; +import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; import { WorkspaceClient } from './workspace_client'; export class WorkspacePlugin implements Plugin<{}, {}, {}> { @@ -19,9 +20,22 @@ export class WorkspacePlugin implements Plugin<{}, {}, {}> { }); } } + private getWorkspaceIdFromURL(basePath?: string): string | null { + return getWorkspaceIdFromUrl(window.location.href, basePath); + } public async setup(core: CoreSetup) { const workspaceClient = new WorkspaceClient(core.http, core.workspaces); await workspaceClient.init(); + + /** + * Retrieve workspace id from url + */ + const workspaceId = this.getWorkspaceIdFromURL(core.http.basePath.getBasePath()); + + if (workspaceId) { + core.workspaces.currentWorkspaceId$.next(workspaceId); + } + return {}; } diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 5ad4df46c919..f5b7da6430e0 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -15,12 +15,29 @@ import { WorkspaceClient } from './workspace_client'; import { registerRoutes } from './routes'; import { WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; import { WorkspaceConflictSavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper_for_check_workspace_conflict'; +import { cleanWorkspaceId, getWorkspaceIdFromUrl } from '../../../core/server/utils'; export class WorkspacePlugin implements Plugin { private readonly logger: Logger; private client?: IWorkspaceClientImpl; private workspaceConflictControl?: WorkspaceConflictSavedObjectsClientWrapper; + private proxyWorkspaceTrafficToRealHandler(setupDeps: CoreSetup) { + /** + * Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to {basePath}{osdPath*} + */ + setupDeps.http.registerOnPreRouting(async (request, response, toolkit) => { + const workspaceId = getWorkspaceIdFromUrl(request.url.toString()); + + if (workspaceId) { + const requestUrl = new URL(request.url.toString()); + requestUrl.pathname = cleanWorkspaceId(requestUrl.pathname); + return toolkit.rewriteUrl(requestUrl.toString()); + } + return toolkit.next(); + }); + } + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('plugins', 'workspace'); } @@ -39,6 +56,7 @@ export class WorkspacePlugin implements Plugin