feat(console): user grant filtering, org domain verification improvements, fix membership detail link (#916)

* user grant filter

* add filter input

* org domain spinner and reload

* user grant filter templates

* single selection filter for grants, same dl btn

* filter margin

* lint style, remove duplicate code

* project count as observable

* deferred reload on delete

* fix user grant formfield

* lint styles

* fix event propagation on pin, change selection

* propagate counter change

* admin warn, localstorage, i18n, sidenav impv

* overlays

* adapt toolbar elevationn, card

* color vars, i18n, admin section

* fix lint

* selection clear on filter

* ts lint

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <[email protected]>

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <[email protected]>

* Update console/src/assets/i18n/en.json

Co-authored-by: Fabi <[email protected]>

Co-authored-by: Fabi <[email protected]>
This commit is contained in:
Max Peintner
2020-11-13 09:59:11 +01:00
committed by GitHub
co-authored by Fabi
parent 966e3850ed
commit 42effd8702
49 changed files with 599 additions and 227 deletions
@@ -42,5 +42,6 @@
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
}
+1
View File
@@ -16,6 +16,7 @@
box-sizing: border-box;
border-radius: .5rem;
outline: none;
height: 100%;
.selection-icon {
opacity: 0;
@@ -5,6 +5,7 @@
margin-bottom: 1rem;
font-weight: 400;
margin-top: 1rem;
font-size: 14px;
}
@mixin changes-theme($theme) {
@@ -8,6 +8,7 @@
display: relative;
width: 100%;
overflow-y: auto;
padding-bottom: 50px;
&.hidden {
flex-basis: 100%;
@@ -7,17 +7,10 @@
$primary-dark: mat-color($primary, A800);
/* stylelint-enable */
$lighter-color: rgba(mat-color($primary, 300), .5);
.meta-wrapper {
.meta {
position: relative;
flex: 1 0 300px;
background: linear-gradient(to bottom right, rgba($lighter-color, .05) 20%, transparent 50%);
&.hidden {
background: linear-gradient(to bottom right, rgba($lighter-color, .05), transparent 50%);
}
&::after {
border-left: 2px solid $primary-color;
@@ -27,7 +20,7 @@
left top,
left bottom,
from($primary-color),
to($primary-dark),
to(rgb(0, 0, 0, .5)),
color-stop(
01,
$primary-dark
@@ -39,7 +32,7 @@
left top,
left bottom,
from($primary-color),
to($primary-dark),
to(rgb(0, 0, 0, .5)),
color-stop(
01,
$primary-dark
@@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}
@@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}
@@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}
@@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}
@@ -16,8 +16,9 @@ h1 {
margin: .5rem;
display: flex;
flex-direction: column;
min-height: 200px;
min-height: 250px;
padding: 1rem;
height: 100%;
@media only screen and (max-width: 450px) {
flex-basis: 100%;
@@ -103,7 +103,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
break;
default:
this.loadingSubject.next(true);
const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, []);
const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, queries ?? []);
this.loadResponse(promise3);
break;
}
@@ -1,6 +1,12 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection">
<mat-form-field @appearfade *ngIf="userGrantSearchKey != undefined" actions class="filtername">
<mat-label>{{'USER.PAGES.FILTER' | translate}}</mat-label>
<input matInput (keyup)="applyFilter($event)"
[placeholder]="('USER.TABLE.FILTER.' + userGrantSearchKey.toString()) | translate" #input>
</mat-form-field>
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i>
@@ -33,19 +39,28 @@
</ng-container>
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_DISPLAY_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant?.displayName}}</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_ORG_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
</ng-container>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
</ng-container>
@@ -63,29 +78,32 @@
</ng-container>
<ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }} </th>
<th mat-header-cell *matHeaderCellDef>
{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantSearchKey.USERGRANTSEARCHKEY_ROLE_KEY}"></template>
</th>
<td mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE">
<ng-container
*ngIf="(grant.grantId && loadedGrantId !== grant.grantId) || (loadedProjectId !== grant.projectId)">
<div class="flex-row">
<span class="role" *ngFor="let role of grant.roleKeysList">{{ role }}</span>
<button mat-stroked-button
*ngIf="grant.grantId ? loadedGrantId !== grant.grantId : loadedProjectId !== grant.projectId"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(click)="grant.grantId ? getGrantRoleOptions(grant.grantId, grant.projectId) : getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
<ng-container
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
<div class="flex-row">
<div class="role">
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
</div>
</ng-container>
<span class="fill-space"></span>
<button mat-stroked-button
*ngIf="grant.grantId ? grantToEdit !== grant.id : grantToEdit !== grant.id"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
</div>
</ng-container>
<ng-container
*ngIf="context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE">
<mat-form-field class="form-field" appearance="outline"
*ngIf="loadedProjectId && loadedProjectId === grant.projectId">
*ngIf="(context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && grantToEdit == grant.id && loadedProjectId && loadedProjectId === grant.projectId">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
@@ -95,12 +113,15 @@
</mat-option>
</mat-select>
</mat-form-field>
<button *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"
mat-icon-button (click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
<ng-container
*ngIf="context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE">
<mat-form-field class="form-field" appearance="outline"
*ngIf="loadedGrantId && loadedGrantId === grant.grantId">
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && loadedGrantId && loadedGrantId === grant.grantId && grantToEdit == grant.id">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
@@ -110,6 +131,10 @@
</mat-option>
</mat-select>
</mat-form-field>
<button *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"
mat-icon-button (click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
</td>
</ng-container>
@@ -123,4 +148,11 @@
[length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</div>
</app-refresh-table>
</app-refresh-table>
<ng-template #templateRef let-key="key">
<button class="search-button" mat-icon-button (click)="setFilter(key)">
<mat-icon *ngIf="this.userGrantSearchKey != key">search</mat-icon>
<mat-icon *ngIf="this.userGrantSearchKey == key">search_off</mat-icon>
</button>
</ng-template>
@@ -21,6 +21,19 @@
}
}
th {
.search-button {
display: none;
}
&:hover,
&.search-active {
.search-button {
display: inline-block;
}
}
}
.selection {
width: 50px;
max-width: 50px;
@@ -35,15 +48,22 @@
.flex-row {
display: flex;
flex-direction: column;
flex-direction: row;
max-width: 400px;
.role {
display: block;
display: flex;
flex-direction: column;
margin: .25rem;
font-size: 14px;
text-overflow: ellipsis;
overflow: hidden;
justify-items: center;
align-self: center;
}
.fill-space {
flex: 1;
}
button {
@@ -60,3 +80,7 @@
.fill-space {
flex: 1;
}
.filtername {
margin-right: 1rem;
}
@@ -1,10 +1,19 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatInput } from '@angular/material/input';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators';
import { ProjectRoleView, UserGrant, UserGrantView } from 'src/app/proto/generated/management_pb';
import { enterAnimations } from 'src/app/animations';
import {
ProjectRoleView,
SearchMethod,
UserGrant,
UserGrantSearchKey,
UserGrantSearchQuery,
UserGrantView,
} from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -14,12 +23,17 @@ import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource
selector: 'app-user-grants',
templateUrl: './user-grants.component.html',
styleUrls: ['./user-grants.component.scss'],
animations: [
enterAnimations,
],
})
export class UserGrantsComponent implements OnInit, AfterViewInit {
public userGrantSearchKey: UserGrantSearchKey | undefined = undefined;
public UserGrantSearchKey: any = UserGrantSearchKey;
public INITIAL_PAGE_SIZE: number = 50;
@Input() context: UserGrantContext = UserGrantContext.NONE;
@Input() refreshOnPreviousRoutes: string[] = [];
public grants: UserGrantView.AsObject[] = [];
public dataSource!: UserGrantsDataSource;
public selection: SelectionModel<UserGrantView.AsObject> = new SelectionModel<UserGrantView.AsObject>(true, []);
@@ -32,6 +46,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
@Input() userId: string = '';
@Input() projectId: string = '';
@Input() grantId: string = '';
@ViewChild('input') public filter!: MatInput;
public grantRoleOptions: string[] = [];
public projectRoleOptions: ProjectRoleView.AsObject[] = [];
@@ -39,6 +54,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public loadedGrantId: string = '';
public loadedProjectId: string = '';
public grantToEdit: string = '';
public UserGrantContext: any = UserGrantContext;
@@ -89,7 +105,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
.subscribe();
}
private loadGrantsPage(): void {
private loadGrantsPage(filterValue?: string): void {
let queries: UserGrantSearchQuery[] = [];
if (this.userGrantSearchKey !== undefined && filterValue) {
const query = new UserGrantSearchQuery();
query.setKey(this.userGrantSearchKey);
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS_IGNORE_CASE);
query.setValue(filterValue);
queries = [query];
}
this.dataSource.loadGrants(
this.context,
this.paginator?.pageIndex ?? 0,
@@ -99,6 +124,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
grantId: this.grantId,
userId: this.userId,
},
queries,
);
}
@@ -114,7 +140,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row));
}
public getGrantRoleOptions(grantId: string, projectId: string): void {
public loadGrantOptions(grant: UserGrantView.AsObject): void {
this.grantToEdit = grant.id;
if (grant.grantId && grant.projectId) {
this.getGrantRoleOptions(grant.grantId, grant.projectId);
} else if (grant.projectId) {
this.getProjectRoleOptions(grant.projectId);
}
}
private getGrantRoleOptions(grantId: string, projectId: string): void {
this.mgmtService.GetGrantedProjectByID(projectId, grantId).then(resp => {
this.loadedGrantId = grantId;
this.grantRoleOptions = resp.toObject().roleKeysList;
@@ -123,7 +158,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
});
}
public getProjectRoleOptions(projectId: string): void {
private getProjectRoleOptions(projectId: string): void {
this.mgmtService.SearchProjectRoles(projectId, 100, 0).then(resp => {
this.loadedProjectId = projectId;
this.projectRoleOptions = resp.toObject().resultList;
@@ -168,4 +203,26 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
},
);
}
public applyFilter(event: Event): void {
this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value;
this.loadGrantsPage(filterValue);
}
public setFilter(key: UserGrantSearchKey): void {
setTimeout(() => {
if (this.filter) {
(this.filter as any).nativeElement.focus();
}
}, 100);
if (this.userGrantSearchKey !== key) {
this.userGrantSearchKey = key;
} else {
this.userGrantSearchKey = undefined;
this.loadGrantsPage();
}
}
}
@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
@@ -39,6 +40,7 @@ import { UserGrantsComponent } from './user-grants.component';
MatCheckboxModule,
MatTooltipModule,
MatSelectModule,
MatInputModule,
MatFormFieldModule,
TranslateModule,
HasRolePipeModule,