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