From 7f0dcae64e5928c9ce4aa34a7655af534b211026 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Thu, 20 May 2021 19:44:04 +0530 Subject: [PATCH 1/6] 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 d5b3d3b5e015bc91c4fb45eec8b0803a793805dc Mon Sep 17 00:00:00 2001 From: Sandeep Kumar Sharma Date: Fri, 21 May 2021 19:59:20 +0530 Subject: [PATCH 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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,