Skip to content

Commit

Permalink
feat: CreateCsvDownloaderService (#2367)
Browse files Browse the repository at this point in the history
* feat: CreateCsvDownloaderService

* feat: fixing comments

* feat: fixing comments

* feat: updating the service

* feat: fix prettier

* feat: service refactored

* feat: fix test

* feat: rename fields

* feat: removing unreleated code

* feat: add UT

* feat: fixing comments

---------

Co-authored-by: Patricio Albizu <[email protected]>
Co-authored-by: Patricio Albizu <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2023
1 parent f4ae953 commit 17f4dca
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 3 deletions.
4 changes: 4 additions & 0 deletions projects/components/src/table/table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ export class TableComponent
@Output()
public readonly columnConfigsChange: EventEmitter<TableColumnConfig[]> = new EventEmitter<TableColumnConfig[]>();

@Output()
public readonly visibleColumnsChange: EventEmitter<TableColumnConfig[]> = new EventEmitter<TableColumnConfig[]>();

@Output()
public readonly sortChange: EventEmitter<SortedColumn<TableColumnConfig>> = new EventEmitter<
SortedColumn<TableColumnConfig>
Expand Down Expand Up @@ -731,6 +734,7 @@ export class TableComponent
private updateVisibleColumns(visibleColumnConfigs: TableColumnConfigExtended[]): void {
this.visibleColumnConfigs = visibleColumnConfigs;
this.visibleColumnIds = this.visibleColumnConfigs.map(column => column.id);
this.visibleColumnsChange.next(this.visibleColumnConfigs.filter(column => !this.isStateColumn(column)));
}

private initializeData(): void {
Expand Down
4 changes: 3 additions & 1 deletion projects/dashboards/src/test/dashboard-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
TimeRangeService,
TimeUnit
} from '@hypertrace/common';
import { NotificationService } from '@hypertrace/components';
import { GraphQlRequestService } from '@hypertrace/graphql-client';
import { ModelJson } from '@hypertrace/hyperdash';
import { DashboardManagerService, LoggerService, RENDERER_API } from '@hypertrace/hyperdash-angular';
import { GraphQlQueryEventService, MetadataService } from '@hypertrace/observability';
import { getMockFlexLayoutProviders } from '@hypertrace/test-utils';
import { mockProvider, Spectator } from '@ngneat/spectator/jest';
import { EMPTY, of } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';

export const isValidModelJson = (
spectator: Spectator<unknown>,
Expand Down Expand Up @@ -55,6 +56,7 @@ export const mockDashboardProviders = [
getFilterAttributes: () => of([]),
getAttributeKeyDisplayName: (_: string, attributeKey: string) => of(attributeKey)
}),
mockProvider(NotificationService, { withNotification: jest.fn().mockReturnValue((x: Observable<unknown>) => x) }),
// https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/55803#discussioncomment-1341954
mockProvider(LoggerService, {
warn: jest.fn().mockReturnValue(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import {
FilterBarComponent,
FilterBuilderLookupService,
FilterOperator,
NotificationService,
ToggleGroupComponent
} from '@hypertrace/components';
import { GraphQlRequestService } from '@hypertrace/graphql-client';
import { getMockFlexLayoutProviders, patchRouterNavigateForTest } from '@hypertrace/test-utils';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { EMPTY, NEVER, of } from 'rxjs';
import { EMPTY, NEVER, Observable, of } from 'rxjs';
import { startWith } from 'rxjs/operators';
import { CartesianSeriesVisualizationType } from '../../shared/components/cartesian/chart';
import { ExploreQueryEditorComponent } from '../../shared/components/explore-query-editor/explore-query-editor.component';
Expand Down Expand Up @@ -94,6 +95,7 @@ describe('Explorer component', () => {
getCurrentTimeRange: () => testTimeRange,
getTimeRangeAndChanges: () => NEVER.pipe(startWith(testTimeRange))
}),
mockProvider(NotificationService, { withNotification: jest.fn().mockReturnValue((x: Observable<unknown>) => x) }),
mockProvider(EntitiesGraphqlQueryBuilderService),
{
provide: ActivatedRoute,
Expand Down
3 changes: 3 additions & 0 deletions projects/observability/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,6 @@ export * from './shared/components/bar-gauge/bar-gauge.module';

// Time Range utils
export * from './shared/utils/time-range';

// CSV Downloader Service
export * from './shared/services/global-csv-download/global-csv-download.service';
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ import {
withLatestFrom
} from 'rxjs/operators';
import { AttributeMetadata, toFilterAttributeType } from '../../../graphql/model/metadata/attribute-metadata';
import {
GlobalCsvDownloadDataType,
GlobalCsvDownloadService
} from '../../../services/global-csv-download/global-csv-download.service';
import { MetadataService } from '../../../services/metadata/metadata.service';
import { InteractionHandler } from '../../interaction/interaction-handler';
import { TableWidgetRowInteractionModel } from './selections/table-widget-row-interaction.model';
Expand Down Expand Up @@ -110,6 +114,7 @@ import { TableWidgetModel } from './table-widget.model';
(rowClicked)="this.onRowClicked($event)"
(selectionsChange)="this.onRowSelection($event)"
(columnConfigsChange)="this.onColumnsChange($event)"
(visibleColumnsChange)="this.onVisibleColumnsChange($event)"
>
</ht-table>
</div>
Expand Down Expand Up @@ -154,7 +159,8 @@ export class TableWidgetRendererComponent
@Inject(RENDERER_API) api: RendererApi<TableWidgetModel>,
changeDetectorRef: ChangeDetectorRef,
private readonly metadataService: MetadataService,
private readonly preferenceService: PreferenceService
private readonly preferenceService: PreferenceService,
private readonly globalCsvDownloadService: GlobalCsvDownloadService
) {
super(api, changeDetectorRef);
}
Expand Down Expand Up @@ -498,6 +504,14 @@ export class TableWidgetRendererComponent
this.columnConfigs$ = this.getColumnConfigs();
}

public onVisibleColumnsChange(columns: TableColumnConfig[]): void {
this.globalCsvDownloadService.registerDataSource(`table-widget-renderer-${this.model.id}`, {
type: GlobalCsvDownloadDataType.Table,
columns: columns,
data: this.model.getData()
});
}

public onColumnsChange(columns: TableColumnConfig[]): void {
if (isNonEmptyString(this.model.getId())) {
this.getLocalPreferences().subscribe(preferences =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest';
import {
GlobalCsvDownloadDataType,
GlobalCsvDownloadService,
GlobalCsvDownloadTableDataSource
} from './global-csv-download.service';
import { of } from 'rxjs';
import { FileDownloadService, TableColumnConfig } from '@hypertrace/components';

describe('Global Csv Download Service', () => {
const mockColumnConfigs: TableColumnConfig[] = [
{
id: 'test_1',
title: 'Test_1',
display: 'Test 1'
},
{
id: 'test_2',
title: 'Test_2',
display: 'Test 2'
}
];

const mockModel = {
getData: jest.fn(() =>
of({
data: [],
totalCount: 0
})
)
};

const serviceFactory = createServiceFactory({
service: GlobalCsvDownloadService,
providers: [
mockProvider(FileDownloadService, {
downloadAsCsv: jest.fn().mockReturnValue(of(undefined))
})
]
});

test('Register, get, check, delete and clean data source should work as expected', () => {
const spectator = serviceFactory();
spectator.service.registerDataSource('test', {
type: GlobalCsvDownloadDataType.Table,
columns: mockColumnConfigs,
data: of(mockModel)
});
expect((spectator.service.getRegisteredDataSource('test') as GlobalCsvDownloadTableDataSource).columns).toEqual(
mockColumnConfigs
);

expect(spectator.service.hasRegisteredDataSource('test')).toBe(true);

spectator.service.deleteRegisteredDataSource('test');
expect(spectator.service.hasRegisteredDataSource('test')).toBe(false);

spectator.service.registerDataSource('test', {
type: GlobalCsvDownloadDataType.Table,
columns: mockColumnConfigs,
data: of(mockModel)
});
spectator.service.clearAllDataSource();
expect(spectator.service.hasRegisteredDataSource('test')).toBe(false);
});

test('Download csv should work as expected', () => {
const spectator = serviceFactory();
spectator.service.downloadCsv();
expect(spectator.inject(FileDownloadService).downloadAsCsv).not.toBeCalled();
spectator.service.downloadCsv({ dataSource: of([]), fileName: 'traces.csv' });
expect(spectator.inject(FileDownloadService).downloadAsCsv).toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Injectable } from '@angular/core';
import { Dictionary } from '@hypertrace/common';
import {
CsvDownloadFileConfig,
FileDownloadService,
TableColumnConfig,
TableDataSource,
TableRow
} from '@hypertrace/components';
import { isNil } from 'lodash-es';
import { Observable, throwError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class GlobalCsvDownloadService {
// Note: This service should be use to connect and donwload data from two unrelated components. The component's data to be downloaded should be register and can be executed from a different component.

private readonly csvDataSourceMap: Map<string, GlobalCsvDownloadData> = new Map();

public constructor(private readonly fileDownloadService: FileDownloadService) {}

public downloadCsv(csvDownloadFileConfig?: CsvDownloadFileConfig): Observable<unknown> {
return !isNil(csvDownloadFileConfig)
? this.fileDownloadService.downloadAsCsv(csvDownloadFileConfig)
: throwError('No data available.');
}

public registerDataSource(key: string, source: GlobalCsvDownloadData): void {
this.csvDataSourceMap.set(key, source);
}

public getRegisteredDataSource(key: string): GlobalCsvDownloadData | undefined {
return this.csvDataSourceMap.get(key);
}

public hasRegisteredDataSource(key: string): boolean {
return this.csvDataSourceMap.has(key);
}

public deleteRegisteredDataSource(key: string): void {
if (this.csvDataSourceMap.has(key)) {
this.csvDataSourceMap.delete(key);
}
}

public clearAllDataSource(): void {
this.csvDataSourceMap.clear();
}
}

export type GlobalCsvDownloadData = GlobalCsvDownloadTableDataSource | GlobalCsvDownloadDataSource;

export const enum GlobalCsvDownloadDataType {
Table = 'table',
DataSource = 'datasource'
}
export interface GlobalCsvDownloadTableDataSource {
type: GlobalCsvDownloadDataType.Table;
columns: TableColumnConfig[];
data: Observable<TableDataSource<TableRow>>;
}

export interface GlobalCsvDownloadDataSource {
type: GlobalCsvDownloadDataType.DataSource;
data: Observable<Dictionary<unknown>[]>;
}

0 comments on commit 17f4dca

Please sign in to comment.