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:27 UTC

[trafficcontrol] 04/05: Add role details.

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);
+		}
+	}
+
+}