From 6ed3b5152a54bbf7704a44b1e7c0c0128d7fb135 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 16 Aug 2024 08:39:47 +0800 Subject: [PATCH 01/15] feat: show workspace picker content in left nav Signed-off-by: SuZhou-Joe --- changelogs/fragments/7716.yml | 2 + .../chrome/nav_group/nav_group_service.ts | 2 + .../collapsible_nav_group_enabled.test.tsx | 70 ------ .../header/collapsible_nav_group_enabled.tsx | 200 +++++------------- .../ui/header/collapsible_nav_groups.test.tsx | 79 +++++++ .../ui/header/collapsible_nav_groups.tsx | 149 +++++++++++++ src/core/public/chrome/ui/header/header.tsx | 23 +- src/core/public/chrome/utils.ts | 2 +- .../workspace_menu/workspace_menu.tsx | 84 +------- .../workspace_picker_content.tsx | 110 ++++++++++ src/plugins/workspace/public/plugin.ts | 25 ++- 11 files changed, 435 insertions(+), 311 deletions(-) create mode 100644 changelogs/fragments/7716.yml create mode 100644 src/core/public/chrome/ui/header/collapsible_nav_groups.test.tsx create mode 100644 src/core/public/chrome/ui/header/collapsible_nav_groups.tsx create mode 100644 src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx diff --git a/changelogs/fragments/7716.yml b/changelogs/fragments/7716.yml new file mode 100644 index 000000000000..d1b91ff51d89 --- /dev/null +++ b/changelogs/fragments/7716.yml @@ -0,0 +1,2 @@ +feat: +- Display workspace picker content when outside workspace ([#7716](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7716)) \ No newline at end of file diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index 88689d88f2fc..e0c69de353b0 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -276,6 +276,8 @@ export class ChromeNavGroupService { const navGroups = appIdNavGroupMap.get(appId); if (navGroups && navGroups.size === 1) { setCurrentNavGroup(navGroups.values().next().value); + } else if (!navGroups) { + setCurrentNavGroup(undefined); } } }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx index 98b2ade3e257..197e5551a2e1 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx @@ -10,7 +10,6 @@ import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { CollapsibleNavGroupEnabled, CollapsibleNavGroupEnabledProps, - NavGroups, } from './collapsible_nav_group_enabled'; import { ChromeNavLink } from '../../nav_links'; import { ChromeRegistrationNavLink, NavGroupItemInMap } from '../../nav_group'; @@ -25,75 +24,6 @@ jest.mock('./collapsible_nav_group_enabled_top', () => ({ const mockBasePath = httpServiceMock.createSetupContract({ basePath: '/test' }).basePath; -describe('', () => { - const getMockedNavLink = ( - navLink: Partial - ): ChromeNavLink & ChromeRegistrationNavLink => ({ - baseUrl: '', - href: '', - id: '', - title: '', - ...navLink, - }); - it('should render correctly', () => { - const navigateToApp = jest.fn(); - const onNavItemClick = jest.fn(); - const { container, getByTestId, queryByTestId } = render( - - ); - expect(container).toMatchSnapshot(); - expect(container.querySelectorAll('.nav-link-item-btn').length).toEqual(5); - fireEvent.click(getByTestId('collapsibleNavAppLink-pure')); - expect(navigateToApp).toBeCalledTimes(0); - // The accordion is collapsed - expect(queryByTestId('collapsibleNavAppLink-subLink')).toBeNull(); - - // Expand the accordion - fireEvent.click(getByTestId('collapsibleNavAppLink-pure')); - fireEvent.click(getByTestId('collapsibleNavAppLink-subLink')); - expect(navigateToApp).toBeCalledWith('subLink'); - }); -}); - const defaultNavGroupMap = { [ALL_USE_CASE_ID]: { ...DEFAULT_NAV_GROUPS[ALL_USE_CASE_ID], diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx index af67974ecb9d..762e7103349b 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -4,15 +4,7 @@ */ import './collapsible_nav_group_enabled.scss'; -import { - EuiFlexItem, - EuiFlyout, - EuiSideNavItemType, - EuiSideNav, - EuiPanel, - EuiText, - EuiHorizontalRule, -} from '@elastic/eui'; +import { EuiFlyout, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React, { useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; @@ -20,7 +12,7 @@ import * as Rx from 'rxjs'; import classNames from 'classnames'; import { WorkspacesStart } from 'src/core/public/workspace'; import { ChromeNavControl, ChromeNavLink } from '../..'; -import { AppCategory, NavGroupStatus } from '../../../../types'; +import { AppCategory, NavGroupStatus, NavGroupType } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; @@ -31,18 +23,15 @@ import { ChromeRegistrationNavLink, NavGroupItemInMap, } from '../../nav_group'; -import { - fulfillRegistrationLinksToChromeNavLinks, - getOrderedLinksOrCategories, - LinkItem, - LinkItemType, -} from '../../utils'; +import { fulfillRegistrationLinksToChromeNavLinks, sortBy } from '../../utils'; import { ALL_USE_CASE_ID, DEFAULT_APP_CATEGORIES } from '../../../../../core/utils'; import { CollapsibleNavTop } from './collapsible_nav_group_enabled_top'; import { HeaderNavControls } from './header_nav_controls'; +import { NavGroups } from './collapsible_nav_groups'; export interface CollapsibleNavGroupEnabledProps { appId$: InternalApplicationStart['currentAppId$']; + collapsibleNavHeaderRender?: () => JSX.Element | null; basePath: HttpStart['basePath']; id: string; isLocked: boolean; @@ -63,141 +52,10 @@ export interface CollapsibleNavGroupEnabledProps { currentWorkspace$: WorkspacesStart['currentWorkspace$']; } -interface NavGroupsProps { - navLinks: ChromeNavLink[]; - suffix?: React.ReactElement; - style?: React.CSSProperties; - appId?: string; - navigateToApp: InternalApplicationStart['navigateToApp']; - onNavItemClick: ( - event: React.MouseEvent, - navItem: ChromeNavLink - ) => void; -} - const titleForSeeAll = i18n.translate('core.ui.primaryNav.seeAllLabel', { defaultMessage: 'See all...', }); -const LEVEL_FOR_ROOT_ITEMS = 1; - -export function NavGroups({ - navLinks, - suffix, - style, - appId, - navigateToApp, - onNavItemClick, -}: NavGroupsProps) { - const createNavItem = ({ - link, - className, - }: { - link: ChromeNavLink; - className?: string; - }): EuiSideNavItemType<{}> => { - const euiListItem = createEuiListItem({ - link, - appId, - dataTestSubj: `collapsibleNavAppLink-${link.id}`, - navigateToApp, - onClick: (event) => { - onNavItemClick(event, link); - }, - }); - - return { - id: `${link.id}-${link.title}`, - name: {link.title}, - onClick: euiListItem.onClick, - href: euiListItem.href, - emphasize: euiListItem.isActive, - className: `nav-link-item ${className || ''}`, - buttonClassName: 'nav-link-item-btn', - 'data-test-subj': euiListItem['data-test-subj'], - 'aria-label': link.title, - }; - }; - const createSideNavItem = ( - navLink: LinkItem, - level: number, - className?: string - ): EuiSideNavItemType<{}> => { - if (navLink.itemType === LinkItemType.LINK) { - if (navLink.link.title === titleForSeeAll) { - const navItem = createNavItem({ - link: navLink.link, - }); - - return { - ...navItem, - name: {navItem.name}, - emphasize: false, - }; - } - - return createNavItem({ - link: navLink.link, - className, - }); - } - - if (navLink.itemType === LinkItemType.PARENT_LINK && navLink.link) { - const props = createNavItem({ link: navLink.link }); - const parentItem = { - ...props, - forceOpen: true, - /** - * The href and onClick should both be undefined to make parent item rendered as accordion. - */ - href: undefined, - onClick: undefined, - className: classNames(props.className, 'nav-link-parent-item'), - buttonClassName: classNames(props.buttonClassName, 'nav-link-parent-item-button'), - items: navLink.links.map((subNavLink) => - createSideNavItem(subNavLink, level + 1, 'nav-nested-item') - ), - }; - /** - * OuiSideBar will never render items of first level as accordion, - * in order to display accordion, we need to render a fake parent item. - */ - if (level === LEVEL_FOR_ROOT_ITEMS) { - return { - className: 'nav-link-fake-item', - buttonClassName: 'nav-link-fake-item-button', - name: '', - items: [parentItem], - id: `fake_${props.id}`, - }; - } - - return parentItem; - } - - if (navLink.itemType === LinkItemType.CATEGORY) { - return { - id: navLink.category?.id ?? '', - name:
{navLink.category?.label ?? ''}
, - items: navLink.links?.map((link) => createSideNavItem(link, level + 1)), - 'aria-label': navLink.category?.label, - }; - } - - return {} as EuiSideNavItemType<{}>; - }; - const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); - const sideNavItems = orderedLinksOrCategories - .map((navLink) => createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS)) - .filter((item): item is EuiSideNavItemType<{}> => !!item); - return ( - - - {suffix} - - ); -} - // Custom category is used for those features not belong to any of use cases in all use case. // and the custom category should always sit before manage category const customCategory: AppCategory = { @@ -224,8 +82,10 @@ export function CollapsibleNavGroupEnabled({ logos, setCurrentNavGroup, capabilities, + collapsibleNavHeaderRender, ...observables }: CollapsibleNavGroupEnabledProps) { + const currentWorkspace = useObservable(observables.currentWorkspace$, undefined); const allNavLinks = useObservable(observables.navLinks$, []); const navLinks = allNavLinks.filter((link) => !link.hidden); const homeLink = useMemo(() => allNavLinks.find((item) => item.id === 'home'), [allNavLinks]); @@ -249,7 +109,39 @@ export function CollapsibleNavGroupEnabled({ [navGroupsMap] ); + const shouldShowCollapsedNavHeaderContent = useMemo( + () => + isNavOpen && + collapsibleNavHeaderRender && + capabilities.workspaces.enabled && + !currentWorkspace && + !currentNavGroup, + [currentWorkspace, capabilities, collapsibleNavHeaderRender, isNavOpen, currentNavGroup] + ); + const navLinksForRender: ChromeNavLink[] = useMemo(() => { + if (shouldShowCollapsedNavHeaderContent) { + const resultLinks: ChromeNavLink[] = []; + Object.values(navGroupsMap) + .sort(sortBy('order')) + .filter((navGroup) => navGroup.type === NavGroupType.SYSTEM) + .forEach((navGroup) => { + const visibleNavLinksWithinNavGroup = fulfillRegistrationLinksToChromeNavLinks( + navGroup.navLinks, + navLinks + ); + if (visibleNavLinksWithinNavGroup[0]) { + resultLinks.push({ + ...visibleNavLinksWithinNavGroup[0], + title: navGroup.title, + category: DEFAULT_APP_CATEGORIES.manage, + }); + } + }); + + return resultLinks; + } + if (currentNavGroup && currentNavGroup.id !== ALL_USE_CASE_ID) { return fulfillRegistrationLinksToChromeNavLinks( navGroupsMap[currentNavGroup.id].navLinks || [], @@ -332,7 +224,13 @@ export function CollapsibleNavGroupEnabled({ }); return fulfillRegistrationLinksToChromeNavLinks(navLinksForAll, navLinks); - }, [navLinks, navGroupsMap, currentNavGroup, visibleUseCases]); + }, [ + navLinks, + navGroupsMap, + currentNavGroup, + visibleUseCases, + shouldShowCollapsedNavHeaderContent, + ]); const width = useMemo(() => { if (!isNavOpen) { @@ -414,6 +312,12 @@ export function CollapsibleNavGroupEnabled({ hasShadow={false} className="eui-yScroll flex-1-container" > + {shouldShowCollapsedNavHeaderContent && collapsibleNavHeaderRender ? ( + <> + {collapsibleNavHeaderRender()} + + + ) : null} ', () => { + const getMockedNavLink = ( + navLink: Partial + ): ChromeNavLink & ChromeRegistrationNavLink => ({ + baseUrl: '', + href: '', + id: '', + title: '', + ...navLink, + }); + it('should render correctly', () => { + const navigateToApp = jest.fn(); + const onNavItemClick = jest.fn(); + const { container, getByTestId, queryByTestId } = render( + + ); + expect(container).toMatchSnapshot(); + expect(container.querySelectorAll('.nav-link-item-btn').length).toEqual(5); + fireEvent.click(getByTestId('collapsibleNavAppLink-pure')); + expect(navigateToApp).toBeCalledTimes(0); + // The accordion is collapsed + expect(queryByTestId('collapsibleNavAppLink-subLink')).toBeNull(); + + // Expand the accordion + fireEvent.click(getByTestId('collapsibleNavAppLink-pure')); + fireEvent.click(getByTestId('collapsibleNavAppLink-subLink')); + expect(navigateToApp).toBeCalledWith('subLink'); + }); +}); diff --git a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx new file mode 100644 index 000000000000..a78267989efa --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './collapsible_nav_group_enabled.scss'; +import { EuiFlexItem, EuiSideNavItemType, EuiSideNav, EuiText } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; +import classNames from 'classnames'; +import { ChromeNavLink } from '../..'; +import { InternalApplicationStart } from '../../../application/types'; +import { createEuiListItem } from './nav_link'; +import { getOrderedLinksOrCategories, LinkItem, LinkItemType } from '../../utils'; + +export interface NavGroupsProps { + navLinks: ChromeNavLink[]; + suffix?: React.ReactElement; + style?: React.CSSProperties; + appId?: string; + navigateToApp: InternalApplicationStart['navigateToApp']; + onNavItemClick: ( + event: React.MouseEvent, + navItem: ChromeNavLink + ) => void; +} + +const titleForSeeAll = i18n.translate('core.ui.primaryNav.seeAllLabel', { + defaultMessage: 'See all...', +}); + +const LEVEL_FOR_ROOT_ITEMS = 1; + +export function NavGroups({ + navLinks, + suffix, + style, + appId, + navigateToApp, + onNavItemClick, +}: NavGroupsProps) { + const createNavItem = ({ + link, + className, + }: { + link: ChromeNavLink; + className?: string; + }): EuiSideNavItemType<{}> => { + const euiListItem = createEuiListItem({ + link, + appId, + dataTestSubj: `collapsibleNavAppLink-${link.id}`, + navigateToApp, + onClick: (event) => { + onNavItemClick(event, link); + }, + }); + + return { + id: `${link.id}-${link.title}`, + name: {link.title}, + onClick: euiListItem.onClick, + href: euiListItem.href, + emphasize: euiListItem.isActive, + className: `nav-link-item ${className || ''}`, + buttonClassName: 'nav-link-item-btn', + 'data-test-subj': euiListItem['data-test-subj'], + 'aria-label': link.title, + }; + }; + const createSideNavItem = ( + navLink: LinkItem, + level: number, + className?: string + ): EuiSideNavItemType<{}> => { + if (navLink.itemType === LinkItemType.LINK) { + if (navLink.link.title === titleForSeeAll) { + const navItem = createNavItem({ + link: navLink.link, + }); + + return { + ...navItem, + name: {navItem.name}, + emphasize: false, + }; + } + + return createNavItem({ + link: navLink.link, + className, + }); + } + + if (navLink.itemType === LinkItemType.PARENT_LINK && navLink.link) { + const props = createNavItem({ link: navLink.link }); + const parentItem = { + ...props, + forceOpen: true, + /** + * The href and onClick should both be undefined to make parent item rendered as accordion. + */ + href: undefined, + onClick: undefined, + className: classNames(props.className, 'nav-link-parent-item'), + buttonClassName: classNames(props.buttonClassName, 'nav-link-parent-item-button'), + items: navLink.links.map((subNavLink) => + createSideNavItem(subNavLink, level + 1, 'nav-nested-item') + ), + }; + /** + * OuiSideBar will never render items of first level as accordion, + * in order to display accordion, we need to render a fake parent item. + */ + if (level === LEVEL_FOR_ROOT_ITEMS) { + return { + className: 'nav-link-fake-item', + buttonClassName: 'nav-link-fake-item-button', + name: '', + items: [parentItem], + id: `fake_${props.id}`, + }; + } + + return parentItem; + } + + if (navLink.itemType === LinkItemType.CATEGORY) { + return { + id: navLink.category?.id ?? '', + name:
{navLink.category?.label ?? ''}
, + items: navLink.links?.map((link) => createSideNavItem(link, level + 1)), + 'aria-label': navLink.category?.label, + }; + } + + return {} as EuiSideNavItemType<{}>; + }; + const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); + const sideNavItems = orderedLinksOrCategories + .map((navLink) => createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS)) + .filter((item): item is EuiSideNavItemType<{}> => !!item); + return ( + + + {suffix} + + ); +} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 492d9c7b3e78..6fa8d9a43a75 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -138,18 +138,10 @@ export function Header({ const isVisible = useObservable(observables.isVisible$, false); const headerVariant = useObservable(observables.headerVariant$, HeaderVariant.PAGE); const isLocked = useObservable(observables.isLocked$, false); - const appId = useObservable(application.currentAppId$, ''); const [isNavOpen, setIsNavOpen] = useState(false); const sidecarConfig = useObservable(observables.sidecarConfig$, undefined); const breadcrumbs = useObservable(observables.breadcrumbs$, []); - /** - * This is a workaround on 2.16 to hide the navigation items within left navigation - * when user is in homepage with workspace enabled + new navigation enabled - */ - const shouldHideExpandIcon = - navGroupEnabled && appId === 'home' && application.capabilities.workspaces.enabled; - const sidecarPaddingStyle = useMemo(() => { return getOsdSidecarPaddingStyle(sidecarConfig); }, [sidecarConfig]); @@ -365,11 +357,9 @@ export function Header({ const renderLegacyHeader = () => ( - {shouldHideExpandIcon ? null : ( - - {renderNavToggle()} - - )} + + {renderNavToggle()} + {renderLeftControls()} @@ -402,7 +392,7 @@ export function Header({ const renderPageHeader = () => (
- {shouldHideExpandIcon || isNavOpen ? null : renderNavToggle()} + {isNavOpen ? null : renderNavToggle()} {renderRecentItems()} @@ -450,7 +440,7 @@ export function Header({ const renderApplicationHeader = () => (
- {shouldHideExpandIcon || isNavOpen ? null : renderNavToggle()} + {isNavOpen ? null : renderNavToggle()} {renderRecentItems()} {renderActionMenu()} @@ -475,10 +465,11 @@ export function Header({ {navGroupEnabled ? ( = keyof T; -const sortBy = (key: KeyOf) => { +export const sortBy = (key: KeyOf) => { return (a: T, b: T): number => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0); }; diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx index c2e8e39331d4..9384812f0272 100644 --- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx @@ -4,48 +4,33 @@ */ import { i18n } from '@osd/i18n'; -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { useObservable } from 'react-use'; import { EuiText, EuiPanel, - EuiTitle, EuiAvatar, EuiButton, EuiPopover, EuiToolTip, EuiFlexItem, EuiFlexGroup, - EuiListGroup, EuiButtonIcon, EuiButtonEmpty, - EuiListGroupItem, } from '@elastic/eui'; import { BehaviorSubject } from 'rxjs'; -import { - WORKSPACE_CREATE_APP_ID, - WORKSPACE_LIST_APP_ID, - MAX_WORKSPACE_PICKER_NUM, -} from '../../../common/constants'; +import { WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID } from '../../../common/constants'; import { CoreStart, WorkspaceObject } from '../../../../../core/public'; -import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils'; -import { recentWorkspaceManager } from '../../recent_workspace_manager'; +import { getFirstUseCaseOfFeatureConfigs } from '../../utils'; import { WorkspaceUseCase } from '../../types'; import { navigateToWorkspaceDetail } from '../utils/workspace'; import { validateWorkspaceColor } from '../../../common/utils'; +import { WorkspacePickerContent } from '../workspace_picker_content/workspace_picker_content'; const defaultHeaderName = i18n.translate('workspace.menu.defaultHeaderName', { defaultMessage: 'Workspaces', }); -const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces', { - defaultMessage: 'All workspaces', -}); - -const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', { - defaultMessage: 'Recent workspaces', -}); - const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorkspace', { defaultMessage: 'Create workspace', }); @@ -73,22 +58,9 @@ interface Props { export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { const [isPopoverOpen, setPopover] = useState(false); const currentWorkspace = useObservable(coreStart.workspaces.currentWorkspace$, null); - const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []); const isDashboardAdmin = coreStart.application.capabilities?.dashboards?.isDashboardAdmin; const availableUseCases = useObservable(registeredUseCases$, []); - const filteredWorkspaceList = useMemo(() => { - return workspaceList.slice(0, MAX_WORKSPACE_PICKER_NUM); - }, [workspaceList]); - - const filteredRecentWorkspaces = useMemo(() => { - return recentWorkspaceManager - .getRecentWorkspaces() - .map((workspace) => workspaceList.find((ws) => ws.id === workspace.id)) - .filter((workspace): workspace is WorkspaceObject => workspace !== undefined) - .slice(0, MAX_WORKSPACE_PICKER_NUM); - }, [workspaceList]); - const currentWorkspaceName = currentWorkspace?.name ?? defaultHeaderName; const getUseCase = (workspace: WorkspaceObject) => { @@ -126,46 +98,6 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { /> ); - const getWorkspaceListGroup = (filterWorkspaceList: WorkspaceObject[], itemType: string) => { - const listItems = filterWorkspaceList.map((workspace: WorkspaceObject) => { - const useCase = getUseCase(workspace); - const useCaseURL = getUseCaseUrl(useCase, workspace, coreStart.application, coreStart.http); - return ( - - } - label={workspace.name} - onClick={() => { - closePopover(); - window.location.assign(useCaseURL); - }} - /> - ); - }); - return ( - <> - -

{itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}

-
- - {listItems} - - - ); - }; - return ( { - {filteredRecentWorkspaces.length > 0 && - getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')} - {filteredWorkspaceList.length > 0 && getWorkspaceListGroup(filteredWorkspaceList, 'all')} + setPopover(false)} + /> diff --git a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx new file mode 100644 index 000000000000..92debb2db291 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx @@ -0,0 +1,110 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React, { useMemo } from 'react'; +import { useObservable } from 'react-use'; +import { EuiTitle, EuiAvatar, EuiListGroup, EuiListGroupItem } from '@elastic/eui'; +import { BehaviorSubject } from 'rxjs'; +import { MAX_WORKSPACE_PICKER_NUM } from '../../../common/constants'; +import { CoreStart, WorkspaceObject } from '../../../../../core/public'; +import { recentWorkspaceManager } from '../../recent_workspace_manager'; +import { WorkspaceUseCase } from '../../types'; +import { validateWorkspaceColor } from '../../../common/utils'; +import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils'; + +const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces', { + defaultMessage: 'All workspaces', +}); + +const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', { + defaultMessage: 'Recent workspaces', +}); + +const getValidWorkspaceColor = (color?: string) => + validateWorkspaceColor(color) ? color : undefined; + +interface Props { + coreStart: CoreStart; + registeredUseCases$: BehaviorSubject; + onClickWorkspace?: () => void; +} + +export const WorkspacePickerContent = ({ + coreStart, + registeredUseCases$, + onClickWorkspace, +}: Props) => { + const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []); + const availableUseCases = useObservable(registeredUseCases$, []); + + const filteredWorkspaceList = useMemo(() => { + return workspaceList.slice(0, MAX_WORKSPACE_PICKER_NUM); + }, [workspaceList]); + + const filteredRecentWorkspaces = useMemo(() => { + return recentWorkspaceManager + .getRecentWorkspaces() + .map((workspace) => workspaceList.find((ws) => ws.id === workspace.id)) + .filter((workspace): workspace is WorkspaceObject => workspace !== undefined) + .slice(0, MAX_WORKSPACE_PICKER_NUM); + }, [workspaceList]); + + const getUseCase = (workspace: WorkspaceObject) => { + if (!workspace.features) { + return; + } + const useCaseId = getFirstUseCaseOfFeatureConfigs(workspace.features); + return availableUseCases.find((useCase) => useCase.id === useCaseId); + }; + + const getWorkspaceListGroup = (filterWorkspaceList: WorkspaceObject[], itemType: string) => { + const listItems = filterWorkspaceList.map((workspace: WorkspaceObject) => { + const useCase = getUseCase(workspace); + const useCaseURL = getUseCaseUrl(useCase, workspace, coreStart.application, coreStart.http); + return ( + + } + label={workspace.name} + onClick={() => { + onClickWorkspace?.(); + window.location.assign(useCaseURL); + }} + /> + ); + }); + return ( + <> + +

{itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}

+
+ + {listItems} + + + ); + }; + + return ( + <> + {filteredRecentWorkspaces.length > 0 && + getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')} + {filteredWorkspaceList.length > 0 && getWorkspaceListGroup(filteredWorkspaceList, 'all')} + + ); +}; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 77969fa9b44d..31b12340d289 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -7,7 +7,7 @@ import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import React from 'react'; import { i18n } from '@osd/i18n'; import { map } from 'rxjs/operators'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiPanel } from '@elastic/eui'; import { Plugin, CoreStart, @@ -55,6 +55,7 @@ import { WorkspaceListCard } from './components/service_card'; import { UseCaseFooter } from './components/home_get_start_card'; import { HOME_CONTENT_AREAS } from '../../home/public'; import { NavigationPublicPluginStart } from '../../../plugins/navigation/public'; +import { WorkspacePickerContent } from './components/workspace_picker_content/workspace_picker_content'; type WorkspaceAppType = ( params: AppMountParameters, @@ -424,6 +425,28 @@ export class WorkspacePlugin }, ]); + if (core.chrome.navGroup.getNavGroupEnabled()) { + /** + * Show workspace picker content when outside of workspace and not in any nav group + */ + core.chrome.registerCollapsibleNavHeader(() => { + if (!this.coreStart) { + return null; + } + return React.createElement(EuiPanel, { + hasShadow: false, + hasBorder: false, + children: [ + React.createElement(WorkspacePickerContent, { + key: 'workspacePickerContent', + coreStart: this.coreStart, + registeredUseCases$: this.registeredUseCases$, + }), + ], + }); + }); + } + return {}; } From f320ab4c0cb53fb9f40aa7d5bb51cf1648a677cb Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 16 Aug 2024 12:52:32 +0800 Subject: [PATCH 02/15] fix: bootstrap error Signed-off-by: SuZhou-Joe --- .../chrome/ui/header/collapsible_nav_group_enabled.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx index 197e5551a2e1..100a711dba01 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx @@ -12,7 +12,7 @@ import { CollapsibleNavGroupEnabledProps, } from './collapsible_nav_group_enabled'; import { ChromeNavLink } from '../../nav_links'; -import { ChromeRegistrationNavLink, NavGroupItemInMap } from '../../nav_group'; +import { NavGroupItemInMap } from '../../nav_group'; import { httpServiceMock } from '../../../mocks'; import { getLogos } from '../../../../common'; import { ALL_USE_CASE_ID, DEFAULT_NAV_GROUPS, WorkspaceObject } from '../../../../public'; From 9b4cd49ccc9daa7b11a0f43d7f39b930a3fb1b5d Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 16 Aug 2024 15:03:16 +0800 Subject: [PATCH 03/15] fix: unit test error Signed-off-by: SuZhou-Joe --- .../header/__snapshots__/header.test.tsx.snap | 370 ------------------ .../public/chrome/ui/header/header.test.tsx | 18 - 2 files changed, 388 deletions(-) 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 219c5dd3d389..ec75a3c41538 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 @@ -160,43 +160,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -6355,43 +6318,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -7343,43 +7269,6 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -9327,43 +9216,6 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -9849,43 +9701,6 @@ exports[`Header renders condensed header 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -14815,43 +14630,6 @@ exports[`Header renders condensed header 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -15300,43 +15078,6 @@ exports[`Header renders page header with application title 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -18661,43 +18402,6 @@ exports[`Header renders page header with application title 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -19183,43 +18887,6 @@ exports[`Header toggles primary navigation menu when clicked 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, @@ -24149,43 +23816,6 @@ exports[`Header toggles primary navigation menu when clicked 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, ], "thrownError": null, }, diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 9cc8652c3e41..7edf893826ac 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -194,24 +194,6 @@ describe('Header', () => { expect(component.find('CollapsibleNavGroupEnabled').exists()).toBeTruthy(); }); - it('show hide expand icon in top left navigation when workspace enabled + homepage + new navigation enabled', () => { - const branding = { - useExpandedHeader: false, - }; - const props = { - ...mockProps(), - branding, - }; - props.application.currentAppId$ = new BehaviorSubject('home'); - props.application.capabilities = { ...props.application.capabilities }; - (props.application.capabilities.workspaces as Record) = {}; - (props.application.capabilities.workspaces as Record).enabled = true; - - const component = mountWithIntl(
); - - expect(component.find('.header__toggleNavButtonSection').exists()).toBeFalsy(); - }); - it('toggles primary navigation menu when clicked', () => { const branding = { useExpandedHeader: false, From d914b004b8e10e0042ab3dd04778aab5be9ab0e8 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 16 Aug 2024 18:29:54 +0800 Subject: [PATCH 04/15] feat: finish picker content Signed-off-by: SuZhou-Joe --- .../chrome/nav_group/nav_group_service.ts | 35 ++- .../header/collapsible_nav_group_enabled.tsx | 202 +++++++++--------- .../collapsible_nav_group_enabled_top.tsx | 4 +- src/core/public/chrome/utils.ts | 9 +- src/plugins/data_explorer/public/plugin.ts | 36 ++++ src/plugins/home/public/plugin.ts | 12 +- src/plugins/workspace/public/plugin.ts | 4 +- 7 files changed, 176 insertions(+), 126 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index e0c69de353b0..4c34d66e2afa 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -14,11 +14,16 @@ import { import { map, switchMap, takeUntil } from 'rxjs/operators'; import { i18n } from '@osd/i18n'; import { IUiSettingsClient } from '../../ui_settings'; -import { fulfillRegistrationLinksToChromeNavLinks, getSortedNavLinks } from '../utils'; +import { + fulfillRegistrationLinksToChromeNavLinks, + getSortedNavLinks, + getVisibleUseCases, +} from '../utils'; import { ChromeNavLinks } from '../nav_links'; import { InternalApplicationStart } from '../../application'; import { NavGroupStatus } from '../../../../core/types'; import { ChromeBreadcrumb, ChromeBreadcrumbEnricher } from '../chrome_service'; +import { ALL_USE_CASE_ID } from '../../../utils'; export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; @@ -260,18 +265,26 @@ export class ChromeNavGroupService { } if (appId && navGroupMap) { const appIdNavGroupMap = new Map>(); - // iterate navGroupMap - Object.keys(navGroupMap) + let visibleNavGroups: NavGroupItemInMap[] = []; + const visibleUseCases = getVisibleUseCases(navGroupMap); + if (visibleUseCases.length === 1 && visibleUseCases[0].id === ALL_USE_CASE_ID) { + // If the only visible use case is all use case + // All the other nav groups will be visible because all use case can visit all of the nav groups. + visibleNavGroups = Object.values(navGroupMap); + } else { // Nav group of Hidden status should be filtered out when counting navGroups the currentApp belongs to - .filter((navGroupId) => navGroupMap[navGroupId].status !== NavGroupStatus.Hidden) - .forEach((navGroupId) => { - navGroupMap[navGroupId].navLinks.forEach((navLink) => { - const navLinkId = navLink.id; - const navGroupSet = appIdNavGroupMap.get(navLinkId) || new Set(); - navGroupSet.add(navGroupId); - appIdNavGroupMap.set(navLinkId, navGroupSet); - }); + visibleNavGroups = Object.values(navGroupMap).filter( + (navGroup) => navGroup.status !== NavGroupStatus.Hidden + ); + } + visibleNavGroups.forEach((navGroup) => { + navGroup.navLinks.forEach((navLink) => { + const navLinkId = navLink.id; + const navGroupSet = appIdNavGroupMap.get(navLinkId) || new Set(); + navGroupSet.add(navGroup.id); + appIdNavGroupMap.set(navLinkId, navGroupSet); }); + }); const navGroups = appIdNavGroupMap.get(appId); if (navGroups && navGroups.size === 1) { diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx index 762e7103349b..7a4b39e59561 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -12,7 +12,7 @@ import * as Rx from 'rxjs'; import classNames from 'classnames'; import { WorkspacesStart } from 'src/core/public/workspace'; import { ChromeNavControl, ChromeNavLink } from '../..'; -import { AppCategory, NavGroupStatus, NavGroupType } from '../../../../types'; +import { AppCategory, NavGroupType } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; @@ -23,7 +23,7 @@ import { ChromeRegistrationNavLink, NavGroupItemInMap, } from '../../nav_group'; -import { fulfillRegistrationLinksToChromeNavLinks, sortBy } from '../../utils'; +import { fulfillRegistrationLinksToChromeNavLinks, getVisibleUseCases, sortBy } from '../../utils'; import { ALL_USE_CASE_ID, DEFAULT_APP_CATEGORIES } from '../../../../../core/utils'; import { CollapsibleNavTop } from './collapsible_nav_group_enabled_top'; import { HeaderNavControls } from './header_nav_controls'; @@ -57,11 +57,11 @@ const titleForSeeAll = i18n.translate('core.ui.primaryNav.seeAllLabel', { }); // Custom category is used for those features not belong to any of use cases in all use case. -// and the custom category should always sit before manage category +// and the custom category should always sit after manage category const customCategory: AppCategory = { id: 'custom', label: i18n.translate('core.ui.customNavList.label', { defaultMessage: 'Custom' }), - order: (DEFAULT_APP_CATEGORIES.manage.order || 0) - 500, + order: (DEFAULT_APP_CATEGORIES.manage.order || 0) + 500, }; enum NavWidth { @@ -85,7 +85,6 @@ export function CollapsibleNavGroupEnabled({ collapsibleNavHeaderRender, ...observables }: CollapsibleNavGroupEnabledProps) { - const currentWorkspace = useObservable(observables.currentWorkspace$, undefined); const allNavLinks = useObservable(observables.navLinks$, []); const navLinks = allNavLinks.filter((link) => !link.hidden); const homeLink = useMemo(() => allNavLinks.find((item) => item.id === 'home'), [allNavLinks]); @@ -101,27 +100,34 @@ export function CollapsibleNavGroupEnabled({ [navGroupsMap, navLinks] ); - const visibleUseCases = useMemo( - () => - Object.values(navGroupsMap).filter( - (group) => group.type === undefined && group.status !== NavGroupStatus.Hidden - ), - [navGroupsMap] - ); + const visibleUseCases = useMemo(() => getVisibleUseCases(navGroupsMap), [navGroupsMap]); + + const currentNavGroupId = useMemo(() => { + if (!currentNavGroup) { + if (visibleUseCases.length === 1) { + return visibleUseCases[0].id; + } + + if (!capabilities.workspaces.enabled) { + return ALL_USE_CASE_ID; + } + } + + return currentNavGroup?.id; + }, [capabilities, currentNavGroup, visibleUseCases]); + + const shouldAppendManageCategory = capabilities.workspaces.enabled + ? !currentNavGroupId + : currentNavGroupId === ALL_USE_CASE_ID; const shouldShowCollapsedNavHeaderContent = useMemo( - () => - isNavOpen && - collapsibleNavHeaderRender && - capabilities.workspaces.enabled && - !currentWorkspace && - !currentNavGroup, - [currentWorkspace, capabilities, collapsibleNavHeaderRender, isNavOpen, currentNavGroup] + () => isNavOpen && collapsibleNavHeaderRender && !currentNavGroupId, + [collapsibleNavHeaderRender, isNavOpen, currentNavGroupId] ); const navLinksForRender: ChromeNavLink[] = useMemo(() => { - if (shouldShowCollapsedNavHeaderContent) { - const resultLinks: ChromeNavLink[] = []; + const getSystemNavGroups = () => { + const result: ChromeNavLink[] = []; Object.values(navGroupsMap) .sort(sortBy('order')) .filter((navGroup) => navGroup.type === NavGroupType.SYSTEM) @@ -131,7 +137,7 @@ export function CollapsibleNavGroupEnabled({ navLinks ); if (visibleNavLinksWithinNavGroup[0]) { - resultLinks.push({ + result.push({ ...visibleNavLinksWithinNavGroup[0], title: navGroup.title, category: DEFAULT_APP_CATEGORIES.manage, @@ -139,97 +145,95 @@ export function CollapsibleNavGroupEnabled({ } }); - return resultLinks; - } + return result; + }; - if (currentNavGroup && currentNavGroup.id !== ALL_USE_CASE_ID) { - return fulfillRegistrationLinksToChromeNavLinks( - navGroupsMap[currentNavGroup.id].navLinks || [], - navLinks - ); - } + const navLinksResult: ChromeRegistrationNavLink[] = []; - if (visibleUseCases.length === 1) { - return fulfillRegistrationLinksToChromeNavLinks( - navGroupsMap[visibleUseCases[0].id].navLinks || [], - navLinks - ); + if (currentNavGroupId && currentNavGroupId !== ALL_USE_CASE_ID) { + navLinksResult.push(...(navGroupsMap[currentNavGroupId].navLinks || [])); } - const navLinksForAll: ChromeRegistrationNavLink[] = []; - - // Append all the links that do not have use case info to keep backward compatible - const linkIdsWithUseGroupInfo = Object.values(navGroupsMap).reduce((total, navGroup) => { - return [...total, ...navGroup.navLinks.map((navLink) => navLink.id)]; - }, [] as string[]); - navLinks - .filter((link) => !linkIdsWithUseGroupInfo.includes(link.id)) - .forEach((navLink) => { - navLinksForAll.push({ - ...navLink, - category: customCategory, + if (currentNavGroupId === ALL_USE_CASE_ID) { + // Append all the links that do not have use case info to keep backward compatible + const linkIdsWithUseGroupInfo = Object.values(navGroupsMap).reduce((total, navGroup) => { + return [...total, ...navGroup.navLinks.map((navLink) => navLink.id)]; + }, [] as string[]); + navLinks + .filter((link) => !linkIdsWithUseGroupInfo.includes(link.id)) + .forEach((navLink) => { + navLinksResult.push({ + ...navLink, + category: customCategory, + }); }); + + // Append all the links registered to all use case + navGroupsMap[ALL_USE_CASE_ID]?.navLinks.forEach((navLink) => { + navLinksResult.push(navLink); }); - // Append all the links registered to all use case - navGroupsMap[ALL_USE_CASE_ID]?.navLinks.forEach((navLink) => { - navLinksForAll.push(navLink); - }); + // Append use case section into left navigation + Object.values(navGroupsMap) + .filter((group) => !group.type) + .forEach((group) => { + const categoryInfo = { + id: group.id, + label: group.title, + order: group.order, + }; - // Append use case section into left navigation - Object.values(navGroupsMap) - .filter((group) => !group.type) - .forEach((group) => { - const categoryInfo = { - id: group.id, - label: group.title, - order: group.order, - }; - const linksForAllUseCaseWithinNavGroup = fulfillRegistrationLinksToChromeNavLinks( - group.navLinks, - navLinks - ) - .filter((navLink) => navLink.showInAllNavGroup) - .map((navLink) => ({ - ...navLink, - category: categoryInfo, - })); + const fulfilledLinksOfNavGroup = fulfillRegistrationLinksToChromeNavLinks( + group.navLinks, + navLinks + ); - navLinksForAll.push(...linksForAllUseCaseWithinNavGroup); + const linksForAllUseCaseWithinNavGroup = fulfilledLinksOfNavGroup + .filter((navLink) => navLink.showInAllNavGroup) + .map((navLink) => ({ + ...navLink, + category: categoryInfo, + })); - if (linksForAllUseCaseWithinNavGroup.length) { - navLinksForAll.push({ - id: group.navLinks[0].id, - title: titleForSeeAll, - order: Number.MAX_SAFE_INTEGER, - category: categoryInfo, - }); - } else { - /** - * Find if there are any links inside a use case but without a `see all` entry. - * If so, append these features into custom category as a fallback - */ - fulfillRegistrationLinksToChromeNavLinks(group.navLinks, navLinks) - // Filter out links that already exists in all use case - .filter( - (navLink) => !navLinksForAll.find((navLinkInAll) => navLinkInAll.id === navLink.id) - ) - .forEach((navLink) => { - navLinksForAll.push({ - ...navLink, - category: customCategory, - }); + navLinksResult.push(...linksForAllUseCaseWithinNavGroup); + + if (linksForAllUseCaseWithinNavGroup.length) { + navLinksResult.push({ + id: fulfilledLinksOfNavGroup[0].id, + title: titleForSeeAll, + order: Number.MAX_SAFE_INTEGER, + category: categoryInfo, }); - } - }); + } else { + /** + * Find if there are any links inside a use case but without a `see all` entry. + * If so, append these features into custom category as a fallback + */ + fulfillRegistrationLinksToChromeNavLinks(group.navLinks, navLinks) + // Filter out links that already exists in all use case + .filter( + (navLink) => !navLinksResult.find((navLinkInAll) => navLinkInAll.id === navLink.id) + ) + .forEach((navLink) => { + navLinksResult.push({ + ...navLink, + category: customCategory, + }); + }); + } + }); + } + + if (shouldAppendManageCategory) { + navLinksResult.push(...getSystemNavGroups()); + } - return fulfillRegistrationLinksToChromeNavLinks(navLinksForAll, navLinks); + return fulfillRegistrationLinksToChromeNavLinks(navLinksResult, navLinks); }, [ navLinks, navGroupsMap, - currentNavGroup, - visibleUseCases, - shouldShowCollapsedNavHeaderContent, + currentNavGroupId, + shouldAppendManageCategory, ]); const width = useMemo(() => { @@ -296,7 +300,7 @@ export function CollapsibleNavGroupEnabled({ navigateToApp={navigateToApp} logos={logos} setCurrentNavGroup={setCurrentNavGroup} - currentNavGroup={currentNavGroup} + currentNavGroup={currentNavGroupId ? navGroupsMap[currentNavGroupId] : undefined} shouldShrinkNavigation={!isNavOpen} onClickShrink={closeNav} visibleUseCases={visibleUseCases} diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx index 23e2f7e6108c..067fb2ffd2e1 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx @@ -56,7 +56,9 @@ export const CollapsibleNavTop = ({ * 3. current nav group is not all use case */ const isInsideSecondLevelOfAllWorkspace = - visibleUseCases.length > 1 && !!currentWorkspace && currentNavGroup?.id !== ALL_USE_CASE_ID; + !!currentWorkspace && + visibleUseCases[0].id === ALL_USE_CASE_ID && + currentNavGroup?.id !== ALL_USE_CASE_ID; const shouldShowBackButton = !shouldShrinkNavigation && isInsideSecondLevelOfAllWorkspace; const shouldShowHomeLink = !shouldShrinkNavigation && !shouldShowBackButton; diff --git a/src/core/public/chrome/utils.ts b/src/core/public/chrome/utils.ts index facb41b9b9a0..2d165e103e9c 100644 --- a/src/core/public/chrome/utils.ts +++ b/src/core/public/chrome/utils.ts @@ -5,7 +5,8 @@ import { AppCategory } from 'opensearch-dashboards/public'; import { ChromeNavLink } from './nav_links'; -import { ChromeRegistrationNavLink } from './nav_group'; +import { ChromeRegistrationNavLink, NavGroupItemInMap } from './nav_group'; +import { NavGroupStatus } from '../../../core/types'; type KeyOf = keyof T; @@ -214,3 +215,9 @@ export const getSortedNavLinks = ( ); return acc; }; + +export const getVisibleUseCases = (navGroupMap: Record) => { + return Object.values(navGroupMap).filter( + (navGroup) => navGroup.status !== NavGroupStatus.Hidden && navGroup.type === undefined + ); +}; diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts index 3b953567fbe8..845e07595022 100644 --- a/src/plugins/data_explorer/public/plugin.ts +++ b/src/plugins/data_explorer/public/plugin.ts @@ -13,6 +13,7 @@ import { AppNavLinkStatus, ScopedHistory, AppUpdater, + DEFAULT_NAV_GROUPS, } from '../../../core/public'; import { DataExplorerPluginSetup, @@ -123,6 +124,41 @@ export class DataExplorerPlugin }, }); + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.observability, [ + { + id: PLUGIN_ID, + order: 300, + }, + ]); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS['security-analytics'], [ + { + id: PLUGIN_ID, + order: 300, + }, + ]); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.essentials, [ + { + id: PLUGIN_ID, + order: 200, + }, + ]); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.search, [ + { + id: PLUGIN_ID, + order: 200, + }, + ]); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [ + { + id: PLUGIN_ID, + order: 200, + }, + ]); + return { ...this.viewService.setup(), }; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index fe1099a8e635..964ff6af1657 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -64,7 +64,6 @@ import { PLUGIN_ID, HOME_APP_BASE_PATH, IMPORT_SAMPLE_DATA_APP_ID } from '../com import { DataSourcePluginStart } from '../../data_source/public'; import { workWithDataSection } from './application/components/homepage/sections/work_with_data'; import { learnBasicsSection } from './application/components/homepage/sections/learn_basics'; -import { DEFAULT_NAV_GROUPS } from '../../../core/public'; import { ContentManagementPluginSetup, ContentManagementPluginStart, @@ -151,9 +150,7 @@ export class HomePublicPlugin core.application.register({ id: PLUGIN_ID, title: 'Home', - navLinkStatus: core.chrome.navGroup.getNavGroupEnabled() - ? undefined - : AppNavLinkStatus.hidden, + navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { const [coreStart] = await core.getStartServices(); setCommonService(); @@ -166,13 +163,6 @@ export class HomePublicPlugin workspaceAvailability: WorkspaceAvailability.outsideWorkspace, }); - core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [ - { - id: PLUGIN_ID, - title: 'Home', - }, - ]); - // Register import sample data as a standalone app so that it is available inside workspace. core.application.register({ id: IMPORT_SAMPLE_DATA_APP_ID, diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 31b12340d289..f1859e7c726e 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -156,15 +156,13 @@ export class WorkspacePlugin * It checks the following conditions: * 1. The navigation group is not a system-level group. * 2. The current workspace has feature configurations set up. - * 3. The current workspace's use case is not "All use case". - * 4. The current navigation group is not included in the feature configurations of the workspace. + * 3. The current navigation group is not included in the feature configurations of the workspace. * * If all these conditions are true, it means that the navigation group should be hidden. */ if ( navGroup.type !== NavGroupType.SYSTEM && currentWorkspace.features && - getFirstUseCaseOfFeatureConfigs(currentWorkspace.features) !== ALL_USE_CASE_ID && !isNavGroupInFeatureConfigs(navGroup.id, currentWorkspace.features) ) { return { From 78a2e03a320bd98ac3dbc89d590df6695d71e46a Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 16 Aug 2024 18:30:09 +0800 Subject: [PATCH 05/15] feat: finish picker content Signed-off-by: SuZhou-Joe --- .../chrome/ui/header/collapsible_nav_group_enabled.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx index 7a4b39e59561..f48a555a24b8 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -229,12 +229,7 @@ export function CollapsibleNavGroupEnabled({ } return fulfillRegistrationLinksToChromeNavLinks(navLinksResult, navLinks); - }, [ - navLinks, - navGroupsMap, - currentNavGroupId, - shouldAppendManageCategory, - ]); + }, [navLinks, navGroupsMap, currentNavGroupId, shouldAppendManageCategory]); const width = useMemo(() => { if (!isNavOpen) { From c06c63d23e842f0b8d26328bea9c6fd0a675da93 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 16 Aug 2024 19:06:24 +0800 Subject: [PATCH 06/15] feat: only register index patterns to settings and setup when workspace is disabled Signed-off-by: SuZhou-Joe --- .../index_pattern_management/public/plugin.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index d74cdaffe97e..5971bcc0fe3e 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -167,13 +167,17 @@ export class IndexPatternManagementPlugin }, }); - core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.settingsAndSetup, [ - { - id: IPM_APP_ID, - title: sectionsHeader, - order: 400, - }, - ]); + core.getStartServices().then(([coreStart]) => { + if (!coreStart.application.capabilities.workspaces.enabled) { + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.settingsAndSetup, [ + { + id: IPM_APP_ID, + title: sectionsHeader, + order: 400, + }, + ]); + } + }); return this.indexPatternManagementService.setup({ httpClient: core.http }); } From e13f81a6d3bd2f3f10b875821b1a28ab86feca93 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 19 Aug 2024 13:50:13 +0800 Subject: [PATCH 07/15] fix: unit test Signed-off-by: SuZhou-Joe --- ...ollapsible_nav_group_enabled.test.tsx.snap | 203 ----------------- .../collapsible_nav_groups.test.tsx.snap | 204 ++++++++++++++++++ ...collapsible_nav_group_enabled_top.test.tsx | 8 +- .../public/plugin.test.ts | 5 +- 4 files changed, 209 insertions(+), 211 deletions(-) create mode 100644 src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_groups.test.tsx.snap diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap index 95e501650f08..aa3c4693300c 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap @@ -412,206 +412,3 @@ exports[` should show all use case when current na
`; - -exports[` should render correctly 1`] = ` -
-
- -
-
-`; diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_groups.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_groups.test.tsx.snap new file mode 100644 index 000000000000..3d923a49dd80 --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_groups.test.tsx.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render correctly 1`] = ` +
+
+ +
+
+`; diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.test.tsx index 9c701e007bbd..d5dac8cc471b 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.test.tsx @@ -44,17 +44,11 @@ describe('', () => { currentWorkspace$: new BehaviorSubject({ id: 'foo', name: 'foo' }), visibleUseCases: [ { - id: 'navGroupFoo', + id: ALL_USE_CASE_ID, title: 'navGroupFoo', description: 'navGroupFoo', navLinks: [], }, - { - id: 'navGroupBar', - title: 'navGroupBar', - description: 'navGroupBar', - navLinks: [], - }, ], currentNavGroup: { id: 'navGroupFoo', diff --git a/src/plugins/index_pattern_management/public/plugin.test.ts b/src/plugins/index_pattern_management/public/plugin.test.ts index ec9a6137ffcf..4947c3d2749a 100644 --- a/src/plugins/index_pattern_management/public/plugin.test.ts +++ b/src/plugins/index_pattern_management/public/plugin.test.ts @@ -12,6 +12,7 @@ import { ManagementAppMountParams, RegisterManagementAppArgs, } from 'src/plugins/management/public'; +import { waitFor } from '@testing-library/dom'; describe('DiscoverPlugin', () => { it('setup successfully', () => { @@ -25,7 +26,9 @@ describe('DiscoverPlugin', () => { }) ).not.toThrow(); expect(setupMock.application.register).toBeCalledTimes(1); - expect(setupMock.chrome.navGroup.addNavLinksToGroup).toBeCalledTimes(1); + waitFor(() => { + expect(setupMock.chrome.navGroup.addNavLinksToGroup).toBeCalledTimes(1); + }); }); it('when new navigation is enabled, should navigate to standard IPM app', async () => { From cf1efbb42d17a0ab53de60870a70d39339229215 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 19 Aug 2024 13:52:41 +0800 Subject: [PATCH 08/15] feat: put discover 2.0 behind discover Signed-off-by: SuZhou-Joe --- src/plugins/data_explorer/public/plugin.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts index 845e07595022..d2c8da53a697 100644 --- a/src/plugins/data_explorer/public/plugin.ts +++ b/src/plugins/data_explorer/public/plugin.ts @@ -127,35 +127,35 @@ export class DataExplorerPlugin core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.observability, [ { id: PLUGIN_ID, - order: 300, + order: 301, // The nav link should be put behind discover }, ]); core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS['security-analytics'], [ { id: PLUGIN_ID, - order: 300, + order: 301, }, ]); core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.essentials, [ { id: PLUGIN_ID, - order: 200, + order: 201, }, ]); core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.search, [ { id: PLUGIN_ID, - order: 200, + order: 201, }, ]); core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [ { id: PLUGIN_ID, - order: 200, + order: 201, }, ]); From 346171ec1e79992746e64455bb9a1114bcd9891f Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 21 Aug 2024 17:09:45 +0800 Subject: [PATCH 09/15] feat: add coverage Signed-off-by: SuZhou-Joe --- .../nav_group/nav_group_service.test.ts | 50 +++++++++++++++++-- .../chrome/nav_group/nav_group_service.ts | 6 +-- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.test.ts b/src/core/public/chrome/nav_group/nav_group_service.test.ts index 91a6b2a0a6de..06712058fb23 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.test.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.test.ts @@ -14,7 +14,7 @@ import { uiSettingsServiceMock } from '../../ui_settings/ui_settings_service.moc import { NavLinksService } from '../nav_links'; import { applicationServiceMock, httpServiceMock, workspacesServiceMock } from '../../mocks'; import { AppCategory } from 'opensearch-dashboards/public'; -import { DEFAULT_NAV_GROUPS } from '../../'; +import { DEFAULT_NAV_GROUPS, NavGroupStatus, ALL_USE_CASE_ID } from '../../'; import { ChromeBreadcrumbEnricher } from '../chrome_service'; const mockedGroupFoo = { @@ -381,7 +381,50 @@ describe('ChromeNavGroupService#start()', () => { expect(currentNavGroup?.title).toEqual('barGroupTitle'); }); - it('should erase current nav group if application is home', async () => { + it('should be able to find the right nav group when visible nav group is all', async () => { + const uiSettings = uiSettingsServiceMock.createSetupContract(); + const navGroupEnabled$ = new Rx.BehaviorSubject(true); + uiSettings.get$.mockImplementation(() => navGroupEnabled$); + + const chromeNavGroupService = new ChromeNavGroupService(); + const chromeNavGroupServiceSetup = chromeNavGroupService.setup({ uiSettings }); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: ALL_USE_CASE_ID, + title: 'fooGroupTitle', + description: 'foo description', + }, + [mockedNavLinkFoo] + ); + + chromeNavGroupServiceSetup.addNavLinksToGroup( + { + id: 'bar-group', + title: 'barGroupTitle', + description: 'bar description', + status: NavGroupStatus.Hidden, + }, + [mockedNavLinkFoo, mockedNavLinkBar] + ); + + const chromeNavGroupServiceStart = await chromeNavGroupService.start({ + navLinks: mockedNavLinkService, + application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), + }); + mockedApplicationService.navigateToApp(mockedNavLinkBar.id); + + const currentNavGroup = await chromeNavGroupServiceStart + .getCurrentNavGroup$() + .pipe(first()) + .toPromise(); + + expect(currentNavGroup?.id).toEqual('bar-group'); + }); + + it('should erase current nav group if application can not be found in any of the visible nav groups', async () => { const uiSettings = uiSettingsServiceMock.createSetupContract(); const navGroupEnabled$ = new Rx.BehaviorSubject(true); uiSettings.get$.mockImplementation(() => navGroupEnabled$); @@ -403,6 +446,7 @@ describe('ChromeNavGroupService#start()', () => { id: 'bar-group', title: 'barGroupTitle', description: 'bar description', + status: NavGroupStatus.Hidden, }, [mockedNavLinkFoo, mockedNavLinkBar] ); @@ -416,7 +460,7 @@ describe('ChromeNavGroupService#start()', () => { chromeNavGroupServiceStart.setCurrentNavGroup('foo-group'); - mockedApplicationService.navigateToApp('home'); + mockedApplicationService.navigateToApp(mockedNavLinkBar.id); const currentNavGroup = await chromeNavGroupServiceStart .getCurrentNavGroup$() .pipe(first()) diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index 4c34d66e2afa..d15aa6f42c0a 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -216,7 +216,7 @@ export class ChromeNavGroupService { const setCurrentNavGroup = (navGroupId: string | undefined) => { const navGroup = navGroupId ? this.navGroupsMap$.getValue()[navGroupId] : undefined; - if (navGroup && navGroup.status !== NavGroupStatus.Hidden) { + if (navGroup) { this.currentNavGroup$.next(navGroup); sessionStorage.setItem(CURRENT_NAV_GROUP_ID, navGroup.id); } else { @@ -259,10 +259,6 @@ export class ChromeNavGroupService { application.currentAppId$, this.getSortedNavGroupsMap$(), ]).subscribe(([appId, navGroupMap]) => { - if (appId === 'home') { - setCurrentNavGroup(undefined); - return; - } if (appId && navGroupMap) { const appIdNavGroupMap = new Map>(); let visibleNavGroups: NavGroupItemInMap[] = []; From 584ab226e5249a49a074a3c358be8aaef302c6b8 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 21 Aug 2024 17:37:25 +0800 Subject: [PATCH 10/15] feat: improve test coverage Signed-off-by: SuZhou-Joe --- .../collapsible_nav_group_enabled.test.tsx | 36 +- src/plugins/workspace/public/plugin.test.ts | 684 +++++++++--------- 2 files changed, 389 insertions(+), 331 deletions(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx index 100a711dba01..418dca694e21 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx @@ -15,7 +15,12 @@ import { ChromeNavLink } from '../../nav_links'; import { NavGroupItemInMap } from '../../nav_group'; import { httpServiceMock } from '../../../mocks'; import { getLogos } from '../../../../common'; -import { ALL_USE_CASE_ID, DEFAULT_NAV_GROUPS, WorkspaceObject } from '../../../../public'; +import { + ALL_USE_CASE_ID, + DEFAULT_APP_CATEGORIES, + DEFAULT_NAV_GROUPS, + WorkspaceObject, +} from '../../../../public'; import { capabilitiesServiceMock } from '../../../application/capabilities/capabilities_service.mock'; jest.mock('./collapsible_nav_group_enabled_top', () => ({ @@ -272,4 +277,33 @@ describe('', () => { expect(getByTestId('collapsibleNavAppLink-link-in-essentials')).toBeInTheDocument(); expect(queryAllByTestId('collapsibleNavAppLink-link-in-all').length).toEqual(1); }); + + it('should render manage category when in all use case if workspace disabled', () => { + const props = mockProps({ + currentNavGroupId: ALL_USE_CASE_ID, + navGroupsMap: { + ...defaultNavGroupMap, + [DEFAULT_NAV_GROUPS.dataAdministration.id]: { + ...DEFAULT_NAV_GROUPS.dataAdministration, + navLinks: [ + { + id: 'link-in-dataAdministration', + title: 'link-in-dataAdministration', + }, + ], + }, + }, + navLinks: [ + { + id: 'link-in-dataAdministration', + title: 'link-in-dataAdministration', + baseUrl: '', + href: '', + }, + ], + }); + const { getByText } = render(); + // Should render manage category + expect(getByText(DEFAULT_APP_CATEGORIES.manage.label)).toBeInTheDocument(); + }); }); diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 41f779911876..8d08c2fe657a 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -32,379 +32,403 @@ describe('Workspace plugin', () => { WorkspaceClientMock.mockClear(); Object.values(workspaceClientMock).forEach((item) => item.mockClear()); }); - it('#setup', async () => { - const setupMock = getSetupMock(); - const savedObjectManagementSetupMock = savedObjectsManagementPluginMock.createSetupContract(); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, { - savedObjectsManagement: savedObjectManagementSetupMock, - management: managementPluginMock.createSetupContract(), + + describe('#setup', () => { + it('#setup', async () => { + const setupMock = getSetupMock(); + const savedObjectManagementSetupMock = savedObjectsManagementPluginMock.createSetupContract(); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + savedObjectsManagement: savedObjectManagementSetupMock, + management: managementPluginMock.createSetupContract(), + }); + expect(setupMock.application.register).toBeCalledTimes(5); + expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1); }); - expect(setupMock.application.register).toBeCalledTimes(5); - expect(WorkspaceClientMock).toBeCalledTimes(1); - expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1); - }); - it('#call savedObjectsClient.setCurrentWorkspace when current workspace id changed', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - workspacePlugin.start(coreStart, mockDependencies); - coreStart.workspaces.currentWorkspaceId$.next('foo'); - expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); - expect(setupMock.application.register).toBeCalledTimes(5); - expect(WorkspaceClientMock).toBeCalledTimes(1); - expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0); - }); + it('#setup call savedObjectsClient.setCurrentWorkspace when current workspace id changed', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + workspacePlugin.start(coreStart, mockDependencies); + coreStart.workspaces.currentWorkspaceId$.next('foo'); + expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); + expect(setupMock.application.register).toBeCalledTimes(5); + expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0); + }); - it('#setup when workspace id is in url and enterWorkspace return error', async () => { - const windowSpy = jest.spyOn(window, 'window', 'get'); - windowSpy.mockImplementation( - () => - ({ - location: { - href: 'http://localhost/w/workspaceId/app', + it('#setup when workspace id is in url and enterWorkspace return error', async () => { + const windowSpy = jest.spyOn(window, 'window', 'get'); + windowSpy.mockImplementation( + () => + ({ + location: { + href: 'http://localhost/w/workspaceId/app', + }, + } as any) + ); + workspaceClientMock.enterWorkspace.mockResolvedValue({ + success: false, + error: 'error', + }); + const setupMock = getSetupMock(); + const applicationStartMock = applicationServiceMock.createStartContract(); + const chromeStartMock = chromeServiceMock.createStartContract(); + setupMock.getStartServices.mockImplementation(() => { + return Promise.resolve([ + { + application: applicationStartMock, + chrome: chromeStartMock, }, - } as any) - ); - workspaceClientMock.enterWorkspace.mockResolvedValue({ - success: false, - error: 'error', - }); - const setupMock = getSetupMock(); - 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, { + management: managementPluginMock.createSetupContract(), + }); + expect(setupMock.application.register).toBeCalledTimes(5); + expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId'); + expect(setupMock.getStartServices).toBeCalledTimes(2); + await waitFor( + () => { + expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_FATAL_ERROR_APP_ID, { + replace: true, + state: { + error: 'error', + }, + }); }, - {}, - {}, - ]) as any; + { + container: document.body, + } + ); + windowSpy.mockRestore(); }); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, { - management: managementPluginMock.createSetupContract(), - }); - expect(setupMock.application.register).toBeCalledTimes(5); - expect(WorkspaceClientMock).toBeCalledTimes(1); - expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId'); - expect(setupMock.getStartServices).toBeCalledTimes(2); - await waitFor( - () => { - expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_FATAL_ERROR_APP_ID, { - replace: true, - state: { - error: 'error', + 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 = getSetupMock(); + const applicationStartMock = applicationServiceMock.createStartContract(); + const chromeStartMock = chromeServiceMock.createStartContract(); + let currentAppIdSubscriber: Subscriber | undefined; + setupMock.getStartServices.mockImplementation(() => { + return Promise.resolve([ + { + application: { + ...applicationStartMock, + currentAppId$: new Observable((subscriber) => { + currentAppIdSubscriber = subscriber; + }), + }, + chrome: chromeStartMock, }, - }); - }, - { - container: document.body, - } - ); - windowSpy.mockRestore(); - }); + {}, + {}, + ]) as any; + }); + + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + management: managementPluginMock.createSetupContract(), + }); + currentAppIdSubscriber?.next(WORKSPACE_FATAL_ERROR_APP_ID); + expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_DETAIL_APP_ID); + 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', + it('#setup should register workspace list with a visible application and register to settingsAndSetup nav group', async () => { + const setupMock = coreMock.createSetup(); + setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_list', + navLinkStatus: AppNavLinkStatus.visible, + }) + ); + expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( + DEFAULT_NAV_GROUPS.settingsAndSetup, + expect.arrayContaining([ + { + id: 'workspace_list', + order: 150, + title: 'Workspace settings', }, - } as any) - ); - workspaceClientMock.enterWorkspace.mockResolvedValue({ - success: true, - error: 'error', + ]) + ); }); - const setupMock = getSetupMock(); - const applicationStartMock = applicationServiceMock.createStartContract(); - const chromeStartMock = chromeServiceMock.createStartContract(); - let currentAppIdSubscriber: Subscriber | undefined; - setupMock.getStartServices.mockImplementation(() => { - return Promise.resolve([ - { - application: { - ...applicationStartMock, - currentAppId$: new Observable((subscriber) => { - currentAppIdSubscriber = subscriber; - }), + + it('#setup should register workspace detail with a visible application and register to all nav group', async () => { + const setupMock = coreMock.createSetup(); + setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_detail', + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); + + expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( + DEFAULT_NAV_GROUPS.all, + expect.arrayContaining([ + { + id: 'workspace_detail', + title: 'Overview', + order: 100, }, - chrome: chromeStartMock, - }, - {}, - {}, - ]) as any; + ]) + ); }); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, { - management: managementPluginMock.createSetupContract(), + it('#setup should register workspace initial with a visible application', async () => { + const setupMock = coreMock.createSetup(); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_initial', + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); }); - currentAppIdSubscriber?.next(WORKSPACE_FATAL_ERROR_APP_ID); - expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_DETAIL_APP_ID); - windowSpy.mockRestore(); - }); - it('#setup should register workspace list with a visible application and register to settingsAndSetup nav group', async () => { - const setupMock = coreMock.createSetup(); - setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - - expect(setupMock.application.register).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_list', - navLinkStatus: AppNavLinkStatus.visible, - }) - ); - expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( - DEFAULT_NAV_GROUPS.settingsAndSetup, - expect.arrayContaining([ - { - id: 'workspace_list', - order: 150, - title: 'Workspace settings', - }, - ]) - ); + it('#setup should register registerCollapsibleNavHeader when new left nav is turned on', async () => { + const setupMock = coreMock.createSetup(); + let collapsibleNavHeaderImplementation = () => null; + setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + setupMock.chrome.registerCollapsibleNavHeader.mockImplementation( + (func) => (collapsibleNavHeaderImplementation = func) + ); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + expect(collapsibleNavHeaderImplementation()).toEqual(null); + const startMock = coreMock.createStart(); + await workspacePlugin.start(startMock, mockDependencies); + expect(collapsibleNavHeaderImplementation()).not.toEqual(null); + }); }); - it('#setup should register workspace detail with a visible application and register to all nav group', async () => { - const setupMock = coreMock.createSetup(); - setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - - expect(setupMock.application.register).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_detail', - navLinkStatus: AppNavLinkStatus.hidden, - }) - ); - - expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( - DEFAULT_NAV_GROUPS.all, - expect.arrayContaining([ - { - id: 'workspace_detail', - title: 'Overview', - order: 100, - }, - ]) - ); - }); + describe('#start', () => { + it('#start add workspace detail page to breadcrumbs when start', async () => { + const startMock = coreMock.createStart(); + const workspaceObject = { + id: 'foo', + name: 'bar', + }; + startMock.workspaces.currentWorkspace$.next(workspaceObject); + const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]); + startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(startMock.chrome.setBreadcrumbs).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + text: 'bar', + }), + expect.objectContaining({ + text: 'Home', + }), + ]) + ); + }); - it('#setup should register workspace initial with a visible application', async () => { - const setupMock = coreMock.createSetup(); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - - expect(setupMock.application.register).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_initial', - navLinkStatus: AppNavLinkStatus.hidden, - }) - ); - }); + it('#start do not add workspace detail page to breadcrumbs when already exists', async () => { + const startMock = coreMock.createStart(); + const workspaceObject = { + id: 'foo', + name: 'bar', + }; + startMock.workspaces.currentWorkspace$.next(workspaceObject); + const breadcrumbs = new BehaviorSubject([ + { text: 'home' }, + { text: 'bar' }, + ]); + startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled(); + }); - it('#start add workspace detail page to breadcrumbs when start', async () => { - const startMock = coreMock.createStart(); - const workspaceObject = { - id: 'foo', - name: 'bar', - }; - startMock.workspaces.currentWorkspace$.next(workspaceObject); - const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]); - startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock, mockDependencies); - expect(startMock.chrome.setBreadcrumbs).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - text: 'bar', - }), + it('#start should register workspace list card into new home page', async () => { + const startMock = coreMock.createStart(); + startMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(mockDependencies.contentManagement.registerContentProvider).toHaveBeenCalledWith( expect.objectContaining({ - text: 'Home', - }), - ]) - ); - }); + id: 'workspace_list_card_home', + }) + ); + }); - it('#start do not add workspace detail page to breadcrumbs when already exists', async () => { - const startMock = coreMock.createStart(); - const workspaceObject = { - id: 'foo', - name: 'bar', - }; - startMock.workspaces.currentWorkspace$.next(workspaceObject); - const breadcrumbs = new BehaviorSubject([ - { text: 'home' }, - { text: 'bar' }, - ]); - startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock, mockDependencies); - expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled(); - }); + it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); - it('#start should register workspace list card into new home page', async () => { - const startMock = coreMock.createStart(); - startMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock, mockDependencies); - expect(mockDependencies.contentManagement.registerContentProvider).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_list_card_home', - }) - ); - }); + expect(setupMock.chrome.navGroup.registerNavGroupUpdater).toHaveBeenCalled(); + const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; - it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); + expect(navGroupUpdater$).toBeTruthy(); + jest.spyOn(navGroupUpdater$, 'next'); - expect(setupMock.chrome.navGroup.registerNavGroupUpdater).toHaveBeenCalled(); - const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; + expect(navGroupUpdater$.next).not.toHaveBeenCalled(); + workspacePlugin.start(coreStart, mockDependencies); - expect(navGroupUpdater$).toBeTruthy(); - jest.spyOn(navGroupUpdater$, 'next'); + waitFor(() => { + expect(navGroupUpdater$.next).toHaveBeenCalled(); + }); + }); - expect(navGroupUpdater$.next).not.toHaveBeenCalled(); - workspacePlugin.start(coreStart, mockDependencies); + it('#start register workspace dropdown menu at left navigation bottom when start', async () => { + const coreStart = coreMock.createStart(); + coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(coreStart, mockDependencies); - waitFor(() => { - expect(navGroupUpdater$.next).toHaveBeenCalled(); + expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1); }); - }); - it('#start register workspace dropdown menu at left navigation bottom when start', async () => { - const coreStart = coreMock.createStart(); - coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(coreStart, mockDependencies); + it('#start should not update systematic use case features after currentWorkspace set', async () => { + const registeredUseCases$ = new BehaviorSubject([ + { + id: 'foo', + title: 'Foo', + features: ['system-feature'], + systematic: true, + description: '', + }, + ]); + jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ + getRegisteredUseCases$: jest.fn(() => registeredUseCases$), + })); + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + const workspaceObject = { + id: 'foo', + name: 'bar', + features: ['baz'], + }; + coreStart.workspaces.currentWorkspace$.next(workspaceObject); - expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1); - }); + const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; - it('#start should not update systematic use case features after currentWorkspace set', async () => { - const registeredUseCases$ = new BehaviorSubject([ - { - id: 'foo', - title: 'Foo', - features: ['system-feature'], - systematic: true, - description: '', - }, - ]); - jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ - getRegisteredUseCases$: jest.fn(() => registeredUseCases$), - })); - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - const workspaceObject = { - id: 'foo', - name: 'bar', - features: ['baz'], - }; - coreStart.workspaces.currentWorkspace$.next(workspaceObject); - - const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; - - workspacePlugin.start(coreStart, mockDependencies); - - const appUpdater = await appUpdater$.pipe(first()).toPromise(); - - expect(appUpdater({ id: 'system-feature', title: '', mount: () => () => {} })).toBeUndefined(); - }); + workspacePlugin.start(coreStart, mockDependencies); - it('#start should update nav group status after currentWorkspace set', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - const workspaceObject = { - id: 'foo', - name: 'bar', - features: ['use-case-foo'], - }; - coreStart.workspaces.currentWorkspace$.next(workspaceObject); + const appUpdater = await appUpdater$.pipe(first()).toPromise(); - const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; + expect( + appUpdater({ id: 'system-feature', title: '', mount: () => () => {} }) + ).toBeUndefined(); + }); + + it('#start should update nav group status after currentWorkspace set', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + const workspaceObject = { + id: 'foo', + name: 'bar', + features: ['use-case-foo'], + }; + coreStart.workspaces.currentWorkspace$.next(workspaceObject); + + const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; - workspacePlugin.start(coreStart, mockDependencies); + workspacePlugin.start(coreStart, mockDependencies); - const navGroupUpdater = await navGroupUpdater$.pipe(first()).toPromise(); + const navGroupUpdater = await navGroupUpdater$.pipe(first()).toPromise(); - expect(navGroupUpdater({ id: 'foo' })).toBeUndefined(); - expect(navGroupUpdater({ id: 'bar' })).toEqual({ - status: NavGroupStatus.Hidden, + expect(navGroupUpdater({ id: 'foo' })).toBeUndefined(); + expect(navGroupUpdater({ id: 'bar' })).toEqual({ + status: NavGroupStatus.Hidden, + }); }); }); - it('#stop should call unregisterNavGroupUpdater', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const unregisterNavGroupUpdater = jest.fn(); - setupMock.chrome.navGroup.registerNavGroupUpdater.mockReturnValueOnce( - unregisterNavGroupUpdater - ); - await workspacePlugin.setup(setupMock, {}); + describe('#stop', () => { + it('#stop should call unregisterNavGroupUpdater', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const unregisterNavGroupUpdater = jest.fn(); + setupMock.chrome.navGroup.registerNavGroupUpdater.mockReturnValueOnce( + unregisterNavGroupUpdater + ); + await workspacePlugin.setup(setupMock, {}); - workspacePlugin.stop(); + workspacePlugin.stop(); - expect(unregisterNavGroupUpdater).toHaveBeenCalled(); - }); + expect(unregisterNavGroupUpdater).toHaveBeenCalled(); + }); - it('#stop should not call appUpdater$.next anymore', async () => { - const registeredUseCases$ = new BehaviorSubject([ - { + it('#stop should not call appUpdater$.next anymore', async () => { + const registeredUseCases$ = new BehaviorSubject([ + { + id: 'foo', + title: 'Foo', + features: ['system-feature'], + systematic: true, + description: '', + }, + ]); + jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ + getRegisteredUseCases$: jest.fn(() => registeredUseCases$), + })); + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + const workspaceObject = { id: 'foo', - title: 'Foo', - features: ['system-feature'], - systematic: true, - description: '', - }, - ]); - jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ - getRegisteredUseCases$: jest.fn(() => registeredUseCases$), - })); - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - const workspaceObject = { - id: 'foo', - name: 'bar', - features: ['baz'], - }; - coreStart.workspaces.currentWorkspace$.next(workspaceObject); - - const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; - const appUpdaterChangeMock = jest.fn(); - appUpdater$.subscribe(appUpdaterChangeMock); - - workspacePlugin.start(coreStart, mockDependencies); - - // Wait for filterNav been executed - await new Promise(setImmediate); - - expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); - - workspacePlugin.stop(); - - registeredUseCases$.next([]); - expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); + name: 'bar', + features: ['baz'], + }; + coreStart.workspaces.currentWorkspace$.next(workspaceObject); + + const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; + const appUpdaterChangeMock = jest.fn(); + appUpdater$.subscribe(appUpdaterChangeMock); + + workspacePlugin.start(coreStart, mockDependencies); + + // Wait for filterNav been executed + await new Promise(setImmediate); + + expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); + + workspacePlugin.stop(); + + registeredUseCases$.next([]); + expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); + }); }); }); From dd992957366b2431942b8b77e3f58cddf2ea72fa Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Thu, 22 Aug 2024 17:58:54 +0800 Subject: [PATCH 11/15] feat: merge conflict Signed-off-by: SuZhou-Joe --- src/plugins/workspace/public/plugin.test.ts | 678 ++++++++++---------- 1 file changed, 347 insertions(+), 331 deletions(-) diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 363dd7106e50..af2598f753d3 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -33,378 +33,394 @@ describe('Workspace plugin', () => { Object.values(workspaceClientMock).forEach((item) => item.mockClear()); }); - describe('#setup', () => { - it('#setup', async () => { - const setupMock = getSetupMock(); - const savedObjectManagementSetupMock = savedObjectsManagementPluginMock.createSetupContract(); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, { - savedObjectsManagement: savedObjectManagementSetupMock, - management: managementPluginMock.createSetupContract(), - }); - expect(setupMock.application.register).toBeCalledTimes(5); - expect(WorkspaceClientMock).toBeCalledTimes(1); - expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1); + it('#setup', async () => { + const setupMock = getSetupMock(); + const savedObjectManagementSetupMock = savedObjectsManagementPluginMock.createSetupContract(); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + savedObjectsManagement: savedObjectManagementSetupMock, + management: managementPluginMock.createSetupContract(), }); + expect(setupMock.application.register).toBeCalledTimes(5); + expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1); + }); - it('#setup call savedObjectsClient.setCurrentWorkspace when current workspace id changed', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - workspacePlugin.start(coreStart, mockDependencies); - coreStart.workspaces.currentWorkspaceId$.next('foo'); - expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); - expect(setupMock.application.register).toBeCalledTimes(5); - expect(WorkspaceClientMock).toBeCalledTimes(1); - expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0); - }); + it('#call savedObjectsClient.setCurrentWorkspace when current workspace id changed', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + workspacePlugin.start(coreStart, mockDependencies); + coreStart.workspaces.currentWorkspaceId$.next('foo'); + expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); + expect(setupMock.application.register).toBeCalledTimes(5); + expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0); + }); - it('#setup when workspace id is in url and enterWorkspace return error', async () => { - const windowSpy = jest.spyOn(window, 'window', 'get'); - windowSpy.mockImplementation( - () => - ({ - location: { - href: 'http://localhost/w/workspaceId/app', - }, - } as any) - ); - workspaceClientMock.enterWorkspace.mockResolvedValue({ - success: false, - error: 'error', - }); - const setupMock = getSetupMock(); - const applicationStartMock = applicationServiceMock.createStartContract(); - const chromeStartMock = chromeServiceMock.createStartContract(); - setupMock.getStartServices.mockImplementation(() => { - return Promise.resolve([ - { - application: applicationStartMock, - chrome: chromeStartMock, + it('#setup when workspace id is in url and enterWorkspace return error', async () => { + const windowSpy = jest.spyOn(window, 'window', 'get'); + windowSpy.mockImplementation( + () => + ({ + location: { + href: 'http://localhost/w/workspaceId/app', }, - {}, - {}, - ]) as any; - }); - - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, { - management: managementPluginMock.createSetupContract(), - }); - expect(setupMock.application.register).toBeCalledTimes(5); - expect(WorkspaceClientMock).toBeCalledTimes(1); - expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId'); - expect(setupMock.getStartServices).toBeCalledTimes(2); - await waitFor( - () => { - expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_FATAL_ERROR_APP_ID, { - replace: true, - state: { - error: 'error', - }, - }); - }, + } as any) + ); + workspaceClientMock.enterWorkspace.mockResolvedValue({ + success: false, + error: 'error', + }); + const setupMock = getSetupMock(); + const applicationStartMock = applicationServiceMock.createStartContract(); + const chromeStartMock = chromeServiceMock.createStartContract(); + setupMock.getStartServices.mockImplementation(() => { + return Promise.resolve([ { - container: document.body, - } - ); - windowSpy.mockRestore(); + application: applicationStartMock, + chrome: chromeStartMock, + }, + {}, + {}, + ]) as any; }); - 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 = getSetupMock(); - const applicationStartMock = applicationServiceMock.createStartContract(); - const chromeStartMock = chromeServiceMock.createStartContract(); - let currentAppIdSubscriber: Subscriber | undefined; - setupMock.getStartServices.mockImplementation(() => { - return Promise.resolve([ - { - application: { - ...applicationStartMock, - currentAppId$: new Observable((subscriber) => { - currentAppIdSubscriber = subscriber; - }), - }, - chrome: chromeStartMock, - }, - {}, - {}, - ]) as any; - }); - - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, { - management: managementPluginMock.createSetupContract(), - }); - currentAppIdSubscriber?.next(WORKSPACE_FATAL_ERROR_APP_ID); - expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_DETAIL_APP_ID); - windowSpy.mockRestore(); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + management: managementPluginMock.createSetupContract(), }); + expect(setupMock.application.register).toBeCalledTimes(5); + expect(WorkspaceClientMock).toBeCalledTimes(1); + expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId'); + expect(setupMock.getStartServices).toBeCalledTimes(2); + await waitFor( + () => { + expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_FATAL_ERROR_APP_ID, { + replace: true, + state: { + error: 'error', + }, + }); + }, + { + container: document.body, + } + ); + windowSpy.mockRestore(); + }); - it('#setup should register workspace list with a visible application and register to settingsAndSetup nav group', async () => { - const setupMock = coreMock.createSetup(); - setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - - expect(setupMock.application.register).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_list', - navLinkStatus: AppNavLinkStatus.visible, - }) - ); - expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( - DEFAULT_NAV_GROUPS.settingsAndSetup, - expect.arrayContaining([ - { - id: 'workspace_list', - order: 150, - title: 'Workspace settings', + 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', }); - - it('#setup should register workspace detail with a visible application and register to all nav group', async () => { - const setupMock = coreMock.createSetup(); - setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - - expect(setupMock.application.register).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_detail', - navLinkStatus: AppNavLinkStatus.hidden, - }) - ); - - expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( - DEFAULT_NAV_GROUPS.all, - expect.arrayContaining([ - { - id: 'workspace_detail', - title: 'Overview', - order: 100, + const setupMock = getSetupMock(); + const applicationStartMock = applicationServiceMock.createStartContract(); + const chromeStartMock = chromeServiceMock.createStartContract(); + let currentAppIdSubscriber: Subscriber | undefined; + setupMock.getStartServices.mockImplementation(() => { + return Promise.resolve([ + { + application: { + ...applicationStartMock, + currentAppId$: new Observable((subscriber) => { + currentAppIdSubscriber = subscriber; + }), }, - ]) - ); + chrome: chromeStartMock, + }, + {}, + {}, + ]) as any; }); - it('#setup should register workspace initial with a visible application', async () => { - const setupMock = coreMock.createSetup(); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - - expect(setupMock.application.register).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'workspace_initial', - navLinkStatus: AppNavLinkStatus.hidden, - }) - ); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + management: managementPluginMock.createSetupContract(), }); + currentAppIdSubscriber?.next(WORKSPACE_FATAL_ERROR_APP_ID); + expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_DETAIL_APP_ID); + windowSpy.mockRestore(); + }); - it('#setup should register registerCollapsibleNavHeader when new left nav is turned on', async () => { - const setupMock = coreMock.createSetup(); - let collapsibleNavHeaderImplementation = () => null; - setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - setupMock.chrome.registerCollapsibleNavHeader.mockImplementation( - (func) => (collapsibleNavHeaderImplementation = func) - ); - const workspacePlugin = new WorkspacePlugin(); - await workspacePlugin.setup(setupMock, {}); - expect(collapsibleNavHeaderImplementation()).toEqual(null); - const startMock = coreMock.createStart(); - await workspacePlugin.start(startMock, mockDependencies); - expect(collapsibleNavHeaderImplementation()).not.toEqual(null); - }); + it('#setup should register workspace list with a visible application and register to settingsAndSetup nav group', async () => { + const setupMock = coreMock.createSetup(); + setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_list', + navLinkStatus: AppNavLinkStatus.visible, + }) + ); + expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( + DEFAULT_NAV_GROUPS.settingsAndSetup, + expect.arrayContaining([ + { + id: 'workspace_list', + order: 150, + title: 'Workspace settings', + }, + ]) + ); }); - describe('#start', () => { - it('#start add workspace detail page to breadcrumbs when start', async () => { - const startMock = coreMock.createStart(); - const workspaceObject = { - id: 'foo', - name: 'bar', - }; - startMock.workspaces.currentWorkspace$.next(workspaceObject); - const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]); - startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock, mockDependencies); - expect(startMock.chrome.setBreadcrumbs).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - text: 'bar', - }), - expect.objectContaining({ - text: 'Home', - }), - ]) - ); - }); + it('#setup should register workspace detail with a visible application and register to all nav group', async () => { + const setupMock = coreMock.createSetup(); + setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_detail', + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); + + expect(setupMock.chrome.navGroup.addNavLinksToGroup).toHaveBeenCalledWith( + DEFAULT_NAV_GROUPS.all, + expect.arrayContaining([ + { + id: 'workspace_detail', + title: 'Overview', + order: 100, + }, + ]) + ); + }); - it('#start do not add workspace detail page to breadcrumbs when already exists', async () => { - const startMock = coreMock.createStart(); - const workspaceObject = { - id: 'foo', - name: 'bar', - }; - startMock.workspaces.currentWorkspace$.next(workspaceObject); - const breadcrumbs = new BehaviorSubject([ - { text: 'home' }, - { text: 'bar' }, - ]); - startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock, mockDependencies); - expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled(); - }); + it('#setup should register workspace initial with a visible application', async () => { + const setupMock = coreMock.createSetup(); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_initial', + navLinkStatus: AppNavLinkStatus.hidden, + }) + ); + }); + + it('#setup should register registerCollapsibleNavHeader when new left nav is turned on', async () => { + const setupMock = coreMock.createSetup(); + let collapsibleNavHeaderImplementation = () => null; + setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + setupMock.chrome.registerCollapsibleNavHeader.mockImplementation( + (func) => (collapsibleNavHeaderImplementation = func) + ); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, {}); + expect(collapsibleNavHeaderImplementation()).toEqual(null); + const startMock = coreMock.createStart(); + await workspacePlugin.start(startMock, mockDependencies); + expect(collapsibleNavHeaderImplementation()).not.toEqual(null); + }); - it('#start should register workspace list card into new home page', async () => { - const startMock = coreMock.createStart(); - startMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock, mockDependencies); - expect(mockDependencies.contentManagement.registerContentProvider).toHaveBeenCalledWith( + it('#start add workspace detail page to breadcrumbs when start', async () => { + const startMock = coreMock.createStart(); + const workspaceObject = { + id: 'foo', + name: 'bar', + }; + startMock.workspaces.currentWorkspace$.next(workspaceObject); + const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]); + startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(startMock.chrome.setBreadcrumbs).toBeCalledWith( + expect.arrayContaining([ expect.objectContaining({ - id: 'workspace_list_card_home', - }) - ); - }); + text: 'bar', + }), + expect.objectContaining({ + text: 'Home', + }), + ]) + ); + }); - it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); + it('#start do not add workspace detail page to breadcrumbs when already exists', async () => { + const startMock = coreMock.createStart(); + const workspaceObject = { + id: 'foo', + name: 'bar', + }; + startMock.workspaces.currentWorkspace$.next(workspaceObject); + const breadcrumbs = new BehaviorSubject([ + { text: 'home' }, + { text: 'bar' }, + ]); + startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled(); + }); - expect(setupMock.chrome.navGroup.registerNavGroupUpdater).toHaveBeenCalled(); - const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; + it('#start should register workspace list card into new home page', async () => { + const startMock = coreMock.createStart(); + startMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(mockDependencies.contentManagement.registerContentProvider).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_list_card_home', + }) + ); + }); - expect(navGroupUpdater$).toBeTruthy(); - jest.spyOn(navGroupUpdater$, 'next'); + it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); - expect(navGroupUpdater$.next).not.toHaveBeenCalled(); - workspacePlugin.start(coreStart, mockDependencies); + expect(setupMock.chrome.navGroup.registerNavGroupUpdater).toHaveBeenCalled(); + const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; - waitFor(() => { - expect(navGroupUpdater$.next).toHaveBeenCalled(); - }); - }); + expect(navGroupUpdater$).toBeTruthy(); + jest.spyOn(navGroupUpdater$, 'next'); - it('#start register workspace dropdown menu at left navigation bottom when start', async () => { - const coreStart = coreMock.createStart(); - coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); - const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(coreStart, mockDependencies); + expect(navGroupUpdater$.next).not.toHaveBeenCalled(); + workspacePlugin.start(coreStart, mockDependencies); - expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1); + waitFor(() => { + expect(navGroupUpdater$.next).toHaveBeenCalled(); }); + }); - it('#start should not update systematic use case features after currentWorkspace set', async () => { - const registeredUseCases$ = new BehaviorSubject([ - { - id: 'foo', - title: 'Foo', - features: ['system-feature'], - systematic: true, - description: '', - }, - ]); - jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ - getRegisteredUseCases$: jest.fn(() => registeredUseCases$), - })); - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - const workspaceObject = { - id: 'foo', - name: 'bar', - features: ['baz'], - }; - coreStart.workspaces.currentWorkspace$.next(workspaceObject); - - const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; - - workspacePlugin.start(coreStart, mockDependencies); + it('#start register workspace dropdown menu at left navigation bottom when start', async () => { + const coreStart = coreMock.createStart(); + coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(coreStart, mockDependencies); - const appUpdater = await appUpdater$.pipe(first()).toPromise(); + expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1); + }); - expect( - appUpdater({ id: 'system-feature', title: '', mount: () => () => {} }) - ).toBeUndefined(); - }); + it('#start should not update systematic use case features after currentWorkspace set', async () => { + const registeredUseCases$ = new BehaviorSubject([ + { + id: 'foo', + title: 'Foo', + features: [{ id: 'system-feature', title: 'System feature' }], + systematic: true, + description: '', + }, + ]); + jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ + getRegisteredUseCases$: jest.fn(() => registeredUseCases$), + })); + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + const workspaceObject = { + id: 'foo', + name: 'bar', + features: ['baz'], + }; + coreStart.workspaces.currentWorkspace$.next(workspaceObject); + + const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; + + workspacePlugin.start(coreStart, mockDependencies); + + const appUpdater = await appUpdater$.pipe(first()).toPromise(); + + expect(appUpdater({ id: 'system-feature', title: '', mount: () => () => {} })).toBeUndefined(); }); - describe('#stop', () => { - it('#stop should call unregisterNavGroupUpdater', async () => { - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const unregisterNavGroupUpdater = jest.fn(); - setupMock.chrome.navGroup.registerNavGroupUpdater.mockReturnValueOnce( - unregisterNavGroupUpdater - ); - await workspacePlugin.setup(setupMock, {}); + it('#start should update nav group status after currentWorkspace set', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + const workspaceObject = { + id: 'foo', + name: 'bar', + features: ['use-case-foo'], + }; + coreStart.workspaces.currentWorkspace$.next(workspaceObject); - workspacePlugin.stop(); + const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; - expect(unregisterNavGroupUpdater).toHaveBeenCalled(); - }); + workspacePlugin.start(coreStart, mockDependencies); - it('#stop should not call appUpdater$.next anymore', async () => { - const registeredUseCases$ = new BehaviorSubject([ - { - id: 'foo', - title: 'Foo', - features: ['system-feature'], - systematic: true, - description: '', - }, - ]); - jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ - getRegisteredUseCases$: jest.fn(() => registeredUseCases$), - })); - const workspacePlugin = new WorkspacePlugin(); - const setupMock = getSetupMock(); - const coreStart = coreMock.createStart(); - await workspacePlugin.setup(setupMock, {}); - const workspaceObject = { - id: 'foo', - name: 'bar', - features: ['baz'], - }; - coreStart.workspaces.currentWorkspace$.next(workspaceObject); - - const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; - const appUpdaterChangeMock = jest.fn(); - appUpdater$.subscribe(appUpdaterChangeMock); + const navGroupUpdater = await navGroupUpdater$.pipe(first()).toPromise(); - workspacePlugin.start(coreStart, mockDependencies); + expect(navGroupUpdater({ id: 'foo' })).toBeUndefined(); + expect(navGroupUpdater({ id: 'bar' })).toEqual({ + status: NavGroupStatus.Hidden, + }); + }); - // Wait for filterNav been executed - await new Promise(setImmediate); + it('#stop should call unregisterNavGroupUpdater', async () => { + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const unregisterNavGroupUpdater = jest.fn(); + setupMock.chrome.navGroup.registerNavGroupUpdater.mockReturnValueOnce( + unregisterNavGroupUpdater + ); + await workspacePlugin.setup(setupMock, {}); - expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); + workspacePlugin.stop(); - workspacePlugin.stop(); + expect(unregisterNavGroupUpdater).toHaveBeenCalled(); + }); - registeredUseCases$.next([]); - expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); - }); + it('#stop should not call appUpdater$.next anymore', async () => { + const registeredUseCases$ = new BehaviorSubject([ + { + id: 'foo', + title: 'Foo', + features: ['system-feature'], + systematic: true, + description: '', + }, + ]); + jest.spyOn(UseCaseService.prototype, 'start').mockImplementationOnce(() => ({ + getRegisteredUseCases$: jest.fn(() => registeredUseCases$), + })); + const workspacePlugin = new WorkspacePlugin(); + const setupMock = getSetupMock(); + const coreStart = coreMock.createStart(); + await workspacePlugin.setup(setupMock, {}); + const workspaceObject = { + id: 'foo', + name: 'bar', + features: ['baz'], + }; + coreStart.workspaces.currentWorkspace$.next(workspaceObject); + + const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; + const appUpdaterChangeMock = jest.fn(); + appUpdater$.subscribe(appUpdaterChangeMock); + + workspacePlugin.start(coreStart, mockDependencies); + + // Wait for filterNav been executed + await new Promise(setImmediate); + + expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); + + workspacePlugin.stop(); + + registeredUseCases$.next([]); + expect(appUpdaterChangeMock).toHaveBeenCalledTimes(2); }); }); From e6ed00ce03c003a71e21e21c5d413a76a9664ef5 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 23 Aug 2024 08:36:45 +0800 Subject: [PATCH 12/15] feat: optimize code based on comment Signed-off-by: SuZhou-Joe --- .../chrome/nav_group/nav_group_service.ts | 29 +++-- .../header/collapsible_nav_group_enabled.tsx | 111 ++++++++++-------- .../workspace_picker_content.tsx | 19 +-- 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts index d15aa6f42c0a..729239081b42 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -261,26 +261,29 @@ export class ChromeNavGroupService { ]).subscribe(([appId, navGroupMap]) => { if (appId && navGroupMap) { const appIdNavGroupMap = new Map>(); - let visibleNavGroups: NavGroupItemInMap[] = []; const visibleUseCases = getVisibleUseCases(navGroupMap); - if (visibleUseCases.length === 1 && visibleUseCases[0].id === ALL_USE_CASE_ID) { - // If the only visible use case is all use case - // All the other nav groups will be visible because all use case can visit all of the nav groups. - visibleNavGroups = Object.values(navGroupMap); - } else { - // Nav group of Hidden status should be filtered out when counting navGroups the currentApp belongs to - visibleNavGroups = Object.values(navGroupMap).filter( - (navGroup) => navGroup.status !== NavGroupStatus.Hidden - ); - } - visibleNavGroups.forEach((navGroup) => { + const mapAppIdToNavGroup = (navGroup: NavGroupItemInMap) => { navGroup.navLinks.forEach((navLink) => { const navLinkId = navLink.id; const navGroupSet = appIdNavGroupMap.get(navLinkId) || new Set(); navGroupSet.add(navGroup.id); appIdNavGroupMap.set(navLinkId, navGroupSet); }); - }); + }; + if (visibleUseCases.length === 1 && visibleUseCases[0].id === ALL_USE_CASE_ID) { + // If the only visible use case is all use case + // All the other nav groups will be visible because all use case can visit all of the nav groups. + Object.values(navGroupMap).forEach((navGroup) => mapAppIdToNavGroup(navGroup)); + } else { + // Nav group of Hidden status should be filtered out when counting navGroups the currentApp belongs to + Object.values(navGroupMap).forEach((navGroup) => { + if (navGroup.status === NavGroupStatus.Hidden) { + return; + } + + mapAppIdToNavGroup(navGroup); + }); + } const navGroups = appIdNavGroupMap.get(appId); if (navGroups && navGroups.size === 1) { diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx index f48a555a24b8..2956bc425097 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -130,8 +130,10 @@ export function CollapsibleNavGroupEnabled({ const result: ChromeNavLink[] = []; Object.values(navGroupsMap) .sort(sortBy('order')) - .filter((navGroup) => navGroup.type === NavGroupType.SYSTEM) .forEach((navGroup) => { + if (navGroup.type !== NavGroupType.SYSTEM) { + return; + } const visibleNavLinksWithinNavGroup = fulfillRegistrationLinksToChromeNavLinks( navGroup.navLinks, navLinks @@ -159,14 +161,15 @@ export function CollapsibleNavGroupEnabled({ const linkIdsWithUseGroupInfo = Object.values(navGroupsMap).reduce((total, navGroup) => { return [...total, ...navGroup.navLinks.map((navLink) => navLink.id)]; }, [] as string[]); - navLinks - .filter((link) => !linkIdsWithUseGroupInfo.includes(link.id)) - .forEach((navLink) => { - navLinksResult.push({ - ...navLink, - category: customCategory, - }); + navLinks.forEach((navLink) => { + if (linkIdsWithUseGroupInfo.includes(navLink.id)) { + return; + } + navLinksResult.push({ + ...navLink, + category: customCategory, }); + }); // Append all the links registered to all use case navGroupsMap[ALL_USE_CASE_ID]?.navLinks.forEach((navLink) => { @@ -174,54 +177,60 @@ export function CollapsibleNavGroupEnabled({ }); // Append use case section into left navigation - Object.values(navGroupsMap) - .filter((group) => !group.type) - .forEach((group) => { - const categoryInfo = { - id: group.id, - label: group.title, - order: group.order, - }; - - const fulfilledLinksOfNavGroup = fulfillRegistrationLinksToChromeNavLinks( - group.navLinks, - navLinks - ); + Object.values(navGroupsMap).forEach((group) => { + if (group.type) { + return; + } + const categoryInfo = { + id: group.id, + label: group.title, + order: group.order, + }; - const linksForAllUseCaseWithinNavGroup = fulfilledLinksOfNavGroup - .filter((navLink) => navLink.showInAllNavGroup) - .map((navLink) => ({ - ...navLink, - category: categoryInfo, - })); + const fulfilledLinksOfNavGroup = fulfillRegistrationLinksToChromeNavLinks( + group.navLinks, + navLinks + ); - navLinksResult.push(...linksForAllUseCaseWithinNavGroup); + const linksForAllUseCaseWithinNavGroup: ChromeRegistrationNavLink[] = []; - if (linksForAllUseCaseWithinNavGroup.length) { - navLinksResult.push({ - id: fulfilledLinksOfNavGroup[0].id, - title: titleForSeeAll, - order: Number.MAX_SAFE_INTEGER, - category: categoryInfo, - }); - } else { - /** - * Find if there are any links inside a use case but without a `see all` entry. - * If so, append these features into custom category as a fallback - */ - fulfillRegistrationLinksToChromeNavLinks(group.navLinks, navLinks) - // Filter out links that already exists in all use case - .filter( - (navLink) => !navLinksResult.find((navLinkInAll) => navLinkInAll.id === navLink.id) - ) - .forEach((navLink) => { - navLinksResult.push({ - ...navLink, - category: customCategory, - }); - }); + fulfilledLinksOfNavGroup.forEach((navLink) => { + if (!navLink.showInAllNavGroup) { + return; } + + linksForAllUseCaseWithinNavGroup.push({ + ...navLink, + category: categoryInfo, + }); }); + + navLinksResult.push(...linksForAllUseCaseWithinNavGroup); + + if (linksForAllUseCaseWithinNavGroup.length) { + navLinksResult.push({ + id: fulfilledLinksOfNavGroup[0].id, + title: titleForSeeAll, + order: Number.MAX_SAFE_INTEGER, + category: categoryInfo, + }); + } else { + /** + * Find if there are any links inside a use case but without a `see all` entry. + * If so, append these features into custom category as a fallback + */ + fulfillRegistrationLinksToChromeNavLinks(group.navLinks, navLinks).forEach((navLink) => { + // Links that already exists in all use case do not need to reappend + if (navLinksResult.find((navLinkInAll) => navLinkInAll.id === navLink.id)) { + return; + } + navLinksResult.push({ + ...navLink, + category: customCategory, + }); + }); + } + }); } if (shouldAppendManageCategory) { diff --git a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx index 92debb2db291..4dace89ea119 100644 --- a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx +++ b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx @@ -67,7 +67,6 @@ export const WorkspacePickerContent = ({ return ( - -

{itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}

-
- - {listItems} - - + + +

{itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}

+ + } + /> + {listItems} +
); }; From 0e5a88385244f9b1626e8663175c3c1104a004e6 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 27 Aug 2024 11:12:43 +0800 Subject: [PATCH 13/15] feat: optimize code based on comment Signed-off-by: SuZhou-Joe --- .../ui/header/collapsible_nav_group_enabled.tsx | 15 +++++++++------ .../chrome/ui/header/collapsible_nav_groups.tsx | 7 ++++--- .../index_pattern_management/public/plugin.ts | 6 ++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx index 1542dce3ebf5..d8867d973d7d 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -120,10 +120,8 @@ export function CollapsibleNavGroupEnabled({ ? !currentNavGroupId : currentNavGroupId === ALL_USE_CASE_ID; - const shouldShowCollapsedNavHeaderContent = useMemo( - () => isNavOpen && collapsibleNavHeaderRender && !currentNavGroupId, - [collapsibleNavHeaderRender, isNavOpen, currentNavGroupId] - ); + const shouldShowCollapsedNavHeaderContent = + isNavOpen && !!collapsibleNavHeaderRender && !currentNavGroupId; const navLinksForRender: ChromeNavLink[] = useMemo(() => { const getSystemNavGroups = () => { @@ -138,6 +136,11 @@ export function CollapsibleNavGroupEnabled({ navGroup.navLinks, navLinks ); + /** + * We will take the first visible app inside the system nav groups + * when customers click the menu. If there is not a visible nav links, + * we should not show the nav group. + */ if (visibleNavLinksWithinNavGroup[0]) { result.push({ ...visibleNavLinksWithinNavGroup[0], @@ -158,11 +161,11 @@ export function CollapsibleNavGroupEnabled({ if (currentNavGroupId === ALL_USE_CASE_ID) { // Append all the links that do not have use case info to keep backward compatible - const linkIdsWithUseGroupInfo = Object.values(navGroupsMap).reduce((total, navGroup) => { + const linkIdsWithNavGroupInfo = Object.values(navGroupsMap).reduce((total, navGroup) => { return [...total, ...navGroup.navLinks.map((navLink) => navLink.id)]; }, [] as string[]); navLinks.forEach((navLink) => { - if (linkIdsWithUseGroupInfo.includes(navLink.id)) { + if (linkIdsWithNavGroupInfo.includes(navLink.id)) { return; } navLinksResult.push({ diff --git a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx index a78267989efa..6466ac82cbec 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx @@ -137,9 +137,10 @@ export function NavGroups({ return {} as EuiSideNavItemType<{}>; }; const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); - const sideNavItems = orderedLinksOrCategories - .map((navLink) => createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS)) - .filter((item): item is EuiSideNavItemType<{}> => !!item); + const sideNavItems = orderedLinksOrCategories.map((navLink) => + createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS) + ); + return ( diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index 5971bcc0fe3e..b64e81a92151 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -168,6 +168,12 @@ export class IndexPatternManagementPlugin }); core.getStartServices().then(([coreStart]) => { + /** + * The `capabilities.workspaces.enabled` indicates + * if workspace feature flag is turned on or not and + * the global index pattern management page should only be registered + * to settings and setup when workspace is turned off, + */ if (!coreStart.application.capabilities.workspaces.enabled) { core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.settingsAndSetup, [ { From 2a2672a5e09a55b7befcfff3b4da2ffaf72c8fb4 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 27 Aug 2024 11:17:35 +0800 Subject: [PATCH 14/15] feat: optimize filter code Signed-off-by: SuZhou-Joe --- src/core/public/chrome/ui/header/collapsible_nav_groups.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx index 6466ac82cbec..3d202034dbbb 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx @@ -137,9 +137,9 @@ export function NavGroups({ return {} as EuiSideNavItemType<{}>; }; const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); - const sideNavItems = orderedLinksOrCategories.map((navLink) => - createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS) - ); + const sideNavItems = orderedLinksOrCategories + .map((navLink) => createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS)) + .filter((navItem) => !!navItem.name); return ( From 3096bea77bd06bcc16cf950b0f19082d823ff8dd Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 27 Aug 2024 22:17:13 +0800 Subject: [PATCH 15/15] feat: update Signed-off-by: SuZhou-Joe --- src/core/public/chrome/ui/header/collapsible_nav_groups.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx index 3d202034dbbb..53a75aeaaddd 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx @@ -139,7 +139,7 @@ export function NavGroups({ const orderedLinksOrCategories = getOrderedLinksOrCategories(navLinks); const sideNavItems = orderedLinksOrCategories .map((navLink) => createSideNavItem(navLink, LEVEL_FOR_ROOT_ITEMS)) - .filter((navItem) => !!navItem.name); + .filter((navItem) => !!navItem.id); return (