diff --git a/changelogs/fragments/8332.yml b/changelogs/fragments/8332.yml new file mode 100644 index 000000000000..57dce1102a30 --- /dev/null +++ b/changelogs/fragments/8332.yml @@ -0,0 +1,3 @@ +feat: +- [navigation] flatten left nav in Analytics(all) use case ([#8332](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8332)) +- [navigation] Adjust the appearances of the left navigation menu and the landing page ([#8332](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8332)) \ No newline at end of file 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 d3918bdeec3d..a8c8e22a96a3 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 @@ -231,6 +231,66 @@ describe('ChromeNavGroupService#start()', () => { expect(groupsMap[mockedGroupBar.id].navLinks.length).toEqual(1); }); + it('should populate links with custom category if the nav link is inside second level but no entry in all use case', async () => { + const chromeNavGroupService = new ChromeNavGroupService(); + const uiSettings = uiSettingsServiceMock.createSetupContract(); + const chromeNavGroupServiceSetup = chromeNavGroupService.setup({ uiSettings }); + + chromeNavGroupServiceSetup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [ + { + id: 'foo', + }, + ]); + chromeNavGroupServiceSetup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.essentials, [ + { + id: 'bar', + title: 'bar', + }, + { + id: 'foo', + title: 'foo', + }, + ]); + const navLinkServiceStart = mockedNavLink.start({ + http: mockedHttpService, + application: mockedApplicationService, + }); + navLinkServiceStart.getNavLinks$ = jest.fn().mockReturnValue( + new Rx.BehaviorSubject([ + { + id: 'foo', + }, + { + id: 'bar', + }, + { + id: 'customized_app', + }, + ]) + ); + const chromeStart = await chromeNavGroupService.start({ + navLinks: navLinkServiceStart, + application: mockedApplicationService, + breadcrumbsEnricher$: new Rx.BehaviorSubject(undefined), + workspaces: workspacesServiceMock.createStartContract(), + }); + const groupsMap = await chromeStart.getNavGroupsMap$().pipe(first()).toPromise(); + expect(groupsMap[ALL_USE_CASE_ID].navLinks).toEqual([ + { + id: 'foo', + }, + { + id: 'bar', + title: 'bar', + category: { id: 'custom', label: 'Custom', order: 8500 }, + }, + { + id: 'customized_app', + category: { id: 'custom', label: 'Custom', order: 8500 }, + }, + ]); + }); + it('should return navGroupEnabled from ui settings', async () => { const chromeNavGroupService = new ChromeNavGroupService(); const uiSettings = uiSettingsServiceMock.createSetupContract(); @@ -381,49 +441,6 @@ describe('ChromeNavGroupService#start()', () => { expect(currentNavGroup?.title).toEqual('barGroupTitle'); }); - 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 be able to find the right nav group when visible nav group length is 1 and is not all nav group', async () => { const uiSettings = uiSettingsServiceMock.createSetupContract(); const navGroupEnabled$ = new Rx.BehaviorSubject(true); 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 e3911d219ee0..5ff758a056ea 100644 --- a/src/core/public/chrome/nav_group/nav_group_service.ts +++ b/src/core/public/chrome/nav_group/nav_group_service.ts @@ -11,6 +11,7 @@ import { ChromeNavLink, WorkspacesStart, } from 'opensearch-dashboards/public'; +import { i18n } from '@osd/i18n'; import { map, switchMap, takeUntil } from 'rxjs/operators'; import { IUiSettingsClient } from '../../ui_settings'; import { @@ -22,7 +23,7 @@ import { ChromeNavLinks } from '../nav_links'; import { InternalApplicationStart } from '../../application'; import { NavGroupStatus, NavGroupType } from '../../../../core/types'; import { ChromeBreadcrumb, ChromeBreadcrumbEnricher } from '../chrome_service'; -import { ALL_USE_CASE_ID } from '../../../utils'; +import { ALL_USE_CASE_ID, DEFAULT_APP_CATEGORIES } from '../../../utils'; export const CURRENT_NAV_GROUP_ID = 'core.chrome.currentNavGroupId'; @@ -74,6 +75,14 @@ export interface ChromeNavGroupServiceStartContract { setCurrentNavGroup: (navGroupId: string | undefined) => void; } +// 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 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, +}; + /** @internal */ export class ChromeNavGroupService { private readonly navGroupsMap$ = new BehaviorSubject>({}); @@ -114,12 +123,87 @@ export class ChromeNavGroupService { } private sortNavGroupNavLinks( - navGroup: NavGroupItemInMap, + navLinks: NavGroupItemInMap['navLinks'], allValidNavLinks: Array> ) { - return getSortedNavLinks( - fulfillRegistrationLinksToChromeNavLinks(navGroup.navLinks, allValidNavLinks) - ); + return getSortedNavLinks(fulfillRegistrationLinksToChromeNavLinks(navLinks, allValidNavLinks)); + } + + private getNavLinksForAllUseCase( + navGroupsMap: Record, + navLinks: Array> + ) { + // Note: we need to use a new pointer when `assign navGroupsMap[ALL_USE_CASE_ID]?.navLinks` + // because we will mutate the array directly in the following code. + const navLinksResult: ChromeRegistrationNavLink[] = [ + ...(navGroupsMap[ALL_USE_CASE_ID]?.navLinks || []), + ]; + + // Append all the links that do not have use case info to keep backward compatible + const linkIdsWithNavGroupInfo = Object.values(navGroupsMap).reduce((accumulator, navGroup) => { + // Nav groups without type will be regarded as use case, + // we should transform use cases to a category and append links with `showInAllNavGroup: true` under the category + if (!navGroup.type) { + // Append use case section into left navigation + const categoryInfo = { + id: navGroup.id, + label: navGroup.title, + order: navGroup.order, + }; + + const fulfilledLinksOfNavGroup = fulfillRegistrationLinksToChromeNavLinks( + navGroup.navLinks, + navLinks + ); + + const linksForAllUseCaseWithinNavGroup: ChromeRegistrationNavLink[] = []; + + fulfilledLinksOfNavGroup.forEach((navLink) => { + if (!navLink.showInAllNavGroup) { + return; + } + + linksForAllUseCaseWithinNavGroup.push({ + ...navLink, + category: categoryInfo, + }); + }); + + navLinksResult.push(...linksForAllUseCaseWithinNavGroup); + + if (!linksForAllUseCaseWithinNavGroup.length) { + /** + * 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(navGroup.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, + }); + } + ); + } + } + + return [...accumulator, ...navGroup.navLinks.map((navLink) => navLink.id)]; + }, [] as string[]); + navLinks.forEach((navLink) => { + if (linkIdsWithNavGroupInfo.includes(navLink.id)) { + return; + } + navLinksResult.push({ + ...navLink, + category: customCategory, + }); + }); + + return navLinksResult; } private getSortedNavGroupsMap$() { @@ -129,10 +213,20 @@ export class ChromeNavGroupService { map(([navGroupsMap, navLinks]) => { return Object.keys(navGroupsMap).reduce((sortedNavGroupsMap, navGroupId) => { const navGroup = navGroupsMap[navGroupId]; - sortedNavGroupsMap[navGroupId] = { - ...navGroup, - navLinks: this.sortNavGroupNavLinks(navGroup, navLinks), - }; + if (navGroupId === ALL_USE_CASE_ID) { + sortedNavGroupsMap[navGroupId] = { + ...navGroup, + navLinks: this.sortNavGroupNavLinks( + this.getNavLinksForAllUseCase(navGroupsMap, navLinks), + navLinks + ), + }; + } else { + sortedNavGroupsMap[navGroupId] = { + ...navGroup, + navLinks: this.sortNavGroupNavLinks(navGroup.navLinks, navLinks), + }; + } return sortedNavGroupsMap; }, {} as Record); }) @@ -270,14 +364,10 @@ export class ChromeNavGroupService { }); }; if (visibleUseCases.length === 1) { - if (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 { - // It means we are in a workspace, we should only use the visible use cases - visibleUseCases.forEach((navGroup) => mapAppIdToNavGroup(navGroup)); - } + // The length will be 1 if inside a workspace + // as workspace plugin will register a filter to only make the selected nav group visible. + // In order to tell which nav group we are in, we should use the only visible use case if the visibleUseCases.length equals 1. + visibleUseCases.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) => { 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 b3a010659c21..74dfa496c9cb 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 @@ -11,7 +11,7 @@ exports[` should render correctly 1`] = ` class="eui-fullHeight left-navigation-wrapper" >
should render correctly 1`] = ` class="euiSideNav euiSideNav-isOpenMobile" >
should render correctly 1`] = ` title="link-in-all" >
link-in-all
@@ -58,176 +59,6 @@ exports[` should render correctly 1`] = `
-
-
- - - - - -
-
- - -
-
-
-
- - - - - -
-
- - -
-
@@ -267,80 +98,7 @@ exports[` should render correctly 2`] = `
`; -exports[` should show all use case by default and able to click see all 1`] = ` -
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-`; - -exports[` should show all use case when current nav group is \`all\` 1`] = ` +exports[` should show use case nav when current nav group is valid 1`] = `
should show all use case when current na class="eui-fullHeight left-navigation-wrapper" >
should show all use case when current na class="euiSideNav euiSideNav-isOpenMobile" >
should render correctly 1`] = ` title="category-1" > @@ -118,8 +146,7 @@ exports[` should render correctly 1`] = ` > + @@ -10174,7 +10169,6 @@ exports[`Header renders application header without title and breadcrumbs 1`] = ` color="text" data-test-subj="toggleNavButton" flush="both" - isSmallScreen={true} onClick={[Function]} > - + title="Menu" + /> + + @@ -19493,7 +19470,6 @@ exports[`Header renders page header with application title 1`] = ` className="newPageTopNavExpander navToggleInSmallScreen eui-hideFor--xl eui-hideFor--l eui-hideFor--xxl eui-hideFor--xxxl" data-test-subj="toggleNavButton" flush="both" - isSmallScreen={true} onClick={[Function]} > - + @@ -19543,7 +19515,6 @@ exports[`Header renders page header with application title 1`] = ` color="text" data-test-subj="toggleNavButton" flush="both" - isSmallScreen={true} onClick={[Function]} > - + title="Menu" + /> + + diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 43f1eb33dfd9..f51ed93d854e 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -51,6 +51,7 @@ import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; import type { Logos } from '../../../../common/types'; +import { getIsCategoryOpen, setIsCategoryOpen } from '../../utils'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -72,20 +73,6 @@ function getOrderedCategories( ); } -function getCategoryLocalStorageKey(id: string) { - return `core.navGroup.${id}`; -} - -function getIsCategoryOpen(id: string, storage: Storage) { - const value = storage.getItem(getCategoryLocalStorageKey(id)) ?? 'true'; - - return value === 'true'; -} - -function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { - storage.setItem(getCategoryLocalStorageKey(id), `${isOpen}`); -} - interface Props { appId$: InternalApplicationStart['currentAppId$']; basePath: HttpStart['basePath']; diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss index 27537d9af8eb..529f0f749ebb 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss @@ -1,33 +1,60 @@ +@import "./variables"; + .context-nav-wrapper { border: none !important; + border-top-right-radius: $euiSizeL; + border-bottom-right-radius: $euiSizeL; + background-color: $ouiSideNavBackgroundColorTemp; + overflow: hidden; .nav-link-item { - padding: calc($euiSize / 4) $euiSize; - border-radius: $euiSize; + padding: $euiSizeS; + border-radius: $euiSizeS; box-shadow: none; margin-bottom: 0; margin-top: 0; .nav-link-item-btn { margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; + } + } - &::after { - display: none; - } + .nav-link-parent-item { + padding-top: 0; + padding-bottom: 0; + margin-bottom: $euiSizeS; + + > .nav-link-item-btn { + padding: $euiSizeS; + margin-bottom: $euiSizeXS; + } + + // Hide the expand / collapse button as we will use + / - + svg { + display: none; + } + + // Show the customized icon + .leftNavCustomizedAccordionIcon { + display: inline-block; } } - .nav-link-parent-item-button { - > span { - flex-direction: row-reverse; + .nav-link-item-category-button { + margin-bottom: $euiSizeXS; - > * { - margin-right: $euiSizeS; - margin-left: 2px; - } + // Use a smaller vertical padding so that category title looks more grouped to the items + .nav-link-item { + padding: $euiSizeXS $euiSizeS; } } + .nav-link-item-category-item { + margin-top: $euiSizeL; + } + .nav-link-fake-item { margin-top: 0; } @@ -37,22 +64,22 @@ } .nav-nested-item { - margin-bottom: 4px; + padding: $euiSizeS 0; - &::after { - height: unset; + &::after, + .nav-link-item-btn::after { + background-color: $euiColorDarkShade; } - .nav-link-item-btn { - padding-left: 0; - padding-right: 0; + // The height is used to comply with the extra padding + &:last-of-type::after { + height: 20px; } } .left-navigation-wrapper { display: flex; flex-direction: column; - border-right: $euiBorderThin; } .flex-1-container { @@ -77,13 +104,16 @@ } &.bottom-container-expanded { + @include euiBottomShadowLarge($euiColorMediumShade, 0.1, true, true); + gap: 16px; - padding-top: $euiSize; - padding-bottom: $euiSize; + padding-top: $euiSizeM; + padding-bottom: $euiSizeM; } } .navGroupEnabledNavTopWrapper { - padding: 0 $euiSizeL; + padding: 0 $euiSizeS; + padding-left: $euiSize; } } 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 e332cb2dac59..709b79597ada 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 @@ -45,7 +45,6 @@ const defaultNavGroupMap = { { id: 'link-in-observability', title: 'link-in-observability', - showInAllNavGroup: true, }, ], }, @@ -126,7 +125,6 @@ describe('', () => { { id: 'link-in-essentials', title: 'link-in-essentials', - showInAllNavGroup: true, }, ], }, @@ -151,52 +149,16 @@ describe('', () => { expect(getAllByTestId('collapsibleNavAppLink-link-in-observability').length).toEqual(1); }); - it('should show all use case by default and able to click see all', async () => { - const props = mockProps({ - navGroupsMap: { - ...defaultNavGroupMap, - [DEFAULT_NAV_GROUPS.essentials.id]: { - ...DEFAULT_NAV_GROUPS.essentials, - navLinks: [ - { - id: 'link-in-essentials', - title: 'link-in-essentials', - showInAllNavGroup: true, - }, - ], - }, - }, - }); - const { container, getAllByTestId } = render( - - ); - fireEvent.click(getAllByTestId('collapsibleNavAppLink-link-in-essentials')[1]); - expect(getAllByTestId('collapsibleNavAppLink-link-in-essentials').length).toEqual(1); - expect(container).toMatchSnapshot(); - }); - - it('should show all use case when current nav group is `all`', async () => { + it('should show use case nav when current nav group is valid', async () => { const props = mockProps({ currentNavGroupId: ALL_USE_CASE_ID, - navGroupsMap: { - ...defaultNavGroupMap, - [DEFAULT_NAV_GROUPS.essentials.id]: { - ...DEFAULT_NAV_GROUPS.essentials, - navLinks: [ - { - id: 'link-in-essentials', - title: 'link-in-essentials', - showInAllNavGroup: true, - }, - ], - }, - }, + navGroupsMap: defaultNavGroupMap, }); const { container, getAllByTestId } = render( ); - fireEvent.click(getAllByTestId('collapsibleNavAppLink-link-in-essentials')[1]); - expect(getAllByTestId('collapsibleNavAppLink-link-in-essentials').length).toEqual(1); + fireEvent.click(getAllByTestId('collapsibleNavAppLink-link-in-all')[0]); + expect(getAllByTestId('collapsibleNavAppLink-link-in-all').length).toEqual(1); expect(container).toMatchSnapshot(); }); @@ -211,7 +173,6 @@ describe('', () => { { id: 'link-in-essentials-but-hidden', title: 'link-in-essentials-but-hidden', - showInAllNavGroup: true, }, ], }, @@ -233,49 +194,6 @@ describe('', () => { expect(queryAllByTestId('collapsibleNavAppLink-link-in-all').length).toEqual(1); }); - it('should show links with custom category if the nav link is inside second level but no entry in all use case', async () => { - const props = mockProps({ - currentNavGroupId: ALL_USE_CASE_ID, - navGroupsMap: { - ...defaultNavGroupMap, - [DEFAULT_NAV_GROUPS.essentials.id]: { - ...DEFAULT_NAV_GROUPS.essentials, - navLinks: [ - { - id: 'link-in-essentials', - title: 'link-in-essentials', - }, - { - id: 'link-in-all', - title: 'link-in-all', - }, - ], - }, - }, - navLinks: [ - { - id: 'link-in-essentials', - title: 'link-in-essentials', - baseUrl: '', - href: '', - }, - { - id: 'link-in-all', - title: 'link-in-all', - baseUrl: '', - href: '', - }, - ], - }); - const { queryAllByTestId, getByText, getByTestId } = render( - - ); - // Should render custom category - expect(getByText('Custom')).toBeInTheDocument(); - 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, 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 4387eb6c1769..c9e2752055e1 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 @@ -19,7 +19,7 @@ import * as Rx from 'rxjs'; import classNames from 'classnames'; import { WorkspacesStart } from 'src/core/public/workspace'; import { ChromeNavControl, ChromeNavLink } from '../..'; -import { AppCategory, NavGroupType } from '../../../../types'; +import { NavGroupType } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { createEuiListItem } from './nav_link'; @@ -60,14 +60,6 @@ const titleForSeeAll = i18n.translate('core.ui.primaryNav.seeAllLabel', { defaultMessage: 'See all...', }); -// 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 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, -}; - enum NavWidth { Expanded = 270, Collapsed = 48, // The Collasped width is supposed to be aligned with the hamburger icon on the top left navigation. @@ -78,7 +70,6 @@ export function CollapsibleNavGroupEnabled({ id, isNavOpen, storage = window.localStorage, - currentWorkspace$, closeNav, navigateToApp, navigateToUrl, @@ -94,6 +85,8 @@ export function CollapsibleNavGroupEnabled({ const appId = useObservable(observables.appId$, ''); const navGroupsMap = useObservable(observables.navGroupsMap$, {}); const currentNavGroup = useObservable(observables.currentNavGroup$, undefined); + const currentWorkspace = useObservable(observables.currentWorkspace$); + const visibleUseCases = useMemo(() => getVisibleUseCases(navGroupsMap), [navGroupsMap]); const currentNavGroupId = useMemo(() => { @@ -146,85 +139,8 @@ export function CollapsibleNavGroupEnabled({ const navLinksResult: ChromeRegistrationNavLink[] = []; - if (currentNavGroupId && currentNavGroupId !== ALL_USE_CASE_ID) { - navLinksResult.push(...(navGroupsMap[currentNavGroupId].navLinks || [])); - } - - if (currentNavGroupId === ALL_USE_CASE_ID) { - // Append all the links that do not have use case info to keep backward compatible - const linkIdsWithNavGroupInfo = Object.values(navGroupsMap).reduce((total, navGroup) => { - return [...total, ...navGroup.navLinks.map((navLink) => navLink.id)]; - }, [] as string[]); - navLinks.forEach((navLink) => { - if (linkIdsWithNavGroupInfo.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) => { - navLinksResult.push(navLink); - }); - - // Append use case section into left navigation - Object.values(navGroupsMap).forEach((group) => { - if (group.type) { - return; - } - const categoryInfo = { - id: group.id, - label: group.title, - order: group.order, - }; - - const fulfilledLinksOfNavGroup = fulfillRegistrationLinksToChromeNavLinks( - group.navLinks, - navLinks - ); - - const linksForAllUseCaseWithinNavGroup: ChromeRegistrationNavLink[] = []; - - 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 (currentNavGroupId) { + navLinksResult.push(...(navGroupsMap[currentNavGroupId]?.navLinks || [])); } if (shouldAppendManageCategory) { @@ -291,21 +207,17 @@ export function CollapsibleNavGroupEnabled({ borderRadius="none" paddingSize="s" hasShadow={false} + color="transparent" style={{ flexGrow: 0 }} > )} @@ -313,9 +225,11 @@ export function CollapsibleNavGroupEnabled({ )} @@ -355,17 +271,17 @@ export function CollapsibleNavGroupEnabled({ return ( <> {rendeLeftNav()} - - {isNavOpen - ? rendeLeftNav({ - type: 'overlay', - size: undefined, - outsideClickCloses: true, - paddingSize: undefined, - ownFocus: true, - }) - : null} - + {isNavOpen ? ( + + {rendeLeftNav({ + type: 'overlay', + size: undefined, + outsideClickCloses: true, + paddingSize: undefined, + ownFocus: true, + })} + + ) : null} ); } diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.scss b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.scss deleted file mode 100644 index 25f4385775ec..000000000000 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.scss +++ /dev/null @@ -1,3 +0,0 @@ -.leftNavTopIcon { - color: $euiColorMediumShade; -} 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 0fd0ceaadb68..42eb3abce767 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 @@ -9,10 +9,9 @@ import { ChromeNavLink } from '../../nav_links'; import { ChromeRegistrationNavLink } from '../../nav_group'; import { httpServiceMock } from '../../../mocks'; import { getLogos } from '../../../../common'; -import { CollapsibleNavTop, CollapsibleNavTopProps } from './collapsible_nav_group_enabled_top'; +import { CollapsibleNavTop } from './collapsible_nav_group_enabled_top'; import { BehaviorSubject } from 'rxjs'; import { WorkspaceObject } from 'src/core/public/workspace'; -import { ALL_USE_CASE_ID, DEFAULT_NAV_GROUPS } from '../../../'; const mockBasePath = httpServiceMock.createSetupContract({ basePath: '/test' }).basePath; @@ -40,53 +39,6 @@ describe('', () => { }; }; - it('should render back icon when inside a workspace of all use case', async () => { - const props: CollapsibleNavTopProps = { - ...getMockedProps(), - currentWorkspace$: new BehaviorSubject({ id: 'foo', name: 'foo' }), - visibleUseCases: [ - { - ...DEFAULT_NAV_GROUPS.all, - title: 'navGroupFoo', - description: 'navGroupFoo', - navLinks: [ - { - id: 'firstVisibleNavLinkOfAllUseCase', - }, - ], - }, - ], - navGroupsMap: { - [DEFAULT_NAV_GROUPS.all.id]: { - ...DEFAULT_NAV_GROUPS.all, - title: 'navGroupFoo', - description: 'navGroupFoo', - navLinks: [ - { - id: 'firstVisibleNavLinkOfAllUseCase', - }, - ], - }, - }, - navLinks: [ - getMockedNavLink({ - id: 'firstVisibleNavLinkOfAllUseCase', - }), - ], - currentNavGroup: { - id: 'navGroupFoo', - title: 'navGroupFoo', - description: 'navGroupFoo', - navLinks: [], - }, - }; - const { findByTestId, getByTestId } = render(); - await findByTestId(`collapsibleNavIcon-${DEFAULT_NAV_GROUPS.all.icon}`); - fireEvent.click(getByTestId(`collapsibleNavIcon-${DEFAULT_NAV_GROUPS.all.icon}`)); - expect(props.navigateToApp).toBeCalledWith('firstVisibleNavLinkOfAllUseCase'); - expect(props.setCurrentNavGroup).toBeCalledWith(ALL_USE_CASE_ID); - }); - it('should render home icon when not in a workspace', async () => { const props = getMockedProps(); const { findByTestId, getByTestId } = render(); @@ -101,18 +53,4 @@ describe('', () => { ); await findByTestId('collapsibleNavShrinkButton'); }); - - it('should render successfully without error when visibleUseCases is empty but inside a workspace', async () => { - expect(() => - render( - ({ id: 'foo', name: 'bar' }) - } - shouldShrinkNavigation - /> - ) - ).not.toThrow(); - }); }); 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 c40cbf33f66d..8a950ba87b84 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 @@ -4,8 +4,7 @@ */ import React, { useCallback, useMemo } from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { Logos, WorkspacesStart } from 'opensearch-dashboards/public'; +import { Logos } from 'opensearch-dashboards/public'; import { EuiButtonEmpty, EuiButtonIcon, @@ -18,25 +17,17 @@ import { } from '@elastic/eui'; import { InternalApplicationStart } from 'src/core/public/application'; import { createEuiListItem } from './nav_link'; -import { ChromeNavGroupServiceStartContract, NavGroupItemInMap } from '../../nav_group'; +import { NavGroupItemInMap } from '../../nav_group'; import { ChromeNavLink } from '../../nav_links'; -import { ALL_USE_CASE_ID } from '../../../../../core/utils'; -import { fulfillRegistrationLinksToChromeNavLinks } from '../../utils'; -import './collapsible_nav_group_enabled_top.scss'; export interface CollapsibleNavTopProps { collapsibleNavHeaderRender?: () => JSX.Element | null; homeLink?: ChromeNavLink; - navGroupsMap: Record; currentNavGroup?: NavGroupItemInMap; navigateToApp: InternalApplicationStart['navigateToApp']; logos: Logos; onClickShrink?: () => void; shouldShrinkNavigation: boolean; - visibleUseCases: NavGroupItemInMap[]; - currentWorkspace$: WorkspacesStart['currentWorkspace$']; - setCurrentNavGroup: ChromeNavGroupServiceStartContract['setCurrentNavGroup']; - navLinks: ChromeNavLink[]; } export const CollapsibleNavTop = ({ @@ -46,42 +37,9 @@ export const CollapsibleNavTop = ({ logos, onClickShrink, shouldShrinkNavigation, - visibleUseCases, - currentWorkspace$, - setCurrentNavGroup, homeLink, - navGroupsMap, - navLinks, }: CollapsibleNavTopProps) => { - const currentWorkspace = useObservable(currentWorkspace$); - const firstVisibleNavLinkInFirstVisibleUseCase = useMemo( - () => - fulfillRegistrationLinksToChromeNavLinks( - navGroupsMap[visibleUseCases[0]?.id]?.navLinks || [], - navLinks - )[0], - [navGroupsMap, navLinks, visibleUseCases] - ); - - /** - * We can ensure that left nav is inside second level once all the following conditions are met: - * 1. Inside a workspace - * 2. The use case type of current workspace is all use case - * 3. current nav group is not all use case - */ - const isInsideSecondLevelOfAllWorkspace = - !!currentWorkspace && - visibleUseCases[0]?.id === ALL_USE_CASE_ID && - currentNavGroup?.id !== ALL_USE_CASE_ID; - const homeIcon = logos.Mark.url; - const icon = - !!currentWorkspace && visibleUseCases.length === 1 - ? visibleUseCases[0].icon || homeIcon - : homeIcon; - - const shouldShowBackButton = !shouldShrinkNavigation && isInsideSecondLevelOfAllWorkspace; - const shouldShowHomeLink = !shouldShrinkNavigation && !shouldShowBackButton; const homeLinkProps = useMemo(() => { if (homeLink) { @@ -102,38 +60,28 @@ export const CollapsibleNavTop = ({ const onIconClick = useCallback( (e: React.MouseEvent) => { - if (shouldShowBackButton || visibleUseCases.length === 1) { - if (firstVisibleNavLinkInFirstVisibleUseCase) { - navigateToApp(firstVisibleNavLinkInFirstVisibleUseCase.id); - } - - setCurrentNavGroup(visibleUseCases[0].id); - } else if (shouldShowHomeLink) { - homeLinkProps.onClick?.(e); - } + homeLinkProps.onClick?.(e); }, - [ - homeLinkProps, - shouldShowBackButton, - firstVisibleNavLinkInFirstVisibleUseCase, - navigateToApp, - setCurrentNavGroup, - visibleUseCases, - shouldShowHomeLink, - ] + [homeLinkProps] ); return ( - - + + {/* The spacer here is used for align with the page header */} + + {!shouldShrinkNavigation ? ( @@ -143,9 +91,10 @@ export const CollapsibleNavTop = ({ onClick={onClickShrink} iconType={shouldShrinkNavigation ? 'menu' : 'menuLeft'} color="subdued" - display={shouldShrinkNavigation ? 'empty' : 'base'} + display="empty" aria-label="shrink-button" data-test-subj="collapsibleNavShrinkButton" + size="xs" /> @@ -155,7 +104,9 @@ export const CollapsibleNavTop = ({ <> {currentNavGroup?.type ? ( - {currentNavGroup?.title} + +

{currentNavGroup.title}

+
) : ( collapsibleNavHeaderRender?.() )} diff --git a/src/core/public/chrome/ui/header/collapsible_nav_groups.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav_groups.test.tsx index 75865190cad8..f536896e455c 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_groups.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups.test.tsx @@ -43,8 +43,8 @@ describe('', () => { }, }), getMockedNavLink({ - id: 'link-in-category-2', - title: 'link-in-category-2', + id: 'link-2-in-category', + title: 'link-2-in-category', category: { id: 'category-1', label: 'category-1', @@ -68,7 +68,7 @@ describe('', () => { expect(container.querySelectorAll('.nav-link-item-btn').length).toEqual(5); fireEvent.click(getByTestId('collapsibleNavAppLink-pure')); expect(navigateToApp).toBeCalledTimes(0); - // The accordion is collapsed + // The accordion is collapsed by default expect(queryByTestId('collapsibleNavAppLink-subLink')).toBeNull(); // Expand the accordion 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 53a75aeaaddd..c22920a633ef 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups.tsx @@ -6,12 +6,13 @@ import './collapsible_nav_group_enabled.scss'; import { EuiFlexItem, EuiSideNavItemType, EuiSideNav, EuiText } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import React from 'react'; +import React, { useState } 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'; +import { CollapsibleNavGroupsLabel, getIsCategoryOpen } from './collapsible_nav_groups_label'; export interface NavGroupsProps { navLinks: ChromeNavLink[]; @@ -23,6 +24,8 @@ export interface NavGroupsProps { event: React.MouseEvent, navItem: ChromeNavLink ) => void; + categoryCollapsible?: boolean; + currentWorkspaceId?: string; } const titleForSeeAll = i18n.translate('core.ui.primaryNav.seeAllLabel', { @@ -38,7 +41,10 @@ export function NavGroups({ appId, navigateToApp, onNavItemClick, + categoryCollapsible, + currentWorkspaceId, }: NavGroupsProps) { + const [, setRenderKey] = useState(Date.now()); const createNavItem = ({ link, className, @@ -58,7 +64,7 @@ export function NavGroups({ return { id: `${link.id}-${link.title}`, - name: {link.title}, + name: {link.title}, onClick: euiListItem.onClick, href: euiListItem.href, emphasize: euiListItem.isActive, @@ -94,17 +100,37 @@ export function NavGroups({ if (navLink.itemType === LinkItemType.PARENT_LINK && navLink.link) { const props = createNavItem({ link: navLink.link }); + const parentOpenKey = `${currentWorkspaceId ? `${currentWorkspaceId}-` : ''}${ + navLink.link.id + }`; const parentItem = { ...props, forceOpen: true, + /** + * The Tree component inside SideNav is not a controllable component, + * so we need to change the id(will pass as key into the Tree component) to remount the component. + */ + id: `${props.id}-${!!getIsCategoryOpen(parentOpenKey)}`, /** * The href and onClick should both be undefined to make parent item rendered as accordion. */ href: undefined, onClick: undefined, + /** + * The data-test-subj has to be undefined because we render the element with the attribute in CollapsibleNavGroupsLabel + */ + 'data-test-subj': undefined, className: classNames(props.className, 'nav-link-parent-item'), - buttonClassName: classNames(props.buttonClassName, 'nav-link-parent-item-button'), - items: navLink.links.map((subNavLink) => + name: ( + setRenderKey(Date.now())} + data-test-subj={props['data-test-subj']} + /> + ), + items: (getIsCategoryOpen(parentOpenKey) ? navLink.links : []).map((subNavLink) => createSideNavItem(subNavLink, level + 1, 'nav-nested-item') ), }; @@ -126,11 +152,32 @@ export function NavGroups({ } if (navLink.itemType === LinkItemType.CATEGORY) { + const categoryOpenKey = `${currentWorkspaceId ? `${currentWorkspaceId}-` : ''}${ + navLink.category?.id + }`; return { id: navLink.category?.id ?? '', - name:
{navLink.category?.label ?? ''}
, - items: navLink.links?.map((link) => createSideNavItem(link, level + 1)), + name: ( + + + {navLink.category?.label ?? ''} + + + } + collapsible={!!categoryCollapsible} + storageKey={categoryOpenKey} + onToggle={() => setRenderKey(Date.now())} + /> + ), + items: (!categoryCollapsible || getIsCategoryOpen(categoryOpenKey) + ? navLink.links + : [] + )?.map((link) => createSideNavItem(link, level + 1)), 'aria-label': navLink.category?.label, + className: 'nav-link-item-category-item', + buttonClassName: 'nav-link-item-category-button', }; } @@ -143,7 +190,7 @@ export function NavGroups({ return ( - + {suffix} ); diff --git a/src/core/public/chrome/ui/header/collapsible_nav_groups_label.tsx b/src/core/public/chrome/ui/header/collapsible_nav_groups_label.tsx new file mode 100644 index 000000000000..0e0dce8e48d7 --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav_groups_label.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './collapsible_nav_group_enabled.scss'; +import { EuiFlexItem, EuiFlexGroup, EuiIcon, EuiFlexGroupProps } from '@elastic/eui'; +import React, { useState } from 'react'; +import { getIsCategoryOpen as getIsCategoryOpenFromStorage, setIsCategoryOpen } from '../../utils'; + +export interface CollapsibleNavGroupsLabelProps { + collapsible: boolean; + storageKey: string; + storage?: Storage; + label?: React.ReactNode; + onToggle?: (isOpen: boolean) => void; + 'data-test-subj'?: EuiFlexGroupProps['data-test-subj']; +} + +export function getIsCategoryOpen(storageKey: string, storage: Storage = window.localStorage) { + return getIsCategoryOpenFromStorage(storageKey, storage); +} + +export function CollapsibleNavGroupsLabel(props: CollapsibleNavGroupsLabelProps) { + const { collapsible, storageKey, storage = window.localStorage, label, onToggle } = props; + const [, setRenderKey] = useState(Date.now()); + const isOpen = collapsible ? getIsCategoryOpen(storageKey, storage) : true; + return ( + { + e.stopPropagation(); + if (!collapsible) { + return; + } + + setIsCategoryOpen(storageKey, !isOpen, storage); + // Trigger the element to rerender because `setIsCategoryOpen` is not updating component's state + setRenderKey(Date.now()); + onToggle?.(!isOpen); + }} + > + {label} + {collapsible ? ( + + + + ) : null} + + ); +} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index b46a65be30c8..bb08a24305e6 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -28,7 +28,6 @@ * under the License. */ import { - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiHeader, @@ -262,49 +261,56 @@ export function Header({ const renderNavToggle = () => { const renderNavToggleWithExtraProps = ( props: EuiHeaderSectionItemButtonProps & { isSmallScreen?: boolean } - ) => ( - setIsNavOpen(!isNavOpen)} - aria-expanded={isNavOpen} - aria-pressed={isNavOpen} - aria-controls={navId} - ref={toggleCollapsibleNavRef} - {...props} - className={classnames( - useUpdatedHeader - ? useApplicationHeader - ? 'newAppTopNavExpander' - : 'newPageTopNavExpander' - : undefined, - props.className - )} - > - {props.isSmallScreen ? ( - - ) : ( - - )} - - ); + ) => { + const { isSmallScreen, ...others } = props; + return ( + setIsNavOpen(!isNavOpen)} + aria-expanded={isNavOpen} + aria-pressed={isNavOpen} + aria-controls={navId} + ref={toggleCollapsibleNavRef} + {...others} + className={classnames( + useUpdatedHeader + ? useApplicationHeader + ? 'newAppTopNavExpander' + : 'newPageTopNavExpander' + : undefined, + props.className + )} + > + {props.isSmallScreen ? ( + /** + * Using here will introduce a warning in console + * because button can not be a child of a button. In order to give the looks of a bordered icon, + * here we use the classes to imitate the style + */ + + + + ) : ( + + )} + + ); + }; return useUpdatedHeader ? ( <> {isNavOpen diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 223678b95d5a..5f1c66a15b16 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -333,7 +333,7 @@ class HeaderHelpMenuUI extends Component { > navGroup.status !== NavGroupStatus.Hidden && navGroup.type === undefined ); }; + +function getCategoryLocalStorageKey(id: string) { + return `core.navGroup.${id}`; +} + +export function getIsCategoryOpen(id: string, storage: Storage) { + const value = storage.getItem(getCategoryLocalStorageKey(id)) ?? 'true'; + + return value === 'true'; +} + +export function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { + storage.setItem(getCategoryLocalStorageKey(id), `${isOpen}`); +} diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 412336817f15..9d9bfa68f82f 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -78,7 +78,7 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze label: i18n.translate('core.ui.investigate.label', { defaultMessage: 'Investigate', }), - order: 1000, + order: 2000, }, // TODO remove this default category dashboardAndReport: { @@ -86,14 +86,14 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze label: i18n.translate('core.ui.visualizeAndReport.label', { defaultMessage: 'Visualize and report', }), - order: 3000, + order: 2000, }, visualizeAndReport: { id: 'visualizeAndReport', label: i18n.translate('core.ui.visualizeAndReport.label', { defaultMessage: 'Visualize and report', }), - order: 3000, + order: 1000, }, analyzeSearch: { id: 'analyzeSearch', @@ -107,14 +107,14 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze label: i18n.translate('core.ui.detect.label', { defaultMessage: 'Detect', }), - order: 3000, + order: 8000, }, configure: { id: 'configure', label: i18n.translate('core.ui.configure.label', { defaultMessage: 'Configure', }), - order: 2000, + order: 3000, }, manage: { id: 'manage', @@ -135,6 +135,6 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze label: i18n.translate('core.ui.manageWorkspaceNav.label', { defaultMessage: 'Manage workspace', }), - order: 8000, + order: 9000, }, }); diff --git a/src/plugins/dev_tools/public/dev_tools_icon.test.tsx b/src/plugins/dev_tools/public/dev_tools_icon.test.tsx index b465ec3c8b07..1f0ca532f6cd 100644 --- a/src/plugins/dev_tools/public/dev_tools_icon.test.tsx +++ b/src/plugins/dev_tools/public/dev_tools_icon.test.tsx @@ -37,7 +37,7 @@ describe('', () => { > -
-

- title1 -

-
+
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title2 -

-
+
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title3 -

-
+
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title4 -

-
+
-
-
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title5 -

-
+
-
-
-
render with complex navLinks 1`] = ` class="euiSpacer euiSpacer--m" />
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title1 -

-
+
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title2 -

-
+
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title3 -

-
+
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title4 -

-
+
-
-
render with complex navLinks 1`] = ` id="generated-idTitle" > -
-

- title5 -

-
+
-
-
-
', () => { }); it('render with complex navLinks', () => { - const { container, getAllByTestId } = render( + const { container } = render( ', () => { /> ); expect(container).toMatchSnapshot(); - expect(getAllByTestId('landingPageRow_1').length).toEqual(2); }); it('click item', () => { diff --git a/src/plugins/management/public/components/feature_cards/feature_cards.tsx b/src/plugins/management/public/components/feature_cards/feature_cards.tsx index d308f185d602..5255b56b93b4 100644 --- a/src/plugins/management/public/components/feature_cards/feature_cards.tsx +++ b/src/plugins/management/public/components/feature_cards/feature_cards.tsx @@ -30,26 +30,20 @@ export const FeatureCards = ({ pageDescription, navigationUI: { HeaderControl }, }: FeatureCardsProps) => { - const itemsPerRow = 4; const groupedCardForDisplay = useMemo(() => { - const grouped: Array<{ category?: AppCategory; navLinks: ChromeNavLink[][] }> = []; + const grouped: Array<{ category?: AppCategory; navLinks: ChromeNavLink[] }> = []; + let lastGroup: { category?: AppCategory; navLinks: ChromeNavLink[] } | undefined; // The navLinks has already been sorted based on link / category's order, // so it is safe to group the links here. navLinks.forEach((link) => { - let lastGroup = grouped.length ? grouped[grouped.length - 1] : undefined; if (!lastGroup || lastGroup.category?.id !== link.category?.id) { - lastGroup = { category: link.category, navLinks: [[]] }; + lastGroup = { category: link.category, navLinks: [] }; grouped.push(lastGroup); } - const lastRow = lastGroup.navLinks[lastGroup.navLinks.length - 1]; - if (lastRow.length < itemsPerRow) { - lastRow.push(link); - } else { - lastGroup.navLinks.push([link]); - } + lastGroup.navLinks.push(link); }); return grouped; - }, [itemsPerRow, navLinks]); + }, [navLinks]); if (!navLinks.length) { return null; } @@ -64,38 +58,31 @@ export const FeatureCards = ({ setMountPoint={setAppDescriptionControls} /> - {groupedCardForDisplay.map((group) => ( -
+ {groupedCardForDisplay.map((group, groupIndex) => ( +
{group.category && (

{group.category.label}

)} - {group.navLinks.map((row, rowIndex) => { - return ( - - {Array.from({ length: itemsPerRow }).map((item, itemIndexInRow) => { - const link = row[itemIndexInRow]; - const content = link ? ( - navigateToApp(link.id)} - titleSize="xs" - /> - ) : null; - return ( - - {content} - - ); - })} - - ); - })} + + {group.navLinks.map((link, index) => { + return ( + + navigateToApp(link.id)} + titleSize="xs" + style={{ width: 240 }} + /> + + ); + })} +
))} diff --git a/src/plugins/management/public/components/settings_icon.tsx b/src/plugins/management/public/components/settings_icon.tsx index ecae0394d963..659cb307abb1 100644 --- a/src/plugins/management/public/components/settings_icon.tsx +++ b/src/plugins/management/public/components/settings_icon.tsx @@ -61,6 +61,7 @@ export function SettingsIcon({ core }: { core: CoreStart }) { aria-label="show-apps" iconType="managementApp" onClick={() => setPopover(true)} + color="text" /> } diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 835bfb306796..6df26eb08d42 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -236,7 +236,7 @@ export class VisualizePlugin { id: visualizeAppId, category: DEFAULT_APP_CATEGORIES.visualizeAndReport, - order: 200, + order: 100, title: titleInLeftNav, }, ]); @@ -244,7 +244,7 @@ export class VisualizePlugin { id: visualizeAppId, category: DEFAULT_APP_CATEGORIES.visualizeAndReport, - order: 200, + order: 100, title: titleInLeftNav, }, ]); @@ -259,16 +259,16 @@ export class VisualizePlugin core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.search, [ { id: visualizeAppId, - category: DEFAULT_APP_CATEGORIES.analyzeSearch, - order: 400, + category: DEFAULT_APP_CATEGORIES.visualizeAndReport, + order: 100, title: titleInLeftNav, }, ]); core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [ { id: visualizeAppId, - category: undefined, - order: 400, + category: DEFAULT_APP_CATEGORIES.visualizeAndReport, + order: 100, title: titleInLeftNav, }, ]); 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 a68aea576ae1..984a22aa94c1 100644 --- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx @@ -82,6 +82,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { onClick={openPopover} aria-label="workspace-select-button" data-test-subj="workspace-select-button" + color="text" /> ); diff --git a/src/plugins/workspace/public/components/workspace_selector/workspace_selector.scss b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.scss new file mode 100644 index 000000000000..4f9b92b074c5 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.scss @@ -0,0 +1,5 @@ +@import "../../../../../core/public/chrome/ui/header/variables"; + +.workspaceNameLabel { + background-color: $ouiSideNavBackgroundColorTemp; +} diff --git a/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx index ca6431da1e1d..e3a530ce507f 100644 --- a/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx +++ b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx @@ -25,6 +25,7 @@ import { getFirstUseCaseOfFeatureConfigs } from '../../utils'; import { WorkspaceUseCase } from '../../types'; import { validateWorkspaceColor } from '../../../common/utils'; import { WorkspacePickerContent } from '../workspace_picker_content/workspace_picker_content'; +import './workspace_selector.scss'; const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorkspace', { defaultMessage: 'Create workspace', @@ -82,13 +83,13 @@ export const WorkspaceSelector = ({ coreStart, registeredUseCases$ }: Props) => padding: '0 5px', }} > - + {i18n.translate('workspace.left.nav.selector.label', { defaultMessage: 'WORKSPACE', })} - + @@ -112,11 +113,7 @@ export const WorkspaceSelector = ({ coreStart, registeredUseCases$ }: Props) => color="subdued" data-test-subj="workspace-selector-current-title" > - - {i18n.translate('workspace.left.nav.selector.title', { - defaultMessage: getUseCase(currentWorkspace)?.title || '', - })} - + {getUseCase(currentWorkspace)?.title} diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index ff6ce2c75898..aea0136023a0 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -7,7 +7,6 @@ import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import React from 'react'; import { i18n } from '@osd/i18n'; import { map } from 'rxjs/operators'; -import { EuiPanel } from '@elastic/eui'; import { Plugin, CoreStart, @@ -59,7 +58,6 @@ import { toMountPoint } from '../../opensearch_dashboards_react/public'; import { UseCaseService } from './services/use_case_service'; import { WorkspaceListCard } from './components/service_card'; import { NavigationPublicPluginStart } from '../../../plugins/navigation/public'; -import { WorkspacePickerContent } from './components/workspace_picker_content/workspace_picker_content'; import { WorkspaceSelector } from './components/workspace_selector/workspace_selector'; import { HOME_CONTENT_AREAS } from '../../../plugins/content_management/public'; import {