diff --git a/projects/common/src/navigation/navigation.config.ts b/projects/common/src/navigation/navigation.config.ts new file mode 100644 index 000000000..f6f655409 --- /dev/null +++ b/projects/common/src/navigation/navigation.config.ts @@ -0,0 +1,28 @@ +import { Observable } from 'rxjs'; + +export type FooterItemConfig = FooterItemLinkConfig; + +export interface FooterItemLinkConfig { + url: string; + label: string; + icon: string; +} + +export interface NavItemHeaderConfig { + type: NavItemType.Header; + label: string; + isBeta?: boolean; + isVisible$?: Observable; +} + +export interface NavItemDividerConfig { + type: NavItemType.Divider; +} + +// Must be exported to be used by AOT compiler in template +export const enum NavItemType { + Header = 'header', + Link = 'link', + Divider = 'divider', + Footer = 'footer' +} diff --git a/projects/common/src/navigation/navigation.service.test.ts b/projects/common/src/navigation/navigation.service.test.ts index ffe9a2c68..076dc73d0 100644 --- a/projects/common/src/navigation/navigation.service.test.ts +++ b/projects/common/src/navigation/navigation.service.test.ts @@ -2,11 +2,9 @@ import { Location } from '@angular/common'; import { Title } from '@angular/platform-browser'; import { Router, UrlSegment } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { IconType } from '@hypertrace/assets-library'; -import { APP_TITLE } from '@hypertrace/common'; -import { NavItemType } from '@hypertrace/components'; import { patchRouterNavigateForTest } from '@hypertrace/test-utils'; import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; +import { APP_TITLE } from './ht-route'; import { ExternalNavigationPathParams, ExternalNavigationWindowHandling, @@ -70,6 +68,7 @@ describe('Navigation Service', () => { patchRouterNavigateForTest(spectator); router = spectator.inject(Router); }); + test('can retrieve a route config relative to the current route', () => { router.navigate(['root']); expect(spectator.service.getRouteConfig(['child'])).toEqual(firstChildRouteConfig); @@ -298,36 +297,6 @@ describe('Navigation Service', () => { } }); - test('decorating navItem with features work as expected', () => { - expect( - spectator.service.decorateNavItem( - { - type: NavItemType.Header, - label: 'Label' - }, - spectator.service.getCurrentActivatedRoute() - ) - ).toEqual({ type: NavItemType.Header, label: 'Label' }); - - expect( - spectator.service.decorateNavItem( - { - type: NavItemType.Link, - label: 'Label', - icon: IconType.None, - matchPaths: ['root'] - }, - spectator.service.rootRoute() - ) - ).toEqual({ - type: NavItemType.Link, - label: 'Label', - icon: IconType.None, - matchPaths: ['root'], - features: ['test-feature'] - }); - }); - test('setting title should work as expected', () => { router.navigate(['root', 'child']); expect(spectator.inject(Title).setTitle).toHaveBeenCalledWith('defaultAppTitle | child1'); diff --git a/projects/common/src/navigation/navigation.service.ts b/projects/common/src/navigation/navigation.service.ts index 02ebc6441..445dd04f8 100644 --- a/projects/common/src/navigation/navigation.service.ts +++ b/projects/common/src/navigation/navigation.service.ts @@ -14,8 +14,6 @@ import { UrlSegment, UrlTree } from '@angular/router'; -import { NavItemConfig, NavItemType } from '@hypertrace/components'; -import { uniq } from 'lodash-es'; import { from, Observable, of } from 'rxjs'; import { distinctUntilChanged, filter, map, share, skip, startWith, switchMap, take, tap } from 'rxjs/operators'; import { isEqualIgnoreFunctions, throwIfNil } from '../utilities/lang/lang-utils'; @@ -243,26 +241,6 @@ export class NavigationService { return this.findRouteConfig(path, childRoutes ? childRoutes : []); } - public decorateNavItem(navItem: NavItemConfig, activatedRoute: ActivatedRoute): NavItemConfig { - if (navItem.type !== NavItemType.Link) { - return { ...navItem }; - } - const features = navItem.matchPaths - .map(path => this.getRouteConfig([path], activatedRoute)) - .filter((maybeRoute): maybeRoute is HtRoute => maybeRoute !== undefined) - .flatMap(route => this.getFeaturesForRoute(route)) - .concat(navItem.features || []); - - return { - ...navItem, - features: uniq(features) - }; - } - - private getFeaturesForRoute(route: HtRoute): string[] { - return (route.data && route.data.features) || []; - } - public rootRoute(): ActivatedRoute { return this.router.routerState.root; } diff --git a/projects/common/src/public-api.ts b/projects/common/src/public-api.ts index a18f2bd6d..86929abfe 100644 --- a/projects/common/src/public-api.ts +++ b/projects/common/src/public-api.ts @@ -83,6 +83,7 @@ export * from './navigation/breadcrumb'; export * from './navigation/navigation.service'; export * from './navigation/ht-route-data'; export * from './navigation/ht-route'; +export * from './navigation/navigation.config'; // Operations export * from './utilities/operations/operation-utilities'; diff --git a/projects/components/src/navigation/nav-item/nav-item.component.test.ts b/projects/components/src/navigation/nav-item/nav-item.component.test.ts index ed04911d1..91346257a 100644 --- a/projects/components/src/navigation/nav-item/nav-item.component.test.ts +++ b/projects/components/src/navigation/nav-item/nav-item.component.test.ts @@ -5,13 +5,12 @@ import { FeatureStateResolver, MemoizeModule, NavigationParamsType, - NavigationService + NavigationService, NavItemConfig, NavItemType } from '@hypertrace/common'; import { BetaTagComponent, IconComponent, LinkComponent } from '@hypertrace/components'; import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { EMPTY, of } from 'rxjs'; -import { NavItemConfig, NavItemType } from '../navigation.config'; import { FeatureConfigCheckModule } from './../../feature-check/feature-config-check.module'; import { NavItemComponent } from './nav-item.component'; diff --git a/projects/components/src/navigation/nav-item/nav-item.component.ts b/projects/components/src/navigation/nav-item/nav-item.component.ts index 47c9c2664..b6d7efe9c 100644 --- a/projects/components/src/navigation/nav-item/nav-item.component.ts +++ b/projects/components/src/navigation/nav-item/nav-item.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { FeatureState, NavigationParams, NavigationParamsType } from '@hypertrace/common'; import { IconSize } from '../../icon/icon-size'; -import { NavItemLinkConfig } from '../navigation.config'; +import { NavItemLinkConfig} from '../navigation-list.service'; @Component({ selector: 'ht-nav-item', diff --git a/projects/components/src/navigation/navigation-list-component.service.test.ts b/projects/components/src/navigation/navigation-list-component.service.test.ts index 11929127c..12a4753f6 100644 --- a/projects/components/src/navigation/navigation-list-component.service.test.ts +++ b/projects/components/src/navigation/navigation-list-component.service.test.ts @@ -1,10 +1,8 @@ -import { FeatureState, FeatureStateResolver } from '@hypertrace/common'; -import { NavItemConfig, NavItemType } from '@hypertrace/components'; +import { FeatureState, FeatureStateResolver , NavItemConfig, NavItemHeaderConfig, NavItemType } from '@hypertrace/common'; import { runFakeRxjs } from '@hypertrace/test-utils'; import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; import { NavigationListComponentService } from './navigation-list-component.service'; -import { NavItemHeaderConfig } from './navigation.config'; describe('Navigation List Component Service', () => { const navItems: NavItemConfig[] = [ diff --git a/projects/components/src/navigation/navigation-list-component.service.ts b/projects/components/src/navigation/navigation-list-component.service.ts index 92c5e402d..499265cd1 100644 --- a/projects/components/src/navigation/navigation-list-component.service.ts +++ b/projects/components/src/navigation/navigation-list-component.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core'; -import { FeatureState, FeatureStateResolver } from '@hypertrace/common'; +import { FeatureState, FeatureStateResolver, NavItemHeaderConfig, NavItemType } from '@hypertrace/common'; import { isEmpty } from 'lodash-es'; import { combineLatest, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; -import { NavItemConfig, NavItemHeaderConfig, NavItemLinkConfig, NavItemType } from './navigation.config'; +import { NavItemConfig, NavItemLinkConfig, } from './navigation-list.service'; @Injectable({ providedIn: 'root' }) export class NavigationListComponentService { diff --git a/projects/components/src/navigation/navigation-list.component.test.ts b/projects/components/src/navigation/navigation-list.component.test.ts index 9d7d2880c..21a2cb109 100644 --- a/projects/components/src/navigation/navigation-list.component.test.ts +++ b/projects/components/src/navigation/navigation-list.component.test.ts @@ -1,6 +1,6 @@ import { ActivatedRoute } from '@angular/router'; import { IconType } from '@hypertrace/assets-library'; -import { MemoizeModule, NavigationService } from '@hypertrace/common'; +import { FooterItemConfig, MemoizeModule, NavigationService, NavItemConfig, NavItemType } from '@hypertrace/common'; import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { EMPTY, of } from 'rxjs'; @@ -10,7 +10,6 @@ import { LinkComponent } from './../link/link.component'; import { NavItemComponent } from './nav-item/nav-item.component'; import { NavigationListComponentService } from './navigation-list-component.service'; import { NavigationListComponent } from './navigation-list.component'; -import { FooterItemConfig, NavItemConfig, NavItemType } from './navigation.config'; describe('Navigation List Component', () => { let spectator: SpectatorHost; const activatedRoute = { diff --git a/projects/components/src/navigation/navigation-list.component.ts b/projects/components/src/navigation/navigation-list.component.ts index 925e6819c..606655b63 100644 --- a/projects/components/src/navigation/navigation-list.component.ts +++ b/projects/components/src/navigation/navigation-list.component.ts @@ -1,12 +1,12 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { IconType } from '@hypertrace/assets-library'; -import { NavigationService } from '@hypertrace/common'; +import { FooterItemConfig, NavigationService, NavItemType } from '@hypertrace/common'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { IconSize } from '../icon/icon-size'; import { NavigationListComponentService } from './navigation-list-component.service'; -import { FooterItemConfig, NavItemConfig, NavItemLinkConfig, NavItemType } from './navigation.config'; +import { NavItemConfig, NavItemLinkConfig } from './navigation-list.service'; @Component({ selector: 'ht-navigation-list', diff --git a/projects/components/src/navigation/navigation-list.service.test.ts b/projects/components/src/navigation/navigation-list.service.test.ts new file mode 100644 index 000000000..db846e9e7 --- /dev/null +++ b/projects/components/src/navigation/navigation-list.service.test.ts @@ -0,0 +1,55 @@ +import { ActivatedRoute } from '@angular/router'; +import { IconType } from '@hypertrace/assets-library'; +import { NavigationService, NavItemType } from '@hypertrace/common'; +import { NavigationListService } from '@hypertrace/components'; +import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; + +describe('Navigation List Service', () => { + let spectator: SpectatorService; + + const buildService = createServiceFactory({ + service: NavigationListService, + providers: [ + mockProvider(ActivatedRoute), + mockProvider(NavigationService, { getRouteConfig: jest.fn().mockReturnValue({ + path: 'root', + data: { features: ['test-feature'] }, + children: [] + })}), + ] + }); + + beforeEach(() => { + spectator = buildService(); + }); + + test('decorating navItem with features work as expected', () => { + expect( + spectator.service.decorateNavItem( + { + type: NavItemType.Header, + label: 'Label' + }, + spectator.inject(ActivatedRoute) + ) + ).toEqual({ type: NavItemType.Header, label: 'Label' }); + + expect( + spectator.service.decorateNavItem( + { + type: NavItemType.Link, + label: 'Label', + icon: IconType.None, + matchPaths: ['root'] + }, + spectator.inject(ActivatedRoute) + ) + ).toEqual({ + type: NavItemType.Link, + label: 'Label', + icon: IconType.None, + matchPaths: ['root'], + features: ['test-feature'] + }); + }); +}); diff --git a/projects/components/src/navigation/navigation-list.service.ts b/projects/components/src/navigation/navigation-list.service.ts new file mode 100644 index 000000000..6fece2392 --- /dev/null +++ b/projects/components/src/navigation/navigation-list.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + Color, + FeatureState, + HtRoute, + NavigationService, + NavItemDividerConfig, + NavItemHeaderConfig, + NavItemType +} from '@hypertrace/common'; +import { uniq } from 'lodash-es'; +import { Observable } from 'rxjs'; +import { IconSize } from '../icon/icon-size'; + +@Injectable({ providedIn: 'root'}) +export class NavigationListService { + public constructor(private readonly navigationService: NavigationService) {} + + public decorateNavItem(navItem: NavItemConfig, activatedRoute: ActivatedRoute): NavItemConfig { + if (navItem.type !== NavItemType.Link) { + return { ...navItem }; + } + const features = navItem.matchPaths + .map(path => this.navigationService.getRouteConfig([path], activatedRoute)) + .filter((maybeRoute): maybeRoute is HtRoute => maybeRoute !== undefined) + .flatMap(route => this.getFeaturesForRoute(route)) + .concat(navItem.features || []); + + return { + ...navItem, + features: uniq(features) + }; + } + + private getFeaturesForRoute(route: HtRoute): string[] { + return (route.data && route.data.features) || []; + } +} + +export type NavItemConfig = NavItemLinkConfig | NavItemHeaderConfig | NavItemDividerConfig; + +export interface NavItemLinkConfig { + type: NavItemType.Link; + icon: string; + iconSize?: IconSize; + label: string; + matchPaths: string[]; // For now, default path is index 0 + features?: string[]; + replaceCurrentHistory?: boolean; + isBeta?: boolean; + trailingIcon?: string; + trailingIconTooltip?: string; + trailingIconColor?: Color; + featureState$?: Observable; +} diff --git a/projects/components/src/navigation/navigation.config.ts b/projects/components/src/navigation/navigation.config.ts deleted file mode 100644 index 18cbd7e0c..000000000 --- a/projects/components/src/navigation/navigation.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Color, FeatureState } from '@hypertrace/common'; -import { Observable } from 'rxjs'; -import { IconSize } from '../icon/icon-size'; - -export type NavItemConfig = NavItemLinkConfig | NavItemHeaderConfig | NavItemDividerConfig; - -export interface NavItemLinkConfig { - type: NavItemType.Link; - icon: string; - iconSize?: IconSize; - label: string; - matchPaths: string[]; // For now, default path is index 0 - features?: string[]; - replaceCurrentHistory?: boolean; - isBeta?: boolean; - trailingIcon?: string; - trailingIconTooltip?: string; - trailingIconColor?: Color; - featureState$?: Observable; -} - -export type FooterItemConfig = FooterItemLinkConfig; - -export interface FooterItemLinkConfig { - url: string; - label: string; - icon: string; -} - -export interface NavItemHeaderConfig { - type: NavItemType.Header; - label: string; - isBeta?: boolean; - isVisible$?: Observable; -} - -export interface NavItemDividerConfig { - type: NavItemType.Divider; -} - -// Must be exported to be used by AOT compiler in template -export const enum NavItemType { - Header = 'header', - Link = 'link', - Divider = 'divider', - Footer = 'footer' -} diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index 71a4e754e..b43bb8f21 100644 --- a/projects/components/src/public-api.ts +++ b/projects/components/src/public-api.ts @@ -155,8 +155,8 @@ export { LayoutChangeModule } from './layout/layout-change.module'; export * from './navigation/navigation-list.component'; export * from './navigation/navigation-list.module'; export * from './navigation/nav-item/nav-item.component'; -export * from './navigation/navigation.config'; export * from './navigation/navigation-list-component.service'; +export * from './navigation/navigation-list.service'; // Let async export { LetAsyncDirective } from './let-async/let-async.directive'; diff --git a/src/app/shared/navigation/navigation.component.ts b/src/app/shared/navigation/navigation.component.ts index 6c9aed540..e0393bc3a 100644 --- a/src/app/shared/navigation/navigation.component.ts +++ b/src/app/shared/navigation/navigation.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { IconType } from '@hypertrace/assets-library'; -import { NavigationService, PreferenceService } from '@hypertrace/common'; -import { NavItemConfig, NavItemType } from '@hypertrace/components'; +import { NavItemType, PreferenceService } from '@hypertrace/common'; +import { NavigationListService, NavItemConfig } from '@hypertrace/components'; import { ObservabilityIconType } from '@hypertrace/observability'; import { Observable } from 'rxjs'; @@ -74,12 +74,12 @@ export class NavigationComponent { ]; public constructor( - private readonly navigationService: NavigationService, + private readonly navigationListService: NavigationListService, private readonly preferenceService: PreferenceService, private readonly activatedRoute: ActivatedRoute ) { this.navItems = this.navItemDefinitions.map(definition => - this.navigationService.decorateNavItem(definition, this.activatedRoute) + this.navigationListService.decorateNavItem(definition, this.activatedRoute) ); this.isCollapsed$ = this.preferenceService.get(NavigationComponent.COLLAPSED_PREFERENCE, false); }