diff --git a/package.json b/package.json index d546d307d..a218ce02c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build:components": "ng build components", "build:dashboards": "ng build dashboards", "build:ci": "node --max_old_space_size=3584 node_modules/@angular/cli/bin/ng build --configuration production --no-progress", - "test": "ng test hypertrace-ui --cache", + "test": "ng test hypertrace-ui --cache --maxWorkers=2", "lint": "ng lint hypertrace-ui", "lint:fix": "ng lint --fix hypertrace-ui", "prettier:check": "prettier --check '**'", diff --git a/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.test.ts b/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.test.ts index f71accb58..c401bfa55 100644 --- a/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.test.ts +++ b/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.test.ts @@ -4,6 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NavigationService } from '@hypertrace/common'; import { GraphQlRequestCacheability, GraphQlRequestService } from '@hypertrace/graphql-client'; import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; +import { EntityBreadcrumb } from './../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver'; import { patchRouterNavigateForTest, runFakeRxjs } from '@hypertrace/test-utils'; import { of } from 'rxjs'; @@ -14,7 +15,7 @@ import { ObservabilityIconType } from '../../../shared/icons/observability-icon- import { ApiDetailBreadcrumbResolver } from './api-detail-breadcrumb.resolver'; describe('Api detail breadcrumb resolver', () => { - let spectator: SpectatorService; + let spectator: SpectatorService>; let activatedRouteSnapshot: ActivatedRouteSnapshot; const buildResolver = createServiceFactory({ service: ApiDetailBreadcrumbResolver, @@ -83,6 +84,8 @@ describe('Api detail breadcrumb resolver', () => { runFakeRxjs(({ expectObservable }) => { expectObservable(breadcrumb$).toBe('(abc|)', { a: { + [entityIdKey]: 'test-service-id', + [entityTypeKey]: ObservabilityEntityType.Service, label: 'test service', icon: ObservabilityIconType.Service, url: ['services', 'service', 'test-service-id'] @@ -93,9 +96,16 @@ describe('Api detail breadcrumb resolver', () => { url: ['services', 'service', 'test-service-id', 'endpoints'] }, c: { + [entityIdKey]: 'test-id', + [entityTypeKey]: ObservabilityEntityType.Api, label: 'test api', icon: ObservabilityIconType.Api, - url: ['api', 'test-id'] + url: ['api', 'test-id'], + name: 'test api', + parentId: 'test-service-id', + parentName: 'test service', + serviceName: 'test service', + serviceId: 'test-service-id' } }); }); @@ -108,7 +118,7 @@ describe('Api detail breadcrumb resolver', () => { entityType: ObservabilityEntityType.Api, id: 'test-id' }), - { cacheability: GraphQlRequestCacheability.NotCacheable } + { cacheability: GraphQlRequestCacheability.Cacheable } ); })); @@ -122,9 +132,14 @@ describe('Api detail breadcrumb resolver', () => { runFakeRxjs(({ expectObservable }) => { expectObservable(breadcrumb$).toBe('(y|)', { y: { + [entityIdKey]: 'test-id', + [entityTypeKey]: ObservabilityEntityType.Api, label: 'test api', icon: ObservabilityIconType.Api, - url: ['api', 'test-id'] + url: ['api', 'test-id'], + name: 'test api', + serviceName: 'test service', + serviceId: 'test-service-id' } }); }); @@ -137,7 +152,7 @@ describe('Api detail breadcrumb resolver', () => { entityType: ObservabilityEntityType.Api, id: 'test-id' }), - { cacheability: GraphQlRequestCacheability.NotCacheable } + { cacheability: GraphQlRequestCacheability.Cacheable } ); })); }); diff --git a/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.ts b/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.ts index 2af9fde7e..190ffae8e 100644 --- a/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.ts +++ b/projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.ts @@ -1,101 +1,86 @@ import { Inject, Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { ActivatedRouteSnapshot } from '@angular/router'; import { Breadcrumb, NavigationService, TimeRangeService } from '@hypertrace/common'; import { BreadcrumbsService } from '@hypertrace/components'; -import { GraphQlRequestCacheability, GraphQlRequestService } from '@hypertrace/graphql-client'; +import { GraphQlRequestService } from '@hypertrace/graphql-client'; +import { entityIdKey } from '@hypertrace/observability'; import { Observable } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { EntityMetadata, EntityMetadataMap, ENTITY_METADATA } from '../../../shared/constants/entity-metadata'; -import { Entity, ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity'; -import { GraphQlTimeRange } from '../../../shared/graphql/model/schema/timerange/graphql-time-range'; -import { SpecificationBuilder } from '../../../shared/graphql/request/builders/specification/specification-builder'; +import { Entity, entityTypeKey, ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity'; import { - EntityGraphQlQueryHandlerService, - ENTITY_GQL_REQUEST -} from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service'; + EntityBreadcrumb, + EntityBreadcrumbResolver +} from '../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver'; +import { EntityIconLookupService } from './../../../shared/services/entity/entity-icon-lookup.service'; @Injectable({ providedIn: 'root' }) -export class ApiDetailBreadcrumbResolver implements Resolve> { - private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder(); +export class ApiDetailBreadcrumbResolver extends EntityBreadcrumbResolver { protected readonly apiEntityMetadata: EntityMetadata | undefined; public constructor( + timeRangeService: TimeRangeService, + graphQlQueryService: GraphQlRequestService, + iconLookupService: EntityIconLookupService, private readonly navigationService: NavigationService, - private readonly timeRangeService: TimeRangeService, - private readonly graphQlQueryService: GraphQlRequestService, protected readonly breadcrumbService: BreadcrumbsService, @Inject(ENTITY_METADATA) private readonly entityMetadataMap: EntityMetadataMap ) { + super(timeRangeService, graphQlQueryService, iconLookupService); this.apiEntityMetadata = this.entityMetadataMap.get(ObservabilityEntityType.Api); } public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise> { const id = activatedRouteSnapshot.paramMap.get('id') as string; - const parentType = this.resolveParentType(); + const parentEntityMetadata = this.resolveParentType(); return Promise.resolve( - this.fetchEntity(id, parentType).pipe( - take(1), + this.fetchEntity(id, ObservabilityEntityType.Api).pipe( + map(apiEntity => ({ + ...apiEntity, + ...this.getParentPartial(apiEntity, parentEntityMetadata) + })), switchMap(api => [ - ...this.getParentBreadcrumbs(api, parentType), + ...this.getParentBreadcrumbs(api, parentEntityMetadata), this.createBreadcrumbForEntity(api, activatedRouteSnapshot) ]) ) ); } - protected createBreadcrumbForEntity( - api: ApiBreadcrumbDetails, - activatedRouteSnapshot: ActivatedRouteSnapshot - ): Breadcrumb { + protected createBreadcrumbForEntity(api: Entity, activatedRouteSnapshot: ActivatedRouteSnapshot): EntityBreadcrumb { return { - label: api.name, + ...api, + label: api.name as string, icon: this.apiEntityMetadata?.icon, url: this.breadcrumbService.getPath(activatedRouteSnapshot) }; } - protected getParentBreadcrumbs(api: ApiBreadcrumbDetails, parentEntityMetadata?: EntityMetadata): Breadcrumb[] { + protected getParentBreadcrumbs( + api: EntityBreadcrumb, + parentEntityMetadata?: EntityMetadata + ): (EntityBreadcrumb | Breadcrumb)[] { return parentEntityMetadata !== undefined ? [ { - label: api.parentName, + [entityIdKey]: api.parentId as string, + [entityTypeKey]: parentEntityMetadata.entityType, + label: api.parentName as string, icon: parentEntityMetadata?.icon, - url: parentEntityMetadata?.detailPath(api.parentId) + url: parentEntityMetadata?.detailPath(api.parentId as string) }, { label: 'Endpoints', icon: this.apiEntityMetadata?.icon, - url: parentEntityMetadata?.apisListPath?.(api.parentId) + url: parentEntityMetadata?.apisListPath?.(api.parentId as string) } ] : []; } - private fetchEntity(id: string, parentEntityMetadata?: EntityMetadata): Observable { - return this.timeRangeService.getTimeRangeAndChanges().pipe( - switchMap(timeRange => - this.graphQlQueryService.query( - { - requestType: ENTITY_GQL_REQUEST, - entityType: ObservabilityEntityType.Api, - id: id, - properties: this.getAttributeKeys(parentEntityMetadata).map(attributeKey => - this.specificationBuilder.attributeSpecificationForKey(attributeKey) - ), - timeRange: new GraphQlTimeRange(timeRange.startTime, timeRange.endTime) - }, - { cacheability: GraphQlRequestCacheability.NotCacheable } - ) - ), - map(apiEntity => ({ - ...apiEntity, - ...this.getParentPartial(apiEntity, parentEntityMetadata) - })) - ); - } - - private getAttributeKeys(parentTypeMetadata?: EntityMetadata): string[] { + protected getAttributeKeys(): string[] { + const parentTypeMetadata = this.resolveParentType(); const parentAttributes = parentTypeMetadata ? [this.getParentNameAttribute(parentTypeMetadata), this.getParentIdAttribute(parentTypeMetadata)] : []; @@ -142,7 +127,7 @@ export class ApiDetailBreadcrumbResolver implements Resolve { +export interface ApiBreadcrumbDetails extends EntityBreadcrumb { name: string; parentName: string; parentId: string; diff --git a/projects/observability/src/pages/apis/api-detail/api-detail.service.ts b/projects/observability/src/pages/apis/api-detail/api-detail.service.ts index 1d5a1aca9..4c3326743 100644 --- a/projects/observability/src/pages/apis/api-detail/api-detail.service.ts +++ b/projects/observability/src/pages/apis/api-detail/api-detail.service.ts @@ -20,6 +20,7 @@ export class ApiDetailService extends EntityDetailService { export interface ApiEntity extends Entity { apiType: ApiType; + name: string; } export const enum ApiType { diff --git a/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.test.ts b/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.test.ts index d8da9b2f9..0dba0f384 100644 --- a/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.test.ts +++ b/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.test.ts @@ -10,10 +10,11 @@ import { entityIdKey, entityTypeKey, ObservabilityEntityType } from '../../../sh import { ENTITY_GQL_REQUEST } from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service'; import { ObservabilityIconType } from '../../../shared/icons/observability-icon-type'; import { EntityIconLookupService } from '../../../shared/services/entity/entity-icon-lookup.service'; +import { EntityBreadcrumb } from './../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver'; import { ServiceDetailBreadcrumbResolver } from './service-detail-breadcrumb.resolver'; describe('Service detail breadcrumb resolver', () => { - let spectator: SpectatorService; + let spectator: SpectatorService>; let activatedRouteSnapshot: ActivatedRouteSnapshot; const buildResolver = createServiceFactory({ service: ServiceDetailBreadcrumbResolver, @@ -55,8 +56,11 @@ describe('Service detail breadcrumb resolver', () => { runFakeRxjs(({ expectObservable }) => { expectObservable(breadcrumb$).toBe('(x|)', { x: { + [entityTypeKey]: ObservabilityEntityType.Service, + [entityIdKey]: 'test-id', label: 'test service', - icon: ObservabilityIconType.Service + icon: ObservabilityIconType.Service, + name: 'test service' } }); }); @@ -69,7 +73,7 @@ describe('Service detail breadcrumb resolver', () => { entityType: ObservabilityEntityType.Service, id: 'test-id' }), - { cacheability: GraphQlRequestCacheability.NotCacheable } + { cacheability: GraphQlRequestCacheability.Cacheable } ); })); }); diff --git a/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.ts b/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.ts index eb5c0d848..7a1667d8b 100644 --- a/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.ts +++ b/projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.ts @@ -1,60 +1,29 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; -import { Breadcrumb, TimeRangeService } from '@hypertrace/common'; -import { GraphQlRequestCacheability, GraphQlRequestService } from '@hypertrace/graphql-client'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { TimeRangeService } from '@hypertrace/common'; +import { GraphQlRequestService } from '@hypertrace/graphql-client'; import { Observable } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; import { ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity'; -import { GraphQlTimeRange } from '../../../shared/graphql/model/schema/timerange/graphql-time-range'; -import { SpecificationBuilder } from '../../../shared/graphql/request/builders/specification/specification-builder'; import { - EntityGraphQlQueryHandlerService, - ENTITY_GQL_REQUEST -} from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service'; -import { EntityIconLookupService } from '../../../shared/services/entity/entity-icon-lookup.service'; -import { ServiceEntity } from './service-detail.service'; + EntityBreadcrumb, + EntityBreadcrumbResolver +} from '../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver'; +import { EntityIconLookupService } from './../../../shared/services/entity/entity-icon-lookup.service'; @Injectable({ providedIn: 'root' }) -export class ServiceDetailBreadcrumbResolver implements Resolve> { - private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder(); - +export class ServiceDetailBreadcrumbResolver extends EntityBreadcrumbResolver { public constructor( - private readonly timeRangeService: TimeRangeService, - private readonly graphQlQueryService: GraphQlRequestService, - protected readonly iconLookupService: EntityIconLookupService - ) {} + timeRangeService: TimeRangeService, + graphQlQueryService: GraphQlRequestService, + iconLookupService: EntityIconLookupService + ) { + super(timeRangeService, graphQlQueryService, iconLookupService); + } - public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise> { + public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise> { const id = activatedRouteSnapshot.paramMap.get('id'); - return Promise.resolve( - this.fetchEntity(id as string).pipe( - take(1), - map(service => ({ - label: service.name, - icon: this.iconLookupService.forEntity(service) - })) - ) - ); - } - - protected fetchEntity(id: string): Observable { - return this.timeRangeService.getTimeRangeAndChanges().pipe( - switchMap(timeRange => - this.graphQlQueryService.query( - { - requestType: ENTITY_GQL_REQUEST, - entityType: ObservabilityEntityType.Service, - id: id, - properties: this.getAttributeKeys().map(attributeKey => - this.specificationBuilder.attributeSpecificationForKey(attributeKey) - ), - timeRange: new GraphQlTimeRange(timeRange.startTime, timeRange.endTime) - }, - { cacheability: GraphQlRequestCacheability.NotCacheable } - ) - ) - ); + return Promise.resolve(this.fetchEntity(id as string, ObservabilityEntityType.Service)); } protected getAttributeKeys(): string[] { diff --git a/projects/observability/src/public-api.ts b/projects/observability/src/public-api.ts index e4e9d0bae..5b746d229 100644 --- a/projects/observability/src/public-api.ts +++ b/projects/observability/src/public-api.ts @@ -92,6 +92,7 @@ export * from './shared/graphql/model/schema/trace'; // Services export * from './pages/trace-detail/trace-detail.service'; export * from './shared/services/log-events/log-events.service'; +export * from './shared/services/entity-breadcrumb/entity-breadcrumb.resolver'; // Span Detail export { SpanData } from './shared/components/span-detail/span-data'; diff --git a/projects/observability/src/shared/services/entity-breadcrumb/entity-breadcrumb.resolver.ts b/projects/observability/src/shared/services/entity-breadcrumb/entity-breadcrumb.resolver.ts new file mode 100644 index 000000000..36b64fa28 --- /dev/null +++ b/projects/observability/src/shared/services/entity-breadcrumb/entity-breadcrumb.resolver.ts @@ -0,0 +1,66 @@ +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { Breadcrumb, TimeRangeService } from '@hypertrace/common'; +import { GraphQlRequestCacheability, GraphQlRequestOptions, GraphQlRequestService } from '@hypertrace/graphql-client'; +import { Observable } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; +import { Entity } from '../../graphql/model/schema/entity'; +import { GraphQlTimeRange } from '../../graphql/model/schema/timerange/graphql-time-range'; +import { SpecificationBuilder } from '../../graphql/request/builders/specification/specification-builder'; +import { EntityGraphQlQueryHandlerService } from '../../graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service'; +import { EntityIconLookupService } from '../entity/entity-icon-lookup.service'; +import { Specification } from './../../graphql/model/schema/specifier/specification'; +import { ENTITY_GQL_REQUEST } from './../../graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service'; + +export abstract class EntityBreadcrumbResolver + implements Resolve> { + private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder(); + + public constructor( + protected readonly timeRangeService: TimeRangeService, + protected readonly graphQlQueryService: GraphQlRequestService, + protected readonly iconLookupService: EntityIconLookupService + ) {} + + public abstract resolve(route: ActivatedRouteSnapshot): Promise>; + + protected abstract getAttributeKeys(): string[]; + + protected getAdditionalSpecifications(): Specification[] { + return []; + } + + protected getRequestOptions(): GraphQlRequestOptions | undefined { + return { cacheability: GraphQlRequestCacheability.Cacheable }; + } + + protected fetchEntity(id: string, entityType: string): Observable { + return this.timeRangeService.getTimeRangeAndChanges().pipe( + switchMap(timeRange => + this.graphQlQueryService.query( + { + requestType: ENTITY_GQL_REQUEST, + entityType: entityType, + id: id, + properties: [...this.getAttributeSpecification(), ...this.getAdditionalSpecifications()], + timeRange: new GraphQlTimeRange(timeRange.startTime, timeRange.endTime) + }, + this.getRequestOptions() + ) + ), + map(entity => ({ + ...entity, + label: entity.name as string, + icon: this.iconLookupService.forEntity(entity) + })), + take(1) + ); + } + + private getAttributeSpecification(): Specification[] { + return this.getAttributeKeys().map(attributeKey => + this.specificationBuilder.attributeSpecificationForKey(attributeKey) + ); + } +} + +export interface EntityBreadcrumb extends Breadcrumb, Entity {}