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 c03bcf8c73831ecdf3e6038eb0df1a0c05c1ce0a Author: Rima Shah <[email protected]> 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); + } + } + +}
