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

View File

@ -66,13 +66,6 @@
} }
} }
.admin-line {
font-size: 12px;
padding: 4px 2rem;
position: relative;
overflow: hidden;
}
.main-container { .main-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -222,6 +215,13 @@
margin: .5rem 0; margin: .5rem 0;
flex: 1; flex: 1;
} }
.hiddenline {
display: block;
visibility: hidden;
// flex: 1;
width: 4rem;
}
} }
@mixin textvar($theme) { @mixin textvar($theme) {
@ -255,3 +255,8 @@
align-items: center; 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 { GrpcAuthService } from './services/grpc-auth.service';
import { ManagementService } from './services/mgmt.service'; import { ManagementService } from './services/mgmt.service';
import { ThemeService } from './services/theme.service'; import { ThemeService } from './services/theme.service';
import { ToastService } from './services/toast.service';
import { UpdateService } from './services/update.service'; import { UpdateService } from './services/update.service';
@Component({ @Component({
@ -57,13 +56,11 @@ export class AppComponent implements OnDestroy {
public showProjectSection: boolean = false; public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
public filterControl: FormControl = new FormControl(''); public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription(); private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription(); private orgSub: Subscription = new Subscription();
public hideAdminWarn: boolean = true;
constructor( constructor(
public viewPortScroller: ViewportScroller, public viewPortScroller: ViewportScroller,
@Inject('windowObject') public window: Window, @Inject('windowObject') public window: Window,
@ -73,15 +70,14 @@ export class AppComponent implements OnDestroy {
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
public overlayContainer: OverlayContainer, public overlayContainer: OverlayContainer,
private themeService: ThemeService, private themeService: ThemeService,
private mgmtService: ManagementService, public mgmtService: ManagementService,
public matIconRegistry: MatIconRegistry, public matIconRegistry: MatIconRegistry,
public domSanitizer: DomSanitizer, public domSanitizer: DomSanitizer,
private toast: ToastService,
private router: Router, private router: Router,
update: UpdateService, update: UpdateService,
@Inject(DOCUMENT) private document: Document, @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('%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 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'); 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(), value.trim().toLowerCase(),
); );
}); });
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
@ -186,6 +184,11 @@ export class AppComponent implements OnDestroy {
this.orgSub.unsubscribe(); this.orgSub.unsubscribe();
} }
public toggleAdminHide(): void {
this.hideAdminWarn = !this.hideAdminWarn;
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
}
public loadOrgs(filter?: string): void { public loadOrgs(filter?: string): void {
let query; let query;
if (filter) { if (filter) {
@ -249,13 +252,9 @@ export class AppComponent implements OnDestroy {
private getProjectCount(): void { private getProjectCount(): void {
this.authService.isAllowed(['project.read$']).subscribe((allowed) => { this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
if (allowed) { if (allowed) {
this.mgmtService.SearchProjects(0, 0).then(res => { this.mgmtService.SearchProjects(0, 0);
this.ownedProjectsCount = res.toObject().totalResult;
});
this.mgmtService.SearchGrantedProjects(0, 0).then(res => { this.mgmtService.SearchGrantedProjects(0, 0);
this.grantedProjectsCount = res.toObject().totalResult;
});
} }
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,10 @@
$primary-dark: mat-color($primary, A800); $primary-dark: mat-color($primary, A800);
/* stylelint-enable */ /* stylelint-enable */
$lighter-color: rgba(mat-color($primary, 300), .5);
.meta-wrapper { .meta-wrapper {
.meta { .meta {
position: relative; position: relative;
flex: 1 0 300px; 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 { &::after {
border-left: 2px solid $primary-color; border-left: 2px solid $primary-color;
@ -27,7 +20,7 @@
left top, left top,
left bottom, left bottom,
from($primary-color), from($primary-color),
to($primary-dark), to(rgb(0, 0, 0, .5)),
color-stop( color-stop(
01, 01,
$primary-dark $primary-dark
@ -39,7 +32,7 @@
left top, left top,
left bottom, left bottom,
from($primary-color), from($primary-color),
to($primary-dark), to(rgb(0, 0, 0, .5)),
color-stop( color-stop(
01, 01,
$primary-dark $primary-dark

View File

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

View File

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

View File

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

View File

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

View File

@ -16,8 +16,9 @@ h1 {
margin: .5rem; margin: .5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 200px; min-height: 250px;
padding: 1rem; padding: 1rem;
height: 100%;
@media only screen and (max-width: 450px) { @media only screen and (max-width: 450px) {
flex-basis: 100%; flex-basis: 100%;

View File

@ -103,7 +103,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
break; break;
default: default:
this.loadingSubject.next(true); this.loadingSubject.next(true);
const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, []); const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, queries ?? []);
this.loadResponse(promise3); this.loadResponse(promise3);
break; break;
} }

View File

@ -1,6 +1,12 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()" <app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp" [emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection"> [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 <button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false"> (click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i> <i class="las la-trash"></i>
@ -33,19 +39,28 @@
</ng-container> </ng-container>
<ng-container matColumnDef="user"> <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"> <td mat-cell *matCellDef="let grant">
{{grant?.displayName}}</td> {{grant?.displayName}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="org"> <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"> <td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td> {{grant.orgName}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="projectId"> <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"> <td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td> {{grant.projectName}} </td>
</ng-container> </ng-container>
@ -63,29 +78,32 @@
</ng-container> </ng-container>
<ng-container matColumnDef="roleNamesList"> <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"> <td mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"> <ng-container
<ng-container *ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
*ngIf="(grant.grantId && loadedGrantId !== grant.grantId) || (loadedProjectId !== grant.projectId)"> <div class="flex-row">
<div class="flex-row"> <div class="role">
<span class="role" *ngFor="let role of grant.roleKeysList">{{ role }}</span> <span *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>
</div> </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>
<ng-container <ng-container
*ngIf="context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE"> *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-form-field class="form-field" appearance="outline">
*ngIf="loadedProjectId && loadedProjectId === grant.projectId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple <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))" [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-option>
</mat-select> </mat-select>
</mat-form-field> </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>
<ng-container <ng-container
*ngIf="context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE"> *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-form-field class="form-field" appearance="outline">
*ngIf="loadedGrantId && loadedGrantId === grant.grantId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple <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))" [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-option>
</mat-select> </mat-select>
</mat-form-field> </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>
</td> </td>
</ng-container> </ng-container>
@ -124,3 +149,10 @@
</mat-paginator> </mat-paginator>
</div> </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>

View File

@ -21,6 +21,19 @@
} }
} }
th {
.search-button {
display: none;
}
&:hover,
&.search-active {
.search-button {
display: inline-block;
}
}
}
.selection { .selection {
width: 50px; width: 50px;
max-width: 50px; max-width: 50px;
@ -35,15 +48,22 @@
.flex-row { .flex-row {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
max-width: 400px; max-width: 400px;
.role { .role {
display: block; display: flex;
flex-direction: column;
margin: .25rem; margin: .25rem;
font-size: 14px; font-size: 14px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
justify-items: center;
align-self: center;
}
.fill-space {
flex: 1;
} }
button { button {
@ -60,3 +80,7 @@
.fill-space { .fill-space {
flex: 1; flex: 1;
} }
.filtername {
margin-right: 1rem;
}

View File

@ -1,10 +1,19 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatInput } from '@angular/material/input';
import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators'; 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 { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -14,12 +23,17 @@ import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource
selector: 'app-user-grants', selector: 'app-user-grants',
templateUrl: './user-grants.component.html', templateUrl: './user-grants.component.html',
styleUrls: ['./user-grants.component.scss'], styleUrls: ['./user-grants.component.scss'],
animations: [
enterAnimations,
],
}) })
export class UserGrantsComponent implements OnInit, AfterViewInit { export class UserGrantsComponent implements OnInit, AfterViewInit {
public userGrantSearchKey: UserGrantSearchKey | undefined = undefined;
public UserGrantSearchKey: any = UserGrantSearchKey;
public INITIAL_PAGE_SIZE: number = 50; public INITIAL_PAGE_SIZE: number = 50;
@Input() context: UserGrantContext = UserGrantContext.NONE; @Input() context: UserGrantContext = UserGrantContext.NONE;
@Input() refreshOnPreviousRoutes: string[] = []; @Input() refreshOnPreviousRoutes: string[] = [];
public grants: UserGrantView.AsObject[] = [];
public dataSource!: UserGrantsDataSource; public dataSource!: UserGrantsDataSource;
public selection: SelectionModel<UserGrantView.AsObject> = new SelectionModel<UserGrantView.AsObject>(true, []); public selection: SelectionModel<UserGrantView.AsObject> = new SelectionModel<UserGrantView.AsObject>(true, []);
@ -32,6 +46,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
@Input() userId: string = ''; @Input() userId: string = '';
@Input() projectId: string = ''; @Input() projectId: string = '';
@Input() grantId: string = ''; @Input() grantId: string = '';
@ViewChild('input') public filter!: MatInput;
public grantRoleOptions: string[] = []; public grantRoleOptions: string[] = [];
public projectRoleOptions: ProjectRoleView.AsObject[] = []; public projectRoleOptions: ProjectRoleView.AsObject[] = [];
@ -39,6 +54,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public loadedGrantId: string = ''; public loadedGrantId: string = '';
public loadedProjectId: string = ''; public loadedProjectId: string = '';
public grantToEdit: string = '';
public UserGrantContext: any = UserGrantContext; public UserGrantContext: any = UserGrantContext;
@ -89,7 +105,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
.subscribe(); .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.dataSource.loadGrants(
this.context, this.context,
this.paginator?.pageIndex ?? 0, this.paginator?.pageIndex ?? 0,
@ -99,6 +124,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
grantId: this.grantId, grantId: this.grantId,
userId: this.userId, userId: this.userId,
}, },
queries,
); );
} }
@ -114,7 +140,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row)); 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.mgmtService.GetGrantedProjectByID(projectId, grantId).then(resp => {
this.loadedGrantId = grantId; this.loadedGrantId = grantId;
this.grantRoleOptions = resp.toObject().roleKeysList; 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.mgmtService.SearchProjectRoles(projectId, 100, 0).then(resp => {
this.loadedProjectId = projectId; this.loadedProjectId = projectId;
this.projectRoleOptions = resp.toObject().resultList; 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 { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
@ -39,6 +40,7 @@ import { UserGrantsComponent } from './user-grants.component';
MatCheckboxModule, MatCheckboxModule,
MatTooltipModule, MatTooltipModule,
MatSelectModule, MatSelectModule,
MatInputModule,
MatFormFieldModule, MatFormFieldModule,
TranslateModule, TranslateModule,
HasRolePipeModule, HasRolePipeModule,

View File

@ -24,6 +24,23 @@
{{'HOME.IAM'| translate}}</h2> {{'HOME.IAM'| translate}}</h2>
<p>{{'HOME.IAM_DESC'| translate}}</p> <p>{{'HOME.IAM_DESC'| translate}}</p>
</div> </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> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
<a color="primary" mat-stroked-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a> <a color="primary" mat-stroked-button [routerLink]="['/iam']">{{'HOME.IAM_BUTTON' | translate}}</a>
@ -37,6 +54,9 @@
{{'HOME.SECURITYANDPRIVACY'| translate}}</h2> {{'HOME.SECURITYANDPRIVACY'| translate}}</h2>
<p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p> <p>{{'HOME.SECURITYANDPRIVACY_DESC'| translate}}</p>
</div> </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> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
<a color="primary" mat-stroked-button <a color="primary" mat-stroked-button
@ -51,6 +71,11 @@
<i class="icon las la-layer-group"></i> <i class="icon las la-layer-group"></i>
{{'HOME.PROJECTS'| translate}}</h2> {{'HOME.PROJECTS'| translate}}</h2>
<p>{{'HOME.PROJECTS_DESC'| translate}}</p> <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> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
@ -66,6 +91,19 @@
<h2> <i class="icon las la-archway"></i> <h2> <i class="icon las la-archway"></i>
{{'HOME.PROTECTION'| translate}}</h2> {{'HOME.PROTECTION'| translate}}</h2>
<p>{{'HOME.PROTECTION_DESC'| translate}}</p> <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> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="footer"> <div class="footer">
@ -82,6 +120,18 @@
<i class="las la-users"></i> <i class="las la-users"></i>
{{'HOME.USERS'| translate}}</h2> {{'HOME.USERS'| translate}}</h2>
<p>{{'HOME.USERS_DESC'| translate}}</p> <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> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="footer"> <div class="footer">

View File

@ -32,7 +32,7 @@
justify-content: space-evenly; justify-content: space-evenly;
.item { .item {
flex: 1 1 45%; flex: 1 0 45%;
margin: 0 1rem; margin: 0 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -80,3 +80,25 @@
margin-top: 3rem; 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> {{'ORG.PAGES.ORGDOMAIN.TYPES.'+ domain.validationType | translate}}</p>
<div *ngIf="domain.validationType !== OrgDomainValidationType.ORGDOMAINVALIDATIONTYPE_UNSPECIFIED" <div *ngIf="domain.validationType !== OrgDomainValidationType.ORGDOMAINVALIDATIONTYPE_UNSPECIFIED"
class="btn-container"> class="btn-container">
<button color="primary" type="submit" mat-raised-button *ngIf="!(dns || http)" <button color="primary" type="submit" mat-raised-button *ngIf="!(dns || http)" (click)="validate()">
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button> {{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
<button *ngIf="!showNew" mat-stroked-button color="primary" <button *ngIf="!showNew" mat-stroked-button color="primary"
(click)="showNew = true">{{'ORG.PAGES.ORGDOMAIN.REQUESTNEWTOKEN' | translate}}</button> (click)="showNew = true">{{'ORG.PAGES.ORGDOMAIN.REQUESTNEWTOKEN' | translate}}</button>
@ -34,8 +36,11 @@
<div class="btn-container"> <div class="btn-container">
<button mat-stroked-button (click)="saveFile()" <button mat-stroked-button (click)="saveFile()"
color="primary">{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button> color="primary">{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
<button color="primary" type="submit" mat-raised-button <button color="primary" class="verify-button" type="submit" mat-raised-button (click)="validate()">
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button> <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>
</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"></i>
<i *ngIf="copied == dns.token" class="las la-clipboard-check"></i> <i *ngIf="copied == dns.token" class="las la-clipboard-check"></i>
</button> </button>
<button color="primary" type="submit" mat-raised-button <button color="primary" type="submit" mat-raised-button class="verify-button" (click)="validate()">
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button> {{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div> </div>
<p class="entry">{{dns?.url}}</p> <p class="entry">{{dns?.url}}</p>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,11 +22,10 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> {{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
</div> </div>
</div> </div>
@ -47,8 +46,8 @@
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> {{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button" <button [ngClass]="{ selected: selection.isSelected(item)}"
mat-icon-button> (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 *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon> <mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button> </button>
@ -56,3 +55,11 @@
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0"> <p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p> {{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div> </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.setPrefixedItem('pinned-granted-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId), this.selection.selected.map(item => item.projectId),
)).then(() => { )).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item)); selection.added.forEach(item => {
filtered.forEach((f, i) => { const index = this.notPinned.findIndex(i => i.projectId === item.projectId);
this.notPinned.splice(i, 1); this.notPinned.splice(index, 1);
}); });
this.notPinned.push(...selection.removed); this.notPinned.push(...selection.removed);
}); });
}); });
@ -74,18 +73,10 @@ export class GrantedProjectGridComponent implements OnChanges {
const array: string[] = JSON.parse(storageEntry); const array: string[] = JSON.parse(storageEntry);
const toSelect: ProjectGrantView.AsObject[] = this.items.filter((item, index) => { const toSelect: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (array.includes(item.projectId)) { if (array.includes(item.projectId)) {
// this.notPinned.splice(index, 1);
return true; return true;
} }
}); });
this.selection.select(...toSelect); 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) { if (resp) {
this.mgmtService.RemoveProject(this.projectId).then(() => { this.mgmtService.RemoveProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true); this.toast.showInfo('PROJECT.TOAST.DELETED', true);
this.router.navigate(['/projects']); const params: Params = {
'deferredReload': true,
};
this.router.navigate(['/projects'], { queryParams: params });
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });

View File

@ -25,11 +25,9 @@
}}</span> }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button> <template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template>
<mat-icon *ngIf="selection.isSelected(item)" svgIcon="mdi_pin"></mat-icon> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(item)"></mat-icon>
</button>
</div> </div>
</div> </div>
@ -52,15 +50,9 @@
}}</span> }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<button *ngIf="item.projectId !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn"
(click)="deleteProject(item)" class="delete-button" mat-icon-button> <template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template>
<i class="las la-trash"></i> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template>
</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>
</div> </div>
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p> <p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
@ -72,3 +64,18 @@
</div> </div>
</ng-template> </ng-template>
</div> </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.setPrefixedItem('pinned-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId), this.selection.selected.map(item => item.projectId),
)).then(() => { )).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item)); selection.added.forEach(item => {
filtered.forEach((f, i) => { const index = this.notPinned.findIndex(i => i.projectId === item.projectId);
this.notPinned.splice(i, 1); this.notPinned.splice(index, 1);
}); });
this.notPinned.push(...selection.removed); this.notPinned.push(...selection.removed);
}); });
}); });
@ -102,13 +101,6 @@ export class OwnedProjectGridComponent implements OnChanges {
} }
}); });
this.selection.select(...toSelect); 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); 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, { const dialogRef = this.dialog.open(WarnDialogComponent, {
data: { data: {
confirmKey: 'ACTIONS.DELETE', confirmKey: 'ACTIONS.DELETE',
@ -152,6 +150,16 @@ export class OwnedProjectGridComponent implements OnChanges {
if (index > -1) { if (index > -1) {
this.items.splice(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 => { }).catch(error => {
this.toast.showError(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 { MatDialog } from '@angular/material/dialog';
import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { ProjectView } from 'src/app/proto/generated/management_pb'; import { ProjectView } from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
@ -59,6 +60,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
public zitadelProjectId: string = ''; public zitadelProjectId: string = '';
constructor(private router: Router, constructor(private router: Router,
private route: ActivatedRoute,
public translate: TranslateService, public translate: TranslateService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private toast: ToastService, private toast: ToastService,
@ -70,7 +72,15 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
} }
public ngOnInit(): void { 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 { public ngOnDestroy(): void {
@ -97,7 +107,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
this.router.navigate(['/projects', 'create']); 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.loadingSubject.next(true);
this.mgmtService.SearchProjects(limit, offset).then(res => { this.mgmtService.SearchProjects(limit, offset).then(res => {
const response = res.toObject(); const response = res.toObject();

View File

@ -68,7 +68,7 @@ export class UserCreateComponent implements OnDestroy {
} }
private async loadOrg(): Promise<void> { 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); const found = domains.resultList.find(domain => domain.primary);
if (found) { if (found) {
this.primaryDomain = found; this.primaryDomain = found;

View File

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

View File

@ -23,7 +23,7 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-template #compact> <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"> <div class="membership-avatar">
<span style="font-size: 16px;">{{memberships.totalResult}}</span> <span style="font-size: 16px;">{{memberships.totalResult}}</span>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,26 @@
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }
.search-button,
.dlt-button {
visibility: hidden;
}
&:hover,
&.search-active {
.search-button {
visibility: visible;
}
}
}
tr {
&:hover {
.dlt-button {
visibility: visible;
}
}
} }
.selection { .selection {
@ -27,31 +47,6 @@
} }
} }
tr {
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
th {
.search-button {
visibility: hidden;
}
&:hover,
&.search-active {
.search-button {
visibility: visible;
}
}
}
.filtername { .filtername {
margin: 0 1rem; margin: 0 1rem;
} }

View File

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

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject } from 'rxjs';
import { import {
AddMachineKeyRequest, AddMachineKeyRequest,
@ -160,6 +161,9 @@ export type ResponseMapper<TResp, TMappedResp> = (resp: TResp) => TMappedResp;
providedIn: 'root', providedIn: 'root',
}) })
export class ManagementService { export class ManagementService {
public ownedProjectsCount: BehaviorSubject<number> = new BehaviorSubject(0);
public grantedProjectsCount: BehaviorSubject<number> = new BehaviorSubject(0);
constructor(private readonly grpcService: GrpcService) { } constructor(private readonly grpcService: GrpcService) { }
public SearchIdps( public SearchIdps(
@ -393,11 +397,9 @@ export class ManagementService {
return this.grpcService.mgmt.removeMyOrgDomain(req); return this.grpcService.mgmt.removeMyOrgDomain(req);
} }
public SearchMyOrgDomains(offset: number, limit: number, queryList?: OrgDomainSearchQuery[]): public SearchMyOrgDomains(queryList?: OrgDomainSearchQuery[]):
Promise<OrgDomainSearchResponse> { Promise<OrgDomainSearchResponse> {
const req: OrgDomainSearchRequest = new OrgDomainSearchRequest(); const req: OrgDomainSearchRequest = new OrgDomainSearchRequest();
req.setLimit(limit);
req.setOffset(offset);
if (queryList) { if (queryList) {
req.setQueriesList(queryList); req.setQueriesList(queryList);
} }
@ -834,13 +836,17 @@ export class ManagementService {
// USER GRANTS // USER GRANTS
public SearchUserGrants( public SearchUserGrants(
limit: number, limit?: number,
offset: number, offset?: number,
queryList?: UserGrantSearchQuery[], queryList?: UserGrantSearchQuery[],
): Promise<UserGrantSearchResponse> { ): Promise<UserGrantSearchResponse> {
const req = new UserGrantSearchRequest(); const req = new UserGrantSearchRequest();
req.setLimit(limit); if (limit) {
req.setOffset(offset); req.setLimit(limit);
}
if (offset) {
req.setOffset(offset);
}
if (queryList) { if (queryList) {
req.setQueriesList(queryList); req.setQueriesList(queryList);
} }
@ -929,14 +935,26 @@ export class ManagementService {
// project // project
public SearchProjects( public SearchProjects(
limit: number, offset: number, queryList?: ProjectSearchQuery[]): Promise<ProjectSearchResponse> { limit?: number, offset?: number, queryList?: ProjectSearchQuery[]): Promise<ProjectSearchResponse> {
const req = new ProjectSearchRequest(); const req = new ProjectSearchRequest();
req.setLimit(limit); if (limit) {
req.setOffset(offset); req.setLimit(limit);
}
if (offset) {
req.setOffset(offset);
}
if (queryList) { if (queryList) {
req.setQueriesList(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( public SearchGrantedProjects(
@ -947,10 +965,12 @@ export class ManagementService {
if (queryList) { if (queryList) {
req.setQueriesList(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> { public GetZitadelDocs(): Promise<ZitadelDocs> {
const req = new Empty(); const req = new Empty();
return this.grpcService.mgmt.getZitadelDocs(req); return this.grpcService.mgmt.getZitadelDocs(req);
@ -972,7 +992,11 @@ export class ManagementService {
public CreateProject(project: ProjectCreateRequest.AsObject): Promise<Project> { public CreateProject(project: ProjectCreateRequest.AsObject): Promise<Project> {
const req = new ProjectCreateRequest(); const req = new ProjectCreateRequest();
req.setName(project.name); 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> { public UpdateProject(id: string, projectView: ProjectView.AsObject): Promise<Project> {
@ -1221,7 +1245,11 @@ export class ManagementService {
public RemoveProject(id: string): Promise<Empty> { public RemoveProject(id: string): Promise<Empty> {
const req = new ProjectID(); const req = new ProjectID();
req.setId(id); 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": "Datenschutz und Personalisierung",
"SECURITYANDPRIVACY_DESC": "Verwalte Deine Informationen und Sicherheitseinstellungen.", "SECURITYANDPRIVACY_DESC": "Verwalte Deine Informationen und Sicherheitseinstellungen.",
"SECURITYANDPRIVACY_BUTTON": "Daten verwalten und personalisieren", "SECURITYANDPRIVACY_BUTTON": "Daten verwalten und personalisieren",
"CHANGE_PWD":"Password ändern",
"PROTECTION": "Organisationsrichtlinien", "PROTECTION": "Organisationsrichtlinien",
"PROTECTION_DESC": "Verwalte Deine Organisationsrichtlinien und entdecke die vorgefertigte Lösungen, die Zeit sparen und Sicherheit gewährleisten.", "PROTECTION_DESC": "Verwalte Deine Organisationsrichtlinien und entdecke die vorgefertigte Lösungen, die Zeit sparen und Sicherheit gewährleisten.",
"PROTECTION_BUTTON": "Erkunden", "PROTECTION_BUTTON": "Erkunden",
"PROJECTS": "Volle Skalierbarkeit und Anpassungsfähigkeit", "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_DESC": "Autorisiere andere Benutzer, Deine eigenen Projekte zu verwenden, oder erstelle benutzerdefinierte Rollen für berechtigte Projekte",
"PROJECTS_BUTTON": "Projektübersicht", "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": "Erstelle und verwalte Deine Benutzer.",
"USERS_DESC": "Überwache Dein Rollenkonzept in Echtzeit. Ergreife sofort Massnahmen.", "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", "USERS_BUTTON": "Benutzer anzeigen",
"IAM": "Identity- und Access-Management", "IAM": "Identity- und Access-Management",
"IAM_DESC": "Verwalte Deine Organisationen und Administratoren.", "IAM_DESC": "Verwalte Deine Organisationen und Administratoren.",
@ -26,6 +38,7 @@
"PERSONAL_INFO": "Persönliche Informationen", "PERSONAL_INFO": "Persönliche Informationen",
"IAM":"Administration", "IAM":"Administration",
"ORGANIZATION": "Organisation", "ORGANIZATION": "Organisation",
"ADMINSECTION":"Administration",
"PROJECTSSECTION":"Projektsektion", "PROJECTSSECTION":"Projektsektion",
"PROJECT": "Projekte", "PROJECT": "Projekte",
"GRANTEDPROJECT":"Berechtigte Projekte", "GRANTEDPROJECT":"Berechtigte Projekte",
@ -106,8 +119,11 @@
"1":"Nach Username filtern", "1":"Nach Username filtern",
"2":"Nach Vornamen filtern", "2":"Nach Vornamen filtern",
"3":"Nach Nachnamen filtern", "3":"Nach Nachnamen filtern",
"4":"Nach rollenschlüssel filtern",
"5":"Nach Display Namen filtern", "5":"Nach Display Namen filtern",
"6":"Nach Email filtern" "6":"Nach Email filtern",
"10":"Nach Organisationsname filtern",
"12":"Project Name"
} }
}, },
"MFA": { "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_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":"Ein Token zur Validierung wurde bereits angefragt. Klicke auf \"Verifizieren\", um dieses Token zu validieren.",
"VERIFICATION_VALIDATION_ONGOING_TYPE":"Typ des Tokens:", "VERIFICATION_VALIDATION_ONGOING_TYPE":"Typ des Tokens:",
"VERIFICATION_SUCCESSFUL":"Domain erfolgreich validiert!",
"REQUESTNEWTOKEN":"Neues Token anfordern", "REQUESTNEWTOKEN":"Neues Token anfordern",
"TYPES": { "TYPES": {
"1":"HTTP", "1":"HTTP",

View File

@ -5,14 +5,26 @@
"SECURITYANDPRIVACY": "Data Protection and Personalisation", "SECURITYANDPRIVACY": "Data Protection and Personalisation",
"SECURITYANDPRIVACY_DESC": "Manage Your Information and Security Settings", "SECURITYANDPRIVACY_DESC": "Manage Your Information and Security Settings",
"SECURITYANDPRIVACY_BUTTON": "Personalise Information and Security", "SECURITYANDPRIVACY_BUTTON": "Personalise Information and Security",
"CHANGE_PWD":"Change Password",
"PROTECTION": "Organisational Policies", "PROTECTION": "Organisational Policies",
"PROTECTION_DESC": "Manage your organisational guidelines. Explore some pre-packaged solutions that save you time and ensure security.", "PROTECTION_DESC": "Manage your organisational guidelines. Explore some pre-packaged solutions that save you time and ensure security.",
"PROTECTION_BUTTON": "Explore", "PROTECTION_BUTTON": "Explore",
"PROJECTS": "Create and Manage Your Applications and Projects", "PROJECTS": "Create and Manage Your Applications and Projects",
"PROJECTS_DESC": "Authorize others to use your projects or define custom roles on eligible projects.", "PROJECTS_DESC": "Authorize others to use your projects or define custom roles on eligible projects.",
"PROJECTS_BUTTON": "Project Overview", "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": "Create and Manage Your Users",
"USERS_DESC": "Monitor your role concept in real time. Take immediate action.", "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", "USERS_BUTTON": "Show Users",
"IAM": "Identity and Access Management", "IAM": "Identity and Access Management",
"IAM_DESC": "Manage your organisations and administrators.", "IAM_DESC": "Manage your organisations and administrators.",
@ -26,6 +38,7 @@
"PERSONAL_INFO": "Personal Information", "PERSONAL_INFO": "Personal Information",
"IAM":"Administration", "IAM":"Administration",
"ORGANIZATION": "Organisation", "ORGANIZATION": "Organisation",
"ADMINSECTION":"Administration",
"PROJECTSSECTION":"Projects Section", "PROJECTSSECTION":"Projects Section",
"PROJECT": "Projects", "PROJECT": "Projects",
"GRANTEDPROJECT":"Granted Projects", "GRANTEDPROJECT":"Granted Projects",
@ -106,8 +119,11 @@
"1":"Filter for Username", "1":"Filter for Username",
"2":"filter for Firstname", "2":"filter for Firstname",
"3":"filter for Lastname", "3":"filter for Lastname",
"4":"filter for role Key",
"5":"filter for DisplayName", "5":"filter for DisplayName",
"6":"filter for email" "6":"filter for email",
"10":"filter for organisation name",
"12":"filter for project name"
} }
}, },
"MFA": { "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_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":"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_VALIDATION_ONGOING_TYPE":"Type of the token:",
"VERIFICATION_SUCCESSFUL":"Domain successfully verified!",
"REQUESTNEWTOKEN":"Request new Token", "REQUESTNEWTOKEN":"Request new Token",
"TYPES": { "TYPES": {
"1":"HTTP", "1":"HTTP",

View File

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

View File

@ -61,20 +61,20 @@ $caos-dark-brand: (
); );
$caos-light-brand: ( $caos-light-brand: (
50: #fff, 50: #eaedfa,
100: #dde6f3, 100: #ccd2f2,
200: #b4c9e4, 200: #aab4ea,
300: #7fa3d1, 300: #8796e1,
400: #6992c9, 400: #6e80da,
500: #5282c1, 500: #5469d4,
600: #4072b4, 600: #4d61cf,
700: #38649d, 700: #4356c9,
800: #305687, 800: #3a4cc3,
900: #284770, 900: #293bb9,
A100: #fff, A100: #f9faff,
A200: #dde6f3, A200: #c6ccff,
A300: #6992c9, A300: #939fff,
A400: #38649d, A400: #7a88ff,
A500:#333, A500:#333,
A600: #000, A600: #000,
A700: #8795a1, A700: #8795a1,
@ -167,7 +167,9 @@ $custom-typography:
@include component-themes($light-theme); @include component-themes($light-theme);
@include angular-material-theme($light-theme); @include angular-material-theme($light-theme);
--table-row-back: #eceef1; --grey: #697386;
--table-row-back: #e7ebf0;
--color-main: #5469d4;
.sidenav, .sidenav,
.main-container, .main-container,
@ -194,13 +196,18 @@ $custom-typography:
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
} }
.root-header {
box-shadow: inset 0 -1px #e3e8ee;
}
} }
.dark-theme { .dark-theme {
@include component-themes($dark-theme); @include component-themes($dark-theme);
@include angular-material-theme($dark-theme); @include angular-material-theme($dark-theme);
--table-row-back: #363738; --table-row-back: #292a2b;
--color-main: #5282c1;
.sidenav, .sidenav,
.main-container, .main-container,
@ -227,6 +234,10 @@ $custom-typography:
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
} }
.root-header {
box-shadow: inset 0 -1px #303131;
}
} }
// @include mat-checkbox-theme($candy-app-theme); // @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); $primary-color: mat-color($primary, 500);
$accent-color: mat-color($accent, 500); $accent-color: mat-color($accent, 500);
$primary-dark: mat-color($primary, A900); $primary-dark: mat-color($primary, A900);
$foreground: map-get($theme, foreground);
$sec-dark: mat-color($primary, A800); $sec-dark: mat-color($primary, A800);
/* stylelint-enable */ /* stylelint-enable */
.nav-item { .nav-item {
color: inherit; color: mat-color($foreground, text) !important;
&:hover { &:hover {
background-color: $sec-dark; background-color: $sec-dark;
@ -48,27 +49,62 @@
} }
.root-header { .root-header {
box-shadow: 0 5px 10px rgba(0, 0, 0, .12); box-shadow: inset 0 -1px #e3e8ee;
background-color: $primary-dark !important; background-color: $primary-dark !important;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1); transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
} }
.admin-line { .admin-line {
background: $accent-color; position: fixed;
bottom: 0;
left: 0;
right: calc(100vw - 300px);
background-color: $primary-color;
color: white; color: white;
margin-right: 1rem; z-index: 1;
border-top-right-radius: 50vw; font-size: 13px;
border-bottom-right-radius: 50vw; 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;
* {
height: 1.2rem;
width: 1.2rem;
line-height: 1rem;
}
}
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 0; width: 0;
bottom: 0; height: 0;
top: 0;
left: 0; left: 0;
border-bottom: 20px solid $primary-dark; border-bottom: 20px solid transparent;
border-right: 20px solid transparent; border-left: 20px solid $primary-dark;
transition: border-color .3s cubic-bezier(.645, .045, .355, 1); 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 { &:hover {
td { td {
background-color: var(--table-row-back); // rgba($inv-color, .05); background: var(--table-row-back); // rgba($inv-color, .05);
} }
} }
} }