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 <38692350+fgerschwiler@users.noreply.github.com>

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Max Peintner 2020-11-13 09:59:11 +01:00 committed by GitHub
parent 966e3850ed
commit 42effd8702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 599 additions and 227 deletions

View File

@ -29,10 +29,12 @@
placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input>
</mat-form-field>
<div class="org-wrapper">
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs$ | async" mat-menu-item (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
</div>
<button class="show-all" mat-menu-item
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
@ -70,8 +72,10 @@
</ng-container>
<ng-container *ngIf="iamuser$ | async">
<div class="divider">
<div @navitem class="divider">
<div class="line"></div>
<span>{{'MENU.ADMINSECTION' | translate}}</span>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/iam']">
<i class="icon las la-gem"></i>
@ -92,7 +96,7 @@
<div @navitem class="divider">
<div class="line"></div>
<span>{{'MENU.PROJECTSSECTION' | translate}}</span>
<div class="line"></div>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']"
@ -102,17 +106,19 @@
<div class="c_label">
<span>{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}
{{'MENU.PROJECT' | translate}} </span>
<span *ngIf="ownedProjectsCount as ownedPCount"
class="count">{{ownedPCount}}</span>
<span *ngIf="(mgmtService?.ownedProjectsCount | async)"
class="count">{{mgmtService?.ownedProjectsCount | async}}</span>
</div>
</a>
<a @navitem *ngIf="grantedProjectsCount as grantPCount" class="nav-item"
[routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']">
<a @navitem
*ngIf="mgmtService?.grantedProjectsCount && (mgmtService?.grantedProjectsCount | async)"
class="nav-item" [routerLinkActive]="['active']"
[routerLink]="[ '/granted-projects']">
<i class="icon las la-layer-group"></i>
<div class="c_label">
<span>{{ 'MENU.GRANTEDPROJECT' | translate }}</span>
<span class="count">{{grantPCount}}</span>
<span class="count">{{mgmtService?.grantedProjectsCount | async}}</span>
</div>
</a>
</ng-template>
@ -122,7 +128,7 @@
<div class="line"></div>
<span class="label">
{{ 'MENU.USERSECTION' | translate }}</span>
<div class="line"></div>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']"
@ -144,7 +150,7 @@
<div class="line"></div>
<span class="label">
{{ 'MENU.GRANTSECTION' | translate }}</span>
<div class="line"></div>
<div class="hiddenline"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/grants']"
@ -161,13 +167,18 @@
</div>
</mat-drawer>
<mat-drawer-content class="content">
<div @toolbar *ngIf="iamuser$ | async" class="admin-line" matTooltip="IAM Administrator">
<span>{{'MENU.IAMADMIN' | translate}}</span>
</div>
<div class="router" [@routeAnimations]="prepareRoute(outlet)">
<router-outlet #outlet="outlet"></router-outlet>
</div>
</mat-drawer-content>
</mat-drawer-container>
<div @toolbar *ngIf="iamuser$ | async" class="admin-line" [ngClass]="{'expanded': !hideAdminWarn}"
matTooltip="IAM Administrator">
<button [matTooltip]="!hideAdminWarn ? 'Unpin': 'Pin'" (click)="toggleAdminHide()" mat-icon-button>
<mat-icon *ngIf="!hideAdminWarn" svgIcon="mdi_pin"></mat-icon>
<mat-icon *ngIf="hideAdminWarn" svgIcon="mdi_pin_outline"></mat-icon>
</button>
<span>{{'MENU.IAMADMIN' | translate}}</span>
</div>
</ng-container>
</ng-container>

View File

@ -66,13 +66,6 @@
}
}
.admin-line {
font-size: 12px;
padding: 4px 2rem;
position: relative;
overflow: hidden;
}
.main-container {
display: flex;
flex-direction: column;
@ -222,6 +215,13 @@
margin: .5rem 0;
flex: 1;
}
.hiddenline {
display: block;
visibility: hidden;
// flex: 1;
width: 4rem;
}
}
@mixin textvar($theme) {
@ -255,3 +255,8 @@
align-items: center;
}
}
.org-wrapper {
max-height: 350px;
overflow-y: auto;
}

View File

@ -23,7 +23,6 @@ import { AuthenticationService } from './services/authentication.service';
import { GrpcAuthService } from './services/grpc-auth.service';
import { ManagementService } from './services/mgmt.service';
import { ThemeService } from './services/theme.service';
import { ToastService } from './services/toast.service';
import { UpdateService } from './services/update.service';
@Component({
@ -57,13 +56,11 @@ export class AppComponent implements OnDestroy {
public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription();
public hideAdminWarn: boolean = true;
constructor(
public viewPortScroller: ViewportScroller,
@Inject('windowObject') public window: Window,
@ -73,15 +70,14 @@ export class AppComponent implements OnDestroy {
private breakpointObserver: BreakpointObserver,
public overlayContainer: OverlayContainer,
private themeService: ThemeService,
private mgmtService: ManagementService,
public mgmtService: ManagementService,
public matIconRegistry: MatIconRegistry,
public domSanitizer: DomSanitizer,
private toast: ToastService,
private router: Router,
update: UpdateService,
@Inject(DOCUMENT) private document: Document,
) {
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5282c1; font-size: 50px');
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5469D4; font-size: 50px');
console.log('%cInserting something here could give attackers access to your zitadel account.', 'color: red; font-size: 18px');
console.log('%cIf you don\'t know exactly what you\'re doing, close the window and stay on the safe side', 'font-size: 16px');
console.log('%cIf you know exactly what you are doing, you should work for us', 'font-size: 16px');
@ -179,6 +175,8 @@ export class AppComponent implements OnDestroy {
value.trim().toLowerCase(),
);
});
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
}
public ngOnDestroy(): void {
@ -186,6 +184,11 @@ export class AppComponent implements OnDestroy {
this.orgSub.unsubscribe();
}
public toggleAdminHide(): void {
this.hideAdminWarn = !this.hideAdminWarn;
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
}
public loadOrgs(filter?: string): void {
let query;
if (filter) {
@ -249,13 +252,9 @@ export class AppComponent implements OnDestroy {
private getProjectCount(): void {
this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
if (allowed) {
this.mgmtService.SearchProjects(0, 0).then(res => {
this.ownedProjectsCount = res.toObject().totalResult;
});
this.mgmtService.SearchProjects(0, 0);
this.mgmtService.SearchGrantedProjects(0, 0).then(res => {
this.grantedProjectsCount = res.toObject().totalResult;
});
this.mgmtService.SearchGrantedProjects(0, 0);
}
});
}

View File

@ -42,5 +42,6 @@
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
}

View File

@ -16,6 +16,7 @@
box-sizing: border-box;
border-radius: .5rem;
outline: none;
height: 100%;
.selection-icon {
opacity: 0;

View File

@ -5,6 +5,7 @@
margin-bottom: 1rem;
font-weight: 400;
margin-top: 1rem;
font-size: 14px;
}
@mixin changes-theme($theme) {

View File

@ -8,6 +8,7 @@
display: relative;
width: 100%;
overflow-y: auto;
padding-bottom: 50px;
&.hidden {
flex-basis: 100%;

View File

@ -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

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -1,5 +1,5 @@
.default {
color: #5282c1;
color: var(--color-main);
margin-top: 0;
}

View File

@ -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%;

View File

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

View File

@ -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)">
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
<div class="flex-row">
<span class="role" *ngFor="let role of grant.roleKeysList">{{ role }}</span>
<div class="role">
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
</div>
<span class="fill-space"></span>
<button mat-stroked-button
*ngIf="grant.grantId ? loadedGrantId !== grant.grantId : loadedProjectId !== grant.projectId"
*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)="grant.grantId ? getGrantRoleOptions(grant.grantId, grant.projectId) : getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
</div>
</ng-container>
</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>
@ -124,3 +149,10 @@
</mat-paginator>
</div>
</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>

View File

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

View File

@ -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();
}
}
}

View File

@ -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,

View File

@ -24,6 +24,23 @@
{{'HOME.IAM'| translate}}</h2>
<p>{{'HOME.IAM_DESC'| translate}}</p>
</div>
<ng-template appHasRole [appHasRole]="['org.create','iam.write']">
<a class="short-link" [routerLink]="[ '/org', 'create']">{{'HOME.IAM_CREATE_ORG' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<a class="short-link"
[routerLink]="[ '/iam', 'policy','iam']">{{'HOME.IAM_POLICY_IAM' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/iam', 'policy','complexity']">{{'HOME.IAM_POLICY_COMPLEXITY' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/iam', 'policy','login']">{{'HOME.IAM_POLICY_LOGIN' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<span class="fill-space"></span>
<div class="footer">
<a color="primary" mat-stroked-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a>
@ -37,6 +54,9 @@
{{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
<p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p>
</div>
<a class="short-link" [routerLink]="[ '/users', 'me','password']">{{'HOME.CHANGE_PWD' | translate}}<i
class="las la-link"></i></a>
<span class="fill-space"></span>
<div class="footer">
<a color="primary" mat-stroked-button
@ -51,6 +71,11 @@
<i class="icon las la-layer-group"></i>
{{'HOME.PROJECTS'| translate}}</h2>
<p>{{'HOME.PROJECTS_DESC'| translate}}</p>
<ng-template appHasRole [appHasRole]="['project.create']">
<a class="short-link"
[routerLink]="[ '/projects', 'create']">{{'HOME.PROJECTS_NEW_LINK' | translate}}<i
class="las la-link"></i></a>
</ng-template>
</div>
<span class="fill-space"></span>
<div class="footer">
@ -66,6 +91,19 @@
<h2> <i class="icon las la-archway"></i>
{{'HOME.PROTECTION'| translate}}</h2>
<p>{{'HOME.PROTECTION_DESC'| translate}}</p>
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<a class="short-link"
[routerLink]="[ '/org', 'policy','iam']">{{'HOME.ORG_POLICY_IAM' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<ng-template appHasRole [appHasRole]="['policy.read']">
<a class="short-link"
[routerLink]="[ '/org', 'policy','complexity']">{{'HOME.ORG_POLICY_COMPLEXITY' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/org', 'policy','login']">{{'HOME.ORG_POLICY_LOGIN' | translate}}<i
class="las la-link"></i></a>
</ng-template>
</div>
<span class="fill-space"></span>
<div class="footer">
@ -82,6 +120,18 @@
<i class="las la-users"></i>
{{'HOME.USERS'| translate}}</h2>
<p>{{'HOME.USERS_DESC'| translate}}</p>
<ng-template appHasRole [appHasRole]="['user.read(:[0-9]*)?']">
<a class="short-link"
[routerLink]="[ '/users', 'list', 'humans']">{{'HOME.USERS_HUMANS' | translate}}<i
class="las la-link"></i></a>
<a class="short-link"
[routerLink]="[ '/users', 'list', 'machines']">{{'HOME.USERS_MACHINES' | translate}}<i
class="las la-link"></i></a>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.read']">
<a class="short-link" [routerLink]="[ '/users', 'create']">{{'HOME.USERS_CREATE' | translate}}<i
class="las la-link"></i></a>
</ng-template>
</div>
<span class="fill-space"></span>
<div class="footer">

View File

@ -32,7 +32,7 @@
justify-content: space-evenly;
.item {
flex: 1 1 45%;
flex: 1 0 45%;
margin: 0 1rem;
display: flex;
flex-direction: column;
@ -80,3 +80,25 @@
margin-top: 3rem;
}
}
.short-link {
margin-bottom: .5rem;
font-size: 15px;
text-decoration: none;
display: block;
position: relative;
.las {
font-size: 1.5rem !important;
height: 1.5rem;
visibility: hidden;
position: absolute;
right: 0;
}
&:hover {
.las {
visibility: visible;
}
}
}

View File

@ -12,8 +12,10 @@
{{'ORG.PAGES.ORGDOMAIN.TYPES.'+ domain.validationType | translate}}</p>
<div *ngIf="domain.validationType !== OrgDomainValidationType.ORGDOMAINVALIDATIONTYPE_UNSPECIFIED"
class="btn-container">
<button color="primary" type="submit" mat-raised-button *ngIf="!(dns || http)"
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
<button color="primary" type="submit" mat-raised-button *ngIf="!(dns || http)" (click)="validate()">
{{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
<button *ngIf="!showNew" mat-stroked-button color="primary"
(click)="showNew = true">{{'ORG.PAGES.ORGDOMAIN.REQUESTNEWTOKEN' | translate}}</button>
@ -34,8 +36,11 @@
<div class="btn-container">
<button mat-stroked-button (click)="saveFile()"
color="primary">{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
<button color="primary" type="submit" mat-raised-button
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
<button color="primary" class="verify-button" type="submit" mat-raised-button (click)="validate()">
<span>{{ 'ACTIONS.VERIFY' | translate }}</span>
<mat-spinner class="spinner" *ngIf="!validating" diameter="20" mode="indeterminate"></mat-spinner>
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div>
</div>
@ -48,8 +53,10 @@
<i *ngIf="copied != dns.token" class="las la-clipboard"></i>
<i *ngIf="copied == dns.token" class="las la-clipboard-check"></i>
</button>
<button color="primary" type="submit" mat-raised-button
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
<button color="primary" type="submit" mat-raised-button class="verify-button" (click)="validate()">
{{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div>
<p class="entry">{{dns?.url}}</p>
</div>

View File

@ -1,10 +1,10 @@
.btn-container {
display: flex;
margin: -.5rem;
align-items: center;
button {
margin: 1rem .5rem;
display: block;
}
}

View File

@ -21,6 +21,7 @@ export class DomainVerificationComponent {
public showNew: boolean = false;
public validating: boolean = false;
constructor(
private toast: ToastService,
public dialogRef: MatDialogRef<DomainVerificationComponent>,
@ -54,10 +55,14 @@ export class DomainVerificationComponent {
}
public validate(): void {
this.validating = true;
this.mgmtService.ValidateMyOrgDomain(this.domain.domain).then(() => {
this.dialogRef.close(false);
this.dialogRef.close(true);
this.toast.showInfo('ORG.PAGES.ORGDOMAIN.VERIFICATION_SUCCESSFUL', true);
this.validating = false;
}).catch((error) => {
this.toast.showError(error);
this.validating = false;
});
}

View File

@ -36,7 +36,7 @@
.verified,
.primary {
color: #5282c1;
color: var(--color-main);
margin-right: 1rem;
}

View File

@ -79,10 +79,12 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
}).catch(error => {
this.toast.showError(error);
});
this.loadMembers();
this.loadDomains();
}
this.mgmtService.SearchMyOrgDomains(0, 100).then(result => {
public loadDomains(): void {
this.mgmtService.SearchMyOrgDomains().then(result => {
this.domains = result.toObject().resultList;
this.primaryDomain = this.domains.find(domain => domain.primary)?.domain ?? '';
});
@ -91,7 +93,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
public setPrimary(domain: OrgDomainView.AsObject): void {
this.mgmtService.setMyPrimaryOrgDomain(domain.domain).then(() => {
this.toast.showInfo('ORG.TOAST.SETPRIMARY', true);
this.getData();
this.loadDomains();
}).catch((error) => {
this.toast.showError(error);
});
@ -202,12 +204,18 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
}
public verifyDomain(domain: OrgDomainView.AsObject): void {
this.dialog.open(DomainVerificationComponent, {
const dialogRef = this.dialog.open(DomainVerificationComponent, {
data: {
domain: domain,
},
width: '500px',
});
dialogRef.afterClosed().subscribe((reload) => {
if (reload) {
this.loadDomains();
}
});
}
public loadMembers(): void {

View File

@ -8,6 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
@ -51,6 +52,7 @@ import { OrgsRoutingModule } from './orgs-routing.module';
MemberCreateDialogModule,
MatMenuModule,
ChangesModule,
MatProgressSpinnerModule,
AddDomainDialogModule,
TranslateModule,
SharedModule,

View File

@ -22,11 +22,10 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
</div>
@ -47,8 +46,8 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<button [ngClass]="{ selected: selection.isSelected(item)}"
(click)="selection.toggle(item); $event.stopPropagation()" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
@ -56,3 +55,11 @@
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
<ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(key)}"
(click)="selection.toggle(key); $event.stopPropagation()" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon>
</button>
</ng-template>

View File

@ -47,11 +47,10 @@ export class GrantedProjectGridComponent implements OnChanges {
this.setPrefixedItem('pinned-granted-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId),
)).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item));
filtered.forEach((f, i) => {
this.notPinned.splice(i, 1);
selection.added.forEach(item => {
const index = this.notPinned.findIndex(i => i.projectId === item.projectId);
this.notPinned.splice(index, 1);
});
this.notPinned.push(...selection.removed);
});
});
@ -74,18 +73,10 @@ export class GrantedProjectGridComponent implements OnChanges {
const array: string[] = JSON.parse(storageEntry);
const toSelect: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (array.includes(item.projectId)) {
// this.notPinned.splice(index, 1);
return true;
}
});
this.selection.select(...toSelect);
const toNotPinned: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (!array.includes(item.projectId)) {
return true;
}
});
this.notPinned = toNotPinned;
}
});
}

View File

@ -182,7 +182,10 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
if (resp) {
this.mgmtService.RemoveProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
this.router.navigate(['/projects']);
const params: Params = {
'deferredReload': true,
};
this.router.navigate(['/projects'], { queryParams: params });
}).catch(error => {
this.toast.showError(error);
});

View File

@ -25,11 +25,9 @@
}}</span>
<span class="fill-space"></span>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
<template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
</div>
@ -52,15 +50,9 @@
}}</span>
<span class="fill-space"></span>
</div>
<button *ngIf="item.projectId !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn"
(click)="deleteProject(item)" class="delete-button" mat-icon-button>
<i class="las la-trash"></i>
</button>
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(item)}"
(click)="selection.toggle(item)" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
<template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
@ -72,3 +64,18 @@
</div>
</ng-template>
</div>
<ng-template #deleteButton let-key="key">
<button *ngIf="key.projectId !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn"
(click)="deleteProject($event, key)" class="delete-button" mat-icon-button>
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(key)}"
(click)="toggle(key,$event)" class="edit-button" mat-icon-button>
<mat-icon *ngIf="selection.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon>
</button>
</ng-template>

View File

@ -63,11 +63,10 @@ export class OwnedProjectGridComponent implements OnChanges {
this.setPrefixedItem('pinned-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId),
)).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item));
filtered.forEach((f, i) => {
this.notPinned.splice(i, 1);
selection.added.forEach(item => {
const index = this.notPinned.findIndex(i => i.projectId === item.projectId);
this.notPinned.splice(index, 1);
});
this.notPinned.push(...selection.removed);
});
});
@ -102,13 +101,6 @@ export class OwnedProjectGridComponent implements OnChanges {
}
});
this.selection.select(...toSelect);
const toNotPinned: ProjectView.AsObject[] = this.items.filter((item, index) => {
if (!array.includes(item.projectId)) {
return true;
}
});
this.notPinned = toNotPinned;
}
});
}
@ -133,7 +125,13 @@ export class OwnedProjectGridComponent implements OnChanges {
this.changedView.emit(true);
}
public deleteProject(item: ProjectView.AsObject): void {
public toggle(item: ProjectView.AsObject, event: any): void {
event.stopPropagation();
this.selection.toggle(item);
}
public deleteProject(event: any, item: ProjectView.AsObject): void {
event.stopPropagation();
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
@ -152,6 +150,16 @@ export class OwnedProjectGridComponent implements OnChanges {
if (index > -1) {
this.items.splice(index, 1);
}
const indexSelection = this.selection.selected.findIndex(iter => iter.projectId === item.projectId);
if (indexSelection > -1) {
this.selection.selected.splice(indexSelection, 1);
}
const indexPinned = this.notPinned.findIndex(iter => iter.projectId === item.projectId);
if (indexPinned > -1) {
this.notPinned.splice(indexPinned, 1);
}
}).catch(error => {
this.toast.showError(error);
});

View File

@ -4,10 +4,11 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { ProjectView } from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
@ -59,6 +60,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
public zitadelProjectId: string = '';
constructor(private router: Router,
private route: ActivatedRoute,
public translate: TranslateService,
private mgmtService: ManagementService,
private toast: ToastService,
@ -70,7 +72,15 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
}
public ngOnInit(): void {
this.getData(10, 0);
this.route.queryParams.pipe(take(1)).subscribe(params => {
console.log(params);
this.getData();
if (params.deferredReload) {
setTimeout(() => {
this.getData();
}, 2000);
}
});
}
public ngOnDestroy(): void {
@ -97,7 +107,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
this.router.navigate(['/projects', 'create']);
}
private async getData(limit: number, offset: number): Promise<void> {
private async getData(limit?: number, offset?: number): Promise<void> {
this.loadingSubject.next(true);
this.mgmtService.SearchProjects(limit, offset).then(res => {
const response = res.toObject();

View File

@ -68,7 +68,7 @@ export class UserCreateComponent implements OnDestroy {
}
private async loadOrg(): Promise<void> {
const domains = (await this.mgmtService.SearchMyOrgDomains(0, 100).then(doms => doms.toObject()));
const domains = (await this.mgmtService.SearchMyOrgDomains().then(doms => doms.toObject()));
const found = domains.resultList.find(domain => domain.primary);
if (found) {
this.primaryDomain = found;

View File

@ -19,7 +19,7 @@
i {
margin-left: 1rem;
color: #5282c1;
color: var(--color-main);
}
}

View File

@ -23,7 +23,7 @@
</ng-container>
</ng-container>
<ng-template #compact>
<div class="avatar-circle" matTooltip="Click to show detail">
<div class="avatar-circle" matTooltip="Click to show detail" (click)="navigateToObject()" role="button">
<div class="membership-avatar">
<span style="font-size: 16px;">{{memberships.totalResult}}</span>
</div>

View File

@ -6,13 +6,13 @@
</a>
<h1>{{user.human ? user.human?.displayName : user.machine?.name}}</h1>
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['user.delete$', 'user.delete:'+user?.id]">
<button mat-raised-button color="warn" (click)="deleteUser()"><i
class="las la-trash"></i>{{'USER.PAGES.DELETE' | translate}}</button>
<button mat-icon-button color="warn" matTooltip="{{'USER.PAGES.DELETE' | translate}}"
(click)="deleteUser()"><i class="las la-trash"></i></button>
</ng-template>
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['user.write$', 'user.write:'+user?.id]">
<button class="state-button" mat-stroked-button color="warn"
*ngIf="user?.state === UserState.USERSTATE_ACTIVE"

View File

@ -12,6 +12,7 @@
h1 {
margin: 0;
margin-right: 1rem;
}
.fill-space {

View File

@ -23,7 +23,7 @@
i {
margin-left: 1rem;
color: #5282c1;
color: var(--color-main);
}
}

View File

@ -125,9 +125,9 @@
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let user">
<button [disabled]="(['user.delete$', 'user.delete:'+user.id] | hasRole | async) == false"
color="warn" mat-icon-button matTooltip="{{'USER.PAGES.DELETE' | translate}}"
(click)="deleteUser(user)">
<button class="dlt-button"
[disabled]="(['user.delete$', 'user.delete:'+user.id] | hasRole | async) == false" color="warn"
mat-icon-button matTooltip="{{'USER.PAGES.DELETE' | translate}}" (click)="deleteUser(user)">
<i class="las la-trash"></i>
</button>
</td>

View File

@ -18,29 +18,9 @@
&:last-child {
padding-right: 0;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
tr {
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
th {
.search-button {
.search-button,
.dlt-button {
visibility: hidden;
}
@ -50,6 +30,21 @@ th {
visibility: visible;
}
}
}
tr {
&:hover {
.dlt-button {
visibility: visible;
}
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
.filtername {

View File

@ -121,6 +121,7 @@ export class UserTableComponent implements OnInit {
}
public applyFilter(event: Event): void {
this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value;
this.getData(

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject } from 'rxjs';
import {
AddMachineKeyRequest,
@ -160,6 +161,9 @@ export type ResponseMapper<TResp, TMappedResp> = (resp: TResp) => TMappedResp;
providedIn: 'root',
})
export class ManagementService {
public ownedProjectsCount: BehaviorSubject<number> = new BehaviorSubject(0);
public grantedProjectsCount: BehaviorSubject<number> = new BehaviorSubject(0);
constructor(private readonly grpcService: GrpcService) { }
public SearchIdps(
@ -393,11 +397,9 @@ export class ManagementService {
return this.grpcService.mgmt.removeMyOrgDomain(req);
}
public SearchMyOrgDomains(offset: number, limit: number, queryList?: OrgDomainSearchQuery[]):
public SearchMyOrgDomains(queryList?: OrgDomainSearchQuery[]):
Promise<OrgDomainSearchResponse> {
const req: OrgDomainSearchRequest = new OrgDomainSearchRequest();
req.setLimit(limit);
req.setOffset(offset);
if (queryList) {
req.setQueriesList(queryList);
}
@ -834,13 +836,17 @@ export class ManagementService {
// USER GRANTS
public SearchUserGrants(
limit: number,
offset: number,
limit?: number,
offset?: number,
queryList?: UserGrantSearchQuery[],
): Promise<UserGrantSearchResponse> {
const req = new UserGrantSearchRequest();
if (limit) {
req.setLimit(limit);
}
if (offset) {
req.setOffset(offset);
}
if (queryList) {
req.setQueriesList(queryList);
}
@ -929,14 +935,26 @@ export class ManagementService {
// project
public SearchProjects(
limit: number, offset: number, queryList?: ProjectSearchQuery[]): Promise<ProjectSearchResponse> {
limit?: number, offset?: number, queryList?: ProjectSearchQuery[]): Promise<ProjectSearchResponse> {
const req = new ProjectSearchRequest();
if (limit) {
req.setLimit(limit);
}
if (offset) {
req.setOffset(offset);
}
if (queryList) {
req.setQueriesList(queryList);
}
return this.grpcService.mgmt.searchProjects(req);
return this.grpcService.mgmt.searchProjects(req).then(value => {
const count = value.toObject().resultList.length;
if (count >= 0) {
this.ownedProjectsCount.next(count);
}
return value;
});
}
public SearchGrantedProjects(
@ -947,10 +965,12 @@ export class ManagementService {
if (queryList) {
req.setQueriesList(queryList);
}
return this.grpcService.mgmt.searchGrantedProjects(req);
return this.grpcService.mgmt.searchGrantedProjects(req).then(value => {
this.grantedProjectsCount.next(value.toObject().resultList.length);
return value;
});
}
public GetZitadelDocs(): Promise<ZitadelDocs> {
const req = new Empty();
return this.grpcService.mgmt.getZitadelDocs(req);
@ -972,7 +992,11 @@ export class ManagementService {
public CreateProject(project: ProjectCreateRequest.AsObject): Promise<Project> {
const req = new ProjectCreateRequest();
req.setName(project.name);
return this.grpcService.mgmt.createProject(req);
return this.grpcService.mgmt.createProject(req).then(value => {
const current = this.ownedProjectsCount.getValue();
this.ownedProjectsCount.next(current + 1);
return value;
});
}
public UpdateProject(id: string, projectView: ProjectView.AsObject): Promise<Project> {
@ -1221,7 +1245,11 @@ export class ManagementService {
public RemoveProject(id: string): Promise<Empty> {
const req = new ProjectID();
req.setId(id);
return this.grpcService.mgmt.removeProject(req);
return this.grpcService.mgmt.removeProject(req).then(value => {
const current = this.ownedProjectsCount.getValue();
this.ownedProjectsCount.next(current > 0 ? current - 1 : 0);
return value;
});
}

View File

@ -5,14 +5,26 @@
"SECURITYANDPRIVACY": "Datenschutz und Personalisierung",
"SECURITYANDPRIVACY_DESC": "Verwalte Deine Informationen und Sicherheitseinstellungen.",
"SECURITYANDPRIVACY_BUTTON": "Daten verwalten und personalisieren",
"CHANGE_PWD":"Password ändern",
"PROTECTION": "Organisationsrichtlinien",
"PROTECTION_DESC": "Verwalte Deine Organisationsrichtlinien und entdecke die vorgefertigte Lösungen, die Zeit sparen und Sicherheit gewährleisten.",
"PROTECTION_BUTTON": "Erkunden",
"PROJECTS": "Volle Skalierbarkeit und Anpassungsfähigkeit",
"PROJECTS_DESC": "Autorisiere andere Benutzer, Deine eigenen Projekte zu verwenden, oder erstelle benutzerdefinierte Rollen für berechtigte Projekte",
"PROJECTS_BUTTON": "Projektübersicht",
"PROJECTS_NEW_LINK":"Neues Projekt erstellen",
"IAM_CREATE_ORG":"Organisation erstellen",
"ORG_POLICY_COMPLEXITY":"Passwort Komplexität",
"ORG_POLICY_IAM":"Organisation Zugangseinstellungen",
"ORG_POLICY_LOGIN":"Login Richtlinie",
"IAM_POLICY_COMPLEXITY":"Systemweite Passwort Komplexität",
"IAM_POLICY_IAM":"Systemweite Zugangseinstellungen",
"IAN_POLICY_LOGIN":"Systemweite Login Richtlinie",
"USERS": "Erstelle und verwalte Deine Benutzer.",
"USERS_DESC": "Überwache Dein Rollenkonzept in Echtzeit. Ergreife sofort Massnahmen.",
"USERS_HUMANS":"Zeige Benutzer",
"USERS_MACHINES": "Zeige Service User",
"USERS_CREATE":"User erstellen",
"USERS_BUTTON": "Benutzer anzeigen",
"IAM": "Identity- und Access-Management",
"IAM_DESC": "Verwalte Deine Organisationen und Administratoren.",
@ -26,6 +38,7 @@
"PERSONAL_INFO": "Persönliche Informationen",
"IAM":"Administration",
"ORGANIZATION": "Organisation",
"ADMINSECTION":"Administration",
"PROJECTSSECTION":"Projektsektion",
"PROJECT": "Projekte",
"GRANTEDPROJECT":"Berechtigte Projekte",
@ -106,8 +119,11 @@
"1":"Nach Username filtern",
"2":"Nach Vornamen filtern",
"3":"Nach Nachnamen filtern",
"4":"Nach rollenschlüssel filtern",
"5":"Nach Display Namen filtern",
"6":"Nach Email filtern"
"6":"Nach Email filtern",
"10":"Nach Organisationsname filtern",
"12":"Project Name"
}
},
"MFA": {
@ -384,6 +400,7 @@
"VERIFICATION_NEWTOKEN_DESC":"Wenn Du ein neues Token anfordern willst, klicke auf die gewünschte Methode. Wenn Du ein vorhandenes Token validieren möchtest, klicke auf \"Verifizieren\".",
"VERIFICATION_VALIDATION_ONGOING":"Ein Token zur Validierung wurde bereits angefragt. Klicke auf \"Verifizieren\", um dieses Token zu validieren.",
"VERIFICATION_VALIDATION_ONGOING_TYPE":"Typ des Tokens:",
"VERIFICATION_SUCCESSFUL":"Domain erfolgreich validiert!",
"REQUESTNEWTOKEN":"Neues Token anfordern",
"TYPES": {
"1":"HTTP",

View File

@ -5,14 +5,26 @@
"SECURITYANDPRIVACY": "Data Protection and Personalisation",
"SECURITYANDPRIVACY_DESC": "Manage Your Information and Security Settings",
"SECURITYANDPRIVACY_BUTTON": "Personalise Information and Security",
"CHANGE_PWD":"Change Password",
"PROTECTION": "Organisational Policies",
"PROTECTION_DESC": "Manage your organisational guidelines. Explore some pre-packaged solutions that save you time and ensure security.",
"PROTECTION_BUTTON": "Explore",
"PROJECTS": "Create and Manage Your Applications and Projects",
"PROJECTS_DESC": "Authorize others to use your projects or define custom roles on eligible projects.",
"PROJECTS_BUTTON": "Project Overview",
"PROJECTS_NEW_LINK":"Create new project",
"ORG_POLICY_COMPLEXITY":"Password Complexity Settings",
"ORG_POLICY_IAM":"Organisation Access Properties",
"ORG_POLICY_LOGIN":"Login Policy",
"IAM_CREATE_ORG":"Create organisation",
"IAM_POLICY_COMPLEXITY":"System Password Complexity Settings",
"IAM_POLICY_IAM":"System Access Properties",
"IAM_POLICY_LOGIN":"System Login Policy",
"USERS": "Create and Manage Your Users",
"USERS_DESC": "Monitor your role concept in real time. Take immediate action.",
"USERS_HUMANS":"Show human users",
"USERS_MACHINES": "Show machine users",
"USERS_CREATE":"Create User",
"USERS_BUTTON": "Show Users",
"IAM": "Identity and Access Management",
"IAM_DESC": "Manage your organisations and administrators.",
@ -26,6 +38,7 @@
"PERSONAL_INFO": "Personal Information",
"IAM":"Administration",
"ORGANIZATION": "Organisation",
"ADMINSECTION":"Administration",
"PROJECTSSECTION":"Projects Section",
"PROJECT": "Projects",
"GRANTEDPROJECT":"Granted Projects",
@ -106,8 +119,11 @@
"1":"Filter for Username",
"2":"filter for Firstname",
"3":"filter for Lastname",
"4":"filter for role Key",
"5":"filter for DisplayName",
"6":"filter for email"
"6":"filter for email",
"10":"filter for organisation name",
"12":"filter for project name"
}
},
"MFA": {
@ -384,6 +400,7 @@
"VERIFICATION_NEWTOKEN_DESC":"If you want to request a new token, select you preferred method. If you want to validate a persisting token, click the button above.",
"VERIFICATION_VALIDATION_ONGOING":"A verification token has already been requested. Click on the button to trigger a verification check.",
"VERIFICATION_VALIDATION_ONGOING_TYPE":"Type of the token:",
"VERIFICATION_SUCCESSFUL":"Domain successfully verified!",
"REQUESTNEWTOKEN":"Request new Token",
"TYPES": {
"1":"HTTP",

View File

@ -1,5 +1,6 @@
@import 'src/app/modules/card/card';
@import './styles/table';
@import './styles/link.scss';
@import './styles/sidenav-list';
@import 'src/app/modules/avatar/avatar.component';
@import 'src/app/modules/changes/changes.component';
@ -22,4 +23,5 @@
@include meta-theme($theme);
@include theme-card($theme);
@include textvar($theme);
@include link-theme($theme);
}

View File

@ -61,20 +61,20 @@ $caos-dark-brand: (
);
$caos-light-brand: (
50: #fff,
100: #dde6f3,
200: #b4c9e4,
300: #7fa3d1,
400: #6992c9,
500: #5282c1,
600: #4072b4,
700: #38649d,
800: #305687,
900: #284770,
A100: #fff,
A200: #dde6f3,
A300: #6992c9,
A400: #38649d,
50: #eaedfa,
100: #ccd2f2,
200: #aab4ea,
300: #8796e1,
400: #6e80da,
500: #5469d4,
600: #4d61cf,
700: #4356c9,
800: #3a4cc3,
900: #293bb9,
A100: #f9faff,
A200: #c6ccff,
A300: #939fff,
A400: #7a88ff,
A500:#333,
A600: #000,
A700: #8795a1,
@ -167,7 +167,9 @@ $custom-typography:
@include component-themes($light-theme);
@include angular-material-theme($light-theme);
--table-row-back: #eceef1;
--grey: #697386;
--table-row-back: #e7ebf0;
--color-main: #5469d4;
.sidenav,
.main-container,
@ -194,13 +196,18 @@ $custom-typography:
border-radius: 8px;
cursor: pointer;
}
.root-header {
box-shadow: inset 0 -1px #e3e8ee;
}
}
.dark-theme {
@include component-themes($dark-theme);
@include angular-material-theme($dark-theme);
--table-row-back: #363738;
--table-row-back: #292a2b;
--color-main: #5282c1;
.sidenav,
.main-container,
@ -227,6 +234,10 @@ $custom-typography:
border-radius: 8px;
cursor: pointer;
}
.root-header {
box-shadow: inset 0 -1px #303131;
}
}
// @include mat-checkbox-theme($candy-app-theme);

View File

@ -0,0 +1,16 @@
@import '~@angular/material/theming';
@mixin link-theme($theme) {
/* stylelint-disable */
$primary: map-get($theme, primary);
$primary-color: mat-color($primary, 500);
$primary-color-lighter: mat-color($primary, A300);
a {
color: $primary-color;
}
a:hover {
color: $primary-color-lighter;
}
}

View File

@ -7,11 +7,12 @@
$primary-color: mat-color($primary, 500);
$accent-color: mat-color($accent, 500);
$primary-dark: mat-color($primary, A900);
$foreground: map-get($theme, foreground);
$sec-dark: mat-color($primary, A800);
/* stylelint-enable */
.nav-item {
color: inherit;
color: mat-color($foreground, text) !important;
&:hover {
background-color: $sec-dark;
@ -48,27 +49,62 @@
}
.root-header {
box-shadow: 0 5px 10px rgba(0, 0, 0, .12);
box-shadow: inset 0 -1px #e3e8ee;
background-color: $primary-dark !important;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
}
.admin-line {
background: $accent-color;
position: fixed;
bottom: 0;
left: 0;
right: calc(100vw - 300px);
background-color: $primary-color;
color: white;
z-index: 1;
font-size: 13px;
padding: 3px 2rem;
transform: translateY(75%);
transition: all .2s;
border-top-right-radius: 5px;
span {
display: none;
}
button {
height: 1.2rem;
width: 1.2rem;
line-height: 1.2rem;
margin-right: 1rem;
border-top-right-radius: 50vw;
border-bottom-right-radius: 50vw;
* {
height: 1.2rem;
width: 1.2rem;
line-height: 1rem;
}
}
&::before {
content: '';
position: absolute;
width: 0;
bottom: 0;
height: 0;
top: 0;
left: 0;
border-bottom: 20px solid $primary-dark;
border-right: 20px solid transparent;
border-bottom: 20px solid transparent;
border-left: 20px solid $primary-dark;
transition: border-color .3s cubic-bezier(.645, .045, .355, 1);
}
&.expanded,
&:hover {
transform: translateY(0%);
right: 0;
span {
display: inline-block;
}
}
}
}

View File

@ -36,7 +36,7 @@
&:hover {
td {
background-color: var(--table-row-back); // rgba($inv-color, .05);
background: var(--table-row-back); // rgba($inv-color, .05);
}
}
}