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..6f1b6466e --- /dev/null +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -0,0 +1,66 @@ +import { Renderer2 } from '@angular/core'; +import { fakeAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { createHostFactory, mockProvider, 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 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, { + createElement: createElementSpy + }), + mockProvider(Renderer2, { + setAttribute: jest.fn() + }) + ], + shallow: true + }); + + const dataSource$: Observable = of({ + spans: [] + }); + + test('should have only download button, when data is not loading', () => { + spectator = createHost(``, { + hostProps: { + dataSource: dataSource$ + } + }); + + expect(spectator.query(ButtonComponent)).toExist(); + }); + + test('should download json file', fakeAsync(() => { + spectator = createHost(``, { + hostProps: { + dataSource: dataSource$ + } + }); + + spyOn(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(); + + spectator.click(element!); + spectator.tick(); + + 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 new file mode 100644 index 000000000..186855eb6 --- /dev/null +++ b/projects/components/src/download-json/download-json.component.ts @@ -0,0 +1,79 @@ +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, take } from 'rxjs/operators'; +import { ButtonSize, ButtonStyle } from '../button/button'; +import { NotificationService } from '../notification/notification.service'; + +@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; + private readonly dlJsonAnchorElement: HTMLAnchorElement; + + public 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; + this.changeDetector.detectChanges(); + }) + ) + .subscribe((data: unknown) => { + if (typeof data === 'string') { + this.downloadData(data); + } else { + this.downloadData(JSON.stringify(data)); + } + }); + } + + private downloadData(data: string): void { + 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(); + } +} 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..801f24056 --- /dev/null +++ b/projects/components/src/download-json/download-json.module.ts @@ -0,0 +1,14 @@ +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, NotificationModule, IconModule, TooltipModule], + exports: [DownloadJsonComponent] +}) +export class DownloadJsonModule {} diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index e75f9523b..60b815fe6 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';