diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 9fcf531f3ffe..5008f8b4a69a 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -31,7 +31,3 @@ 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.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index eb58a982140c..8673abe0550d 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -233,11 +233,7 @@ export function CollapsibleNav({ outsideClickCloses={false} > - + {/* merged NavLinks */} {mergedNavLinks.map((item, i) => { diff --git a/src/core/public/chrome/ui/header/collapsible_nav_header.tsx b/src/core/public/chrome/ui/header/collapsible_nav_header.tsx index 8c902ac598a5..a3e983e9971d 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_header.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_header.tsx @@ -3,68 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ import { i18n } from '@osd/i18n'; -import React, { useState } from 'react'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { - EuiContextMenu, - EuiPopover, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiCollapsibleNavGroup, -} from '@elastic/eui'; -import { - HttpStart, - WorkspaceStart, - WorkspaceAttribute, - MANAGEMENT_WORKSPACE, -} from '../../../../public'; -import { InternalApplicationStart } from '../../../application'; -import { formatUrlWithWorkspaceId } from '../../../utils'; -import { - WORKSPACE_CREATE_APP_ID, - WORKSPACE_LIST_APP_ID, - WORKSPACE_OVERVIEW_APP_ID, -} from '../../constants'; +import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiCollapsibleNavGroup } from '@elastic/eui'; +import { WorkspaceStart } from '../../../../public'; interface Props { workspaces: WorkspaceStart; - basePath: HttpStart['basePath']; - getUrlForApp: InternalApplicationStart['getUrlForApp']; } -function getFilteredWorkspaceList( - workspaceList: WorkspaceAttribute[], - currentWorkspace: WorkspaceAttribute | null -): WorkspaceAttribute[] { - // list top5 workspaces except management workspace, place current workspace at the top - return [ - ...(currentWorkspace ? [currentWorkspace] : []), - ...workspaceList.filter( - (workspace) => workspace.id !== MANAGEMENT_WORKSPACE && workspace.id !== currentWorkspace?.id - ), - ].slice(0, 5); -} - -export function CollapsibleNavHeader({ workspaces, getUrlForApp, basePath }: Props) { +export function CollapsibleNavHeader({ workspaces }: 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 ( @@ -81,157 +36,7 @@ export function CollapsibleNavHeader({ workspaces, getUrlForApp, basePath }: Pro ); + } else { + return workspaces.renderWorkspaceMenu(); } - 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/core_system.ts b/src/core/public/core_system.ts index 356e3dd0690a..b1fb35483dad 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -225,7 +225,7 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, overlays }); - const workspaces = this.workspaces.start(); + const workspaces = this.workspaces.start({ application, http }); const chrome = await this.chrome.start({ application, docLinks, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 50f92b4a2fe4..d31febbd40d1 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -348,8 +348,14 @@ export { export { __osdBootstrap__ } from './osd_bootstrap'; -export { WorkspaceStart, WorkspaceService, WorkspaceAttribute } from './workspace'; +export { + WorkspaceStart, + WorkspaceSetup, + WorkspaceService, + WorkspaceAttribute, + WorkspaceObservables, +} from './workspace'; -export { WorkspacePermissionMode, PUBLIC_WORKSPACE, MANAGEMENT_WORKSPACE } from '../utils'; +export { WorkspacePermissionMode, PUBLIC_WORKSPACE_ID, MANAGEMENT_WORKSPACE_ID } from '../utils'; export { getWorkspaceIdFromUrl, WORKSPACE_TYPE } from './utils'; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index b0e3245f74e5..10b4bcd5eb97 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -42,7 +42,7 @@ import { import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; -import { PUBLIC_WORKSPACE } from '../../utils'; +import { PUBLIC_WORKSPACE_ID } from '../../utils'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, @@ -382,7 +382,7 @@ export class SavedObjectsClient { if (options.hasOwnProperty('workspaces')) { finalWorkspaces = options.workspaces; } else if (typeof currentWorkspaceId === 'string') { - finalWorkspaces = Array.from(new Set([PUBLIC_WORKSPACE, currentWorkspaceId])); + finalWorkspaces = Array.from(new Set([PUBLIC_WORKSPACE_ID, currentWorkspaceId])); } const renamedQuery = renameKeys(renameMap, { diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index 13d01ef1fe56..9b831f6c46b3 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, formatUrlWithWorkspaceId } from './workspace'; -export { WORKSPACE_PATH_PREFIX, PUBLIC_WORKSPACE, MANAGEMENT_WORKSPACE } from '../../utils'; +export { getWorkspaceIdFromUrl, WORKSPACE_TYPE } from './workspace'; +export { WORKSPACE_PATH_PREFIX, PUBLIC_WORKSPACE_ID, MANAGEMENT_WORKSPACE_ID } from '../../utils'; diff --git a/src/core/public/utils/workspace.ts b/src/core/public/utils/workspace.ts index 33012d4fbe4a..9a0f55b3fa8c 100644 --- a/src/core/public/utils/workspace.ts +++ b/src/core/public/utils/workspace.ts @@ -3,9 +3,6 @@ * 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); @@ -17,28 +14,4 @@ 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/index.ts b/src/core/public/workspace/index.ts index c2c12bf20715..c446ecda4499 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -2,5 +2,10 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -export { WorkspaceStart, WorkspaceService, WorkspaceSetup } from './workspaces_service'; +export { + WorkspaceStart, + WorkspaceService, + WorkspaceSetup, + WorkspaceObservables, +} from './workspaces_service'; export type { WorkspaceAttribute } from './workspaces_service'; diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index 57b9d976e4e5..b45d38542b55 100644 --- a/src/core/public/workspace/workspaces_service.mock.ts +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -16,11 +16,18 @@ const createWorkspacesSetupContractMock = () => ({ workspaceList$, currentWorkspace$, workspaceEnabled$, + registerWorkspaceMenuRender: jest.fn(), }); -const createWorkspacesStartContractMock = createWorkspacesSetupContractMock; +const createWorkspacesStartContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + workspaceEnabled$, + renderWorkspaceMenu: jest.fn(), +}); export const workspacesServiceMock = { - createSetupContractMock: createWorkspacesStartContractMock, + createSetupContractMock: createWorkspacesSetupContractMock, createStartContract: createWorkspacesStartContractMock, }; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index 142ac67fed38..d11a4f5da380 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -2,20 +2,39 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + import { BehaviorSubject } from 'rxjs'; import { CoreService } from '../../types'; +import { InternalApplicationStart } from '../application'; +import { HttpSetup } from '../http'; -/** - * @public - */ -export interface WorkspaceStart { +type WorkspaceMenuRenderFn = ({ + basePath, + getUrlForApp, + observables, +}: { + getUrlForApp: InternalApplicationStart['getUrlForApp']; + basePath: HttpSetup['basePath']; + observables: WorkspaceObservables; +}) => JSX.Element | null; + +export interface WorkspaceObservables { currentWorkspaceId$: BehaviorSubject; currentWorkspace$: BehaviorSubject; workspaceList$: BehaviorSubject; workspaceEnabled$: BehaviorSubject; } -export type WorkspaceSetup = WorkspaceStart; +/** + * @public + */ +export interface WorkspaceSetup extends WorkspaceObservables { + registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => void; +} + +export interface WorkspaceStart extends WorkspaceObservables { + renderWorkspaceMenu: () => JSX.Element | null; +} export interface WorkspaceAttribute { id: string; @@ -32,6 +51,7 @@ export class WorkspaceService implements CoreService([]); private currentWorkspace$ = new BehaviorSubject(null); private workspaceEnabled$ = new BehaviorSubject(false); + private _renderWorkspaceMenu: WorkspaceMenuRenderFn | null = null; public setup(): WorkspaceSetup { return { @@ -39,16 +59,37 @@ export class WorkspaceService implements CoreService + (this._renderWorkspaceMenu = render), }; } - public start(): WorkspaceStart { - return { + public start({ + http, + application, + }: { + application: InternalApplicationStart; + http: HttpSetup; + }): WorkspaceStart { + const observables = { currentWorkspaceId$: this.currentWorkspaceId$, currentWorkspace$: this.currentWorkspace$, workspaceList$: this.workspaceList$, workspaceEnabled$: this.workspaceEnabled$, }; + return { + ...observables, + renderWorkspaceMenu: () => { + if (this._renderWorkspaceMenu) { + return this._renderWorkspaceMenu({ + basePath: http.basePath, + getUrlForApp: application.getUrlForApp, + observables, + }); + } + return null; + }, + }; } public async stop() { @@ -56,5 +97,6 @@ export class WorkspaceService implements CoreService { (obj) => obj.workspaces && obj.workspaces.length > 0 && - !obj.workspaces.includes(PUBLIC_WORKSPACE) + !obj.workspaces.includes(PUBLIC_WORKSPACE_ID) ) .map((obj) => ({ id: obj.id, type: obj.type, workspaces: obj.workspaces })); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 6adefe16848d..e479b1e95bc1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -87,7 +87,7 @@ import { FIND_DEFAULT_PER_PAGE, SavedObjectsUtils, } from './utils'; -import { PUBLIC_WORKSPACE } from '../../../../utils/constants'; +import { PUBLIC_WORKSPACE_ID } from '../../../../utils/constants'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -1299,7 +1299,7 @@ export class SavedObjectsRepository { if ( obj.workspaces && obj.workspaces.length > 0 && - !obj.workspaces.includes(PUBLIC_WORKSPACE) + !obj.workspaces.includes(PUBLIC_WORKSPACE_ID) ) { return intersection(obj.workspaces, options.workspaces).length === 0; } @@ -1352,7 +1352,7 @@ export class SavedObjectsRepository { params: { time, workspaces, - globalWorkspaceId: PUBLIC_WORKSPACE, + globalWorkspaceId: PUBLIC_WORKSPACE_ID, }, }, }, diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index 004e9b58a91b..72291f34ec10 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -13,6 +13,6 @@ export enum WorkspacePermissionMode { LibraryWrite = 'library_write', } -export const PUBLIC_WORKSPACE = 'public'; +export const PUBLIC_WORKSPACE_ID = 'public'; -export const MANAGEMENT_WORKSPACE = 'management'; +export const MANAGEMENT_WORKSPACE_ID = 'management'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 2adab8bd8926..636d09ba0992 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -40,6 +40,6 @@ export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; export { WORKSPACE_PATH_PREFIX, WorkspacePermissionMode, - PUBLIC_WORKSPACE, - MANAGEMENT_WORKSPACE, + PUBLIC_WORKSPACE_ID, + MANAGEMENT_WORKSPACE_ID, } from './constants'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index fb621d13f8d0..482b33978edf 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -96,7 +96,7 @@ import { import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { SavedObjectsCopyModal } from './components/copy_modal'; -import { PUBLIC_WORKSPACE, MANAGEMENT_WORKSPACE } from '../../../../../core/public'; +import { PUBLIC_WORKSPACE_ID, MANAGEMENT_WORKSPACE_ID } from '../../../../../core/public'; interface ExportAllOption { id: string; @@ -193,12 +193,12 @@ export class SavedObjectsTable extends Component ws.id); - } else if (workspaceId === PUBLIC_WORKSPACE) { - return [PUBLIC_WORKSPACE]; + } else if (workspaceId === PUBLIC_WORKSPACE_ID) { + return [PUBLIC_WORKSPACE_ID]; } else { - return [workspaceId, PUBLIC_WORKSPACE]; + return [workspaceId, PUBLIC_WORKSPACE_ID]; } } @@ -247,7 +247,7 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || PUBLIC_WORKSPACE + (wsName) => this.wsNameIdLookup?.get(wsName) || PUBLIC_WORKSPACE_ID ); } @@ -340,7 +340,7 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || PUBLIC_WORKSPACE + (wsName) => this.wsNameIdLookup?.get(wsName) || PUBLIC_WORKSPACE_ID ); findOptions.workspaces = workspaceIds; } diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx index fc06e52d2e81..000b89e5caeb 100644 --- a/src/plugins/workspace/public/application.tsx +++ b/src/plugins/workspace/public/application.tsx @@ -5,15 +5,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParameters, CoreStart } from '../../../core/public'; +import { AppMountParameters } from '../../../core/public'; import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public'; import { WorkspaceListApp } from './components/workspace_list_app'; import { WorkspaceCreatorApp } from './components/workspace_creator_app'; import { WorkspaceUpdaterApp } from './components/workspace_updater_app'; import { WorkspaceOverviewApp } from './components/workspace_overview_app'; -import { WorkspaceClient } from './workspace_client'; - -export type Services = CoreStart & { workspaceClient: WorkspaceClient }; +import { Services } from './types'; export const renderListApp = ( { element, history, appBasePath }: AppMountParameters, diff --git a/src/plugins/workspace/public/components/utils/workspace.ts b/src/plugins/workspace/public/components/utils/workspace.ts index 6be21538838f..63d88ae93e19 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 '../../../../../core/public/utils'; +import { formatUrlWithWorkspaceId } from '../../utils'; type Core = Pick; diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx new file mode 100644 index 000000000000..ae48d50a2287 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx @@ -0,0 +1,224 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React, { useState } from 'react'; +import { useObservable } from 'react-use'; +import { + EuiCollapsibleNavGroup, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiText, +} from '@elastic/eui'; + +import { + ApplicationStart, + HttpSetup, + MANAGEMENT_WORKSPACE_ID, + WorkspaceAttribute, + WorkspaceObservables, +} from '../../../../../core/public'; +import { + WORKSPACE_CREATE_APP_ID, + WORKSPACE_LIST_APP_ID, + WORKSPACE_OVERVIEW_APP_ID, +} from '../../../common/constants'; +import { formatUrlWithWorkspaceId } from '../../utils'; + +interface Props { + getUrlForApp: ApplicationStart['getUrlForApp']; + basePath: HttpSetup['basePath']; + observables: WorkspaceObservables; +} + +function getFilteredWorkspaceList( + workspaceList: WorkspaceAttribute[], + currentWorkspace: WorkspaceAttribute | null +): WorkspaceAttribute[] { + // list top5 workspaces except management workspace, place current workspace at the top + return [ + ...(currentWorkspace ? [currentWorkspace] : []), + ...workspaceList.filter( + (workspace) => + workspace.id !== MANAGEMENT_WORKSPACE_ID && workspace.id !== currentWorkspace?.id + ), + ].slice(0, 5); +} + +export const WorkspaceMenu = ({ basePath, getUrlForApp, observables }: Props) => { + const [isPopoverOpen, setPopover] = useState(false); + const currentWorkspace = useObservable(observables.currentWorkspace$, null); + const workspaceList = useObservable(observables.workspaceList$, []); + + const defaultHeaderName = i18n.translate( + 'core.ui.primaryNav.workspacePickerMenu.defaultHeaderName', + { + defaultMessage: 'OpenSearch Analytics', + } + ); + const managementWorkspaceName = + workspaceList.find((workspace) => workspace.id === MANAGEMENT_WORKSPACE_ID)?.name ?? + i18n.translate('core.ui.primaryNav.workspacePickerMenu.managementWorkspaceName', { + defaultMessage: 'Management', + }); + const filteredWorkspaceList = getFilteredWorkspaceList(workspaceList, currentWorkspace); + const currentWorkspaceName = currentWorkspace?.name ?? 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_ID, + basePath + ), + }, + ], + }, + { + id: 1, + title: 'Workspaces', + items: getWorkspaceListItems(), + }, + ]; + + return ( + + + + ); +}; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index aafb22ead142..4dd0846107a9 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -29,7 +29,8 @@ import { SavedObjectsManagementPluginSetup } from '../../saved_objects_managemen import { getWorkspaceColumn } from './components/utils/workspace_column'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; import { WorkspaceClient } from './workspace_client'; -import { Services } from './application'; +import { renderWorkspaceMenu } from './render_workspace_menu'; +import { Services } from './types'; interface WorkspacePluginSetupDeps { savedObjectsManagement?: SavedObjectsManagementPluginSetup; @@ -46,6 +47,8 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> workspaceClient.init(); core.workspaces.workspaceEnabled$.next(true); + core.workspaces.registerWorkspaceMenuRender(renderWorkspaceMenu); + /** * Retrieve workspace id from url */ diff --git a/src/plugins/workspace/public/render_workspace_menu.tsx b/src/plugins/workspace/public/render_workspace_menu.tsx new file mode 100644 index 000000000000..12313594cbdc --- /dev/null +++ b/src/plugins/workspace/public/render_workspace_menu.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; +import { ApplicationStart, HttpSetup, WorkspaceObservables } from '../../../core/public'; + +export function renderWorkspaceMenu({ + basePath, + getUrlForApp, + observables, +}: { + getUrlForApp: ApplicationStart['getUrlForApp']; + basePath: HttpSetup['basePath']; + observables: WorkspaceObservables; +}) { + 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/utils.ts b/src/plugins/workspace/public/utils.ts new file mode 100644 index 000000000000..ccb286860061 --- /dev/null +++ b/src/plugins/workspace/public/utils.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WORKSPACE_PATH_PREFIX } from '../../../core/public/utils'; +import { 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(); +}; diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts index 9c2fed440d1f..5e8080cba3f0 100644 --- a/src/plugins/workspace/public/workspace_client.ts +++ b/src/plugins/workspace/public/workspace_client.ts @@ -10,7 +10,7 @@ import { HttpFetchOptions, HttpSetup, WorkspaceAttribute, - WorkspaceStart, + WorkspaceSetup, } from '../../../core/public'; import { WorkspacePermissionMode } from '../../../core/public'; @@ -61,9 +61,9 @@ interface WorkspaceFindOptions { */ export class WorkspaceClient { private http: HttpSetup; - private workspaces: WorkspaceStart; + private workspaces: WorkspaceSetup; - constructor(http: HttpSetup, workspaces: WorkspaceStart) { + constructor(http: HttpSetup, workspaces: WorkspaceSetup) { this.http = http; this.workspaces = workspaces; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 710eaeea1819..dfddcd372c17 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -13,8 +13,8 @@ import { ISavedObjectsRepository, WORKSPACE_TYPE, ACL, - PUBLIC_WORKSPACE, - MANAGEMENT_WORKSPACE, + PUBLIC_WORKSPACE_ID, + MANAGEMENT_WORKSPACE_ID, Permissions, WorkspacePermissionMode, SavedObjectsClient, @@ -125,7 +125,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { await Promise.all([ this.checkAndCreateWorkspace( internalRepository, - PUBLIC_WORKSPACE, + PUBLIC_WORKSPACE_ID, { name: i18n.translate('workspaces.public.workspace.default.name', { defaultMessage: 'public', @@ -135,7 +135,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { ), this.checkAndCreateWorkspace( internalRepository, - MANAGEMENT_WORKSPACE, + MANAGEMENT_WORKSPACE_ID, { name: i18n.translate('workspaces.management.workspace.default.name', { defaultMessage: 'Management',