diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index f69c13c8fc0e..566a6b7095e5 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -43,9 +43,9 @@ const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { getHeaderComponent: jest.fn(), navLinks: { + setNavLinks: jest.fn(), getNavLinks$: jest.fn(), - getFilteredNavLinks$: jest.fn(), - setFilteredNavLinks: jest.fn(), + getAllNavLinks$: 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 cdbc58723dbf..928bfebf78f3 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -277,7 +277,7 @@ export class ChromeService { homeHref={http.basePath.prepend('/app/home')} isVisible$={this.isVisible$} opensearchDashboardsVersion={injectedMetadata.getOpenSearchDashboardsVersion()} - navLinks$={navLinks.getFilteredNavLinks$()} + navLinks$={navLinks.getNavLinks$()} customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index 3fe2b57676e0..8f4f5dbfa4d6 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -32,18 +32,12 @@ import { NavLinksService } from './nav_links_service'; import { take, map, takeLast } from 'rxjs/operators'; import { App } from '../../application'; import { BehaviorSubject } from 'rxjs'; +import { ChromeNavLink } from 'opensearch-dashboards/public'; const availableApps = new Map([ ['app1', { id: 'app1', order: 0, title: 'App 1', icon: 'app1' }], - [ - 'app2', - { - id: 'app2', - order: -10, - title: 'App 2', - euiIconType: 'canvasApp', - }, - ], + ['app2', { id: 'app2', order: -10, title: 'App 2', euiIconType: 'canvasApp' }], + ['app3', { id: 'app3', order: 10, title: 'App 3', icon: 'app3' }], ['chromelessApp', { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }], ]); @@ -66,7 +60,110 @@ describe('NavLinksService', () => { start = service.start({ application: mockAppService, http: mockHttp }); }); - describe('#getNavLinks$()', () => { + describe('#getAllNavLinks$()', () => { + it('does not include `chromeless` applications', async () => { + expect( + await start + .getAllNavLinks$() + .pipe( + take(1), + map((links) => links.map((l) => l.id)) + ) + .toPromise() + ).not.toContain('chromelessApp'); + }); + + it('sorts navLinks by `order` property', async () => { + expect( + await start + .getAllNavLinks$() + .pipe( + take(1), + map((links) => links.map((l) => l.id)) + ) + .toPromise() + ).toEqual(['app2', 'app1', 'app3']); + }); + + it('emits multiple values', async () => { + const navLinkIds$ = start.getAllNavLinks$().pipe(map((links) => links.map((l) => l.id))); + const emittedLinks: string[][] = []; + navLinkIds$.subscribe((r) => emittedLinks.push(r)); + start.update('app1', { href: '/foo' }); + + service.stop(); + expect(emittedLinks).toEqual([ + ['app2', 'app1', 'app3'], + ['app2', 'app1', 'app3'], + ]); + }); + + it('completes when service is stopped', async () => { + const last$ = start.getAllNavLinks$().pipe(takeLast(1)).toPromise(); + service.stop(); + await expect(last$).resolves.toBeInstanceOf(Array); + }); + }); + + describe('#getNavLinks$() when non null', () => { + // set nav links, nav link with order smaller than 0 will be filtered + beforeEach(() => { + const navLinks = new Map(); + start.getAllNavLinks$().subscribe((links) => + links.forEach((link) => { + if (link.order !== undefined && link.order >= 0) { + navLinks.set(link.id, link); + } + }) + ); + start.setNavLinks(navLinks); + }); + + it('does not include `app2` applications', async () => { + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map((links) => links.map((l) => l.id)) + ) + .toPromise() + ).not.toContain('app2'); + }); + + it('sorts navLinks by `order` property', async () => { + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map((links) => links.map((l) => l.id)) + ) + .toPromise() + ).toEqual(['app1', 'app3']); + }); + + it('emits multiple values', async () => { + const navLinkIds$ = start.getNavLinks$().pipe(map((links) => links.map((l) => l.id))); + const emittedLinks: string[][] = []; + navLinkIds$.subscribe((r) => emittedLinks.push(r)); + start.update('app1', { href: '/foo' }); + + service.stop(); + expect(emittedLinks).toEqual([ + ['app1', 'app3'], + ['app1', 'app3'], + ]); + }); + + it('completes when service is stopped', async () => { + const last$ = start.getNavLinks$().pipe(takeLast(1)).toPromise(); + service.stop(); + await expect(last$).resolves.toBeInstanceOf(Array); + }); + }); + + describe('#getNavLinks$() when null', () => { it('does not include `chromeless` applications', async () => { expect( await start @@ -79,7 +176,19 @@ describe('NavLinksService', () => { ).not.toContain('chromelessApp'); }); - it('sorts navlinks by `order` property', async () => { + it('include `app2` applications', async () => { + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map((links) => links.map((l) => l.id)) + ) + .toPromise() + ).toContain('app2'); + }); + + it('sorts navLinks by `order` property', async () => { expect( await start .getNavLinks$() @@ -88,7 +197,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1']); + ).toEqual(['app2', 'app1', 'app3']); }); it('emits multiple values', async () => { @@ -99,8 +208,8 @@ describe('NavLinksService', () => { service.stop(); expect(emittedLinks).toEqual([ - ['app2', 'app1'], - ['app2', 'app1'], + ['app2', 'app1', 'app3'], + ['app2', 'app1', 'app3'], ]); }); @@ -123,7 +232,7 @@ describe('NavLinksService', () => { describe('#getAll()', () => { it('returns a sorted array of navlinks', () => { - expect(start.getAll().map((l) => l.id)).toEqual(['app2', 'app1']); + expect(start.getAll().map((l) => l.id)).toEqual(['app2', 'app1', 'app3']); }); }); @@ -148,7 +257,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1']); + ).toEqual(['app2', 'app1', 'app3']); }); it('does nothing on chromeless applications', async () => { @@ -161,7 +270,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1']); + ).toEqual(['app2', 'app1', 'app3']); }); it('removes all other links', async () => { 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 ddbef0beed6d..d4c899a57be8 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -54,14 +54,14 @@ export interface ChromeNavLinks { getNavLinks$(): Observable>>; /** - * Set an observable for a sorted list of filtered navlinks. + * Get an observable for a sorted list of all navlinks. */ - getFilteredNavLinks$(): Observable>>; + getAllNavLinks$(): Observable>>; /** - * Set filtered navlinks. + * Set navlinks. */ - setFilteredNavLinks(filteredNavLinks: ReadonlyMap): void; + setNavLinks(navLinks: ReadonlyMap): void; /** * Get the state of a navlink at this point in time. @@ -126,9 +126,6 @@ type LinksUpdater = (navLinks: Map) => Map | undefined>( - undefined - ); public start({ application, http }: StartDeps): ChromeNavLinks { const appLinks$ = application.applications$.pipe( @@ -145,7 +142,10 @@ export class NavLinksService { // manual link modifications to be able to re-apply then after every // availableApps$ changes. const linkUpdaters$ = new BehaviorSubject([]); - const navLinks$ = new BehaviorSubject>(new Map()); + const displayedNavLinks$ = new BehaviorSubject | undefined>( + undefined + ); + const allNavLinks$ = new BehaviorSubject>(new Map()); combineLatest([appLinks$, linkUpdaters$]) .pipe( @@ -154,42 +154,40 @@ export class NavLinksService { }) ) .subscribe((navLinks) => { - navLinks$.next(navLinks); + allNavLinks$.next(navLinks); }); const forceAppSwitcherNavigation$ = new BehaviorSubject(false); return { getNavLinks$: () => { - return navLinks$.pipe(map(sortNavLinks), takeUntil(this.stop$)); + return combineLatest([allNavLinks$, displayedNavLinks$]).pipe( + map(([allNavLinks, displayedNavLinks]) => + displayedNavLinks === undefined ? sortLinks(allNavLinks) : sortLinks(displayedNavLinks) + ), + takeUntil(this.stop$) + ); }, - setFilteredNavLinks: (filteredNavLinks: ReadonlyMap) => { - this.filteredNavLinks$.next(filteredNavLinks); + setNavLinks: (navLinks: ReadonlyMap) => { + displayedNavLinks$.next(navLinks); }, - getFilteredNavLinks$: () => { - return combineLatest([navLinks$, this.filteredNavLinks$]).pipe( - map(([navLinks, filteredNavLinks]) => - filteredNavLinks === undefined - ? sortNavLinks(navLinks) - : sortChromeNavLinks(filteredNavLinks) - ), - takeUntil(this.stop$) - ); + getAllNavLinks$: () => { + return allNavLinks$.pipe(map(sortLinks), takeUntil(this.stop$)); }, get(id: string) { - const link = navLinks$.value.get(id); + const link = allNavLinks$.value.get(id); return link && link.properties; }, getAll() { - return sortNavLinks(navLinks$.value); + return sortLinks(allNavLinks$.value); }, has(id: string) { - return navLinks$.value.has(id); + return allNavLinks$.value.has(id); }, showOnly(id: string) { @@ -237,16 +235,9 @@ export class NavLinksService { } } -function sortNavLinks(navLinks: ReadonlyMap) { - return sortBy( - [...navLinks.values()].map((link) => link.properties), - 'order' - ); -} - -function sortChromeNavLinks(chromeNavLinks: ReadonlyMap) { +function sortLinks(links: ReadonlyMap) { return sortBy( - [...chromeNavLinks.values()].map((link) => link as Readonly), + [...links.values()].map((link) => ('properties' in link ? link.properties : link)), '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 b732d2904e22..3cc044260d1c 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 @@ -79,13 +79,51 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "closed": false, "hasError": false, "isStopped": false, - "observers": Array [], + "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, } } getUrlForApp={[MockFunction]} homeHref="/" - id="collapsibe-nav" + id="collapsible-nav" isLocked={false} isNavOpen={true} logos={ @@ -382,7 +420,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + + OpenSearch Dashboards + + +
+
+
+
+
+
+
+
+
- +
- -
+
    - -
    +
  • - - -
  • -
    - -
    - -
    - - - OpenSearch Dashboards - - -
    -
    -
    -
    -
-
+ Custom link + + + + + +
+
+
+ +
+
+ +
- +
- -
+
+
- +
- -
- - - -
-
- + + + +
+
+ +
+
- -
- - - OpenSearch Dashboards - - -
-
+ + + OpenSearch Dashboards + +
- +
-
+
-
- + +
+
+
+ +
- +
- -
+
+
- +
- -
- - - -
-
- + + + +
+
+ +
+
- -
- - - OpenSearch Dashboards - - -
-
+ + + OpenSearch Dashboards + +
- +
-
+
-
- + +
+
+
+ +
- +
- -
+
+
- +
- -
- - - -
-
- + + + +
+
+ +
+
- -
- - - OpenSearch Dashboards - - -
-
+ + + OpenSearch Dashboards + +
- +
-
+
-
- + +
+
+
+ +
- +
- -
+
+
- +
- -
- - - -
-
- + + + +
+
+ +
+
- -
- - - OpenSearch Dashboards - - -
-
+ + + OpenSearch Dashboards + +
- +
-
+
-
- + +
+
+
+ +
- +
- -
+
+
- +
- -
- - - -
-
- + + + +
+
+ +
+
- -
- - - OpenSearch Dashboards - - -
-
+ + + OpenSearch Dashboards + +
- +
-
+
-
- + +
+
+
+ +
+ +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + + OpenSearch Dashboards + + +
+
+
+
+
+
+
+
+
- +
- -
+
    - -
    +
  • - - -
  • -
    - -
    - -
    - - - OpenSearch Dashboards - - -
    -
    -
    -
    -
-
+ Manage cloud deployment + + + + + +
+
+
+ +
+
+ +
, - categoryDictionary: ReturnType -) { - return sortBy( - Object.keys(mainCategories), - (categoryName) => categoryDictionary[categoryName]?.order - ); -} - -function getMergedNavLinks( - orderedCategories: string[], +function getSortedLinksAndCategories( uncategorizedLinks: CollapsibleNavLink[], categoryDictionary: ReturnType -): Array { - const uncategorizedLinksWithOrder = sortBy( - uncategorizedLinks.filter((link) => link.order !== null), - 'order' - ); +): Array { + // uncategorized links and categories are ranked according the order + // if order is not defined, categories will be placed above uncategorized links + const categories = Object.values(categoryDictionary).filter( + (category) => category !== undefined + ) as AppCategory[]; + const uncategorizedLinksWithOrder = uncategorizedLinks.filter((link) => link.order !== null); const uncategorizedLinksWithoutOrder = uncategorizedLinks.filter((link) => link.order === null); - const orderedCategoryWithOrder = orderedCategories - .filter((categoryName) => categoryDictionary[categoryName]?.order !== null) - .map((categoryName) => ({ categoryName, order: categoryDictionary[categoryName]?.order })); - const orderedCategoryWithoutOrder = orderedCategories.filter( - (categoryName) => categoryDictionary[categoryName]?.order === null - ); - const mergedNavLinks = sortBy( - [...uncategorizedLinksWithOrder, ...orderedCategoryWithOrder], + const categoriesWithOrder = categories.filter((category) => category.order !== null); + const categoriesWithoutOrder = categories.filter((category) => category.order === null); + const sortedLinksAndCategories = sortBy( + [...uncategorizedLinksWithOrder, ...categoriesWithOrder], 'order' - ).map((navLink) => ('categoryName' in navLink ? navLink.categoryName : navLink)); - // if order is not defined , categorized links will be placed before uncategorized links - return [...mergedNavLinks, ...orderedCategoryWithoutOrder, ...uncategorizedLinksWithoutOrder]; + ); + return [ + ...sortedLinksAndCategories, + ...categoriesWithoutOrder, + ...uncategorizedLinksWithoutOrder, + ]; } function getCategoryLocalStorageKey(id: string) { @@ -153,6 +145,10 @@ export function CollapsibleNav({ ...observables }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + let customNavLink = useObservable(observables.customNavLink$, undefined); + if (customNavLink) { + customNavLink = { ...customNavLink, externalLink: true }; + } const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); const allNavLinks: CollapsibleNavLink[] = [...navLinks]; if (recentlyAccessed.length) { @@ -167,9 +163,7 @@ export function CollapsibleNav({ const groupedNavLinks = groupBy(allNavLinks, (link) => link?.category?.id); const { undefined: uncategorizedLinks = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); - const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); - const mergedNavLinks = getMergedNavLinks( - orderedCategories, + const sortedLinksAndCategories = getSortedLinksAndCategories( uncategorizedLinks, categoryDictionary ); @@ -204,30 +198,62 @@ export function CollapsibleNav({ onClose={closeNav} outsideClickCloses={false} > - - {collapsibleNavHeaderRender ? ( - collapsibleNavHeaderRender() - ) : ( - - - - - - - - {defaultHeaderName} - - - - - )} + {collapsibleNavHeaderRender ? ( + collapsibleNavHeaderRender() + ) : ( + + + + + + + + {defaultHeaderName} + + + + + )} - {/* merged NavLinks */} - {mergedNavLinks.map((item, i) => { - if (typeof item === 'string') { - const category = categoryDictionary[item]!; + {customNavLink && ( + + + + + + + + + + )} + + + {sortedLinksAndCategories.map((item, i) => { + if (!('href' in item)) { + // CollapsibleNavLink has href property, while AppCategory does not have + const category = item; const opensearchLinkLogo = - category.id === 'opensearchDashboards' ? logos.Mark.url : category.euiIconType; + category.id === DEFAULT_APP_CATEGORIES.opensearchDashboards.id + ? logos.Mark.url + : category.euiIconType; return ( readyForEUI(link))} + listItems={allCategorizedLinks[item.id].map((link) => readyForEUI(link))} maxWidth="none" color="subdued" gutterSize="none" diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index e8b335db1015..5665e0ecb25f 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -40,7 +40,7 @@ export const isModifiedOrPrevented = (event: React.MouseEvent(''); -const workspaceList$ = new BehaviorSubject([]); -const currentWorkspace$ = new BehaviorSubject(null); +const workspaceList$ = new BehaviorSubject([]); +const currentWorkspace$ = new BehaviorSubject(null); const initialized$ = new BehaviorSubject(false); const createWorkspacesSetupContractMock = () => ({ diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index 09d394f098ea..d9d6664582b7 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -5,10 +5,7 @@ import { BehaviorSubject, combineLatest } from 'rxjs'; import { isEqual } from 'lodash'; - -import { CoreService, WorkspaceAttribute } from '../../types'; - -type WorkspaceObject = WorkspaceAttribute & { libraryReadonly?: boolean }; +import { CoreService, WorkspaceObject } from '../../types'; interface WorkspaceObservables { /** @@ -47,8 +44,8 @@ export type WorkspacesStart = WorkspaceObservables; export class WorkspacesService implements CoreService { private currentWorkspaceId$ = new BehaviorSubject(''); - private workspaceList$ = new BehaviorSubject([]); - private currentWorkspace$ = new BehaviorSubject(null); + private workspaceList$ = new BehaviorSubject([]); + private currentWorkspace$ = new BehaviorSubject(null); private initialized$ = new BehaviorSubject(false); constructor() { diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts index e99744183cac..d66a93fcc61d 100644 --- a/src/core/types/workspace.ts +++ b/src/core/types/workspace.ts @@ -13,3 +13,7 @@ export interface WorkspaceAttribute { defaultVISTheme?: string; reserved?: boolean; } + +export interface WorkspaceObject extends WorkspaceAttribute { + readonly?: boolean; +} diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index b452b5c867ff..9679c0cfdccf 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -223,11 +223,11 @@ exports[`dashboard listing hideWriteControls 1`] = ` "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -1359,11 +1359,11 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -2556,11 +2556,11 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -3753,11 +3753,11 @@ exports[`dashboard listing renders table rows 1`] = ` "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -4950,11 +4950,11 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 9b8b911685a9..5e05e64ba8cd 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -211,11 +211,11 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -1172,11 +1172,11 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -2133,11 +2133,11 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -3094,11 +3094,11 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -4055,11 +4055,11 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, @@ -5016,11 +5016,11 @@ exports[`Dashboard top nav render with all components 1`] = ` "enableForcedAppSwitcherNavigation": [MockFunction], "get": [MockFunction], "getAll": [MockFunction], - "getFilteredNavLinks$": [MockFunction], + "getAllNavLinks$": [MockFunction], "getForceAppSwitcherNavigation$": [MockFunction], "getNavLinks$": [MockFunction], "has": [MockFunction], - "setFilteredNavLinks": [MockFunction], + "setNavLinks": [MockFunction], "showOnly": [MockFunction], "update": [MockFunction], }, diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 3b0b54569cbd..9a476b60d208 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Observable, Subscriber } from 'rxjs'; +import { Observable, Subscriber, of } from 'rxjs'; import { waitFor } from '@testing-library/dom'; +import { ChromeNavLink } from 'opensearch-dashboards/public'; import { workspaceClientMock, WorkspaceClientMock } from './workspace_client.mock'; import { applicationServiceMock, chromeServiceMock, coreMock } from '../../../core/public/mocks'; import { WorkspacePlugin } from './plugin'; @@ -131,4 +132,30 @@ describe('Workspace plugin', () => { expect(applicationStartMock.navigateToApp).toBeCalledWith(WORKSPACE_OVERVIEW_APP_ID); windowSpy.mockRestore(); }); + + it('#start filter nav links according to workspace feature', () => { + const workspacePlugin = new WorkspacePlugin(); + const coreStart = coreMock.createStart(); + const navLinksService = coreStart.chrome.navLinks; + const devToolsNavLink = { + id: 'dev_tools', + category: { id: 'management', label: 'Management' }, + }; + const discoverNavLink = { + id: 'discover', + category: { id: 'opensearchDashboards', label: 'Library' }, + }; + const workspace = { + id: 'test', + name: 'test', + features: ['dev_tools'], + }; + const allNavLinks = of([devToolsNavLink, discoverNavLink] as ChromeNavLink[]); + const filteredNavLinksMap = new Map(); + filteredNavLinksMap.set(devToolsNavLink.id, devToolsNavLink as ChromeNavLink); + navLinksService.getAllNavLinks$.mockReturnValue(allNavLinks); + coreStart.workspaces.currentWorkspace$.next(workspace); + workspacePlugin.start(coreStart); + expect(navLinksService.setNavLinks).toHaveBeenCalledWith(filteredNavLinksMap); + }); }); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 5592a32390fd..6a5da6a84efb 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -14,7 +14,7 @@ import { CoreSetup, CoreStart, Plugin, - WorkspaceAttribute, + WorkspaceObject, DEFAULT_APP_CATEGORIES, } from '../../../core/public'; import { @@ -39,15 +39,15 @@ interface WorkspacePluginSetupDeps { export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> { private coreStart?: CoreStart; private currentWorkspaceSubscription?: Subscription; + private getWorkspaceIdFromURL(): string | null { return getWorkspaceIdFromUrl(window.location.href); } + public async setup(core: CoreSetup, { savedObjectsManagement }: WorkspacePluginSetupDeps) { core.chrome.registerCollapsibleNavHeader(renderWorkspaceMenu); - const workspaceClient = new WorkspaceClient(core.http, core.workspaces); await workspaceClient.init(); - /** * Retrieve workspace id from url */ @@ -179,7 +179,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> } } - private filterByWorkspace(workspace: WorkspaceAttribute | null, allNavLinks: ChromeNavLink[]) { + private filterByWorkspace(workspace: WorkspaceObject | null, allNavLinks: ChromeNavLink[]) { if (!workspace) return allNavLinks; const features = workspace.features ?? ['*']; return allNavLinks.filter(featureMatchesConfig(features)); @@ -187,18 +187,18 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> private filterNavLinks(core: CoreStart) { const navLinksService = core.chrome.navLinks; - const chromeNavLinks$ = navLinksService.getNavLinks$(); + const allNavLinks$ = navLinksService.getAllNavLinks$(); const currentWorkspace$ = core.workspaces.currentWorkspace$; combineLatest([ - chromeNavLinks$.pipe(map(this.changeCategoryNameByWorkspaceFeatureFlag)), + allNavLinks$.pipe(map(this.changeCategoryNameByWorkspaceFeatureFlag)), currentWorkspace$, - ]).subscribe(([chromeNavLinks, currentWorkspace]) => { - const filteredNavLinks = new Map(); - chromeNavLinks = this.filterByWorkspace(currentWorkspace, chromeNavLinks); - chromeNavLinks.forEach((chromeNavLink) => { - filteredNavLinks.set(chromeNavLink.id, chromeNavLink); + ]).subscribe(([allNavLinks, currentWorkspace]) => { + const filteredNavLinks = this.filterByWorkspace(currentWorkspace, allNavLinks); + const navLinks = new Map(); + filteredNavLinks.forEach((chromeNavLink) => { + navLinks.set(chromeNavLink.id, chromeNavLink); }); - navLinksService.setFilteredNavLinks(filteredNavLinks); + navLinksService.setNavLinks(navLinks); }); }