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: [