diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts index 27cfa9bf058..f80d41631b9 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 8c10d10017e..934e4cbc939 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 e60e506dfc0..5671064e4c5 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 3a8358d0f2a..1f26a26de8e 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 c0bfc1bc17a..6786739cd1d 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 e93355aa00e..00000000000 --- 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 e46cac0b4b5..4b9b2c86f64 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 d2c9e0086ad..42b01e72b0d 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 1b7d2940644..f2884c60581 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 00000000000..7d2a1f700c5 --- /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 00000000000..c369f95d581 --- /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 0102cf68473..2b150654563 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 63d88ae93e1..6be21538838 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 f1ede156e5a..85a383781bf 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 00000000000..594066e959f --- /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 00000000000..d98e0063dcf --- /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 0509eb095bc..b1081e92237 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 09528c2b080..f7c59dbfc53 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 00000000000..2ceeae5627d --- /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 00000000000..7d05c3f2245 --- /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 4dc023462fe..9f1a106a0a5 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 9447b7c6dc8..fe44b4d7175 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 6a6f2685609..a7cdb8c97d4 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 00ad05f3c96..2701c2a767a 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 2f46f5c7eb1..bf3337c4ae4 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 56e40dfb3eb..8fbb9ac7035 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 {