From e4b45a99e26c6bfe3c32cb83752fa64fd231a448 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 11 Aug 2023 16:50:51 +0800 Subject: [PATCH 1/5] Show objects without workspace info when no workspaces are provided in find query. (#83) * temp: modify Signed-off-by: SuZhou-Joe * feat: update Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- .../workspace_saved_objects_client_wrapper.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 170498fb40bb..5b2b45b353d7 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -327,6 +327,16 @@ export class WorkspaceSavedObjectsClientWrapper { workspaces: permittedWorkspaceIds, }, }, + // TODO: remove this child clause when home workspace proposal is finalized. + { + bool: { + must_not: { + exists: { + field: 'workspaces', + }, + }, + }, + }, ], }, }, From 9393525acca195c793598d67fe375b3a537ac75b Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 15 Aug 2023 10:34:34 +0800 Subject: [PATCH 2/5] feat: call saved objects with internal user (#80) Signed-off-by: Lin Wang --- src/plugins/workspace/server/plugin.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 1189af540d88..710eaeea1819 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -17,6 +17,7 @@ import { MANAGEMENT_WORKSPACE, Permissions, WorkspacePermissionMode, + SavedObjectsClient, } from '../../../core/server'; import { IWorkspaceDBImpl, WorkspaceAttribute } from './types'; import { WorkspaceClientWithSavedObject } from './workspace_client'; @@ -72,6 +73,10 @@ export class WorkspacePlugin implements Plugin<{}, {}> { client: this.client as IWorkspaceDBImpl, }); + core.savedObjects.setClientFactoryProvider((repositoryFactory) => () => + new SavedObjectsClient(repositoryFactory.createInternalRepository()) + ); + return { client: this.client, }; From b49aa1f5eaf0c226cfc5fecdbaf7eedb51208bcb Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Thu, 17 Aug 2023 07:55:58 +0800 Subject: [PATCH 3/5] feat: workspace context menu and picker menu (#86) * place current workspace at the top of worksapce list Signed-off-by: yuye-aws * prototype for workspace context menu and picker menu Signed-off-by: yuye-aws * resolve import issue and add props to test Signed-off-by: yuye-aws * move formatUrlWithWorkspaceId from plugin workspace to core Signed-off-by: yuye-aws * add workspaceEnabled props Signed-off-by: yuye-aws * implement logo and color for context and picker menu Signed-off-by: yuye-aws * bold texts Signed-off-by: yuye-aws * workspace disabled left menu header Signed-off-by: yuye-aws * move workspace applications to picker menu and context menu Signed-off-by: yuye-aws * refactor workspace disabled logic Signed-off-by: yuye-aws * add app id constants Signed-off-by: yuye-aws * only highlight current workspace Signed-off-by: yuye-aws * fix type error and key error Signed-off-by: yuye-aws * fix icon bug and import management workspace const Signed-off-by: yuye-aws * change const order Signed-off-by: yuye-aws * warp string with i18n Signed-off-by: yuye-aws * refactor getFilteredWorkspaceList function Signed-off-by: yuye-aws * remove unused props Signed-off-by: yuye-aws * avoid inline styles Signed-off-by: yuye-aws --------- Signed-off-by: yuye-aws --- src/core/public/chrome/chrome_service.tsx | 7 +- src/core/public/chrome/constants.ts | 4 + .../chrome/ui/header/collapsible_nav.test.tsx | 3 + .../chrome/ui/header/collapsible_nav.tsx | 12 + .../ui/header/collapsible_nav_header.tsx | 237 ++++++++++++++++++ .../public/chrome/ui/header/header.test.tsx | 3 +- src/core/public/chrome/ui/header/header.tsx | 7 +- src/core/public/chrome/ui/header/nav_link.tsx | 1 - src/core/public/core_system.ts | 1 + src/core/public/utils/index.ts | 2 +- src/core/public/utils/workspace.ts | 27 ++ .../workspace/workspaces_service.mock.ts | 2 + .../public/workspace/workspaces_service.ts | 4 + .../public/components/utils/workspace.ts | 2 +- .../workspace_creator/workspace_creator.tsx | 6 +- .../workspace_updater/workspace_updater.tsx | 2 +- src/plugins/workspace/public/plugin.ts | 55 +--- src/plugins/workspace/public/utils.ts | 31 --- 18 files changed, 312 insertions(+), 94 deletions(-) create mode 100644 src/core/public/chrome/ui/header/collapsible_nav_header.tsx delete mode 100644 src/plugins/workspace/public/utils.ts diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index e3ccfdb6d1d2..560dc86ab36a 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,7 +34,7 @@ import { FormattedMessage } from '@osd/i18n/react'; import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { EuiLink } from '@elastic/eui'; -import { mountReactNode } from '../utils/mount'; +import { mountReactNode } from '../utils'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; import { HttpStart } from '../http'; @@ -48,7 +48,7 @@ import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; -import { Branding } from '../'; +import { Branding, WorkspaceStart } from '../'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -99,6 +99,7 @@ interface StartDeps { injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; + workspaces: WorkspaceStart; } /** @internal */ @@ -152,6 +153,7 @@ export class ChromeService { injectedMetadata, notifications, uiSettings, + workspaces, }: StartDeps): Promise { this.initVisibility(application); @@ -262,6 +264,7 @@ export class ChromeService { isLocked$={getIsNavDrawerLocked$} branding={injectedMetadata.getBranding()} survey={injectedMetadata.getSurvey()} + workspaces={workspaces} /> ), diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 5008f8b4a69a..9fcf531f3ffe 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -31,3 +31,7 @@ export const OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK = 'https://forum.opensearch.org/'; export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose'; + +export const WORKSPACE_CREATE_APP_ID = 'workspace_create'; +export const WORKSPACE_LIST_APP_ID = 'workspace_list'; +export const WORKSPACE_OVERVIEW_APP_ID = 'workspace_overview'; diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index ebca8014a205..414e9d56fe78 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -37,6 +37,7 @@ import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..'; import { httpServiceMock } from '../../../http/http_service.mock'; import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; import { CollapsibleNav } from './collapsible_nav'; +import { workspacesServiceMock } from '../../../workspace/workspaces_service.mock'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -79,6 +80,8 @@ function mockProps() { closeNav: () => {}, navigateToApp: () => Promise.resolve(), navigateToUrl: () => Promise.resolve(), + getUrlForApp: jest.fn(), + workspaces: workspacesServiceMock.createStartContract(), customNavLink$: new BehaviorSubject(undefined), branding: { darkMode: false, diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 9a7667e365f1..e119cc6a05b0 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -43,6 +43,7 @@ import { groupBy, sortBy } from 'lodash'; import React, { useRef } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; +import { WorkspaceStart } from 'opensearch-dashboards/public'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; import { InternalApplicationStart } from '../../../application'; @@ -50,6 +51,7 @@ import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; import { createEuiListItem, isModifiedOrPrevented, createRecentNavLink } from './nav_link'; import { ChromeBranding } from '../../chrome_service'; +import { CollapsibleNavHeader } from './collapsible_nav_header'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -121,10 +123,12 @@ interface Props { storage?: Storage; onIsLockedUpdate: OnIsLockedUpdate; closeNav: () => void; + getUrlForApp: InternalApplicationStart['getUrlForApp']; navigateToApp: InternalApplicationStart['navigateToApp']; navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; branding: ChromeBranding; + workspaces: WorkspaceStart; } export function CollapsibleNav({ @@ -136,9 +140,11 @@ export function CollapsibleNav({ storage = window.localStorage, onIsLockedUpdate, closeNav, + getUrlForApp, navigateToApp, navigateToUrl, branding, + workspaces, ...observables }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); @@ -215,6 +221,12 @@ export function CollapsibleNav({ outsideClickCloses={false} > + + {/* Recently viewed */} workspace.id !== MANAGEMENT_WORKSPACE && workspace.id !== currentWorkspace?.id + ), + ].slice(0, 5); +} + +export function CollapsibleNavHeader({ workspaces, getUrlForApp, basePath }: Props) { + const workspaceEnabled = useObservable(workspaces.workspaceEnabled$, false); + const workspaceList = useObservable(workspaces.workspaceList$, []); + const currentWorkspace = useObservable(workspaces.currentWorkspace$, null); + const filteredWorkspaceList = getFilteredWorkspaceList(workspaceList, currentWorkspace); + const defaultHeaderName = i18n.translate( + 'core.ui.primaryNav.workspacePickerMenu.defaultHeaderName', + { + defaultMessage: 'OpenSearch Analytics', + } + ); + const managementWorkspaceName = + workspaceList.find((workspace) => workspace.id === MANAGEMENT_WORKSPACE)?.name ?? + i18n.translate('core.ui.primaryNav.workspacePickerMenu.managementWorkspaceName', { + defaultMessage: 'Management', + }); + const currentWorkspaceName = currentWorkspace?.name ?? defaultHeaderName; + const [isPopoverOpen, setPopover] = useState(false); + + if (!workspaceEnabled) { + return ( + + + + + + + + {defaultHeaderName} + + + + + ); + } + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const workspaceToItem = (workspace: WorkspaceAttribute, index: number) => { + const href = formatUrlWithWorkspaceId( + getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, { + absolute: false, + }), + workspace.id, + basePath + ); + const name = + currentWorkspace !== null && index === 0 ? ( + + {workspace.name} + + ) : ( + workspace.name + ); + return { + href, + name, + key: index.toString(), + icon: , + }; + }; + + const getWorkspaceListItems = () => { + const workspaceListItems = filteredWorkspaceList.map((workspace, index) => + workspaceToItem(workspace, index) + ); + const length = workspaceListItems.length; + workspaceListItems.push({ + icon: , + name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.createWorkspace', { + defaultMessage: 'Create workspace', + }), + key: length.toString(), + href: formatUrlWithWorkspaceId( + getUrlForApp(WORKSPACE_CREATE_APP_ID, { + absolute: false, + }), + currentWorkspace?.id ?? '', + basePath + ), + }); + workspaceListItems.push({ + icon: , + name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.allWorkspace', { + defaultMessage: 'All workspaces', + }), + key: (length + 1).toString(), + href: formatUrlWithWorkspaceId( + getUrlForApp(WORKSPACE_LIST_APP_ID, { + absolute: false, + }), + currentWorkspace?.id ?? '', + basePath + ), + }); + return workspaceListItems; + }; + + const currentWorkspaceButton = ( + + + + + + + + {currentWorkspaceName} + + + + + + + + ); + + const currentWorkspaceTitle = ( + + + + + + + {currentWorkspaceName} + + + + + + + ); + + const panels = [ + { + id: 0, + title: currentWorkspaceTitle, + items: [ + { + name: ( + + + {i18n.translate('core.ui.primaryNav.workspacePickerMenu.workspaceList', { + defaultMessage: 'Workspaces', + })} + + + ), + icon: 'folderClosed', + panel: 1, + }, + { + name: managementWorkspaceName, + icon: 'managementApp', + href: formatUrlWithWorkspaceId( + getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, { + absolute: false, + }), + MANAGEMENT_WORKSPACE, + basePath + ), + }, + ], + }, + { + id: 1, + title: 'Workspaces', + items: getWorkspaceListItems(), + }, + ]; + + return ( + + + + ); +} diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 319dea4c394b..75dbd107ebff 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -33,7 +33,7 @@ import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { httpServiceMock } from '../../../http/http_service.mock'; -import { applicationServiceMock } from '../../../mocks'; +import { applicationServiceMock, workspacesServiceMock } from '../../../mocks'; import { Header } from './header'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; @@ -76,6 +76,7 @@ function mockProps() { applicationTitle: 'OpenSearch Dashboards', }, survey: '/', + workspaces: workspacesServiceMock.createStartContract(), }; } diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 9496b76b9980..07c18b883e7a 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -44,6 +44,7 @@ import classnames from 'classnames'; import React, { createRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; +import { WorkspaceStart } from 'opensearch-dashboards/public'; import { LoadingIndicator } from '../'; import { ChromeBadge, @@ -52,7 +53,7 @@ import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, } from '../..'; -import { InternalApplicationStart } from '../../../application/types'; +import { InternalApplicationStart } from '../../../application'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension, ChromeBranding } from '../../chrome_service'; import { OnIsLockedUpdate } from './'; @@ -91,6 +92,7 @@ export interface HeaderProps { onIsLockedUpdate: OnIsLockedUpdate; branding: ChromeBranding; survey: string | undefined; + workspaces: WorkspaceStart; } export function Header({ @@ -102,6 +104,7 @@ export function Header({ homeHref, branding, survey, + workspaces, ...observables }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); @@ -249,6 +252,7 @@ export function Header({ isNavOpen={isNavOpen} homeHref={homeHref} basePath={basePath} + getUrlForApp={application.getUrlForApp} navigateToApp={application.navigateToApp} navigateToUrl={application.navigateToUrl} onIsLockedUpdate={onIsLockedUpdate} @@ -260,6 +264,7 @@ export function Header({ }} customNavLink$={observables.customNavLink$} branding={branding} + workspaces={workspaces} /> diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 832708122d5e..ea35e192e7bd 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -73,7 +73,6 @@ export function createEuiListItem({ } if ( - !link.externalLink && // ignore external links event.button === 0 && // ignore everything but left clicks !isModifiedOrPrevented(event) ) { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index d4683087cdab..356e3dd0690a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -233,6 +233,7 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, + workspaces, }); this.coreApp.start({ application, http, notifications, uiSettings }); diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index 32ea0ba3c101..13d01ef1fe56 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -31,5 +31,5 @@ export { shareWeakReplay } from './share_weak_replay'; export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; -export { getWorkspaceIdFromUrl, WORKSPACE_TYPE } from './workspace'; +export { getWorkspaceIdFromUrl, WORKSPACE_TYPE, formatUrlWithWorkspaceId } from './workspace'; export { WORKSPACE_PATH_PREFIX, PUBLIC_WORKSPACE, MANAGEMENT_WORKSPACE } from '../../utils'; diff --git a/src/core/public/utils/workspace.ts b/src/core/public/utils/workspace.ts index 9a0f55b3fa8c..33012d4fbe4a 100644 --- a/src/core/public/utils/workspace.ts +++ b/src/core/public/utils/workspace.ts @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { IBasePath } from '../http'; +import { WORKSPACE_PATH_PREFIX } from '../../utils'; + export const getWorkspaceIdFromUrl = (url: string): string => { const regexp = /\/w\/([^\/]*)/; const urlObject = new URL(url); @@ -14,4 +17,28 @@ export const getWorkspaceIdFromUrl = (url: string): string => { return ''; }; +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(); +}; + export const WORKSPACE_TYPE = 'workspace'; diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index 08e2ae597713..57b9d976e4e5 100644 --- a/src/core/public/workspace/workspaces_service.mock.ts +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -9,11 +9,13 @@ import { WorkspaceAttribute } from '../workspace'; const currentWorkspaceId$ = new BehaviorSubject(''); const workspaceList$ = new BehaviorSubject([]); const currentWorkspace$ = new BehaviorSubject(null); +const workspaceEnabled$ = new BehaviorSubject(false); const createWorkspacesSetupContractMock = () => ({ currentWorkspaceId$, workspaceList$, currentWorkspace$, + workspaceEnabled$, }); const createWorkspacesStartContractMock = createWorkspacesSetupContractMock; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index eb75bc2e81f5..edf4ec998731 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -12,6 +12,7 @@ export interface WorkspaceStart { currentWorkspaceId$: BehaviorSubject; currentWorkspace$: BehaviorSubject; workspaceList$: BehaviorSubject; + workspaceEnabled$: BehaviorSubject; } export type WorkspaceSetup = WorkspaceStart; @@ -30,12 +31,14 @@ export class WorkspaceService implements CoreService(''); private workspaceList$ = new BehaviorSubject([]); private currentWorkspace$ = new BehaviorSubject(null); + private workspaceEnabled$ = new BehaviorSubject(false); public setup(): WorkspaceSetup { return { currentWorkspaceId$: this.currentWorkspaceId$, currentWorkspace$: this.currentWorkspace$, workspaceList$: this.workspaceList$, + workspaceEnabled$: this.workspaceEnabled$, }; } @@ -44,6 +47,7 @@ export class WorkspaceService implements CoreService; 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 eec5a03392aa..bfdbc1536fc3 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -6,12 +6,10 @@ import React, { useCallback } from 'react'; import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; import { i18n } from '@osd/i18n'; - -import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; - +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceForm, WorkspaceFormData } 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_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index c474b4c3a2df..972dc91d40c8 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -20,7 +20,7 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; import { WORKSPACE_OVERVIEW_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; -import { formatUrlWithWorkspaceId } from '../../utils'; +import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; import { WorkspaceClient } from '../../workspace_client'; export const WorkspaceUpdater = () => { diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index ebc204a351a3..aafb22ead142 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -8,7 +8,6 @@ import type { Subscription } from 'rxjs'; import { combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import { - ApplicationStart, AppMountParameters, AppNavLinkStatus, ChromeNavLink, @@ -17,7 +16,6 @@ import { Plugin, WorkspaceAttribute, DEFAULT_APP_CATEGORIES, - HttpSetup, } from '../../../core/public'; import { WORKSPACE_LIST_APP_ID, @@ -30,7 +28,6 @@ import { mountDropdownList } from './mount'; import { SavedObjectsManagementPluginSetup } from '../../saved_objects_management/public'; import { getWorkspaceColumn } from './components/utils/workspace_column'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; -import { formatUrlWithWorkspaceId } from './utils'; import { WorkspaceClient } from './workspace_client'; import { Services } from './application'; @@ -47,6 +44,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> public async setup(core: CoreSetup, { savedObjectsManagement }: WorkspacePluginSetupDeps) { const workspaceClient = new WorkspaceClient(core.http, core.workspaces); workspaceClient.init(); + core.workspaces.workspaceEnabled$.next(true); /** * Retrieve workspace id from url @@ -133,7 +131,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> }), euiIconType: 'folderClosed', category: WORKSPACE_NAV_CATEGORY, - navLinkStatus: workspaceId ? AppNavLinkStatus.hidden : AppNavLinkStatus.default, + navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { const { renderListApp } = await import('./application'); return mountWorkspaceApp(params, renderListApp); @@ -143,36 +141,6 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> return {}; } - private workspaceToChromeNavLink( - workspace: WorkspaceAttribute, - http: HttpSetup, - application: ApplicationStart, - index: number - ): ChromeNavLink { - const id = WORKSPACE_OVERVIEW_APP_ID + '/' + workspace.id; - const url = formatUrlWithWorkspaceId( - application.getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, { - absolute: true, - }), - workspace.id, - http.basePath - ); - return { - id, - url, - order: index, - hidden: false, - disabled: false, - baseUrl: url, - href: url, - category: WORKSPACE_NAV_CATEGORY, - title: i18n.translate('core.ui.workspaceNavList.workspaceName', { - defaultMessage: workspace.name, - }), - externalLink: true, - }; - } - private async _changeSavedObjectCurrentWorkspace() { if (this.coreStart) { return this.coreStart.workspaces.currentWorkspaceId$.subscribe((currentWorkspaceId) => { @@ -183,10 +151,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> } } - private filterByWorkspace( - workspace: WorkspaceAttribute | null | undefined, - allNavLinks: ChromeNavLink[] - ) { + private filterByWorkspace(workspace: WorkspaceAttribute | null, allNavLinks: ChromeNavLink[]) { if (!workspace) return allNavLinks; const features = workspace.features ?? []; return allNavLinks.filter((item) => features.includes(item.id)); @@ -195,28 +160,16 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> private filterNavLinks(core: CoreStart) { const navLinksService = core.chrome.navLinks; const chromeNavLinks$ = navLinksService.getNavLinks$(); - const workspaceList$ = core.workspaces.workspaceList$; const currentWorkspace$ = core.workspaces.currentWorkspace$; combineLatest([ - workspaceList$, chromeNavLinks$.pipe(map(this.changeCategoryNameByWorkspaceFeatureFlag)), currentWorkspace$, - ]).subscribe(([workspaceList, chromeNavLinks, currentWorkspace]) => { + ]).subscribe(([chromeNavLinks, currentWorkspace]) => { const filteredNavLinks = new Map(); chromeNavLinks = this.filterByWorkspace(currentWorkspace, chromeNavLinks); chromeNavLinks.forEach((chromeNavLink) => { filteredNavLinks.set(chromeNavLink.id, chromeNavLink); }); - if (!currentWorkspace) { - workspaceList - .filter((workspace, index) => index < 5) - .map((workspace, index) => - this.workspaceToChromeNavLink(workspace, core.http, core.application, index) - ) - .forEach((workspaceNavLink) => - filteredNavLinks.set(workspaceNavLink.id, workspaceNavLink) - ); - } navLinksService.setFilteredNavLinks(filteredNavLinks); }); } diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts deleted file mode 100644 index 71d3e3c1caf2..000000000000 --- a/src/plugins/workspace/public/utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { IBasePath } from '../../../core/public'; -import { WORKSPACE_PATH_PREFIX } from '../../../core/public/utils'; - -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(); -}; From 7b6800f7aaf59d1e41870b201b451d1b22b1ccf1 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 17 Aug 2023 11:48:28 +0800 Subject: [PATCH 4/5] fix: permission check error (#88) Signed-off-by: SuZhou-Joe --- .../workspace_saved_objects_client_wrapper.ts | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 5b2b45b353d7..dbde03f91150 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -73,16 +73,15 @@ export class WorkspaceSavedObjectsClientWrapper { if (!workspaceId) { return; } - if ( - !(await this.permissionControl.validate( - request, - { - type: WORKSPACE_TYPE, - id: workspaceId, - }, - this.formatWorkspacePermissionModeToStringArray(permissionMode) - )) - ) { + const validateResult = await this.permissionControl.validate( + request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + this.formatWorkspacePermissionModeToStringArray(permissionMode) + ); + if (!validateResult?.result) { throw generateWorkspacePermissionError(); } } @@ -96,16 +95,15 @@ export class WorkspaceSavedObjectsClientWrapper { return; } for (const workspaceId of workspaces) { - if ( - !(await this.permissionControl.validate( - request, - { - type: WORKSPACE_TYPE, - id: workspaceId, - }, - this.formatWorkspacePermissionModeToStringArray(permissionMode) - )) - ) { + const validateResult = await this.permissionControl.validate( + request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + this.formatWorkspacePermissionModeToStringArray(permissionMode) + ); + if (!validateResult?.result) { throw generateWorkspacePermissionError(); } } @@ -121,16 +119,15 @@ export class WorkspaceSavedObjectsClientWrapper { } let permitted = false; for (const workspaceId of workspaces) { - if ( - await this.permissionControl.validate( - request, - { - type: WORKSPACE_TYPE, - id: workspaceId, - }, - this.formatWorkspacePermissionModeToStringArray(permissionMode) - ) - ) { + const validateResult = await this.permissionControl.validate( + request, + { + type: WORKSPACE_TYPE, + id: workspaceId, + }, + this.formatWorkspacePermissionModeToStringArray(permissionMode) + ); + if (validateResult?.result) { permitted = true; break; } From d73283388a9794f13af42385d7ee017c4a14f75e Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Thu, 17 Aug 2023 14:01:41 +0800 Subject: [PATCH 5/5] fix: redirect to home only when delete and exit workspace successfully (#89) * only navigate to home page when delete and exit workspace successfully Signed-off-by: yuye-aws * unsubscribe workspaceEnabled when workspace service stop Signed-off-by: yuye-aws * only hide delete modal when delete workspace successfully Signed-off-by: yuye-aws --------- Signed-off-by: yuye-aws --- .../public/workspace/workspaces_service.ts | 1 + .../workspace_updater/workspace_updater.tsx | 45 ++++++++++--------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index edf4ec998731..142ac67fed38 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -55,5 +55,6 @@ export class WorkspaceService implements CoreService { defaultMessage: 'Delete workspace successfully', }), }); + setDeleteWorkspaceModalVisible(false); + if (http && application) { + const homeUrl = application.getUrlForApp('home', { + path: '/', + absolute: false, + }); + const targetUrl = http.basePath.prepend(http.basePath.remove(homeUrl), { + withoutWorkspace: true, + }); + await application.navigateToUrl(targetUrl); + } } else { notifications?.toasts.addDanger({ title: i18n.translate('workspace.delete.failed', { @@ -126,17 +137,6 @@ export const WorkspaceUpdater = () => { }); } } - setDeleteWorkspaceModalVisible(false); - if (http && application) { - const homeUrl = application.getUrlForApp('home', { - path: '/', - absolute: false, - }); - const targetUrl = http.basePath.prepend(http.basePath.remove(homeUrl), { - withoutWorkspace: true, - }); - await application.navigateToUrl(targetUrl); - } }; const exitWorkspace = async () => { @@ -152,7 +152,18 @@ export const WorkspaceUpdater = () => { }); return; } - if (!result?.success) { + if (result.success) { + if (http && application) { + const homeUrl = application.getUrlForApp('home', { + path: '/', + absolute: false, + }); + const targetUrl = http.basePath.prepend(http.basePath.remove(homeUrl), { + withoutWorkspace: true, + }); + await application.navigateToUrl(targetUrl); + } + } else { notifications?.toasts.addDanger({ title: i18n.translate('workspace.exit.failed', { defaultMessage: 'Failed to exit workspace', @@ -161,16 +172,6 @@ export const WorkspaceUpdater = () => { }); return; } - if (http && application) { - const homeUrl = application.getUrlForApp('home', { - path: '/', - absolute: false, - }); - const targetUrl = http.basePath.prepend(http.basePath.remove(homeUrl), { - withoutWorkspace: true, - }); - await application.navigateToUrl(targetUrl); - } }; return (