This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 692e86ab2d TPv2 Add Download Options Dialog to Generic Table (#7472)
692e86ab2d is described below

commit 692e86ab2d439ec5c7d3f2e1a717a6488faa2e6f
Author: Steve Hamrick <[email protected]>
AuthorDate: Mon Aug 26 02:15:52 2024 -0600

    TPv2 Add Download Options Dialog to Generic Table (#7472)
    
    * Add download options dialog to generic table
    
    * Better coverage
    
    * Fix lint
    
    * Fix rebase
---
 .../download-options-dialog.component.html         |  35 ++++++++
 .../download-options-dialog.component.scss         |  18 ++++
 .../download-options-dialog.component.spec.ts      |  86 ++++++++++++++++++
 .../download-options-dialog.component.ts           | 100 +++++++++++++++++++++
 .../generic-table/generic-table.component.spec.ts  |  28 ++++--
 .../generic-table/generic-table.component.ts       |  38 +++++---
 .../traffic-portal/src/app/shared/shared.module.ts |   4 +-
 7 files changed, 288 insertions(+), 21 deletions(-)

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 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>Export Options</h2>
+<form method="dialog" ngNativeValidate (ngSubmit)="onSubmit()">
+       <div class="content" mat-dialog-content>
+               <mat-form-field appearance="fill">
+                       <mat-label>File Name (no extension)</mat-label>
+                       <input name="fileName" matInput type="text" 
[(ngModel)]="fileName" required />
+               </mat-form-field>
+               <mat-form-field appearance="fill">
+                       <mat-label>Delimiter</mat-label>
+                       <input name="delimiter" matInput type="text" 
[(ngModel)]="seperator" required />
+               </mat-form-field>
+               <mat-checkbox name="includeHeaders" 
[(ngModel)]="includeHeaders">Include Headers</mat-checkbox>
+               <mat-checkbox name="includeHidden" 
*ngIf="this.visibleColumns.length !== this.columns.length" 
[(ngModel)]="includeHidden">Include Hidden Columns 
({{this.visibleColumns.length}}/{{this.columns.length}} visible)</mat-checkbox>
+               <mat-checkbox name="includeFiltered" *ngIf="visibleRows !== 
allRows" [(ngModel)]="includeFiltered">Include Filtered Rows 
({{visibleRows}}/{{allRows}} visible)</mat-checkbox>
+               <mat-checkbox name="onlySelected" *ngIf="selectedRows" 
[(ngModel)]="onlySelected">Only Selected Rows ({{selectedRows}}/{{allRows}} 
selected)</mat-checkbox>
+       </div>
+       <div mat-dialog-actions>
+               <button mat-button type="submit">Confirm</button>
+               <button mat-button type="button" 
[mat-dialog-close]="undefined">Cancel</button>
+       </div>
+</form>
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<DownloadOptionsDialogComponent>;
+       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<unknown>[];
+
+       /**
+        * 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<ColDef<unknown>>;
+
+       public columns: Array<ColDef<unknown>>;
+
+       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<DownloadOptionsDialogComponent,
+       CsvExportParams>, @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<unknown>;
        let fixture: ComponentFixture<GenericTableComponent<unknown>>;
        let fuzzySearch: BehaviorSubject<string>;
+       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<T> 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<T> 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,

Reply via email to