This is an automated email from the ASF dual-hosted git repository.

shamrick 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 40b6549074 TPv2 Tenants table (#6964)
40b6549074 is described below

commit 40b654907473b6cb6c34e593302672cc8dd6ce7c
Author: ocket8888 <[email protected]>
AuthorDate: Fri Jul 15 11:00:44 2022 -0600

    TPv2 Tenants table (#6964)
    
    * Move table styling to common stylesheet
    
    * Ensure no mutation of input data sets in generic tables, remove orderby
    
    * Add Tenant table component and route
    
    * Fix generic table not properly detecting that a multi-row action should 
be disabled when no rows are explicitly selected
    
    * Add header links to new Tenants page
    
    * Add/fix tests
    
    * Fix not setting page title appropriately
    
    * Fix typo, buttons not disabled that should be
---
 .../cache-group-table.component.html               |   4 +-
 .../cache-group-table.component.scss               |  19 ---
 .../traffic-portal/src/app/core/core.module.ts     |   7 +-
 .../app/core/dashboard/dashboard.component.html    |   6 +-
 .../app/core/dashboard/dashboard.component.scss    |  30 ++--
 .../servers-table/servers-table.component.html     |   4 +-
 .../servers-table/servers-table.component.scss     |  18 ---
 .../tenants/tenants.component.html}                |  18 ++-
 .../app/core/users/tenants/tenants.component.scss  |   0
 .../core/users/tenants/tenants.component.spec.ts   |  90 +++++++++++
 .../app/core/users/tenants/tenants.component.ts    | 168 +++++++++++++++++++++
 .../generic-table/generic-table.component.ts       |   4 +-
 .../traffic-portal/src/app/shared/shared.module.ts |   4 +-
 .../app/shared/tp-header/tp-header.component.html  |  14 +-
 .../app/shared/tp-header/tp-header.component.scss  |   2 +-
 experimental/traffic-portal/src/styles.scss        |  28 ++++
 16 files changed, 340 insertions(+), 76 deletions(-)

diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
 
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
index eb225571eb..500304b1a9 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
@@ -11,8 +11,8 @@ 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>
-       <div>
+<mat-card class="table-page-content">
+       <div class="search-container">
                <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Servers" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()"/>
        </div>
        <tp-generic-table
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.scss
 
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.scss
index 5e8f090ff8..dfe77f5b53 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.scss
+++ 
b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.scss
@@ -17,25 +17,6 @@ ag-grid-angular {
        height: 85vh;
 }
 
-mat-card {
-       width: fit-content;
-       margin: 15px auto;
-
-       div {
-               width: 50%;
-               margin: auto;
-               padding-right: 10px;
-               position: sticky;
-               top: 0;
-               z-index: 2;
-
-               input {
-                       width: 100%;
-                       margin: 15px 0;
-               }
-       }
-}
-
 @media(max-width: 1230px) {
        mat-card > div {
                width: 75%;
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts 
b/experimental/traffic-portal/src/app/core/core.module.ts
index 4e123e3655..ed858d037f 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -36,6 +36,7 @@ import { NewDeliveryServiceComponent } from 
"./new-delivery-service/new-delivery
 import { ServerDetailsComponent } from 
"./servers/server-details/server-details.component";
 import { ServersTableComponent } from 
"./servers/servers-table/servers-table.component";
 import { UpdateStatusComponent } from 
"./servers/update-status/update-status.component";
+import { TenantsComponent } from "./users/tenants/tenants.component";
 import { UserDetailsComponent } from 
"./users/user-details/user-details.component";
 import { UsersComponent } from "./users/users.component";
 
@@ -49,7 +50,8 @@ const routes: Routes = [
        { canActivate: [AuthenticatedGuard], component: 
InvalidationJobsComponent, path: "deliveryservice/:id/invalidation-jobs" },
        { canActivate: [AuthenticatedGuard], component: CurrentuserComponent, 
path: "me" },
        { canActivate: [AuthenticatedGuard], component: 
NewDeliveryServiceComponent, path: "new.Delivery.Service" },
-       { canActivate: [AuthenticatedGuard], component: 
CacheGroupTableComponent, path: "cache-groups" }
+       { canActivate: [AuthenticatedGuard], component: 
CacheGroupTableComponent, path: "cache-groups" },
+       { canActivate: [AuthenticatedGuard], component: TenantsComponent, path: 
"tenants"}
 ];
 
 /**
@@ -70,7 +72,8 @@ const routes: Routes = [
                CacheGroupTableComponent,
                NewInvalidationJobDialogComponent,
                UpdateStatusComponent,
-               UserDetailsComponent
+               UserDetailsComponent,
+               TenantsComponent
        ],
        exports: [
        ],
diff --git 
a/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html 
b/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html
index 7e2dcd2c8a..d4b0f11934 100644
--- 
a/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html
+++ 
b/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.html
@@ -11,11 +11,11 @@ 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.
 -->
-<main>
-       <div><input type="search" role="search" aria-label="Fuzzy Search 
Delivery Service(s)" autofocus inputmode="search" name="fuzzControl" 
[formControl]="fuzzControl" (input)="updateURL($event)" accesskey="/" 
placeholder="Fuzzy Search"/></div>
+<main class="table-page-content">
+       <div class="search-container"><input type="search" role="search" 
aria-label="Fuzzy Search Delivery Service(s)" autofocus inputmode="search" 
name="fuzzControl" [formControl]="fuzzControl" (input)="updateURL($event)" 
accesskey="/" placeholder="Fuzzy Search"/></div>
        <article id="deliveryservices" [hidden]="loading">
                <ds-card *ngFor="let ds of filteredDSes; trackBy: tracker; let 
first=first; let last=last;" [deliveryService]="ds" [now]="now" [today]="today" 
[first]=first [last]=last></ds-card>
        </article>
        <div id="loading" *ngIf="loading"><tp-loading></tp-loading></div>
 </main>
-<a mat-fab id="new" *ngIf="canCreateDeliveryServices" title="Create a new 
Delivery Service" 
routerLink="/core/new.Delivery.Service"><mat-icon>add</mat-icon></a>
+<a mat-fab class="page-fab" id="new" *ngIf="canCreateDeliveryServices" 
title="Create a new Delivery Service" 
routerLink="/core/new.Delivery.Service"><mat-icon>add</mat-icon></a>
diff --git 
a/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.scss 
b/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.scss
index 92fafd9fa5..2cc9b816e1 100644
--- 
a/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.scss
+++ 
b/experimental/traffic-portal/src/app/core/dashboard/dashboard.component.scss
@@ -17,16 +17,20 @@
        margin: auto;
 }
 
-main > div {
-       width: 50%;
-       margin: auto;
-       padding-right: 10px;
-       position: sticky;
-       top: 0;
-       z-index: 2;
-       input {
-               width: 100%;
-               margin: 15px 0;
+main {
+       width: initial;
+
+       & > div {
+               width: 50%;
+               margin: auto;
+               padding-right: 10px;
+               position: sticky;
+               top: 0;
+               z-index: 2;
+               input {
+                       width: 100%;
+                       margin: 15px 0;
+               }
        }
 }
 
@@ -71,9 +75,3 @@ main > div {
        top: auto;
        position: static;
 }
-
-a#new {
-       position: fixed;
-       bottom: 5px;
-       right: 5px;
-}
diff --git 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
index 80df0155f7..0ef67af7cf 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.html
@@ -12,8 +12,8 @@ See the License for the specific language governing 
permissions and
 limitations under the License.
 -->
 <main>
-       <mat-card>
-               <div>
+       <mat-card class="table-page-content">
+               <div class="search-container">
                        <input type="search" name="fuzzControl" 
aria-label="Fuzzy Search Servers" autofocus inputmode="search" role="search" 
accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" 
(input)="updateURL()"/>
                </div>
                <tp-generic-table
diff --git 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.scss
 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.scss
index 74f6c40702..740cda2d42 100644
--- 
a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.scss
+++ 
b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.scss
@@ -18,24 +18,6 @@ ag-grid-angular {
        height: 85vh;
 }
 
-mat-card {
-       width: fit-content;
-       margin: 1em auto;
-
-       div {
-               width: 50%;
-               margin: auto;
-               padding-right: 10px;
-               position: sticky;
-               top: 0;
-               z-index: 2;
-               input {
-                       width: 100%;
-                       margin: 0 0 15px;
-               }
-       }
-}
-
 @media(max-width: 1230px) {
        mat-card > div {
                width: 75%
diff --git 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
 b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html
similarity index 58%
copy from 
experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
copy to 
experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html
index eb225571eb..843d65d5e8 100644
--- 
a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.html
+++ 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.html
@@ -11,16 +11,20 @@ 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>
-       <div>
-               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Servers" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()"/>
+<mat-card class="table-page-content">
+       <div class="search-container">
+               <input type="search" name="fuzzControl" aria-label="Fuzzy 
Search Users" autofocus inputmode="search" role="search" accesskey="/" 
placeholder="Fuzzy Search" [(ngModel)]="searchText" (input)="updateURL()"/>
        </div>
        <tp-generic-table
-               [data]="cacheGroups | async"
+               [data]="tenants"
                [cols]="columnDefs"
-               [fuzzySearch]="fuzzySubject"
-               context="cache-groups"
+               [fuzzySearch]="searchSubject"
+               context="tenants"
                [contextMenuItems]="contextMenuItems"
-               (contextMenuAction)="handleContextMenu($event)">
+               (contextMenuAction)="handleContextMenu($event)"
+       >
        </tp-generic-table>
+       <div id="loading" *ngIf="loading"><tp-loading></tp-loading></div>
 </mat-card>
+
+<button class="page-fab" mat-fab title="Create a new Tenant" 
disabled><mat-icon>add</mat-icon></button>
diff --git 
a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.scss 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git 
a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.spec.ts
new file mode 100644
index 0000000000..c3fb5e20ea
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.spec.ts
@@ -0,0 +1,90 @@
+/*
+* 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 { BehaviorSubject } from "rxjs";
+
+import { APITestingModule } from "src/app/api/testing";
+import { CurrentUserService } from 
"src/app/shared/currentUser/current-user.service";
+
+import { TenantsComponent } from "./tenants.component";
+
+describe("TenantsComponent", () => {
+       let component: TenantsComponent;
+       let fixture: ComponentFixture<TenantsComponent>;
+
+       beforeEach(async () => {
+               await TestBed.configureTestingModule({
+                       declarations: [ TenantsComponent ],
+                       imports: [ APITestingModule ],
+                       providers: [
+                               {
+                                       provide: CurrentUserService,
+                                       useValue: {
+                                               currentUser: {
+                                                       tenantId: 1
+                                               },
+                                               hasPermission: (): true => true,
+                                               userChanged: new 
BehaviorSubject({})
+                                       }
+                               }
+                       ]
+               })
+                       .compileComponents();
+
+               fixture = TestBed.createComponent(TenantsComponent);
+               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.searchSubject.subscribe(spy);
+               tick();
+               expect(spy).toHaveBeenCalled();
+               component.searchText = text;
+               component.updateURL();
+               tick();
+               expect(spy).toHaveBeenCalledTimes(2);
+       }));
+
+       it("renders parent Tenants", () => {
+               expect(component.getParentString({active: true, id: 1, 
lastUpdated: new Date(), name: "root", parentId: null})).toBe("");
+       });
+
+       it("handles contextmenu events", () => {
+               expect(()=>component.handleContextMenu({
+                       action: component.contextMenuItems[0].name,
+                       data: {
+                               active: true,
+                               id: 1,
+                               lastUpdated: new Date(),
+                               name: "root",
+                               parentId: null
+                       }
+               })).not.toThrow();
+       });
+});
diff --git 
a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts
new file mode 100644
index 0000000000..7b59f51ae0
--- /dev/null
+++ 
b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts
@@ -0,0 +1,168 @@
+/*
+* 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 OnDestroy, type OnInit } from "@angular/core";
+import type { ValueGetterParams } from "ag-grid-community";
+import { BehaviorSubject, type Subscription } from "rxjs";
+
+import { UserService } from "src/app/api";
+import type { Tenant } from "src/app/models";
+import { CurrentUserService } from 
"src/app/shared/currentUser/current-user.service";
+import type { ContextMenuActionEvent, ContextMenuItem } from 
"src/app/shared/generic-table/generic-table.component";
+import { TpHeaderService } from "src/app/shared/tp-header/tp-header.service";
+
+/**
+ * TenantsComponent is the controller for the table that lists Tenants.
+ */
+@Component({
+       selector: "tp-tenants",
+       styleUrls: ["./tenants.component.scss"],
+       templateUrl: "./tenants.component.html"
+})
+export class TenantsComponent implements OnInit, OnDestroy {
+
+       private tenantMap: Record<number, Tenant> = {};
+
+       public searchText = "";
+       public searchSubject = new BehaviorSubject("");
+
+       public tenants: Array<Tenant> = [{
+               active: true,
+               id: 1,
+               lastUpdated: new Date(),
+               name: "root",
+               parentId: null
+       }];
+
+       /** Definitions of the table's columns according to the ag-grid API */
+       public columnDefs = [
+               {
+                       field: "active",
+                       filter: "tpBooleanFilter",
+                       headerName: "Active",
+                       hide: false
+               },
+               {
+                       field: "id",
+                       filter: "agNumberColumnFilter",
+                       headerName: "ID",
+                       hide: true,
+               },
+               {
+                       field: "lastUpdated",
+                       filter: "agDateColumnFilter",
+                       headerName: "Last Updated",
+                       hide: true,
+               },
+               {
+                       field: "name",
+                       headerName: "Name",
+                       hide: false,
+               },
+               {
+                       field: "parentId",
+                       headerName: "Parent",
+                       hide: false,
+                       valueGetter: (params: ValueGetterParams): string => 
this.getParentString(params.data)
+               }
+       ];
+
+       public readonly contextMenuItems: ContextMenuItem<Readonly<Tenant>>[] = 
[
+               {
+                       action: "viewDetails",
+                       name: "View Details"
+               },
+               {
+                       action: "openInNewTab",
+                       name: "Open in New Tab"
+               }
+       ];
+
+       public loading = true;
+       private subscription!: Subscription;
+
+       constructor(
+               private readonly userService: UserService,
+               private readonly auth: CurrentUserService,
+               private readonly headerSvc: TpHeaderService
+       ) {
+               this.headerSvc.headerTitle.next("Tenant");
+       }
+
+       /**
+        * Angular lifecycle hook; fetches API data.
+        */
+       public async ngOnInit(): Promise<void> {
+               this.tenants = await this.userService.getTenants();
+               this.tenantMap = Object.fromEntries((this.tenants).map(t => 
[t.id, t]));
+               this.subscription = this.auth.userChanged.subscribe(
+                       () => {
+                               if (this.auth.hasPermission("USER:READ")) {
+                                       this.contextMenuItems.push({
+                                               action: "viewUsers",
+                                               multiRow: true,
+                                               name: "View Users"
+                                       });
+                               }
+                               if (this.auth.hasPermission("TENANT:UPDATE")) {
+                                       this.contextMenuItems.push({
+                                               action: "disable",
+                                               disabled: (ts): boolean => 
ts.some(t=>t.name === "root" || t.id === this.auth.currentUser?.tenantId),
+                                               multiRow: true,
+                                               name: "Disable"
+                                       });
+                               }
+                       }
+               );
+               this.loading = false;
+       }
+
+       /**
+        * Gets a string representation for the Parent of the given Tenant.
+        *
+        * @param t The Tenant for which the Parent will be rendered.
+        * @returns An empty string for the root Tenant, otherwise the parent
+        * Tenant's name and ID as a string.
+        */
+       public getParentString(t: Tenant): string {
+               if (t.parentId === null) {
+                       return "";
+               }
+               return `${this.tenantMap[t.parentId].name} (#${t.parentId})`;
+       }
+
+       /**
+        * Angular lifecycle hook; cleans up persistent resources.
+        */
+       public ngOnDestroy(): void {
+               this.subscription.unsubscribe();
+       }
+
+       /**
+        * Handles a context menu event.
+        *
+        * @param a The action selected from the context menu.
+        */
+        public handleContextMenu(a: ContextMenuActionEvent<Readonly<Tenant>>): 
void {
+               console.log("action:", a);
+       }
+
+       /**
+        * Updates the "search" query parameter in the URL every time the search
+        * text input changes.
+        */
+       public updateURL(): void {
+               this.searchSubject.next(this.searchText);
+       }
+}
diff --git 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
index 99ba19557f..aec0e73349 100644
--- 
a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
+++ 
b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts
@@ -161,7 +161,7 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
        /** Optionally a context to load from localstorage. Providing a unique 
value for this allows for persistent filter, sort, etc. */
        @Input() public context: string | undefined;
        /** Optionally a set of context menu items. If not given, the context 
menu is disabled. */
-       @Input() public contextMenuItems: Array<ContextMenuItem<T>> = [];
+       @Input() public contextMenuItems: readonly 
ContextMenuItem<Readonly<T>>[] = [];
        /** Emits when context menu actions are clicked. Type safety is the 
host's responsibility! */
        @Output() public contextMenuAction = new 
EventEmitter<ContextMenuActionEvent<T>>();
 
@@ -546,7 +546,7 @@ export class GenericTableComponent<T> implements OnInit, 
OnDestroy {
                }
                if (a.disabled) {
                        if (a.multiRow) {
-                               return a.disabled(this.fullSelection, 
this.gridAPI);
+                               return a.disabled(this.selectionCount > 1 ? 
this.fullSelection : [this.selected], this.gridAPI);
                        }
                        return a.disabled(this.selected, this.gridAPI);
                }
diff --git a/experimental/traffic-portal/src/app/shared/shared.module.ts 
b/experimental/traffic-portal/src/app/shared/shared.module.ts
index 55b9d14c69..81bf704fbf 100644
--- a/experimental/traffic-portal/src/app/shared/shared.module.ts
+++ b/experimental/traffic-portal/src/app/shared/shared.module.ts
@@ -14,6 +14,7 @@
 import { CommonModule } from "@angular/common";
 import { HTTP_INTERCEPTORS } from "@angular/common/http";
 import { NgModule } from "@angular/core";
+import { MatMenuModule } from "@angular/material/menu";
 import { RouterModule } from "@angular/router";
 
 import { AppUIModule } from "src/app/app.ui.module";
@@ -64,7 +65,8 @@ import { CustomvalidityDirective } from 
"./validation/customvalidity.directive";
        imports: [
                AppUIModule,
                CommonModule,
-               RouterModule
+               RouterModule,
+               MatMenuModule
        ],
        providers: [
                { multi: true, provide: HTTP_INTERCEPTORS, useClass: 
ErrorInterceptor },
diff --git 
a/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.html 
b/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.html
index a769c79b4c..34328717ee 100644
--- 
a/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.html
+++ 
b/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.html
@@ -13,14 +13,16 @@ limitations under the License.
 -->
 <mat-toolbar color="primary" *ngIf="!hidden">
        <a routerLink="/core/">
-               <img src="/assets/logo.svg" alt="ATC logo/home redirect"/>
+               <img src="/assets/logo.svg" alt="ATC logo"/>
        </a>
        <h1>{{title ? title : 'Welcome to Traffic Portal!'}}</h1>
        <div></div>
        <nav id="expanded">
                <ul>
                        <li><a mat-button routerLink="/core/">Home</a></li>
-                       <li *ngIf="hasPermission('USER:READ')"><a mat-button 
routerLink="/core/users">Users</a></li>
+                       <li *ngIf="hasPermission('USER:READ') || 
hasPermission('TENANT:READ')">
+                               <button mat-button type="button" 
[matMenuTriggerFor]="usersMenu">Users</button>
+                       </li>
                        <li *ngIf="hasPermission('SERVER:READ')"><a mat-button 
routerLink="/core/servers">Servers</a></li>
                        <li>
                                <button mat-icon-button 
[matMenuTriggerFor]="expandedMenu">
@@ -38,7 +40,7 @@ limitations under the License.
                <button mat-icon-button 
[matMenuTriggerFor]="collapsedMenu"><mat-icon>menu</mat-icon></button>
                <mat-menu #collapsedMenu="matMenu">
                        <a mat-menu-item routerLink="/core/">Home</a>
-                       <a *ngIf="hasPermission('USER:READ')" mat-menu-item 
routerLink="/core/users">Users</a>
+                       <button type="button" 
*ngIf="hasPermission('USER:READ')||hasPermission('TENANT:READ')" mat-menu-item 
[matMenuTriggerFor]="usersMenu">Users</button>
                        <a *ngIf="hasPermission('SERVER:READ')" mat-menu-item 
routerLink="/core/servers">Servers</a>
                        <a mat-menu-item routerLink="/core/me">Profile</a>
                        <button mat-menu-item (click)="logout()">Logout</button>
@@ -48,4 +50,10 @@ limitations under the License.
        <mat-menu #themeMenu="matMenu">
                <button mat-menu-item *ngFor="let theme of themeSvc.themes" 
(click)="themeSvc.loadTheme(theme)">{{theme.name}}</button>
        </mat-menu>
+       <mat-menu #usersMenu="matMenu">
+               <a mat-menu-item routerLink="/core/users" 
*ngIf="hasPermission('USER:READ')">View Users</a>
+               <button disabled mat-button type="button" 
*ngIf="!hasPermission('USER:READ')">View Users</button>
+               <a mat-menu-item routerLink="/core/tenants" 
*ngIf="hasPermission('TENANT:READ')">View Tenants</a>
+               <button disabled mat-button type="button" 
*ngIf="!hasPermission('TENANT:READ')">View Tenants</button>
+       </mat-menu>
 </mat-toolbar>
diff --git 
a/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.scss 
b/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.scss
index b1d4231cbf..1126312847 100644
--- 
a/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.scss
+++ 
b/experimental/traffic-portal/src/app/shared/tp-header/tp-header.component.scss
@@ -76,7 +76,7 @@ nav#expanded > ul {
 
                button {
                        appearance: none;
-                       font: inherit;
+                       font-family: inherit;
                        background: transparent;
                        border: none;
                        color: inherit;
diff --git a/experimental/traffic-portal/src/styles.scss 
b/experimental/traffic-portal/src/styles.scss
index 21ba4217b8..1c3949f1ec 100644
--- a/experimental/traffic-portal/src/styles.scss
+++ b/experimental/traffic-portal/src/styles.scss
@@ -125,3 +125,31 @@ button {
        cursor: pointer;
        text-transform: uppercase;
 }
+
+.table-page-content {
+       width: fit-content;
+       min-width: 50%;
+       margin: 1em auto;
+
+       & > div.search-container {
+               width: 50%;
+               margin: auto;
+               padding-right: 10px;
+               position: sticky;
+               top: 0;
+               z-index: 2;
+
+               input[type="search"] {
+                       width: 100%;
+                       margin: 0 0 15px;
+               }
+
+       }
+
+}
+
+.page-fab, .page-fab.mat-fab {
+       position: fixed;
+       bottom: 16px;
+       right: 16px;
+}

Reply via email to