Skip to content

Commit 2189e8a

Browse files
feat: download json component (#866)
* feat: download json component
1 parent f2d465f commit 2189e8a

File tree

5 files changed

+172
-0
lines changed

5 files changed

+172
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import 'color-palette';
2+
3+
.download-json {
4+
width: 40px;
5+
height: 40px;
6+
display: flex;
7+
justify-content: center;
8+
align-items: center;
9+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Renderer2 } from '@angular/core';
2+
import { fakeAsync } from '@angular/core/testing';
3+
import { RouterTestingModule } from '@angular/router/testing';
4+
import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
5+
import { MockComponent } from 'ng-mocks';
6+
import { Observable, of } from 'rxjs';
7+
import { ButtonComponent } from '../button/button.component';
8+
import { IconComponent } from '../icon/icon.component';
9+
import { DownloadJsonComponent } from './download-json.component';
10+
import { DownloadJsonModule } from './download-json.module';
11+
12+
describe('Button Component', () => {
13+
let spectator: Spectator<DownloadJsonComponent>;
14+
const mockElement = document.createElement('a');
15+
const createElementSpy = jest.fn().mockReturnValue(mockElement);
16+
17+
const createHost = createHostFactory({
18+
component: DownloadJsonComponent,
19+
imports: [DownloadJsonModule, RouterTestingModule],
20+
declarations: [MockComponent(ButtonComponent), MockComponent(IconComponent)],
21+
providers: [
22+
mockProvider(Document, {
23+
createElement: createElementSpy
24+
}),
25+
mockProvider(Renderer2, {
26+
setAttribute: jest.fn()
27+
})
28+
],
29+
shallow: true
30+
});
31+
32+
const dataSource$: Observable<unknown> = of({
33+
spans: []
34+
});
35+
36+
test('should have only download button, when data is not loading', () => {
37+
spectator = createHost(`<ht-download-json [dataSource]="dataSource"></ht-download-json>`, {
38+
hostProps: {
39+
dataSource: dataSource$
40+
}
41+
});
42+
43+
expect(spectator.query(ButtonComponent)).toExist();
44+
});
45+
46+
test('should download json file', fakeAsync(() => {
47+
spectator = createHost(`<ht-download-json [dataSource]="dataSource"></ht-download-json>`, {
48+
hostProps: {
49+
dataSource: dataSource$
50+
}
51+
});
52+
53+
spyOn(spectator.component, 'triggerDownload');
54+
55+
expect(spectator.component.dataLoading).toBe(false);
56+
expect(spectator.component.fileName).toBe('download');
57+
expect(spectator.component.tooltip).toBe('Download Json');
58+
const element = spectator.query('.download-json');
59+
expect(element).toExist();
60+
61+
spectator.click(element!);
62+
spectator.tick();
63+
64+
expect(spectator.component.triggerDownload).toHaveBeenCalledTimes(1);
65+
}));
66+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { DOCUMENT } from '@angular/common';
2+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, Renderer2 } from '@angular/core';
3+
import { IconType } from '@hypertrace/assets-library';
4+
import { IconSize } from '@hypertrace/components';
5+
import { Observable } from 'rxjs';
6+
import { catchError, finalize, take } from 'rxjs/operators';
7+
import { ButtonSize, ButtonStyle } from '../button/button';
8+
import { NotificationService } from '../notification/notification.service';
9+
10+
@Component({
11+
selector: 'ht-download-json',
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
styleUrls: ['./download-json.component.scss'],
14+
template: `
15+
<div class="download-json" [htTooltip]="this.tooltip" (click)="this.triggerDownload()">
16+
<ht-button
17+
*ngIf="!this.dataLoading"
18+
class="download-button"
19+
icon="${IconType.Download}"
20+
display="${ButtonStyle.Text}"
21+
size="${ButtonSize.Large}"
22+
></ht-button>
23+
<ht-icon *ngIf="this.dataLoading" icon="${IconType.Loading}" size="${IconSize.Large}"></ht-icon>
24+
</div>
25+
`
26+
})
27+
export class DownloadJsonComponent {
28+
@Input()
29+
public dataSource!: Observable<unknown>;
30+
31+
@Input()
32+
public fileName: string = 'download';
33+
34+
@Input()
35+
public tooltip: string = 'Download Json';
36+
37+
public dataLoading: boolean = false;
38+
private readonly dlJsonAnchorElement: HTMLAnchorElement;
39+
40+
public constructor(
41+
@Inject(DOCUMENT) private readonly document: Document,
42+
private readonly renderer: Renderer2,
43+
private readonly changeDetector: ChangeDetectorRef,
44+
private readonly notificationService: NotificationService
45+
) {
46+
this.dlJsonAnchorElement = this.document.createElement('a');
47+
}
48+
49+
public triggerDownload(): void {
50+
this.dataLoading = true;
51+
this.dataSource
52+
.pipe(
53+
take(1),
54+
catchError(() => this.notificationService.createFailureToast('Download failed')),
55+
finalize(() => {
56+
this.dataLoading = false;
57+
this.changeDetector.detectChanges();
58+
})
59+
)
60+
.subscribe((data: unknown) => {
61+
if (typeof data === 'string') {
62+
this.downloadData(data);
63+
} else {
64+
this.downloadData(JSON.stringify(data));
65+
}
66+
});
67+
}
68+
69+
private downloadData(data: string): void {
70+
this.renderer.setAttribute(
71+
this.dlJsonAnchorElement,
72+
'href',
73+
`data:text/json;charset=utf-8,${encodeURIComponent(data)}`
74+
);
75+
this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', `${this.fileName}.json`);
76+
this.renderer.setAttribute(this.dlJsonAnchorElement, 'display', 'none');
77+
this.dlJsonAnchorElement.click();
78+
}
79+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { CommonModule } from '@angular/common';
2+
import { NgModule } from '@angular/core';
3+
import { ButtonModule } from '../button/button.module';
4+
import { IconModule } from '../icon/icon.module';
5+
import { NotificationModule } from '../notification/notification.module';
6+
import { TooltipModule } from '../tooltip/tooltip.module';
7+
import { DownloadJsonComponent } from './download-json.component';
8+
9+
@NgModule({
10+
declarations: [DownloadJsonComponent],
11+
imports: [CommonModule, ButtonModule, NotificationModule, IconModule, TooltipModule],
12+
exports: [DownloadJsonComponent]
13+
})
14+
export class DownloadJsonModule {}

projects/components/src/public-api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export { MenuDropdownComponent } from './menu-dropdown/menu-dropdown.component';
6262
export { MenuItemComponent } from './menu-dropdown/menu-item/menu-item.component';
6363
export { MenuDropdownModule } from './menu-dropdown/menu-dropdown.module';
6464

65+
// Download JSON
66+
export * from './download-json/download-json.component';
67+
export * from './download-json/download-json.module';
68+
6569
// Dynamic label
6670
export * from './highlighted-label/highlighted-label.component';
6771
export * from './highlighted-label/highlighted-label.module';

0 commit comments

Comments
 (0)