diff --git a/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.html b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.html new file mode 100644 index 0000000000..cbc008bebf --- /dev/null +++ b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.html @@ -0,0 +1,35 @@ + + +

Export Options

+
+
+ + File Name (no extension) + + + + Delimiter + + + Include Headers + Include Hidden Columns ({{this.visibleColumns.length}}/{{this.columns.length}} visible) + Include Filtered Rows ({{visibleRows}}/{{allRows}} visible) + Only Selected Rows ({{selectedRows}}/{{allRows}} selected) +
+
+ + +
+
diff --git a/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.scss b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.scss new file mode 100644 index 0000000000..75c24942f9 --- /dev/null +++ b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.scss @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +div.content { + display: grid; +} diff --git a/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.spec.ts b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.spec.ts new file mode 100644 index 0000000000..eadf318563 --- /dev/null +++ b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.spec.ts @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HarnessLoader } from "@angular/cdk/testing"; +import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatCheckboxHarness } from "@angular/material/checkbox/testing"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; + +import { AppUIModule } from "src/app/app.ui.module"; +import { + DownloadOptionsDialogComponent, + DownloadOptionsDialogData +} from "src/app/shared/generic-table/download-options/download-options-dialog.component"; + +let loader: HarnessLoader; +describe("DownloadOptionsComponent", () => { + let component: DownloadOptionsDialogComponent; + let fixture: ComponentFixture; + const data: DownloadOptionsDialogData = { + allRows: 5, + columns: [{ + hide: true + }, { + hide: false + }], + name: "test", + selectedRows: undefined, + visibleRows: 5 + }; + const spyRef = jasmine.createSpyObj("MatDialogRef", ["close"]); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DownloadOptionsDialogComponent ], + imports: [ + AppUIModule, + NoopAnimationsModule + ], + providers: [ + {provide: MatDialogRef, useValue: spyRef}, + {provide: MAT_DIALOG_DATA, useValue: data} + ] + }).compileComponents(); + + fixture = TestBed.createComponent(DownloadOptionsDialogComponent); + component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("defaults set", async () => { + expect(fixture.componentInstance.allRows).toEqual(data.allRows); + expect(fixture.componentInstance.columns).toEqual(data.columns); + expect(fixture.componentInstance.fileName).toEqual(data.name); + expect(fixture.componentInstance.selectedRows).toEqual(data.selectedRows); + expect(fixture.componentInstance.visibleRows).toEqual(data.visibleRows); + + expect(fixture.componentInstance.visibleColumns).toEqual(data.columns.filter(c => !c.hide)); + expect(fixture.componentInstance.columns).toEqual(data.columns); + }); + + it("default submission", async () => { + const cbs = await loader.getAllHarnesses(MatCheckboxHarness); + expect(cbs.length).toBe(2); + + fixture.componentInstance.onSubmit(); + expect(spyRef.close.calls.count()).toBe(1); + }); +}); diff --git a/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.ts b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.ts new file mode 100644 index 0000000000..af19bdfb09 --- /dev/null +++ b/experimental/traffic-portal/src/app/shared/generic-table/download-options/download-options-dialog.component.ts @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Inject } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { ColDef, CsvExportParams } from "ag-grid-community"; + +/** + * Data passed to DownloadOptionsComponent from the grid + */ +export interface DownloadOptionsDialogData { + name: string; + + columns: ColDef[]; + + /** + * Number of rows selected, should be undefined when only a single. + */ + selectedRows: number | undefined; + + visibleRows: number; + + allRows: number; +} + +/** + * Controller for the DownloadOptions component. + */ +@Component({ + selector: "tp-download-options", + styleUrls: ["./download-options-dialog.component.scss"], + templateUrl: "./download-options-dialog.component.html" +}) +export class DownloadOptionsDialogComponent { + public fileName: string; + + public visibleColumns: Array>; + + public columns: Array>; + + public includeHidden = false; + public includeHeaders = true; + + public includeFiltered = false; + + public onlySelected = false; + + /** + * Number of selected rows, undefined if single selection. + */ + public selectedRows: number | undefined; + public allRows: number; + public visibleRows: number; + + /** 'C'SV delimiter */ + public seperator = ","; + + constructor(private readonly dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) data: DownloadOptionsDialogData) { + this.fileName = data.name; + this.selectedRows = data.selectedRows; + this.allRows = data.allRows; + this.visibleRows = data.visibleRows; + this.visibleColumns = []; + this.columns = []; + for(const col of data.columns) { + if(!col.hide) { + this.visibleColumns.push(col); + } + this.columns.push(col); + } + } + + /** + * Called when submitting the form, converts data into export params. + */ + public onSubmit(): void { + const params: CsvExportParams = { + allColumns: this.includeHidden, + columnSeparator: this.seperator, + exportedRows: this.includeFiltered ? "all" : "filteredAndSorted", + fileName: `${this.fileName}.csv`, + onlySelected: this.onlySelected, + skipColumnHeaders: !this.includeHeaders, + }; + this.dialogRef.close(params); + } + +} diff --git a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.spec.ts b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.spec.ts index 5cdf362596..bd17b040e2 100644 --- a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.spec.ts +++ b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.spec.ts @@ -13,12 +13,19 @@ */ import { type ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialog, MatDialogModule } from "@angular/material/dialog"; import { MatMenuModule } from "@angular/material/menu"; import { Params } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { AgGridModule } from "ag-grid-angular"; -import type { CellContextMenuEvent, ColDef, GridApi, RowNode, ValueGetterParams } from "ag-grid-community"; -import { BehaviorSubject } from "rxjs"; +import type { + CellContextMenuEvent, + ColDef, + GridApi, + RowNode, + ValueGetterParams +} from "ag-grid-community"; +import { BehaviorSubject, of } from "rxjs"; import { type ContextMenuAction, GenericTableComponent, getColType, ContextMenuItem } from "./generic-table.component"; @@ -136,18 +143,22 @@ describe("GenericTableComponent", () => { let component: GenericTableComponent; let fixture: ComponentFixture>; let fuzzySearch: BehaviorSubject; + const dialogSpy = jasmine.createSpyObj("MatDialog", ["open", "afterClosed"]); beforeEach(async () => { fuzzySearch = new BehaviorSubject(""); await TestBed.configureTestingModule({ declarations: [ GenericTableComponent, - ], imports: [ AgGridModule, RouterTestingModule, - MatMenuModule + MatMenuModule, + MatDialogModule + ], + providers: [ + { provide: MatDialog, useValue: dialogSpy } ] }).compileComponents(); @@ -330,12 +341,11 @@ describe("GenericTableComponent", () => { it("triggers a download of CSV data properly", async () => { component.selected = {}; await fixture.whenStable(); - const spy = spyOn(component.gridOptions.api as GridApi, "exportDataAsCsv"); - component.download(); - expect(spy).toHaveBeenCalledWith({onlySelected: false}); - component.context = "test-context"; + dialogSpy.open.and.returnValue({afterClosed: () => of({fileName: "test.csv"})}); + const exportSpy = spyOn(component.gridOptions.api as GridApi, "exportDataAsCsv"); component.download(); - expect(spy).toHaveBeenCalledWith({fileName: "test-context.csv", onlySelected: false}); + expect(dialogSpy.open.calls.count()).toBe(1); + expect(exportSpy).toHaveBeenCalledWith({fileName: "test.csv"}); }); it("checks if a menu action is disabled", async () => { diff --git a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts index 61b7c69c60..6701f7d087 100644 --- a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts +++ b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts @@ -23,6 +23,7 @@ import { Output, ViewChild } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; import { ActivatedRoute, type ParamMap, type Params, Router } from "@angular/router"; import type { CellContextMenuEvent, @@ -30,7 +31,6 @@ import type { ColGroupDef, Column, ColumnApi, - CsvExportParams, DateFilterModel, FilterChangedEvent, GridApi, @@ -43,6 +43,7 @@ import type { } from "ag-grid-community"; import type { BehaviorSubject, Subscription } from "rxjs"; +import { DownloadOptionsDialogComponent } from "src/app/shared/generic-table/download-options/download-options-dialog.component"; import { fuzzyScore } from "src/app/utils"; import { LoggingService } from "../logging.service"; @@ -406,7 +407,10 @@ export class GenericTableComponent implements OnInit, OnDestroy { return (this.columnAPI.getColumns() ?? []).reverse(); } - constructor(private readonly router: Router, private readonly route: ActivatedRoute, private readonly log: LoggingService) { + constructor(private readonly router: Router, + private readonly route: ActivatedRoute, + private readonly dialog: MatDialog, + private readonly log: LoggingService) { this.gridOptions = { defaultColDef: { filter: true, @@ -829,15 +833,27 @@ export class GenericTableComponent implements OnInit, OnDestroy { * Downloads the table data as a CSV file. */ public download(): void { - const params: CsvExportParams = { - onlySelected: this.gridAPI.getSelectedNodes().length > 0, - }; - - if (this.context) { - params.fileName = `${this.context}.csv`; - } - - this.gridAPI.exportDataAsCsv(params); + const nodes = this.gridAPI.getSelectedNodes(); + const model = this.gridAPI.getModel(); + let visible = 0; + let all = 0; + model.forEachNode(rowNode => { + if(rowNode.displayed) { + visible++; + } + all++; + }); + this.dialog.open(DownloadOptionsDialogComponent, { + data: { + allRows: all, + columns: this.gridAPI.getColumnDefs() ?? [], + name: this.context, + selectedRows: nodes.length > 0 ? nodes.length : undefined, + visibleRows: visible + } + }).afterClosed().subscribe(value => { + this.gridAPI.exportDataAsCsv(value); + }); } /** diff --git a/experimental/traffic-portal/src/app/shared/shared.module.ts b/experimental/traffic-portal/src/app/shared/shared.module.ts index d271e98a21..84bc606a81 100644 --- a/experimental/traffic-portal/src/app/shared/shared.module.ts +++ b/experimental/traffic-portal/src/app/shared/shared.module.ts @@ -17,6 +17,7 @@ import { NgModule } from "@angular/core"; import { RouterModule } from "@angular/router"; import { AppUIModule } from "src/app/app.ui.module"; +import { DownloadOptionsDialogComponent } from "src/app/shared/generic-table/download-options/download-options-dialog.component"; import { TpHeaderComponent } from "src/app/shared/navigation/tp-header/tp-header.component"; import { TpSidebarComponent } from "src/app/shared/navigation/tp-sidebar/tp-sidebar.component"; @@ -64,7 +65,8 @@ import { CustomvalidityDirective } from "./validation/customvalidity.directive"; TextDialogComponent, DecisionDialogComponent, CollectionChoiceDialogComponent, - ImportJsonTxtComponent + ImportJsonTxtComponent, + DownloadOptionsDialogComponent ], exports: [ AlertComponent,