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