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"

Reply via email to