From f48755fec13ad473e9c67eb0c91b11d3dc3cfeb6 Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Mon, 24 Jul 2023 10:25:24 +0800 Subject: [PATCH] Refactor navigation links from left menu hard code to workspace plugin register (#55) * feature: add public/workspaces service Signed-off-by: SuZhoue-Joe * Exit workspace from left menu Signed-off-by: yuye-aws * Show exit workspace button with small window size Signed-off-by: yuye-aws * Remove recently viewed and workspace overview on left menu Signed-off-by: yuye-aws * Add buttons for outside, inside workspace case Signed-off-by: yuye-aws * Implement home button and workspace over view button on left menu Signed-off-by: yuye-aws * Implement workspace dropdown list in left menu Signed-off-by: yuye-aws * Add props on recently accessed and custom nav link Signed-off-by: yuye-aws * Reuse method getWorkspaceUrl Signed-off-by: yuye-aws * Remove recently accessed and custom nav props in test Signed-off-by: yuye-aws * Revert "Remove recently accessed and custom nav props in test" This reverts commit 7895e5c5dcde9e134f26b2d6a3df54a2d62e9274. * Wrap title with i18n Signed-off-by: yuye-aws * Add redirect for workspace app Signed-off-by: yuye-aws * Enable users to go to workspace lists page via see more under workspaces in left menu Signed-off-by: yuye-aws * Fix build error and part of test error (#42) * fix: fix build error and some ut Signed-off-by: tygao * chore: remove saved object client test diff Signed-off-by: tygao --------- Signed-off-by: tygao * Comment Alerts and Favorites in left menu Signed-off-by: yuye-aws * Recover recently viewed items in left menu Signed-off-by: yuye-aws * Move exit workspace from left menu to update page Signed-off-by: yuye-aws * Remove unused import Signed-off-by: yuye-aws * Add workspace category info Signed-off-by: yuye-aws * Remove workspace nav link Signed-off-by: yuye-aws * Remove unused import Signed-off-by: yuye-aws * Add FilteredNavLinks props to chrome service mock Signed-off-by: yuye-aws * Remove workspace related constans from chrome Signed-off-by: yuye-aws * Remove workspace related props from chrome and core Signed-off-by: yuye-aws * Remove workspace related props from header Signed-off-by: yuye-aws * Shorten import path for workspace updater Signed-off-by: yuye-aws * Add euiIconType for workspace left menu category Signed-off-by: yuye-aws * Remove workspace related props for collapsible nav Signed-off-by: yuye-aws * Remove workspace related props for collapsible nav Signed-off-by: yuye-aws * Implement navigation for delete and exit workspace Signed-off-by: yuye-aws * Navigate external links through url change Signed-off-by: yuye-aws * Implement filteredNavLinks and sort ChromeNavLinks in nav link service Signed-off-by: yuye-aws * Add workspace list, see more, admin and overview into chromenavlinks Signed-off-by: yuye-aws * fix: unit test failure (#50) Signed-off-by: SuZhou-Joe * Fix osd bootstrap error Signed-off-by: yuye-aws * Check workspace enabled for left menu Signed-off-by: yuye-aws * Add home nav link to left menu when outside workspace Signed-off-by: yuye-aws * Fix unit test for collapsible nav Signed-off-by: yuye-aws * Fix unit test for header Signed-off-by: yuye-aws * Fix unit test for collapsible nav Signed-off-by: yuye-aws * Fix unit test for collapsible nav Signed-off-by: yuye-aws * Update snapshot for unit tests Signed-off-by: yuye-aws * fix osd bootstrap error Signed-off-by: yuye-aws * fix combinelatest import error Signed-off-by: yuye-aws * update snapshot for unit tests Signed-off-by: yuye-aws * variable rename Signed-off-by: yuye-aws * move custom nav link to mock props Signed-off-by: yuye-aws * move default filtered nav link to core Signed-off-by: yuye-aws * change navigation method in workspace updater Signed-off-by: yuye-aws * Update src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx Co-authored-by: SuZhou-Joe * revert some unncessary changes Signed-off-by: yuye-aws * fix navigation url bug Signed-off-by: yuye-aws * move default filtered nav link value setting from core to workspace plugin Signed-off-by: yuye-aws * move filter nav link to a new function Signed-off-by: yuye-aws * process filter nav links when workspace is disabled Signed-off-by: yuye-aws * change navigation method Signed-off-by: yuye-aws --------- Signed-off-by: SuZhoue-Joe Signed-off-by: yuye-aws Signed-off-by: tygao Signed-off-by: SuZhou-Joe Co-authored-by: SuZhoue-Joe Co-authored-by: raintygao --- src/core/public/chrome/chrome_service.mock.ts | 2 + src/core/public/chrome/chrome_service.tsx | 48 +- src/core/public/chrome/constants.ts | 6 - src/core/public/chrome/nav_links/nav_link.ts | 5 + .../chrome/nav_links/nav_links_service.ts | 26 + .../collapsible_nav.test.tsx.snap | 5797 ++++++++--------- .../header/__snapshots__/header.test.tsx.snap | 998 +-- .../chrome/ui/header/collapsible_nav.test.tsx | 2 - .../chrome/ui/header/collapsible_nav.tsx | 334 +- .../public/chrome/ui/header/header.test.tsx | 3 - src/core/public/chrome/ui/header/header.tsx | 13 +- src/core/public/chrome/ui/header/nav_link.tsx | 41 +- src/core/public/core_system.ts | 3 +- src/plugins/workspace/common/constants.ts | 12 + .../workspace_updater/workspace_updater.tsx | 52 +- src/plugins/workspace/public/plugin.ts | 156 +- 16 files changed, 3371 insertions(+), 4127 deletions(-) diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 14b516ff95bc..b6f130270b8b 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -38,6 +38,8 @@ const createStartContractMock = () => { getHeaderComponent: jest.fn(), navLinks: { getNavLinks$: jest.fn(), + getFilteredNavLinks$: jest.fn(), + setFilteredNavLinks: jest.fn(), has: jest.fn(), get: jest.fn(), getAll: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 1463a0bd6bd4..d3dbdc3f5a04 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,7 +34,6 @@ import { FormattedMessage } from '@osd/i18n/react'; import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { EuiLink } from '@elastic/eui'; -import { i18n } from '@osd/i18n'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; @@ -42,14 +41,14 @@ import { HttpStart } from '../http'; import { InjectedMetadataStart } from '../injected_metadata'; import { NotificationsStart } from '../notifications'; import { IUiSettingsClient } from '../ui_settings'; -import { OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK, WORKSPACE_APP_ID } from './constants'; +import { OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK } from './constants'; import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; -import { Branding, WorkspacesStart } from '../'; +import { Branding } from '../'; import { getLogos } from '../../common'; import type { Logos } from '../../common/types'; @@ -97,7 +96,6 @@ interface StartDeps { injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; - workspaces: WorkspacesStart; } /** @internal */ @@ -151,7 +149,6 @@ export class ChromeService { injectedMetadata, notifications, uiSettings, - workspaces, }: StartDeps): Promise { this.initVisibility(application); @@ -177,41 +174,6 @@ export class ChromeService { docTitle.reset(); }); - const getWorkspaceUrl = (id: string) => { - return workspaces.formatUrlWithWorkspaceId( - application.getUrlForApp(WORKSPACE_APP_ID, { - path: '/', - absolute: true, - }), - id - ); - }; - - const exitWorkspace = async () => { - let result; - try { - result = await workspaces.client.exitWorkspace(); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.exit.failed', { - defaultMessage: 'Failed to exit workspace', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - return; - } - if (!result?.success) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.exit.failed', { - defaultMessage: 'Failed to exit workspace', - }), - text: result?.error, - }); - return; - } - await application.navigateToApp('home'); - }; - const setIsNavDrawerLocked = (isLocked: boolean) => { isNavDrawerLocked$.next(isLocked); localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); @@ -287,7 +249,7 @@ export class ChromeService { homeHref={http.basePath.prepend('/app/home')} isVisible$={this.isVisible$} opensearchDashboardsVersion={injectedMetadata.getOpenSearchDashboardsVersion()} - navLinks$={navLinks.getNavLinks$()} + navLinks$={navLinks.getFilteredNavLinks$()} customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} @@ -296,14 +258,10 @@ export class ChromeService { navControlsExpandedCenter$={navControls.getExpandedCenter$()} navControlsExpandedRight$={navControls.getExpandedRight$()} onIsLockedUpdate={setIsNavDrawerLocked} - exitWorkspace={exitWorkspace} - getWorkspaceUrl={getWorkspaceUrl} isLocked$={getIsNavDrawerLocked$} branding={injectedMetadata.getBranding()} logos={logos} survey={injectedMetadata.getSurvey()} - currentWorkspace$={workspaces.client.currentWorkspace$} - workspaceList$={workspaces.client.workspaceList$} /> ), diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 6de7c01f1d13..5008f8b4a69a 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -31,9 +31,3 @@ export const OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK = 'https://forum.opensearch.org/'; export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose'; - -export const WORKSPACE_APP_ID = 'workspace'; - -export const PATHS = { - list: '/list', -}; diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index cddd45234514..8479c8468b74 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -102,6 +102,11 @@ export interface ChromeNavLink { * Hides a link from the navigation. */ readonly hidden?: boolean; + + /** + * Links can be navigated through url. + */ + readonly externalLink?: boolean; } /** @public */ diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 93c138eac62c..1d3dc7994445 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -53,6 +53,16 @@ export interface ChromeNavLinks { */ getNavLinks$(): Observable>>; + /** + * Set an observable for a sorted list of filtered navlinks. + */ + getFilteredNavLinks$(): Observable>>; + + /** + * Set filtered navlinks. + */ + setFilteredNavLinks(filteredNavLinks: ReadonlyMap): void; + /** * Get the state of a navlink at this point in time. * @param id @@ -116,6 +126,7 @@ type LinksUpdater = (navLinks: Map) => Map>(new Map()); public start({ application, http }: StartDeps): ChromeNavLinks { const appLinks$ = application.applications$.pipe( @@ -151,6 +162,14 @@ export class NavLinksService { return navLinks$.pipe(map(sortNavLinks), takeUntil(this.stop$)); }, + setFilteredNavLinks: (filteredNavLinks: ReadonlyMap) => { + this.filteredNavLinks$.next(filteredNavLinks); + }, + + getFilteredNavLinks$: () => { + return this.filteredNavLinks$.pipe(map(sortChromeNavLinks), takeUntil(this.stop$)); + }, + get(id: string) { const link = navLinks$.value.get(id); return link && link.properties; @@ -215,3 +234,10 @@ function sortNavLinks(navLinks: ReadonlyMap) { 'order' ); } + +function sortChromeNavLinks(chromeNavLinks: ReadonlyMap) { + return sortBy( + [...chromeNavLinks.values()].map((link) => link as Readonly), + 'order' + ); +} diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 3d175b7cf8fe..7b88a0cac417 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -64,92 +64,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } branding={Object {}} closeNav={[Function]} - currentWorkspace$={ - BehaviorSubject { - "_isScalar": false, - "_value": null, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ - 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, - }, - 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, - } - } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -169,8 +83,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "thrownError": null, } } - exitWorkspace={[Function]} - getWorkspaceUrl={[Function]} homeHref="/" id="collapsibe-nav" isLocked={false} @@ -415,25 +327,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "closed": false, "hasError": false, "isStopped": false, - "observers": Array [], - "thrownError": null, - } - } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } - workspaceList$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [], - "closed": false, - "hasError": false, - "isStopped": false, "observers": Array [ Subscriber { "_parentOrParents": null, @@ -472,47 +365,18 @@ exports[`CollapsibleNav renders links grouped by category 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, } } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } > - - - - Favorites + Recently viewed } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
- -
- - - -
-
- Favorites + Recently viewed
@@ -1802,68 +1647,113 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
- -
- -
-

- No Favorites -

-
-
-
-
- -
- + + recent 1 + + + + + -
-

- SEE MORE -

-
-
-
-
+ + + recent 2 + + + + + +
@@ -1873,10 +1763,14 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
@@ -1903,21 +1797,27 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Library } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="/defaultModeLogo" + data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
@@ -1984,7 +1884,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Library
@@ -2011,70 +1911,145 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
- -
- -
-

- No Workspaces -

-
-
-
-
- -
- + + discover + + + + + -
-

- SEE MORE -

-
-
-
-
+ + + visualize + + + + + +
  • + + + dashboard + + +
  • +
    + +
    @@ -2084,1297 +2059,271 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
    -
    -
    + -
    - + + + -
    - - - -
    -
    - -
    - -

    - Admin -

    -
    -
    -
    -
    + Observability + + + -
    -
    -
    - -
    - -
      - - + + + + + +
      + +
      - - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" + + +
      +
      + +
      + +

      + Observability +

      +
      +
      +
      +
      +
      +
      + +
    +
    + +
    +
    -
  • - -
  • - - - - -
    -
    - -
    - - - - - -`; - -exports[`CollapsibleNav renders the default nav 1`] = ` - - - -`; - -exports[`CollapsibleNav renders the default nav 2`] = ` - - - -`; - -exports[`CollapsibleNav renders the default nav 3`] = ` - - - -
    - +
    @@ -3401,21 +2350,27 @@ exports[`CollapsibleNav renders the default nav 3`] = ` className="euiCollapsibleNavGroup__title" id="mockId__title" > - Favorites + Security } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="logoSecurity" + data-test-subj="collapsibleNavGroup-securitySolution" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    @@ -3482,7 +2437,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Favorites + Security
    @@ -3509,68 +2464,67 @@ exports[`CollapsibleNav renders the default nav 3`] = `
    - -
    - -
    -

    - No Favorites -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + siem + + + + + +
    @@ -3580,10 +2534,14 @@ exports[`CollapsibleNav renders the default nav 3`] = `
    @@ -3610,21 +2568,27 @@ exports[`CollapsibleNav renders the default nav 3`] = ` className="euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Management } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="managementApp" + data-test-subj="collapsibleNavGroup-management" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    @@ -3691,7 +2655,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Management
    @@ -3718,70 +2682,67 @@ exports[`CollapsibleNav renders the default nav 3`] = `
    - -
    - -
    -

    - No Workspaces -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + monitoring + + + + + +
    @@ -3791,92 +2752,84 @@ exports[`CollapsibleNav renders the default nav 3`] = `
    - -
    - -
    - - -
    -
    - -
    - -

    - Admin -

    -
    -
    -
    -
    -
    + canvas + + + + + +
    - -
    + +
    - -
      - - Undock navigation + Dock navigation , } } color="subdued" data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" + iconType="lockOpen" + label="Dock navigation" onClick={[Function]} size="xs" > @@ -3906,7 +2859,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" > - -
    -
    + + +
    - -
    + + @@ -3947,12 +2900,437 @@ exports[`CollapsibleNav renders the default nav 3`] = ` `; -exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1`] = ` +exports[`CollapsibleNav renders the default nav 1`] = ` + + + +`; + +exports[`CollapsibleNav renders the default nav 2`] = ` + + +`; + +exports[`CollapsibleNav renders the default nav 3`] = ` + + + + + + + +`; + +exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1`] = ` + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + - Favorites + Library @@ -4620,6 +4602,7 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1` initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    - Favorites + Library
    @@ -4720,68 +4703,67 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1`
    - -
    - -
    -

    - No Favorites -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + discover + + + + + +
    @@ -4791,10 +4773,14 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1`
    @@ -4821,21 +4807,27 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1` className="euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Observability } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="logoObservability" + data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    @@ -4902,7 +4894,7 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Observability
    @@ -4929,158 +4921,96 @@ exports[`CollapsibleNav with custom branding renders the nav bar in dark mode 1`
    - -
    - -
    -

    - No Workspaces -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    -
    -
    - - - - -
    -
    - -
    -
    - -
    - -
    - - - -
    -
    - -
    - -

    - Admin -

    -
    + +
  • + + + discover + + +
  • +
    + + +
    - -
    - +
    + + - +
    - -
    + +
    - -
      - - -
    -
    + + +
    - -
    + + @@ -6551,25 +6481,6 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode "closed": false, "hasError": false, "isStopped": false, - "observers": Array [], - "thrownError": null, - } - } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } - workspaceList$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [], - "closed": false, - "hasError": false, - "isStopped": false, "observers": Array [ Subscriber { "_parentOrParents": null, @@ -6608,84 +6519,18 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode "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, - }, - 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, } } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } > + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + - Favorites + Library @@ -6776,6 +6818,7 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    - Favorites + Library
    @@ -6876,68 +6919,67 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode
    - -
    - -
    -

    - No Favorites -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + discover + + + + + +
    @@ -6947,10 +6989,14 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode
    @@ -6977,21 +7023,27 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode className="euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Observability } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="logoObservability" + data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    @@ -7058,7 +7110,7 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Observability
    @@ -7069,86 +7121,83 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode
    - -
    -
    -
    - -
    - -
    -

    - No Workspaces -

    -
    -
    -
    -
    - + +
    +
    +
    + -
    - -
    -

    - SEE MORE -

    -
    -
    -
    - + + + discover + + + + + +
    @@ -7157,86 +7206,27 @@ exports[`CollapsibleNav without custom branding renders the nav bar in dark mode
    - -
    -
    - -
    - -
    - - - -
    -
    - -
    - -

    - Admin -

    -
    -
    -
    -
    -
    -
    -
    -
    - -
    +
    - -
      - - - - - Dock navigation - - - - - -
    -
    -
    -
    -
    -
    - - - - - -`; - -exports[`CollapsibleNav without custom branding renders the nav bar in default mode 1`] = ` - + + + Dock navigation + + + + + + +
    +
    +
    + + + + + +
    +
    +`; + +exports[`CollapsibleNav without custom branding renders the nav bar in default mode 1`] = ` + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    + + - Favorites + Library @@ -7901,6 +7886,7 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    - Favorites + Library
    @@ -8001,68 +7987,67 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m
    - -
    - -
    -

    - No Favorites -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + discover + + + + + +
    @@ -8072,10 +8057,14 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m
    @@ -8102,21 +8091,27 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m className="euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Observability } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-opensearch-logo="logoObservability" + data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    @@ -8183,7 +8178,7 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Workspaces + Observability
    @@ -8210,70 +8205,67 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m
    - -
    - -
    -

    - No Workspaces -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + discover + + + + + +
    @@ -8282,86 +8274,27 @@ exports[`CollapsibleNav without custom branding renders the nav bar in default m
    - -
    -
    - -
    - -
    - - - -
    -
    - -
    - -

    - Admin -

    -
    -
    -
    -
    -
    -
    -
    -
    - -
    +
    - -
      - - -
    -
    + + +
    - -
    + + 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 9b99f35ad63f..cefa87860d76 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 @@ -303,55 +303,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - currentWorkspace$={ - BehaviorSubject { - "_isScalar": false, - "_value": null, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ - 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, - } - } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -368,7 +319,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - exitWorkspace={[Function]} forceAppSwitcherNavigation$={ BehaviorSubject { "_isScalar": false, @@ -455,7 +405,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - getWorkspaceUrl={[Function]} helpExtension$={ BehaviorSubject { "_isScalar": false, @@ -1897,18 +1846,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "closed": false, "hasError": false, "isStopped": false, - "observers": Array [], - "thrownError": null, - } - } - survey="/" - workspaceList$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [], - "closed": false, - "hasError": false, - "isStopped": false, "observers": Array [ Subscriber { "_parentOrParents": null, @@ -1951,6 +1888,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + survey="/" >
    -
    -
    - -
    - -
    - - - -
    -
    - -
    - -

    - Home -

    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    - -
    - - - -
    -
    - -
    - -

    - Alerts -

    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - Favorites + Recently viewed } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} + onToggle={[Function]} paddingSize="none" >
    - -
    - - - -
    -
    - Favorites + Recently viewed
    @@ -6537,68 +6277,71 @@ exports[`Header handles visibility and lock changes 1`] = `
    - -
    - -
    -

    - No Favorites -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    + + + dashboard + + + + + +
    @@ -6608,296 +6351,77 @@ exports[`Header handles visibility and lock changes 1`] = `
    - - - - - - -

    - Workspaces -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    - -
    -
    -
    - -
    - -
    -

    - No Workspaces -

    -
    -
    -
    -
    - -
    - -
    -

    - SEE MORE -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - -
    -
    - -
    - -
    - - -
    -
    - -
    - -

    - Admin -

    -
    -
    -
    -
    -
    + opensearchDashboards + + + + + +
    - -
    + +
    - -
      - - -
    -
    + + +
    -
    -
    + + @@ -7280,92 +6804,6 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } - currentWorkspace$={ - BehaviorSubject { - "_isScalar": false, - "_value": null, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ - 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, - }, - 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, - } - } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -7377,7 +6815,6 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } - exitWorkspace={[Function]} forceAppSwitcherNavigation$={ BehaviorSubject { "_isScalar": false, @@ -7427,7 +6864,6 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } - getWorkspaceUrl={[Function]} helpExtension$={ BehaviorSubject { "_isScalar": false, @@ -8737,18 +8173,6 @@ exports[`Header renders condensed header 1`] = ` opensearchDashboardsDocLink="/docs" opensearchDashboardsVersion="1.0.0" recentlyAccessed$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - } - } - survey="/" - workspaceList$={ BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -8793,47 +8217,11 @@ 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, } } + survey="/" >
    {}, navigateToApp: () => Promise.resolve(), navigateToUrl: () => Promise.resolve(), - exitWorkspace: () => {}, - getWorkspaceUrl: (id: string) => '', customNavLink$: new BehaviorSubject(undefined), branding, logos: getLogos(branding, mockBasePath.serverBasePath), diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 12841ab980bb..c1c4e508e917 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -49,9 +49,8 @@ import { InternalApplicationStart } from '../../../application'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; import type { Logos } from '../../../../common/types'; -import { createEuiListItem, isModifiedOrPrevented, createWorkspaceNavLink } from './nav_link'; import { WorkspaceAttribute } from '../../../workspace'; -import { WORKSPACE_APP_ID, PATHS } from '../../constants'; +import { createEuiListItem, isModifiedOrPrevented, createRecentNavLink } from './nav_link'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -117,8 +116,6 @@ export function CollapsibleNav({ homeHref, storage = window.localStorage, onIsLockedUpdate, - exitWorkspace, - getWorkspaceUrl, closeNav, navigateToApp, navigateToUrl, @@ -126,12 +123,10 @@ export function CollapsibleNav({ ...observables }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); const appId = useObservable(observables.appId$, ''); - const currentWorkspace = useObservable(observables.currentWorkspace$); - const workspaceList = useObservable(observables.workspaceList$, []).slice(0, 5); const lockRef = useRef(null); - const filteredLinks = getFilterLinks(currentWorkspace, navLinks); - const groupedNavLinks = groupBy(filteredLinks, (link) => link?.category?.id); + const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); @@ -147,17 +142,6 @@ export function CollapsibleNav({ }); }; - function getFilterLinks( - workspace: WorkspaceAttribute | null | undefined, - allNavLinks: ChromeNavLink[] - ) { - if (!workspace) return allNavLinks; - - const features = workspace.features ?? []; - const links = allNavLinks.filter((item) => features.indexOf(item.id) > -1); - return links; - } - return ( - {/* Home, Alerts, Favorites, Projects and Admin outside workspace */} - {!currentWorkspace && ( - <> - { - closeNav(); - await navigateToApp('home'); - }} - iconType={'logoOpenSearch'} - title={i18n.translate('core.ui.primaryNavSection.home', { - defaultMessage: 'Home', + {/* Recently viewed */} + + setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage) + } + data-test-subj="collapsibleNavGroup-recentlyViewed" + > + {recentlyAccessed.length > 0 ? ( + { + // TODO #64541 + // Can remove icon from recent links completely + const { iconType, onClick, ...hydratedLink } = createRecentNavLink( + link, + navLinks, + basePath, + navigateToUrl + ); + + return { + ...hydratedLink, + 'data-test-subj': 'collapsibleNavAppLink--recent', + onClick: (event) => { + if (!isModifiedOrPrevented(event)) { + closeNav(); + onClick(event); + } + }, + }; })} + maxWidth="none" + color="subdued" + gutterSize="none" + size="s" + className="osdCollapsibleNav__recentsListGroup" /> + ) : ( + +

    + {i18n.translate('core.ui.EmptyRecentlyViewed', { + defaultMessage: 'No recently viewed items', + })} +

    +
    + )} +
    + + {/* Alerts and Favorites */} + {/* ( + <> + + ) */} + + {/* OpenSearchDashboards, Observability, Security, and Management sections */} + {orderedCategories.map((categoryName) => { + const category = categoryDictionary[categoryName]!; + const opensearchLinkLogo = + category.id === 'opensearchDashboards' ? logos.Mark.url : category.euiIconType; + + return ( setIsCategoryOpen(category.id, isCategoryOpen, storage)} + data-test-subj={`collapsibleNavGroup-${category.id}`} + data-test-opensearch-logo={opensearchLinkLogo} > - {workspaceList?.length > 0 ? ( - { - const href = getWorkspaceUrl(workspace.id); - const hydratedLink = createWorkspaceNavLink(href, workspace, navLinks); - return { - href, - ...hydratedLink, - 'data-test-subj': 'collapsibleNavAppLink--workspace', - onClick: async (event) => { - if (!isModifiedOrPrevented(event)) { - closeNav(); - } - }, - }; - })} - maxWidth="none" - color="subdued" - gutterSize="none" - size="s" - /> - ) : ( - -

    - {i18n.translate('core.ui.EmptyWorkspaceList', { - defaultMessage: 'No Workspaces', - })} -

    -
    - )} - readyForEUI(link))} + maxWidth="none" color="subdued" - style={{ padding: '0 8px 8px' }} - onClick={async () => { - await navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.list, - }); - }} - > -

    - {i18n.translate('core.ui.SeeMoreWorkspace', { - defaultMessage: 'SEE MORE', - })} -

    -
    + gutterSize="none" + size="s" + />
    - - - )} - - {/* Workspace name and Overview inside workspace */} - {currentWorkspace && ( - <> - - { - window.location.href = getWorkspaceUrl(currentWorkspace.id); - }} - iconType={'grid'} - title={i18n.translate('core.ui.primaryNavSection.overview', { - defaultMessage: 'Overview', - })} - /> - - )} + ); + })} - {/* OpenSearchDashboards, Observability, Security, and Management sections inside workspace */} - {currentWorkspace && - orderedCategories.map((categoryName) => { - const category = categoryDictionary[categoryName]!; - const opensearchLinkLogo = - category.id === 'opensearchDashboards' ? logos.Mark.url : category.euiIconType; + {/* Things with no category (largely for custom plugins) */} + {unknowns.map((link, i) => ( + + + + + + ))} - return ( - - setIsCategoryOpen(category.id, isCategoryOpen, storage) - } - data-test-subj={`collapsibleNavGroup-${category.id}`} - data-test-opensearch-logo={opensearchLinkLogo} - > - readyForEUI(link))} - maxWidth="none" - color="subdued" - gutterSize="none" - size="s" - /> - - ); - })} - - {/* Things with no category (largely for custom plugins) inside workspace */} - {currentWorkspace && - unknowns.map((link, i) => ( - - - - - - ))} - - - - {/* Exit workspace button only within a workspace*/} - {currentWorkspace && ( + {/* Docking button only for larger screens that can support it*/} + + + - )} - {/* Docking button only for larger screens that can support it*/} - { - - { + onIsLockedUpdate(!isLocked); + if (lockRef.current) { + lockRef.current.focus(); } - onClick={() => { - onIsLockedUpdate(!isLocked); - if (lockRef.current) { - lockRef.current.focus(); - } - }} - iconType={isLocked ? 'lock' : 'lockOpen'} - /> - - } - - + }} + iconType={isLocked ? 'lock' : 'lockOpen'} + /> + + +
    ); diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 711d4d0bd55e..97c13aff36a2 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -36,7 +36,6 @@ import { httpServiceMock } from '../../../http/http_service.mock'; import { applicationServiceMock, chromeServiceMock } from '../../../mocks'; import { Header } from './header'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { workspacesServiceMock } from '../../../workspace/workspaces_service.mock'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -75,8 +74,6 @@ function mockProps() { getWorkspaceUrl: (id: string) => '', survey: '/', logos: chromeServiceMock.createStartContract().logos, - currentWorkspace$: workspacesServiceMock.createStartContract().client.currentWorkspace$, - workspaceList$: workspacesServiceMock.createStartContract().client.workspaceList$, }; } diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 46d313af2b39..acc7c6869145 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -43,7 +43,7 @@ import { i18n } from '@osd/i18n'; import classnames from 'classnames'; import React, { createRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { Observable, BehaviorSubject } from 'rxjs'; +import { Observable } from 'rxjs'; import { LoadingIndicator } from '../'; import { ChromeBadge, @@ -65,7 +65,6 @@ import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; import { HeaderLogo } from './header_logo'; import type { Logos } from '../../../../common/types'; -import { WorkspaceAttribute } from '../../../workspace'; export interface HeaderProps { opensearchDashboardsVersion: string; @@ -91,13 +90,9 @@ export interface HeaderProps { isLocked$: Observable; loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; - exitWorkspace: () => void; - getWorkspaceUrl: (id: string) => string; branding: ChromeBranding; logos: Logos; survey: string | undefined; - currentWorkspace$: BehaviorSubject; - workspaceList$: BehaviorSubject; } export function Header({ @@ -106,8 +101,6 @@ export function Header({ application, basePath, onIsLockedUpdate, - exitWorkspace, - getWorkspaceUrl, homeHref, branding, survey, @@ -263,8 +256,6 @@ export function Header({ navigateToApp={application.navigateToApp} navigateToUrl={application.navigateToUrl} onIsLockedUpdate={onIsLockedUpdate} - exitWorkspace={exitWorkspace} - getWorkspaceUrl={getWorkspaceUrl} closeNav={() => { setIsNavOpen(false); if (toggleCollapsibleNavRef.current) { @@ -273,8 +264,6 @@ export function Header({ }} customNavLink$={observables.customNavLink$} logos={logos} - currentWorkspace$={observables.currentWorkspace$} - workspaceList$={observables.workspaceList$} />
    diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 8281b1ee2f96..832708122d5e 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -31,12 +31,7 @@ import { EuiIcon } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React from 'react'; -import { - ChromeNavLink, - ChromeRecentlyAccessedHistoryItem, - CoreStart, - WorkspaceAttribute, -} from '../../..'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; import { HttpStart } from '../../../http'; import { InternalApplicationStart } from '../../../application/types'; import { relativeToAbsolute } from '../../nav_links/to_nav_link'; @@ -65,7 +60,6 @@ export function createEuiListItem({ onClick = () => {}, navigateToApp, dataTestSubj, - externalLink = false, }: Props) { const { href, id, title, disabled, euiIconType, icon, tooltip } = link; @@ -79,7 +73,7 @@ export function createEuiListItem({ } if ( - !externalLink && // ignore external links + !link.externalLink && // ignore external links event.button === 0 && // ignore everything but left clicks !isModifiedOrPrevented(event) ) { @@ -153,34 +147,3 @@ export function createRecentNavLink( }, }; } - -export interface WorkspaceNavLink { - label: string; - title: string; - 'aria-label': string; -} - -export function createWorkspaceNavLink( - href: string, - workspace: WorkspaceAttribute, - navLinks: ChromeNavLink[] -): WorkspaceNavLink { - const label = workspace.name; - let titleAndAriaLabel = label; - const navLink = navLinks.find((nl) => href.startsWith(nl.baseUrl)); - if (navLink) { - titleAndAriaLabel = i18n.translate('core.ui.workspaceLinks.linkItem.screenReaderLabel', { - defaultMessage: '{workspaceItemLinkName}, type: {pageType}', - values: { - workspaceItemLinkName: label, - pageType: navLink.title, - }, - }); - } - - return { - label, - title: titleAndAriaLabel, - 'aria-label': titleAndAriaLabel, - }; -} diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 1e756ddcf8c9..0f1d1c267899 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -31,7 +31,7 @@ import { pick } from '@osd/std'; import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; -import { CoreSetup, CoreStart } from '.'; +import { ChromeNavLink, CoreSetup, CoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; @@ -233,7 +233,6 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, - workspaces, }); this.coreApp.start({ application, http, notifications, uiSettings }); diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 557b889d6111..878a0ca9441e 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; +import { AppCategory } from '../../../core/types'; + export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; @@ -14,3 +17,12 @@ export const PATHS = { }; export const WORKSPACE_OP_TYPE_CREATE = 'create'; export const WORKSPACE_OP_TYPE_UPDATE = 'update'; + +export const WORKSPACE_NAV_CATEGORY: AppCategory = { + id: 'workspace', + label: i18n.translate('core.ui.workspaceNavList.label', { + defaultMessage: 'Workspaces', + }), + euiIconType: 'folderClosed', + order: 2000, +}; diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index a3dc973ee095..278ed962966f 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -15,10 +15,8 @@ import { import { useObservable } from 'react-use'; import { i18n } from '@osd/i18n'; import { of } from 'rxjs'; - import { WorkspaceAttribute } from 'opensearch-dashboards/public'; -import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; - +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { PATHS } from '../../../common/constants'; import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; import { WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; @@ -27,7 +25,7 @@ import { DeleteWorkspaceModal } from '../delete_workspace_modal'; export const WorkspaceUpdater = () => { const { - services: { application, workspaces, notifications }, + services: { application, workspaces, notifications, http }, } = useOpenSearchDashboards<{ application: ApplicationStart }>(); const currentWorkspace = useObservable( @@ -129,7 +127,50 @@ export const WorkspaceUpdater = () => { } } setDeleteWorkspaceModalVisible(false); - await application.navigateToApp('home'); + if (http) { + const homeUrl = application.getUrlForApp('home', { + path: '/', + absolute: false, + }); + const targetUrl = http.basePath.prepend(http.basePath.remove(homeUrl), { + withoutWorkspace: true, + }); + await application.navigateToUrl(targetUrl); + } + }; + + const exitWorkspace = async () => { + let result; + try { + result = await workspaces?.client.exitWorkspace(); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.exit.failed', { + defaultMessage: 'Failed to exit workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return; + } + if (!result?.success) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.exit.failed', { + defaultMessage: 'Failed to exit workspace', + }), + text: result?.error, + }); + return; + } + if (http) { + const homeUrl = application.getUrlForApp('home', { + path: '/', + absolute: false, + }); + const targetUrl = http.basePath.prepend(http.basePath.remove(homeUrl), { + withoutWorkspace: true, + }); + await application.navigateToUrl(targetUrl); + } }; return ( @@ -139,6 +180,7 @@ export const WorkspaceUpdater = () => { restrictWidth pageTitle="Update Workspace" rightSideItems={[ + Exit, setDeleteWorkspaceModalVisible(true)}> Delete , diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index f79eb6d9bf50..e154796fe783 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -5,14 +5,19 @@ import { i18n } from '@osd/i18n'; import type { Subscription } from 'rxjs'; +import { combineLatest } from 'rxjs'; import { + ApplicationStart, + AppMountParameters, + AppNavLinkStatus, + ChromeNavLink, CoreSetup, CoreStart, Plugin, - AppMountParameters, - AppNavLinkStatus, + WorkspaceAttribute, + WorkspacesStart, } from '../../../core/public'; -import { WORKSPACE_APP_ID } from '../common/constants'; +import { PATHS, WORKSPACE_APP_ID, WORKSPACE_NAV_CATEGORY } from '../common/constants'; import { mountDropdownList } from './mount'; import { SavedObjectsManagementPluginSetup } from '../../saved_objects_management/public'; import { getWorkspaceColumn } from './components/utils/workspace_column'; @@ -99,7 +104,35 @@ export class WorkspacesPlugin implements Plugin<{}, {}, WorkspacesPluginSetupDep return {}; } - private _changeSavedObjectCurrentWorkspace() { + private workspaceToChromeNavLink( + workspace: WorkspaceAttribute, + workspacesStart: WorkspacesStart, + application: ApplicationStart + ): ChromeNavLink { + const id = WORKSPACE_APP_ID + '/' + workspace.id; + const url = workspacesStart?.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: '/', + absolute: true, + }), + workspace.id + ); + return { + id, + url, + hidden: false, + disabled: false, + baseUrl: url, + href: url, + category: WORKSPACE_NAV_CATEGORY, + title: i18n.translate('core.ui.workspaceNavList.workspaceName', { + defaultMessage: workspace.name, + }), + externalLink: true, + }; + } + + private async _changeSavedObjectCurrentWorkspace() { if (this.coreStart) { return this.coreStart.workspaces.client.currentWorkspaceId$.subscribe( (currentWorkspaceId) => { @@ -109,9 +142,121 @@ export class WorkspacesPlugin implements Plugin<{}, {}, WorkspacesPluginSetupDep } } + private filterByWorkspace( + workspace: WorkspaceAttribute | null | undefined, + allNavLinks: ChromeNavLink[] + ) { + if (!workspace) return allNavLinks; + const features = workspace.features ?? []; + return allNavLinks.filter((item) => features.includes(item.id)); + } + + private filterNavLinks(core: CoreStart, workspaceEnabled: boolean) { + const navLinksService = core.chrome.navLinks; + const chromeNavLinks$ = navLinksService.getNavLinks$(); + if (workspaceEnabled) { + const workspaceList$ = core.workspaces.client.workspaceList$; + const currentWorkspace$ = core.workspaces.client.currentWorkspace$; + combineLatest([workspaceList$, chromeNavLinks$, currentWorkspace$]).subscribe( + ([workspaceList, chromeNavLinks, currentWorkspace]) => { + const filteredNavLinks = new Map(); + chromeNavLinks = this.filterByWorkspace(currentWorkspace, chromeNavLinks); + chromeNavLinks.forEach((chromeNavLink) => { + if (chromeNavLink.id === 'home') { + // set hidden, icon and order for home nav link + const homeNavLink: ChromeNavLink = { + ...chromeNavLink, + hidden: currentWorkspace !== null, + euiIconType: 'logoOpenSearch', + order: 1000, + }; + filteredNavLinks.set(chromeNavLink.id, homeNavLink); + } else { + filteredNavLinks.set(chromeNavLink.id, chromeNavLink); + } + }); + workspaceList + .filter((value, index) => !currentWorkspace && index < 5) + .map((workspace) => + this.workspaceToChromeNavLink(workspace, core.workspaces, core.application) + ) + .forEach((workspaceNavLink) => + filteredNavLinks.set(workspaceNavLink.id, workspaceNavLink) + ); + // See more + const seeMoreId = WORKSPACE_APP_ID + PATHS.list; + const seeMoreUrl = WORKSPACE_APP_ID + PATHS.list; + const seeMoreNavLink: ChromeNavLink = { + id: seeMoreId, + title: i18n.translate('core.ui.workspaceNavList.seeMore', { + defaultMessage: 'SEE MORE', + }), + hidden: currentWorkspace !== null, + disabled: false, + baseUrl: seeMoreUrl, + href: seeMoreUrl, + category: WORKSPACE_NAV_CATEGORY, + }; + filteredNavLinks.set(seeMoreId, seeMoreNavLink); + // Admin + const adminId = 'admin'; + const adminUrl = '/app/admin'; + const adminNavLink: ChromeNavLink = { + id: adminId, + title: i18n.translate('core.ui.workspaceNavList.admin', { + defaultMessage: 'Admin', + }), + hidden: currentWorkspace !== null, + disabled: true, + baseUrl: adminUrl, + href: adminUrl, + euiIconType: 'managementApp', + order: 9000, + }; + filteredNavLinks.set(adminId, adminNavLink); + // Overview only inside workspace + if (currentWorkspace) { + const overviewId = WORKSPACE_APP_ID + PATHS.update; + const overviewUrl = core.workspaces.formatUrlWithWorkspaceId( + core.application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.update, + absolute: true, + }), + currentWorkspace.id + ); + const overviewNavLink: ChromeNavLink = { + id: overviewId, + title: i18n.translate('core.ui.workspaceNavList.overview', { + defaultMessage: 'Overview', + }), + hidden: false, + disabled: false, + baseUrl: overviewUrl, + href: overviewUrl, + euiIconType: 'grid', + order: 1000, + }; + filteredNavLinks.set(overviewId, overviewNavLink); + } + navLinksService.setFilteredNavLinks(filteredNavLinks); + } + ); + } else { + chromeNavLinks$.subscribe((chromeNavLinks) => { + const filteredNavLinks = new Map(); + chromeNavLinks.forEach((chromeNavLink) => + filteredNavLinks.set(chromeNavLink.id, chromeNavLink) + ); + navLinksService.setFilteredNavLinks(filteredNavLinks); + }); + } + } + public start(core: CoreStart) { // If workspace feature is disabled, it will not load the workspace plugin if (core.uiSettings.get('workspace:enabled') === false) { + // set default value for filtered nav links + this.filterNavLinks(core, false); return {}; } @@ -123,6 +268,9 @@ export class WorkspacesPlugin implements Plugin<{}, {}, WorkspacesPluginSetupDep chrome: core.chrome, }); this.currentWorkspaceSubscription = this._changeSavedObjectCurrentWorkspace(); + if (core) { + this.filterNavLinks(core, true); + } return {}; }