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
+
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,