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

ocket8888 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 d8352bbfc1 Traffic Portal v2 CDNs table page (#7459)
d8352bbfc1 is described below

commit d8352bbfc1533b78d3b4cd2c236351f743a6e1cc
Author: Zach Hoffman <[email protected]>
AuthorDate: Mon Jul 3 13:03:18 2023 -0600

    Traffic Portal v2 CDNs table page (#7459)
    
    * Traffic Portal v2 CDNs table page
    
    * Update JSDoc
    
    * Remove unused attribute
    
    * Use anchor tag for links
    
    * Grammar
    
    * Check for array type
    
    * Enable profiles link
    
    * Enable servers link
    
    * Accept either ResponseCDN or a CDN ID
    
    * Disable unimplemented snapshot diff
    
    * queueCDNUpates() can accept an ID
    
    * Add queueUpdates() to mock CDN service
    
    * Separate field for query parameters
    
    * Manage -> View
    
    * Do not hard-code action name
    
    * Implement `[doubleClickLink]`
    
    * reformat
    
    * Use queueServerUpdates() instead of queueCDNUpdates()
    
    * Regex for Cleard -> Cleared
    
    * Field name is domainName
    
    * Add CDNTableComponent unit tests
    
    * Reformat TreeNavNode[] literal
    
    * Add CDNs table to the sidebar
    
    * Error message wording
    
    * Add a test for queueing CDN updates
    
    * ng lint --fix
    
    * table-page-content -> page-content for #7582 changes
---
 .../core/cdns/cdn-table/cdn-table.component.html   |  29 +++
 .../core/cdns/cdn-table/cdn-table.component.scss   |  13 +
 .../cdns/cdn-table/cdn-table.component.spec.ts     |  99 ++++++++
 .../app/core/cdns/cdn-table/cdn-table.component.ts | 276 +++++++++++++++++++++
 .../traffic-portal/src/app/core/core.module.ts     |   3 +
 .../app/shared/navigation/navigation.service.ts    |  69 +++---
 6 files changed, 458 insertions(+), 31 deletions(-)

diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.html
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.html
new file mode 100644
index 0000000000..3160ba2037
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.html
@@ -0,0 +1,29 @@
+<!--
+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.
+-->
+<mat-card class="page-content">
+       <div class="search-container">
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search CDNs" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy 
Search" [formControl]="fuzzControl" (input)="updateURL()"/>
+       </div>
+       <tp-generic-table
+               [data]="cdns | async"
+               [doubleClickLink]="doubleClickLink"
+               [cols]="columnDefs"
+               [fuzzySearch]="fuzzySubject"
+               context="cdns"
+               [contextMenuItems]="contextMenuItems"
+               (contextMenuAction)="handleContextMenu($event)">
+       </tp-generic-table>
+</mat-card>
+
+<a class="page-fab" mat-fab title="Create a new CDN" 
*ngIf="auth.hasPermission('CDN:CREATE')" 
routerLink="new"><mat-icon>add</mat-icon></a>
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.scss
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.scss
new file mode 100644
index 0000000000..ebe77042d3
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.scss
@@ -0,0 +1,13 @@
+/*
+* 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.
+*/
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.spec.ts
new file mode 100644
index 0000000000..8ffed35515
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.spec.ts
@@ -0,0 +1,99 @@
+/*
+* 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 { HttpClientModule } from "@angular/common/http";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ReactiveFormsModule } from "@angular/forms";
+import { MatButtonHarness } from "@angular/material/button/testing";
+import { MatDialogModule } from "@angular/material/dialog";
+import { MatDialogHarness } from "@angular/material/dialog/testing";
+import { MatSelectModule } from "@angular/material/select";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { RouterTestingModule } from "@angular/router/testing";
+import { ReplaySubject } from "rxjs";
+import { ResponseCDN } from "trafficops-types";
+
+import { CDNService } from "src/app/api";
+import { APITestingModule } from "src/app/api/testing";
+import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
+
+import { CDNTableComponent } from "./cdn-table.component";
+
+const sampleCDN: ResponseCDN = {
+       dnssecEnabled: false,
+       domainName: "*",
+       id: 2,
+       lastUpdated: new Date(),
+       name: "*",
+};
+
+describe("CDNTableComponent", () => {
+       let component: CDNTableComponent;
+       let fixture: ComponentFixture<CDNTableComponent>;
+       let loader: HarnessLoader;
+
+       const navService = jasmine.createSpyObj([],{headerHidden: new 
ReplaySubject<boolean>(), headerTitle: new ReplaySubject<string>()});
+
+       beforeEach(async () => {
+               await TestBed.configureTestingModule({
+                       declarations: [CDNTableComponent],
+                       imports: [
+                               APITestingModule,
+                               HttpClientModule,
+                               ReactiveFormsModule,
+                               RouterTestingModule,
+                               MatDialogModule,
+                               NoopAnimationsModule,
+                               MatSelectModule,
+                       ],
+                       providers: [
+                               {provide: NavigationService, useValue: 
navService},
+                       ],
+               })
+                       .compileComponents();
+
+               fixture = TestBed.createComponent(CDNTableComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
+       });
+
+       it("should create", () => {
+               expect(component).toBeTruthy();
+       });
+
+       it("queues CDN updates", async () => {
+               component = fixture.componentInstance;
+               const service = TestBed.inject(CDNService);
+               const queueSpy = spyOn(service, "queueServerUpdates");
+               expect(queueSpy).not.toHaveBeenCalled();
+
+               let dialogs = await loader.getAllHarnesses(MatDialogHarness);
+               expect(dialogs.length).toBe(0);
+
+               component.handleContextMenu({action: "queue", data: sampleCDN});
+               dialogs = await loader.getAllHarnesses(MatDialogHarness);
+               expect(dialogs.length).toBe(1);
+               const dialog = dialogs[0];
+               const buttons = await dialog.getAllHarnesses(MatButtonHarness);
+               expect(buttons.length).toBe(2);
+               const button = buttons[0];
+               await button.click();
+
+               expect(queueSpy).toHaveBeenCalledTimes(1);
+       });
+
+});
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts
new file mode 100644
index 0000000000..a5d6d07cb0
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts
@@ -0,0 +1,276 @@
+/*
+* 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, OnInit } from "@angular/core";
+import { FormControl } from "@angular/forms";
+import { MatDialog } from "@angular/material/dialog";
+import { ActivatedRoute, Params } from "@angular/router";
+import { ColDef } from "ag-grid-community";
+import { BehaviorSubject } from "rxjs";
+import { AlertLevel, ResponseCDN } from "trafficops-types";
+
+import { CDNService } from "src/app/api";
+import { AlertService } from "src/app/shared/alert/alert.service";
+import { CurrentUserService } from 
"src/app/shared/current-user/current-user.service";
+import {
+       DecisionDialogComponent,
+       DecisionDialogData
+} from "src/app/shared/dialogs/decision-dialog/decision-dialog.component";
+import type {
+       ContextMenuActionEvent,
+       ContextMenuItem,
+       DoubleClickLink
+} from "src/app/shared/generic-table/generic-table.component";
+
+/**
+ * CDNTableComponent is the controller for the "CDNs" table.
+ */
+@Component({
+       selector: "tp-cdn-table",
+       styleUrls: ["./cdn-table.component.scss"],
+       templateUrl: "./cdn-table.component.html",
+})
+export class CDNTableComponent implements OnInit {
+       public cdns: Promise<ResponseCDN[]>;
+
+       /* Definitions of the table's columns according to the ag-grid API */
+       public columnDefs: ColDef[] = [
+               {
+                       field: "dnssecEnabled",
+                       filter: "tpBooleanFilter",
+                       headerName: "DNSSEC Enabled",
+                       hide: false
+               },
+               {
+                       field: "domainName",
+                       filter: "agTextColumnFilter",
+                       headerName: "Domain",
+                       hide: false,
+               },
+               {
+                       field: "id",
+                       filter: "agNumberColumnFilter",
+                       headerName: "ID",
+                       hide: true,
+               },
+               {
+                       field: "lastUpdated",
+                       filter: "agDateColumnFilter",
+                       headerName: "Last Updated",
+                       hide: true
+               },
+               {
+                       field: "name",
+                       filter: "agTextColumnFilter",
+                       headerName: "Name",
+                       hide: false,
+               },
+       ];
+
+       /** Defines what the table should do when a row is double-clicked. */
+       public doubleClickLink: DoubleClickLink<ResponseCDN> = {
+               href: (row: ResponseCDN): string => `/core/cdns/${row.id}`
+       };
+
+       /**
+        * Definitions for the context menu items (which act on augmented
+        * CDN data).
+        */
+       public contextMenuItems: Array<ContextMenuItem<ResponseCDN>> = [
+               {
+                       href: (selectedRow): string => 
`/core/cdns/${selectedRow.id}`,
+                       name: "Open in New Tab",
+                       newTab: true
+               },
+               {
+                       href: (selectedRow): string => 
`/core/cdns/${selectedRow.id}`,
+                       name: "Edit"
+               },
+               {
+                       action: "delete",
+                       multiRow: false,
+                       name: "Delete",
+               },
+               {
+                       action: "snapshot-diff",
+                       disabled: (): true => true,
+                       multiRow: false,
+                       name: "Diff Snapshot",
+               },
+               {
+                       action: "queue",
+                       multiRow: false,
+                       name: "Queue Server Updates"
+               },
+               {
+                       action: "dequeue",
+                       multiRow: false,
+                       name: "Clear Queued Updates"
+               },
+               {
+                       disabled: (): true => true,
+                       href: (selectedRow): string => 
`/core/cdns/${selectedRow.id}/dnssec-keys`,
+                       name: "Manage DNSSEC Keys"
+               },
+               {
+                       disabled: (): true => true,
+                       href: (selectedRow): string => 
`/core/cdns/${selectedRow.id}/federations`,
+                       name: "Manage Federations"
+               },
+               {
+                       disabled: (): true => true,
+                       href: (selectedRow): string => 
`/core/cdns/${selectedRow.id}/delivery-services`,
+                       name: "Manage Delivery Services"
+               },
+               {
+                       href: "/core/profiles",
+                       name: "View Profiles",
+                       queryParams: (selectedRow): Params => ({cdnName: 
selectedRow.name}),
+               },
+               {
+                       href: "/core/servers",
+                       name: "View Servers",
+                       queryParams: (selectedRow): Params => ({cdnName: 
selectedRow.name}),
+               },
+               {
+                       disabled: (): true => true,
+                       href: (selectedRow): string => 
`/core/cdns/${selectedRow.id}/notifications`,
+                       name: "Manage Notifications"
+               },
+       ];
+
+       /**
+        * A subject that child components can subscribe to for access to the 
fuzzy
+        * search query text.
+        */
+       public fuzzySubject: BehaviorSubject<string>;
+
+       /** Form controller for the user search input. */
+       public fuzzControl: FormControl = new FormControl("");
+
+       constructor(
+               private readonly alerts: AlertService,
+               private readonly api: CDNService,
+               public readonly auth: CurrentUserService,
+               private readonly dialog: MatDialog,
+               private readonly route: ActivatedRoute,
+       ) {
+               this.fuzzySubject = new BehaviorSubject<string>("");
+               this.cdns = this.api.getCDNs();
+       }
+
+       /** Initializes table data, loading it from Traffic Ops. */
+       public ngOnInit(): void {
+               this.route.queryParamMap.subscribe(
+                       m => {
+                               const search = m.get("search");
+                               if (search) {
+                                       
this.fuzzControl.setValue(decodeURIComponent(search));
+                                       this.updateURL();
+                               }
+                       }
+               );
+       }
+
+       /** Update the URL's 'search' query parameter for the user's search 
input. */
+       public updateURL(): void {
+               this.fuzzySubject.next(this.fuzzControl.value);
+       }
+
+       /**
+        * Queues or clears updates on a group of CDNs.
+        *
+        * @param cdn The CDN on which to queue updates.
+        * @param queue Whether updates should be queued (`true`) or cleared
+        * (`false`).
+        */
+       private async queueUpdates(cdn: ResponseCDN | number, queue: boolean = 
true): Promise<void> {
+               if (typeof cdn === "number") {
+                       cdn = await this.api.getCDNs(cdn);
+               }
+               const readableAction = queue ? "Queue" : "Clear";
+               const title = `${readableAction} Updates on ${cdn.name}?`;
+               const ref = this.dialog.open<DecisionDialogComponent, 
DecisionDialogData, boolean>(DecisionDialogComponent, {
+                       data: {
+                               message: `Are you sure you want to 
${readableAction.toLowerCase()} server updates for all of the ${cdn.name} 
servers?`,
+                               title,
+                       }
+               });
+               if (!await ref.afterClosed().toPromise()) {
+                       return;
+               }
+               if (queue) {
+                       await this.api.queueServerUpdates(cdn);
+               } else {
+                       await this.api.dequeueServerUpdates(cdn);
+               }
+               this.alerts.newAlert(
+                       AlertLevel.SUCCESS,
+                       `${readableAction.replace(/(?<!e)$/, "e")}d CDN server 
updates`,
+               );
+       }
+
+       /**
+        * Asks the user for confirmation before deleting a CDN.
+        *
+        * @param cdn The CDN (potentially) being deleted.
+        */
+       private async delete(cdn: ResponseCDN | number): Promise<void> {
+               if (typeof cdn === "number") {
+                       cdn = await this.api.getCDNs(cdn);
+               }
+               const ref = this.dialog.open<DecisionDialogComponent, 
DecisionDialogData, boolean>(DecisionDialogComponent, {
+                       data: {
+                               message: `Are you sure you want to delete the 
${cdn.name} CDN?`,
+                               title: `Delete ${cdn.name}`
+                       }
+               });
+               if (await ref.afterClosed().toPromise()) {
+                       await this.api.deleteCDN(cdn);
+                       this.cdns = this.api.getCDNs();
+               }
+       }
+
+       /**
+        * Handles a context menu event.
+        *
+        * @param a The action selected from the context menu.
+        */
+       public handleContextMenu(a: ContextMenuActionEvent<ResponseCDN>): void {
+               switch (a.action) {
+                       case "queue":
+                               if (Array.isArray(a.data)) {
+                                       console.error("cannot queue multiple 
cdns at once:", a.data);
+                                       return;
+                               }
+                               this.queueUpdates(a.data);
+                               break;
+                       case "dequeue":
+                               if (Array.isArray(a.data)) {
+                                       console.error("cannot dequeue multiple 
cdns at once:", a.data);
+                                       return;
+                               }
+                               this.queueUpdates(a.data, false);
+                               break;
+                       case "delete":
+                               if (Array.isArray(a.data)) {
+                                       console.error("cannot delete multiple 
cdns at once:", a.data);
+                                       return;
+                               }
+                               this.delete(a.data);
+                               break;
+                       default:
+                               console.error("unrecognized context menu 
action:", a.action);
+               }
+       }
+}
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts 
b/experimental/traffic-portal/src/app/core/core.module.ts
index 41e9168af4..fa0757870c 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -35,6 +35,7 @@ import { DivisionsTableComponent } from 
"./cache-groups/divisions/table/division
 import { RegionDetailComponent } from 
"./cache-groups/regions/detail/region-detail.component";
 import { RegionsTableComponent } from 
"./cache-groups/regions/table/regions-table.component";
 import { CDNDetailComponent } from "./cdns/cdn-detail/cdn-detail.component";
+import { CDNTableComponent } from "./cdns/cdn-table/cdn-table.component";
 import { ChangeLogsComponent } from "./change-logs/change-logs.component";
 import { LastDaysComponent } from 
"./change-logs/last-days/last-days.component";
 import { CurrentuserComponent } from "./currentuser/currentuser.component";
@@ -81,6 +82,7 @@ export const ROUTES: Routes = [
        { component: RegionDetailComponent, path: "regions/:id" },
        { component: UsersComponent, path: "users" },
        { component: UserDetailsComponent, path: "users/:id"},
+       { component: CDNTableComponent, path: "cdns" },
        { component: CDNDetailComponent, path: "cdns/:id" },
        { component: ServersTableComponent, path: "servers" },
        { component: ServerDetailsComponent, path: "servers/:id" },
@@ -157,6 +159,7 @@ export const ROUTES: Routes = [
                StatusesTableComponent,
                StatusDetailsComponent,
                ISOGenerationFormComponent,
+               CDNTableComponent,
                CDNDetailComponent,
                ParametersTableComponent,
                ParameterDetailComponent,
diff --git 
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts 
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
index 28d20490bd..f76aa12b93 100644
--- 
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
+++ 
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
@@ -112,39 +112,46 @@ export class NavigationService {
                        name: "Dashboard"
                }, {
                        children: [{
-                               href: "/core/servers",
-                               name: "Servers"
-                       }, {
-                               href: "/core/phys-locs",
-                               name: "Physical Locations"
-                       },
-                       {
-                               href: "/core/statuses",
-                               name: "Statuses"
-                       },
-                       {
-                               href: "/core/capabilities",
-                               name: "Capabilities",
-                       },
-                       {
-                               children: [{
-                                       href: "/core/cache-groups",
-                                       name: "Cache Groups"
-                               }, {
-                                       href: "/core/coordinates",
-                                       name: "Coordinates"
-                               }, {
-                                       href: "/core/divisions",
-                                       name: "Divisions"
-                               }, {
-                                       href: "/core/regions",
-                                       name: "Regions"
+                               href: "/core/cdns",
+                               name: "CDNs"
+                       }],
+                       name: "CDNs",
+               }, {
+                       children: [
+                               {
+                                       href: "/core/servers",
+                                       name: "Servers"
                                }, {
-                                       href: "/core/asns",
-                                       name: "ASNs"
+                                       href: "/core/phys-locs",
+                                       name: "Physical Locations"
+                               },
+                               {
+                                       href: "/core/statuses",
+                                       name: "Statuses"
+                               },
+                               {
+                                       href: "/core/capabilities",
+                                       name: "Capabilities",
+                               },
+                               {
+                                       children: [{
+                                               href: "/core/cache-groups",
+                                               name: "Cache Groups"
+                                       }, {
+                                               href: "/core/coordinates",
+                                               name: "Coordinates"
+                                       }, {
+                                               href: "/core/divisions",
+                                               name: "Divisions"
+                                       }, {
+                                               href: "/core/regions",
+                                               name: "Regions"
+                                       }, {
+                                               href: "/core/asns",
+                                               name: "ASNs"
+                                       }],
+                                       name: "Cache Groups"
                                }],
-                               name: "Cache Groups"
-                       }],
                        name: "Servers"
                }, {
                        children: [

Reply via email to