From 9b8bdf6c8b5f85b03f922351d17105c9b0d23b52 Mon Sep 17 00:00:00 2001
From: SuZhou-Joe
Date: Mon, 16 Oct 2023 12:58:04 +0800
Subject: [PATCH] [Backport workspace][Workspace]Add workspace id in basePath
(#212) (#225)
* [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
(cherry picked from commit 43e91faec7ab11131207f598668b073f727ef073)
* feat: some sync
Signed-off-by: SuZhou-Joe
* feat: remove useless code
Signed-off-by: SuZhou-Joe
* fix: modify import path
Signed-off-by: SuZhou-Joe
* fix: unit test
Signed-off-by: SuZhou-Joe
* fix: unit test
Signed-off-by: SuZhou-Joe
---------
Signed-off-by: SuZhou-Joe
---
src/core/public/http/base_path.test.ts | 32 ++++
src/core/public/http/http_service.mock.ts | 10 +-
src/core/public/http/http_service.test.ts | 26 +++
src/core/public/index.ts | 2 -
src/core/public/utils/index.ts | 5 +-
src/core/public/utils/workspace.ts | 15 --
src/core/public/workspace/index.ts | 1 +
src/core/server/utils/index.ts | 1 +
src/core/utils/index.ts | 1 +
src/core/utils/workspace.test.ts | 32 ++++
src/core/utils/workspace.ts | 42 ++++
.../saved_objects_table.test.tsx.snap | 6 -
.../objects_table/saved_objects_table.tsx | 5 +-
.../public/components/utils/workspace.ts | 2 +-
.../workspace_creator/workspace_creator.tsx | 2 +-
.../workspace_fatal_error.test.tsx.snap | 180 +++++++++++++++++
.../workspace_fatal_error.test.tsx | 71 +++++++
.../workspace_fatal_error.tsx | 7 +-
.../workspace_menu/workspace_menu.tsx | 2 +-
.../workspace_updater/workspace_updater.tsx | 2 +-
src/plugins/workspace/public/plugin.test.ts | 134 +++++++++++++
src/plugins/workspace/public/utils.ts | 27 +--
.../workspace/public/workspace_client.mock.ts | 25 +++
.../workspace/public/workspace_client.test.ts | 181 ++++++++++++++++++
.../workspace/public/workspace_client.ts | 25 +--
src/plugins/workspace/server/index.ts | 1 +
src/plugins/workspace/server/plugin.ts | 9 +-
src/plugins/workspace/server/routes/index.ts | 1 +
src/plugins/workspace/server/types.ts | 1 +
.../workspace/server/workspace_client.ts | 1 +
30 files changed, 766 insertions(+), 83 deletions(-)
delete mode 100644 src/core/public/utils/workspace.ts
create mode 100644 src/core/utils/workspace.test.ts
create mode 100644 src/core/utils/workspace.ts
create mode 100644 src/plugins/workspace/public/components/workspace_fatal_error/__snapshots__/workspace_fatal_error.test.tsx.snap
create mode 100644 src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.test.tsx
create mode 100644 src/plugins/workspace/public/plugin.test.ts
create mode 100644 src/plugins/workspace/public/workspace_client.mock.ts
create mode 100644 src/plugins/workspace/public/workspace_client.test.ts
diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts
index 27cfa9bf0581..f80d41631b9b 100644
--- a/src/core/public/http/base_path.test.ts
+++ b/src/core/public/http/base_path.test.ts
@@ -110,4 +110,36 @@ describe('BasePath', () => {
expect(new BasePath('/foo/bar', '/foo').serverBasePath).toEqual('/foo');
});
});
+
+ describe('workspaceBasePath', () => {
+ it('get path with workspace', () => {
+ expect(new BasePath('/foo/bar', '/foo/bar', '/workspace').get()).toEqual(
+ '/foo/bar/workspace'
+ );
+ });
+
+ it('getBasePath with workspace provided', () => {
+ expect(new BasePath('/foo/bar', '/foo/bar', '/workspace').getBasePath()).toEqual('/foo/bar');
+ });
+
+ it('prepend with workspace provided', () => {
+ expect(new BasePath('/foo/bar', '/foo/bar', '/workspace').prepend('/prepend')).toEqual(
+ '/foo/bar/workspace/prepend'
+ );
+ });
+
+ it('prepend with workspace provided but calls without workspace', () => {
+ expect(
+ new BasePath('/foo/bar', '/foo/bar', '/workspace').prepend('/prepend', {
+ withoutWorkspace: true,
+ })
+ ).toEqual('/foo/bar/prepend');
+ });
+
+ it('remove with workspace provided', () => {
+ expect(
+ new BasePath('/foo/bar', '/foo/bar', '/workspace').remove('/foo/bar/workspace/remove')
+ ).toEqual('/remove');
+ });
+ });
});
diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts
index 8c10d10017e5..934e4cbc9394 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 = '', workspaceBasePath = '' } = {}): 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, workspaceBasePath),
anonymousPaths: {
register: jest.fn(),
isAnonymous: jest.fn(),
@@ -58,14 +58,14 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({
intercept: jest.fn(),
});
-const createMock = ({ basePath = '' } = {}) => {
+const createMock = ({ basePath = '', workspaceBasePath = '' } = {}) => {
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, workspaceBasePath }));
+ mocked.start.mockReturnValue(createServiceMock({ basePath, workspaceBasePath }));
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/index.ts b/src/core/public/index.ts
index 3a8358d0f2a0..1f26a26de8ef 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -358,5 +358,3 @@ export {
MANAGEMENT_WORKSPACE_ID,
WORKSPACE_TYPE,
} from '../utils';
-
-export { getWorkspaceIdFromUrl } from './utils';
diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts
index c0bfc1bc17a5..6786739cd1d6 100644
--- a/src/core/public/utils/index.ts
+++ b/src/core/public/utils/index.ts
@@ -31,10 +31,11 @@
export { shareWeakReplay } from './share_weak_replay';
export { Sha256 } from './crypto';
export { MountWrapper, mountReactNode } from './mount';
-export { getWorkspaceIdFromUrl } from './workspace';
export {
WORKSPACE_PATH_PREFIX,
+ WORKSPACE_TYPE,
+ formatUrlWithWorkspaceId,
+ getWorkspaceIdFromUrl,
PUBLIC_WORKSPACE_ID,
MANAGEMENT_WORKSPACE_ID,
- WORKSPACE_TYPE,
} from '../../utils';
diff --git a/src/core/public/utils/workspace.ts b/src/core/public/utils/workspace.ts
deleted file mode 100644
index e93355aa00e3..000000000000
--- a/src/core/public/utils/workspace.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export const getWorkspaceIdFromUrl = (url: string): string => {
- const regexp = /\/w\/([^\/]*)/;
- const urlObject = new URL(url);
- const matchedResult = urlObject.pathname.match(regexp);
- if (matchedResult) {
- return matchedResult[1];
- }
-
- return '';
-};
diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts
index e46cac0b4b51..4b9b2c86f649 100644
--- a/src/core/public/workspace/index.ts
+++ b/src/core/public/workspace/index.ts
@@ -2,4 +2,5 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+
export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service';
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/index.ts b/src/core/utils/index.ts
index 1b7d29406443..f2884c60581c 100644
--- a/src/core/utils/index.ts
+++ b/src/core/utils/index.ts
@@ -45,3 +45,4 @@ export {
WORKSPACE_TYPE,
PERSONAL_WORKSPACE_ID_PREFIX,
} 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..7d2a1f700c5f
--- /dev/null
+++ b/src/core/utils/workspace.test.ts
@@ -0,0 +1,32 @@
+/*
+ * 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('');
+ });
+});
+
+describe('#formatUrlWithWorkspaceId', () => {
+ const basePathWithoutWorkspaceBasePath = httpServiceMock.createSetupContract().basePath;
+ it('return url with workspace prefix when format with a id provided', () => {
+ expect(
+ formatUrlWithWorkspaceId('/app/dashboard', 'foo', basePathWithoutWorkspaceBasePath)
+ ).toEqual('http://localhost/w/foo/app/dashboard');
+ });
+
+ it('return url without workspace prefix when format without a id', () => {
+ expect(
+ formatUrlWithWorkspaceId('/w/foo/app/dashboard', '', basePathWithoutWorkspaceBasePath)
+ ).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..c369f95d5817
--- /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): string => {
+ const regexp = /\/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, {
+ withoutWorkspace: true,
+ });
+
+ return newUrl.toString();
+};
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 0102cf684738..2b1506545635 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
@@ -200,12 +200,6 @@ exports[`SavedObjectsTable export should allow the user to choose when exporting
exports[`SavedObjectsTable should render normally 1`] = `
+
{this.renderFlyout()}
{this.renderRelationships()}
{this.renderDeleteConfirmModal()}
diff --git a/src/plugins/workspace/public/components/utils/workspace.ts b/src/plugins/workspace/public/components/utils/workspace.ts
index 63d88ae93e19..6be21538838f 100644
--- a/src/plugins/workspace/public/components/utils/workspace.ts
+++ b/src/plugins/workspace/public/components/utils/workspace.ts
@@ -5,7 +5,7 @@
import { WORKSPACE_OVERVIEW_APP_ID } from '../../../common/constants';
import { CoreStart } from '../../../../../core/public';
-import { formatUrlWithWorkspaceId } from '../../utils';
+import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
type Core = Pick;
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 f1ede156e5a4..85a383781bff 100644
--- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx
+++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx
@@ -9,7 +9,7 @@ import { i18n } from '@osd/i18n';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { WorkspaceForm, WorkspaceFormSubmitData } from './workspace_form';
import { WORKSPACE_OVERVIEW_APP_ID, WORKSPACE_OP_TYPE_CREATE } from '../../../common/constants';
-import { formatUrlWithWorkspaceId } from '../../utils';
+import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
import { WorkspaceClient } from '../../workspace_client';
export const WorkspaceCreator = () => {
diff --git a/src/plugins/workspace/public/components/workspace_fatal_error/__snapshots__/workspace_fatal_error.test.tsx.snap b/src/plugins/workspace/public/components/workspace_fatal_error/__snapshots__/workspace_fatal_error.test.tsx.snap
new file mode 100644
index 000000000000..594066e959f7
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_fatal_error/__snapshots__/workspace_fatal_error.test.tsx.snap
@@ -0,0 +1,180 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` render error with callout 1`] = `
+
+
+
+
+
+
+
+
+
+ Something went wrong
+
+
+
+
+
+
+
+ The workspace you want to go can not be found, try go back to home.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[` render normally 1`] = `
+
+
+
+
+
+
+
+
+
+ Something went wrong
+
+
+
+
+
+
+
+ The workspace you want to go can not be found, try go back to home.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.test.tsx b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.test.tsx
new file mode 100644
index 000000000000..d98e0063dcfa
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.test.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { IntlProvider } from 'react-intl';
+import { fireEvent, render, waitFor } from '@testing-library/react';
+import { WorkspaceFatalError } from './workspace_fatal_error';
+import { context } from '../../../../opensearch_dashboards_react/public';
+import { coreMock } from '../../../../../core/public/mocks';
+
+describe('', () => {
+ it('render normally', async () => {
+ const { findByText, container } = render(
+
+
+
+ );
+ await findByText('Something went wrong');
+ expect(container).toMatchSnapshot();
+ });
+
+ it('render error with callout', async () => {
+ const { findByText, container } = render(
+
+
+
+ );
+ await findByText('errorInCallout');
+ expect(container).toMatchSnapshot();
+ });
+
+ it('click go back to home', async () => {
+ const { location } = window;
+ const setHrefSpy = jest.fn((href) => href);
+ if (window.location) {
+ // @ts-ignore
+ delete window.location;
+ }
+ window.location = {} as Location;
+ Object.defineProperty(window.location, 'href', {
+ get: () => 'http://localhost/',
+ set: setHrefSpy,
+ });
+ const coreStartMock = coreMock.createStart();
+ const { getByText } = render(
+
+
+
+
+
+ );
+ fireEvent.click(getByText('Go back to home'));
+ await waitFor(
+ () => {
+ expect(setHrefSpy).toBeCalledTimes(1);
+ },
+ {
+ container: document.body,
+ }
+ );
+ window.location = location;
+ });
+});
diff --git a/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx
index 0509eb095bce..b1081e92237f 100644
--- a/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx
+++ b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx
@@ -13,8 +13,9 @@ import {
} from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@osd/i18n/react';
+import { IBasePath } from 'opensearch-dashboards/public';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
-import { formatUrlWithWorkspaceId } from '../../utils';
+import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
export function WorkspaceFatalError(props: { error?: string }) {
const {
@@ -24,7 +25,7 @@ export function WorkspaceFatalError(props: { error?: string }) {
window.location.href = formatUrlWithWorkspaceId(
application?.getUrlForApp('home') || '',
'',
- http?.basePath
+ http?.basePath as IBasePath
);
};
return (
@@ -51,7 +52,7 @@ export function WorkspaceFatalError(props: { error?: string }) {
}
actions={[
-
+
{
+ beforeEach(() => {
+ WorkspaceClientMock.mockClear();
+ Object.values(workspaceClientMock).forEach((item) => item.mockClear());
+ });
+ it('#setup', async () => {
+ const setupMock = coreMock.createSetup();
+ const workspacePlugin = new WorkspacePlugin();
+ await workspacePlugin.setup(
+ {
+ ...setupMock,
+ chrome: chromeServiceMock.createSetupContract(),
+ },
+ { savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract() }
+ );
+ expect(setupMock.application.register).toBeCalledTimes(5);
+ expect(WorkspaceClientMock).toBeCalledTimes(1);
+ expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0);
+ });
+
+ 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)
+ );
+ workspaceClientMock.enterWorkspace.mockResolvedValue({
+ success: false,
+ error: 'error',
+ });
+ const setupMock = coreMock.createSetup();
+ const applicationStartMock = applicationServiceMock.createStartContract();
+ const chromeStartMock = chromeServiceMock.createStartContract();
+ setupMock.getStartServices.mockImplementation(() => {
+ return Promise.resolve([
+ {
+ application: applicationStartMock,
+ chrome: chromeStartMock,
+ },
+ {},
+ {},
+ ]) as any;
+ });
+
+ const workspacePlugin = new WorkspacePlugin();
+ await workspacePlugin.setup(
+ {
+ ...setupMock,
+ chrome: chromeServiceMock.createSetupContract(),
+ },
+ { savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract() }
+ );
+ expect(setupMock.application.register).toBeCalledTimes(5);
+ expect(WorkspaceClientMock).toBeCalledTimes(1);
+ expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId');
+ expect(setupMock.getStartServices).toBeCalledTimes(1);
+ await waitFor(
+ () => {
+ expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_FATAL_ERROR_APP_ID, {
+ replace: true,
+ state: {
+ error: 'error',
+ },
+ });
+ },
+ {
+ container: document.body,
+ }
+ );
+ windowSpy.mockRestore();
+ });
+
+ it('#setup when workspace id is in url and enterWorkspace return success', async () => {
+ const windowSpy = jest.spyOn(window, 'window', 'get');
+ windowSpy.mockImplementation(
+ () =>
+ ({
+ location: {
+ href: 'http://localhost/w/workspaceId/app',
+ },
+ } as any)
+ );
+ workspaceClientMock.enterWorkspace.mockResolvedValue({
+ success: true,
+ error: 'error',
+ });
+ const setupMock = coreMock.createSetup();
+ const applicationStartMock = applicationServiceMock.createStartContract();
+ let currentAppIdSubscriber: Subscriber | undefined;
+ setupMock.getStartServices.mockImplementation(() => {
+ return Promise.resolve([
+ {
+ application: {
+ ...applicationStartMock,
+ currentAppId$: new Observable((subscriber) => {
+ currentAppIdSubscriber = subscriber;
+ }),
+ },
+ },
+ {},
+ {},
+ ]) as any;
+ });
+
+ const workspacePlugin = new WorkspacePlugin();
+ await workspacePlugin.setup(
+ {
+ ...setupMock,
+ chrome: chromeServiceMock.createSetupContract(),
+ },
+ { savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract() }
+ );
+ currentAppIdSubscriber?.next(WORKSPACE_FATAL_ERROR_APP_ID);
+ expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_OVERVIEW_APP_ID);
+ windowSpy.mockRestore();
+ });
+});
diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts
index 09528c2b080f..f7c59dbfc53c 100644
--- a/src/plugins/workspace/public/utils.ts
+++ b/src/plugins/workspace/public/utils.ts
@@ -3,32 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { WORKSPACE_PATH_PREFIX } from '../../../core/public/utils';
-import { AppCategory, IBasePath } from '../../../core/public';
-
-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 = newUrl.pathname.replace(/^\/w\/([^\/]*)/, '');
- }
-
- newUrl.pathname =
- basePath?.prepend(newUrl.pathname, {
- withoutWorkspace: true,
- }) || '';
-
- return newUrl.toString();
-};
+import { AppCategory } from '../../../core/public';
/**
* Given a list of feature config, check if a feature matches config
diff --git a/src/plugins/workspace/public/workspace_client.mock.ts b/src/plugins/workspace/public/workspace_client.mock.ts
new file mode 100644
index 000000000000..2ceeae5627d1
--- /dev/null
+++ b/src/plugins/workspace/public/workspace_client.mock.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const workspaceClientMock = {
+ init: jest.fn(),
+ enterWorkspace: jest.fn(),
+ getCurrentWorkspaceId: jest.fn(),
+ getCurrentWorkspace: jest.fn(),
+ create: jest.fn(),
+ delete: jest.fn(),
+ list: jest.fn(),
+ get: jest.fn(),
+ update: jest.fn(),
+ stop: jest.fn(),
+};
+
+export const WorkspaceClientMock = jest.fn(function () {
+ return workspaceClientMock;
+});
+
+jest.doMock('./workspace_client', () => ({
+ WorkspaceClient: WorkspaceClientMock,
+}));
diff --git a/src/plugins/workspace/public/workspace_client.test.ts b/src/plugins/workspace/public/workspace_client.test.ts
new file mode 100644
index 000000000000..7d05c3f22458
--- /dev/null
+++ b/src/plugins/workspace/public/workspace_client.test.ts
@@ -0,0 +1,181 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { httpServiceMock, workspacesServiceMock } from '../../../core/public/mocks';
+import { WorkspaceClient } from './workspace_client';
+
+const getWorkspaceClient = () => {
+ const httpSetupMock = httpServiceMock.createSetupContract();
+ const workspaceMock = workspacesServiceMock.createSetupContract();
+ return {
+ httpSetupMock,
+ workspaceMock,
+ workspaceClient: new WorkspaceClient(httpSetupMock, workspaceMock),
+ };
+};
+
+describe('#WorkspaceClient', () => {
+ it('#init', async () => {
+ const { workspaceClient, httpSetupMock, workspaceMock } = getWorkspaceClient();
+ await workspaceClient.init();
+ expect(workspaceMock.initialized$.getValue()).toEqual(true);
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', {
+ method: 'POST',
+ body: JSON.stringify({
+ perPage: 999,
+ }),
+ });
+ });
+
+ it('#enterWorkspace', async () => {
+ const { workspaceClient, httpSetupMock, workspaceMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: false,
+ });
+ const result = await workspaceClient.enterWorkspace('foo');
+ expect(result.success).toEqual(false);
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ });
+ const successResult = await workspaceClient.enterWorkspace('foo');
+ expect(workspaceMock.currentWorkspaceId$.getValue()).toEqual('foo');
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', {
+ method: 'GET',
+ });
+ expect(successResult.success).toEqual(true);
+ });
+
+ it('#getCurrentWorkspaceId', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ });
+ await workspaceClient.enterWorkspace('foo');
+ expect(await workspaceClient.getCurrentWorkspaceId()).toEqual({
+ success: true,
+ result: 'foo',
+ });
+ });
+
+ it('#getCurrentWorkspace', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ result: {
+ name: 'foo',
+ },
+ });
+ await workspaceClient.enterWorkspace('foo');
+ expect(await workspaceClient.getCurrentWorkspace()).toEqual({
+ success: true,
+ result: {
+ name: 'foo',
+ },
+ });
+ });
+
+ it('#create', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ result: {
+ name: 'foo',
+ workspaces: [],
+ },
+ });
+ await workspaceClient.create({
+ name: 'foo',
+ });
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces', {
+ method: 'POST',
+ body: JSON.stringify({
+ attributes: {
+ name: 'foo',
+ },
+ }),
+ });
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', {
+ method: 'POST',
+ body: JSON.stringify({
+ perPage: 999,
+ }),
+ });
+ });
+
+ it('#delete', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ result: {
+ name: 'foo',
+ workspaces: [],
+ },
+ });
+ await workspaceClient.delete('foo');
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', {
+ method: 'DELETE',
+ });
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', {
+ method: 'POST',
+ body: JSON.stringify({
+ perPage: 999,
+ }),
+ });
+ });
+
+ it('#list', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ result: {
+ workspaces: [],
+ },
+ });
+ await workspaceClient.list({
+ perPage: 999,
+ });
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', {
+ method: 'POST',
+ body: JSON.stringify({
+ perPage: 999,
+ }),
+ });
+ });
+
+ it('#get', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ await workspaceClient.get('foo');
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', {
+ method: 'GET',
+ });
+ });
+
+ it('#update', async () => {
+ const { workspaceClient, httpSetupMock } = getWorkspaceClient();
+ httpSetupMock.fetch.mockResolvedValue({
+ success: true,
+ result: {
+ workspaces: [],
+ },
+ });
+ await workspaceClient.update('foo', {
+ name: 'foo',
+ });
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', {
+ method: 'PUT',
+ body: JSON.stringify({
+ attributes: {
+ name: 'foo',
+ },
+ }),
+ });
+ expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', {
+ method: 'POST',
+ body: JSON.stringify({
+ perPage: 999,
+ }),
+ });
+ });
+});
diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts
index 4dc023462fe1..9f1a106a0a50 100644
--- a/src/plugins/workspace/public/workspace_client.ts
+++ b/src/plugins/workspace/public/workspace_client.ts
@@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+
import {
HttpFetchError,
HttpFetchOptions,
@@ -102,7 +103,7 @@ export class WorkspaceClient {
}
};
- private getPath(path: Array): string {
+ private getPath(...path: Array): string {
return [WORKSPACES_API_BASE_URL, join(...path)].filter((item) => item).join('/');
}
@@ -179,7 +180,7 @@ export class WorkspaceClient {
permissions: WorkspaceRoutePermissionItem[];
}
): Promise> {
- const path = this.getPath([]);
+ const path = this.getPath();
const result = await this.safeFetch(path, {
method: 'POST',
@@ -202,7 +203,7 @@ export class WorkspaceClient {
* @returns
*/
public async delete(id: string): Promise> {
- const result = await this.safeFetch(this.getPath([id]), { method: 'DELETE' });
+ const result = await this.safeFetch(this.getPath(id), { method: 'DELETE' });
if (result.success) {
await this.updateWorkspaceList();
@@ -216,15 +217,15 @@ export class WorkspaceClient {
*
* @param {object} [options={}]
* @property {string} options.search
- * @property {string} options.search_fields - see OpenSearch Simple Query String
+ * @property {string} options.searchFields - see OpenSearch Simple Query String
* Query field argument for more information
* @property {integer} [options.page=1]
- * @property {integer} [options.per_page=20]
+ * @property {integer} [options.perPage=20]
* @property {array} options.fields
* @property {string array} permissionModes
* @returns A find result with workspaces matching the specified search.
*/
- public list = (
+ public list(
options?: WorkspaceFindOptions
): Promise<
IResponse<{
@@ -233,13 +234,13 @@ export class WorkspaceClient {
per_page: number;
page: number;
}>
- > => {
- const path = this.getPath(['_list']);
+ > {
+ const path = this.getPath('_list');
return this.safeFetch(path, {
method: 'POST',
body: JSON.stringify(options || {}),
});
- };
+ }
/**
* Fetches a single workspace
@@ -247,8 +248,8 @@ export class WorkspaceClient {
* @param {string} id
* @returns The workspace for the given id.
*/
- public async get(id: string): Promise> {
- const path = this.getPath([id]);
+ public get(id: string): Promise> {
+ const path = this.getPath(id);
return this.safeFetch(path, {
method: 'GET',
});
@@ -269,7 +270,7 @@ export class WorkspaceClient {
}
>
): Promise> {
- const path = this.getPath([id]);
+ const path = this.getPath(id);
const body = {
attributes,
};
diff --git a/src/plugins/workspace/server/index.ts b/src/plugins/workspace/server/index.ts
index 9447b7c6dc8c..fe44b4d71757 100644
--- a/src/plugins/workspace/server/index.ts
+++ b/src/plugins/workspace/server/index.ts
@@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+
import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server';
import { WorkspacePlugin } from './plugin';
import { configSchema } from '../config';
diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts
index 6a6f26856090..a7cdb8c97d4b 100644
--- a/src/plugins/workspace/server/plugin.ts
+++ b/src/plugins/workspace/server/plugin.ts
@@ -23,6 +23,8 @@ import {
} from './permission_control/client';
import { registerPermissionCheckRoutes } from './permission_control/routes';
import { WorkspacePluginConfigType } from '../config';
+import { cleanWorkspaceId, getWorkspaceIdFromUrl } from '../../../core/server/utils';
+
export class WorkspacePlugin implements Plugin<{}, {}> {
private readonly logger: Logger;
private client?: IWorkspaceDBImpl;
@@ -34,12 +36,11 @@ export class WorkspacePlugin implements Plugin<{}, {}> {
* Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to {basePath}{osdPath*}
*/
setupDeps.http.registerOnPreRouting(async (request, response, toolkit) => {
- const regexp = /\/w\/([^\/]*)/;
- const matchedResult = request.url.pathname.match(regexp);
+ const workspaceId = getWorkspaceIdFromUrl(request.url.toString());
- if (matchedResult) {
+ if (workspaceId) {
const requestUrl = new URL(request.url.toString());
- requestUrl.pathname = requestUrl.pathname.replace(regexp, '');
+ requestUrl.pathname = cleanWorkspaceId(requestUrl.pathname);
return toolkit.rewriteUrl(requestUrl.toString());
}
return toolkit.next();
diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts
index 00ad05f3c961..2701c2a767ab 100644
--- a/src/plugins/workspace/server/routes/index.ts
+++ b/src/plugins/workspace/server/routes/index.ts
@@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+
import { schema } from '@osd/config-schema';
import { ensureRawRequest } from '../../../../core/server';
diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts
index 2f46f5c7eb16..bf3337c4ae46 100644
--- a/src/plugins/workspace/server/types.ts
+++ b/src/plugins/workspace/server/types.ts
@@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+
import {
Logger,
OpenSearchDashboardsRequest,
diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts
index 56e40dfb3ebb..8fbb9ac7035e 100644
--- a/src/plugins/workspace/server/workspace_client.ts
+++ b/src/plugins/workspace/server/workspace_client.ts
@@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+
import { i18n } from '@osd/i18n';
import { omit } from 'lodash';
import type {