mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-02 12:22:15 +00:00
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:
@@ -42,5 +42,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
-1
@@ -1,5 +1,5 @@
|
||||
.default {
|
||||
color: #5282c1;
|
||||
color: var(--color-main);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user