This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new e7dbab875e [TPv2] Added Roles table (#7486)
e7dbab875e is described below
commit e7dbab875e7c94ad72b42a781efff3d5b3871c6d
Author: Rima Shah <[email protected]>
AuthorDate: Wed May 10 15:35:51 2023 -0600
[TPv2] Added Roles table (#7486)
* Added files for roles table and a route for roles
* Added test case (e2e and unit test for roles)
* Updated folder name
* Fixed Api version and browser page path
* Fixed query param name
* Reordered json.
* Added unit tests for context menu.
* Fixed lint issue
* Added filter to lastUpdated and hid the field, added role type
---
.../traffic-portal/nightwatch/globals/globals.ts | 26 +++-
.../nightwatch/page_objects/common.ts | 1 +
.../nightwatch/page_objects/users/rolesTable.ts | 46 +++++++
.../nightwatch/tests/users/role/table.spec.ts | 26 ++++
.../cache-group-table.component.spec.ts | 2 +-
.../traffic-portal/src/app/core/core.module.ts | 3 +
.../users/roles/table/roles-table.component.html | 28 +++++
.../users/roles/table/roles-table.component.scss | 13 ++
.../roles/table/roles-table.component.spec.ts | 133 +++++++++++++++++++++
.../users/roles/table/roles-table.component.ts | 115 ++++++++++++++++++
.../app/shared/navigation/navigation.service.ts | 3 +
11 files changed, 391 insertions(+), 5 deletions(-)
diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts
b/experimental/traffic-portal/nightwatch/globals/globals.ts
index bcc09ca6bf..3ca3773ea6 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -40,6 +40,7 @@ import type { ServersPageObject } from
"nightwatch/page_objects/servers/servers"
import type { StatusDetailPageObject } from
"nightwatch/page_objects/statuses/statusDetail";
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 { 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";
@@ -59,6 +60,7 @@ import {
type RequestPhysicalLocation,
type RequestProfile,
type RequestRegion,
+ type RequestRole,
type RequestServerCapability,
type RequestStatus,
type RequestSteeringTarget,
@@ -73,6 +75,7 @@ import {
type ResponsePhysicalLocation,
type ResponseProfile,
type ResponseRegion,
+ type ResponseRole,
type ResponseServerCapability,
type ResponseStatus,
type ResponseTenant,
@@ -131,6 +134,7 @@ declare module "nightwatch" {
};
users: {
changeLogs: () => ChangeLogsPageObject;
+ roles: () => RolesPageObject;
tenants: () => TenantsPageObject;
tenantDetail: () => TenantDetailPageObject;
users: () => UsersPageObject;
@@ -158,6 +162,7 @@ declare module "nightwatch" {
* Contains the data created by the client before the test suite runs.
*/
export interface CreatedData {
+ asn: ResponseASN;
cacheGroup: ResponseCacheGroup;
capability: ResponseServerCapability;
cdn: ResponseCDN;
@@ -165,14 +170,14 @@ export interface CreatedData {
division: ResponseDivision;
ds: ResponseDeliveryService;
ds2: ResponseDeliveryService;
+ profile: ResponseProfile;
physLoc: ResponsePhysicalLocation;
region: ResponseRegion;
- asn: ResponseASN;
+ role: ResponseRole;
+ statuses: ResponseStatus;
steeringDS: ResponseDeliveryService;
tenant: ResponseTenant;
type: TypeFromResponse;
- statuses: ResponseStatus;
- profile: ResponseProfile;
}
const testData = {};
@@ -418,7 +423,7 @@ const globals = {
url = `${apiUrl}/statuses`;
resp = await client.post(url, JSON.stringify(status));
const respStatus: ResponseStatus = resp.data.response;
- console.log(`Successfully created Profile
${respStatus.name}`);
+ console.log(`Successfully created Status
${respStatus.name}`);
data.statuses = respStatus;
const profile: RequestProfile = {
@@ -443,6 +448,19 @@ const globals = {
console.log("Successfully created Capability:",
respCap);
data.capability = respCap;
+ const role: RequestRole = {
+ description: "Has access to everything - cannot
be modified or deleted",
+ name: `admin${globals.uniqueString}`,
+ permissions: [
+ "ALL"
+ ]
+ };
+ url = `${apiUrl}/roles`;
+ resp = await client.post(url, JSON.stringify(role));
+ const respRole: ResponseRole = resp.data.response;
+ console.log(`Successfully created Roles
${respRole.name}`);
+ data.role = respRole;
+
} catch(e) {
console.error("Request for", url, "failed:", (e as
AxiosError).message);
throw e;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/common.ts
b/experimental/traffic-portal/nightwatch/page_objects/common.ts
index c93a3292f5..2803e26a1d 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/common.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/common.ts
@@ -62,6 +62,7 @@ const commonPageObject = {
profile: "[aria-label='Navigate to My
Profile']",
profiles: "[aria-label='Navigate to Profiles']",
regions: "[aria-label='Navigate to Regions']",
+ roles: "[aria-label='Navigate to Roles']",
servers: "[aria-label='Navigate to Servers']",
serversContainer: "[aria-label='Toggle
Servers']",
statuses: "[aria-label='Navigate to Statuses']",
diff --git
a/experimental/traffic-portal/nightwatch/page_objects/users/rolesTable.ts
b/experimental/traffic-portal/nightwatch/page_objects/users/rolesTable.ts
new file mode 100644
index 0000000000..b66168b151
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/users/rolesTable.ts
@@ -0,0 +1,46 @@
+/*
+ * 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, EnhancedSectionInstance, NightwatchAPI } from
"nightwatch";
+
+import { TABLE_COMMANDS, TableSectionCommands } from "../../globals/tables";
+
+/**
+ * Defines the Roles table commands
+ */
+type RolesTableCommands = TableSectionCommands;
+
+/**
+ * Defines the Page Object for the Roles page.
+ */
+export type RolesPageObject = EnhancedPageObject<{}, {},
+EnhancedSectionInstance<RolesTableCommands>>;
+
+const rolesPageObject = {
+ api: {} as NightwatchAPI,
+ sections: {
+ rolesTable: {
+ commands: {
+ ...TABLE_COMMANDS
+ },
+ elements: {},
+ selector: "mat-card"
+ }
+ },
+ url(): string {
+ return `${this.api.launchUrl}/core/roles`;
+ }
+};
+
+export default rolesPageObject;
diff --git
a/experimental/traffic-portal/nightwatch/tests/users/role/table.spec.ts
b/experimental/traffic-portal/nightwatch/tests/users/role/table.spec.ts
new file mode 100644
index 0000000000..531eaabb43
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/users/role/table.spec.ts
@@ -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.
+ */
+
+describe("Roles Spec", () => {
+ it("Loads elements", async () => {
+ await browser.page.common()
+ .section.sidebar
+ .navigateToNode("roles", ["usersContainer"]);
+ await browser.waitForElementPresent("input[name=fuzzControl]");
+ await browser.elements("css selector", "div.ag-row", rows => {
+ browser.assert.ok(rows.status === 0);
+ browser.assert.ok((rows.value as []).length >= 1);
+ });
+ });
+});
diff --git
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.spec.ts
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.spec.ts
index e62b5056c3..6cf5724506 100644
---
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.spec.ts
+++
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.spec.ts
@@ -202,7 +202,7 @@ describe("CacheGroupTableComponent", () => {
expect(item.href).toBe("/core/asns");
if (typeof(item.queryParams) !== "function") {
return fail(
- `'Mange ASNs' context menu item should use a
function to determine query params, instead uses: ${item.queryParams}`
+ `'Manage ASNs' context menu item should use a
function to determine query params, instead uses: ${item.queryParams}`
);
}
expect(item.queryParams(sampleCG)).toEqual({cachegroup:
sampleCG.name});
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts
b/experimental/traffic-portal/src/app/core/core.module.ts
index 72b657ab10..93fa833903 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -61,6 +61,7 @@ import { StatusDetailsComponent } from
"./statuses/status-details/status-details
import { StatusesTableComponent } from
"./statuses/statuses-table/statuses-table.component";
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 { 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";
@@ -89,6 +90,7 @@ export const ROUTES: Routes = [
{ component: NewDeliveryServiceComponent, path: "new.Delivery.Service"
},
{ component: CacheGroupTableComponent, path: "cache-groups" },
{ component: CacheGroupDetailsComponent, path: "cache-groups/:id"},
+ { component: RolesTableComponent, path: "roles"},
{ component: TenantsComponent, path: "tenants"},
{ component: ChangeLogsComponent, path: "change-logs" },
{ component: TenantDetailsComponent, path: "tenants/:id"},
@@ -126,6 +128,7 @@ export const ROUTES: Routes = [
UserDetailsComponent,
TenantsComponent,
UserRegistrationDialogComponent,
+ RolesTableComponent,
TenantDetailsComponent,
ChangeLogsComponent,
LastDaysComponent,
diff --git
a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.html
b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.html
new file mode 100644
index 0000000000..1ac559b6e1
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/users/roles/table/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" 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/table/roles-table.component.scss
b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.scss
new file mode 100644
index 0000000000..ebe77042d3
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/users/roles/table/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/table/roles-table.component.spec.ts
b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.spec.ts
new file mode 100644
index 0000000000..55d1ad2088
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.spec.ts
@@ -0,0 +1,133 @@
+/*
+* 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 { ResponseRole } from "trafficops-types";
+
+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";
+
+describe("RolesTableComponent", () => {
+ let component: RolesTableComponent;
+ let fixture: ComponentFixture<RolesTableComponent>;
+
+ const role: ResponseRole = {
+ description: "Test Role",
+ lastUpdated: new Date(),
+ name: "test"
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ RolesTableComponent ],
+ imports: [ APITestingModule, RouterTestingModule,
MatDialogModule ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(RolesTableComponent);
+ 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: {description: "Can only read", lastUpdated: new
Date(), name: "test"}
+ })).not.toThrow();
+ });
+
+ it("builds an 'Open in New Tab' link", () => {
+ const item = component.contextMenuItems.find(i => i.name ===
"Open in New Tab");
+ if (!item) {
+ return fail("missing 'Open in New Tab' context menu
item");
+ }
+
+ if (isAction(item)) {
+ return fail("incorrect type for 'Open in New Tab' menu
item. Expected an action, not a link");
+ }
+
+ expect(item.newTab).toBe(true);
+
+ if (typeof(item.href) !== "function") {
+ return fail("link should be built from data, not
static");
+ }
+
+ 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");
+ if (!item) {
+ return fail("missing 'Edit' 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");
+ }
+ });
+
+ it("generate 'View Users' context menu item href", () => {
+ const item = component.contextMenuItems.find(i => i.name ===
"View Users");
+ if (!item) {
+ return fail("missing 'View Users' context menu item");
+ }
+ if (isAction(item)) {
+ return fail("incorrect type for 'View Users' menu item.
Expected an action, not a link");
+ }
+ if (!item.href) {
+ return fail("missing 'href' property");
+ }
+ if (typeof(item.href) !== "string") {
+ return fail("'View Users' context menu item should use
a static string to determine href, instead uses a function");
+ }
+ expect(item.href).toBe("/core/users");
+ if (typeof(item.queryParams) !== "function") {
+ return fail(
+ `'View Users' context menu item should use a
function to determine query params, instead uses: ${item.queryParams}`
+ );
+ }
+ expect(item.queryParams(role)).toEqual({role: role.name});
+ expect(item.fragment).toBeUndefined();
+ expect(item.newTab).toBeFalsy();
+ });
+});
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
new file mode 100644
index 0000000000..6e65b7ed2e
--- /dev/null
+++
b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts
@@ -0,0 +1,115 @@
+/*
+* 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, type Params} 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";
+/**
+ * RolesTableComponent is the controller for the "Roles" 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",
+ filter: "agDateColumnFilter",
+ headerName: "Last Updated",
+ hide: true,
+ }
+ ];
+
+ /** 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"
+ },
+ {
+ href: (selectedRow: ResponseRole): string =>
`${selectedRow.name}`,
+ name: "Open in New Tab",
+ newTab: true
+ },
+ {
+ href: "/core/users",
+ name: "View Users",
+ queryParams: (selectedRow): Params => ({role:
selectedRow.name})
+ },
+ ];
+
+ /** 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);
+ }
+}
diff --git
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
index 52799d9c51..9a31ae2319 100644
---
a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
+++
b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
@@ -163,6 +163,9 @@ export class NavigationService {
}, {
href: "/core/me",
name: "My Profile"
+ }, {
+ href: "/core/roles",
+ name: "Roles"
}, {
href: "/core/tenants",
name: "Tenants"