diff --git a/projects/components/src/link/link.component.scss b/projects/components/src/link/link.component.scss index 74ed5dc2d..5418020e5 100644 --- a/projects/components/src/link/link.component.scss +++ b/projects/components/src/link/link.component.scss @@ -4,6 +4,6 @@ .ht-link { text-decoration-line: none; text-decoration: none; - color: $gray-9; + color: inherit; @include link-hover; } diff --git a/projects/distributed-tracing/src/shared/dashboard/interaction/span-trace/model/span-trace-navigation-handler.model.test.ts b/projects/distributed-tracing/src/shared/dashboard/interaction/span-trace/model/span-trace-navigation-handler.model.test.ts index 2571417ac..60b45eba9 100644 --- a/projects/distributed-tracing/src/shared/dashboard/interaction/span-trace/model/span-trace-navigation-handler.model.test.ts +++ b/projects/distributed-tracing/src/shared/dashboard/interaction/span-trace/model/span-trace-navigation-handler.model.test.ts @@ -1,4 +1,4 @@ -import { NavigationService } from '@hypertrace/common'; +import { NavigationParamsType, NavigationService } from '@hypertrace/common'; import { createModelFactory } from '@hypertrace/dashboards/testing'; import { mockProvider } from '@ngneat/spectator/jest'; import { Span, spanIdKey } from '../../../../graphql/model/schema/span'; @@ -12,7 +12,7 @@ describe('Span Trace Navigation Handler Model', () => { const buildModel = createModelFactory({ providers: [ mockProvider(NavigationService, { - navigateWithinApp: jest.fn() + navigate: jest.fn() }) ] }); @@ -23,11 +23,14 @@ describe('Span Trace Navigation Handler Model', () => { spectator.model.execute(span); - expect(navService.navigateWithinApp).not.toHaveBeenCalled(); + expect(navService.navigate).not.toHaveBeenCalled(); span.traceId = 'test-trace-id'; spectator.model.execute(span); - expect(navService.navigateWithinApp).toHaveBeenLastCalledWith(['trace', 'test-trace-id', { spanId: 'test-id' }]); + expect(navService.navigate).toHaveBeenLastCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'test-trace-id', { spanId: 'test-id' }] + }); }); }); diff --git a/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.test.ts b/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.test.ts index 14aa28ee2..7a6e06441 100644 --- a/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.test.ts +++ b/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.test.ts @@ -1,4 +1,4 @@ -import { NavigationService } from '@hypertrace/common'; +import { NavigationParamsType, NavigationService } from '@hypertrace/common'; import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; import { TracingNavigationService } from './tracing-navigation.service'; @@ -6,51 +6,82 @@ describe('Tracing Navigation Service', () => { let spectator: SpectatorService; const buildService = createServiceFactory({ - service: TracingNavigationService + service: TracingNavigationService, + providers: [ + mockProvider(NavigationService, { + navigate: jest.fn(), + isRelativePathActive: () => true + }) + ] }); test('can navigate correctly to trace detail', () => { - spectator = buildService({ - providers: [ - mockProvider(NavigationService, { - navigateWithinApp: jest.fn(), - isRelativePathActive: () => true - }) - ] - }); + spectator = buildService(); const navigationService = spectator.inject(NavigationService); spectator.service.navigateToTraceDetail('trace-id', 'span-id', '1608150110610'); - expect(navigationService.navigateWithinApp).toHaveBeenLastCalledWith([ - 'trace', - 'trace-id', - { spanId: 'span-id', startTime: '1608150110610' } - ]); + expect(navigationService.navigate).toHaveBeenLastCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'trace-id', { spanId: 'span-id', startTime: '1608150110610' }] + }); spectator.service.navigateToTraceDetail('trace-id', 'span-id'); - expect(navigationService.navigateWithinApp).toHaveBeenLastCalledWith(['trace', 'trace-id', { spanId: 'span-id' }]); + expect(navigationService.navigate).toHaveBeenLastCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'trace-id', { spanId: 'span-id' }] + }); spectator.service.navigateToTraceDetail('trace-id'); - expect(navigationService.navigateWithinApp).toHaveBeenLastCalledWith(['trace', 'trace-id', {}]); + expect(navigationService.navigate).toHaveBeenLastCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'trace-id', {}] + }); }); - test('can navigate correctly to Api trace detail', () => { - spectator = buildService({ - providers: [ - mockProvider(NavigationService, { - navigateWithinApp: jest.fn(), - isRelativePathActive: () => true - }) - ] + test('builds correct trace detail navigation params', () => { + spectator = buildService(); + expect(spectator.service.buildTraceDetailNavigationParam('trace-id', 'span-id', '1608150110610')).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'trace-id', { spanId: 'span-id', startTime: '1608150110610' }] + }); + + expect(spectator.service.buildTraceDetailNavigationParam('trace-id', 'span-id')).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'trace-id', { spanId: 'span-id' }] + }); + + expect(spectator.service.buildTraceDetailNavigationParam('trace-id')).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/trace', 'trace-id', {}] + }); + }); + + test('builds correct Api trace detail navigation params', () => { + spectator = buildService(); + + expect(spectator.service.buildApiTraceDetailNavigationParam('trace-id', '1608150110610')).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/api-trace', 'trace-id', { startTime: '1608150110610' }] + }); + + expect(spectator.service.buildApiTraceDetailNavigationParam('trace-id')).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/api-trace', 'trace-id', {}] }); + }); + + test('can navigate correctly to Api trace detail', () => { + spectator = buildService(); const navigationService = spectator.inject(NavigationService); spectator.service.navigateToApiTraceDetail('trace-id', '1608150110610'); - expect(navigationService.navigateWithinApp).toHaveBeenLastCalledWith([ - 'api-trace', - 'trace-id', - { startTime: '1608150110610' } - ]); + expect(navigationService.navigate).toHaveBeenLastCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/api-trace', 'trace-id', { startTime: '1608150110610' }] + }); spectator.service.navigateToApiTraceDetail('trace-id'); - expect(navigationService.navigateWithinApp).toHaveBeenLastCalledWith(['api-trace', 'trace-id', {}]); + expect(navigationService.navigate).toHaveBeenLastCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/api-trace', 'trace-id', {}] + }); }); }); diff --git a/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.ts b/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.ts index 4d4c95803..df4106e65 100644 --- a/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.ts +++ b/projects/distributed-tracing/src/shared/services/navigation/tracing-navigation.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Dictionary, NavigationService } from '@hypertrace/common'; +import { Dictionary, NavigationParams, NavigationParamsType, NavigationService } from '@hypertrace/common'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) @@ -7,6 +7,14 @@ export class TracingNavigationService { public constructor(private readonly navigationService: NavigationService) {} public navigateToTraceDetail(traceId: string, spanId?: string, startTime?: string | number): Observable { + return this.navigationService.navigate(this.buildTraceDetailNavigationParam(traceId, spanId, startTime)); + } + + public buildTraceDetailNavigationParam( + traceId: string, + spanId?: string, + startTime?: string | number + ): NavigationParams { const optionalParams: Dictionary = {}; if (startTime !== undefined) { @@ -17,16 +25,26 @@ export class TracingNavigationService { optionalParams.spanId = spanId; } - return this.navigationService.navigateWithinApp(['trace', traceId, optionalParams]); + return { + navType: NavigationParamsType.InApp, + path: ['/trace', traceId, optionalParams] + }; } public navigateToApiTraceDetail(traceId: string, startTime?: string | number): Observable { + return this.navigationService.navigate(this.buildApiTraceDetailNavigationParam(traceId, startTime)); + } + + public buildApiTraceDetailNavigationParam(traceId: string, startTime?: string | number): NavigationParams { const optionalParams: Dictionary = {}; if (startTime !== undefined) { optionalParams.startTime = `${String(startTime)}`; } - return this.navigationService.navigateWithinApp(['api-trace', traceId, optionalParams]); + return { + navType: NavigationParamsType.InApp, + path: ['/api-trace', traceId, optionalParams] + }; } } diff --git a/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.scss b/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.scss index 50f7c86ac..6591209c4 100644 --- a/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.scss +++ b/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.scss @@ -2,26 +2,23 @@ @import 'font'; .ht-entity-renderer { - @include body-1-regular(currentColor); - display: flex; - flex-direction: row; - align-items: center; - font-weight: inherit; - color: $gray-9; + .name-with-icon { + display: flex; + flex-direction: row; + align-items: center; + font-weight: inherit; - .icon { - padding-right: 12px; - } + .icon { + padding-right: 12px; + } - .name { - @include ellipsis-overflow(); + .name { + @include ellipsis-overflow(); + } } } -.navigable { - @include link-hover(); -} - -.inherit-text-color { - @include body-1-regular(inherit); +.default-text-style { + color: $gray-9; + @include body-1-regular(currentColor); } diff --git a/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.test.ts b/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.test.ts index 44d2a9209..3cbb68325 100644 --- a/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.test.ts +++ b/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.test.ts @@ -1,6 +1,6 @@ import { IconType } from '@hypertrace/assets-library'; -import { NavigationService } from '@hypertrace/common'; -import { IconComponent } from '@hypertrace/components'; +import { FormattingModule, NavigationParamsType, NavigationService } from '@hypertrace/common'; +import { IconComponent, LinkComponent } from '@hypertrace/components'; import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { entityIdKey, entityTypeKey, ObservabilityEntityType } from '../../graphql/model/schema/entity'; @@ -14,9 +14,13 @@ describe('Entity Renderer Component', () => { const createHost = createHostFactory({ component: EntityRendererComponent, + imports: [FormattingModule], providers: [ mockProvider(EntityNavigationService, { - navigateToEntity: jest.fn() + buildEntityDetailNavigationParams: jest.fn().mockReturnValue({ + navType: NavigationParamsType.InApp, + path: ['/endpoint', 'test-id'] + }) }), mockProvider(NavigationService), mockProvider(EntityIconLookupService, { @@ -24,7 +28,7 @@ describe('Entity Renderer Component', () => { }) ], shallow: true, - declarations: [MockComponent(IconComponent)] + declarations: [MockComponent(IconComponent), MockComponent(LinkComponent)] }); test('renders a basic entity without navigation', () => { @@ -49,12 +53,12 @@ describe('Entity Renderer Component', () => { const entityNavService = spectator.inject(EntityNavigationService); const rendererElement = spectator.query('.ht-entity-renderer')!; + expect(rendererElement).toExist(); + expect(spectator.query('.name')).toHaveText('test api'); expect(spectator.query(IconComponent)!.icon).toBe(ObservabilityIconType.Api); - expect(rendererElement).not.toHaveClass('navigable'); - spectator.dispatchFakeEvent(rendererElement, 'click'); - expect(entityNavService.navigateToEntity).toHaveBeenCalledTimes(0); + expect(entityNavService.buildEntityDetailNavigationParams).not.toHaveBeenCalled(); }); test('renders a basic entity with navigation', () => { @@ -75,15 +79,19 @@ describe('Entity Renderer Component', () => { } } ); - const entityNavService = spectator.inject(EntityNavigationService); - const rendererElement = spectator.query('.ht-entity-renderer')!; expect(spectator.query('.name')).toHaveText('test api'); expect(spectator.query(IconComponent)!.icon).toBe(ObservabilityIconType.Api); - expect(rendererElement).toHaveClass('navigable'); - spectator.dispatchFakeEvent(rendererElement, 'click'); - expect(entityNavService.navigateToEntity).toHaveBeenCalledWith(entity, false); + expect(spectator.inject(EntityNavigationService).buildEntityDetailNavigationParams).toHaveBeenCalledWith( + entity, + false + ); + + expect(spectator.query(LinkComponent)?.paramsOrUrl).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/endpoint', 'test-id'] + }); }); test('renders an entity without icon by default', () => { @@ -143,23 +151,24 @@ describe('Entity Renderer Component', () => { }; spectator = createHost( - ` + ` `, { hostProps: { entity: entity, - inheritTextColor: false + inheritTextStyle: false } } ); - expect(spectator.query('.inherit-text-color')).not.toExist(); + expect(spectator.query('.default-text-style')).toExist(); + expect(spectator.query('.ht-entity-renderer')).toBe(spectator.query('.default-text-style')); spectator.setHostInput({ - inheritTextColor: true + inheritTextStyle: true }); - expect(spectator.query('.inherit-text-color')).toExist(); + expect(spectator.query('.default-text-style')).not.toExist(); - expect(spectator.query('.ht-entity-renderer')).toEqual(spectator.query('.inherit-text-color')); + expect(spectator.query('.ht-entity-renderer')).not.toBe(spectator.query('.default-text-style')); }); }); diff --git a/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.ts b/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.ts index e62951cdf..cc7c2ff10 100644 --- a/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.ts +++ b/projects/observability/src/shared/components/entity-renderer/entity-renderer.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { TypedSimpleChanges } from '@hypertrace/common'; +import { InAppNavigationParams, TypedSimpleChanges } from '@hypertrace/common'; import { IconSize } from '@hypertrace/components'; import { Entity } from '../../graphql/model/schema/entity'; import { EntityIconLookupService } from '../../services/entity/entity-icon-lookup.service'; @@ -13,19 +13,29 @@ import { EntityNavigationService } from '../../services/navigation/entity/entity
- -
{{ this.name }}
+
-
-
+ + + + + + + + +
+ +
{{ this.name | htDisplayString }}
+
+
` }) export class EntityRendererComponent implements OnChanges { @@ -45,10 +55,11 @@ export class EntityRendererComponent implements OnChanges { public showIcon: boolean = false; @Input() - public inheritTextColor: boolean = false; + public inheritTextStyle: boolean = false; public name?: string; public entityIconType?: string; + public navigationParams?: InAppNavigationParams; public constructor( private readonly iconLookupService: EntityIconLookupService, @@ -59,6 +70,7 @@ export class EntityRendererComponent implements OnChanges { if (changes.entity) { this.setName(); this.setIconType(); + this.setNavigationParams(); } if (changes.icon) { @@ -66,10 +78,6 @@ export class EntityRendererComponent implements OnChanges { } } - public onClickNavigate(): void { - this.navigable && this.entity && this.entityNavService.navigateToEntity(this.entity, this.inactive); - } - private setName(): void { this.name = this.entity?.name as string; } @@ -78,4 +86,11 @@ export class EntityRendererComponent implements OnChanges { this.entityIconType = this.icon ?? (this.entity !== undefined ? this.iconLookupService.forEntity(this.entity) : undefined); } + + private setNavigationParams(): void { + this.navigationParams = + this.navigable && this.entity !== undefined + ? this.entityNavService.buildEntityDetailNavigationParams(this.entity, this.inactive) + : undefined; + } } diff --git a/projects/observability/src/shared/components/entity-renderer/entity-renderer.module.ts b/projects/observability/src/shared/components/entity-renderer/entity-renderer.module.ts index ea15d8ae2..0d65a0f38 100644 --- a/projects/observability/src/shared/components/entity-renderer/entity-renderer.module.ts +++ b/projects/observability/src/shared/components/entity-renderer/entity-renderer.module.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormattingModule } from '@hypertrace/common'; -import { IconModule, TooltipModule } from '@hypertrace/components'; +import { IconModule, LinkModule, TooltipModule } from '@hypertrace/components'; import { EntityRendererComponent } from './entity-renderer.component'; @NgModule({ - imports: [CommonModule, TooltipModule, IconModule], + imports: [CommonModule, TooltipModule, IconModule, LinkModule, FormattingModule], declarations: [EntityRendererComponent], exports: [EntityRendererComponent] }) diff --git a/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.test.ts b/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.test.ts index 93627e4d7..a25ccba97 100644 --- a/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.test.ts +++ b/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.test.ts @@ -1,5 +1,5 @@ import { RouterTestingModule } from '@angular/router/testing'; -import { NavigationService } from '@hypertrace/common'; +import { NavigationParamsType, NavigationService } from '@hypertrace/common'; import { patchRouterNavigateForTest } from '@hypertrace/test-utils'; import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; import { EntityMetadata, ENTITY_METADATA } from '../../../constants/entity-metadata'; @@ -15,7 +15,7 @@ describe('Entity Navigation Service', () => { service: EntityNavigationService, providers: [ mockProvider(NavigationService, { - navigateWithinApp: jest.fn(), + navigate: jest.fn(), isRelativePathActive: () => true }), { @@ -66,7 +66,22 @@ describe('Entity Navigation Service', () => { [entityTypeKey]: ObservabilityEntityType.Service }); - expect(navigationService.navigateWithinApp).toHaveBeenCalledWith(['services', 'service', 'test-id']); + expect(navigationService.navigate).toHaveBeenCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/', 'services', 'service', 'test-id'] + }); + }); + + test('builds correct navigation params for service', () => { + expect( + spectator.service.buildEntityDetailNavigationParams({ + [entityIdKey]: 'test-id', + [entityTypeKey]: ObservabilityEntityType.Service + }) + ).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/', 'services', 'service', 'test-id'] + }); }); test('can navigate correctly to api', () => { @@ -75,7 +90,22 @@ describe('Entity Navigation Service', () => { [entityTypeKey]: ObservabilityEntityType.Api }); - expect(navigationService.navigateWithinApp).toHaveBeenCalledWith(['services', 'endpoint', 'test-id']); + expect(navigationService.navigate).toHaveBeenCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/', 'services', 'endpoint', 'test-id'] + }); + }); + + test('build correct navigation params to endpoint', () => { + expect( + spectator.service.buildEntityDetailNavigationParams({ + [entityIdKey]: 'test-id', + [entityTypeKey]: ObservabilityEntityType.Api + }) + ).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/', 'services', 'endpoint', 'test-id'] + }); }); test('can navigate correctly to backend', () => { @@ -84,6 +114,21 @@ describe('Entity Navigation Service', () => { [entityTypeKey]: ObservabilityEntityType.Backend }); - expect(navigationService.navigateWithinApp).toHaveBeenCalledWith(['backends', 'backend', 'test-id']); + expect(navigationService.navigate).toHaveBeenCalledWith({ + navType: NavigationParamsType.InApp, + path: ['/', 'backends', 'backend', 'test-id'] + }); + }); + + test('build correct navigation params to backend', () => { + expect( + spectator.service.buildEntityDetailNavigationParams({ + [entityIdKey]: 'test-id', + [entityTypeKey]: ObservabilityEntityType.Backend + }) + ).toEqual({ + navType: NavigationParamsType.InApp, + path: ['/', 'backends', 'backend', 'test-id'] + }); }); }); diff --git a/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.ts b/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.ts index a54c29e14..2e5c930c5 100644 --- a/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.ts +++ b/projects/observability/src/shared/services/navigation/entity/entity-navigation.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@angular/core'; -import { NavigationService } from '@hypertrace/common'; +import { InAppNavigationParams, NavigationParamsType, NavigationService } from '@hypertrace/common'; import { Observable, throwError } from 'rxjs'; import { EntityMetadataMap, ENTITY_METADATA } from '../../../constants/entity-metadata'; import { Entity, entityIdKey, EntityType, entityTypeKey } from '../../../graphql/model/schema/entity'; @@ -11,18 +11,28 @@ export class EntityNavigationService { @Inject(ENTITY_METADATA) private readonly entityMetadata: EntityMetadataMap ) { Array.from(this.entityMetadata.values()).forEach(item => { - this.registerEntityNavigationAction(item.entityType, (id, sourceRoute, inactive) => - this.navigationService.navigateWithinApp(item.detailPath(id, sourceRoute, inactive)) - ); + this.registerEntityNavigationAction(item.entityType, (id, sourceRoute, inactive) => ({ + navType: NavigationParamsType.InApp, + path: ['/', ...item.detailPath(id, sourceRoute, inactive)] + })); }); } private readonly entityNavigationMap: Map< EntityType, - (id: string, sourceRoute?: string, inactive?: boolean) => Observable + (id: string, sourceRoute?: string, inactive?: boolean) => InAppNavigationParams > = new Map(); public navigateToEntity(entity: Entity, isInactive?: boolean): Observable { + const entityType = entity[entityTypeKey]; + const navigationParams = this.buildEntityDetailNavigationParams(entity, isInactive); + + return navigationParams + ? this.navigationService.navigate(navigationParams) + : throwError(`Requested entity type not registered for navigation: ${entityType}`); + } + + public buildEntityDetailNavigationParams(entity: Entity, isInactive?: boolean): InAppNavigationParams | undefined { const entityType = entity[entityTypeKey]; const entityId = entity[entityIdKey]; const navigationFunction = this.entityNavigationMap.get(entityType); @@ -34,14 +44,12 @@ export class EntityNavigationService { this.navigationService.isRelativePathActive([item], this.navigationService.rootRoute()) ); - return navigationFunction - ? navigationFunction(entityId, sourceRoute, isInactive) - : throwError(`Requested entity type not registered for navigation: ${entityType}`); + return navigationFunction ? navigationFunction(entityId, sourceRoute, isInactive) : undefined; } public registerEntityNavigationAction( entityType: EntityType, - action: (id: string, sourceRoute?: string, inactive?: boolean) => Observable + action: (id: string, sourceRoute?: string, inactive?: boolean) => InAppNavigationParams ): void { this.entityNavigationMap.set(entityType, action); }