Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 '**'",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ 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 { ApiDetailBreadcrumbResolver } from './api-detail-breadcrumb.resolver';
import { ApiEntity } from './api-detail.service';

describe('Api detail breadcrumb resolver', () => {
let spectator: SpectatorService<ApiDetailBreadcrumbResolver>;
let spectator: SpectatorService<ApiDetailBreadcrumbResolver<ApiEntity>>;
let activatedRouteSnapshot: ActivatedRouteSnapshot;
const buildResolver = createServiceFactory({
service: ApiDetailBreadcrumbResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,101 +1,78 @@
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 { 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 {
EntityGraphQlQueryHandlerService,
ENTITY_GQL_REQUEST
} from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service';
import { EntityBreadcrumbResolver } from '../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
import { EntityIconLookupService } from './../../../shared/services/entity/entity-icon-lookup.service';
import { ApiEntity } from './api-detail.service';

@Injectable({ providedIn: 'root' })
export class ApiDetailBreadcrumbResolver implements Resolve<Observable<Breadcrumb>> {
private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder();
export class ApiDetailBreadcrumbResolver<T extends ApiEntity> extends EntityBreadcrumbResolver<T> {
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<Observable<Breadcrumb>> {
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: T & Breadcrumb, activatedRouteSnapshot: ActivatedRouteSnapshot): ApiEntity {
return {
...api,
label: api.name,
icon: this.apiEntityMetadata?.icon,
url: this.breadcrumbService.getPath(activatedRouteSnapshot)
};
}

protected getParentBreadcrumbs(api: ApiBreadcrumbDetails, parentEntityMetadata?: EntityMetadata): Breadcrumb[] {
protected getParentBreadcrumbs(api: T & Breadcrumb, parentEntityMetadata?: EntityMetadata): Breadcrumb[] {
return parentEntityMetadata !== undefined
? [
{
label: api.parentName,
label: api.parentName as string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

types are wrong here, shouldn't need to cast

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ts is not able to detect the types in the pipe -> map operator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the type on line 42? Are we sure we're not just dropping it on 59? That map should produce a T & Partial<Pick<ApiBreadcrumbDetails, 'parentName' | 'parentId'>> since we're spreading them together (that partial could use a type alias!).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit btw)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I am not able to make it work. IMO, getParentPartial is making the parentName and parentId optional and then we are required fields in ApiBreadcrumbDetails.

We wouldn't have to cast if we can ensure that following code returns an ApiBreadcrumbDetails object

 map(apiEntity => ({
          ...apiEntity,
          ...this.getParentPartial(apiEntity, parentEntityMetadata)
        })),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that by assigning '' instead of undefined but i don't know if it would create any run time issue. So, I am ignoring it for this change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could just make those two fields optional on ApiBreadcrumbDetails I think? They are optional, they're omitted if we can't resolve them, hence the check on line 60 before we access them.

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<ApiBreadcrumbDetails> {
return this.timeRangeService.getTimeRangeAndChanges().pipe(
switchMap(timeRange =>
this.graphQlQueryService.query<EntityGraphQlQueryHandlerService, ApiBreadcrumbDetails>(
{
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)]
: [];
Expand Down Expand Up @@ -142,7 +119,7 @@ export class ApiDetailBreadcrumbResolver implements Resolve<Observable<Breadcrum
}
}

export interface ApiBreadcrumbDetails extends Entity<ObservabilityEntityType.Api> {
export interface ApiBreadcrumbDetails extends ApiEntity {
name: string;
parentName: string;
parentId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { Breadcrumb } from '@hypertrace/common';
import { Entity, ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity';
import { EntityDetailService } from '../../../shared/services/entity/entity-detail.service';

Expand All @@ -18,8 +19,9 @@ export class ApiDetailService extends EntityDetailService<ApiEntity> {
}
}

export interface ApiEntity extends Entity {
export interface ApiEntity extends Entity, Breadcrumb {
apiType: ApiType;
name: string;
}

export const enum ApiType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { ENTITY_GQL_REQUEST } from '../../../shared/graphql/request/handlers/ent
import { ObservabilityIconType } from '../../../shared/icons/observability-icon-type';
import { EntityIconLookupService } from '../../../shared/services/entity/entity-icon-lookup.service';
import { ServiceDetailBreadcrumbResolver } from './service-detail-breadcrumb.resolver';
import { ServiceEntity } from './service-detail.service';

describe('Service detail breadcrumb resolver', () => {
let spectator: SpectatorService<ServiceDetailBreadcrumbResolver>;
let spectator: SpectatorService<ServiceDetailBreadcrumbResolver<ServiceEntity>>;
let activatedRouteSnapshot: ActivatedRouteSnapshot;
const buildResolver = createServiceFactory({
service: ServiceDetailBreadcrumbResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,27 @@
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 { EntityBreadcrumbResolver } from '../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
import { EntityIconLookupService } from './../../../shared/services/entity/entity-icon-lookup.service';
import { ServiceEntity } from './service-detail.service';

@Injectable({ providedIn: 'root' })
export class ServiceDetailBreadcrumbResolver implements Resolve<Observable<Breadcrumb>> {
private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder();

export class ServiceDetailBreadcrumbResolver<T extends ServiceEntity> extends EntityBreadcrumbResolver<T> {
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<Observable<Breadcrumb>> {
public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise<Observable<T>> {
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<ServiceEntity> {
return this.timeRangeService.getTimeRangeAndChanges().pipe(
switchMap(timeRange =>
this.graphQlQueryService.query<EntityGraphQlQueryHandlerService, ServiceEntity>(
{
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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { Breadcrumb } from '@hypertrace/common';
import { Entity, ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity';
import { EntityDetailService } from '../../../shared/services/entity/entity-detail.service';

Expand All @@ -19,6 +20,6 @@ export class ServiceDetailService extends EntityDetailService<ServiceEntity> {
}
}

export interface ServiceEntity extends Entity {
export interface ServiceEntity extends Entity, Breadcrumb {
name: string;
}
1 change: 1 addition & 0 deletions projects/observability/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { Breadcrumb, TimeRangeService } from '@hypertrace/common';
import { GraphQlRequestCacheability, 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<T extends Entity> implements Resolve<Observable<Breadcrumb>> {
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<Observable<Breadcrumb>>;

protected abstract getAttributeKeys(): string[];

protected getAdditionalSpecifications(): Specification[] {
return [];
}

protected fetchEntity(id: string, entityType: string): Observable<T & Breadcrumb> {
return this.timeRangeService.getTimeRangeAndChanges().pipe(
switchMap(timeRange =>
this.graphQlQueryService.query<EntityGraphQlQueryHandlerService, T>(
{
requestType: ENTITY_GQL_REQUEST,
entityType: entityType,
id: id,
properties: [...this.getAttributeSpecification(), ...this.getAdditionalSpecifications()],
timeRange: new GraphQlTimeRange(timeRange.startTime, timeRange.endTime)
},
{ cacheability: GraphQlRequestCacheability.NotCacheable }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these not cacheable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because we can do crud operations on entities -> labels update and probably rate limits

Copy link
Contributor

@aaron-steinfeld aaron-steinfeld Nov 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the main thing remaining. Labels would be the only thing that could affect it, but never caching breadcrumbs is overkill. Could we pull this out into a protected method as a starting point, but leave it cacheable - we can go from there and make decisions per breadcrumb resolver as needed.

)
),
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)
);
}
}