From 7f0dcae64e5928c9ce4aa34a7655af534b211026 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Thu, 20 May 2021 19:44:04 +0530 Subject: [PATCH 01/11] feat: download json component --- .../download-json.component.scss | 9 +++ .../download-json.component.test.ts | 31 ++++++++++ .../download-json/download-json.component.ts | 59 +++++++++++++++++++ .../src/download-json/download-json.module.ts | 13 ++++ projects/components/src/public-api.ts | 4 ++ 5 files changed, 116 insertions(+) create mode 100644 projects/components/src/download-json/download-json.component.scss create mode 100644 projects/components/src/download-json/download-json.component.test.ts create mode 100644 projects/components/src/download-json/download-json.component.ts create mode 100644 projects/components/src/download-json/download-json.module.ts diff --git a/projects/components/src/download-json/download-json.component.scss b/projects/components/src/download-json/download-json.component.scss new file mode 100644 index 000000000..652ed3456 --- /dev/null +++ b/projects/components/src/download-json/download-json.component.scss @@ -0,0 +1,9 @@ +@import 'color-palette'; + +.download-json { + width: 40px; + height: 40px; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/projects/components/src/download-json/download-json.component.test.ts b/projects/components/src/download-json/download-json.component.test.ts new file mode 100644 index 000000000..bcb7797be --- /dev/null +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -0,0 +1,31 @@ +import { RouterTestingModule } from '@angular/router/testing'; +import { createHostFactory, Spectator } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; +import { Observable, of } from 'rxjs'; +import { ButtonComponent } from '../button/button.component'; +import { IconComponent } from '../icon/icon.component'; +import { DownloadJsonComponent } from './download-json.component'; +import { DownloadJsonModule } from './download-json.module'; + +describe('Button Component', () => { + let spectator: Spectator; + + const createHost = createHostFactory({ + component: DownloadJsonComponent, + imports: [DownloadJsonModule, RouterTestingModule], + declarations: [MockComponent(ButtonComponent), MockComponent(IconComponent)], + shallow: true + }); + + const dataSource$: Observable = of('{}'); + + test('should have only download button, when data is not loading', () => { + spectator = createHost(``, { + hostProps: { + dataSource: dataSource$ + } + }); + + expect(spectator.query(ButtonComponent)).toExist(); + }); +}); diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts new file mode 100644 index 000000000..815df1295 --- /dev/null +++ b/projects/components/src/download-json/download-json.component.ts @@ -0,0 +1,59 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { IconType } from '@hypertrace/assets-library'; +import { IconSize } from '@hypertrace/components'; +import { Observable } from 'rxjs'; +import { ButtonSize, ButtonStyle } from '../button/button'; + +@Component({ + selector: 'ht-download-json', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./download-json.component.scss'], + template: ` +
+ + +
+ ` +}) +export class DownloadJsonComponent { + @Input() + public dataSource!: Observable; + + @Input() + public fileName: string = 'download'; + + @Input() + public tooltip: string = 'Download Json'; + + public dataLoading: boolean = false; + + public triggerDownload(): void { + this.dataLoading = true; + this.dataSource.subscribe((data: unknown) => { + this.dataLoading = false; + if (typeof data === 'string') { + this.downloadData(data); + } else { + this.downloadData(JSON.stringify(data)); + } + }); + } + + private downloadData(data: string): void { + const dlJsonAnchorElement: HTMLAnchorElement = document.createElement('a'); + const downloadURL = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; + dlJsonAnchorElement?.setAttribute('href', downloadURL); + dlJsonAnchorElement?.setAttribute('href', downloadURL); + dlJsonAnchorElement?.setAttribute('download', `${this.fileName}.json`); + dlJsonAnchorElement.click(); + dlJsonAnchorElement.remove(); + } +} diff --git a/projects/components/src/download-json/download-json.module.ts b/projects/components/src/download-json/download-json.module.ts new file mode 100644 index 000000000..5093b2644 --- /dev/null +++ b/projects/components/src/download-json/download-json.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ButtonModule } from '../button/button.module'; +import { IconModule } from '../icon/icon.module'; +import { TooltipModule } from '../tooltip/tooltip.module'; +import { DownloadJsonComponent } from './download-json.component'; + +@NgModule({ + declarations: [DownloadJsonComponent], + imports: [CommonModule, ButtonModule, IconModule, TooltipModule], + exports: [DownloadJsonComponent] +}) +export class DownloadJsonModule {} diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index faad798b1..a3a80fac6 100644 --- a/projects/components/src/public-api.ts +++ b/projects/components/src/public-api.ts @@ -62,6 +62,10 @@ export { MenuDropdownComponent } from './menu-dropdown/menu-dropdown.component'; export { MenuItemComponent } from './menu-dropdown/menu-item/menu-item.component'; export { MenuDropdownModule } from './menu-dropdown/menu-dropdown.module'; +// Download JSON +export * from './download-json/download-json.component'; +export * from './download-json/download-json.module'; + // Dynamic label export * from './highlighted-label/highlighted-label.component'; export * from './highlighted-label/highlighted-label.module'; From 695126ccf63440527ab151961641302e1924c3fa Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Thu, 20 May 2021 19:51:45 +0530 Subject: [PATCH 02/11] feat: download trace --- .../trace-detail.page.component.ts | 9 ++ .../trace-detail/trace-detail.page.module.ts | 2 + .../trace-detail/trace-detail.service.ts | 19 +++ .../distributed-tracing/src/public-api.ts | 1 + .../graphql/graphql-handler-configuration.ts | 2 + ...pans-graphql-query-handler.service.test.ts | 124 ++++++++++++++++++ ...ort-spans-graphql-query-handler.service.ts | 88 +++++++++++++ .../api-trace-detail.page.module.ts | 4 +- 8 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts create mode 100644 projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts index 96d7893d3..275f571bf 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts @@ -43,6 +43,13 @@ import { TraceDetails, TraceDetailService } from './trace-detail.service';
+ + @@ -60,6 +67,7 @@ export class TraceDetailPageComponent { public static readonly TRACE_ID_PARAM_NAME: string = 'id'; public readonly traceDetails$: Observable; + public readonly exportSpans$: Observable; public constructor( private readonly subscriptionLifecycle: SubscriptionLifecycle, @@ -67,6 +75,7 @@ export class TraceDetailPageComponent { private readonly traceDetailService: TraceDetailService ) { this.traceDetails$ = this.traceDetailService.fetchTraceDetails(); + this.exportSpans$ = this.traceDetailService.fetchExportSpans(); } public onDashboardReady(dashboard: Dashboard): void { diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts index e441735d0..f769cd64e 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts @@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router'; import { FormattingModule, TraceRoute } from '@hypertrace/common'; import { CopyShareableLinkToClipboardModule, + DownloadJsonModule, IconModule, LabelModule, LoadAsyncModule, @@ -33,6 +34,7 @@ const ROUTE_CONFIG: TraceRoute[] = [ LoadAsyncModule, FormattingModule, CopyShareableLinkToClipboardModule, + DownloadJsonModule, NavigableDashboardModule.withDefaultDashboards(traceDetailDashboard) ] }) diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts index 5d6a80881..1efc4bd24 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts @@ -7,6 +7,10 @@ import { Observable, Subject } from 'rxjs'; import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators'; import { Trace, traceIdKey, TraceType, traceTypeKey } from '../../shared/graphql/model/schema/trace'; import { SpecificationBuilder } from '../../shared/graphql/request/builders/specification/specification-builder'; +import { + ExportSpansGraphQlQueryHandlerService, + EXPORT_SPANS_GQL_REQUEST +} from '../../shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service'; import { TraceGraphQlQueryHandlerService, TRACE_GQL_REQUEST @@ -82,6 +86,21 @@ export class TraceDetailService implements OnDestroy { ); } + public fetchExportSpans(): Observable { + return this.routeIds$.pipe( + switchMap(routeIds => + this.graphQlQueryService.query({ + requestType: EXPORT_SPANS_GQL_REQUEST, + traceId: routeIds.traceId, + timestamp: this.dateCoercer.coerce(routeIds.startTime), + limit: 1000 + }) + ), + takeUntil(this.destroyed$), + shareReplay(1) + ); + } + private fetchTrace(traceId: string, spanId?: string, startTime?: string | number): Observable { return this.graphQlQueryService.query({ requestType: TRACE_GQL_REQUEST, diff --git a/projects/distributed-tracing/src/public-api.ts b/projects/distributed-tracing/src/public-api.ts index 327e12e1f..a7b4aedd8 100644 --- a/projects/distributed-tracing/src/public-api.ts +++ b/projects/distributed-tracing/src/public-api.ts @@ -38,6 +38,7 @@ export * from './shared/dashboard/widgets/table/table-widget-view-toggle.model'; export * from './shared/services/filter-builder/graphql-filter-builder.service'; // Handlers +export * from './shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service'; export * from './shared/graphql/request/handlers/traces/trace-graphql-query-handler.service'; export * from './shared/graphql/request/handlers/traces/traces-graphql-query-handler.service'; export * from './shared/graphql/request/handlers/spans/span-graphql-query-handler.service'; diff --git a/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts b/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts index f7ece9781..ba8a69527 100644 --- a/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts +++ b/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts @@ -1,10 +1,12 @@ import { SpanGraphQlQueryHandlerService } from '../../../graphql/request/handlers/spans/span-graphql-query-handler.service'; import { SpansGraphQlQueryHandlerService } from '../../../graphql/request/handlers/spans/spans-graphql-query-handler.service'; +import { ExportSpansGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/export-spans-graphql-query-handler.service'; import { TraceGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/trace-graphql-query-handler.service'; import { TracesGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/traces-graphql-query-handler.service'; import { MetadataGraphQlQueryHandlerService } from '../../../services/metadata/handler/metadata-graphql-query-handler.service'; export const GRAPHQL_DATA_SOURCE_HANDLER_PROVIDERS = [ + ExportSpansGraphQlQueryHandlerService, TracesGraphQlQueryHandlerService, TraceGraphQlQueryHandlerService, SpansGraphQlQueryHandlerService, diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts new file mode 100644 index 000000000..8b9357b23 --- /dev/null +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts @@ -0,0 +1,124 @@ +import { FixedTimeRange, TimeDuration, TimeRangeService, TimeUnit } from '@hypertrace/common'; +import { GraphQlEnumArgument } from '@hypertrace/graphql-client'; +import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; +import { GraphQlFilterType } from '../../../model/schema/filter/graphql-filter'; +import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range'; +import { TRACE_SCOPE } from '../../../model/schema/trace'; +import { + ExportSpansGraphQlQueryHandlerService, + EXPORT_SPANS_GQL_REQUEST, + GraphQlExportSpansRequest +} from './export-spans-graphql-query-handler.service'; + +describe('ExportSpansGraphQlQueryHandlerService', () => { + const createService = createServiceFactory({ + service: ExportSpansGraphQlQueryHandlerService, + providers: [ + mockProvider(TimeRangeService, { + getCurrentTimeRange: jest + .fn() + .mockReturnValue(new FixedTimeRange(new Date(1568907645141), new Date(1568911245141))) + }) + ] + }); + + const testTimeRange = GraphQlTimeRange.fromTimeRange( + new FixedTimeRange(new Date(1568907645141), new Date(1568911245141)) + ); + const buildRequest = (timestamp?: Date): GraphQlExportSpansRequest => ({ + requestType: EXPORT_SPANS_GQL_REQUEST, + traceId: 'test-id', + timestamp: timestamp, + limit: 1 + }); + + test('matches request', () => { + const spectator = createService(); + expect(spectator.service.matchesRequest(buildRequest())).toBe(true); + expect(spectator.service.matchesRequest({ requestType: 'other' })).toBe(false); + }); + + test('produces expected graphql', () => { + const spectator = createService(); + const expected = spectator.service.convertRequest(buildRequest()); + expect(expected).toEqual({ + path: 'exportSpans', + arguments: [ + { + name: 'limit', + value: 1 + }, + { + name: 'between', + value: { + startTime: new Date(testTimeRange.from), + endTime: new Date(testTimeRange.to) + } + }, + { + name: 'filterBy', + value: [ + { + operator: new GraphQlEnumArgument('EQUALS'), + value: 'test-id', + type: new GraphQlEnumArgument(GraphQlFilterType.Id), + idType: new GraphQlEnumArgument(TRACE_SCOPE) + } + ] + } + ], + children: [ + { + path: 'result' + } + ] + }); + }); + + test('produces expected graphql with timestamp', () => { + const spectator = createService(); + const traceTimestamp = new Date(new TimeDuration(30, TimeUnit.Minute).toMillis()); + const expected = spectator.service.convertRequest(buildRequest(traceTimestamp)); + expect(expected).toEqual({ + path: 'exportSpans', + arguments: [ + { + name: 'limit', + value: 1 + }, + { + name: 'between', + value: { + startTime: new Date(0), + endTime: new Date(traceTimestamp.getTime() * 2) + } + }, + { + name: 'filterBy', + value: [ + { + operator: new GraphQlEnumArgument('EQUALS'), + value: 'test-id', + type: new GraphQlEnumArgument(GraphQlFilterType.Id), + idType: new GraphQlEnumArgument(TRACE_SCOPE) + } + ] + } + ], + children: [ + { + path: 'result' + } + ] + }); + }); + + test('converts response', () => { + const spectator = createService(); + const exportSpansResponse = { + result: '{}' + }; + + expect(spectator.service.convertResponse(exportSpansResponse)).toEqual('{}'); + }); +}); diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts new file mode 100644 index 000000000..e71771c00 --- /dev/null +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@angular/core'; +import { TimeDuration, TimeRangeService, TimeUnit } from '@hypertrace/common'; +import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hypertrace/graphql-client'; +import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service'; +import { GraphQlFilter } from '../../../model/schema/filter/graphql-filter'; +import { GraphQlIdFilter } from '../../../model/schema/filter/id/graphql-id-filter'; +import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range'; +import { resolveTraceType, TraceType } from '../../../model/schema/trace'; +import { GraphQlArgumentBuilder } from '../../builders/argument/graphql-argument-builder'; + +@Injectable({ providedIn: 'root' }) +export class ExportSpansGraphQlQueryHandlerService + implements GraphQlQueryHandler { + public readonly type: GraphQlHandlerType.Query = GraphQlHandlerType.Query; + private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder(); + + public constructor( + private readonly timeRangeService: TimeRangeService, + private readonly globalGraphQlFilterService: GlobalGraphQlFilterService + ) {} + + public matchesRequest(request: unknown): request is GraphQlExportSpansRequest { + return ( + typeof request === 'object' && + request !== null && + (request as Partial).requestType === EXPORT_SPANS_GQL_REQUEST + ); + } + + public convertRequest(request: GraphQlExportSpansRequest): GraphQlSelection { + const timeRange = this.buildTimeRange(request.timestamp); + + return { + path: 'exportSpans', + arguments: [ + this.argBuilder.forLimit(request.limit), + this.argBuilder.forTimeRange(timeRange), + ...this.argBuilder.forFilters( + this.globalGraphQlFilterService.mergeGlobalFilters(resolveTraceType(request.traceType), [ + this.buildTraceIdFilter(request) + ]) + ) + ], + children: [ + { + path: 'result' + } + ] + }; + } + + public convertResponse(response: ExportSpansResponse): string | undefined { + return response.result; + } + + private buildTraceIdFilter(request: GraphQlExportSpansRequest): GraphQlFilter { + return new GraphQlIdFilter(request.traceId, resolveTraceType(request.traceType)); + } + + protected buildTimeRange(timestamp?: Date): GraphQlTimeRange { + let timeRange; + if (timestamp) { + const duration = new TimeDuration(30, TimeUnit.Minute); + timeRange = { + startTime: timestamp.getTime() - duration.toMillis(), + endTime: timestamp.getTime() + duration.toMillis() + }; + } else { + timeRange = this.timeRangeService.getCurrentTimeRange(); + } + + return new GraphQlTimeRange(timeRange.startTime, timeRange.endTime); + } +} + +export const EXPORT_SPANS_GQL_REQUEST = Symbol('GraphQL Export Spans Request'); + +export interface GraphQlExportSpansRequest { + requestType: typeof EXPORT_SPANS_GQL_REQUEST; + traceType?: TraceType; + traceId: string; + limit: number; + timestamp?: Date; +} + +export interface ExportSpansResponse { + result: string; +} diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts index 328463a6b..f22463142 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts @@ -5,6 +5,7 @@ import { FormattingModule, TraceRoute } from '@hypertrace/common'; import { ButtonModule, CopyShareableLinkToClipboardModule, + DownloadJsonModule, IconModule, LabelModule, LoadAsyncModule, @@ -32,7 +33,8 @@ const ROUTE_CONFIG: TraceRoute[] = [ LoadAsyncModule, FormattingModule, ButtonModule, - CopyShareableLinkToClipboardModule + CopyShareableLinkToClipboardModule, + DownloadJsonModule ] }) export class ApiTraceDetailPageModule {} From d5b3d3b5e015bc91c4fb45eec8b0803a793805dc Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Fri, 21 May 2021 19:59:20 +0530 Subject: [PATCH 03/11] fix: addressing review comments --- .../download-json/download-json.component.ts | 45 ++++++++++++------- .../src/download-json/download-json.module.ts | 3 +- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index 815df1295..d9bfa6679 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -1,8 +1,11 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, Renderer2 } from '@angular/core'; import { IconType } from '@hypertrace/assets-library'; import { IconSize } from '@hypertrace/components'; import { Observable } from 'rxjs'; +import { catchError, finalize } from 'rxjs/operators'; import { ButtonSize, ButtonStyle } from '../button/button'; +import { NotificationService } from '../notification/notification.service'; @Component({ selector: 'ht-download-json', @@ -12,7 +15,7 @@ import { ButtonSize, ButtonStyle } from '../button/button';
{ - this.dataLoading = false; - if (typeof data === 'string') { - this.downloadData(data); - } else { - this.downloadData(JSON.stringify(data)); - } - }); + this.dataSource + .pipe( + catchError(() => this.notificationService.createFailureToast('Download failed')), + finalize(() => { + this.dataLoading = false; + this.changeDetector.detectChanges(); + }) + ) + .subscribe((data: unknown) => { + if (typeof data === 'string') { + this.downloadData(data); + } else { + this.downloadData(JSON.stringify(data)); + } + }); } private downloadData(data: string): void { - const dlJsonAnchorElement: HTMLAnchorElement = document.createElement('a'); - const downloadURL = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; - dlJsonAnchorElement?.setAttribute('href', downloadURL); - dlJsonAnchorElement?.setAttribute('href', downloadURL); - dlJsonAnchorElement?.setAttribute('download', `${this.fileName}.json`); + const dlJsonAnchorElement: HTMLAnchorElement = this.document.createElement('a'); + this.renderer.setAttribute(dlJsonAnchorElement, 'href', `data:text/json;charset=utf-8,${encodeURIComponent(data)}`); + this.renderer.setAttribute(dlJsonAnchorElement, 'download', `${this.fileName}.json`); dlJsonAnchorElement.click(); dlJsonAnchorElement.remove(); } diff --git a/projects/components/src/download-json/download-json.module.ts b/projects/components/src/download-json/download-json.module.ts index 5093b2644..801f24056 100644 --- a/projects/components/src/download-json/download-json.module.ts +++ b/projects/components/src/download-json/download-json.module.ts @@ -2,12 +2,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ButtonModule } from '../button/button.module'; import { IconModule } from '../icon/icon.module'; +import { NotificationModule } from '../notification/notification.module'; import { TooltipModule } from '../tooltip/tooltip.module'; import { DownloadJsonComponent } from './download-json.component'; @NgModule({ declarations: [DownloadJsonComponent], - imports: [CommonModule, ButtonModule, IconModule, TooltipModule], + imports: [CommonModule, ButtonModule, NotificationModule, IconModule, TooltipModule], exports: [DownloadJsonComponent] }) export class DownloadJsonModule {} From c72f0cb80c7dd132e1cc161d1b3e4b44c30d6646 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Fri, 21 May 2021 22:00:18 +0530 Subject: [PATCH 04/11] fix: test cases --- .../download-json.component.test.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/projects/components/src/download-json/download-json.component.test.ts b/projects/components/src/download-json/download-json.component.test.ts index bcb7797be..3086c4124 100644 --- a/projects/components/src/download-json/download-json.component.test.ts +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -1,4 +1,7 @@ +import { Renderer2 } from '@angular/core'; +// import { tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { mockProvider } from '@ngneat/spectator'; import { createHostFactory, Spectator } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { Observable, of } from 'rxjs'; @@ -14,10 +17,16 @@ describe('Button Component', () => { component: DownloadJsonComponent, imports: [DownloadJsonModule, RouterTestingModule], declarations: [MockComponent(ButtonComponent), MockComponent(IconComponent)], + providers: [ + mockProvider(Document), + mockProvider(Renderer2, { + setAttribute: jest.fn() + }) + ], shallow: true }); - const dataSource$: Observable = of('{}'); + const dataSource$: Observable = of('string'); test('should have only download button, when data is not loading', () => { spectator = createHost(``, { @@ -28,4 +37,36 @@ describe('Button Component', () => { expect(spectator.query(ButtonComponent)).toExist(); }); + + test('should download json file', () => { + spectator = createHost(``, { + hostProps: { + dataSource: dataSource$ + } + }); + spyOn(spectator.component, 'triggerDownload'); + + const spyObj = { + click: jest.fn(), + remove: jest.fn() + }; + spyOn(spectator.inject(Document), 'createElement').and.returnValue(spyObj); + spyOn(spectator.inject(Renderer2), 'setAttribute'); + expect(spectator.query('.download-button')).toExist(); + spectator.click('.download-button'); + // spectator.component.triggerDownload(); + expect(spectator.component.dataLoading).toBe(false); + expect(spectator.component.fileName).toBe('download'); + expect(spectator.component.tooltip).toBe('Download Json'); + + expect(spectator.component.triggerDownload).toHaveBeenCalledTimes(1); + expect(spectator.inject(Document).createElement).toHaveBeenCalledTimes(1); + expect(spectator.inject(Document).createElement).toHaveBeenCalledWith('a'); + expect(spectator.inject(Renderer2).setAttribute).toHaveBeenCalledTimes(2); + + expect(spyObj.click).toHaveBeenCalledTimes(1); + expect(spyObj.click).toHaveBeenCalledWith(); + expect(spyObj.remove).toHaveBeenCalledTimes(1); + expect(spyObj.remove).toHaveBeenCalledWith(); + }); }); From 10eb2041ac8ce728c9924a712c139e9d6011eec0 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 11:32:32 +0530 Subject: [PATCH 05/11] fix: addressing review comments --- .../download-json/download-json.component.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index d9bfa6679..3e26d83cf 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, R import { IconType } from '@hypertrace/assets-library'; import { IconSize } from '@hypertrace/components'; import { Observable } from 'rxjs'; -import { catchError, finalize } from 'rxjs/operators'; +import { catchError, finalize, take } from 'rxjs/operators'; import { ButtonSize, ButtonStyle } from '../button/button'; import { NotificationService } from '../notification/notification.service'; @@ -37,18 +37,22 @@ export class DownloadJsonComponent { public tooltip: string = 'Download Json'; public dataLoading: boolean = false; + private readonly dlJsonAnchorElement: HTMLAnchorElement; constructor( @Inject(DOCUMENT) private readonly document: Document, private readonly renderer: Renderer2, private readonly changeDetector: ChangeDetectorRef, private readonly notificationService: NotificationService - ) {} + ) { + this.dlJsonAnchorElement = this.document.createElement('a'); + } public triggerDownload(): void { this.dataLoading = true; this.dataSource .pipe( + take(1), catchError(() => this.notificationService.createFailureToast('Download failed')), finalize(() => { this.dataLoading = false; @@ -65,10 +69,13 @@ export class DownloadJsonComponent { } private downloadData(data: string): void { - const dlJsonAnchorElement: HTMLAnchorElement = this.document.createElement('a'); - this.renderer.setAttribute(dlJsonAnchorElement, 'href', `data:text/json;charset=utf-8,${encodeURIComponent(data)}`); - this.renderer.setAttribute(dlJsonAnchorElement, 'download', `${this.fileName}.json`); - dlJsonAnchorElement.click(); - dlJsonAnchorElement.remove(); + this.renderer.setAttribute( + this.dlJsonAnchorElement, + 'href', + `data:text/json;charset=utf-8,${encodeURIComponent(data)}` + ); + this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', `${this.fileName}.json`); + this.renderer.setAttribute(this.dlJsonAnchorElement, 'display', 'none'); + this.dlJsonAnchorElement.click(); } } From dfffe3746f4bd40200154e94c4d6d0443ba2cf68 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 11:52:13 +0530 Subject: [PATCH 06/11] fix: applying some fix --- .../components/src/download-json/download-json.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index 3e26d83cf..66b8cbb8a 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -12,15 +12,13 @@ import { NotificationService } from '../notification/notification.service'; changeDetection: ChangeDetectionStrategy.OnPush, styleUrls: ['./download-json.component.scss'], template: ` -
+
From ff4a965f8967f8fd93aac79d47eb67c432db0bb3 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 16:35:13 +0530 Subject: [PATCH 07/11] fix: test case and lint errors --- .../download-json.component.test.ts | 44 ++++++++----------- .../download-json/download-json.component.ts | 2 +- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/projects/components/src/download-json/download-json.component.test.ts b/projects/components/src/download-json/download-json.component.test.ts index 3086c4124..6f1b6466e 100644 --- a/projects/components/src/download-json/download-json.component.test.ts +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -1,8 +1,7 @@ import { Renderer2 } from '@angular/core'; -// import { tick } from '@angular/core/testing'; +import { fakeAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { mockProvider } from '@ngneat/spectator'; -import { createHostFactory, Spectator } from '@ngneat/spectator/jest'; +import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; import { Observable, of } from 'rxjs'; import { ButtonComponent } from '../button/button.component'; @@ -12,13 +11,17 @@ import { DownloadJsonModule } from './download-json.module'; describe('Button Component', () => { let spectator: Spectator; + const mockElement = document.createElement('a'); + const createElementSpy = jest.fn().mockReturnValue(mockElement); const createHost = createHostFactory({ component: DownloadJsonComponent, imports: [DownloadJsonModule, RouterTestingModule], declarations: [MockComponent(ButtonComponent), MockComponent(IconComponent)], providers: [ - mockProvider(Document), + mockProvider(Document, { + createElement: createElementSpy + }), mockProvider(Renderer2, { setAttribute: jest.fn() }) @@ -26,7 +29,9 @@ describe('Button Component', () => { shallow: true }); - const dataSource$: Observable = of('string'); + const dataSource$: Observable = of({ + spans: [] + }); test('should have only download button, when data is not loading', () => { spectator = createHost(``, { @@ -38,35 +43,24 @@ describe('Button Component', () => { expect(spectator.query(ButtonComponent)).toExist(); }); - test('should download json file', () => { - spectator = createHost(``, { + test('should download json file', fakeAsync(() => { + spectator = createHost(``, { hostProps: { dataSource: dataSource$ } }); + spyOn(spectator.component, 'triggerDownload'); - const spyObj = { - click: jest.fn(), - remove: jest.fn() - }; - spyOn(spectator.inject(Document), 'createElement').and.returnValue(spyObj); - spyOn(spectator.inject(Renderer2), 'setAttribute'); - expect(spectator.query('.download-button')).toExist(); - spectator.click('.download-button'); - // spectator.component.triggerDownload(); expect(spectator.component.dataLoading).toBe(false); expect(spectator.component.fileName).toBe('download'); expect(spectator.component.tooltip).toBe('Download Json'); + const element = spectator.query('.download-json'); + expect(element).toExist(); - expect(spectator.component.triggerDownload).toHaveBeenCalledTimes(1); - expect(spectator.inject(Document).createElement).toHaveBeenCalledTimes(1); - expect(spectator.inject(Document).createElement).toHaveBeenCalledWith('a'); - expect(spectator.inject(Renderer2).setAttribute).toHaveBeenCalledTimes(2); + spectator.click(element!); + spectator.tick(); - expect(spyObj.click).toHaveBeenCalledTimes(1); - expect(spyObj.click).toHaveBeenCalledWith(); - expect(spyObj.remove).toHaveBeenCalledTimes(1); - expect(spyObj.remove).toHaveBeenCalledWith(); - }); + expect(spectator.component.triggerDownload).toHaveBeenCalledTimes(1); + })); }); diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index 66b8cbb8a..186855eb6 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -37,7 +37,7 @@ export class DownloadJsonComponent { public dataLoading: boolean = false; private readonly dlJsonAnchorElement: HTMLAnchorElement; - constructor( + public constructor( @Inject(DOCUMENT) private readonly document: Document, private readonly renderer: Renderer2, private readonly changeDetector: ChangeDetectorRef, From edf6307fbe637091fc291d88e43952641c746a1d Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 19:23:45 +0530 Subject: [PATCH 08/11] fix: addressing review comments --- .../src/download-json/download-json.component.test.ts | 3 +-- .../src/download-json/download-json.component.ts | 5 +---- .../src/download-json/download-json.module.ts | 3 +-- .../pages/trace-detail/trace-detail.page.component.ts | 2 +- .../src/pages/trace-detail/trace-detail.page.module.ts | 4 +++- .../export-spans-graphql-query-handler.service.ts | 10 ++-------- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/projects/components/src/download-json/download-json.component.test.ts b/projects/components/src/download-json/download-json.component.test.ts index 6f1b6466e..9808385c2 100644 --- a/projects/components/src/download-json/download-json.component.test.ts +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -9,7 +9,7 @@ import { IconComponent } from '../icon/icon.component'; import { DownloadJsonComponent } from './download-json.component'; import { DownloadJsonModule } from './download-json.module'; -describe('Button Component', () => { +describe('Download Component', () => { let spectator: Spectator; const mockElement = document.createElement('a'); const createElementSpy = jest.fn().mockReturnValue(mockElement); @@ -54,7 +54,6 @@ describe('Button Component', () => { expect(spectator.component.dataLoading).toBe(false); expect(spectator.component.fileName).toBe('download'); - expect(spectator.component.tooltip).toBe('Download Json'); const element = spectator.query('.download-json'); expect(element).toExist(); diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index 186855eb6..ff7246367 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -12,7 +12,7 @@ import { NotificationService } from '../notification/notification.service'; changeDetection: ChangeDetectionStrategy.OnPush, styleUrls: ['./download-json.component.scss'], template: ` -
+
diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts index f769cd64e..541fb3332 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts @@ -8,7 +8,8 @@ import { IconModule, LabelModule, LoadAsyncModule, - SummaryValueModule + SummaryValueModule, + TooltipModule } from '@hypertrace/components'; import { NavigableDashboardModule } from '../../shared/dashboard/dashboard-wrapper/navigable-dashboard.module'; import { TracingDashboardModule } from '../../shared/dashboard/tracing-dashboard.module'; @@ -31,6 +32,7 @@ const ROUTE_CONFIG: TraceRoute[] = [ TracingDashboardModule, IconModule, SummaryValueModule, + TooltipModule, LoadAsyncModule, FormattingModule, CopyShareableLinkToClipboardModule, diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts index e71771c00..7560371c3 100644 --- a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts @@ -58,18 +58,12 @@ export class ExportSpansGraphQlQueryHandlerService } protected buildTimeRange(timestamp?: Date): GraphQlTimeRange { - let timeRange; if (timestamp) { const duration = new TimeDuration(30, TimeUnit.Minute); - timeRange = { - startTime: timestamp.getTime() - duration.toMillis(), - endTime: timestamp.getTime() + duration.toMillis() - }; - } else { - timeRange = this.timeRangeService.getCurrentTimeRange(); + return new GraphQlTimeRange(timestamp.getTime() - duration.toMillis(), timestamp.getTime() + duration.toMillis()); } - return new GraphQlTimeRange(timeRange.startTime, timeRange.endTime); + return GraphQlTimeRange.fromTimeRange(this.timeRangeService.getCurrentTimeRange()); } } From b50c9fee2d870552904f34778b7c35e0689b8fd0 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 20:03:20 +0530 Subject: [PATCH 09/11] fix: addressing review comments --- .../src/download-json/download-json.component.test.ts | 4 ++-- .../src/download-json/download-json.component.ts | 4 ++-- .../pages/trace-detail/trace-detail.page.component.ts | 2 +- .../traces/export-spans-graphql-query-handler.service.ts | 9 ++++----- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/projects/components/src/download-json/download-json.component.test.ts b/projects/components/src/download-json/download-json.component.test.ts index 9808385c2..c5f7fd56c 100644 --- a/projects/components/src/download-json/download-json.component.test.ts +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -9,7 +9,7 @@ import { IconComponent } from '../icon/icon.component'; import { DownloadJsonComponent } from './download-json.component'; import { DownloadJsonModule } from './download-json.module'; -describe('Download Component', () => { +describe('Download Json Component', () => { let spectator: Spectator; const mockElement = document.createElement('a'); const createElementSpy = jest.fn().mockReturnValue(mockElement); @@ -53,7 +53,7 @@ describe('Download Component', () => { spyOn(spectator.component, 'triggerDownload'); expect(spectator.component.dataLoading).toBe(false); - expect(spectator.component.fileName).toBe('download'); + expect(spectator.component.fileName).toBe('download.json'); const element = spectator.query('.download-json'); expect(element).toExist(); diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index ff7246367..42120fd19 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -29,7 +29,7 @@ export class DownloadJsonComponent { public dataSource!: Observable; @Input() - public fileName: string = 'download'; + public fileName: string = 'download.json'; public dataLoading: boolean = false; private readonly dlJsonAnchorElement: HTMLAnchorElement; @@ -69,7 +69,7 @@ export class DownloadJsonComponent { 'href', `data:text/json;charset=utf-8,${encodeURIComponent(data)}` ); - this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', `${this.fileName}.json`); + this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', this.fileName); this.renderer.setAttribute(this.dlJsonAnchorElement, 'display', 'none'); this.dlJsonAnchorElement.click(); } diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts index 240c33675..128f95542 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts @@ -47,7 +47,7 @@ import { TraceDetails, TraceDetailService } from './trace-detail.service';
diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts index 7560371c3..9931ea8a3 100644 --- a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts @@ -58,12 +58,11 @@ export class ExportSpansGraphQlQueryHandlerService } protected buildTimeRange(timestamp?: Date): GraphQlTimeRange { - if (timestamp) { - const duration = new TimeDuration(30, TimeUnit.Minute); - return new GraphQlTimeRange(timestamp.getTime() - duration.toMillis(), timestamp.getTime() + duration.toMillis()); - } + const duration = new TimeDuration(30, TimeUnit.Minute); - return GraphQlTimeRange.fromTimeRange(this.timeRangeService.getCurrentTimeRange()); + return timestamp + ? new GraphQlTimeRange(timestamp.getTime() - duration.toMillis(), timestamp.getTime() + duration.toMillis()) + : GraphQlTimeRange.fromTimeRange(this.timeRangeService.getCurrentTimeRange()); } } From 89d988f993646431d1499a43a23e64febc805ce2 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 21:04:22 +0530 Subject: [PATCH 10/11] fix: suggested changes --- .../src/pages/trace-detail/trace-detail.page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts index 128f95542..64abd18ed 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts @@ -47,7 +47,7 @@ import { TraceDetails, TraceDetailService } from './trace-detail.service';
From cf4a8ab8a3795800511a16067a1ce01fecff1efb Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Mon, 24 May 2021 21:14:14 +0530 Subject: [PATCH 11/11] fix: addressing review comments --- .../pages/api-trace-detail/api-trace-detail.page.module.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts index f22463142..328463a6b 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts @@ -5,7 +5,6 @@ import { FormattingModule, TraceRoute } from '@hypertrace/common'; import { ButtonModule, CopyShareableLinkToClipboardModule, - DownloadJsonModule, IconModule, LabelModule, LoadAsyncModule, @@ -33,8 +32,7 @@ const ROUTE_CONFIG: TraceRoute[] = [ LoadAsyncModule, FormattingModule, ButtonModule, - CopyShareableLinkToClipboardModule, - DownloadJsonModule + CopyShareableLinkToClipboardModule ] }) export class ApiTraceDetailPageModule {}