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

shamrick 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 54e509bfe6 Yuengling profile table parity (#7434)
54e509bfe6 is described below

commit 54e509bfe60b073cb488a088c22c2337b35dff9b
Author: Kannan.G.B <[email protected]>
AuthorDate: Tue Apr 18 21:58:27 2023 +0530

    Yuengling profile table parity (#7434)
    
    * navigation for profile
    
    * profile table for parity CDN-19162
    
    * latest changes
    
    * comment addressed
    
    * duplicate configuration removed, older restored
    
    * config order updated
    
    * e2e test
    
    * lint fix
    
    * added profiles in sidebar object
    
    * container corrected
---
 .../traffic-portal/nightwatch/globals/globals.ts   |  21 +++
 .../nightwatch/page_objects/common.ts              |   1 +
 .../page_objects/profiles/profilesTable.ts         |  46 +++++++
 .../nightwatch/tests/profiles/table.spec.ts        |  26 ++++
 .../traffic-portal/src/app/api/profile.service.ts  |  24 +++-
 .../src/app/api/testing/profile.service.ts         |  36 ++++-
 .../traffic-portal/src/app/core/core.module.ts     |   3 +
 .../profile-table/profile-table.component.html     |  27 ++++
 .../profile-table/profile-table.component.scss     |  13 ++
 .../profile-table/profile-table.component.spec.ts  | 126 ++++++++++++++++++
 .../profile-table/profile-table.component.ts       | 148 +++++++++++++++++++++
 .../app/shared/navigation/navigation.service.ts    |   4 +
 12 files changed, 472 insertions(+), 3 deletions(-)

diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts 
b/experimental/traffic-portal/nightwatch/globals/globals.ts
index 5db7fdc9a9..afcd947938 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -32,6 +32,7 @@ import type { DeliveryServiceCardPageObject } from 
"nightwatch/page_objects/deli
 import type { DeliveryServiceDetailPageObject } from 
"nightwatch/page_objects/deliveryServices/deliveryServiceDetail";
 import type { DeliveryServiceInvalidPageObject } from 
"nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs";
 import type { LoginPageObject } from "nightwatch/page_objects/login";
+import type { ProfilePageObject } from 
"nightwatch/page_objects/profiles/profilesTable";
 import type { PhysLocDetailPageObject } from 
"nightwatch/page_objects/servers/physLocDetail";
 import type { PhysLocTablePageObject } from 
"nightwatch/page_objects/servers/physLocTable";
 import type { ServersPageObject } from 
"nightwatch/page_objects/servers/servers";
@@ -65,6 +66,9 @@ import {
        ResponseCoordinate,
        RequestCoordinate,
        RequestType,
+       ResponseProfile,
+       RequestProfile,
+       ProfileType
 } from "trafficops-types";
 
 import * as config from "../config.json";
@@ -98,6 +102,9 @@ declare module "nightwatch" {
                        deliveryServiceInvalidationJobs: () => 
DeliveryServiceInvalidPageObject;
                };
                login: () => LoginPageObject;
+               profiles: {
+                       profileTable: () => ProfilePageObject;
+               };
                servers: {
                        physLocDetail: () => PhysLocDetailPageObject;
                        physLocTable: () => PhysLocTablePageObject;
@@ -144,6 +151,7 @@ export interface CreatedData {
        steeringDS: ResponseDeliveryService;
        tenant: ResponseTenant;
        type: TypeFromResponse;
+       profile: ResponseProfile;
 }
 
 const testData = {};
@@ -382,6 +390,19 @@ const globals = {
                        console.log(`Successfully created Type 
${respType.name}`);
                        data.type = respType;
 
+                       const profile: RequestProfile = {
+                               cdn: 1,
+                               description: "blah",
+                               name: `profile${globals.uniqueString}`,
+                               routingDisabled: false,
+                               type: ProfileType.ATS_PROFILE,
+                       };
+                       url = `${apiUrl}/profiles`;
+                       resp = await client.post(url, JSON.stringify(profile));
+                       const respProfile: ResponseProfile = resp.data.response;
+                       console.log(`Successfully created Profile 
${respProfile.name}`);
+                       data.profile = respProfile;
+
                } catch(e) {
                        console.error("Request for", url, "failed:", (e as 
AxiosError).message);
                        throw e;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/common.ts 
b/experimental/traffic-portal/nightwatch/page_objects/common.ts
index 9c4b975a6f..091b647a65 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/common.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/common.ts
@@ -59,6 +59,7 @@ const commonPageObject = {
                                otherContainer: "[aria-label='Toggle Other']",
                                physicalLocations: "[aria-label='Navigate to 
Physical Locations']",
                                profile: "[aria-label='Navigate to My 
Profile']",
+                               profiles: "[aria-label='Navigate to Profiles']",
                                regions: "[aria-label='Navigate to Regions']",
                                servers: "[aria-label='Navigate to Servers']",
                                serversContainer: "[aria-label='Toggle 
Servers']",
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/profiles/profilesTable.ts 
b/experimental/traffic-portal/nightwatch/page_objects/profiles/profilesTable.ts
new file mode 100644
index 0000000000..60e36b9cb9
--- /dev/null
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/profiles/profilesTable.ts
@@ -0,0 +1,46 @@
+/*
+ * 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, EnhancedSectionInstance, NightwatchAPI } from 
"nightwatch";
+
+import { TABLE_COMMANDS, TableSectionCommands } from "../../globals/tables";
+
+/**
+ * Defines the Profiles table commands
+ */
+type ProfileTableCommands = TableSectionCommands;
+
+/**
+ * Defines the Page Object for the Profiles page.
+ */
+export type ProfilePageObject = EnhancedPageObject<{}, {},
+EnhancedSectionInstance<ProfileTableCommands>>;
+
+const profilePageObject = {
+       api: {} as NightwatchAPI,
+       sections: {
+               profileTable: {
+                       commands: {
+                               ...TABLE_COMMANDS
+                       },
+                       elements: {},
+                       selector: "mat-card"
+               }
+       },
+       url(): string {
+               return `${this.api.launchUrl}/core/profiles`;
+       }
+};
+
+export default profilePageObject;
diff --git 
a/experimental/traffic-portal/nightwatch/tests/profiles/table.spec.ts 
b/experimental/traffic-portal/nightwatch/tests/profiles/table.spec.ts
new file mode 100644
index 0000000000..f7a2e78e30
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/profiles/table.spec.ts
@@ -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.
+ */
+
+describe("Profiles Spec", () => {
+       it("Loads elements", async () => {
+               await browser.page.common()
+                       .section.sidebar
+                       .navigateToNode("profiles", ["configurationContainer"]);
+               await browser.waitForElementPresent("input[name=fuzzControl]");
+               await browser.elements("css selector", "div.ag-row", rows => {
+                       browser.assert.ok(rows.status === 0);
+                       browser.assert.ok((rows.value as []).length >= 1);
+               });
+       });
+});
diff --git a/experimental/traffic-portal/src/app/api/profile.service.ts 
b/experimental/traffic-portal/src/app/api/profile.service.ts
index bf638dbbf1..af494b50c6 100644
--- a/experimental/traffic-portal/src/app/api/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/profile.service.ts
@@ -14,7 +14,7 @@
 
 import { HttpClient } from "@angular/common/http";
 import { Injectable } from "@angular/core";
-import { ResponseProfile } from "trafficops-types";
+import { RequestProfile, ResponseProfile } from "trafficops-types";
 
 import { APIService } from "./base-api.service";
 
@@ -57,4 +57,26 @@ export class ProfileService extends APIService {
                }
                return this.get<Array<ResponseProfile>>(path).toPromise();
        }
+
+       /**
+        * Creates a new type.
+        *
+        * @param profile The type to create.
+        * @returns The created type.
+        */
+       public async createProfile(profile: RequestProfile): 
Promise<ResponseProfile> {
+               return this.post<ResponseProfile>("profiles", 
profile).toPromise();
+       }
+
+       /**
+        * Deletes an existing type.
+        *
+        * @param profileId Id of the profile to delete.
+        * @returns The success message.
+        */
+       public async deleteProfile(profileId: number | ResponseProfile): 
Promise<ResponseProfile> {
+               const id = typeof (profileId) === "number" ? profileId : 
profileId.id;
+               return 
this.delete<ResponseProfile>(`profiles/${id}`).toPromise();
+       }
+
 }
diff --git a/experimental/traffic-portal/src/app/api/testing/profile.service.ts 
b/experimental/traffic-portal/src/app/api/testing/profile.service.ts
index 5f25ad622e..1f4a96b174 100644
--- a/experimental/traffic-portal/src/app/api/testing/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/profile.service.ts
@@ -13,14 +13,14 @@
 */
 
 import { Injectable } from "@angular/core";
-import { ProfileType, type ResponseProfile } from "trafficops-types";
+import { ProfileType, RequestProfile, type ResponseProfile } from 
"trafficops-types";
 
 /**
  * ProfileService exposes API functionality related to Profiles.
  */
 @Injectable()
 export class ProfileService {
-
+       private lastID = 10;
        private readonly profiles: ResponseProfile[] = [
                {
                        cdn: 1,
@@ -173,4 +173,36 @@ export class ProfileService {
                        })
                );
        }
+
+       /**
+        * Creates a new profile.
+        *
+        * @param profile The profile to create.
+        * @returns The created profile.
+        */
+       public async createProfile(profile: RequestProfile): 
Promise<ResponseProfile> {
+               const t = {
+                       ...profile,
+                       cdnName: null,
+                       id: ++this.lastID,
+                       lastUpdated: new Date()
+               };
+               this.profiles.push(t);
+               return t;
+       }
+
+       /**
+        * Deletes an existing profile.
+        *
+        * @param id Id of the profile to delete.
+        * @returns The success message.
+        */
+       public async deleteProfile(id: number): Promise<ResponseProfile> {
+               const index = this.profiles.findIndex(t => t.id === id);
+               if (index === -1) {
+                       throw new Error(`no such Type: ${id}`);
+               }
+               return this.profiles.splice(index, 1)[0];
+       }
+
 }
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts 
b/experimental/traffic-portal/src/app/core/core.module.ts
index e4f0aa3123..9828ae033c 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -48,6 +48,7 @@ import {
 } from 
"./deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component";
 import { NewDeliveryServiceComponent } from 
"./deliveryservice/new-delivery-service/new-delivery-service.component";
 import { ISOGenerationFormComponent } from 
"./misc/isogeneration-form/isogeneration-form.component";
+import { ProfileTableComponent } from 
"./profiles/profile-table/profile-table.component";
 import { PhysLocDetailComponent } from 
"./servers/phys-loc/detail/phys-loc-detail.component";
 import { PhysLocTableComponent } from 
"./servers/phys-loc/table/phys-loc-table.component";
 import { ServerDetailsComponent } from 
"./servers/server-details/server-details.component";
@@ -90,6 +91,7 @@ export const ROUTES: Routes = [
        { component: TypesTableComponent, path: "types" },
        { component: TypeDetailComponent, path: "types/:id"},
        { component: ISOGenerationFormComponent, path: "iso-gen"},
+       { component: ProfileTableComponent, path: "profiles"},
 ].map(r => ({...r, canActivate: [AuthenticatedGuard]}));
 
 /**
@@ -131,6 +133,7 @@ export const ROUTES: Routes = [
                TypesTableComponent,
                TypeDetailComponent,
                ISOGenerationFormComponent,
+               ProfileTableComponent,
                CDNDetailComponent,
        ],
        exports: [],
diff --git 
a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.html
 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.html
new file mode 100644
index 0000000000..121b7cdcd9
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.html
@@ -0,0 +1,27 @@
+<!--
+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="table-page-content">
+       <div class="search-container">
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Profiles" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+       </div>
+       <tp-generic-table
+               [data]="profiles | async"
+               [cols]="columnDefs"
+               [fuzzySearch]="fuzzySubject"
+               context="profiles"
+               [contextMenuItems]="contextMenuItems"
+               (contextMenuAction)="handleContextMenu($event)">
+       </tp-generic-table>
+</mat-card>
diff --git 
a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.scss
 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.scss
new file mode 100644
index 0000000000..ebe77042d3
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-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/profiles/profile-table/profile-table.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.spec.ts
new file mode 100644
index 0000000000..53326e993d
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.spec.ts
@@ -0,0 +1,126 @@
+/*
+* 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, fakeAsync, tick } from 
"@angular/core/testing";
+import { MatDialog, MatDialogModule, type MatDialogRef } from 
"@angular/material/dialog";
+import { ActivatedRoute } from "@angular/router";
+import { RouterTestingModule } from "@angular/router/testing";
+import { of } from "rxjs";
+import { ProfileType } from "trafficops-types";
+
+import { ProfileService } from "src/app/api";
+import { APITestingModule } from "src/app/api/testing";
+import { isAction } from 
"src/app/shared/generic-table/generic-table.component";
+
+import { ProfileTableComponent } from "./profile-table.component";
+
+describe("ProfileTableComponent", () => {
+       let component: ProfileTableComponent;
+       let fixture: ComponentFixture<ProfileTableComponent>;
+
+       beforeEach(async () => {
+               await TestBed.configureTestingModule({
+                       declarations: [ProfileTableComponent],
+                       imports: [
+                               APITestingModule,
+                               RouterTestingModule,
+                               MatDialogModule
+                       ]
+               })
+                       .compileComponents();
+
+               fixture = TestBed.createComponent(ProfileTableComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+       });
+
+       it("should create", () => {
+               expect(component).toBeTruthy();
+       });
+
+       it("sets the fuzzy search subject based on the search query param", 
fakeAsync(() => {
+               const router = TestBed.inject(ActivatedRoute);
+               const searchString = "testquest";
+               spyOnProperty(router, "queryParamMap").and.returnValue(of(new 
Map([["search", searchString]])));
+
+               let searchValue = "not the right string";
+               component.fuzzySubject.subscribe(
+                       s => searchValue = s
+               );
+
+               component.ngOnInit();
+               tick();
+
+               expect(searchValue).toBe(searchString);
+       }));
+
+       it("updates the fuzzy search output", fakeAsync(() => {
+               let called = false;
+               const text = "testquest";
+               const spy = jasmine.createSpy("subscriber", (txt: string): void 
=> {
+                       if (!called) {
+                               expect(txt).toBe("");
+                               called = true;
+                       } else {
+                               expect(txt).toBe(text);
+                       }
+               });
+               component.fuzzySubject.subscribe(spy);
+               tick();
+               expect(spy).toHaveBeenCalled();
+               component.fuzzControl.setValue(text);
+               component.updateURL();
+               tick();
+               expect(spy).toHaveBeenCalledTimes(2);
+       }));
+
+       it("handles the 'delete' context menu item", fakeAsync(async () => {
+               const item = component.contextMenuItems.find(c => c.name === 
"Delete");
+               if (!item) {
+                       return fail("missing 'Delete' context menu item");
+               }
+               if (!isAction(item)) {
+                       return fail("expected an action, not a link");
+               }
+               expect(item.multiRow).toBeFalsy();
+               expect(item.disabled).toBeUndefined();
+
+               const api = TestBed.inject(ProfileService);
+               const spy = spyOn(api, "deleteProfile").and.callThrough();
+               expect(spy).not.toHaveBeenCalled();
+
+               const dialogService = TestBed.inject(MatDialog);
+               const openSpy = spyOn(dialogService, "open").and.returnValue({
+                       afterClosed: () => of(true)
+               } as MatDialogRef<unknown>);
+
+               const profile = await api.createProfile({
+                       cdn: 1,
+                       description: "blah",
+                       name: "test",
+                       routingDisabled: false,
+                       type: ProfileType.ATS_PROFILE
+               });
+               expect(openSpy).not.toHaveBeenCalled();
+               const asyncExpectation = 
expectAsync(component.handleContextMenu({ action: "delete", data: profile 
})).toBeResolvedTo(undefined);
+               tick();
+
+               expect(openSpy).toHaveBeenCalled();
+               tick();
+
+               expect(spy).toHaveBeenCalled();
+
+               await asyncExpectation;
+       }));
+});
diff --git 
a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.ts
 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.ts
new file mode 100644
index 0000000000..075ee04d23
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.ts
@@ -0,0 +1,148 @@
+/*
+* 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, UntypedFormControl } from "@angular/forms";
+import { MatDialog } from "@angular/material/dialog";
+import { ActivatedRoute, Params } from "@angular/router";
+import { BehaviorSubject } from "rxjs";
+import { ResponseProfile } from "trafficops-types";
+
+import { ProfileService } from "src/app/api";
+import { CurrentUserService } from 
"src/app/shared/current-user/current-user.service";
+import { DecisionDialogComponent } from 
"src/app/shared/dialogs/decision-dialog/decision-dialog.component";
+import { ContextMenuActionEvent, ContextMenuItem } from 
"src/app/shared/generic-table/generic-table.component";
+import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
+
+/**
+ * ProfileTableComponent is the controller for the profiles page - which
+ * principally contains a table.
+ */
+@Component({
+       selector: "tp-profile-table",
+       styleUrls: ["./profile-table.component.scss"],
+       templateUrl: "./profile-table.component.html"
+})
+export class ProfileTableComponent implements OnInit {
+       /** All the physical locations which should appear in the table. */
+       public profiles: Promise<Array<ResponseProfile>>;
+
+       /** Definitions of the table's columns according to the ag-grid API */
+       public columnDefs = [{
+               field: "cdnName",
+               headerName: "CDN"
+       }, {
+               field: "description",
+               headerName: "Description",
+       }, {
+               field: "id",
+               headerName: "ID",
+               hide: true
+       }, {
+               field: "lastUpdated",
+               headerName: "Last Updated",
+               hide: true
+       }, {
+               field: "name",
+               headerName: "Name"
+       }, {
+               field: "routingDisabled",
+               headerName: "Routing Disabled"
+       }, {
+               field: "type",
+               headerName: "Type"
+       }];
+
+       /** Definitions for the context menu items (which act on augmented 
cache-group data). */
+       public contextMenuItems: Array<ContextMenuItem<ResponseProfile>> = [
+               {
+                       action: "delete",
+                       multiRow: false,
+                       name: "Delete"
+               },
+               {
+                       href: "/core/servers",
+                       name: "View Servers",
+                       queryParams: (profile: ResponseProfile): Params => 
({profileName: profile.name})
+               }
+       ];
+
+       /** 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: UntypedFormControl = new FormControl<string>("", 
{nonNullable: true});
+
+       /**
+        * Constructs the component with its required injections.
+        *
+        * @param api The Servers API which is used to provide row data.
+        * @param route A reference to the route of this view which is used to 
set the fuzzy search box text from the 'search' query parameter.
+        * @param router Angular router
+        * @param navSvc Manages the header
+        * @param dialog Dialog manager
+        */
+       constructor(
+               private readonly api: ProfileService,
+               private readonly route: ActivatedRoute,
+               private readonly navSvc: NavigationService,
+               private readonly dialog: MatDialog,
+               public readonly auth: CurrentUserService) {
+               this.fuzzySubject = new BehaviorSubject<string>("");
+               this.profiles = this.api.getProfiles();
+               this.navSvc.headerTitle.next("Profiles");
+       }
+
+       /** 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);
+       }
+
+       /**
+        * Handles a context menu event.
+        *
+        * @param evt The action selected from the context menu.
+        */
+       public async handleContextMenu(evt: 
ContextMenuActionEvent<ResponseProfile>): Promise<void> {
+               const data = evt.data as ResponseProfile;
+               switch (evt.action) {
+                       case "delete":
+                               const ref = 
this.dialog.open(DecisionDialogComponent, {
+                                       data: {
+                                               message: `Are you sure to 
delete Profile ${data.name} with id ${data.id}?`,
+                                               title: "Confirm Delete"
+                                       }
+                               });
+                               ref.afterClosed().subscribe(result => {
+                                       if (result) {
+                                               
this.api.deleteProfile(data.id).then(async () => this.profiles = 
this.api.getProfiles());
+                                       }
+                               });
+                               break;
+               }
+       }
+}
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 b85b2d69e2..14c0dff7aa 100644
--- 
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
+++ 
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
@@ -141,6 +141,10 @@ export class NavigationService {
                        children: [{
                                href: "/core/types",
                                name: "Types"
+                       },
+                       {
+                               href: "/core/profiles",
+                               name: "Profiles"
                        }],
                        name: "Configuration"
                }, {

Reply via email to