From b26aef0c9b1435073962159638cf8b54f010c102 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 12 Oct 2023 17:45:28 +0800 Subject: [PATCH] [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 --- .../collapsible_nav.test.tsx.snap | 8 + .../header/__snapshots__/header.test.tsx.snap | 4 + src/core/public/http/base_path.test.ts | 2 +- .../dashboard_listing.test.tsx.snap | 5 + .../dashboard_top_nav.test.tsx.snap | 6 + .../dashboard_empty_screen.test.tsx.snap | 3 + .../saved_objects_table.test.tsx.snap | 1 + .../__snapshots__/flyout.test.tsx.snap | 1 + ...telemetry_management_section.test.tsx.snap | 1 + src/plugins/workspace/common/constants.ts | 2 + .../workspace/opensearch_dashboards.json | 2 +- src/plugins/workspace/public/application.tsx | 26 +++ .../workspace_fatal_error.test.tsx.snap | 180 ++++++++++++++++++ .../components/workspace_fatal_error/index.ts | 6 + .../workspace_fatal_error.test.tsx | 71 +++++++ .../workspace_fatal_error.tsx | 68 +++++++ src/plugins/workspace/public/plugin.test.ts | 89 ++++++++- src/plugins/workspace/public/plugin.ts | 65 ++++++- src/plugins/workspace/public/types.ts | 9 + .../workspace/public/workspace_client.ts | 2 +- 20 files changed, 535 insertions(+), 16 deletions(-) create mode 100644 src/plugins/workspace/public/application.tsx 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/index.ts create mode 100644 src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.test.tsx create mode 100644 src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx create mode 100644 src/plugins/workspace/public/types.ts 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 62f00bee2c74..d6094f78e24b 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 @@ -60,6 +60,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={Object {}} @@ -2013,6 +2014,7 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={Object {}} @@ -2317,6 +2319,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={Object {}} @@ -2622,6 +2625,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={Object {}} @@ -3218,6 +3222,7 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -4335,6 +4340,7 @@ exports[`CollapsibleNav with custom branding renders the nav bar in default mode "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -5451,6 +5457,7 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -6560,6 +6567,7 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={Object {}} 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 790f24bc20e9..3d3e5a440c27 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 @@ -248,6 +248,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={Object {}} @@ -5953,6 +5954,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } closeNav={[Function]} @@ -7016,6 +7018,7 @@ exports[`Header renders condensed header 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -11492,6 +11495,7 @@ exports[`Header renders condensed header 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } closeNav={[Function]} diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts index 921ec13e6db2..641a04e4a60e 100644 --- a/src/core/public/http/base_path.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -157,4 +157,4 @@ describe('BasePath', () => { ).toEqual('/client_base_path/remove'); }); }); -}); +}); \ No newline at end of file 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 5e6645f56d9c..b8b555bd288b 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 @@ -862,6 +862,7 @@ exports[`dashboard listing hideWriteControls 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -2004,6 +2005,7 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -3207,6 +3209,7 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -4410,6 +4413,7 @@ exports[`dashboard listing renders table rows 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -5613,6 +5617,7 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], 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 262deaaaeaf0..ace497ea397e 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 @@ -754,6 +754,7 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -1721,6 +1722,7 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -2688,6 +2690,7 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -3655,6 +3658,7 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -4622,6 +4626,7 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -5589,6 +5594,7 @@ exports[`Dashboard top nav render with all components 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], 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 c2c83ff6f356..8c8043ae7a99 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 @@ -17,6 +17,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -386,6 +387,7 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -765,6 +767,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], 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 a76c1bfe32ab..8d04d1182d13 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 @@ -266,6 +266,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", } } canDelete={false} 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 d4a33e4a0569..7b6604c1657e 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 @@ -174,6 +174,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], 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 2761ce16fea3..7ad1fb8cd938 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 @@ -319,6 +319,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "prepend": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index e60bb6aea0eb..6ae89c0edad5 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +export const WORKSPACE_OVERVIEW_APP_ID = 'workspace_overview'; +export const WORKSPACE_FATAL_ERROR_APP_ID = 'workspace_fatal_error'; export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; export const WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace_conflict_control'; diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index f34106ab4fed..4443b7e99834 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -7,5 +7,5 @@ "savedObjects" ], "optionalPlugins": [], - "requiredBundles": [] + "requiredBundles": ["opensearchDashboardsReact"] } diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx new file mode 100644 index 000000000000..a6f496304889 --- /dev/null +++ b/src/plugins/workspace/public/application.tsx @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, ScopedHistory } from '../../../core/public'; +import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public'; +import { WorkspaceFatalError } from './components/workspace_fatal_error'; +import { Services } from './types'; + +export const renderFatalErrorApp = (params: AppMountParameters, services: Services) => { + const { element } = params; + const history = params.history as ScopedHistory<{ error?: string }>; + ReactDOM.render( + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; 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/index.ts b/src/plugins/workspace/public/components/workspace_fatal_error/index.ts new file mode 100644 index 000000000000..afb34b10d913 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_fatal_error/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { WorkspaceFatalError } from './workspace_fatal_error'; 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 new file mode 100644 index 000000000000..b1081e92237f --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiEmptyPrompt, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiCallOut, +} 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 '../../../../../core/public/utils'; + +export function WorkspaceFatalError(props: { error?: string }) { + const { + services: { application, http }, + } = useOpenSearchDashboards(); + const goBackToHome = () => { + window.location.href = formatUrlWithWorkspaceId( + application?.getUrlForApp('home') || '', + '', + http?.basePath as IBasePath + ); + }; + return ( + + + + + + + } + body={ +

+ +

+ } + actions={[ + + + , + ]} + /> + {props.error ? : null} +
+
+
+ ); +} diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index e54a20552329..f1b68a1f8fb9 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -3,24 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { waitFor } from '@testing-library/dom'; import { workspaceClientMock, WorkspaceClientMock } from './workspace_client.mock'; -import { chromeServiceMock, coreMock } from '../../../core/public/mocks'; +import { applicationServiceMock, chromeServiceMock, coreMock } from '../../../core/public/mocks'; import { WorkspacePlugin } from './plugin'; +import { WORKSPACE_FATAL_ERROR_APP_ID, WORKSPACE_OVERVIEW_APP_ID } from '../common/constants'; +import { Observable, Subscriber } from 'rxjs'; describe('Workspace plugin', () => { - const getSetupMock = () => ({ - ...coreMock.createSetup(), - chrome: chromeServiceMock.createSetupContract(), - }); beforeEach(() => { WorkspaceClientMock.mockClear(); Object.values(workspaceClientMock).forEach((item) => item.mockClear()); }); it('#setup', async () => { - const setupMock = getSetupMock(); + const setupMock = coreMock.createSetup(); const workspacePlugin = new WorkspacePlugin(); await workspacePlugin.setup(setupMock); + expect(setupMock.application.register).toBeCalledTimes(1); expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0); }); it('#call savedObjectsClient.setCurrentWorkspace when current workspace id changed', () => { @@ -30,6 +31,7 @@ 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'); @@ -41,11 +43,82 @@ describe('Workspace plugin', () => { }, } as any) ); - const setupMock = getSetupMock(); + 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); + expect(setupMock.application.register).toBeCalledTimes(1); + 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); - expect(setupMock.workspaces.currentWorkspaceId$.getValue()).toEqual('workspaceId'); + currentAppIdSubscriber?.next(WORKSPACE_FATAL_ERROR_APP_ID); + expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_OVERVIEW_APP_ID); windowSpy.mockRestore(); }); }); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 8a69d597c84b..346e243ce1bf 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -4,9 +4,13 @@ */ import type { Subscription } from 'rxjs'; -import { Plugin, CoreStart, CoreSetup } from '../../../core/public'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; import { WorkspaceClient } from './workspace_client'; +import { AppMountParameters, AppNavLinkStatus, CoreSetup, Plugin, CoreStart } from '../../../core/public'; +import { WORKSPACE_FATAL_ERROR_APP_ID, WORKSPACE_OVERVIEW_APP_ID } from '../common/constants'; +import { Services } from './types'; + +type WorkspaceAppType = (params: AppMountParameters, services: Services) => () => void; export class WorkspacePlugin implements Plugin<{}, {}, {}> { private coreStart?: CoreStart; @@ -20,8 +24,8 @@ export class WorkspacePlugin implements Plugin<{}, {}, {}> { }); } } - private getWorkspaceIdFromURL(basePath?: string): string | null { - return getWorkspaceIdFromUrl(window.location.href, basePath); + private getWorkspaceIdFromURL(): string | null { + return getWorkspaceIdFromUrl(window.location.href); } public async setup(core: CoreSetup) { const workspaceClient = new WorkspaceClient(core.http, core.workspaces); @@ -30,12 +34,63 @@ export class WorkspacePlugin implements Plugin<{}, {}, {}> { /** * Retrieve workspace id from url */ - const workspaceId = this.getWorkspaceIdFromURL(core.http.basePath.getBasePath()); + const workspaceId = this.getWorkspaceIdFromURL(); if (workspaceId) { - core.workspaces.currentWorkspaceId$.next(workspaceId); + const result = await workspaceClient.enterWorkspace(workspaceId); + if (!result.success) { + /** + * Fatal error service does not support customized actions + * So we have to use a self-hosted page to show the errors and redirect. + */ + (async () => { + const [{ application, chrome }] = await core.getStartServices(); + chrome.setIsVisible(false); + application.navigateToApp(WORKSPACE_FATAL_ERROR_APP_ID, { + replace: true, + state: { + error: result.error, + }, + }); + })(); + } else { + /** + * If the workspace id is valid and user is currently on workspace_fatal_error page, + * we should redirect user to overview page of workspace. + */ + (async () => { + const [{ application }] = await core.getStartServices(); + const currentAppIdSubscription = application.currentAppId$.subscribe((currentAppId) => { + if (currentAppId === WORKSPACE_FATAL_ERROR_APP_ID) { + application.navigateToApp(WORKSPACE_OVERVIEW_APP_ID); + } + currentAppIdSubscription.unsubscribe(); + }); + })(); + } } + const mountWorkspaceApp = async (params: AppMountParameters, renderApp: WorkspaceAppType) => { + const [coreStart] = await core.getStartServices(); + const services = { + ...coreStart, + workspaceClient, + }; + + return renderApp(params, services); + }; + + // workspace fatal error + core.application.register({ + id: WORKSPACE_FATAL_ERROR_APP_ID, + title: '', + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + const { renderFatalErrorApp } = await import('./application'); + return mountWorkspaceApp(params, renderFatalErrorApp); + }, + }); + return {}; } diff --git a/src/plugins/workspace/public/types.ts b/src/plugins/workspace/public/types.ts new file mode 100644 index 000000000000..1b3f38e50857 --- /dev/null +++ b/src/plugins/workspace/public/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart } from '../../../core/public'; +import { WorkspaceClient } from './workspace_client'; + +export type Services = CoreStart & { workspaceClient: WorkspaceClient }; diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts index 3e988f38b265..dc07a83ab1bd 100644 --- a/src/plugins/workspace/public/workspace_client.ts +++ b/src/plugins/workspace/public/workspace_client.ts @@ -291,4 +291,4 @@ export class WorkspaceClient { this.workspaces.workspaceList$.unsubscribe(); this.workspaces.currentWorkspaceId$.unsubscribe(); } -} +} \ No newline at end of file