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 8946fcf008 Zuni profile detail (#7466)
8946fcf008 is described below

commit 8946fcf008406517e0430d7aa667a0538c51d5ab
Author: Kannan.G.B <[email protected]>
AuthorDate: Wed May 10 00:52:05 2023 +0530

    Zuni profile detail (#7466)
    
    * initial commit
    
    * latest update for profile details edit, delete
    
    * test case error fix
    
    * latest
    
    * e2e fix
    
    * input to textarea
    
    * order changed
    
    * comments addressed
    
    * comments addressed
    
    * url correction
    
    * e2e fixees
    
    * angular 15 upgrade
    
    * lint fixes
---
 .../traffic-portal/nightwatch/globals/globals.ts   |   2 +
 .../page_objects/profiles/profileDetail.ts         |  51 +++++++
 .../nightwatch/tests/profiles/detail.spec.ts       |  50 +++++++
 .../traffic-portal/src/app/api/profile.service.ts  |  19 ++-
 .../src/app/api/testing/profile.service.ts         |  17 ++-
 .../asns/table/asns-table.component.html           |   2 +-
 .../cache-group-table.component.html               |   2 +-
 .../table/coordinates-table.component.html         |   2 +-
 .../divisions/table/divisions-table.component.html |   2 +-
 .../regions/table/regions-table.component.html     |   2 +-
 .../core/change-logs/change-logs.component.html    |   2 +-
 .../traffic-portal/src/app/core/core.module.ts     |   3 +
 .../app/core/dashboard/dashboard.component.html    |   2 +-
 .../new-delivery-service.component.html            |   4 +-
 .../profile-detail/profile-detail.component.html   |  61 +++++++++
 .../profile-detail/profile-detail.component.scss}  |  21 +--
 .../profile-detail.component.spec.ts               |  81 +++++++++++
 .../profile-detail/profile-detail.component.ts     | 151 +++++++++++++++++++++
 .../profile-table/profile-table.component.html     |   4 +-
 .../profile-table/profile-table.component.ts       |  27 ++++
 .../phys-loc/table/phys-loc-table.component.html   |   2 +-
 .../servers-table/servers-table.component.html     |   2 +-
 .../status-details/status-details.component.scss   |   4 +-
 .../statuses-table/statuses-table.component.html   |   2 +-
 .../core/types/table/types-table.component.html    |   2 +-
 .../app/core/users/tenants/tenants.component.html  |   2 +-
 .../src/app/core/users/users.component.html        |   2 +-
 .../src/app/login/login.component.html             |   2 +-
 28 files changed, 489 insertions(+), 34 deletions(-)

diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts 
b/experimental/traffic-portal/nightwatch/globals/globals.ts
index d8c3925b58..e842afab71 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 { ProfileDetailPageObject } from 
"nightwatch/page_objects/profiles/profileDetail";
 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";
@@ -108,6 +109,7 @@ declare module "nightwatch" {
                login: () => LoginPageObject;
                profiles: {
                        profileTable: () => ProfilePageObject;
+                       profileDetail: () => ProfileDetailPageObject;
                };
                servers: {
                        physLocDetail: () => PhysLocDetailPageObject;
diff --git 
a/experimental/traffic-portal/nightwatch/page_objects/profiles/profileDetail.ts 
b/experimental/traffic-portal/nightwatch/page_objects/profiles/profileDetail.ts
new file mode 100644
index 0000000000..a8f5593c03
--- /dev/null
+++ 
b/experimental/traffic-portal/nightwatch/page_objects/profiles/profileDetail.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 Profile Details.
+ */
+export type ProfileDetailPageObject = EnhancedPageObject<{}, typeof 
profileDetailPageObject.elements>;
+
+const profileDetailPageObject = {
+       elements: {
+               cdn: {
+                       selector: "mat-select[name='cdn']"
+               },
+               description: {
+                       selector: "textarea[name='description']"
+               },
+               id: {
+                       selector: "input[name='id']"
+               },
+               lastUpdated: {
+                       selector: "input[name='lastUpdated']"
+               },
+               name: {
+                       selector: "input[name='name']"
+               },
+               routingDisabled: {
+                       selector: "mat-select[name='routingDisabled']"
+               },
+               saveBtn: {
+                       selector: "button[type='submit']"
+               },
+               type: {
+                       selector: "mat-select[name='type']"
+               }
+       },
+};
+
+export default profileDetailPageObject;
diff --git 
a/experimental/traffic-portal/nightwatch/tests/profiles/detail.spec.ts 
b/experimental/traffic-portal/nightwatch/tests/profiles/detail.spec.ts
new file mode 100644
index 0000000000..4c1faf0c5b
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/profiles/detail.spec.ts
@@ -0,0 +1,50 @@
+/*
+ * 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("Profile Detail Spec", () => {
+       it("Test Profile", () => {
+               const page = browser.page.profiles.profileDetail();
+               
browser.url(`${page.api.launchUrl}/core/profiles/${browser.globals.testData.profile.id}`,
 res => {
+                       browser.assert.ok(res.status === 0);
+                       page.waitForElementVisible("mat-card")
+                               .assert.enabled("@name")
+                               .assert.enabled("@cdn")
+                               .assert.enabled("@type")
+                               .assert.enabled("@routingDisabled")
+                               .assert.enabled("@description")
+                               .assert.not.enabled("@id")
+                               .assert.not.enabled("@lastUpdated")
+                               .assert.enabled("@saveBtn")
+                               .assert.valueEquals("@name", 
browser.globals.testData.profile.name)
+                               .assert.valueEquals("@id", 
String(browser.globals.testData.profile.id));
+               });
+       });
+
+       it("New Profile", () => {
+               const page = browser.page.profiles.profileDetail();
+               browser.url(`${page.api.launchUrl}/core/profiles/new`, res => {
+                       browser.assert.ok(res.status === 0);
+                       page.waitForElementVisible("mat-card")
+                               .assert.enabled("@name")
+                               .assert.enabled("@cdn")
+                               .assert.enabled("@type")
+                               .assert.enabled("@routingDisabled")
+                               .assert.enabled("@description")
+                               .assert.not.elementPresent("@id")
+                               .assert.not.elementPresent("@lastUpdated")
+                               .assert.enabled("@saveBtn")
+                               .assert.valueEquals("@name", "");
+               });
+       });
+});
diff --git a/experimental/traffic-portal/src/app/api/profile.service.ts 
b/experimental/traffic-portal/src/app/api/profile.service.ts
index 6eaea53501..01a00b0122 100644
--- a/experimental/traffic-portal/src/app/api/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/profile.service.ts
@@ -72,17 +72,28 @@ export class ProfileService extends APIService {
        }
 
        /**
-        * Creates a new type.
+        * Creates a new profile.
         *
-        * @param profile The type to create.
-        * @returns The created type.
+        * @param profile The profile to create.
+        * @returns The created profile.
         */
        public async createProfile(profile: RequestProfile): 
Promise<ResponseProfile> {
                return this.post<ResponseProfile>("profiles", 
profile).toPromise();
        }
 
        /**
-        * Deletes an existing type.
+        * Replaces the current definition of a profile with the one given.
+        *
+        * @param profile The new profile.
+        * @returns The updated profile.
+        */
+       public async updateProfile(profile: ResponseProfile): 
Promise<ResponseProfile> {
+               const path = `profiles/${profile.id}`;
+               return this.put<ResponseProfile>(path, profile).toPromise();
+       }
+
+       /**
+        * Deletes an existing profile.
         *
         * @param profileId Id of the profile to delete.
         * @returns The success message.
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 1f4a96b174..fa2a100873 100644
--- a/experimental/traffic-portal/src/app/api/testing/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/profile.service.ts
@@ -191,13 +191,28 @@ export class ProfileService {
                return t;
        }
 
+       /**
+        * Updates an existing profile.
+        *
+        * @param profile the profile to update.
+        * @returns The success message.
+        */
+       public async updateProfile(profile: ResponseProfile): 
Promise<ResponseProfile> {
+               const id = this.profiles.findIndex(d => d.id === profile.id);
+               if (id === -1) {
+                       throw new Error(`no such profile: ${profile.id}`);
+               }
+               this.profiles[id] = profile;
+               return profile;
+       }
+
        /**
         * Deletes an existing profile.
         *
         * @param id Id of the profile to delete.
         * @returns The success message.
         */
-       public async deleteProfile(id: number): Promise<ResponseProfile> {
+       public async deleteProfile(id: number | ResponseProfile): 
Promise<ResponseProfile> {
                const index = this.profiles.findIndex(t => t.id === id);
                if (index === -1) {
                        throw new Error(`no such Type: ${id}`);
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.html
 
b/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.html
index 37c6e9bb28..48b75f809f 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search ASNs" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search ASNs" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy 
Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="asns | async"
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
 
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
index fe491c7755..b2fdc94978 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
@@ -13,7 +13,7 @@ limitations under the License.
 -->
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Cache Groups" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()"/>
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Cache Groups" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()"/>
        </div>
        <tp-generic-table
                [data]="cacheGroups | async"
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/coordinates/table/coordinates-table.component.html
 
b/experimental/traffic-portal/src/app/core/cache-groups/coordinates/table/coordinates-table.component.html
index 791b32c58c..75b4e8a2b3 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/coordinates/table/coordinates-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/coordinates/table/coordinates-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Coordinates" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Coordinates" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="coordinates | async"
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/divisions/table/divisions-table.component.html
 
b/experimental/traffic-portal/src/app/core/cache-groups/divisions/table/divisions-table.component.html
index 6d4835a9b2..80195e9f50 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/divisions/table/divisions-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/divisions/table/divisions-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Divisions" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Divisions" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="divisions | async"
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.html
 
b/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.html
index 7f1193fba9..abdcd8860f 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Regions" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Regions" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="regions | async"
diff --git 
a/experimental/traffic-portal/src/app/core/change-logs/change-logs.component.html
 
b/experimental/traffic-portal/src/app/core/change-logs/change-logs.component.html
index 552240e0e4..0bd1a62d99 100644
--- 
a/experimental/traffic-portal/src/app/core/change-logs/change-logs.component.html
+++ 
b/experimental/traffic-portal/src/app/core/change-logs/change-logs.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined">
        <div>
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Change Logs" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [(ngModel)]="searchText" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Change Logs" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [(ngModel)]="searchText" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="changeLogs | async"
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts 
b/experimental/traffic-portal/src/app/core/core.module.ts
index 833519fe07..9d6f1ecf1a 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 { ProfileDetailComponent } from 
"./profiles/profile-detail/profile-detail.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";
@@ -95,6 +96,7 @@ export const ROUTES: Routes = [
        { component: StatusesTableComponent, path: "statuses" },
        { component: StatusDetailsComponent, path: "statuses/:id" },
        { component: ISOGenerationFormComponent, path: "iso-gen"},
+       { component: ProfileDetailComponent, path: "profiles/:id"},
        { component: ProfileTableComponent, path: "profiles"},
 ].map(r => ({...r, canActivate: [AuthenticatedGuard]}));
 
@@ -141,6 +143,7 @@ export const ROUTES: Routes = [
                ISOGenerationFormComponent,
                ProfileTableComponent,
                CDNDetailComponent,
+               ProfileDetailComponent,
        ],
        exports: [],
        imports: [
diff --git 
a/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html 
b/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html
index d4b0f11934..7f61696f5f 100644
--- 
a/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html
+++ 
b/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html
@@ -12,7 +12,7 @@ See the License for the specific language governing 
permissions and
 limitations under the License.
 -->
 <main class="table-page-content">
-       <div class="search-container"><input type="search" role="search" 
aria-label="Fuzzy Search Delivery Service(s)" autofocus inputmode="search" 
name="fuzzControl" [formControl]="fuzzControl" (input)="updateURL($event)" 
accesskey="/" placeholder="Fuzzy Search"/></div>
+       <div class="search-container"><input type="search" role="search" 
aria-label="Fuzzy Search Delivery Service(s)" inputmode="search" 
name="fuzzControl" [formControl]="fuzzControl" (input)="updateURL($event)" 
accesskey="/" placeholder="Fuzzy Search"/></div>
        <article id="deliveryservices" [hidden]="loading">
                <ds-card *ngFor="let ds of filteredDSes; trackBy: tracker; let 
first=first; let last=last;" [deliveryService]="ds" [now]="now" [today]="today" 
[first]=first [last]=last></ds-card>
        </article>
diff --git 
a/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.html
 
b/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.html
index 0bf814f6bf..1404bbf90e 100644
--- 
a/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.html
+++ 
b/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.html
@@ -17,7 +17,7 @@ limitations under the License.
                        <h2>Step 1 - Origin Server</h2>
                        <div class="form-content">
                                <label for="origin">I want to create a Delivery 
Service for</label>
-                               <input [formControl]="originURL" type="url" 
id="origin" name="origin" placeholder="this URL" autofocus title="Must be a URL 
(should start with `http://` or `https://`)" 
pattern="(https?|HTTPS?)://[a-zA-Z][a-zA-z0-9\-]*(\.[a-zA-Z][a-zA-z0-9\-]*)*(/[\w\.]+)*/?"
 required/>
+                               <input [formControl]="originURL" type="url" 
id="origin" name="origin" placeholder="this URL" title="Must be a URL (should 
start with `http://` or `https://`)" 
pattern="(https?|HTTPS?)://[a-zA-Z][a-zA-z0-9\-]*(\.[a-zA-Z][a-zA-z0-9\-]*)*(/[\w\.]+)*/?"
 required/>
                                <label for="active-immediately">This Delivery 
Service should become active immediately</label>
                                <input [formControl]="activeImmediately" 
id="active-immediately" type="checkbox" name="active-immediately" title="This 
Delivery Service should become active immediately"/>
                        </div>
@@ -32,7 +32,7 @@ limitations under the License.
                        <h2>Step 2 - Meta Information</h2>
                        <div class="form-content">
                                <label for="displayName">This Delivery 
Service's name will be</label>
-                               <input type="text" autofocus title="This will 
be the name of the Delivery Service as it appears on the 'Home' screen" 
name="displayName" id="displayName" (change)="updateDisplayName()" 
[formControl]="displayName" placeholder="{{deliveryService.displayName}}" 
required>
+                               <input type="text" title="This will be the name 
of the Delivery Service as it appears on the 'Home' screen" name="displayName" 
id="displayName" (change)="updateDisplayName()" [formControl]="displayName" 
placeholder="{{deliveryService.displayName}}" required>
                                <label for="longDesc">Please briefly describe 
this Delivery Service's purpose and function</label>
                                <textarea id="longDesc" name="longDesc" 
title="No character limit - be as verbose as you like." required 
placeholder="e.g. This Delivery Service is for my website's image assets." 
[formControl]="description" rows="3"></textarea>
                        </div>
diff --git 
a/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.html
 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.html
new file mode 100644
index 0000000000..d581b5a358
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.html
@@ -0,0 +1,61 @@
+<!--
+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="loading"></tp-loading>
+    <form ngNativeValidate (ngSubmit)="submit($event)" *ngIf="profile">
+        <mat-card-content>
+            <mat-form-field *ngIf="!new">
+                               <mat-label>ID</mat-label>
+                               <input matInput type="text" name="id" disabled 
readonly [defaultValue]="profile['id']" />
+                       </mat-form-field>
+            <mat-form-field>
+                <mat-label>Name</mat-label>
+                <input matInput type="text" name="name" 
[(ngModel)]="profile['name']" required />
+            </mat-form-field>
+            <mat-form-field>
+                <mat-label>CDN</mat-label>
+                <mat-select name="cdn" [(ngModel)]="profile['cdn']" required>
+                    <mat-option [value]="cdn.id" *ngFor="let cdn of 
cdns">{{cdn.name}}</mat-option>
+                </mat-select>
+            </mat-form-field>
+            <mat-form-field>
+                <mat-label>Type</mat-label>
+                <mat-select name="type" [(ngModel)]="profile['type']" required>
+                    <mat-option [value]="type.value" *ngFor="let type of 
types">{{type.value}}</mat-option>
+                </mat-select>
+            </mat-form-field>
+            <mat-form-field>
+                <mat-label>Routing Disabled</mat-label>
+                <mat-select name="routingDisabled" 
[(ngModel)]="profile['routingDisabled']" required>
+                    <mat-option [value]="true">True</mat-option>
+                    <mat-option [value]="false">False</mat-option>
+                </mat-select>
+            </mat-form-field>
+            <mat-form-field>
+                <mat-label>Description</mat-label>
+                <textarea matInput name="description" 
[(ngModel)]="profile['description']" required></textarea>
+            </mat-form-field>
+            <mat-form-field *ngIf="!new">
+                               <mat-label>Last Updated</mat-label>
+                               <input matInput type="text" name="lastUpdated" 
disabled readonly [defaultValue]="profile['lastUpdated']" />
+                       </mat-form-field>
+        </mat-card-content>
+
+        <mat-card-actions align="end">
+                       <button mat-raised-button type="button" *ngIf="!new" 
color="warn" (click)="deleteProfile()">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/statuses/status-details/status-details.component.scss
 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.scss
similarity index 72%
copy from 
experimental/traffic-portal/src/app/core/statuses/status-details/status-details.component.scss
copy to 
experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.scss
index 0a24e3195e..1571bf9b6f 100644
--- 
a/experimental/traffic-portal/src/app/core/statuses/status-details/status-details.component.scss
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.scss
@@ -11,15 +11,16 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-mat-card {
-    margin: 1em auto;
-    width: 95%;
-    min-width: 350px;
 
-    mat-card-content {
-        display: grid;
-        grid-template-columns: 1fr;
-        row-gap: 2em;
-        margin: 1em auto 10px;
-    }
+.mat-mdc-card {
+       margin: 1em auto;
+       width: 80%;
+       min-width: 350px;
+
+       .mat-mdc-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/profiles/profile-detail/profile-detail.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.spec.ts
new file mode 100644
index 0000000000..8bf93f7a07
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.spec.ts
@@ -0,0 +1,81 @@
+/*
+* 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 { ProfileService } from "src/app/api";
+import { APITestingModule } from "src/app/api/testing";
+import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
+
+import { ProfileDetailComponent } from "./profile-detail.component";
+
+describe("ProfileDetailComponent", () => {
+       let component: ProfileDetailComponent;
+       let fixture: ComponentFixture<ProfileDetailComponent>;
+       let route: ActivatedRoute;
+       let paramMap: jasmine.Spy;
+       let service: ProfileService;
+
+       const navSvc = jasmine.createSpyObj([],{headerHidden: new 
ReplaySubject<boolean>(), headerTitle: new ReplaySubject<string>()});
+       beforeEach(async () => {
+               await TestBed.configureTestingModule({
+                       declarations: [ ProfileDetailComponent ],
+                       imports: [ APITestingModule, RouterTestingModule, 
MatDialogModule ],
+                       providers: [ { provide: NavigationService, useValue: 
navSvc } ]
+               })
+                       .compileComponents();
+
+               route = TestBed.inject(ActivatedRoute);
+               paramMap = spyOn(route.snapshot.paramMap, "get");
+               service = TestBed.inject(ProfileService);
+               paramMap.and.returnValue(null);
+               fixture = TestBed.createComponent(ProfileDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+       });
+
+       it("should create", () => {
+               expect(component).toBeTruthy();
+       });
+
+       it("new profile", async () => {
+               paramMap.and.returnValue("new");
+
+               fixture = TestBed.createComponent(ProfileDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               await fixture.whenStable();
+               expect(paramMap).toHaveBeenCalled();
+               expect(component.profile).not.toBeNull();
+               expect(component.new).toBeTrue();
+       });
+
+       it("existing profile", async () => {
+       const id = 1;
+               paramMap.and.returnValue(id);
+       const profile = await service.getProfiles(id);
+               fixture = TestBed.createComponent(ProfileDetailComponent);
+               component = fixture.componentInstance;
+               fixture.detectChanges();
+               await fixture.whenStable();
+               expect(paramMap).toHaveBeenCalled();
+               expect(component.profile).not.toBeNull();
+               expect(component.profile.name).toBe(profile.name);
+               expect(component.new).toBeFalse();
+       });
+});
diff --git 
a/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts
 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts
new file mode 100644
index 0000000000..78e0c362ff
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts
@@ -0,0 +1,151 @@
+/*
+* 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 { MatDialog } from "@angular/material/dialog";
+import { ActivatedRoute, Router } from "@angular/router";
+import { ProfileType, ResponseCDN, ResponseProfile } from "trafficops-types";
+
+import { CDNService, ProfileService } from "src/app/api";
+import { DecisionDialogComponent } from 
"src/app/shared/dialogs/decision-dialog/decision-dialog.component";
+import { NavigationService } from 
"src/app/shared/navigation/navigation.service";
+
+/**
+ * ProfileDetailComponent is the controller for the profile add/edit form.
+ */
+@Component({
+       selector: "tp-profile-detail",
+       styleUrls: ["./profile-detail.component.scss"],
+       templateUrl: "./profile-detail.component.html"
+})
+export class ProfileDetailComponent implements OnInit {
+       public new = false;
+
+       /** Loader status for the actions */
+       public loading = true;
+
+       /** All details of profile requested */
+       public profile!: ResponseProfile;
+
+       /** All cdns used for profile creation as input */
+       public cdns!: ResponseCDN[];
+
+       public types = [
+               { value: "ATS_PROFILE" },
+               { value: "TR_PROFILE" },
+               { value: "TM_PROFILE" },
+               { value: "TS_PROFILE" },
+               { value: "TP_PROFILE" },
+               { value: "INFLUXDB_PROFILE" },
+               { value: "RIAK_PROFILE" },
+               { value: "SPLUNK_PROFILE" },
+               { value: "DS_PROFILE" },
+               { value: "ORG_PROFILE" },
+               { value: "KAFKA_PROFILE" },
+               { value: "LOGSTASH_PROFILE" },
+               { value: "ES_PROFILE" },
+               { value: "UNK_PROFILE" },
+               { value: "GROVE_PROFILE" }
+       ];
+
+       /**
+        * Constructor.
+        *
+        * @param api The Profiles API which is used to provide functions for 
create, edit and delete profiles.
+        * @param cdnService The CDN service API which is used to provide cdns.
+        * @param dialog Dialog manager
+        * @param navSvc Manages the header
+        * @param route A reference to the route of this view which is used to 
get the 'id' query parameter of profile.
+        * @param router Angular router
+        */
+       constructor(
+               private readonly api: ProfileService,
+               private readonly cdnService: CDNService,
+               private readonly dialog: MatDialog,
+               private readonly navSvc: NavigationService,
+               private readonly route: ActivatedRoute,
+               private readonly router: Router
+       ) { }
+
+       /**
+        * Angular lifecycle hook where data is initialized.
+        */
+       public async ngOnInit(): Promise<void> {
+               // Getting id from the route
+               const id = this.route.snapshot.paramMap.get("id");
+
+               this.cdns = await this.cdnService.getCDNs();
+               if (id && id !== "new") {
+                       const numID = parseInt(id, 10);
+                       if (Number.isNaN(numID)) {
+                               throw new Error(`route parameter 'id' was 
non-number:  ${{ id }}`);
+                       } else {
+                               this.profile = await 
this.api.getProfiles(Number(id));
+                               this.navSvc.headerTitle.next(`Profile: 
${this.profile.name}`);
+                       }
+                       this.loading = false;
+               } else {
+                       this.new = true;
+                       this.navSvc.headerTitle.next("New Profile");
+                       this.profile = {
+                               cdn: 1,
+                               cdnName: "",
+                               description: "",
+                               id: -1,
+                               lastUpdated: new Date(),
+                               name: "",
+                               routingDisabled: false,
+                               type: ProfileType.ATS_PROFILE
+                       };
+                       this.loading = false;
+               }
+       }
+
+       /**
+        * Submits new/updated profile.
+        *
+        * @param e HTML form submission event.
+        */
+       public async submit(e: Event): Promise<void> {
+               e.preventDefault();
+               e.stopPropagation();
+               if(this.new) {
+                       this.profile = await 
this.api.createProfile(this.profile);
+                       this.new = false;
+               } else {
+                       this.profile = await 
this.api.updateProfile(this.profile);
+               }
+       }
+
+       /**
+        * Deletes the current profile.
+        */
+       public async deleteProfile(): Promise<void> {
+               if (this.new) {
+                       console.error("Unable to delete new profile");
+                       return;
+               }
+               const ref = this.dialog.open(DecisionDialogComponent, {
+                       data: {
+                               message: `Are you sure to delete Profile 
${this.profile.name} with id ${this.profile.id}?`,
+                               title: "Confirm Delete"
+                       }
+               });
+               ref.afterClosed().subscribe(result => {
+                       if (result) {
+                               
this.api.deleteProfile(this.profile.id).then(async () => 
this.router.navigate(["/core/profiles"]));
+                       }
+               });
+       }
+}
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
index fe908502c9..2cd0ba90b4 100644
--- 
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
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" 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()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Profiles" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="profiles | async"
@@ -25,3 +25,5 @@ limitations under the License.
                (contextMenuAction)="handleContextMenu($event)">
        </tp-generic-table>
 </mat-card>
+
+<a class="page-fab" mat-fab title="Create a new Profile" 
*ngIf="auth.hasPermission('PROFILE:CREATE')" 
routerLink="new"><mat-icon>add</mat-icon></a>
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
index 075ee04d23..c33c0c7d9e 100644
--- 
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
@@ -66,11 +66,38 @@ export class ProfileTableComponent implements OnInit {
 
        /** Definitions for the context menu items (which act on augmented 
cache-group data). */
        public contextMenuItems: Array<ContextMenuItem<ResponseProfile>> = [
+               {
+                       href: (profile: ResponseProfile): string => 
`${profile.id}`,
+                       name: "Open in New Tab",
+                       newTab: true
+               },
+               {
+                       href: (type: ResponseProfile): string => `${type.id}`,
+                       name: "Edit"
+               },
                {
                        action: "delete",
                        multiRow: false,
                        name: "Delete"
                },
+               {
+                       action: "import-profile",
+                       disabled: (): true => true,
+                       multiRow: false,
+                       name: "Import Profile",
+               },
+               {
+                       action: "export-profile",
+                       disabled: (): true => true,
+                       multiRow: false,
+                       name: "Export Profile",
+               },
+               {
+                       action: "manage-parameters",
+                       disabled: (): true => true,
+                       multiRow: false,
+                       name: "Manage Parameters",
+               },
                {
                        href: "/core/servers",
                        name: "View Servers",
diff --git 
a/experimental/traffic-portal/src/app/core/servers/phys-loc/table/phys-loc-table.component.html
 
b/experimental/traffic-portal/src/app/core/servers/phys-loc/table/phys-loc-table.component.html
index f6b605593f..eb60ccc806 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/phys-loc/table/phys-loc-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/servers/phys-loc/table/phys-loc-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Physical Locations" autofocus inputmode="search" role="search" 
accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" 
(input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Physical Locations" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="physLocations | async"
diff --git 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
index a466753365..cc133b3273 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 <main>
        <mat-card appearance="outlined" class="table-page-content">
                <div class="search-container">
-                       <input type="search" name="fuzzControl" 
aria-label="Fuzzy Search Servers" autofocus inputmode="search" role="search" 
accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" 
(input)="updateURL()"/>
+                       <input type="search" name="fuzzControl" 
aria-label="Fuzzy Search Servers" inputmode="search" role="search" 
accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" 
(input)="updateURL()"/>
                </div>
                <tp-generic-table
                        [data]="servers | async"
diff --git 
a/experimental/traffic-portal/src/app/core/statuses/status-details/status-details.component.scss
 
b/experimental/traffic-portal/src/app/core/statuses/status-details/status-details.component.scss
index 0a24e3195e..87e81b002c 100644
--- 
a/experimental/traffic-portal/src/app/core/statuses/status-details/status-details.component.scss
+++ 
b/experimental/traffic-portal/src/app/core/statuses/status-details/status-details.component.scss
@@ -11,12 +11,12 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-mat-card {
+.mat-mdc-card {
     margin: 1em auto;
     width: 95%;
     min-width: 350px;
 
-    mat-card-content {
+    .mat-mdc-card-content {
         display: grid;
         grid-template-columns: 1fr;
         row-gap: 2em;
diff --git 
a/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
 
b/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
index 0dabb5c1ee..014b69904d 100644
--- 
a/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/statuses/statuses-table/statuses-table.component.html
@@ -13,7 +13,7 @@ limitations under the License.
 -->
 <mat-card class="table-page-content">
     <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Search 
Statuses" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Search 
Statuses" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy 
Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
     <tp-generic-table
                [data]="statuses | async"
diff --git 
a/experimental/traffic-portal/src/app/core/types/table/types-table.component.html
 
b/experimental/traffic-portal/src/app/core/types/table/types-table.component.html
index ce93df88bf..37eb3064e8 100644
--- 
a/experimental/traffic-portal/src/app/core/types/table/types-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/types/table/types-table.component.html
@@ -14,7 +14,7 @@ limitations under the License.
 
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Types" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Types" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy 
Search" [formControl]="fuzzControl" (input)="updateURL()" />
        </div>
        <tp-generic-table
                [data]="types | async"
diff --git 
a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html
index 40978e7ed5..372cd4fcdb 100644
--- 
a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html
+++ 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html
@@ -13,7 +13,7 @@ limitations under the License.
 -->
 <mat-card appearance="outlined" class="table-page-content">
        <div class="search-container">
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Tenants" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [(ngModel)]="searchText" (input)="updateURL()"/>
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Tenants" inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [(ngModel)]="searchText" (input)="updateURL()"/>
        </div>
        <tp-generic-table
                [data]="tenants"
diff --git 
a/experimental/traffic-portal/src/app/core/users/users.component.html 
b/experimental/traffic-portal/src/app/core/users/users.component.html
index 34c4a6f5f1..ec79e75eb4 100644
--- a/experimental/traffic-portal/src/app/core/users/users.component.html
+++ b/experimental/traffic-portal/src/app/core/users/users.component.html
@@ -13,7 +13,7 @@ limitations under the License.
 -->
 <mat-card appearance="outlined">
        <div>
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Users" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [(ngModel)]="searchText" (input)="updateURL()"/>
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Users" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy 
Search" [(ngModel)]="searchText" (input)="updateURL()"/>
        </div>
        <tp-generic-table
                [data]="users"
diff --git a/experimental/traffic-portal/src/app/login/login.component.html 
b/experimental/traffic-portal/src/app/login/login.component.html
index 58b3f3360a..b928349559 100644
--- a/experimental/traffic-portal/src/app/login/login.component.html
+++ b/experimental/traffic-portal/src/app/login/login.component.html
@@ -20,7 +20,7 @@ limitations under the License.
                <mat-card-content>
                        <mat-form-field appearance="fill">
                                <mat-label>Username</mat-label>
-                               <input matInput required autofocus type="text" 
[(ngModel)]="u" name="u"/>
+                               <input matInput required type="text" 
[(ngModel)]="u" name="u"/>
                        </mat-form-field>
                        <mat-form-field appearance="fill">
                                <mat-label>Password</mat-label>


Reply via email to