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 7741530c49 Traffic Portal v2 CDNs detail page (#7353)
7741530c49 is described below

commit 7741530c494d797758d226885abee3ed7feda7be
Author: Zach Hoffman <[email protected]>
AuthorDate: Wed Mar 29 13:05:23 2023 -0600

    Traffic Portal v2 CDNs detail page (#7353)
    
    * Add missing entries to TPv2 gitignore
    
    * Traffic Portal v2 CDNs detail page
    
    * Add CRUD scaffolding to test CDN service
    
    * Set the title after asetting the CDN's newness
    
    * linting issues
    
    * Input validation for CDN name and CDN domain name
    
    * Disallow underscore
    
    * Tidy domainName pattern
    
    * Disallow more invalid CDN names and domain names, add tests
---
 experimental/traffic-portal/.gitignore             |  10 +-
 .../traffic-portal/nightwatch/globals/globals.ts   |   4 +
 .../nightwatch/page_objects/cdns/cdnDetail.ts      |  45 +++++++
 .../nightwatch/tests/cdns/detail.spec.ts           |  42 +++++++
 .../traffic-portal/src/app/api/cdn.service.ts      |  63 +++++++++-
 .../src/app/api/testing/cdn.service.ts             |  92 +++++++++++++-
 .../core/cdns/cdn-detail/cdn-detail.component.html |  41 ++++++
 .../core/cdns/cdn-detail/cdn-detail.component.scss |  26 ++++
 .../cdns/cdn-detail/cdn-detail.component.spec.ts   | 113 +++++++++++++++++
 .../core/cdns/cdn-detail/cdn-detail.component.ts   | 139 +++++++++++++++++++++
 .../traffic-portal/src/app/core/core.module.ts     |   3 +
 11 files changed, 572 insertions(+), 6 deletions(-)

diff --git a/experimental/traffic-portal/.gitignore 
b/experimental/traffic-portal/.gitignore
index 4403abee39..c9b66a2fed 100644
--- a/experimental/traffic-portal/.gitignore
+++ b/experimental/traffic-portal/.gitignore
@@ -10,7 +10,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# CDN in a Box for Developers
+/aes.key
+/server.crt
+/server.csr
+/server.key
+
 # compiled output
+*.css
+*.css.map
 /dist
 /.angular
 /.npm
@@ -50,7 +58,7 @@ speed-measure-plugin*.json
 .history/*
 
 # misc
-/.sass-cache
+.sass-cache/
 /connect.lock
 /coverage
 /libpeerconnection.log
diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts 
b/experimental/traffic-portal/nightwatch/globals/globals.ts
index 75f22f4a9f..b9b609ab3d 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -26,6 +26,7 @@ import type { DivisionDetailPageObject } from 
"nightwatch/page_objects/cacheGrou
 import type { DivisionsPageObject } from 
"nightwatch/page_objects/cacheGroups/divisionsTable";
 import type { RegionDetailPageObject } from 
"nightwatch/page_objects/cacheGroups/regionDetail";
 import type { RegionsPageObject } from 
"nightwatch/page_objects/cacheGroups/regionsTable";
+import type { CDNDetailPageObject } from 
"nightwatch/page_objects/cdns/cdnDetail";
 import type { CommonPageObject } from "nightwatch/page_objects/common";
 import type { DeliveryServiceCardPageObject } from 
"nightwatch/page_objects/deliveryServices/deliveryServiceCard";
 import type { DeliveryServiceDetailPageObject } from 
"nightwatch/page_objects/deliveryServices/deliveryServiceDetail";
@@ -87,6 +88,9 @@ declare module "nightwatch" {
                        asnsTable: () => AsnsPageObject;
                        asnDetail: () => AsnDetailPageObject;
                };
+               cdns: {
+                       cdnDetail: () => CDNDetailPageObject;
+               };
                deliveryServices: {
                        deliveryServiceCard: () => 
DeliveryServiceCardPageObject;
                        deliveryServiceDetail: () => 
DeliveryServiceDetailPageObject;
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts
new file mode 100644
index 0000000000..cad2861acd
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/cdns/cdnDetail.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { EnhancedPageObject } from "nightwatch";
+
+/**
+ * Defines the PageObject for CDN Detail.
+ */
+export type CDNDetailPageObject = EnhancedPageObject<{}, typeof 
cdnDetailPageObject.elements>;
+
+const cdnDetailPageObject = {
+       elements: {
+               dnssecEnabled: {
+                       selector: "input[name='dnssecEnabled']"
+               },
+               domainName: {
+                       selector: "input[name='domainName']"
+               },
+               id: {
+                       selector: "input[name='id']"
+               },
+               lastUpdated: {
+                       selector: "input[name='lastUpdated']"
+               },
+               name: {
+                       selector: "input[name='name']"
+               },
+               saveBtn: {
+                       selector: "button[type='submit']"
+               },
+       },
+};
+
+export default cdnDetailPageObject;
diff --git a/experimental/traffic-portal/nightwatch/tests/cdns/detail.spec.ts 
b/experimental/traffic-portal/nightwatch/tests/cdns/detail.spec.ts
new file mode 100644
index 0000000000..6031c8eb15
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/cdns/detail.spec.ts
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+describe("CDN Detail Spec", () => {
+       it("Test CDN", () => {
+               const page = browser.page.cdns.cdnDetail();
+               
browser.url(`${page.api.launchUrl}/core/cdns/${browser.globals.testData.cdn.id}`,
 res => {
+                       browser.assert.ok(res.status === 0);
+                       page.waitForElementVisible("mat-card")
+                               .assert.enabled("@name")
+                               .assert.enabled("@saveBtn")
+                               .assert.not.enabled("@id")
+                               .assert.not.enabled("@lastUpdated")
+                               .assert.valueEquals("@name", 
browser.globals.testData.cdn.name)
+                               .assert.valueEquals("@id", 
String(browser.globals.testData.cdn.id));
+               });
+       });
+
+       it("New CDN", () => {
+               const page = browser.page.cdns.cdnDetail();
+               browser.url(`${page.api.launchUrl}/core/cdns/new`, res => {
+                       browser.assert.ok(res.status === 0);
+                       page.waitForElementVisible("mat-card")
+                               .assert.enabled("@name")
+                               .assert.enabled("@saveBtn")
+                               .assert.not.elementPresent("@id")
+                               .assert.not.elementPresent("@lastUpdated")
+                               .assert.valueEquals("@name", "");
+               });
+       });
+});
diff --git a/experimental/traffic-portal/src/app/api/cdn.service.ts 
b/experimental/traffic-portal/src/app/api/cdn.service.ts
index 3cbd990743..379e26cd22 100644
--- a/experimental/traffic-portal/src/app/api/cdn.service.ts
+++ b/experimental/traffic-portal/src/app/api/cdn.service.ts
@@ -13,7 +13,7 @@
 */
 import { HttpClient } from "@angular/common/http";
 import { Injectable } from "@angular/core";
-import type { ResponseCDN } from "trafficops-types";
+import type { RequestCDN, ResponseCDN } from "trafficops-types";
 
 import { APIService } from "./base-api.service";
 
@@ -48,4 +48,65 @@ export class CDNService extends APIService {
                }
                return this.get<Array<ResponseCDN>>(path).toPromise();
        }
+
+       /**
+        * Deletes a CDN.
+        *
+        * @param cdn The CDN to be deleted, or just its ID.
+        */
+       public async deleteCDN(cdn: ResponseCDN | number): Promise<void> {
+               const id = typeof cdn === "number" ? cdn : cdn.id;
+               return this.delete(`cdns/${id}`).toPromise();
+       }
+
+       /**
+        * Creates a new CDN.
+        *
+        * @param cdn The CDN to create.
+        */
+       public async createCDN(cdn: RequestCDN): Promise<ResponseCDN> {
+               return this.post<ResponseCDN>("cdns", cdn).toPromise();
+       }
+
+       /**
+        * Replaces an existing CDN with the provided new definition of a
+        * CDN.
+        *
+        * @param id The if of the CDN being updated.
+        * @param cdn The new definition of the CDN.
+        */
+       public async updateCDN(id: number, cdn: RequestCDN): 
Promise<ResponseCDN>;
+       /**
+        * Replaces an existing CDN with the provided new definition of a
+        * CDN.
+        *
+        * @param cdn The full new definition of the CDN being
+        * updated.
+        */
+       public async updateCDN(cdn: ResponseCDN): Promise<ResponseCDN>;
+       /**
+        * Replaces an existing CDN with the provided new definition of a
+        * CDN.
+        *
+        * @param cdnOrID The full new definition of the CDN being
+        * updated, or just its ID.
+        * @param payload The new definition of the CDN. This is required if
+        * `cdnOrID` is an ID, and ignored otherwise.
+        */
+       public async updateCDN(cdnOrID: ResponseCDN | number, payload?: 
RequestCDN): Promise<ResponseCDN> {
+               let id;
+               let body;
+               if (typeof(cdnOrID) === "number") {
+                       if (!payload) {
+                               throw new TypeError("invalid call signature - 
missing request payload");
+                       }
+                       body = payload;
+                       id = cdnOrID;
+               } else {
+                       body = cdnOrID;
+                       ({id} = cdnOrID);
+               }
+
+               return this.put<ResponseCDN>(`cdns/${id}`, body).toPromise();
+       }
 }
diff --git a/experimental/traffic-portal/src/app/api/testing/cdn.service.ts 
b/experimental/traffic-portal/src/app/api/testing/cdn.service.ts
index 0d65ab3330..2bcf2afa20 100644
--- a/experimental/traffic-portal/src/app/api/testing/cdn.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/cdn.service.ts
@@ -13,13 +13,14 @@
 */
 
 import { Injectable } from "@angular/core";
-import { ResponseCDN } from "trafficops-types";
+import { RequestCDN, ResponseCDN } from "trafficops-types";
 
 /**
  * CDNService expose API functionality relating to CDNs.
  */
 @Injectable()
 export class CDNService {
+       private lastID = 10;
 
        private readonly cdns = [
                {
@@ -27,14 +28,14 @@ export class CDNService {
                        domainName: "-",
                        id: 1,
                        lastUpdated: new Date(),
-                       name: "ALL"
+                       name: "ALL",
                },
                {
                        dnssecEnabled: false,
                        domainName: "mycdn.test.test",
                        id: 2,
                        lastUpdated: new Date(),
-                       name: "test"
+                       name: "test",
                }
        ];
 
@@ -50,7 +51,7 @@ export class CDNService {
         */
        public async getCDNs(id?: number): Promise<Array<ResponseCDN> | 
ResponseCDN> {
                if (id !== undefined) {
-                       const cdn = this.cdns.find(c=>c.id===id);
+                       const cdn = this.cdns.find(c => c.id === id);
                        if (!cdn) {
                                throw new Error(`no such CDN #${id}`);
                        }
@@ -58,4 +59,87 @@ export class CDNService {
                }
                return this.cdns;
        }
+
+       /**
+        * Deletes a CDN.
+        *
+        * @param cdn The CDN to be deleted, or just its ID.
+        */
+       public async deleteCDN(cdn: ResponseCDN | number): Promise<void> {
+               const id = typeof cdn === "number" ? cdn : cdn.id;
+               const idx = this.cdns.findIndex(c => c.id === id);
+               if (idx < 0) {
+                       throw new Error(`no such CDN: #${id}`);
+               }
+               this.cdns.splice(idx, 1);
+       }
+
+       /**
+        * Creates a new CDN.
+        *
+        * @param cdn The CDN to create.
+        */
+       public async createCDN(cdn: RequestCDN): Promise<ResponseCDN> {
+               const c = {
+                       ...cdn,
+                       id: ++this.lastID,
+                       lastUpdated: new Date(),
+               };
+               this.cdns.push(c);
+               return c;
+       }
+
+       /**
+        * Replaces an existing CDN with the provided new definition of a
+        * CDN.
+        *
+        * @param id The if of the CDN being updated.
+        * @param cdn The new definition of the CDN.
+        */
+       public async updateCDN(id: number, cdn: RequestCDN): 
Promise<ResponseCDN>;
+       /**
+        * Replaces an existing CDN with the provided new definition of a
+        * CDN.
+        *
+        * @param cdn The full new definition of the CDN being
+        * updated.
+        */
+       public async updateCDN(cdn: ResponseCDN): Promise<ResponseCDN>;
+       /**
+        * Replaces an existing CDN with the provided new definition of a
+        * CDN.
+        *
+        * @param cdnOrID The full new definition of the CDN being
+        * updated, or just its ID.
+        * @param payload The new definition of the CDN. This is required if
+        * `cdnOrID` is an ID, and ignored otherwise.
+        */
+       public async updateCDN(cdnOrID: ResponseCDN | number, payload?: 
RequestCDN): Promise<ResponseCDN> {
+               let idx;
+               let cdn;
+               if (typeof cdnOrID === "number") {
+                       if (!payload) {
+                               throw new TypeError("invalid call signature - 
missing request payload");
+                       }
+                       idx = this.cdns.findIndex(c => c.id === cdnOrID);
+                       cdn = {
+                               ...payload,
+                               id: ++this.lastID,
+                               lastUpdated: new Date(),
+                       };
+               } else {
+                       idx = this.cdns.findIndex(c => c.id === cdnOrID.id);
+                       cdn = {
+                               ...cdnOrID,
+                               lastUpdated: new Date()
+                       };
+               }
+
+               if (idx < 0) {
+                       throw new Error(`no such CDN: #${cdnOrID}`);
+               }
+
+               this.cdns[idx] = cdn;
+               return cdn;
+       }
 }
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.html
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.html
new file mode 100644
index 0000000000..a1a122c70f
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.html
@@ -0,0 +1,41 @@
+<!--
+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>
+       <tp-loading *ngIf="!cdn"></tp-loading>
+       <form ngNativeValidate (ngSubmit)="submit($event)" *ngIf="cdn">
+               <mat-card-content>
+                       <mat-form-field>
+                               <mat-label>Name</mat-label>
+                               <input matInput type="text" name="name" 
required [(ngModel)]="cdn.name" pattern="(?!-)[A-Za-z0-9.\-]+(?<!-)">
+                       </mat-form-field>
+                       <mat-form-field *ngIf="!new">
+                               <mat-label>ID</mat-label>
+                               <input matInput type="text" name="id" disabled 
readonly [defaultValue]="cdn.id">
+                       </mat-form-field>
+                       <mat-form-field *ngIf="!new">
+                               <mat-label>Last Updated</mat-label>
+                               <input matInput type="text" name="lastUpdated" 
disabled readonly [defaultValue]="cdn.lastUpdated">
+                       </mat-form-field>
+                       <mat-form-field>
+                               <mat-label>Domain</mat-label>
+                               <input matInput type="text" name="domainName" 
required [(ngModel)]="cdn.domainName" 
pattern="((?!-)(xn--)?[a-z0-9\-_]{0,61}[a-z0-9]{1,1}(?<!-)\.)*(xn--)?(?!-)[a-z0-9\-]{1,61}(?<!-)(\.[a-z]{2,})?">
+                       </mat-form-field>
+                       <mat-checkbox [labelPosition]="'before'" 
name="dnssecEnabled" [(ngModel)]="cdn.dnssecEnabled">DNSSEC 
Enabled</mat-checkbox>
+               </mat-card-content>
+               <mat-card-actions align="end">
+                       <button mat-raised-button type="button" *ngIf="!new" 
color="warn" (click)="delete()">Delete</button>
+                       <button mat-raised-button color="primary" 
type="submit">Save</button>
+               </mat-card-actions>
+       </form>
+</mat-card>
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.scss
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.scss
new file mode 100644
index 0000000000..48848d2385
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.scss
@@ -0,0 +1,26 @@
+/*
+* 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 {
+       margin: 1em auto;
+       width: 80%;
+       min-width: 350px;
+
+       mat-card-content {
+               display: grid;
+               grid-template-columns: 1fr;
+               grid-row-gap: 2em;
+               margin: 1em auto 50px;
+       }
+}
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.spec.ts
new file mode 100644
index 0000000000..31a30389eb
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.spec.ts
@@ -0,0 +1,113 @@
+/*
+* 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 { ComponentFixture, TestBed } from "@angular/core/testing";
+import { MatDialogModule } from "@angular/material/dialog";
+import { ActivatedRoute } from "@angular/router";
+import { RouterTestingModule } from "@angular/router/testing";
+import { ReplaySubject } from "rxjs";
+
+import { APITestingModule } from "src/app/api/testing";
+import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
+
+import { CDNDetailComponent } from "./cdn-detail.component";
+
+describe("CDNDetailComponent", () => {
+       let component: CDNDetailComponent;
+       let fixture: ComponentFixture<CDNDetailComponent>;
+       let route: ActivatedRoute;
+       let paramMap: jasmine.Spy;
+
+       const navSvc = jasmine.createSpyObj([], {
+               headerHidden: new ReplaySubject<boolean>(),
+               headerTitle: new ReplaySubject<string>(),
+       });
+       beforeEach(async () => {
+               await TestBed.configureTestingModule({
+                       declarations: [CDNDetailComponent],
+                       imports: [APITestingModule, RouterTestingModule, 
MatDialogModule],
+                       providers: [{provide: NavigationService, useValue: 
navSvc}],
+               })
+                       .compileComponents();
+
+               route = TestBed.inject(ActivatedRoute);
+               paramMap = spyOn(route.snapshot.paramMap, "get");
+               paramMap.and.returnValue(null);
+               fixture = TestBed.createComponent(CDNDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+       });
+
+       it("rejects invalid CDN names", async () => {
+               paramMap.and.returnValue("new");
+               fixture = TestBed.createComponent(CDNDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               await fixture.whenStable();
+               const form = fixture.nativeElement as HTMLFormElement;
+               expect(form instanceof HTMLFormElement);
+               const nameElement = form.querySelector('[name="name"]') as 
HTMLInputElement;
+               expect(nameElement instanceof HTMLInputElement);
+               const invalidCDNNames = ["-", "_", "^"];
+               for (const cdnName of invalidCDNNames) {
+                       nameElement.value = cdnName;
+                       expect(nameElement.checkValidity()).toBeFalse();
+               }
+       });
+
+       it("rejects invalid CDN domain names", async () => {
+               paramMap.and.returnValue("new");
+               fixture = TestBed.createComponent(CDNDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               await fixture.whenStable();
+               const form = fixture.nativeElement as HTMLFormElement;
+               expect(form instanceof HTMLFormElement);
+               const domainNameElement = 
form.querySelector('[name="domainName"]') as HTMLInputElement;
+               expect(domainNameElement instanceof HTMLInputElement);
+               domainNameElement.value = "-";
+               expect(domainNameElement.checkValidity()).toBeFalse();
+       });
+
+       it("should create", () => {
+               expect(component).toBeTruthy();
+               expect(paramMap).toHaveBeenCalled();
+       });
+
+       it("new cdn", async () => {
+               paramMap.and.returnValue("new");
+
+               fixture = TestBed.createComponent(CDNDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               await fixture.whenStable();
+               expect(paramMap).toHaveBeenCalled();
+               expect(component.cdn).toBeInstanceOf(Object);
+               expect(component.cdn.name).toBe("");
+               expect(component.new).toBeTrue();
+       });
+
+       it("existing cdn", async () => {
+               paramMap.and.returnValue("2");
+
+               fixture = TestBed.createComponent(CDNDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               await fixture.whenStable();
+               expect(paramMap).toHaveBeenCalled();
+               expect(component.cdn).toBeInstanceOf(Object);
+               expect(component.cdn.name).toBe("test");
+               expect(component.new).toBeFalse();
+       });
+});
diff --git 
a/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts
 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts
new file mode 100644
index 0000000000..fc8d14c480
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts
@@ -0,0 +1,139 @@
+/*
+* 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 { Location } from "@angular/common";
+import { Component, OnInit } from "@angular/core";
+import { MatDialog } from "@angular/material/dialog";
+import { ActivatedRoute } from "@angular/router";
+import { ResponseCDN } from "trafficops-types";
+
+import { CDNService } from "src/app/api";
+import {
+       DecisionDialogComponent,
+       DecisionDialogData,
+} from "src/app/shared/dialogs/decision-dialog/decision-dialog.component";
+import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
+
+/**
+ * CDNDetailComponent is the controller for a CDN's "detail" page.
+ */
+@Component({
+       selector: "tp-cdn-detail",
+       styleUrls: ["./cdn-detail.component.scss"],
+       templateUrl: "./cdn-detail.component.html",
+})
+export class CDNDetailComponent implements OnInit {
+       public new = false;
+       public cdn: ResponseCDN = {
+               dnssecEnabled: false,
+               domainName: "",
+               id: -1,
+               lastUpdated: new Date(),
+               name: "",
+       };
+       public showErrors = false;
+       public cdns: Array<ResponseCDN> = [];
+
+       constructor(
+               private readonly route: ActivatedRoute,
+               private readonly api: CDNService,
+               private readonly location: Location,
+               private readonly dialog: MatDialog,
+               private readonly navSvc: NavigationService
+       ) {
+       }
+
+       /**
+        * Angular lifecycle hook where data is initialized.
+        */
+       public async ngOnInit(): Promise<void> {
+               const ID = this.route.snapshot.paramMap.get("id");
+               if (ID === null) {
+                       console.error("missing required route parameter 'id'");
+                       return;
+               }
+
+               const cdnsPromise = this.api.getCDNs().then(cdns => this.cdns = 
cdns);
+               if (ID === "new") {
+                       this.new = true;
+                       this.setTitle();
+                       await cdnsPromise;
+                       return;
+               }
+               const numID = parseInt(ID, 10);
+               if (Number.isNaN(numID)) {
+                       throw new Error(`route parameter 'id' was non-number: 
${ID}`);
+               }
+               await cdnsPromise;
+               const index = this.cdns.findIndex(c => c.id === numID);
+               if (index < 0) {
+                       console.error(`no such CDN: #${ID}`);
+                       return;
+               }
+               this.cdn = this.cdns.splice(index, 1)[0];
+       }
+
+       /**
+        * Sets the title of the page to either "new" or the name of the 
displayed
+        * CDN, depending on the value of
+        * {@link CDNDetailComponent.new}.
+        */
+       private setTitle(): void {
+               const title = this.new ? "New CDN" : `CDN: ${this.cdn.name}`;
+               this.navSvc.headerTitle.next(title);
+       }
+
+       /**
+        * Deletes the CDN.
+        */
+       public async delete(): Promise<void> {
+               if (this.new) {
+                       console.error("Unable to delete new CDN");
+                       return;
+               }
+               const ref = this.dialog.open<DecisionDialogComponent, 
DecisionDialogData, boolean>(
+                       DecisionDialogComponent,
+                       {
+                               data: {
+                                       message: `Are you sure you want to 
delete CDN ${this.cdn.name} (#${this.cdn.id})?`,
+                                       title: "Confirm Delete"
+                               }
+                       }
+               );
+               ref.afterClosed().subscribe(result => {
+                       if (result) {
+                               this.api.deleteCDN(this.cdn);
+                               this.location.replaceState("core/cdns");
+                       }
+               });
+       }
+
+       /**
+        * Submits new/updated CDN.
+        *
+        * @param e HTML form submission event.
+        */
+       public async submit(e: Event): Promise<void> {
+               e.preventDefault();
+               e.stopPropagation();
+               this.showErrors = true;
+               if (this.new) {
+                       this.cdn = await this.api.createCDN(this.cdn);
+                       this.new = false;
+               } else {
+                       this.cdn = await this.api.updateCDN(this.cdn);
+               }
+               this.setTitle();
+       }
+}
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts 
b/experimental/traffic-portal/src/app/core/core.module.ts
index af02d05a7f..e4f0aa3123 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -34,6 +34,7 @@ import { DivisionDetailComponent } from 
"./cache-groups/divisions/detail/divisio
 import { DivisionsTableComponent } from 
"./cache-groups/divisions/table/divisions-table.component";
 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 { ChangeLogsComponent } from "./change-logs/change-logs.component";
 import { LastDaysComponent } from 
"./change-logs/last-days/last-days.component";
 import { CurrentuserComponent } from "./currentuser/currentuser.component";
@@ -70,6 +71,7 @@ export const ROUTES: Routes = [
        { component: RegionDetailComponent, path: "regions/:id" },
        { component: UsersComponent, path: "users" },
        { component: UserDetailsComponent, path: "users/:id"},
+       { component: CDNDetailComponent, path: "cdns/:id" },
        { component: ServersTableComponent, path: "servers" },
        { component: ServerDetailsComponent, path: "servers/:id" },
        { component: DeliveryserviceComponent, path: "deliveryservice/:id" },
@@ -129,6 +131,7 @@ export const ROUTES: Routes = [
                TypesTableComponent,
                TypeDetailComponent,
                ISOGenerationFormComponent,
+               CDNDetailComponent,
        ],
        exports: [],
        imports: [

Reply via email to