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