-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: inbuilt csv download support for tables (#2420)
- Loading branch information
Showing
23 changed files
with
471 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
projects/components/src/table/cells/csv-generators/table-cell-boolean-csv-generator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { createServiceFactory } from '@ngneat/spectator/jest'; | ||
import { TableCellBooleanCsvGenerator } from './table-cell-boolean-csv-generator'; | ||
|
||
describe('TableCellBooleanCsvGenerator', () => { | ||
const createService = createServiceFactory({ | ||
service: TableCellBooleanCsvGenerator | ||
}); | ||
|
||
test('should return data as expected', () => { | ||
const spectator = createService(); | ||
expect(spectator.service.generateSafeCsv(undefined)).toBeUndefined(); | ||
expect(spectator.service.generateSafeCsv(null)).toBeUndefined(); | ||
|
||
expect(spectator.service.generateSafeCsv(true)).toEqual('true'); | ||
expect(spectator.service.generateSafeCsv(false)).toEqual('false'); | ||
}); | ||
}); |
12 changes: 12 additions & 0 deletions
12
projects/components/src/table/cells/csv-generators/table-cell-boolean-csv-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { TableCellCsvGenerator } from '../table-cell-csv-generator'; | ||
import { Injectable } from '@angular/core'; | ||
import { isNil } from 'lodash-es'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class TableCellBooleanCsvGenerator implements TableCellCsvGenerator<boolean> { | ||
public readonly cellType: string = 'boolean'; | ||
|
||
public generateSafeCsv(cellData: boolean | null | undefined): string | undefined { | ||
return isNil(cellData) ? undefined : String(cellData); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
projects/components/src/table/cells/csv-generators/table-cell-number-csv-generator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { createServiceFactory } from '@ngneat/spectator/jest'; | ||
import { TableCellNumberCsvGenerator } from './table-cell-number-csv-generator'; | ||
|
||
describe('TableCellNumberCsvGenerator', () => { | ||
const createService = createServiceFactory({ | ||
service: TableCellNumberCsvGenerator | ||
}); | ||
|
||
test('should return data as expected', () => { | ||
const spectator = createService(); | ||
expect(spectator.service.generateSafeCsv(undefined)).toBeUndefined(); | ||
expect(spectator.service.generateSafeCsv(null)).toBeUndefined(); | ||
|
||
expect(spectator.service.generateSafeCsv(1)).toEqual('1'); | ||
}); | ||
}); |
12 changes: 12 additions & 0 deletions
12
projects/components/src/table/cells/csv-generators/table-cell-number-csv-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { TableCellCsvGenerator } from '../table-cell-csv-generator'; | ||
import { CoreTableCellRendererType } from '../types/core-table-cell-renderer-type'; | ||
import { Injectable } from '@angular/core'; | ||
import { isNil } from 'lodash-es'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class TableCellNumberCsvGenerator implements TableCellCsvGenerator<number> { | ||
public readonly cellType: string = CoreTableCellRendererType.Number; | ||
public generateSafeCsv(cellData: number | undefined | null): string | undefined { | ||
return isNil(cellData) ? undefined : cellData.toString(); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...s/components/src/table/cells/csv-generators/table-cell-string-array-csv-generator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { createServiceFactory } from '@ngneat/spectator/jest'; | ||
import { TableCellStringArrayCsvGenerator } from './table-cell-string-array-csv-generator'; | ||
|
||
describe('TableCellStringArrayCsvGenerator', () => { | ||
const createService = createServiceFactory({ | ||
service: TableCellStringArrayCsvGenerator | ||
}); | ||
|
||
test('should return data as expected', () => { | ||
const spectator = createService(); | ||
expect(spectator.service.generateSafeCsv(undefined)).toBeUndefined(); | ||
expect(spectator.service.generateSafeCsv(null)).toBeUndefined(); | ||
|
||
expect(spectator.service.generateSafeCsv(['a'])).toEqual('a'); | ||
expect(spectator.service.generateSafeCsv(['a', 'b'])).toEqual('a, b'); | ||
}); | ||
}); |
13 changes: 13 additions & 0 deletions
13
projects/components/src/table/cells/csv-generators/table-cell-string-array-csv-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { TableCellCsvGenerator } from '../table-cell-csv-generator'; | ||
import { CoreTableCellRendererType } from '../types/core-table-cell-renderer-type'; | ||
import { Injectable } from '@angular/core'; | ||
import { isNil } from 'lodash-es'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class TableCellStringArrayCsvGenerator implements TableCellCsvGenerator<string[]> { | ||
public readonly cellType: string = CoreTableCellRendererType.StringArray; | ||
|
||
public generateSafeCsv(cellData: string[] | null | undefined): string | undefined { | ||
return isNil(cellData) ? undefined : cellData.join(', '); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
projects/components/src/table/cells/csv-generators/table-cell-string-csv-generator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { createServiceFactory } from '@ngneat/spectator/jest'; | ||
import { TableCellStringCsvGenerator } from './table-cell-string-csv-generator'; | ||
|
||
describe('TableCellStringCsvGenerator', () => { | ||
const createService = createServiceFactory({ | ||
service: TableCellStringCsvGenerator | ||
}); | ||
|
||
test('should return data as expected', () => { | ||
const spectator = createService(); | ||
expect(spectator.service.generateSafeCsv(undefined)).toBeUndefined(); | ||
expect(spectator.service.generateSafeCsv(null)).toBeUndefined(); | ||
|
||
expect(spectator.service.generateSafeCsv('test')).toEqual('test'); | ||
}); | ||
}); |
12 changes: 12 additions & 0 deletions
12
projects/components/src/table/cells/csv-generators/table-cell-string-csv-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { TableCellCsvGenerator } from '../table-cell-csv-generator'; | ||
import { Injectable } from '@angular/core'; | ||
import { CoreTableCellRendererType } from '../types/core-table-cell-renderer-type'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class TableCellStringCsvGenerator implements TableCellCsvGenerator<string> { | ||
public cellType: string = CoreTableCellRendererType.Text; | ||
|
||
public generateSafeCsv(cellData: string | null | undefined): string | undefined { | ||
return cellData ?? undefined; | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
...ects/components/src/table/cells/csv-generators/table-cell-timestamp-csv-generator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { createServiceFactory } from '@ngneat/spectator/jest'; | ||
import { TableCellTimestampCsvGenerator } from './table-cell-timestamp-csv-generator'; | ||
import { DateFormatMode, DisplayDatePipe } from '@hypertrace/common'; | ||
|
||
describe('TableCellTimestampCsvGenerator', () => { | ||
const createService = createServiceFactory({ | ||
service: TableCellTimestampCsvGenerator | ||
}); | ||
|
||
test('should return data as expected', () => { | ||
const displayDatePipe = new DisplayDatePipe(); | ||
const spectator = createService(); | ||
expect(spectator.service.generateSafeCsv(undefined)).toEqual('-'); | ||
expect(spectator.service.generateSafeCsv(null)).toEqual('-'); | ||
|
||
expect(spectator.service.generateSafeCsv(new Date('2021-05-25T15:53:27.376Z'))).toEqual( | ||
displayDatePipe.transform(new Date('2021-05-25T15:53:27.376Z'), { mode: DateFormatMode.TimeWithSeconds }) | ||
); | ||
expect(spectator.service.generateSafeCsv(1697654315)).toEqual( | ||
displayDatePipe.transform(1697654315, { mode: DateFormatMode.TimeWithSeconds }) | ||
); | ||
}); | ||
}); |
14 changes: 14 additions & 0 deletions
14
projects/components/src/table/cells/csv-generators/table-cell-timestamp-csv-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { TableCellCsvGenerator } from '../table-cell-csv-generator'; | ||
import { DateFormatMode, DisplayDatePipe } from '@hypertrace/common'; | ||
import { CoreTableCellRendererType } from '../types/core-table-cell-renderer-type'; | ||
import { Injectable } from '@angular/core'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class TableCellTimestampCsvGenerator implements TableCellCsvGenerator<Date | number | string> { | ||
public cellType: string = CoreTableCellRendererType.Timestamp; | ||
private readonly displayDate: DisplayDatePipe = new DisplayDatePipe(); | ||
|
||
public generateSafeCsv(cellData: Date | number | string | null | undefined): string { | ||
return this.displayDate.transform(cellData, { mode: DateFormatMode.TimeWithSeconds }); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
projects/components/src/table/cells/table-cell-csv-generator-management.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { TableCellCsvGenerator } from './table-cell-csv-generator'; | ||
import { isArray, isUndefined } from 'lodash-es'; | ||
|
||
@Injectable({ | ||
providedIn: 'root' | ||
}) | ||
export class TableCellCsvGeneratorManagementService { | ||
private readonly csvGenerators: TableCellCsvGenerator<unknown>[] = []; | ||
|
||
public register(csvGenerators: TableCellCsvGenerator<unknown> | TableCellCsvGenerator<unknown>[]): void { | ||
isArray(csvGenerators) ? csvGenerators.forEach(item => this.addGenerator(item)) : this.addGenerator(csvGenerators); | ||
} | ||
|
||
private addGenerator(csvGenerator: TableCellCsvGenerator<unknown>): void { | ||
if (isUndefined(this.findMatchingGenerator(csvGenerator.cellType))) { | ||
this.csvGenerators.push(csvGenerator); | ||
} | ||
} | ||
|
||
public findMatchingGenerator(type?: string): TableCellCsvGenerator<unknown> | undefined { | ||
return this.csvGenerators.find(generator => generator.cellType === type); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
projects/components/src/table/cells/table-cell-csv-generator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Dictionary } from '@hypertrace/common'; | ||
import { InjectionToken } from '@angular/core'; | ||
import { TableRow } from '../table-api'; | ||
|
||
export const TABLE_CELL_CSV_GENERATORS = new InjectionToken<TableCellCsvGenerator<unknown>[][]>( | ||
'TABLE_CELL_CSV_GENERATORS' | ||
); | ||
|
||
export interface TableCellCsvGenerator<TCellData, TRowData = TableRow> { | ||
readonly cellType: string; | ||
|
||
generateSafeCsv( | ||
cellData: TCellData | null | undefined, | ||
rowData?: TRowData | null | ||
): string | Dictionary<string> | undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
projects/components/src/table/table-csv-downloader.service.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; | ||
import { FileDownloadService, NotificationService, TableCsvDownloaderService, TableRow } from '@hypertrace/components'; | ||
import { EMPTY, of } from 'rxjs'; | ||
import { fakeAsync, tick } from '@angular/core/testing'; | ||
import { TableColumnConfigExtended } from './table.service'; | ||
import { Dictionary } from '@hypertrace/common'; | ||
|
||
describe('TableCsvDownloaderService', () => { | ||
const createService = createServiceFactory({ | ||
service: TableCsvDownloaderService, | ||
providers: [ | ||
mockProvider(FileDownloadService, { | ||
downloadAsCsv: jest.fn().mockReturnValue(EMPTY) | ||
}), | ||
mockProvider(NotificationService, { | ||
createInfoToast: jest.fn() | ||
}) | ||
] | ||
}); | ||
|
||
test('execute download should behave as expected for no data', fakeAsync(() => { | ||
const spectator = createService(); | ||
spectator.service.executeDownload(of({ rows: [], columnConfigs: [] }), 'table-id'); | ||
|
||
tick(); | ||
expect(spectator.inject(NotificationService).createInfoToast).toHaveBeenCalledWith('No data to download'); | ||
expect(spectator.inject(FileDownloadService).downloadAsCsv).not.toHaveBeenCalled(); | ||
})); | ||
|
||
test('execute download should behave as expected when data is present', fakeAsync(() => { | ||
const spectator = createService(); | ||
spectator.service.executeDownload( | ||
of({ | ||
rows: [{ key1: 'value1' }] as TableRow[], | ||
columnConfigs: [ | ||
{ | ||
id: 'key1', | ||
name: 'key1', | ||
csvGenerator: { generateSafeCsv: (cellData: Dictionary<string>, _row): Dictionary<string> => cellData } | ||
} | ||
] as TableColumnConfigExtended[] | ||
}), | ||
'table-id' | ||
); | ||
tick(); | ||
|
||
expect(spectator.inject(FileDownloadService).downloadAsCsv).toHaveBeenCalled(); | ||
})); | ||
}); |
72 changes: 72 additions & 0 deletions
72
projects/components/src/table/table-csv-downloader.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { EMPTY, Observable, of, Subject } from 'rxjs'; | ||
import { Injectable } from '@angular/core'; | ||
import { TableColumnConfigExtended } from './table.service'; | ||
import { TableRow } from './table-api'; | ||
import { FileDownloadService } from '../download-file/service/file-download.service'; | ||
import { map, switchMap } from 'rxjs/operators'; | ||
import { isEmpty, isNil, isString } from 'lodash-es'; | ||
import { Dictionary } from '@hypertrace/common'; | ||
import { NotificationService } from '../notification/notification.service'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class TableCsvDownloaderService { | ||
private readonly csvDownloadRequestSubject: Subject<string> = new Subject<string>(); | ||
public csvDownloadRequest$: Observable<string> = this.csvDownloadRequestSubject.asObservable(); | ||
|
||
public constructor( | ||
private readonly fileDownloadService: FileDownloadService, | ||
private readonly notificationService: NotificationService | ||
) {} | ||
public triggerDownload(tableId: string): void { | ||
this.csvDownloadRequestSubject.next(tableId); | ||
} | ||
|
||
public executeDownload( | ||
dataAndConfigs$: Observable<{ rows: TableRow[]; columnConfigs: TableColumnConfigExtended[] }>, | ||
id?: string | ||
): void { | ||
dataAndConfigs$ | ||
.pipe( | ||
map(({ rows, columnConfigs }) => { | ||
const csvGeneratorMap = new Map( | ||
columnConfigs.filter(column => !isNil(column.csvGenerator)).map(column => [column.id, column.csvGenerator]) | ||
); | ||
|
||
return rows | ||
.map(row => { | ||
let rowValue: Dictionary<string | undefined> = {}; | ||
Array.from(csvGeneratorMap.keys()).forEach(columnKey => { | ||
const value = row[columnKey]; | ||
const csvGenerator = csvGeneratorMap.get(columnKey)!; // Safe to assert here since we are processing columns with valid csv generators only | ||
|
||
const csvContent = csvGenerator.generateSafeCsv(value, row); | ||
|
||
if (!isNil(csvContent)) { | ||
if (isString(csvContent)) { | ||
rowValue[columnKey] = csvContent; | ||
} else { | ||
rowValue = { ...rowValue, ...csvContent }; | ||
} | ||
} | ||
}); | ||
|
||
return rowValue; | ||
}) | ||
.filter(row => !isEmpty(row)); | ||
}), | ||
switchMap((content: Dictionary<string | undefined>[]) => { | ||
if (isEmpty(content)) { | ||
this.notificationService.createInfoToast('No data to download'); | ||
|
||
return EMPTY; | ||
} | ||
|
||
return this.fileDownloadService.downloadAsCsv({ | ||
fileName: `table-data-${id}-${new Date().toISOString()}.csv`, | ||
dataSource: of(content) | ||
}); | ||
}) | ||
) | ||
.subscribe(); | ||
} | ||
} |
Oops, something went wrong.