You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by rs...@apache.org on 2023/05/11 05:36:23 UTC

[trafficcontrol] branch feature/tpv2-role-details created (now 2b172108f9)

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

rshah pushed a change to branch feature/tpv2-role-details
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


      at 2b172108f9 Corrections wrt tests

This branch includes the following new commits:

     new 37f8ff5fbf Added files for roles table and a route for roles
     new c235b1a1c0 Added test case (e2e and unit test for roles)
     new cacfc77372 Updated folder name
     new ecde018433 Add role details.
     new 2b172108f9 Corrections wrt tests

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[trafficcontrol] 02/05: Added test case (e2e and unit test for roles)

Posted by rs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rshah pushed a commit to branch feature/tpv2-role-details
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit c235b1a1c0453969ae85dab28f25682f14d03112
Author: Rima Shah <ri...@comcast.com>
AuthorDate: Wed May 3 22:11:05 2023 -0600

    Added test case (e2e and unit test for roles)
---
 .../traffic-portal/src/app/core/core.module.ts     |  1 -
 .../users/roles/tables/roles-table.component.html  |  2 +-
 .../roles/tables/roles-table.component.spec.ts     | 68 ++++++++++++++++++++++
 .../users/roles/tables/roles-table.component.ts    |  5 +-
 4 files changed, 71 insertions(+), 5 deletions(-)

diff --git a/experimental/traffic-portal/src/app/core/core.module.ts b/experimental/traffic-portal/src/app/core/core.module.ts
index 7ea7c23260..93fa833903 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -67,7 +67,6 @@ import { TenantsComponent } from "./users/tenants/tenants.component";
 import { UserDetailsComponent } from "./users/user-details/user-details.component";
 import { UserRegistrationDialogComponent } from "./users/user-registration-dialog/user-registration-dialog.component";
 import { UsersComponent } from "./users/users.component";
-import { RolesTableComponent } from "./users/roles/tables/roles-table.component";
 
 export const ROUTES: Routes = [
 	{ component: DashboardComponent, path: "" },
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
index 550ed1bead..1ac559b6e1 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
+++ b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
@@ -12,7 +12,7 @@ limitations under the License.
 
 <mat-card class="table-page-content">
 	<div class="search-container">
-		<input type="search" name="fuzzControl" aria-label="Fuzzy Search Roles" autofocus inputmode="search" role="search" accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+		<input type="search" name="fuzzControl" aria-label="Fuzzy Search Roles" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
 	</div>
 	<tp-generic-table
 		[data]="roles | async"
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts
index e69de29bb2..f7df2655fc 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts
+++ b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts
@@ -0,0 +1,68 @@
+/*
+* 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, fakeAsync, TestBed, tick } from "@angular/core/testing";
+import { MatDialogModule } from "@angular/material/dialog";
+import { RouterTestingModule } from "@angular/router/testing";
+
+import { APITestingModule } from "src/app/api/testing";
+import { RoleTableComponent } from "src/app/core/users/roles/table/roles-table.component";
+
+describe("RoleTableComponent", () => {
+	let component: RoleTableComponent;
+	let fixture: ComponentFixture<RoleTableComponent>;
+
+	beforeEach(async () => {
+		await TestBed.configureTestingModule({
+			declarations: [ RoleTableComponent ],
+			imports: [ APITestingModule, RouterTestingModule, MatDialogModule ]
+		})
+			.compileComponents();
+
+		fixture = TestBed.createComponent(RoleTableComponent);
+		component = fixture.componentInstance;
+		fixture.detectChanges();
+	});
+
+	it("should create", () => {
+		expect(component).toBeTruthy();
+	});
+
+	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 contextmenu events", () => {
+		expect(async () => component.handleContextMenu({
+			action: component.contextMenuItems[0].name,
+			data: {name: "test", description: "Can only read", lastUpdated: new Date()}
+		})).not.toThrow();
+	});
+});
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
index e8bbba1603..814e4863b9 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
+++ b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
@@ -14,15 +14,14 @@
 
 import { Component, type OnInit } from "@angular/core";
 import { FormControl } from "@angular/forms";
+import { ActivatedRoute } from "@angular/router";
 import { BehaviorSubject } from "rxjs";
 import type { ResponseRole } from "trafficops-types";
 
 import { UserService } from "src/app/api";
 import { CurrentUserService } from "src/app/shared/current-user/current-user.service";
-import type { ContextMenuItem } from "src/app/shared/generic-table/generic-table.component";
+import type { ContextMenuActionEvent, ContextMenuItem } from "src/app/shared/generic-table/generic-table.component";
 import { NavigationService } from "src/app/shared/navigation/navigation.service";
-import { ActivatedRoute } from "@angular/router";
-import {ContextMenuActionEvent} from "src/app/shared/generic-table/generic-table.component";
 /**
  * AsnsTableComponent is the controller for the "Asns" table.
  */


[trafficcontrol] 04/05: Add role details.

Posted by rs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rshah pushed a commit to branch feature/tpv2-role-details
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit ecde018433e5380c65842ebe43ea12508a3d9fe4
Author: Rima Shah <ri...@comcast.com>
AuthorDate: Fri May 5 09:48:51 2023 -0600

    Add role details.
---
 .../traffic-portal/nightwatch/globals/globals.ts   |   2 +
 .../nightwatch/page_objects/users/roleDetails.ts   |  42 +++++++++
 .../nightwatch/tests/users/role/detail.spec.ts     |  44 +++++++++
 .../src/app/api/testing/user.service.ts            |  46 ++++++++++
 .../traffic-portal/src/app/api/user.service.ts     |  31 +++++++
 .../traffic-portal/src/app/core/core.module.ts     |   2 +
 .../users/roles/detail/role-detail.component.html  |  42 +++++++++
 .../users/roles/detail/role-detail.component.scss  |  26 ++++++
 .../roles/detail/role-detail.component.spec.ts     |  77 ++++++++++++++++
 .../users/roles/detail/role-detail.component.ts    | 101 +++++++++++++++++++++
 10 files changed, 413 insertions(+)

diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts b/experimental/traffic-portal/nightwatch/globals/globals.ts
index 3ca3773ea6..4034d330cb 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -41,6 +41,7 @@ import type { StatusDetailPageObject } from "nightwatch/page_objects/statuses/st
 import type { StatusesTablePageObject } from "nightwatch/page_objects/statuses/statusesTable";
 import type { ChangeLogsPageObject } from "nightwatch/page_objects/users/changeLogs";
 import type { RolesPageObject } from "nightwatch/page_objects/users/rolesTable";
+import type { RoleDetailPageObject } from "nightwatch/page_objects/users/roleDetail";
 import type { TenantDetailPageObject } from "nightwatch/page_objects/users/tenantDetail";
 import type { TenantsPageObject } from "nightwatch/page_objects/users/tenants";
 import type { UsersPageObject } from "nightwatch/page_objects/users/users";
@@ -135,6 +136,7 @@ declare module "nightwatch" {
 		users: {
 			changeLogs: () => ChangeLogsPageObject;
 			roles: () => RolesPageObject;
+			roleDetail: () => RoleDetailPageObject;
 			tenants: () => TenantsPageObject;
 			tenantDetail: () => TenantDetailPageObject;
 			users: () => UsersPageObject;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/users/roleDetails.ts b/experimental/traffic-portal/nightwatch/page_objects/users/roleDetails.ts
new file mode 100644
index 0000000000..de01fda53d
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/users/roleDetails.ts
@@ -0,0 +1,42 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { EnhancedPageObject } from "nightwatch";
+
+/**
+ * Defines the PageObject for Role Details.
+ */
+export type RoleDetailPageObject = EnhancedPageObject<{}, typeof roleDetailPageObject.elements>;
+
+const roleDetailPageObject = {
+	elements: {
+		description: {
+			selector: "input[name='description']"
+		},
+		lastUpdated: {
+			selector: "input[name='lastUpdated']"
+		},
+		name: {
+			selector: "input[name='name']"
+		},
+		permissions: {
+			selector: "input[name='permissions']"
+		},
+		saveBtn: {
+			selector: "button[type='submit']"
+		}
+	},
+};
+
+export default roleDetailPageObject;
diff --git a/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts b/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts
new file mode 100644
index 0000000000..8856106316
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts
@@ -0,0 +1,44 @@
+/*
+ * 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("Role Detail Spec", () => {
+	it("Test Role", () => {
+		const page = browser.page.users.roles();
+		browser.url(`${page.api.launchUrl}/core/roles/${browser.globals.testData.role.name}`, res => {
+			browser.assert.ok(res.status === 0);
+			page.waitForElementVisible("mat-card")
+				.assert.enabled("@role")
+				.assert.enabled("@description")
+				.assert.enabled("@permissions")
+				.assert.enabled("@saveBtn")
+				.assert.not.enabled("@lastUpdated")
+				.assert.valueEquals("@name", String(browser.globals.testData.role.name))
+				.assert.valueEquals("@permission", String(browser.globals.testData.role.permissions));
+		});
+	});
+
+	// it("New asn", () => {
+	// 	const page = browser.page.cacheGroups.asnDetail();
+	// 	browser.url(`${page.api.launchUrl}/core/asns/new`, res => {
+	// 		browser.assert.ok(res.status === 0);
+	// 		page.waitForElementVisible("mat-card")
+	// 			.assert.enabled("@asn")
+	// 			.assert.enabled("@cachegroup")
+	// 			.assert.enabled("@saveBtn")
+	// 			.assert.not.elementPresent("@id")
+	// 			.assert.not.elementPresent("@lastUpdated")
+	// 			.assert.valueEquals("@asn", "1");
+	// 	});
+	// });
+});
diff --git a/experimental/traffic-portal/src/app/api/testing/user.service.ts b/experimental/traffic-portal/src/app/api/testing/user.service.ts
index caf06c84a7..bcaf7c6dc2 100644
--- a/experimental/traffic-portal/src/app/api/testing/user.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/user.service.ts
@@ -17,6 +17,7 @@ import { Injectable } from "@angular/core";
 import type {
 	PostRequestUser,
 	PutRequestUser,
+	RequestRole,
 	RequestTenant,
 	ResponseCurrentUser,
 	ResponseRole,
@@ -428,6 +429,51 @@ export class UserService {
 		return this.roles;
 	}
 
+	/**
+	 * Creates a new role.
+	 *
+	 * @param role The role to create.
+	 * @returns The created role.
+	 */
+	public async createRole(role: RequestRole): Promise<ResponseRole> {
+		const resp = {
+			...role,
+			name: role.name,
+			lastUpdated: new Date(),
+		};
+		this.roles.push(resp);
+		return resp;
+	}
+
+	/**
+	 * Updates an existing role.
+	 *
+	 * @param role The role to update.
+	 * @returns The updated role.
+	 */
+	public async updateRole(role: ResponseRole): Promise<ResponseRole> {
+		const name = this.tenants.findIndex(r => r.name === role.name);
+		if (name === null) {
+			throw new Error(`no such Role: ${role.name}`);
+		}
+		this.roles[name] = role;
+		return role;
+	}
+
+	/**
+	 * Deletes an existing role.
+	 *
+	 * @param tenant The role to be deleted.
+	 * @returns The deleted role.
+	 */
+	public async deleteRole(role: ResponseRole): Promise<ResponseRole> {
+		const index = this.tenants.findIndex(r => r.name === role.name);
+		if (index < 0) {
+			throw new Error(`no such role: ${role.name}`);
+		}
+		return this.roles.splice(index, 1)[0];
+	}
+
 	/**
 	 * Retrieves all (visible) Tenants from Traffic Ops.
 	 *
diff --git a/experimental/traffic-portal/src/app/api/user.service.ts b/experimental/traffic-portal/src/app/api/user.service.ts
index 8ad95889cb..3b1bb51b1c 100644
--- a/experimental/traffic-portal/src/app/api/user.service.ts
+++ b/experimental/traffic-portal/src/app/api/user.service.ts
@@ -27,6 +27,7 @@ import {
 } from "trafficops-types";
 
 import { APIService } from "./base-api.service";
+import {RequestRole} from "trafficops-types";
 
 /**
  * UserService exposes API functionality related to Users, Roles and Tenants.
@@ -267,6 +268,36 @@ export class UserService extends APIService {
 		return this.get<Array<ResponseRole>>(path).toPromise();
 	}
 
+	/**
+	 * Creates a new Role.
+	 *
+	 * @param role The role to create.
+	 * @returns The created role.
+	 */
+	public async createRole(role: RequestRole): Promise<ResponseRole> {
+		return this.post<ResponseRole>("roles", role).toPromise();
+	}
+
+	/**
+	 * Updates an existing Role.
+	 *
+	 * @param role The role to update.
+	 * @returns The updated role.
+	 */
+	public async updateRole(role: ResponseRole): Promise<ResponseRole> {
+		return this.put<ResponseRole>(`roles/${role.name}`, role).toPromise();
+	}
+
+	/**
+	 * Deletes an existing role.
+	 *
+	 * @param tenant The role to be deleted.
+	 * @returns The deleted role.
+	 */
+	public async deleteRole(role: ResponseRole): Promise<ResponseRole> {
+		return this.delete<ResponseRole>(`roles/${role.name}`).toPromise();
+	}
+
 	/**
 	 * Retrieves Tenants from Traffic Ops.
 	 *
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts b/experimental/traffic-portal/src/app/core/core.module.ts
index 93fa833903..f90d850688 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -62,6 +62,7 @@ import { StatusesTableComponent } from "./statuses/statuses-table/statuses-table
 import { TypeDetailComponent } from "./types/detail/type-detail.component";
 import { TypesTableComponent } from "./types/table/types-table.component";
 import { RolesTableComponent } from "./users/roles/table/roles-table.component";
+import { RoleDetailComponent } from "./users/roles/detail/role-detail.component";
 import { TenantDetailsComponent } from "./users/tenants/tenant-details/tenant-details.component";
 import { TenantsComponent } from "./users/tenants/tenants.component";
 import { UserDetailsComponent } from "./users/user-details/user-details.component";
@@ -91,6 +92,7 @@ export const ROUTES: Routes = [
 	{ component: CacheGroupTableComponent, path: "cache-groups" },
 	{ component: CacheGroupDetailsComponent, path: "cache-groups/:id"},
 	{ component: RolesTableComponent, path: "roles"},
+	{ component: RoleDetailComponent, path: "roles/:name"},
 	{ component: TenantsComponent, path: "tenants"},
 	{ component: ChangeLogsComponent, path: "change-logs" },
 	{ component: TenantDetailsComponent, path: "tenants/:id"},
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html
new file mode 100644
index 0000000000..512420ec62
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html
@@ -0,0 +1,42 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<mat-card>
+	<tp-loading *ngIf="!role"></tp-loading>
+	<form ngNativeValidate (ngSubmit)="submit($event)" *ngIf="role">
+		<mat-card-content>
+			<mat-form-field *ngIf="!new">
+				<mat-label>ID</mat-label>
+				<input matInput type="text" name="name" disabled readonly [defaultValue]="role.name" />
+			</mat-form-field>
+			<mat-form-field>
+				<mat-label>ASN</mat-label>
+				<input matInput type="text" name="description" required [(ngModel)]="role.description" />
+			</mat-form-field>
+			<mat-form-field>
+				<mat-label>Cache Group</mat-label>
+				<mat-select name="permissions" [(ngModel)]="role.permissions" required>
+					<mat-option *ngFor="" [value]="">{{}}</mat-option>
+				</mat-select>
+			</mat-form-field>
+			<mat-form-field *ngIf="!new">
+				<mat-label>Last Updated</mat-label>
+				<input matInput type="text" name="lastUpdated" disabled readonly [defaultValue]="role.lastUpdated" /> </mat-form-field>
+		</mat-card-content>
+		<mat-card-actions align="end">
+			<button mat-raised-button type="button" *ngIf="!new" color="warn" (click)="deleteRole()">Delete</button>
+			<button mat-raised-button type="submit" color="primary">Save</button>
+		</mat-card-actions>
+	</form>
+</mat-card>
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.scss b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.scss
new file mode 100644
index 0000000000..fdfbde7654
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.scss
@@ -0,0 +1,26 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+mat-card {
+	margin: 1em auto;
+	width: 80%;
+	min-width: 350px;
+
+	mat-card-content {
+		display: grid;
+		grid-template-columns: 1fr;
+		row-gap: 2em;
+		margin: 1em auto 50px;
+	}
+}
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts
new file mode 100644
index 0000000000..7bbe9d896f
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts
@@ -0,0 +1,77 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { MatDialogModule } from "@angular/material/dialog";
+import { ActivatedRoute } from "@angular/router";
+import { RouterTestingModule } from "@angular/router/testing";
+import { ReplaySubject } from "rxjs";
+
+import { APITestingModule } from "src/app/api/testing";
+import { RoleDetailComponent } from "src/app/core/users/roles/detail/role-detail.component";
+import { NavigationService } from "src/app/shared/navigation/navigation.service";
+
+describe("RoleDetailComponent", () => {
+	let component: RoleDetailComponent;
+	let fixture: ComponentFixture<RoleDetailComponent>;
+	let route: ActivatedRoute;
+	let paramMap: jasmine.Spy;
+
+	const headerSvc = jasmine.createSpyObj([],{headerHidden: new ReplaySubject<boolean>(), headerTitle: new ReplaySubject<string>()});
+	beforeEach(async () => {
+		await TestBed.configureTestingModule({
+			declarations: [ RoleDetailComponent ],
+			imports: [ APITestingModule, RouterTestingModule, MatDialogModule ],
+			providers: [ { provide: NavigationService, useValue: headerSvc } ]
+		})
+			.compileComponents();
+
+		route = TestBed.inject(ActivatedRoute);
+		paramMap = spyOn(route.snapshot.paramMap, "get");
+		paramMap.and.returnValue(null);
+		fixture = TestBed.createComponent(RoleDetailComponent);
+		component = fixture.componentInstance;
+		fixture.detectChanges();
+	});
+
+	it("should create", () => {
+		expect(component).toBeTruthy();
+		expect(paramMap).toHaveBeenCalled();
+	});
+
+	it("new role", async () => {
+		paramMap.and.returnValue("new");
+
+		fixture = TestBed.createComponent(RoleDetailComponent);
+		component = fixture.componentInstance;
+		fixture.detectChanges();
+		await fixture.whenStable();
+		expect(paramMap).toHaveBeenCalled();
+		expect(component.roles).not.toBeNull();
+		expect(component.roles.name).toBe(1);
+		expect(component.new).toBeTrue();
+	});
+
+	it("existing role", async () => {
+		paramMap.and.returnValue("1");
+
+		fixture = TestBed.createComponent(RoleDetailComponent);
+		component = fixture.componentInstance;
+		fixture.detectChanges();
+		await fixture.whenStable();
+		expect(paramMap).toHaveBeenCalled();
+		expect(component.roles).not.toBeNull();
+		expect(component.roles.name).toBe(0);
+		expect(component.new).toBeFalse();
+	});
+});
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts
new file mode 100644
index 0000000000..9e7897eee3
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts
@@ -0,0 +1,101 @@
+/*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+import { Location } from "@angular/common";
+import { Component, OnInit } from "@angular/core";
+import { MatDialog } from "@angular/material/dialog";
+import { ActivatedRoute } from "@angular/router";
+import { ResponseRole } from "trafficops-types";
+
+import { UserService } 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";
+
+/**
+ * AsnDetailComponent is the controller for the ASN add/edit form.
+ */
+@Component({
+	selector: "tp-role-detail",
+	styleUrls: ["./role-detail.component.scss"],
+	templateUrl: "./role-detail.component.html"
+})
+export class RoleDetailComponent implements OnInit {
+	public new = false;
+	public asn!: ResponseRole;
+	constructor(private readonly route: ActivatedRoute, private readonly location: Location,
+				private readonly dialog: MatDialog, private readonly header: NavigationService) {
+	}
+
+	/**
+	 * Angular lifecycle hook where data is initialized.
+	 */
+	public async ngOnInit(): Promise<void> {
+		const role = this.route.snapshot.paramMap.get("name");
+		if (role === null) {
+			console.error("missing required route parameter 'name'");
+			return;
+		}
+
+		if (role === "new") {
+			this.header.headerTitle.next("New Role");
+			this.new = true;
+			this.role = {
+				description: "Read Only",
+				lastUpdated: new Date(),
+				name: "test",
+				permissions: []
+			};
+			return;
+		}
+
+		this.role = await this.UserService.getRoles(role);
+		this.header.headerTitle.next(`Role: ${this.role.name}`);
+	}
+
+	/**
+	 * Deletes the current ASN.
+	 */
+	public async deleteRole(): Promise<void> {
+		if (this.new) {
+			console.error("Unable to delete new role");
+			return;
+		}
+		const ref = this.dialog.open(DecisionDialogComponent, {
+			data: {message: `Are you sure you want to delete role ${this.role.name} with description ${this.role.description}`,
+				title: "Confirm Delete"}
+		});
+		ref.afterClosed().subscribe(result => {
+			if(result) {
+				this.UserService.deleteRole(this.role.name);
+				this.location.back();
+			}
+		});
+	}
+
+	/**
+	 * Submits new/updated ASN.
+	 *
+	 * @param e HTML form submission event.
+	 */
+	public async submit(e: Event): Promise<void> {
+		e.preventDefault();
+		e.stopPropagation();
+		if(this.new) {
+			this.asn = await this.UserService.createRole(this.role);
+			this.new = false;
+		} else {
+			this.asn = await this.UserService.updateRoleN(this.Role);
+		}
+	}
+
+}


[trafficcontrol] 05/05: Corrections wrt tests

Posted by rs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rshah pushed a commit to branch feature/tpv2-role-details
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit 2b172108f9c77bc263440fcb7637e2da180152eb
Author: Rima Shah <ri...@comcast.com>
AuthorDate: Wed May 10 23:31:57 2023 -0600

    Corrections wrt tests
---
 .../users/{roleDetails.ts => roleDetail.ts}        |  3 --
 .../nightwatch/tests/users/role/detail.spec.ts     | 32 ++++++++-------
 .../src/app/api/testing/user.service.ts            | 30 +++++++++------
 .../traffic-portal/src/app/api/user.service.ts     |  9 +++--
 .../traffic-portal/src/app/core/core.module.ts     |  1 +
 .../users/roles/detail/role-detail.component.html  | 19 ++++-----
 .../roles/detail/role-detail.component.spec.ts     | 19 ++++-----
 .../users/roles/detail/role-detail.component.ts    | 20 +++++-----
 .../roles/table/roles-table.component.spec.ts      | 45 ++++++++++++++++------
 .../users/roles/table/roles-table.component.ts     | 32 ++++++++++++---
 10 files changed, 124 insertions(+), 86 deletions(-)

diff --git a/experimental/traffic-portal/nightwatch/page_objects/users/roleDetails.ts b/experimental/traffic-portal/nightwatch/page_objects/users/roleDetail.ts
similarity index 94%
rename from experimental/traffic-portal/nightwatch/page_objects/users/roleDetails.ts
rename to experimental/traffic-portal/nightwatch/page_objects/users/roleDetail.ts
index de01fda53d..c4b0521b25 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/users/roleDetails.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/users/roleDetail.ts
@@ -24,9 +24,6 @@ const roleDetailPageObject = {
 		description: {
 			selector: "input[name='description']"
 		},
-		lastUpdated: {
-			selector: "input[name='lastUpdated']"
-		},
 		name: {
 			selector: "input[name='name']"
 		},
diff --git a/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts b/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts
index 8856106316..95d9d2a9ad 100644
--- a/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts
+++ b/experimental/traffic-portal/nightwatch/tests/users/role/detail.spec.ts
@@ -14,31 +14,29 @@
 
 describe("Role Detail Spec", () => {
 	it("Test Role", () => {
-		const page = browser.page.users.roles();
+		const page = browser.page.users.roleDetail();
 		browser.url(`${page.api.launchUrl}/core/roles/${browser.globals.testData.role.name}`, res => {
 			browser.assert.ok(res.status === 0);
 			page.waitForElementVisible("mat-card")
-				.assert.enabled("@role")
 				.assert.enabled("@description")
+				.assert.enabled("@name")
 				.assert.enabled("@permissions")
 				.assert.enabled("@saveBtn")
-				.assert.not.enabled("@lastUpdated")
 				.assert.valueEquals("@name", String(browser.globals.testData.role.name))
-				.assert.valueEquals("@permission", String(browser.globals.testData.role.permissions));
+				.assert.valueEquals("@permissions", String(browser.globals.testData.role.permissions));
 		});
 	});
 
-	// it("New asn", () => {
-	// 	const page = browser.page.cacheGroups.asnDetail();
-	// 	browser.url(`${page.api.launchUrl}/core/asns/new`, res => {
-	// 		browser.assert.ok(res.status === 0);
-	// 		page.waitForElementVisible("mat-card")
-	// 			.assert.enabled("@asn")
-	// 			.assert.enabled("@cachegroup")
-	// 			.assert.enabled("@saveBtn")
-	// 			.assert.not.elementPresent("@id")
-	// 			.assert.not.elementPresent("@lastUpdated")
-	// 			.assert.valueEquals("@asn", "1");
-	// 	});
-	// });
+	it("New asn", () => {
+		const page = browser.page.users.roleDetail();
+		browser.url(`${page.api.launchUrl}/core/roles/new`, res => {
+			browser.assert.ok(res.status === 0);
+			page.waitForElementVisible("mat-card")
+				.assert.enabled("@description")
+				.assert.enabled("@name")
+				.assert.enabled("@permissions")
+				.assert.enabled("@saveBtn")
+				.assert.valueEquals("@name", "test");
+		});
+	});
 });
diff --git a/experimental/traffic-portal/src/app/api/testing/user.service.ts b/experimental/traffic-portal/src/app/api/testing/user.service.ts
index bcaf7c6dc2..e176d4ce9f 100644
--- a/experimental/traffic-portal/src/app/api/testing/user.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/user.service.ts
@@ -86,6 +86,15 @@ export class UserService {
 			privLevel: 30
 		}
 	];
+	private readonly roleDetail: Array<ResponseRole> = [{
+		description: "Has access to everything - cannot be modified or deleted",
+		lastUpdated: new Date(),
+		name: "admin",
+		permissions: [
+			"ALL"
+		],
+	}
+	];
 
 	private readonly tenants: Array<ResponseTenant> = [
 		{
@@ -438,10 +447,8 @@ export class UserService {
 	public async createRole(role: RequestRole): Promise<ResponseRole> {
 		const resp = {
 			...role,
-			name: role.name,
-			lastUpdated: new Date(),
 		};
-		this.roles.push(resp);
+		this.roleDetail.push(resp);
 		return resp;
 	}
 
@@ -452,11 +459,11 @@ export class UserService {
 	 * @returns The updated role.
 	 */
 	public async updateRole(role: ResponseRole): Promise<ResponseRole> {
-		const name = this.tenants.findIndex(r => r.name === role.name);
-		if (name === null) {
+		const roleName = this.roleDetail.findIndex(r => r.name === role.name);
+		if (roleName < 0 ) {
 			throw new Error(`no such Role: ${role.name}`);
 		}
-		this.roles[name] = role;
+		this.roleDetail[roleName] = role;
 		return role;
 	}
 
@@ -466,12 +473,13 @@ export class UserService {
 	 * @param tenant The role to be deleted.
 	 * @returns The deleted role.
 	 */
-	public async deleteRole(role: ResponseRole): Promise<ResponseRole> {
-		const index = this.tenants.findIndex(r => r.name === role.name);
-		if (index < 0) {
-			throw new Error(`no such role: ${role.name}`);
+	public async deleteRole(role: string | ResponseRole): Promise<ResponseRole> {
+		const roleName = typeof(role) === "string" ? role : role.name;
+		const index = this.roleDetail.findIndex(r => r.name === roleName);
+		if (index === -1) {
+			throw new Error(`no such role: ${role}`);
 		}
-		return this.roles.splice(index, 1)[0];
+		return this.roleDetail.splice(index, 1)[0];
 	}
 
 	/**
diff --git a/experimental/traffic-portal/src/app/api/user.service.ts b/experimental/traffic-portal/src/app/api/user.service.ts
index 3b1bb51b1c..4a1ad356e9 100644
--- a/experimental/traffic-portal/src/app/api/user.service.ts
+++ b/experimental/traffic-portal/src/app/api/user.service.ts
@@ -17,6 +17,7 @@ import { Injectable } from "@angular/core";
 import {
 	type ResponseUser,
 	type PostRequestUser,
+	type RequestRole,
 	type RequestTenant,
 	type ResponseCurrentUser,
 	type ResponseRole,
@@ -27,7 +28,6 @@ import {
 } from "trafficops-types";
 
 import { APIService } from "./base-api.service";
-import {RequestRole} from "trafficops-types";
 
 /**
  * UserService exposes API functionality related to Users, Roles and Tenants.
@@ -285,7 +285,7 @@ export class UserService extends APIService {
 	 * @returns The updated role.
 	 */
 	public async updateRole(role: ResponseRole): Promise<ResponseRole> {
-		return this.put<ResponseRole>(`roles/${role.name}`, role).toPromise();
+		return this.put<ResponseRole>(`roles?name=${role.name}`, role).toPromise();
 	}
 
 	/**
@@ -294,8 +294,9 @@ export class UserService extends APIService {
 	 * @param tenant The role to be deleted.
 	 * @returns The deleted role.
 	 */
-	public async deleteRole(role: ResponseRole): Promise<ResponseRole> {
-		return this.delete<ResponseRole>(`roles/${role.name}`).toPromise();
+	public async deleteRole(role: string | ResponseRole): Promise<void> {
+		const roleName = typeof(role) === "string" ? role : role.name;
+		return this.delete(`roles?name=${roleName}`).toPromise();
 	}
 
 	/**
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts b/experimental/traffic-portal/src/app/core/core.module.ts
index f90d850688..e9b68f3cd6 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -131,6 +131,7 @@ export const ROUTES: Routes = [
 		TenantsComponent,
 		UserRegistrationDialogComponent,
 		RolesTableComponent,
+		RoleDetailComponent,
 		TenantDetailsComponent,
 		ChangeLogsComponent,
 		LastDaysComponent,
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html
index 512420ec62..9a46d242fb 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.html
@@ -16,23 +16,18 @@ limitations under the License.
 	<tp-loading *ngIf="!role"></tp-loading>
 	<form ngNativeValidate (ngSubmit)="submit($event)" *ngIf="role">
 		<mat-card-content>
-			<mat-form-field *ngIf="!new">
-				<mat-label>ID</mat-label>
-				<input matInput type="text" name="name" disabled readonly [defaultValue]="role.name" />
+			<mat-form-field>
+				<mat-label>Name</mat-label>
+				<input matInput type="text" name="name" required [(ngModel)]="role.name" />
 			</mat-form-field>
 			<mat-form-field>
-				<mat-label>ASN</mat-label>
-				<input matInput type="text" name="description" required [(ngModel)]="role.description" />
+				<mat-label>Description</mat-label>
+				<input matInput type="text" name="description" [(ngModel)]="role.description" />
 			</mat-form-field>
 			<mat-form-field>
-				<mat-label>Cache Group</mat-label>
-				<mat-select name="permissions" [(ngModel)]="role.permissions" required>
-					<mat-option *ngFor="" [value]="">{{}}</mat-option>
-				</mat-select>
+				<mat-label>Permissions</mat-label>
+				<textarea name="permissions" [(ngModel)]="role.permissions" matInput rows="10" wrap="hard" cdkTextareaAutosize></textarea>
 			</mat-form-field>
-			<mat-form-field *ngIf="!new">
-				<mat-label>Last Updated</mat-label>
-				<input matInput type="text" name="lastUpdated" disabled readonly [defaultValue]="role.lastUpdated" /> </mat-form-field>
 		</mat-card-content>
 		<mat-card-actions align="end">
 			<button mat-raised-button type="button" *ngIf="!new" color="warn" (click)="deleteRole()">Delete</button>
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts
index 7bbe9d896f..6e1d180395 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.spec.ts
@@ -12,11 +12,12 @@
 * limitations under the License.
 */
 import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { MatDialogModule } from "@angular/material/dialog";
+import { MatDialog, MatDialogModule, type MatDialogRef } from "@angular/material/dialog";
 import { ActivatedRoute } from "@angular/router";
 import { RouterTestingModule } from "@angular/router/testing";
-import { ReplaySubject } from "rxjs";
+import { of, ReplaySubject } from "rxjs";
 
+import { UserService } from "src/app/api";
 import { APITestingModule } from "src/app/api/testing";
 import { RoleDetailComponent } from "src/app/core/users/roles/detail/role-detail.component";
 import { NavigationService } from "src/app/shared/navigation/navigation.service";
@@ -57,21 +58,15 @@ describe("RoleDetailComponent", () => {
 		fixture.detectChanges();
 		await fixture.whenStable();
 		expect(paramMap).toHaveBeenCalled();
-		expect(component.roles).not.toBeNull();
-		expect(component.roles.name).toBe(1);
+		expect(component.role).not.toBeNull();
+		expect(component.role.name).toBe("");
 		expect(component.new).toBeTrue();
 	});
 
 	it("existing role", async () => {
-		paramMap.and.returnValue("1");
-
-		fixture = TestBed.createComponent(RoleDetailComponent);
-		component = fixture.componentInstance;
-		fixture.detectChanges();
-		await fixture.whenStable();
 		expect(paramMap).toHaveBeenCalled();
-		expect(component.roles).not.toBeNull();
-		expect(component.roles.name).toBe(0);
+		expect(component.role).not.toBeNull();
+		expect(component.role.name).toBe("");
 		expect(component.new).toBeFalse();
 	});
 });
diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts
index 9e7897eee3..ba81b5170f 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts
+++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts
@@ -31,9 +31,10 @@ import { NavigationService } from "src/app/shared/navigation/navigation.service"
 })
 export class RoleDetailComponent implements OnInit {
 	public new = false;
-	public asn!: ResponseRole;
-	constructor(private readonly route: ActivatedRoute, private readonly location: Location,
-				private readonly dialog: MatDialog, private readonly header: NavigationService) {
+	public role!: ResponseRole;
+	constructor(private readonly route: ActivatedRoute, private readonly userService: UserService,
+				private readonly location: Location, private readonly dialog: MatDialog,
+				private readonly header: NavigationService) {
 	}
 
 	/**
@@ -50,15 +51,14 @@ export class RoleDetailComponent implements OnInit {
 			this.header.headerTitle.next("New Role");
 			this.new = true;
 			this.role = {
-				description: "Read Only",
-				lastUpdated: new Date(),
-				name: "test",
+				description: "",
+				name: "",
 				permissions: []
 			};
 			return;
 		}
 
-		this.role = await this.UserService.getRoles(role);
+		this.role = await this.userService.getRoles(role);
 		this.header.headerTitle.next(`Role: ${this.role.name}`);
 	}
 
@@ -76,7 +76,7 @@ export class RoleDetailComponent implements OnInit {
 		});
 		ref.afterClosed().subscribe(result => {
 			if(result) {
-				this.UserService.deleteRole(this.role.name);
+				this.userService.deleteRole(this.role);
 				this.location.back();
 			}
 		});
@@ -91,10 +91,10 @@ export class RoleDetailComponent implements OnInit {
 		e.preventDefault();
 		e.stopPropagation();
 		if(this.new) {
-			this.asn = await this.UserService.createRole(this.role);
+			this.role = await this.userService.createRole(this.role);
 			this.new = false;
 		} else {
-			this.asn = await this.UserService.updateRoleN(this.Role);
+			this.role = await this.userService.updateRole(this.role);
 		}
 	}
 
diff --git a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.spec.ts b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.spec.ts
index 55d1ad2088..50455309ff 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.spec.ts
+++ b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.spec.ts
@@ -13,10 +13,12 @@
 */
 
 import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
-import { MatDialogModule } from "@angular/material/dialog";
+import { MatDialog, MatDialogModule, type MatDialogRef } from "@angular/material/dialog";
 import { RouterTestingModule } from "@angular/router/testing";
 import { ResponseRole } from "trafficops-types";
+import { of } from "rxjs";
 
+import { UserService } from "src/app/api";
 import { APITestingModule } from "src/app/api/testing";
 import { RolesTableComponent } from "src/app/core/users/roles/table/roles-table.component";
 import { isAction } from "src/app/shared/generic-table/generic-table.component";
@@ -35,8 +37,7 @@ describe("RolesTableComponent", () => {
 		await TestBed.configureTestingModule({
 			declarations: [ RolesTableComponent ],
 			imports: [ APITestingModule, RouterTestingModule, MatDialogModule ]
-		})
-			.compileComponents();
+		}).compileComponents();
 
 		fixture = TestBed.createComponent(RolesTableComponent);
 		component = fixture.componentInstance;
@@ -93,18 +94,38 @@ describe("RolesTableComponent", () => {
 		expect(item.href(role)).toBe(role.name);
 	});
 
-	it("has context menu items that aren't implemented yet", () => {
-		const item = component.contextMenuItems.find(i => i.name === "Edit");
+	it("deletes Roles", fakeAsync(async () => {
+		const item = component.contextMenuItems.find(i => i.name === "Delete");
 		if (!item) {
-			return fail("missing 'Edit' context menu item");
+			return fail("missing 'Delete' context menu item");
 		}
-		if (isAction(item)) {
-			return fail("incorrect type for 'Edit' menu item. Expected an action, not a link");
-		}
-		if (typeof(item.disabled) !== "function") {
-			return fail("'Edit' context menu item should be disabled, but no disabled function is defined");
+		if (!isAction(item)) {
+			return fail("incorrect type for 'Delete' menu item. Expected an action, not a link");
 		}
-	});
+		expect(item.multiRow).toBeFalsy();
+		expect(item.disabled).toBeUndefined();
+
+		const api = TestBed.inject(UserService);
+		const spy = spyOn(api, "deleteRole").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 testRole = await api.createRole(role);
+		expect(openSpy).not.toHaveBeenCalled();
+		const asyncExpectation = expectAsync(component.handleContextMenu({action: "delete", data: testRole})).toBeResolvedTo(undefined);
+		tick();
+
+		expect(openSpy).toHaveBeenCalled();
+		tick();
+
+		expect(spy).toHaveBeenCalled();
+
+		await asyncExpectation;
+	}));
 
 	it("generate 'View Users' context menu item href", () => {
 		const item = component.contextMenuItems.find(i => i.name === "View Users");
diff --git a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts
index 6e65b7ed2e..5e00253cce 100644
--- a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts
+++ b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts
@@ -20,8 +20,10 @@ import type { ResponseRole } from "trafficops-types";
 
 import { UserService } 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 type { ContextMenuActionEvent, ContextMenuItem } from "src/app/shared/generic-table/generic-table.component";
 import { NavigationService } from "src/app/shared/navigation/navigation.service";
+import {MatDialog} from "@angular/material/dialog";
 /**
  * RolesTableComponent is the controller for the "Roles" table.
  */
@@ -34,7 +36,7 @@ export class RolesTableComponent implements OnInit {
 	/** List of roles */
 	public roles: Promise<Array<ResponseRole>>;
 	constructor(private readonly route: ActivatedRoute, private readonly headerSvc: NavigationService,
-		private readonly api: UserService, public readonly auth: CurrentUserService) {
+		private readonly api: UserService, private readonly dialog: MatDialog, public readonly auth: CurrentUserService) {
 		this.fuzzySubject = new BehaviorSubject<string>("");
 		this.roles = this.api.getRoles();
 		this.headerSvc.headerTitle.next("Roles");
@@ -77,7 +79,6 @@ export class RolesTableComponent implements OnInit {
 	/** Definitions for the context menu items (which act on augmented roles data). */
 	public contextMenuItems: Array<ContextMenuItem<ResponseRole>> = [
 		{
-			disabled: (): true => true,
 			href: (selectedRow: ResponseRole): string => `${selectedRow.name}`,
 			name: "Edit"
 		},
@@ -86,6 +87,11 @@ export class RolesTableComponent implements OnInit {
 			name: "Open in New Tab",
 			newTab: true
 		},
+		{
+			action: "delete",
+			multiRow: false,
+			name: "Delete"
+		},
 		{
 			href: "/core/users",
 			name: "View Users",
@@ -107,9 +113,25 @@ export class RolesTableComponent implements OnInit {
 	/**
 	 * Handles a context menu event.
 	 *
-	 * @param a The action selected from the context menu.
+	 * @param evt The action selected from the context menu.
 	 */
-	public handleContextMenu(a: ContextMenuActionEvent<Readonly<ResponseRole>>): void {
-		console.log("action:", a);
+	public async handleContextMenu(evt: ContextMenuActionEvent<ResponseRole>): Promise<void> {
+		if (Array.isArray(evt.data)) {
+			console.error("cannot delete multiple roles at once:", evt.data);
+			return;
+		}
+		const data = evt.data;
+		switch(evt.action) {
+			case "delete":
+				const ref = this.dialog.open(DecisionDialogComponent, {
+					data: {message: `Are you sure you want to delete '${data.name}' role?`, title: "Confirm Delete"}
+				});
+				ref.afterClosed().subscribe(result => {
+					if(result) {
+						this.api.deleteRole(data.name).then(async () => this.roles = this.api.getRoles());
+					}
+				});
+				break;
+		}
 	}
 }


[trafficcontrol] 03/05: Updated folder name

Posted by rs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rshah pushed a commit to branch feature/tpv2-role-details
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit cacfc77372e52d859c921bc8fc11a5ebb49a4962
Author: Rima Shah <ri...@comcast.com>
AuthorDate: Wed May 3 22:34:58 2023 -0600

    Updated folder name
---
 .../users/roles/tables/roles-table.component.html  |  28 ------
 .../users/roles/tables/roles-table.component.scss  |  13 ---
 .../roles/tables/roles-table.component.spec.ts     |  68 -------------
 .../users/roles/tables/roles-table.component.ts    | 111 ---------------------
 4 files changed, 220 deletions(-)

diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
deleted file mode 100644
index 1ac559b6e1..0000000000
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-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 Roles" inputmode="search" role="search" accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
-	</div>
-	<tp-generic-table
-		[data]="roles | async"
-		[cols]="columnDefs"
-		[fuzzySearch]="fuzzySubject"
-		context="roles"
-		[contextMenuItems]="contextMenuItems"
-		(contextMenuAction)="handleContextMenu($event)">
-	</tp-generic-table>
-</mat-card>
-
-
-<a class="page-fab" mat-fab title="Create a new role" *ngIf="auth.hasPermission('ROLE:CREATE')" routerLink="new"><mat-icon>add</mat-icon></a>
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.scss b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.scss
deleted file mode 100644
index ebe77042d3..0000000000
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
-* 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/users/roles/tables/roles-table.component.spec.ts b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts
deleted file mode 100644
index f7df2655fc..0000000000
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
-* 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, fakeAsync, TestBed, tick } from "@angular/core/testing";
-import { MatDialogModule } from "@angular/material/dialog";
-import { RouterTestingModule } from "@angular/router/testing";
-
-import { APITestingModule } from "src/app/api/testing";
-import { RoleTableComponent } from "src/app/core/users/roles/table/roles-table.component";
-
-describe("RoleTableComponent", () => {
-	let component: RoleTableComponent;
-	let fixture: ComponentFixture<RoleTableComponent>;
-
-	beforeEach(async () => {
-		await TestBed.configureTestingModule({
-			declarations: [ RoleTableComponent ],
-			imports: [ APITestingModule, RouterTestingModule, MatDialogModule ]
-		})
-			.compileComponents();
-
-		fixture = TestBed.createComponent(RoleTableComponent);
-		component = fixture.componentInstance;
-		fixture.detectChanges();
-	});
-
-	it("should create", () => {
-		expect(component).toBeTruthy();
-	});
-
-	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 contextmenu events", () => {
-		expect(async () => component.handleContextMenu({
-			action: component.contextMenuItems[0].name,
-			data: {name: "test", description: "Can only read", lastUpdated: new Date()}
-		})).not.toThrow();
-	});
-});
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
deleted file mode 100644
index 814e4863b9..0000000000
--- a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-* 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, type OnInit } from "@angular/core";
-import { FormControl } from "@angular/forms";
-import { ActivatedRoute } from "@angular/router";
-import { BehaviorSubject } from "rxjs";
-import type { ResponseRole } from "trafficops-types";
-
-import { UserService } from "src/app/api";
-import { CurrentUserService } from "src/app/shared/current-user/current-user.service";
-import type { ContextMenuActionEvent, ContextMenuItem } from "src/app/shared/generic-table/generic-table.component";
-import { NavigationService } from "src/app/shared/navigation/navigation.service";
-/**
- * AsnsTableComponent is the controller for the "Asns" table.
- */
-@Component({
-	selector: "tp-roles",
-	styleUrls: ["./roles-table.component.scss"],
-	templateUrl: "./roles-table.component.html"
-})
-export class RolesTableComponent implements OnInit {
-	/** List of roles */
-	public roles: Promise<Array<ResponseRole>>;
-	constructor(private readonly route: ActivatedRoute, private readonly headerSvc: NavigationService,
-		private readonly api: UserService, public readonly auth: CurrentUserService) {
-		this.fuzzySubject = new BehaviorSubject<string>("");
-		this.roles = this.api.getRoles();
-		this.headerSvc.headerTitle.next("Roles");
-	}
-
-	/** 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();
-				}
-			},
-			e => {
-				console.error("Failed to get query parameters:", e);
-			}
-		);
-	}
-
-	/** Definitions of the table's columns according to the ag-grid API */
-	public columnDefs = [
-		{
-			field: "name",
-			headerName: "Name"
-		},
-		{
-			field: "description",
-			headerName: "Description",
-		},
-		{
-			field: "lastUpdated",
-			headerName: "Last Updated"
-		}
-	];
-
-	/** Definitions for the context menu items (which act on augmented asn data). */
-	public contextMenuItems: Array<ContextMenuItem<ResponseRole>> = [
-		{
-			href: (selectedRow: ResponseRole): string => `${selectedRow.name}`,
-			name: "View Role"
-		},
-		{
-			href: (selectedRow: ResponseRole): string => `${selectedRow.name}`,
-			name: "Open in New Tab",
-			newTab: true
-		},
-		{
-			href: (selectedRow: ResponseRole): string => `/core/users?role=${selectedRow.name}`,
-			name: "View Users"
-		},
-	];
-
-	/** 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 = new FormControl<string>("");
-
-	/** 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 a The action selected from the context menu.
-	 */
-	public handleContextMenu(a: ContextMenuActionEvent<Readonly<ResponseRole>>): void {
-		console.log("action:", a);
-	}
-}


[trafficcontrol] 01/05: Added files for roles table and a route for roles

Posted by rs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rshah pushed a commit to branch feature/tpv2-role-details
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit 37f8ff5fbf6bdab847850ceebed57358ed515fba
Author: Rima Shah <ri...@comcast.com>
AuthorDate: Wed May 3 11:42:59 2023 -0600

    Added files for roles table and a route for roles
---
 .../traffic-portal/src/app/core/core.module.ts     |   1 +
 .../users/roles/tables/roles-table.component.html  |  28 ++++++
 .../users/roles/tables/roles-table.component.scss  |  13 +++
 .../roles/tables/roles-table.component.spec.ts     |   0
 .../users/roles/tables/roles-table.component.ts    | 112 +++++++++++++++++++++
 5 files changed, 154 insertions(+)

diff --git a/experimental/traffic-portal/src/app/core/core.module.ts b/experimental/traffic-portal/src/app/core/core.module.ts
index 93fa833903..7ea7c23260 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -67,6 +67,7 @@ import { TenantsComponent } from "./users/tenants/tenants.component";
 import { UserDetailsComponent } from "./users/user-details/user-details.component";
 import { UserRegistrationDialogComponent } from "./users/user-registration-dialog/user-registration-dialog.component";
 import { UsersComponent } from "./users/users.component";
+import { RolesTableComponent } from "./users/roles/tables/roles-table.component";
 
 export const ROUTES: Routes = [
 	{ component: DashboardComponent, path: "" },
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
new file mode 100644
index 0000000000..550ed1bead
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.html
@@ -0,0 +1,28 @@
+<!--
+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 Roles" autofocus inputmode="search" role="search" accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+	</div>
+	<tp-generic-table
+		[data]="roles | async"
+		[cols]="columnDefs"
+		[fuzzySearch]="fuzzySubject"
+		context="roles"
+		[contextMenuItems]="contextMenuItems"
+		(contextMenuAction)="handleContextMenu($event)">
+	</tp-generic-table>
+</mat-card>
+
+
+<a class="page-fab" mat-fab title="Create a new role" *ngIf="auth.hasPermission('ROLE:CREATE')" routerLink="new"><mat-icon>add</mat-icon></a>
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.scss b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.scss
new file mode 100644
index 0000000000..ebe77042d3
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-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/users/roles/tables/roles-table.component.spec.ts b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.spec.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
new file mode 100644
index 0000000000..e8bbba1603
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/users/roles/tables/roles-table.component.ts
@@ -0,0 +1,112 @@
+/*
+* 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, type OnInit } from "@angular/core";
+import { FormControl } from "@angular/forms";
+import { BehaviorSubject } from "rxjs";
+import type { ResponseRole } from "trafficops-types";
+
+import { UserService } from "src/app/api";
+import { CurrentUserService } from "src/app/shared/current-user/current-user.service";
+import type { ContextMenuItem } from "src/app/shared/generic-table/generic-table.component";
+import { NavigationService } from "src/app/shared/navigation/navigation.service";
+import { ActivatedRoute } from "@angular/router";
+import {ContextMenuActionEvent} from "src/app/shared/generic-table/generic-table.component";
+/**
+ * AsnsTableComponent is the controller for the "Asns" table.
+ */
+@Component({
+	selector: "tp-roles",
+	styleUrls: ["./roles-table.component.scss"],
+	templateUrl: "./roles-table.component.html"
+})
+export class RolesTableComponent implements OnInit {
+	/** List of roles */
+	public roles: Promise<Array<ResponseRole>>;
+	constructor(private readonly route: ActivatedRoute, private readonly headerSvc: NavigationService,
+		private readonly api: UserService, public readonly auth: CurrentUserService) {
+		this.fuzzySubject = new BehaviorSubject<string>("");
+		this.roles = this.api.getRoles();
+		this.headerSvc.headerTitle.next("Roles");
+	}
+
+	/** 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();
+				}
+			},
+			e => {
+				console.error("Failed to get query parameters:", e);
+			}
+		);
+	}
+
+	/** Definitions of the table's columns according to the ag-grid API */
+	public columnDefs = [
+		{
+			field: "name",
+			headerName: "Name"
+		},
+		{
+			field: "description",
+			headerName: "Description",
+		},
+		{
+			field: "lastUpdated",
+			headerName: "Last Updated"
+		}
+	];
+
+	/** Definitions for the context menu items (which act on augmented asn data). */
+	public contextMenuItems: Array<ContextMenuItem<ResponseRole>> = [
+		{
+			href: (selectedRow: ResponseRole): string => `${selectedRow.name}`,
+			name: "View Role"
+		},
+		{
+			href: (selectedRow: ResponseRole): string => `${selectedRow.name}`,
+			name: "Open in New Tab",
+			newTab: true
+		},
+		{
+			href: (selectedRow: ResponseRole): string => `/core/users?role=${selectedRow.name}`,
+			name: "View Users"
+		},
+	];
+
+	/** 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 = new FormControl<string>("");
+
+	/** 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 a The action selected from the context menu.
+	 */
+	public handleContextMenu(a: ContextMenuActionEvent<Readonly<ResponseRole>>): void {
+		console.log("action:", a);
+	}
+}