From b53104bca37d9158c37850693700e87c899d1245 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 10 Jul 2023 10:12:01 +0800 Subject: [PATCH] feat: make url stateful (#35) * feat: make url stateful Signed-off-by: SuZhoue-Joe * feat: optimize code Signed-off-by: SuZhoue-Joe * feat: remove useless change Signed-off-by: SuZhoue-Joe * feat: optimize url listener Signed-off-by: SuZhoue-Joe * feat: make formatUrlWithWorkspaceId extensible Signed-off-by: SuZhoue-Joe * feat: modify to related components Signed-off-by: SuZhoue-Joe * feat: modify the async format to be sync function Signed-off-by: SuZhoue-Joe * feat: modify the async format to be sync function Signed-off-by: SuZhoue-Joe * fix: type check Signed-off-by: SuZhoue-Joe * feat: use path to maintain workspace info Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhoue-Joe Signed-off-by: SuZhou-Joe --- .../fatal_errors/fatal_errors_service.mock.ts | 1 + src/core/public/http/base_path.ts | 24 +++++-- src/core/public/http/http_service.ts | 3 +- src/core/public/http/types.ts | 11 +++- src/core/public/index.ts | 1 - .../injected_metadata_service.ts | 11 ++++ src/core/public/utils/index.ts | 1 + src/core/public/utils/workspace.ts | 15 +++++ src/core/public/workspace/consts.ts | 2 - src/core/public/workspace/index.ts | 1 - .../public/workspace/workspaces_service.ts | 30 +++------ .../server/workspaces/workspaces_service.ts | 19 ++++++ src/plugins/workspace/common/constants.ts | 1 - .../public/components/workspace_overview.tsx | 17 ----- .../workspace_updater/workspace_updater.tsx | 19 +++--- src/plugins/workspace/public/plugin.ts | 66 +++++++------------ 16 files changed, 118 insertions(+), 104 deletions(-) create mode 100644 src/core/public/utils/workspace.ts diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index e495d66ae568..a28547bf88ed 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -82,6 +82,7 @@ const createWorkspacesSetupContractMock = () => ({ update: jest.fn(), }, formatUrlWithWorkspaceId: jest.fn(), + setFormatUrlWithWorkspaceId: jest.fn(), }); const createWorkspacesStartContractMock = createWorkspacesSetupContractMock; diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index b31504676dba..8c45d707cf26 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -33,14 +33,28 @@ import { modifyUrl } from '@osd/std'; export class BasePath { constructor( private readonly basePath: string = '', - public readonly serverBasePath: string = basePath + public readonly serverBasePath: string = basePath, + private readonly workspaceBasePath: string = '' ) {} public get = () => { + return `${this.basePath}${this.workspaceBasePath}`; + }; + + public getBasePath = () => { return this.basePath; }; public prepend = (path: string): string => { + if (!this.get()) return path; + return modifyUrl(path, (parts) => { + if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { + parts.pathname = `${this.get()}${parts.pathname}`; + } + }); + }; + + public prependWithoutWorkspacePath = (path: string): string => { if (!this.basePath) return path; return modifyUrl(path, (parts) => { if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { @@ -50,16 +64,16 @@ export class BasePath { }; public remove = (path: string): string => { - if (!this.basePath) { + if (!this.get()) { return path; } - if (path === this.basePath) { + if (path === this.get()) { return '/'; } - if (path.startsWith(`${this.basePath}/`)) { - return path.slice(this.basePath.length); + if (path.startsWith(`${this.get()}/`)) { + return path.slice(this.get().length); } return path; diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index f26323f261aa..10d51bb2de7d 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -52,7 +52,8 @@ export class HttpService implements CoreService { const opensearchDashboardsVersion = injectedMetadata.getOpenSearchDashboardsVersion(); const basePath = new BasePath( injectedMetadata.getBasePath(), - injectedMetadata.getServerBasePath() + injectedMetadata.getServerBasePath(), + injectedMetadata.getWorkspaceBasePath() ); const fetchService = new Fetch({ basePath, opensearchDashboardsVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index ab046e6d2d5a..e5fb68b464e9 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -93,17 +93,17 @@ export type HttpStart = HttpSetup; */ export interface IBasePath { /** - * Gets the `basePath` string. + * Gets the `basePath + workspace` string. */ get: () => string; /** - * Prepends `path` with the basePath. + * Prepends `path` with the basePath + workspace. */ prepend: (url: string) => string; /** - * Removes the prepended basePath from the `path`. + * Removes the prepended basePath + workspace from the `path`. */ remove: (url: string) => string; @@ -113,6 +113,11 @@ export interface IBasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ readonly serverBasePath: string; + + /** + * Prepends `path` with the basePath. + */ + prependWithoutWorkspacePath: (url: string) => string; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 236048a012e7..ae35430d5800 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -356,5 +356,4 @@ export { WorkspacesService, WorkspaceAttribute, WorkspaceFindOptions, - WORKSPACE_ID_QUERYSTRING_NAME, } from './workspace'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index f4c6a7f7b91a..ccda2fbc925a 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -38,6 +38,7 @@ import { UserProvidedValues, } from '../../server/types'; import { AppCategory, Branding } from '../'; +import { getWorkspaceIdFromUrl } from '../utils'; export interface InjectedPluginMetadata { id: PluginName; @@ -151,6 +152,15 @@ export class InjectedMetadataService { getSurvey: () => { return this.state.survey; }, + + getWorkspaceBasePath: () => { + const workspaceId = getWorkspaceIdFromUrl(window.location.href); + if (workspaceId) { + return `/w/${workspaceId}`; + } + + return ''; + }, }; } } @@ -186,6 +196,7 @@ export interface InjectedMetadataSetup { }; getBranding: () => Branding; getSurvey: () => string | undefined; + getWorkspaceBasePath: () => string; } /** @internal */ diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index 7676b9482aac..0719f5e83c53 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -31,3 +31,4 @@ export { shareWeakReplay } from './share_weak_replay'; export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; +export { getWorkspaceIdFromUrl } from './workspace'; diff --git a/src/core/public/utils/workspace.ts b/src/core/public/utils/workspace.ts new file mode 100644 index 000000000000..e93355aa00e3 --- /dev/null +++ b/src/core/public/utils/workspace.ts @@ -0,0 +1,15 @@ +/* + * 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/consts.ts b/src/core/public/workspace/consts.ts index 662baeaa5d19..b02fa29f1013 100644 --- a/src/core/public/workspace/consts.ts +++ b/src/core/public/workspace/consts.ts @@ -5,8 +5,6 @@ export const WORKSPACES_API_BASE_URL = '/api/workspaces'; -export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; - export enum WORKSPACE_ERROR_REASON_MAP { WORKSPACE_STALED = 'WORKSPACE_STALED', } diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index d0fb17ead0c1..359eee93f664 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -5,4 +5,3 @@ export { WorkspacesClientContract, WorkspacesClient } from './workspaces_client'; export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service'; export type { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; -export { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index 908530885760..7d30ac52f49f 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -5,7 +5,6 @@ import { CoreService } from 'src/core/types'; import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; import type { WorkspaceAttribute } from '../../server/types'; -import { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; import { HttpSetup } from '../http'; /** @@ -14,43 +13,34 @@ import { HttpSetup } from '../http'; export interface WorkspacesStart { client: WorkspacesClientContract; formatUrlWithWorkspaceId: (url: string, id: WorkspaceAttribute['id']) => string; + setFormatUrlWithWorkspaceId: (formatFn: WorkspacesStart['formatUrlWithWorkspaceId']) => void; } export type WorkspacesSetup = WorkspacesStart; -function setQuerystring(url: string, params: Record): string { - const urlObj = new URL(url); - const searchParams = new URLSearchParams(urlObj.search); - - for (const key in params) { - if (params.hasOwnProperty(key)) { - const value = params[key]; - searchParams.set(key, value); - } - } - - urlObj.search = searchParams.toString(); - return urlObj.toString(); -} - export class WorkspacesService implements CoreService { private client?: WorkspacesClientContract; private formatUrlWithWorkspaceId(url: string, id: string) { - return setQuerystring(url, { - [WORKSPACE_ID_QUERYSTRING_NAME]: id, - }); + return url; + } + private setFormatUrlWithWorkspaceId(formatFn: WorkspacesStart['formatUrlWithWorkspaceId']) { + this.formatUrlWithWorkspaceId = formatFn; } public async setup({ http }: { http: HttpSetup }) { this.client = new WorkspacesClient(http); return { client: this.client, - formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + formatUrlWithWorkspaceId: (url: string, id: string) => this.formatUrlWithWorkspaceId(url, id), + setFormatUrlWithWorkspaceId: (fn: WorkspacesStart['formatUrlWithWorkspaceId']) => + this.setFormatUrlWithWorkspaceId(fn), }; } public async start(): Promise { return { client: this.client as WorkspacesClientContract, formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + setFormatUrlWithWorkspaceId: (fn: WorkspacesStart['formatUrlWithWorkspaceId']) => + this.setFormatUrlWithWorkspaceId(fn), }; } public async stop() { diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 6faec9a6496e..7aa01db34beb 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -2,6 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { URL } from 'node:url'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { InternalHttpServiceSetup } from '../http'; @@ -43,11 +44,29 @@ export class WorkspacesService this.logger = coreContext.logger.get('workspaces-service'); } + private proxyWorkspaceTrafficToRealHandler(setupDeps: WorkspacesSetupDeps) { + /** + * Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to + * {basePath}{osdPath*} + */ + setupDeps.http.registerOnPreRouting((request, response, toolkit) => { + const regexp = /\/w\/([^\/]*)/; + const matchedResult = request.url.pathname.match(regexp); + if (matchedResult) { + const requestUrl = new URL(request.url.toString()); + requestUrl.pathname = requestUrl.pathname.replace(regexp, ''); + return toolkit.rewriteUrl(requestUrl.toString()); + } + return toolkit.next(); + }); + } + public async setup(setupDeps: WorkspacesSetupDeps): Promise { this.logger.debug('Setting up Workspaces service'); this.client = new WorkspacesClientWithSavedObject(setupDeps); await this.client.setup(setupDeps); + this.proxyWorkspaceTrafficToRealHandler(setupDeps); registerRoutes({ http: setupDeps.http, diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 2f67640bcd3f..557b889d6111 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -5,7 +5,6 @@ export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; -export const WORKSPACE_ID_IN_SESSION_STORAGE = '_workspace_id_'; export const PATHS = { create: '/create', diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx index 97c9a07092d9..55de87d20b66 100644 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -7,11 +7,8 @@ import React, { useState } from 'react'; import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { useObservable } from 'react-use'; import { of } from 'rxjs'; -import { i18n } from '@osd/i18n'; import { ApplicationStart } from '../../../../core/public'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { PATHS } from '../../common/constants'; -import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../../common/constants'; export const WorkspaceOverview = () => { const { @@ -22,20 +19,6 @@ export const WorkspaceOverview = () => { workspaces ? workspaces.client.currentWorkspace$ : of(null) ); - const onUpdateWorkspaceClick = () => { - if (!currentWorkspace || !currentWorkspace.id) { - notifications?.toasts.addDanger({ - title: i18n.translate('Cannot find current workspace', { - defaultMessage: 'Cannot update workspace', - }), - }); - return; - } - application.navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.update + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, - }); - }; - return ( <> diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index f3c8be0abb48..a3dc973ee095 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -21,11 +21,7 @@ import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearc import { PATHS } from '../../../common/constants'; import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; -import { - WORKSPACE_APP_ID, - WORKSPACE_ID_IN_SESSION_STORAGE, - WORKSPACE_OP_TYPE_UPDATE, -} from '../../../common/constants'; +import { WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; import { ApplicationStart } from '../../../../../core/public'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; @@ -80,9 +76,14 @@ export const WorkspaceUpdater = () => { defaultMessage: 'Update workspace successfully', }), }); - await application.navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.overview + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, - }); + window.location.href = + workspaces?.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.overview, + absolute: true, + }), + currentWorkspace.id + ) || ''; return; } notifications?.toasts.addDanger({ @@ -92,7 +93,7 @@ export const WorkspaceUpdater = () => { text: result?.error, }); }, - [notifications?.toasts, workspaces?.client, currentWorkspace, application] + [notifications?.toasts, workspaces, currentWorkspace, application] ); if (!currentWorkspaceFormData.name) { diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 4933cda2a43a..07f1b84f32fe 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -11,59 +11,42 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE, PATHS } from '../common/constants'; -import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; +import { WORKSPACE_APP_ID, PATHS } from '../common/constants'; import { mountDropdownList } from './mount'; +import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; export class WorkspacesPlugin implements Plugin<{}, {}> { private core?: CoreSetup; - private addWorkspaceListener() { - this.core?.workspaces.client.currentWorkspaceId$.subscribe((newWorkspaceId) => { - try { - sessionStorage.setItem(WORKSPACE_ID_IN_SESSION_STORAGE, newWorkspaceId); - } catch (e) { - /** - * in incognize mode, this method may throw an error - * */ - } - }); + private getWorkpsaceIdFromURL(): string | null { + return getWorkspaceIdFromUrl(window.location.href); } - private getWorkpsaceIdFromQueryString(): string | null { - const searchParams = new URLSearchParams(window.location.search); - return searchParams.get(WORKSPACE_ID_QUERYSTRING_NAME); - } - private getWorkpsaceIdFromSessionStorage(): string { - try { - return sessionStorage.getItem(WORKSPACE_ID_IN_SESSION_STORAGE) || ''; - } catch (e) { - /** - * in incognize mode, this method may throw an error - * */ - return ''; - } - } - private clearWorkspaceIdFromSessionStorage(): void { - try { - sessionStorage.removeItem(WORKSPACE_ID_IN_SESSION_STORAGE); - } catch (e) { - /** - * in incognize mode, this method may throw an error - * */ + private getPatchedUrl = (url: string, workspaceId: string) => { + const newUrl = new URL(url, window.location.href); + /** + * Patch workspace id into path + */ + newUrl.pathname = this.core?.http.basePath.remove(newUrl.pathname) || ''; + if (workspaceId) { + newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}/w/${workspaceId}${ + newUrl.pathname + }`; + } else { + newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}${newUrl.pathname}`; } - } + + return newUrl.toString(); + }; public async setup(core: CoreSetup) { this.core = core; + this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); /** - * Retrive workspace id from url or sessionstorage - * url > sessionstorage + * Retrive workspace id from url */ - const workspaceId = - this.getWorkpsaceIdFromQueryString() || this.getWorkpsaceIdFromSessionStorage(); + const workspaceId = this.getWorkpsaceIdFromURL(); if (workspaceId) { const result = await core.workspaces.client.enterWorkspace(workspaceId); if (!result.success) { - this.clearWorkspaceIdFromSessionStorage(); core.fatalErrors.add( result.error || i18n.translate('workspace.error.setup', { @@ -73,11 +56,6 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } } - /** - * register a listener - */ - this.addWorkspaceListener(); - core.application.register({ id: WORKSPACE_APP_ID, title: i18n.translate('workspace.settings.title', {