Skip to content

Commit b1d8250

Browse files
authored
refactor: breadcrumb to support additional specifications (#1254)
* refactor: breadcrumb to support additional specifications * refactor: fixing lint * refactor: fix test * refactor: addressing review comments * refactor: fixing test * refactor: fixing lint * refactor: addressing review comments * refactor: fixing test * refactor: update breadcrumb and fix tests
1 parent a9293f3 commit b1d8250

File tree

8 files changed

+148
-107
lines changed

8 files changed

+148
-107
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"build:components": "ng build components",
1616
"build:dashboards": "ng build dashboards",
1717
"build:ci": "node --max_old_space_size=3584 node_modules/@angular/cli/bin/ng build --configuration production --no-progress",
18-
"test": "ng test hypertrace-ui --cache",
18+
"test": "ng test hypertrace-ui --cache --maxWorkers=2",
1919
"lint": "ng lint hypertrace-ui",
2020
"lint:fix": "ng lint --fix hypertrace-ui",
2121
"prettier:check": "prettier --check '**'",

projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.test.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing';
44
import { NavigationService } from '@hypertrace/common';
55
import { GraphQlRequestCacheability, GraphQlRequestService } from '@hypertrace/graphql-client';
66
import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest';
7+
import { EntityBreadcrumb } from './../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
78

89
import { patchRouterNavigateForTest, runFakeRxjs } from '@hypertrace/test-utils';
910
import { of } from 'rxjs';
@@ -14,7 +15,7 @@ import { ObservabilityIconType } from '../../../shared/icons/observability-icon-
1415
import { ApiDetailBreadcrumbResolver } from './api-detail-breadcrumb.resolver';
1516

1617
describe('Api detail breadcrumb resolver', () => {
17-
let spectator: SpectatorService<ApiDetailBreadcrumbResolver>;
18+
let spectator: SpectatorService<ApiDetailBreadcrumbResolver<EntityBreadcrumb>>;
1819
let activatedRouteSnapshot: ActivatedRouteSnapshot;
1920
const buildResolver = createServiceFactory({
2021
service: ApiDetailBreadcrumbResolver,
@@ -83,6 +84,8 @@ describe('Api detail breadcrumb resolver', () => {
8384
runFakeRxjs(({ expectObservable }) => {
8485
expectObservable(breadcrumb$).toBe('(abc|)', {
8586
a: {
87+
[entityIdKey]: 'test-service-id',
88+
[entityTypeKey]: ObservabilityEntityType.Service,
8689
label: 'test service',
8790
icon: ObservabilityIconType.Service,
8891
url: ['services', 'service', 'test-service-id']
@@ -93,9 +96,16 @@ describe('Api detail breadcrumb resolver', () => {
9396
url: ['services', 'service', 'test-service-id', 'endpoints']
9497
},
9598
c: {
99+
[entityIdKey]: 'test-id',
100+
[entityTypeKey]: ObservabilityEntityType.Api,
96101
label: 'test api',
97102
icon: ObservabilityIconType.Api,
98-
url: ['api', 'test-id']
103+
url: ['api', 'test-id'],
104+
name: 'test api',
105+
parentId: 'test-service-id',
106+
parentName: 'test service',
107+
serviceName: 'test service',
108+
serviceId: 'test-service-id'
99109
}
100110
});
101111
});
@@ -108,7 +118,7 @@ describe('Api detail breadcrumb resolver', () => {
108118
entityType: ObservabilityEntityType.Api,
109119
id: 'test-id'
110120
}),
111-
{ cacheability: GraphQlRequestCacheability.NotCacheable }
121+
{ cacheability: GraphQlRequestCacheability.Cacheable }
112122
);
113123
}));
114124

@@ -122,9 +132,14 @@ describe('Api detail breadcrumb resolver', () => {
122132
runFakeRxjs(({ expectObservable }) => {
123133
expectObservable(breadcrumb$).toBe('(y|)', {
124134
y: {
135+
[entityIdKey]: 'test-id',
136+
[entityTypeKey]: ObservabilityEntityType.Api,
125137
label: 'test api',
126138
icon: ObservabilityIconType.Api,
127-
url: ['api', 'test-id']
139+
url: ['api', 'test-id'],
140+
name: 'test api',
141+
serviceName: 'test service',
142+
serviceId: 'test-service-id'
128143
}
129144
});
130145
});
@@ -137,7 +152,7 @@ describe('Api detail breadcrumb resolver', () => {
137152
entityType: ObservabilityEntityType.Api,
138153
id: 'test-id'
139154
}),
140-
{ cacheability: GraphQlRequestCacheability.NotCacheable }
155+
{ cacheability: GraphQlRequestCacheability.Cacheable }
141156
);
142157
}));
143158
});

projects/observability/src/pages/apis/api-detail/api-detail-breadcrumb.resolver.ts

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,86 @@
11
import { Inject, Injectable } from '@angular/core';
2-
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
2+
import { ActivatedRouteSnapshot } from '@angular/router';
33
import { Breadcrumb, NavigationService, TimeRangeService } from '@hypertrace/common';
44
import { BreadcrumbsService } from '@hypertrace/components';
5-
import { GraphQlRequestCacheability, GraphQlRequestService } from '@hypertrace/graphql-client';
5+
import { GraphQlRequestService } from '@hypertrace/graphql-client';
6+
import { entityIdKey } from '@hypertrace/observability';
67
import { Observable } from 'rxjs';
7-
import { map, switchMap, take } from 'rxjs/operators';
8+
import { map, switchMap } from 'rxjs/operators';
89
import { EntityMetadata, EntityMetadataMap, ENTITY_METADATA } from '../../../shared/constants/entity-metadata';
9-
import { Entity, ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity';
10-
import { GraphQlTimeRange } from '../../../shared/graphql/model/schema/timerange/graphql-time-range';
11-
import { SpecificationBuilder } from '../../../shared/graphql/request/builders/specification/specification-builder';
10+
import { Entity, entityTypeKey, ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity';
1211
import {
13-
EntityGraphQlQueryHandlerService,
14-
ENTITY_GQL_REQUEST
15-
} from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service';
12+
EntityBreadcrumb,
13+
EntityBreadcrumbResolver
14+
} from '../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
15+
import { EntityIconLookupService } from './../../../shared/services/entity/entity-icon-lookup.service';
1616

1717
@Injectable({ providedIn: 'root' })
18-
export class ApiDetailBreadcrumbResolver implements Resolve<Observable<Breadcrumb>> {
19-
private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder();
18+
export class ApiDetailBreadcrumbResolver<T extends EntityBreadcrumb> extends EntityBreadcrumbResolver<T> {
2019
protected readonly apiEntityMetadata: EntityMetadata | undefined;
2120

2221
public constructor(
22+
timeRangeService: TimeRangeService,
23+
graphQlQueryService: GraphQlRequestService,
24+
iconLookupService: EntityIconLookupService,
2325
private readonly navigationService: NavigationService,
24-
private readonly timeRangeService: TimeRangeService,
25-
private readonly graphQlQueryService: GraphQlRequestService,
2626
protected readonly breadcrumbService: BreadcrumbsService,
2727
@Inject(ENTITY_METADATA) private readonly entityMetadataMap: EntityMetadataMap
2828
) {
29+
super(timeRangeService, graphQlQueryService, iconLookupService);
2930
this.apiEntityMetadata = this.entityMetadataMap.get(ObservabilityEntityType.Api);
3031
}
3132

3233
public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise<Observable<Breadcrumb>> {
3334
const id = activatedRouteSnapshot.paramMap.get('id') as string;
34-
const parentType = this.resolveParentType();
35+
const parentEntityMetadata = this.resolveParentType();
3536

3637
return Promise.resolve(
37-
this.fetchEntity(id, parentType).pipe(
38-
take(1),
38+
this.fetchEntity(id, ObservabilityEntityType.Api).pipe(
39+
map(apiEntity => ({
40+
...apiEntity,
41+
...this.getParentPartial(apiEntity, parentEntityMetadata)
42+
})),
3943
switchMap(api => [
40-
...this.getParentBreadcrumbs(api, parentType),
44+
...this.getParentBreadcrumbs(api, parentEntityMetadata),
4145
this.createBreadcrumbForEntity(api, activatedRouteSnapshot)
4246
])
4347
)
4448
);
4549
}
4650

47-
protected createBreadcrumbForEntity(
48-
api: ApiBreadcrumbDetails,
49-
activatedRouteSnapshot: ActivatedRouteSnapshot
50-
): Breadcrumb {
51+
protected createBreadcrumbForEntity(api: Entity, activatedRouteSnapshot: ActivatedRouteSnapshot): EntityBreadcrumb {
5152
return {
52-
label: api.name,
53+
...api,
54+
label: api.name as string,
5355
icon: this.apiEntityMetadata?.icon,
5456
url: this.breadcrumbService.getPath(activatedRouteSnapshot)
5557
};
5658
}
5759

58-
protected getParentBreadcrumbs(api: ApiBreadcrumbDetails, parentEntityMetadata?: EntityMetadata): Breadcrumb[] {
60+
protected getParentBreadcrumbs(
61+
api: EntityBreadcrumb,
62+
parentEntityMetadata?: EntityMetadata
63+
): (EntityBreadcrumb | Breadcrumb)[] {
5964
return parentEntityMetadata !== undefined
6065
? [
6166
{
62-
label: api.parentName,
67+
[entityIdKey]: api.parentId as string,
68+
[entityTypeKey]: parentEntityMetadata.entityType,
69+
label: api.parentName as string,
6370
icon: parentEntityMetadata?.icon,
64-
url: parentEntityMetadata?.detailPath(api.parentId)
71+
url: parentEntityMetadata?.detailPath(api.parentId as string)
6572
},
6673
{
6774
label: 'Endpoints',
6875
icon: this.apiEntityMetadata?.icon,
69-
url: parentEntityMetadata?.apisListPath?.(api.parentId)
76+
url: parentEntityMetadata?.apisListPath?.(api.parentId as string)
7077
}
7178
]
7279
: [];
7380
}
7481

75-
private fetchEntity(id: string, parentEntityMetadata?: EntityMetadata): Observable<ApiBreadcrumbDetails> {
76-
return this.timeRangeService.getTimeRangeAndChanges().pipe(
77-
switchMap(timeRange =>
78-
this.graphQlQueryService.query<EntityGraphQlQueryHandlerService, ApiBreadcrumbDetails>(
79-
{
80-
requestType: ENTITY_GQL_REQUEST,
81-
entityType: ObservabilityEntityType.Api,
82-
id: id,
83-
properties: this.getAttributeKeys(parentEntityMetadata).map(attributeKey =>
84-
this.specificationBuilder.attributeSpecificationForKey(attributeKey)
85-
),
86-
timeRange: new GraphQlTimeRange(timeRange.startTime, timeRange.endTime)
87-
},
88-
{ cacheability: GraphQlRequestCacheability.NotCacheable }
89-
)
90-
),
91-
map(apiEntity => ({
92-
...apiEntity,
93-
...this.getParentPartial(apiEntity, parentEntityMetadata)
94-
}))
95-
);
96-
}
97-
98-
private getAttributeKeys(parentTypeMetadata?: EntityMetadata): string[] {
82+
protected getAttributeKeys(): string[] {
83+
const parentTypeMetadata = this.resolveParentType();
9984
const parentAttributes = parentTypeMetadata
10085
? [this.getParentNameAttribute(parentTypeMetadata), this.getParentIdAttribute(parentTypeMetadata)]
10186
: [];
@@ -142,7 +127,7 @@ export class ApiDetailBreadcrumbResolver implements Resolve<Observable<Breadcrum
142127
}
143128
}
144129

145-
export interface ApiBreadcrumbDetails extends Entity<ObservabilityEntityType.Api> {
130+
export interface ApiBreadcrumbDetails extends EntityBreadcrumb {
146131
name: string;
147132
parentName: string;
148133
parentId: string;

projects/observability/src/pages/apis/api-detail/api-detail.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class ApiDetailService extends EntityDetailService<ApiEntity> {
2020

2121
export interface ApiEntity extends Entity {
2222
apiType: ApiType;
23+
name: string;
2324
}
2425

2526
export const enum ApiType {

projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import { entityIdKey, entityTypeKey, ObservabilityEntityType } from '../../../sh
1010
import { ENTITY_GQL_REQUEST } from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service';
1111
import { ObservabilityIconType } from '../../../shared/icons/observability-icon-type';
1212
import { EntityIconLookupService } from '../../../shared/services/entity/entity-icon-lookup.service';
13+
import { EntityBreadcrumb } from './../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
1314
import { ServiceDetailBreadcrumbResolver } from './service-detail-breadcrumb.resolver';
1415

1516
describe('Service detail breadcrumb resolver', () => {
16-
let spectator: SpectatorService<ServiceDetailBreadcrumbResolver>;
17+
let spectator: SpectatorService<ServiceDetailBreadcrumbResolver<EntityBreadcrumb>>;
1718
let activatedRouteSnapshot: ActivatedRouteSnapshot;
1819
const buildResolver = createServiceFactory({
1920
service: ServiceDetailBreadcrumbResolver,
@@ -55,8 +56,11 @@ describe('Service detail breadcrumb resolver', () => {
5556
runFakeRxjs(({ expectObservable }) => {
5657
expectObservable(breadcrumb$).toBe('(x|)', {
5758
x: {
59+
[entityTypeKey]: ObservabilityEntityType.Service,
60+
[entityIdKey]: 'test-id',
5861
label: 'test service',
59-
icon: ObservabilityIconType.Service
62+
icon: ObservabilityIconType.Service,
63+
name: 'test service'
6064
}
6165
});
6266
});
@@ -69,7 +73,7 @@ describe('Service detail breadcrumb resolver', () => {
6973
entityType: ObservabilityEntityType.Service,
7074
id: 'test-id'
7175
}),
72-
{ cacheability: GraphQlRequestCacheability.NotCacheable }
76+
{ cacheability: GraphQlRequestCacheability.Cacheable }
7377
);
7478
}));
7579
});

projects/observability/src/pages/apis/service-detail/service-detail-breadcrumb.resolver.ts

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,29 @@
11
import { Injectable } from '@angular/core';
2-
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
3-
import { Breadcrumb, TimeRangeService } from '@hypertrace/common';
4-
import { GraphQlRequestCacheability, GraphQlRequestService } from '@hypertrace/graphql-client';
2+
import { ActivatedRouteSnapshot } from '@angular/router';
3+
import { TimeRangeService } from '@hypertrace/common';
4+
import { GraphQlRequestService } from '@hypertrace/graphql-client';
55
import { Observable } from 'rxjs';
6-
import { map, switchMap, take } from 'rxjs/operators';
76
import { ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity';
8-
import { GraphQlTimeRange } from '../../../shared/graphql/model/schema/timerange/graphql-time-range';
9-
import { SpecificationBuilder } from '../../../shared/graphql/request/builders/specification/specification-builder';
107
import {
11-
EntityGraphQlQueryHandlerService,
12-
ENTITY_GQL_REQUEST
13-
} from '../../../shared/graphql/request/handlers/entities/query/entity/entity-graphql-query-handler.service';
14-
import { EntityIconLookupService } from '../../../shared/services/entity/entity-icon-lookup.service';
15-
import { ServiceEntity } from './service-detail.service';
8+
EntityBreadcrumb,
9+
EntityBreadcrumbResolver
10+
} from '../../../shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
11+
import { EntityIconLookupService } from './../../../shared/services/entity/entity-icon-lookup.service';
1612

1713
@Injectable({ providedIn: 'root' })
18-
export class ServiceDetailBreadcrumbResolver implements Resolve<Observable<Breadcrumb>> {
19-
private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder();
20-
14+
export class ServiceDetailBreadcrumbResolver<T extends EntityBreadcrumb> extends EntityBreadcrumbResolver<T> {
2115
public constructor(
22-
private readonly timeRangeService: TimeRangeService,
23-
private readonly graphQlQueryService: GraphQlRequestService,
24-
protected readonly iconLookupService: EntityIconLookupService
25-
) {}
16+
timeRangeService: TimeRangeService,
17+
graphQlQueryService: GraphQlRequestService,
18+
iconLookupService: EntityIconLookupService
19+
) {
20+
super(timeRangeService, graphQlQueryService, iconLookupService);
21+
}
2622

27-
public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise<Observable<Breadcrumb>> {
23+
public async resolve(activatedRouteSnapshot: ActivatedRouteSnapshot): Promise<Observable<T>> {
2824
const id = activatedRouteSnapshot.paramMap.get('id');
2925

30-
return Promise.resolve(
31-
this.fetchEntity(id as string).pipe(
32-
take(1),
33-
map(service => ({
34-
label: service.name,
35-
icon: this.iconLookupService.forEntity(service)
36-
}))
37-
)
38-
);
39-
}
40-
41-
protected fetchEntity(id: string): Observable<ServiceEntity> {
42-
return this.timeRangeService.getTimeRangeAndChanges().pipe(
43-
switchMap(timeRange =>
44-
this.graphQlQueryService.query<EntityGraphQlQueryHandlerService, ServiceEntity>(
45-
{
46-
requestType: ENTITY_GQL_REQUEST,
47-
entityType: ObservabilityEntityType.Service,
48-
id: id,
49-
properties: this.getAttributeKeys().map(attributeKey =>
50-
this.specificationBuilder.attributeSpecificationForKey(attributeKey)
51-
),
52-
timeRange: new GraphQlTimeRange(timeRange.startTime, timeRange.endTime)
53-
},
54-
{ cacheability: GraphQlRequestCacheability.NotCacheable }
55-
)
56-
)
57-
);
26+
return Promise.resolve(this.fetchEntity(id as string, ObservabilityEntityType.Service));
5827
}
5928

6029
protected getAttributeKeys(): string[] {

projects/observability/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export * from './shared/graphql/model/schema/trace';
9292
// Services
9393
export * from './pages/trace-detail/trace-detail.service';
9494
export * from './shared/services/log-events/log-events.service';
95+
export * from './shared/services/entity-breadcrumb/entity-breadcrumb.resolver';
9596

9697
// Span Detail
9798
export { SpanData } from './shared/components/span-detail/span-data';

0 commit comments

Comments
 (0)