feat(console-v2): login policy extension, domain policy, filter and UI fixes (#3644)

* show filter count when set

* toast contrast color

* fix notification settings, password dialog

* app-create, user-create layout

* domain policy

* login-policy, project grid loader, i18n

* login policy

* login policy save lifetimes

* private labeling optim

* granted project grantId

* smtp address matching

* i18n

* i18n

* i18n

* replace url strategy

* fix privatelabeling color picker saving

* stylelint

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Max Peintner 2022-05-17 16:18:37 +02:00 committed by GitHub
parent 8d0cf9f368
commit 8baf0fe08c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1967 additions and 1212 deletions

View File

@ -47,9 +47,9 @@
width: 32px;
line-height: 1rem;
border-radius: 50%;
-webkit-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5);
-moz-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5);
box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5);
-webkit-box-shadow: 1px 0 3px -1px rgba(33, 34, 36, 0.5);
-moz-box-shadow: 1px 0 3px -1px rgba(33, 34, 36, 0.5);
box-shadow: 1px 0 3px -1px rgba(33, 34, 36, 0.5);
cursor: pointer;
display: flex;
align-items: center;

View File

@ -1,8 +1,12 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()">
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
<div class="filter-row" id="filtercomp">
<div class="email-query">
<mat-checkbox id="name" class="cb" [checked]="getSubFilter(SubQuery.NAME)"
(change)="changeCheckbox(SubQuery.NAME, $event )">{{'FILTER.ORGNAME' | translate}}
<mat-checkbox
id="name"
class="cb"
[checked]="getSubFilter(SubQuery.NAME)"
(change)="changeCheckbox(SubQuery.NAME, $event)"
>{{ 'FILTER.ORGNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.NAME) as eq">
<cnsl-form-field class="filter-select-method">

View File

@ -50,7 +50,7 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
});
this.searchQueries = orgQueries.filter((q) => q !== undefined) as OrgQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : undefined);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
@ -88,11 +88,9 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
switch (subquery) {
case SubQuery.NAME:
(query as OrgNameQuery).setName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
}
this.filterCount$.next(this.filterCount);
}
public getSubFilter(subquery: SubQuery): any {
@ -109,23 +107,17 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
}
public resetFilter(): void {
this.searchQueries = [];
this.emitFilter();
}
public get filterCount(): number {
return this.searchQueries.length;
}
}

View File

@ -1,25 +1,12 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()">
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
<div class="filter-row" id="filtercomp">
<!-- <div class="name-query">
<mat-checkbox id="resourceowner" class="cb" [checked]="getSubFilter(SubQuery.RESOURCEOWNER)"
(change)="changeCheckbox(SubQuery.RESOURCEOWNER, $event )">{{'FILTER.RESOURCEOWNER' | translate}}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.RESOURCEOWNER) as ronq">
<span class="nomethod cnsl-secondary-text">
{{ 'FILTER.METHODS.1' | translate }}
</span>
<cnsl-form-field class="filter-input-value">
<input cnslInput name="value" [value]="ronq.getResourceOwner()"
(change)="setValue(SubQuery.RESOURCEOWNER, ronq, $event)" />
</cnsl-form-field>
</div>
</div> -->
<div class="email-query">
<mat-checkbox id="name" class="cb" [checked]="getSubFilter(SubQuery.NAME)"
(change)="changeCheckbox(SubQuery.NAME, $event )">{{'FILTER.PROJECTNAME' | translate}}
<mat-checkbox
id="name"
class="cb"
[checked]="getSubFilter(SubQuery.NAME)"
(change)="changeCheckbox(SubQuery.NAME, $event)"
>{{ 'FILTER.PROJECTNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.NAME) as eq">
<cnsl-form-field class="filter-select-method">

View File

@ -50,7 +50,7 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
});
this.searchQueries = projectQueries.filter((q) => q !== undefined) as ProjectQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
@ -60,15 +60,6 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
if (event.checked) {
switch (subquery) {
// case SubQuery.RESOURCEOWNER:
// const ronq = new ProjectResourceOwnerQuery();
// ronq.setResourceOwner('');
// const ro_sq = new ProjectQuery();
// ro_sq.setProjectResourceOwnerQuery(ronq);
// this.searchQueries.push(ro_sq);
// break;
case SubQuery.NAME:
const nq = new ProjectNameQuery();
nq.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
@ -82,14 +73,6 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
}
} else {
switch (subquery) {
// case SubQuery.RESOURCEOWNER:
// const index_s = this.searchQueries.findIndex(
// (q) => (q as ProjectQuery).toObject().projectResourceOwnerQuery !== undefined,
// );
// if (index_s > -1) {
// this.searchQueries.splice(index_s, 1);
// }
// break;
case SubQuery.NAME:
const index_dn = this.searchQueries.findIndex((q) => (q as ProjectQuery).toObject().nameQuery !== undefined);
if (index_dn > -1) {
@ -103,28 +86,15 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public setValue(subquery: SubQuery, query: any, event: any) {
const value = event?.target?.value ?? event.value;
switch (subquery) {
// case SubQuery.RESOURCEOWNER:
// (query as ProjectResourceOwnerQuery).setResourceOwner(value);
// this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
// break;
case SubQuery.NAME:
(query as ProjectNameQuery).setName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
}
this.filterCount$.next(this.filterCount);
}
public getSubFilter(subquery: SubQuery): any {
switch (subquery) {
// case SubQuery.RESOURCEOWNER:
// const s = this.searchQueries.find((q) => (q as ProjectQuery).toObject().projectResourceOwnerQuery !== undefined);
// if (s) {
// return (s as ProjectQuery).getProjectResourceOwnerQuery();
// } else {
// return undefined;
// }
case SubQuery.NAME:
const dn = this.searchQueries.find((q) => (q as ProjectQuery).toObject().nameQuery !== undefined);
if (dn) {
@ -137,15 +107,13 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
}
public resetFilter(): void {
@ -153,7 +121,7 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
this.emitFilter();
}
public get filterCount(): number {
public get filterCounter(): number {
return this.searchQueries.length;
}
}

View File

@ -1,4 +1,4 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()">
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
<div class="filter-row" id="filtercomp">
<div class="name-query">
<mat-checkbox

View File

@ -82,7 +82,7 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
});
this.searchQueries = userQueries.filter((q) => q !== undefined) as UserGrantQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
@ -170,23 +170,21 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
switch (subquery) {
case SubQuery.DISPLAYNAME:
(query as DisplayNameQuery).setDisplayName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.USERNAME:
(query as UserNameQuery).setUserName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.ORGNAME:
(query as UserGrantOrgNameQuery).setOrgName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.PROJECTNAME:
(query as UserGrantProjectNameQuery).setProjectName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
}
this.filterCount$.next(this.filterCount);
}
public getSubFilter(subquery: SubQuery): any {
@ -225,23 +223,17 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
}
public resetFilter(): void {
this.searchQueries = [];
this.emitFilter();
}
public get filterCount(): number {
return this.searchQueries.length;
}
}

View File

@ -1,8 +1,12 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()">
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
<div class="filter-row" id="filtercomp">
<div class="name-query">
<mat-checkbox id="state" class="cb" [checked]="getSubFilter(SubQuery.STATE)"
(change)="changeCheckbox(SubQuery.STATE, $event )">{{'FILTER.STATE' | translate}}
<mat-checkbox
id="state"
class="cb"
[checked]="getSubFilter(SubQuery.STATE)"
(change)="changeCheckbox(SubQuery.STATE, $event)"
>{{ 'FILTER.STATE' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.STATE) as sq">
<span class="nomethod cnsl-secondary-text">
@ -20,8 +24,12 @@
</div>
<div class="name-query">
<mat-checkbox id="displayname" class="cb" [checked]="getSubFilter(SubQuery.DISPLAYNAME)"
(change)="changeCheckbox(SubQuery.DISPLAYNAME, $event )">{{'FILTER.DISPLAYNAME' | translate}}
<mat-checkbox
id="displayname"
class="cb"
[checked]="getSubFilter(SubQuery.DISPLAYNAME)"
(change)="changeCheckbox(SubQuery.DISPLAYNAME, $event)"
>{{ 'FILTER.DISPLAYNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.DISPLAYNAME) as dnq">
<cnsl-form-field class="filter-select-method" appearance="outline">
@ -33,15 +41,23 @@
</cnsl-form-field>
<cnsl-form-field class="filter-input-value" appearance="outline">
<input cnslInput name="value" [value]="dnq.getDisplayName()"
(change)="setValue(SubQuery.DISPLAYNAME, dnq, $event)" />
<input
cnslInput
name="value"
[value]="dnq.getDisplayName()"
(change)="setValue(SubQuery.DISPLAYNAME, dnq, $event)"
/>
</cnsl-form-field>
</div>
</div>
<div class="email-query">
<mat-checkbox id="email" class="cb" [checked]="getSubFilter(SubQuery.EMAIL)"
(change)="changeCheckbox(SubQuery.EMAIL, $event )">{{'FILTER.EMAIL' | translate}}
<mat-checkbox
id="email"
class="cb"
[checked]="getSubFilter(SubQuery.EMAIL)"
(change)="changeCheckbox(SubQuery.EMAIL, $event)"
>{{ 'FILTER.EMAIL' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.EMAIL) as eq">
<cnsl-form-field class="filter-select-method" appearance="outline">
@ -53,15 +69,18 @@
</cnsl-form-field>
<cnsl-form-field class="filter-input-value" appearance="outline">
<input cnslInput name="value" [value]="eq.getEmailAddress()"
(change)="setValue(SubQuery.EMAIL, eq, $event)" />
<input cnslInput name="value" [value]="eq.getEmailAddress()" (change)="setValue(SubQuery.EMAIL, eq, $event)" />
</cnsl-form-field>
</div>
</div>
<div class="usernane-query">
<mat-checkbox id="username" class="cb" [checked]="getSubFilter(SubQuery.USERNAME)"
(change)="changeCheckbox(SubQuery.USERNAME, $event )">{{'FILTER.USERNAME' | translate}}
<mat-checkbox
id="username"
class="cb"
[checked]="getSubFilter(SubQuery.USERNAME)"
(change)="changeCheckbox(SubQuery.USERNAME, $event)"
>{{ 'FILTER.USERNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.USERNAME) as unq">
<cnsl-form-field class="filter-select-method" appearance="outline">
@ -73,8 +92,7 @@
</cnsl-form-field>
<cnsl-form-field class="filter-input-value" appearance="outline">
<input cnslInput name="value" [value]="unq.getUserName()"
(change)="setValue(SubQuery.USERNAME, unq, $event)" />
<input cnslInput name="value" [value]="unq.getUserName()" (change)="setValue(SubQuery.USERNAME, unq, $event)" />
</cnsl-form-field>
</div>
</div>

View File

@ -91,7 +91,7 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
});
this.searchQueries = userQueries.filter((q) => q !== undefined) as UserSearchQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
@ -181,23 +181,21 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
switch (subquery) {
case SubQuery.STATE:
(query as StateQuery).setState(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.DISPLAYNAME:
(query as DisplayNameQuery).setDisplayName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.EMAIL:
(query as EmailQuery).setEmailAddress(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.USERNAME:
(query as UserNameQuery).setUserName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
}
this.filterCount$.next(this.filterCount);
}
public getSubFilter(subquery: SubQuery): any {
@ -235,23 +233,17 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
}
public resetFilter(): void {
this.searchQueries = [];
this.emitFilter();
}
public get filterCount(): number {
return this.searchQueries.length;
}
}

View File

@ -1,21 +1,33 @@
<div class="cnsl-filter-button-wrapper">
<button mat-stroked-button class="filter-toggle cnsl-action-button" (click)="toggleFilter()" cdkOverlayOrigin
#trigger="cdkOverlayOrigin">
<button
mat-stroked-button
class="filter-toggle cnsl-action-button"
(click)="toggleFilter()"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
>
<i class="las la-filter no-margin"></i>
<span>{{ 'ACTIONS.FILTER' | translate }}</span>
<span *ngIf="(filterCount$ | async)" class="filter-count">{{filterCount$ | async}}</span>
<span *ngIf="queryCount" class="filter-count">{{ queryCount }}</span>
<cnsl-action-keys [doNotUseContrast]="true" [type]="ActionKeysType.FILTER" (actionTriggered)="toggleFilter()">
</cnsl-action-keys>
</button>
<ng-template cdkConnectedOverlay [cdkConnectedOverlayHasBackdrop]="true" [flexibleDimensions]="true"
[lockPosition]="true" [cdkConnectedOverlayOffsetY]="10" [cdkConnectedOverlayPositions]="positions"
[cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOpen]="showFilter"
cdkConnectedOverlayBackdropClass="transparent-backdrop" (backdropClick)="showFilter= false"
(detach)="showFilter = false">
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayHasBackdrop]="true"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayPositions]="positions"
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="showFilter"
cdkConnectedOverlayBackdropClass="transparent-backdrop"
(backdropClick)="showFilter = false"
(detach)="showFilter = false"
>
<div class="cnsl-filter-wrapper" cdkTrapFocus>
<div class="filter-top">
<button mat-stroked-button (click)="resetted.emit()">{{'ACTIONS.RESET' |
translate}}</button>
<button mat-stroked-button (click)="resetted.emit()">{{ 'ACTIONS.RESET' | translate }}</button>
<span class="filter-middle">{{ 'FILTER.TITLE' | translate }}</span>
<button mat-raised-button color="primary" (click)="emitFilter()">{{ 'ACTIONS.FINISH' | translate }}</button>
</div>

View File

@ -1,7 +1,7 @@
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
import { Component, EventEmitter, OnDestroy, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
import { Observable, Subject, takeUntil } from 'rxjs';
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
@ -24,15 +24,16 @@ type FilterSearchQueryAsObject =
styleUrls: ['./filter.component.scss'],
})
export class FilterComponent implements OnDestroy {
@Output() public filterChanged: EventEmitter<FilterSearchQuery[] | undefined> = new EventEmitter();
@Output() public filterChanged: EventEmitter<FilterSearchQuery[]> = new EventEmitter();
@Output() public filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
@Output() public resetted: EventEmitter<void> = new EventEmitter();
@Output() public trigger: EventEmitter<void> = new EventEmitter();
private destroy$: Subject<void> = new Subject();
@Input() public queryCount: number = 0;
public filterCount$: BehaviorSubject<number> = new BehaviorSubject(0);
private destroy$: Subject<void> = new Subject();
public filterChanged$: Observable<FilterSearchQuery[]> = this.filterChanged.asObservable();
public showFilter: boolean = false;
public methods: TextQueryMethod[] = [
@ -59,7 +60,6 @@ export class FilterComponent implements OnDestroy {
}
public ngOnDestroy(): void {
this.filterCount$.complete();
this.destroy$.next();
this.destroy$.complete();
}
@ -83,6 +83,7 @@ export class FilterComponent implements OnDestroy {
queryParams: {
['filter']: JSON.stringify(filters),
},
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
});

View File

@ -68,9 +68,16 @@ export class HeaderComponent implements OnDestroy {
}
public get isOnInstance(): boolean {
return (
['/instance', '/views', '/orgs', '/settings', '/failed-events', '/instance/members'].includes(this.router.url) ||
this.router.url.includes('/settings')
);
const pages: string[] = [
'/instance',
'/settings',
'/views',
'/orgs',
'/settings',
'/failed-events',
'/instance/members',
];
return pages.findIndex((p) => this.router.url.includes(p)) > -1;
}
}

View File

@ -1,6 +1,11 @@
<cnsl-refresh-table *ngIf="dataSource" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<cnsl-refresh-table
*ngIf="dataSource"
(refreshed)="changePage()"
[dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp"
[selection]="selection"
[loading]="dataSource?.loading$ | async"
>
<ng-container actions *ngIf="selection.hasValue()">
<ng-content select="[selectactions]"></ng-content>
</ng-container>
@ -13,17 +18,32 @@
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="!canWrite" color="primary" (change)="$event ? masterToggle() : null"
<mat-checkbox
[disabled]="!canWrite"
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
[indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<cnsl-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [avatarUrl]="row.avatarUrl|| ''"
[forColor]="row?.preferredLoginName" [size]="32">
<mat-checkbox
[disabled]="!canWrite"
color="primary"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
>
<cnsl-avatar
*ngIf="row?.displayName && row.firstName && row.lastName; else cog"
class="avatar"
[name]="row.displayName"
[avatarUrl]="row.avatarUrl || ''"
[avatarUrl]="row.avatarUrl || ''"
[forColor]="row?.preferredLoginName"
[size]="32"
>
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar [forColor]="row.preferredLoginName" [isMachine]="true">
@ -37,19 +57,22 @@
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.USERID' | translate }}</th>
<td mat-cell *matCellDef="let member">
{{member.userId}} </td>
{{ member.userId }}
</td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.DISPLAYNAME' | translate }}</th>
<td mat-cell *matCellDef="let member">
{{member.displayName}} </td>
{{ member.displayName }}
</td>
</ng-container>
<ng-container matColumnDef="loginname">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.LOGINNAME' | translate }}</th>
<td mat-cell *matCellDef="let member">
{{member.preferredLoginName}} </td>
{{ member.preferredLoginName }}
</td>
</ng-container>
<ng-container matColumnDef="email">
@ -63,11 +86,18 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let member" class="member-action-tr">
<cnsl-table-actions [hasActions]="true">
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn"
(click)="triggerDeleteMember(member)" mat-icon-button><i class="las la-trash"></i></button>
<button
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="$event.stopPropagation(); triggerDeleteMember(member)"
mat-icon-button
>
<i class="las la-trash"></i>
</button>
<button menuActions mat-menu-item [routerLink]="['/users', member.userId]">
{{'ACTIONS.TABLE.SHOWUSER' | translate : ({value: member.displayName})}}
{{ 'ACTIONS.TABLE.SHOWUSER' | translate: { value: member.displayName } }}
</button>
</cnsl-table-actions>
</td>
@ -77,8 +107,13 @@
<th mat-header-cell *matHeaderCellDef class="role-row">{{ 'ROLESLABEL' | translate }}</th>
<td mat-cell *matCellDef="let member" class="role-row">
<mat-chip-list class="cnsl-chip-list" aria-label="role selection">
<mat-chip class="cnsl-chip" *ngFor="let role of member.rolesList" [removable]="true" [selectable]="false"
(removed)="removeRole(member, role)">
<mat-chip
class="cnsl-chip"
*ngFor="let role of member.rolesList"
[removable]="true"
[selectable]="false"
(removed)="removeRole(member, role)"
>
<div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
<span>{{ role | roletransform }}</span>
<button matChipRemove>
@ -90,13 +125,23 @@
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight pointer" (click)="addRole(member)" mat-row
*matRowDef="let member; columns: displayedColumns;">
</tr>
<tr
class="highlight pointer"
(click)="addRole(member)"
mat-row
*matRowDef="let member; columns: displayedColumns"
></tr>
</table>
</div>
<cnsl-paginator *ngIf="dataSource" class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp"
[pageSize]="INITIALPAGESIZE" [length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]"
(page)="changePage($event)">
<cnsl-paginator
*ngIf="dataSource"
class="paginator"
#paginator
[timestamp]="dataSource?.viewTimestamp"
[pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult"
[pageSizeOptions]="[25, 50, 100, 250]"
(page)="changePage($event)"
>
</cnsl-paginator>
</cnsl-refresh-table>

View File

@ -15,7 +15,14 @@
</a>
</ng-template>
<table [dataSource]="dataSource" mat-table class="table" aria-label="Elements">
<table
[dataSource]="dataSource"
mat-table
class="table"
aria-label="Organizations"
matSort
(matSortChange)="sortChange($event)"
>
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.ACTIVE' | translate }}
@ -51,6 +58,7 @@
</ng-container>
<ng-container matColumnDef="name">
<!-- mat-sort-header -->
<th mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.NAME' | translate }}
</th>

View File

@ -1,9 +1,11 @@
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { Component, Input, ViewChild } from '@angular/core';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, takeUntil } from 'rxjs';
import { Org, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { Org, OrgFieldName, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
@ -38,6 +40,7 @@ export class OrgTableComponent {
public filterOpen: boolean = false;
public OrgState: any = OrgState;
public copied: string = '';
@ViewChild(MatSort) public sort!: MatSort;
private searchQueries: OrgQuery[] = [];
private destroy$: Subject<void> = new Subject();
@ -48,7 +51,12 @@ export class OrgTableComponent {
});
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$));
constructor(private authService: GrpcAuthService, private router: Router, private toast: ToastService) {
constructor(
private authService: GrpcAuthService,
private router: Router,
private toast: ToastService,
private _liveAnnouncer: LiveAnnouncer,
) {
this.requestOrgs$.next({ limit: this.initialLimit, offset: 0, queries: this.searchQueries });
this.authService.getActiveOrg().then((org) => (this.activeOrg = org));
@ -60,7 +68,17 @@ export class OrgTableComponent {
public loadOrgs(request: Request): Observable<Org.AsObject[]> {
this.loadingSubject.next(true);
return from(this.authService.listMyProjectOrgs(request.limit, request.offset, request.queries)).pipe(
let sortingField: OrgFieldName | undefined = undefined;
if (this.sort?.active && this.sort?.direction)
switch (this.sort.active) {
case 'name':
sortingField = OrgFieldName.ORG_FIELD_NAME_NAME;
break;
}
return from(
this.authService.listMyProjectOrgs(request.limit, request.offset, request.queries, sortingField, this.sort?.direction),
).pipe(
map((resp) => {
this.timestamp = resp.details?.viewTimestamp;
this.totalResult = resp.details?.totalResult ?? 0;
@ -86,6 +104,15 @@ export class OrgTableComponent {
});
}
public sortChange(sortState: Sort) {
if (sortState.direction && sortState.active) {
this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
this.refresh();
} else {
this._liveAnnouncer.announce('Sorting cleared');
}
}
public applySearchQuery(searchQueries: OrgQuery[]): void {
this.searchQueries = searchQueries;
this.requestOrgs$.next({

View File

@ -0,0 +1,52 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<h2>{{ 'POLICY.DOMAIN_POLICY.TITLE' | translate }}</h2>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<!-- <ng-template cnslHasRole [hasRole]="['domain.policy.delete']"> -->
<button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
<!-- </ng-template> -->
<!-- [disabled]="(['domain.policy.write'] | hasRole | async) === false" -->
<div class="content" *ngIf="domainData">
<div class="row">
<mat-checkbox color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="domainData.userLoginMustBeDomain">
{{ 'POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate }}
</mat-checkbox>
</div>
<!-- [disabled]="(['domain.policy.write'] | hasRole | async) === false" -->
<div class="row">
<mat-checkbox color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="domainData.validateOrgDomains">
{{ 'POLICY.DATA.VALIDATEORGDOMAINS' | translate }}
</mat-checkbox>
</div>
<div class="row">
<mat-checkbox
color="primary"
name="hasNumber"
ngDefaultControl
[(ngModel)]="domainData.smtpSenderAddressMatchesInstanceDomain"
>
{{ 'POLICY.DATA.SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN' | translate }}
</mat-checkbox>
</div>
</div>
<!-- [disabled]="(['domain.policy.write'] | hasRole | async) === false" -->
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DomainPolicyComponent } from './domain-policy.component';
describe('DomainPolicyComponent', () => {
let component: DomainPolicyComponent;
let fixture: ComponentFixture<DomainPolicyComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DomainPolicyComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DomainPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,32 +1,43 @@
import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { Subscription } from 'rxjs';
import { GetCustomOrgIAMPolicyResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import {
AddCustomDomainPolicyRequest,
GetCustomOrgIAMPolicyResponse,
UpdateDomainPolicyRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { GetOrgIAMPolicyResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { OrgIAMPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { DomainPolicy, OrgIAMPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageLocation, StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'cnsl-org-iam-policy',
templateUrl: './org-iam-policy.component.html',
styleUrls: ['./org-iam-policy.component.scss'],
selector: 'cnsl-domain-policy',
templateUrl: './domain-policy.component.html',
styleUrls: ['./domain-policy.component.scss'],
})
export class OrgIamPolicyComponent implements OnInit, OnDestroy {
export class DomainPolicyComponent implements OnInit, OnDestroy {
private managementService!: ManagementService;
@Input() public serviceType!: PolicyComponentServiceType;
public iamData!: OrgIAMPolicy.AsObject;
public domainData!: DomainPolicy.AsObject;
public loading: boolean = false;
private sub: Subscription = new Subscription();
private org!: Org.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(private toast: ToastService, private injector: Injector, private adminService: AdminService) {}
constructor(
private toast: ToastService,
private injector: Injector,
private adminService: AdminService,
private storageService: StorageService,
) {}
ngOnInit(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
@ -40,22 +51,32 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
}
public fetchData(): void {
this.getData().then((resp) => {
this.loading = true;
this.getData()
.then((resp) => {
this.loading = false;
if (resp?.policy) {
this.iamData = resp.policy;
this.domainData = resp.policy;
}
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
}
private async getData(): Promise<GetCustomOrgIAMPolicyResponse.AsObject | GetOrgIAMPolicyResponse.AsObject | any> {
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org?.id) {
this.org = org;
}
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return this.managementService.getOrgIAMPolicy();
return this.managementService.getDomainPolicy();
case PolicyComponentServiceType.ADMIN:
if (this.org?.id) {
return this.adminService.getCustomOrgIAMPolicy(this.org.id);
}
break;
return this.adminService.getCustomDomainPolicy(this.org.id);
default:
return Promise.reject();
}
@ -64,23 +85,33 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
public savePolicy(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.iamData as OrgIAMPolicy.AsObject).isDefault) {
if ((this.domainData as OrgIAMPolicy.AsObject).isDefault) {
const req = new AddCustomDomainPolicyRequest();
req.setOrgId(this.org.id);
req.setUserLoginMustBeDomain(this.domainData.userLoginMustBeDomain);
req.setValidateOrgDomains(this.domainData.validateOrgDomains);
req.setSmtpSenderAddressMatchesInstanceDomain(this.domainData.smtpSenderAddressMatchesInstanceDomain);
this.adminService
.addCustomOrgIAMPolicy(this.org.id, this.iamData.userLoginMustBeDomain)
.addCustomDomainPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
});
break;
} else {
const req = new AddCustomDomainPolicyRequest();
req.setOrgId(this.org.id);
req.setUserLoginMustBeDomain(this.domainData.userLoginMustBeDomain);
req.setValidateOrgDomains(this.domainData.validateOrgDomains);
req.setSmtpSenderAddressMatchesInstanceDomain(this.domainData.smtpSenderAddressMatchesInstanceDomain);
this.adminService
.updateCustomOrgIAMPolicy(this.org.id, this.iamData.userLoginMustBeDomain)
.updateCustomDomainPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
@ -88,12 +119,15 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
break;
}
case PolicyComponentServiceType.ADMIN:
// update Default org iam policy?
const req = new UpdateDomainPolicyRequest();
req.setUserLoginMustBeDomain(this.domainData.userLoginMustBeDomain);
req.setValidateOrgDomains(this.domainData.validateOrgDomains);
req.setSmtpSenderAddressMatchesInstanceDomain(this.domainData.smtpSenderAddressMatchesInstanceDomain);
this.adminService
.updateOrgIAMPolicy(this.iamData.userLoginMustBeDomain)
.updateDomainPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
@ -105,7 +139,7 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.adminService
.resetCustomOrgIAMPolicyToDefault(this.org.id)
.resetCustomDomainPolicyToDefault(this.org.id)
.then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
@ -119,8 +153,8 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
}
public get isDefault(): boolean {
if (this.iamData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.iamData as OrgIAMPolicy.AsObject).isDefault;
if (this.domainData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.domainData as OrgIAMPolicy.AsObject).isDefault;
} else {
return false;
}

View File

@ -2,8 +2,9 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
@ -13,27 +14,26 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
import { CardModule } from '../../card/card.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { OrgIamPolicyRoutingModule } from './org-iam-policy-routing.module';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
import { DomainPolicyComponent } from './domain-policy.component';
@NgModule({
declarations: [OrgIamPolicyComponent],
declarations: [DomainPolicyComponent],
imports: [
OrgIamPolicyRoutingModule,
CommonModule,
FormsModule,
CardModule,
InputModule,
MatButtonModule,
HasRolePipeModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatProgressSpinnerModule,
MatTooltipModule,
InfoSectionModule,
MatCheckboxModule,
TranslateModule,
DetailLayoutModule,
],
exports: [OrgIamPolicyComponent],
exports: [DomainPolicyComponent],
})
export class OrgIamPolicyModule {}
export class DomainPolicyModule {}

View File

@ -45,15 +45,9 @@
<p class="cnsl-secondary-text">{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}</p>
<div *ngIf="loginData" class="login-policy-row">
<mat-slide-toggle
card-actions
class="login-policy-toggle"
color="primary"
ngDefaultControl
[(ngModel)]="loginData.forceMfa"
>
<mat-checkbox card-actions class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.forceMfa">
{{ 'POLICY.DATA.FORCEMFA' | translate }}
</mat-slide-toggle>
</mat-checkbox>
</div>
<cnsl-card class="max-card-width">
<cnsl-mfa-table
@ -77,11 +71,60 @@
<br />
<h2>{{ 'POLICY.LOGIN_POLICY.LIFETIMEDURATIONS' | translate }}</h2>
<form class="lifetime-form" (ngSubmit)="savePolicy()" [formGroup]="lifetimeForm" autocomplete="off">
<cnsl-form-field class="lifetime-form-field" label="Password Check Lifetime" required="true">
<cnsl-label
>{{ 'POLICY.DATA.PASSWORDCHECKLIFETIME' | translate }}&nbsp;<strong
>({{ 'POLICY.DATA.INHOURS' | translate }})</strong
></cnsl-label
>
<input cnslInput type="number" name="passwordCheckLifetime" formControlName="passwordCheckLifetime" />
</cnsl-form-field>
<cnsl-form-field class="lifetime-form-field" label="external Login Check Lifetime" required="true">
<cnsl-label
>{{ 'POLICY.DATA.EXTERNALLOGINCHECKLIFETIME' | translate }}&nbsp;<strong
>({{ 'POLICY.DATA.INHOURS' | translate }})</strong
></cnsl-label
>
<input cnslInput type="number" name="externalLoginCheckLifetime" formControlName="externalLoginCheckLifetime" />
</cnsl-form-field>
<cnsl-form-field class="lifetime-form-field" label="MFA Init Skip Lifetime" required="true">
<cnsl-label
>{{ 'POLICY.DATA.MFAINITSKIPLIFETIME' | translate }}&nbsp;<strong
>({{ 'POLICY.DATA.INHOURS' | translate }})</strong
></cnsl-label
>
<input cnslInput type="number" name="mfaInitSkipLifetime" formControlName="mfaInitSkipLifetime" />
</cnsl-form-field>
<cnsl-form-field class="lifetime-form-field" label="Second Factor Check Lifetime" required="true">
<cnsl-label
>{{ 'POLICY.DATA.SECONDFACTORCHECKLIFETIME' | translate }}&nbsp;
<strong>({{ 'POLICY.DATA.INHOURS' | translate }})</strong></cnsl-label
>
<input cnslInput type="number" name="secondFactorCheckLifetime" formControlName="secondFactorCheckLifetime" />
</cnsl-form-field>
<cnsl-form-field class="lifetime-form-field" label="Multi Factor Check Lifetime" required="true">
<cnsl-label
>{{ 'POLICY.DATA.MULTIFACTORCHECKLIFETIME' | translate }}&nbsp;
<strong>({{ 'POLICY.DATA.INHOURS' | translate }})</strong></cnsl-label
>
<input cnslInput type="number" name="multiFactorCheckLifetime" formControlName="multiFactorCheckLifetime" />
</cnsl-form-field>
</form>
<br />
<h2>{{ 'POLICY.LOGIN_POLICY.ADVANCED' | translate }}</h2>
<cnsl-card class="max-card-width login-policy-content" *ngIf="loginData">
<div class="max-card-width login-policy-content" *ngIf="loginData">
<div class="login-policy-row">
<mat-slide-toggle
<mat-checkbox
class="login-policy-toggle"
color="primary"
matTooltip="{{ 'POLICY.DATA.FORCEMFA_DESC' | translate }}"
@ -89,16 +132,16 @@
[(ngModel)]="loginData.allowUsernamePassword"
>
{{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate }}
</mat-slide-toggle>
</mat-checkbox>
<!-- <cnsl-info-section class="info">
{{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate }}
</cnsl-info-section> -->
</div>
<div class="login-policy-row">
<mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.allowRegister">
<mat-checkbox class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.allowRegister">
{{ 'POLICY.DATA.ALLOWREGISTER' | translate }}
</mat-slide-toggle>
</mat-checkbox>
<!-- <ng-template #regInfo>
<cnsl-info-section class="info">
@ -107,9 +150,9 @@
</ng-template> -->
</div>
<div class="login-policy-row">
<mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.allowExternalIdp">
<mat-checkbox class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.allowExternalIdp">
{{ 'POLICY.DATA.ALLOWEXTERNALIDP' | translate }}
</mat-slide-toggle>
</mat-checkbox>
<!-- <ng-template #idpInfo>
<cnsl-info-section class="info">
@ -119,9 +162,9 @@
</div>
<div class="login-policy-row">
<mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.hidePasswordReset">
<mat-checkbox class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.hidePasswordReset">
{{ 'POLICY.DATA.HIDEPASSWORDRESET' | translate }}
</mat-slide-toggle>
</mat-checkbox>
<!-- <ng-template #passwordResetInfo>
<cnsl-info-section class="info">
@ -131,9 +174,14 @@
</div>
<div class="login-policy-row">
<mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.ignoreUnknownUsernames">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
[(ngModel)]="loginData.ignoreUnknownUsernames"
>
{{ 'POLICY.DATA.IGNOREUNKNOWNUSERNAMES' | translate }}
</mat-slide-toggle>
</mat-checkbox>
</div>
<div class="login-policy-row">
@ -142,19 +190,13 @@
<input cnslInput placeholder="https://" [(ngModel)]="loginData.defaultRedirectUri" />
</cnsl-form-field>
</div>
</cnsl-card>
<div class="login-policy-btn-container">
<button class="login-policy-save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT">
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault">
<ng-template cnslHasRole [hasRole]="['policy.delete']">
<button
*ngIf="!isDefault"
color="primary"
class="loginpolicy-reset-button"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
@ -164,3 +206,11 @@
</button>
</ng-template>
</ng-container>
<br />
<div class="login-policy-btn-container">
<button class="login-policy-save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@ -12,6 +12,20 @@
display: block;
}
.lifetime-form {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: 1rem;
@media only screen and (max-width: 950px) {
grid-template-columns: 1fr 1fr;
}
@media only screen and (max-width: 500px) {
grid-template-columns: 1fr;
}
}
.login-policy-content {
.login-policy-row {
padding-bottom: 0.5rem;
@ -26,6 +40,11 @@
}
}
.loginpolicy-reset-button {
margin: 1rem 0;
display: block;
}
.login-policy-btn-container {
display: flex;
justify-content: flex-start;

View File

@ -1,4 +1,6 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import {
GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
UpdateLoginPolicyRequest,
@ -37,16 +39,54 @@ export class LoginPolicyComponent implements OnInit {
public loading: boolean = false;
public InfoSectionType: any = InfoSectionType;
public PasswordlessType: any = PasswordlessType;
constructor(private toast: ToastService, private injector: Injector) {}
public lifetimeForm!: FormGroup;
constructor(private toast: ToastService, private injector: Injector, private fb: FormBuilder) {
this.lifetimeForm = this.fb.group({
passwordCheckLifetime: [240, [Validators.required]],
externalLoginCheckLifetime: [12, [Validators.required]],
mfaInitSkipLifetime: [720, [Validators.required]],
secondFactorCheckLifetime: [12, [Validators.required]],
multiFactorCheckLifetime: [12, [Validators.required]],
});
}
private fetchData(): void {
this.getData().then((resp) => {
this.getData()
.then((resp) => {
console.log(resp);
if (resp.policy) {
this.loginData = resp.policy;
this.loading = false;
this.passwordCheckLifetime?.setValue(
this.loginData.passwordCheckLifetime?.seconds ? this.loginData.passwordCheckLifetime?.seconds / 60 / 60 : 240,
);
this.externalLoginCheckLifetime?.setValue(
this.loginData.externalLoginCheckLifetime?.seconds
? this.loginData.externalLoginCheckLifetime?.seconds / 60 / 60
: 12,
);
this.mfaInitSkipLifetime?.setValue(
this.loginData.mfaInitSkipLifetime?.seconds ? this.loginData.mfaInitSkipLifetime?.seconds / 60 / 60 : 720,
);
this.secondFactorCheckLifetime?.setValue(
this.loginData.secondFactorCheckLifetime?.seconds
? this.loginData.secondFactorCheckLifetime?.seconds / 60 / 60
: 12,
);
this.multiFactorCheckLifetime?.setValue(
this.loginData.multiFactorCheckLifetime?.seconds
? this.loginData.multiFactorCheckLifetime?.seconds / 60 / 60
: 12,
);
}
});
})
.catch(this.toast.showError);
}
public ngOnInit(): void {
@ -88,6 +128,22 @@ export class LoginPolicyComponent implements OnInit {
mgmtreq.setForceMfa(this.loginData.forceMfa);
mgmtreq.setPasswordlessType(this.loginData.passwordlessType);
mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset);
const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60);
mgmtreq.setPasswordCheckLifetime(pcl);
const elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 12) * 60 * 60);
mgmtreq.setExternalLoginCheckLifetime(elcl);
const misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 720) * 60 * 60);
mgmtreq.setMfaInitSkipLifetime(misl);
const sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 12) * 60 * 60);
mgmtreq.setSecondFactorCheckLifetime(sfcl);
const mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 12) * 60 * 60);
mgmtreq.setMultiFactorCheckLifetime(mficl);
mgmtreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
@ -107,6 +163,21 @@ export class LoginPolicyComponent implements OnInit {
adminreq.setForceMfa(this.loginData.forceMfa);
adminreq.setPasswordlessType(this.loginData.passwordlessType);
adminreq.setHidePasswordReset(this.loginData.hidePasswordReset);
const admin_pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60);
adminreq.setPasswordCheckLifetime(admin_pcl);
const admin_elcl = new Duration().setSeconds((this.externalLoginCheckLifetime?.value ?? 12) * 60 * 60);
adminreq.setExternalLoginCheckLifetime(admin_elcl);
const admin_misl = new Duration().setSeconds((this.mfaInitSkipLifetime?.value ?? 720) * 60 * 60);
adminreq.setMfaInitSkipLifetime(admin_misl);
const admin_sfcl = new Duration().setSeconds((this.secondFactorCheckLifetime?.value ?? 12) * 60 * 60);
adminreq.setSecondFactorCheckLifetime(admin_sfcl);
const admin_mficl = new Duration().setSeconds((this.multiFactorCheckLifetime?.value ?? 12) * 60 * 60);
adminreq.setMultiFactorCheckLifetime(admin_mficl);
adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
adminreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
// adminreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime);
@ -153,4 +224,24 @@ export class LoginPolicyComponent implements OnInit {
return false;
}
}
public get passwordCheckLifetime(): AbstractControl | null {
return this.lifetimeForm.get('passwordCheckLifetime');
}
public get externalLoginCheckLifetime(): AbstractControl | null {
return this.lifetimeForm.get('externalLoginCheckLifetime');
}
public get mfaInitSkipLifetime(): AbstractControl | null {
return this.lifetimeForm.get('mfaInitSkipLifetime');
}
public get secondFactorCheckLifetime(): AbstractControl | null {
return this.lifetimeForm.get('secondFactorCheckLifetime');
}
public get multiFactorCheckLifetime(): AbstractControl | null {
return this.lifetimeForm.get('multiFactorCheckLifetime');
}
}

View File

@ -1,7 +1,8 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRippleModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@ -30,6 +31,7 @@ import { MfaTableComponent } from './mfa-table/mfa-table.component';
InfoSectionModule,
FormsModule,
CardModule,
ReactiveFormsModule,
InputModule,
MatIconModule,
MatButtonModule,
@ -37,6 +39,7 @@ import { MfaTableComponent } from './mfa-table/mfa-table.component';
HasRoleModule,
MatDialogModule,
HasRolePipeModule,
MatCheckboxModule,
MatTooltipModule,
DetailLayoutModule,
MatProgressSpinnerModule,

View File

@ -1,12 +1,16 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
<mat-spinner diameter="30" *ngIf="smtpLoading || smsProvidersLoading" color="primary"></mat-spinner>
</div>
<h2>{{ 'SETTING.SMTP.TITLE' | translate }}</h2>
<cnsl-info-section *ngIf="!form.valid" class="info-section-warn" [fitWidth]="true" [type]="InfoSectionType.ALERT">{{
'SETTING.SMTP.REQUIREDWARN' | translate
}}</cnsl-info-section>
<cnsl-info-section
*ngIf="!smtpLoading && !form.valid"
class="info-section-warn"
[fitWidth]="true"
[type]="InfoSectionType.ALERT"
>{{ 'SETTING.SMTP.REQUIREDWARN' | translate }}</cnsl-info-section
>
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
@ -33,17 +37,9 @@
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Password" required="true">
<cnsl-label>{{ 'SETTING.SMTP.PASSWORD' | translate }}</cnsl-label>
<input
id="smtp-password"
cnslInput
name="smtp-password"
autocomplete="smtp-password"
type="password"
formControlName="password"
/>
</cnsl-form-field>
<button class="set-password-btn" (click)="setSMTPPassword()" mat-stroked-button>
{{ 'SETTING.SMTP.SETPASSWORD' | translate }}
</button>
<div class="general-btn-container">
<button class="save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>

View File

@ -18,6 +18,10 @@
margin: 1rem 0;
}
.set-password-btn {
margin-bottom: 1rem;
}
.general-btn-container {
display: flex;
justify-content: flex-start;

View File

@ -14,6 +14,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { InfoSectionType } from '../../info-section/info-section.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { SMTPPasswordDialogComponent } from './smtp-password-dialog/smtp-password-dialog.component';
@Component({
selector: 'cnsl-notification-settings',
@ -25,7 +26,12 @@ export class NotificationSettingsComponent implements OnInit {
public smsProviders: SMSProvider.AsObject[] = [];
public logNotificationProvider!: DebugNotificationProvider.AsObject;
public fileNotificationProvider!: DebugNotificationProvider.AsObject;
public loading: boolean = false;
public smtpLoading: boolean = false;
public smsProvidersLoading: boolean = false;
public logProviderLoading: boolean = false;
public fileProviderLoading: boolean = false;
public form!: FormGroup;
public SMSProviderConfigState: any = SMSProviderConfigState;
@ -45,7 +51,6 @@ export class NotificationSettingsComponent implements OnInit {
tls: [true, [Validators.required]],
host: ['', [Validators.required]],
user: ['', [Validators.required]],
password: ['', [Validators.required]],
});
}
@ -54,47 +59,61 @@ export class NotificationSettingsComponent implements OnInit {
}
private fetchData(): void {
this.smtpLoading = true;
this.service
.getSMTPConfig()
.then((smtpConfig) => {
this.smtpLoading = false;
if (smtpConfig.smtpConfig) {
this.form.patchValue(smtpConfig.smtpConfig);
}
})
.catch((error) => {
this.smtpLoading = false;
if (error && error.code === 5) {
console.log(error);
}
});
this.service.listSMSProviders().then((smsProviders) => {
this.smsProvidersLoading = true;
this.service
.listSMSProviders()
.then((smsProviders) => {
this.smsProvidersLoading = false;
if (smsProviders.resultList) {
this.smsProviders = smsProviders.resultList;
console.log(this.smsProviders);
}
})
.catch((error) => {
this.smsProvidersLoading = false;
this.toast.showError(error);
});
this.logProviderLoading = true;
this.service
.getLogNotificationProvider()
.then((logNotificationProvider) => {
this.logProviderLoading = false;
if (logNotificationProvider.provider) {
this.logNotificationProvider = logNotificationProvider.provider;
}
})
.catch((error) => {
this.logProviderLoading = false;
this.toast.showError(error);
});
this.fileProviderLoading = true;
this.service
.getFileSystemNotificationProvider()
.then((fileNotificationProvider) => {
this.fileProviderLoading = false;
if (fileNotificationProvider.provider) {
console.log(fileNotificationProvider);
this.fileNotificationProvider = fileNotificationProvider.provider;
}
})
.catch((error) => {
console.log('hehe');
this.fileProviderLoading = false;
this.toast.showError(error);
});
}
@ -107,16 +126,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
return this.service.updateSMTPConfig(req).then(() => {
let passwordReq: UpdateSMTPConfigPasswordRequest;
if (this.password) {
passwordReq = new UpdateSMTPConfigPasswordRequest();
passwordReq.setPassword(this.password.value);
return this.service.updateSMTPConfigPassword(passwordReq);
} else {
return;
}
});
return this.service.updateSMTPConfig(req).catch(this.toast.showError);
}
public savePolicy(): void {
@ -125,7 +135,6 @@ export class NotificationSettingsComponent implements OnInit {
prom
.then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
@ -155,6 +164,28 @@ export class NotificationSettingsComponent implements OnInit {
});
}
public setSMTPPassword(): void {
const dialogRef = this.dialog.open(SMTPPasswordDialogComponent, {
width: '400px',
});
dialogRef.afterClosed().subscribe((password: string) => {
if (password) {
const passwordReq = new UpdateSMTPConfigPasswordRequest();
passwordReq.setPassword(password);
this.service
.updateSMTPConfigPassword(passwordReq)
.then(() => {
this.toast.showInfo('SETTING.SMTP.PASSWORDSET', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio);
}
@ -178,8 +209,4 @@ export class NotificationSettingsComponent implements OnInit {
public get host(): AbstractControl | null {
return this.form.get('host');
}
public get password(): AbstractControl | null {
return this.form.get('password');
}
}

View File

@ -14,9 +14,10 @@ import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component';
import { SMTPPasswordDialogComponent } from './smtp-password-dialog/smtp-password-dialog.component';
@NgModule({
declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent],
declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent, SMTPPasswordDialogComponent],
imports: [
CommonModule,
CardModule,

View File

@ -0,0 +1,25 @@
<h1 mat-dialog-title>
<span>{{ 'SETTING.SMTP.SETPASSWORD' | translate }} {{ data?.number }}</span>
</h1>
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'SETTING.SMTP.PASSWORD' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="password" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{ 'ACTIONS.CLOSE' | translate }}
</button>
<button
[disabled]="!password"
cdkFocusInitial
color="primary"
mat-raised-button
class="ok-button"
(click)="closeDialog(password)"
>
{{ 'ACTIONS.OK' | translate }}
</button>
</div>

View File

@ -0,0 +1,25 @@
h1 {
font-size: 1.5rem;
margin: 0;
}
.desc {
font-size: 14px;
}
.formfield {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: 0.5rem;
}
button {
border-radius: 0.5rem;
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SMTPPasswordDialogComponent } from './smtp-password-dialog.component';
describe('CodeDialogComponent', () => {
let component: SMTPPasswordDialogComponent;
let fixture: ComponentFixture<SMTPPasswordDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SMTPPasswordDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SMTPPasswordDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'cnsl-smtp-password-dialog',
templateUrl: './smtp-password-dialog.component.html',
styleUrls: ['./smtp-password-dialog.component.scss'],
})
export class SMTPPasswordDialogComponent {
public password: string = '';
constructor(public dialogRef: MatDialogRef<SMTPPasswordDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {}
closeDialog(password: string = ''): void {
this.dialogRef.close(password);
}
}

View File

@ -1,20 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
const routes: Routes = [
{
path: '',
component: OrgIamPolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrgIamPolicyRoutingModule { }

View File

@ -1,41 +0,0 @@
<h2>{{ 'POLICY.IAM_POLICY.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'POLICY.IAM_POLICY.DESCRIPTION' | translate }}</p>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<ng-template cnslHasRole [hasRole]="['iam.policy.delete']">
<button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
<div class="content" *ngIf="iamData">
<div class="row">
<mat-slide-toggle
color="primary"
name="hasNumber"
ngDefaultControl
[disabled]="(['iam.policy.write'] | hasRole | async) === false"
[(ngModel)]="iamData.userLoginMustBeDomain"
>
{{ 'POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate }}
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">
<button
(click)="savePolicy()"
[disabled]="(['iam.policy.write'] | hasRole | async) === false"
color="primary"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
describe('OrgIamPolicyComponent', () => {
let component: OrgIamPolicyComponent;
let fixture: ComponentFixture<OrgIamPolicyComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgIamPolicyComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgIamPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -40,7 +40,7 @@
</div>
</div>
<div class="row">
<mat-slide-toggle
<mat-checkbox
class="slide-toggle"
color="primary"
name="hasNumber"
@ -52,10 +52,10 @@
<mat-icon class="icon" svgIcon="mdi_numeric"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASNUMBER' | translate }}</span>
</div>
</mat-slide-toggle>
</mat-checkbox>
</div>
<div class="row">
<mat-slide-toggle
<mat-checkbox
class="slide-toggle"
color="primary"
name="hasSymbol"
@ -67,10 +67,10 @@
<mat-icon class="icon" svgIcon="mdi_symbol"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASSYMBOL' | translate }}</span>
</div>
</mat-slide-toggle>
</mat-checkbox>
</div>
<div class="row">
<mat-slide-toggle
<mat-checkbox
class="slide-toggle"
color="primary"
name="hasLowercase"
@ -82,10 +82,10 @@
<mat-icon class="icon" svgIcon="mdi_format-letter-case-lower"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASLOWERCASE' | translate }}</span>
</div>
</mat-slide-toggle>
</mat-checkbox>
</div>
<div class="row">
<mat-slide-toggle
<mat-checkbox
class="slide-toggle"
color="primary"
name="hasUppercase"
@ -97,7 +97,7 @@
<mat-icon class="icon" svgIcon="mdi_format-letter-case-upper"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASUPPERCASE' | translate }}</span>
</div>
</mat-slide-toggle>
</mat-checkbox>
</div>
</div>
</cnsl-card>

View File

@ -32,12 +32,12 @@
}
.slide-toggle {
margin-left: 1.75rem;
margin-left: 2.25rem;
.slide-toggle-row {
display: flex;
align-items: center;
margin-left: 1.5rem;
margin-left: 2.25rem;
.icon {
margin-right: 1rem;

View File

@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
@ -25,10 +25,10 @@ import { PasswordComplexityPolicyComponent } from './password-complexity-policy.
FormsModule,
InputModule,
MatButtonModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatTooltipModule,
MatCheckboxModule,
HasRolePipeModule,
TranslateModule,
DetailLayoutModule,

View File

@ -2,18 +2,30 @@
<p class="name cnsl-secondary-text">{{ name }}</p>
<div class="wrapper">
<button [disabled]="disabled" class="color-shadow" [style.background-color]="previewColor"
(click)="isOpen = !isOpen" cdkOverlayOrigin #trigger="cdkOverlayOrigin"
matTooltip="{{'ACTIONS.SET' | translate}}"> </button>
<button
[disabled]="disabled"
class="color-shadow"
[style.background-color]="previewColor"
(click)="isOpen = !isOpen"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
matTooltip="{{ 'ACTIONS.SET' | translate }}"
></button>
<div class="hex-wrapper" [ngClass]="{'pointer': !disabled}" (click)="disabled ? null : (isOpen = !isOpen)">
<div class="hex-wrapper" [ngClass]="{ pointer: !disabled }" (click)="disabled ? null : (isOpen = !isOpen)">
<i class="las la-hashtag"></i>
<span class="hex">{{ previewColorCropped }}</span>
</div>
</div>
</div>
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOpen]="isOpen"
(overlayOutsideClick)="isOpen = false">
<ng-template
cdkConnectedOverlay
(detach)="submitColor()"
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
(overlayOutsideClick)="isOpen = false"
>
<color-chrome class="picker" [color]="previewColor" (onChangeComplete)="changeComplete($event)"> </color-chrome>
<button class="close-icon" mat-mini-fab color="primary" (click)="submitColor()"><mat-icon>check</mat-icon></button>
</ng-template>

View File

@ -42,12 +42,20 @@
}
.picker {
margin: 1rem 0;
position: relative;
}
.close-icon {
position: absolute;
top: 0;
right: 0;
transform: translateX(50%) translateY(-50%);
}
// stylelint-disable
::ng-deep .chrome-picker {
border-radius: 0.5rem !important;
box-shadow: 0 0 1px #00000020, 0 1px 3px #00000020 !important;
}
::ng-deep .saturation {

View File

@ -123,7 +123,12 @@ export class ColorComponent implements OnInit {
}
public changeComplete(event: ColorEvent): void {
this.emitPreview(event.color.hex);
this.color = event.color.hex;
}
public submitColor(): void {
this.emitPreview(this.color);
this.isOpen = false;
}
public get previewColorCropped(): string {

View File

@ -1,17 +1,26 @@
@use '@angular/material' as mat;
@mixin preview-theme($theme) {
@import '../../../../../styles/input.scss';
@import '../../../label/label.component.scss';
@mixin preview-theme($show-dark) {
$theme: if($show-dark, $caos-dark-app-theme, $caos-light-app-theme);
@include input-theme($theme);
@include cnsl-label-theme($theme);
@include mat.all-component-themes($theme);
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$p-border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
.preview {
pointer-events: none;
border-radius: 0.5rem;
transform: scale(0.9);
color: map-get($foreground, base);
* {
pointer-events: none;
@ -77,18 +86,6 @@
}
}
}
&.light {
.login-wrapper *:not(.cnsl-label):not(.mat-button-wrapper) {
color: #000;
}
}
&.dark {
.login-wrapper *:not(.cnsl-label):not(.mat-button-wrapper) {
color: #fff;
}
}
}
}
}

View File

@ -34,6 +34,10 @@
<mat-button-toggle [value]="View.PREVIEW">
<div class="toggle-row">
<span>{{ 'POLICY.PRIVATELABELING.VIEWS.PREVIEW' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'POLICY.PRIVATELABELING.PREVIEW_DESCRIPTION' | translate }}"
></i>
<div *ngIf="view === View.PREVIEW" class="current-dot"></div>
</div>
</mat-button-toggle>
@ -71,10 +75,6 @@
</button>
</div>
<cnsl-info-section *ngIf="view === View.PREVIEW" class="desc cnsl-secondary-text">
{{ 'POLICY.PRIVATELABELING.PREVIEW_DESCRIPTION' | translate }}
</cnsl-info-section>
<div *ngIf="previewData && data" class="lab-policy-content">
<mat-accordion class="settings">
<mat-expansion-panel class="expansion">
@ -223,7 +223,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.BACKGROUNDDARK"
(previewChanged)="previewData.backgroundColorDark = $event; savePolicy()"
(previewChanged)="previewData.backgroundColorDark !== $event ? setDarkBackgroundColorAndSave($event) : null"
name="Background Color"
[color]="data.backgroundColorDark"
[previewColor]="previewData.backgroundColorDark"
@ -234,7 +234,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.PRIMARY"
(previewChanged)="previewData.primaryColorDark = $event; savePolicy()"
(previewChanged)="previewData.primaryColorDark !== $event ? setDarkPrimaryColorAndSave($event) : null"
name="Primary Color"
[color]="data.primaryColorDark"
[previewColor]="previewData.primaryColorDark"
@ -246,7 +246,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.WARN"
(previewChanged)="previewData.warnColorDark = $event; savePolicy()"
(previewChanged)="previewData.warnColorDark !== $event ? setDarkWarnColorAndSave($event) : null"
name="Warn Color"
[color]="data.warnColorDark"
[previewColor]="previewData.warnColorDark"
@ -258,7 +258,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.FONTDARK"
(previewChanged)="previewData.fontColorDark = $event; savePolicy()"
(previewChanged)="previewData.fontColorDark !== $event ? setDarkFontColorAndSave($event) : null"
name="Font Color"
[color]="data.fontColorDark"
[previewColor]="previewData.fontColorDark"
@ -274,7 +274,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.BACKGROUNDLIGHT"
(previewChanged)="previewData.backgroundColor = $event; savePolicy()"
(previewChanged)="previewData.backgroundColor !== $event ? setBackgroundColorAndSave($event) : null"
name="Background Color"
[color]="data.backgroundColor"
[previewColor]="previewData.backgroundColor"
@ -285,7 +285,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.PRIMARY"
(previewChanged)="previewData.primaryColor = $event; savePolicy()"
(previewChanged)="previewData.primaryColor !== $event ? setPrimaryColorAndSave($event) : null"
name="Primary Color"
[color]="data.primaryColor"
[previewColor]="previewData.primaryColor"
@ -298,7 +298,7 @@
[disabled]="view === View.CURRENT"
[colorType]="ColorType.WARN"
name="Warn Color"
(previewChanged)="previewData.warnColor = $event; savePolicy()"
(previewChanged)="previewData.warnColor !== $event ? setWarnColorAndSave($event) : null"
[color]="data.warnColor"
[previewColor]="previewData.warnColor"
></cnsl-color>
@ -308,7 +308,7 @@
<cnsl-color
[disabled]="view === View.CURRENT"
[colorType]="ColorType.FONTLIGHT"
(previewChanged)="previewData.fontColor = $event; savePolicy()"
(previewChanged)="previewData.fontColor !== $event ? setFontColorAndSave($event) : null"
name="Font Color"
[color]="data.fontColor"
[previewColor]="previewData.fontColor"
@ -420,6 +420,7 @@
[refresh]="refreshPreview"
[theme]="theme"
class="preview"
[ngClass]="{ darkmode: theme === Theme.DARK, lightmode: theme === Theme.LIGHT }"
[policy]="view === View.PREVIEW ? previewData : data"
>
</cnsl-preview>

View File

@ -1,5 +1,7 @@
@use '@angular/material' as mat;
@import './preview/preview.component.scss';
@mixin private-label-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
@ -68,6 +70,12 @@
margin-right: 0.5rem;
}
.info-i {
font-size: 1.2rem;
margin-left: 0.5rem;
margin-right: 0;
}
.current-dot {
height: 8px;
width: 8px;
@ -314,6 +322,16 @@
min-height: 600px;
width: fit-content;
margin: auto;
.preview {
&.lightmode {
@include preview-theme(false);
}
&.darkmode {
@include preview-theme(true);
}
}
}
}
}

View File

@ -473,6 +473,46 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
}
}
public setDarkBackgroundColorAndSave($event: string): void {
this.previewData.backgroundColorDark = $event;
this.savePolicy();
}
public setDarkPrimaryColorAndSave($event: string): void {
this.previewData.primaryColorDark = $event;
this.savePolicy();
}
public setDarkWarnColorAndSave($event: string): void {
this.previewData.warnColorDark = $event;
this.savePolicy();
}
public setDarkFontColorAndSave($event: string): void {
this.previewData.fontColorDark = $event;
this.savePolicy();
}
public setBackgroundColorAndSave($event: string): void {
this.previewData.backgroundColor = $event;
this.savePolicy();
}
public setPrimaryColorAndSave($event: string): void {
this.previewData.primaryColor = $event;
this.savePolicy();
}
public setWarnColorAndSave($event: string): void {
this.previewData.warnColor = $event;
this.savePolicy();
}
public setFontColorAndSave($event: string): void {
this.previewData.fontColor = $event;
this.savePolicy();
}
public overwriteValues(req: AddCustomLabelPolicyRequest | UpdateCustomLabelPolicyRequest): void {
req.setBackgroundColorDark(this.previewData.backgroundColorDark);
req.setBackgroundColor(this.previewData.backgroundColor);

View File

@ -1,34 +1,88 @@
<cnsl-detail-layout *ngIf="project" [hasBackButton]="true"
title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}">
<cnsl-detail-layout
*ngIf="project"
[hasBackButton]="true"
title="{{ projectName }} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
>
<p class="subinfo" sub>
<span class="cnsl-secondary-text">{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}</span>
<a mat-icon-button href="https://docs.zitadel.ch/docs/manuals/admin-managers" target="_blank">
<i class="las la-info-circle"></i>
</a>
</p>
<cnsl-members-table *ngIf="project" [dataSource]="dataSource" [memberRoleOptions]="memberRoleOptions"
(updateRoles)="updateRoles($event.member, $event.change)" [factoryLoadFunc]="changePageFactory"
(changedSelection)="selection = $event" [refreshTrigger]="changePage"
[canWrite]="['project.member.write$', 'project.member.write:'+ (projectType === ProjectType.PROJECTTYPE_OWNED) ? $any(project)?.id : (projectType === ProjectType.PROJECTTYPE_GRANTED) ? $any(project)?.projectId: ''] | hasRole | async"
[canDelete]="['project.member.delete$', 'project.member.delete:'+(projectType === ProjectType.PROJECTTYPE_OWNED) ? $any(project)?.id : (projectType === ProjectType.PROJECTTYPE_GRANTED) ? $any(project)?.projectId: ''] | hasRole | async"
(deleteMember)="removeProjectMember($event)">
<ng-template cnslHasRole selectactions
[hasRole]="['project.member.delete:' + (projectType === ProjectType.PROJECTTYPE_OWNED) ? $any(project)?.id : (projectType === ProjectType.PROJECTTYPE_GRANTED) ? $any(project)?.projectId: '', 'project.member.delete']">
<button (click)="removeProjectMemberSelection()" color="warn"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" mat-raised-button>
<cnsl-members-table
*ngIf="project"
[dataSource]="dataSource"
[memberRoleOptions]="memberRoleOptions"
(updateRoles)="updateRoles($event.member, $event.change)"
[factoryLoadFunc]="changePageFactory"
(changedSelection)="selection = $event"
[refreshTrigger]="changePage"
[canWrite]="
[
'project.member.write$',
'project.member.write:' + (projectType === ProjectType.PROJECTTYPE_OWNED)
? $any(project)?.id
: projectType === ProjectType.PROJECTTYPE_GRANTED
? $any(project)?.projectId
: ''
]
| hasRole
| async
"
[canDelete]="
[
'project.member.delete$',
'project.member.delete:' + (projectType === ProjectType.PROJECTTYPE_OWNED)
? $any(project)?.id
: projectType === ProjectType.PROJECTTYPE_GRANTED
? $any(project)?.projectId
: ''
]
| hasRole
| async
"
(deleteMember)="removeProjectMember($event)"
>
<ng-template
cnslHasRole
selectactions
[hasRole]="[
'project.member.delete:' + (projectType === ProjectType.PROJECTTYPE_OWNED)
? $any(project)?.id
: projectType === ProjectType.PROJECTTYPE_GRANTED
? $any(project)?.projectId
: '',
'project.member.delete'
]"
>
<button
(click)="($event.stopPropagation); removeProjectMemberSelection()"
color="warn"
matTooltip="{{ 'ORG_DETAIL.TABLE.DELETE' | translate }}"
mat-raised-button
>
<i class="las la-trash"></i>
<span>{{ 'ACTIONS.SELECTIONDELETE' | translate }}</span>
<cnsl-action-keys [type]="ActionKeysType.DELETE" (actionTriggered)="removeProjectMemberSelection()">
</cnsl-action-keys>
</button>
</ng-template>
<ng-template cnslHasRole writeactions
[hasRole]="['project.member.write:'+(projectType === ProjectType.PROJECTTYPE_OWNED) ? $any(project)?.id : (projectType === ProjectType.PROJECTTYPE_GRANTED) ? $any(project)?.projectId: '','project.member.write']">
<ng-template
cnslHasRole
writeactions
[hasRole]="[
'project.member.write:' + (projectType === ProjectType.PROJECTTYPE_OWNED)
? $any(project)?.id
: projectType === ProjectType.PROJECTTYPE_GRANTED
? $any(project)?.projectId
: '',
'project.member.write'
]"
>
<button color="primary" (click)="openAddMember()" class="cnsl-action-button" mat-raised-button>
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="openAddMember()">
</cnsl-action-keys>
<cnsl-action-keys (actionTriggered)="openAddMember()"> </cnsl-action-keys>
</button>
</ng-template>
</cnsl-members-table>

View File

@ -103,6 +103,19 @@ export class ProjectMembersComponent {
this.grantId,
);
};
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.GRANTEDPROJECT,
param: { key: 'projectid', value: (this.project as GrantedProject.AsObject).projectId },
routerLink: ['/projects', (this.project as GrantedProject.AsObject).projectId],
}),
];
breadcrumbService.setBreadcrumb(breadcrumbs);
}
});
}

View File

@ -16,9 +16,11 @@
<cnsl-password-lockout-policy [serviceType]="serviceType"></cnsl-password-lockout-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'login'">
<!-- <cnsl-org-iam-policy [serviceType]="serviceType"></cnsl-org-iam-policy> -->
<cnsl-login-policy [serviceType]="serviceType"></cnsl-login-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'domain'">
<cnsl-domain-policy [serviceType]="serviceType"></cnsl-domain-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'idp'">
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
</ng-container>

View File

@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../card/card.module';
import { DomainPolicyModule } from '../policies/domain-policy/domain-policy.module';
import { GeneralSettingsModule } from '../policies/general-settings/general-settings.module';
import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module';
import { LoginPolicyModule } from '../policies/login-policy/login-policy.module';
@ -12,7 +13,6 @@ import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.modu
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
import { OrgIamPolicyModule } from '../policies/org-iam-policy/org-iam-policy.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
import { PrivacyPolicyModule } from '../policies/privacy-policy/privacy-policy.module';
@ -37,7 +37,7 @@ import { SettingsListComponent } from './settings-list.component';
PrivacyPolicyModule,
MessageTextsPolicyModule,
LoginTextsPolicyModule,
OrgIamPolicyModule,
DomainPolicyModule,
TranslateModule,
HasRolePipeModule,
NotificationSettingsModule,

View File

@ -19,10 +19,12 @@ export const LOGIN: SidenavSetting = {
id: 'login',
i18nKey: 'SETTINGS.LIST.LOGIN',
groupI18nKey: 'SETTINGS.GROUPS.LOGIN',
// requiredRoles: {
// [PolicyComponentServiceType.ADMIN]: true,
// [PolicyComponentServiceType.MGMT]: true,
// }
};
export const DOMAIN: SidenavSetting = {
id: 'domain',
i18nKey: 'SETTINGS.LIST.DOMAIN',
groupI18nKey: 'SETTINGS.GROUPS.DOMAIN',
};
export const LOCKOUT: SidenavSetting = {

View File

@ -55,6 +55,7 @@ export class SidenavComponent implements ControlValueAccessor, OnInit {
queryParams: {
[this.queryParam]: setting,
},
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
});

View File

@ -1,6 +1,12 @@
<cnsl-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()" [hideRefresh]="true"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult ?? 0" [selection]="selection">
<cnsl-refresh-table
[loading]="dataSource?.loading$ | async"
(refreshed)="changePage()"
[hideRefresh]="true"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
[timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult ?? 0"
[selection]="selection"
>
<!-- <div leftActions class="user-grants-table-left-actions">
<button class="user-grant-type-button" [ngClass]="{'active': type === undefined}"
(click)="setType(undefined)">{{'PROJECT.GRANT.ALL' | translate}}</button>
@ -10,19 +16,36 @@
(click)="setType(Type.TYPE_MACHINE)">{{'USER.TABLE.TYPES.MACHINE' | translate}}</button>
</div> -->
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="cnsl-action-button" mat-raised-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete === false">
<button
color="warn"
matTooltip="{{ 'GRANTS.DELETE' | translate }}"
class="cnsl-action-button"
mat-raised-button
actions
(click)="deleteGrantSelection()"
*ngIf="selection.hasValue() && disableDelete === false"
>
<i class="las la-trash"></i>
<span>{{ 'ACTIONS.DELETE' | translate }}</span>
<cnsl-action-keys (actionTriggered)="deleteGrantSelection()" [type]="ActionKeysType.DELETE"></cnsl-action-keys>
</button>
<cnsl-filter-user-grants actions *ngIf="!selection.hasValue()" (filterChanged)="applySearchQuery($any($event))"
(filterOpen)="filterOpen = $event"></cnsl-filter-user-grants>
<cnsl-filter-user-grants
actions
*ngIf="!selection.hasValue()"
(filterChanged)="applySearchQuery($any($event))"
(filterOpen)="filterOpen = $event"
></cnsl-filter-user-grants>
<a actions *ngIf="disableWrite === false && (selection.hasValue() === false)"
matTooltip="{{'GRANTS.ADD' | translate}}" color="primary" class="cnsl-action-button" mat-raised-button
[routerLink]="routerLink">
<a
actions
*ngIf="disableWrite === false && selection.hasValue() === false"
matTooltip="{{ 'GRANTS.ADD' | translate }}"
color="primary"
class="cnsl-action-button"
mat-raised-button
[routerLink]="routerLink"
>
<mat-icon class="icon">add</mat-icon>
<span>{{ 'GRANTS.ADD_BTN' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoCreateLink(routerLink)" [type]="ActionKeysType.ADD"></cnsl-action-keys>
@ -32,23 +55,47 @@
<table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="disableWrite" color="primary" (change)="$event ? masterToggle() : null"
<mat-checkbox
[disabled]="disableWrite"
color="primary"
(change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
[indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + row?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + row?.id] : []) | hasRole | async))"
color="primary" (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
<cnsl-avatar *ngIf="row && row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [forColor]="row?.preferredLoginName"
[size]="32">
[disabled]="
disableWrite ||
!(
(['user.grant.write$'] | hasRole | async) ||
((context === UserGrantContext.OWNED_PROJECT
? ['user.grant.write:' + row?.projectId]
: context === UserGrantContext.GRANTED_PROJECT
? ['user.grant.write:' + row?.id]
: []
)
| hasRole
| async)
)
"
color="primary"
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
>
<cnsl-avatar
*ngIf="row && row?.displayName && row.firstName && row.lastName; else cog"
class="avatar"
[name]="row.displayName"
[avatarUrl]="row.avatarUrl || ''"
[forColor]="row?.preferredLoginName"
[size]="32"
>
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar class="avatar" [isMachine]="true" [forColor]="row.preferredLoginName" [size]="32">
</cnsl-avatar>
<cnsl-avatar class="avatar" [isMachine]="true" [forColor]="row.preferredLoginName" [size]="32"> </cnsl-avatar>
</ng-template>
</mat-checkbox>
</td>
@ -57,21 +104,22 @@
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.USER' | translate }}</th>
<td mat-cell *matCellDef="let grant">
<a class="user">{{grant.displayName ? grant.displayName :
grant.userName ? grant.userName : ''}}</a>
<a class="user">{{ grant.displayName ? grant.displayName : grant.userName ? grant.userName : '' }}</a>
</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.ORG' | translate }}</th>
<td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
{{ grant.orgName }}
</td>
</ng-container>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.PROJECTNAME' | translate }}</th>
<td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
{{ grant.projectName }}
</td>
</ng-container>
<ng-container matColumnDef="type">
@ -84,16 +132,14 @@
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.CREATIONDATE' | translate }}</th>
<td mat-cell *matCellDef="let grant">
<span class="no-break">{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm'
}}</span>
<span class="no-break">{{ grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.CHANGEDATE' | translate }}</th>
<td mat-cell *matCellDef="let grant">
<span class="no-break">{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm'
}}</span>
<span class="no-break">{{ grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
@ -103,7 +149,8 @@
</th>
<td mat-cell *matCellDef="let grant; let i = index" class="role-data">
<div class="flex-row">
<cnsl-project-role-chip [roleName]="role" *ngFor="let role of grant.roleKeysList">{{ role }}
<cnsl-project-role-chip [roleName]="role" *ngFor="let role of grant.roleKeysList"
>{{ role }}
</cnsl-project-role-chip>
</div>
</td>
@ -113,32 +160,45 @@
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
<td mat-cell class="user-tr-actions" *matCellDef="let grant; let i = index">
<cnsl-table-actions [hasActions]="true">
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" (click)="deleteGrant(grant)"
mat-icon-button>
<button
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="deleteGrant($event, grant)"
mat-icon-button
>
<i class="las la-trash"></i>
</button>
<button menuActions mat-menu-item [routerLink]="['/users', grant.userId]">
{{'ACTIONS.TABLE.SHOWUSER' | translate : ({value: grant.displayName})}}
{{ 'ACTIONS.TABLE.SHOWUSER' | translate: { value: grant.displayName } }}
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr (click)="openEditDialog(grant)" class="highlight pointer" mat-row
*matRowDef="let grant; columns: displayedColumns;">
</tr>
<tr
(click)="openEditDialog(grant)"
class="highlight pointer"
mat-row
*matRowDef="let grant; columns: displayedColumns"
></tr>
</table>
</div>
<div *ngIf="(dataSource.loading$ | async) === false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'GRANTS.EMPTY' | translate }}</span>
</div>
<cnsl-paginator class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp" [length]="dataSource.totalResult"
[pageSize]="INITIAL_PAGE_SIZE" [length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]"
(page)="changePage($event)">
<cnsl-paginator
class="paginator"
#paginator
[timestamp]="dataSource?.viewTimestamp"
[length]="dataSource.totalResult"
[pageSize]="INITIAL_PAGE_SIZE"
[length]="dataSource.totalResult"
[pageSizeOptions]="[2, 3, 25, 50, 100, 250]"
(page)="changePage($event)"
>
</cnsl-paginator>
</cnsl-refresh-table>

View File

@ -200,7 +200,9 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
});
}
public deleteGrant(grant: UserGrant.AsObject): void {
public deleteGrant(event: any, grant: UserGrant.AsObject): void {
event.stopPropagation();
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',

View File

@ -8,6 +8,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import {
BRANDING,
COMPLEXITY,
DOMAIN,
GENERAL,
IDP,
LOCKOUT,
@ -31,12 +32,14 @@ export class InstanceSettingsComponent {
public settingsList: SidenavSetting[] = [
GENERAL,
// notifications
{ showWarn: true, ...NOTIFICATIONS },
// { showWarn: true, ...NOTIFICATIONS },
NOTIFICATIONS,
// login
LOGIN,
COMPLEXITY,
LOCKOUT,
IDP,
DOMAIN,
// appearance
BRANDING,
MESSAGETEXTS,

View File

@ -8,6 +8,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import {
BRANDING,
COMPLEXITY,
DOMAIN,
IDP,
LOCKOUT,
LOGIN,
@ -29,6 +30,7 @@ export class OrgSettingsComponent {
COMPLEXITY,
LOCKOUT,
IDP,
DOMAIN,
BRANDING,
MESSAGETEXTS,
LOGINTEXTS,

View File

@ -1,28 +1,35 @@
<div class="app-create-container">
<div class="max-width-container">
<div class="enlarged-container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span>
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span
><span class="abort-2">Step {{ currentCreateStep }} of {{ createSteps }}</span>
</div>
<div class="app-create-content">
<h1>{{ 'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate }}</h1>
<p class="desc cnsl-secondary-text">{{'APP.PAGES.CREATE_OIDC_DESC_SUB' | translate}}</p>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
{{ 'APP.OIDC.PROSWITCH' | translate }}
</mat-checkbox>
<mat-horizontal-stepper class="stepper" *ngIf="!devmode" linear #stepper labelPosition="bottom"
(selectionChange)="changeStep($event)">
<mat-horizontal-stepper
class="stepper"
*ngIf="!devmode"
linear
#stepper
labelPosition="bottom"
(selectionChange)="changeStep($event)"
>
<mat-step [stepControl]="firstFormGroup" [editable]="true">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>{{ 'APP.OIDC.NAMEANDTYPESECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.TITLEFIRST' | translate }}</p>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-form-field appearance="outline" class="name-formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput cdkFocusInitial formControlName="name" />
<span cnslError *ngIf="name?.errors?.required">{{ 'PROJECT.APP.NAMEREQUIRED' | translate }}</span>
@ -33,29 +40,47 @@
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
</cnsl-type-radio>
<div class="app-create-actions">
<span class="fill-space"></span>
<button mat-raised-button [disabled]="firstFormGroup.invalid" color="primary" matStepperNext
[attr.data-e2e]="'continue-button-nameandtype'">{{'ACTIONS.CONTINUE' | translate}}</button>
<button
mat-raised-button
[disabled]="firstFormGroup.invalid"
color="primary"
matStepperNext
[attr.data-e2e]="'continue-button-nameandtype'"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</form>
</mat-step>
<!-- skip for native applications -->
<mat-step *ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE" [stepControl]="secondFormGroup"
[editable]="true">
<mat-step
*ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE"
[stepControl]="secondFormGroup"
[editable]="true"
>
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>{{ 'APP.AUTHMETHODSECTION' | translate }}</ng-template>
<cnsl-auth-method-radio [authMethods]="authMethods" [selected]="authMethod?.value"
[isOIDC]="appType?.value?.createType === AppCreateType.OIDC" (selectedMethod)="authMethod?.setValue($event)">
<cnsl-auth-method-radio
[authMethods]="authMethods"
[selected]="authMethod?.value"
[isOIDC]="appType?.value?.createType === AppCreateType.OIDC"
(selectedMethod)="authMethod?.setValue($event)"
>
</cnsl-auth-method-radio>
<div class="app-create-actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' |
translate}}</button>
<span class="fill-space"></span>
<button mat-raised-button color="primary" [disabled]="secondFormGroup.invalid" matStepperNext
[attr.data-e2e]="'continue-button-authmethod'">{{'ACTIONS.CONTINUE' | translate}}</button>
<button class="bck-button" mat-stroked-button matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
<button
mat-raised-button
color="primary"
[disabled]="secondFormGroup.invalid"
matStepperNext
[attr.data-e2e]="'continue-button-authmethod'"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</form>
</mat-step>
@ -65,42 +90,63 @@
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
<p class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p>
<p
class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
</p>
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
</p>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)" [urisList]="oidcAppRequest.redirectUrisList"
[getValues]="requestRedirectValuesSubject$" title="{{ 'APP.OIDC.REDIRECT' | translate }}">
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
[urisList]="oidcAppRequest.redirectUrisList"
[getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
>
</cnsl-redirect-uris>
<p class="step-title">{{ 'APP.OIDC.POSTREDIRECTTITLE' | translate }}</p>
<p class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p>
<p class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB || oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT">
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p>
<p
class="step-description cnsl-secondary-text"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
</p>
<p
class="step-description cnsl-secondary-text"
*ngIf="
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB ||
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
"
>
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
</p>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
<div class="app-create-actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<span class="fill-space"></span>
<button mat-raised-button color="primary" matStepperNext
[attr.data-e2e]="'continue-button-redirecturis'">{{'ACTIONS.CONTINUE' | translate}}</button>
<button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
<button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'">
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'APP.OIDC.OVERVIEWSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.OVERVIEWTITLE' | translate }}</p>
@ -129,7 +175,8 @@
<span class="right" *ngIf="oidcAppRequest.grantTypesList && oidcAppRequest.grantTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.grantTypesList; index as i">
{{ 'APP.OIDC.GRANT.' + element | translate }}
{{i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span>]
{{ i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row cnsl-secondary-text">
@ -138,8 +185,9 @@
</span>
<span class="right" *ngIf="oidcAppRequest.responseTypesList && oidcAppRequest.responseTypesList.length > 0">
[<span *ngFor="let element of oidcAppRequest.responseTypesList; index as i">
{{('APP.OIDC.RESPONSE.'+element | translate)}}
{{i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span>]
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
{{ i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
@ -161,7 +209,8 @@
<span class="right" *ngIf="oidcAppRequest.redirectUrisList && oidcAppRequest.redirectUrisList.length > 0">
[<span *ngFor="let redirect of oidcAppRequest.redirectUrisList; index as i">
{{ redirect }}
{{i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span>]
{{ i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
@ -169,11 +218,14 @@
<span class="left">
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
</span>
<span class="right"
*ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0">
<span
class="right"
*ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0"
>
[<span *ngFor="let redirect of oidcAppRequest.postLogoutRedirectUrisList; index as i">
{{ redirect }}
{{i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span>]
{{ i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
</ng-container>
@ -192,11 +244,16 @@
</ng-container>
<div class="app-create-actions">
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | translate}}</button>
<span class="fill-space"></span>
<button mat-raised-button color="primary" (click)="createApp()"
[attr.data-e2e]="'create-button'">{{'ACTIONS.CREATE' |
translate}}</button>
<button mat-stroked-button matStepperPrevious class="bck-button">{{ 'ACTIONS.BACK' | translate }}</button>
<button
mat-raised-button
class="create-button"
color="primary"
(click)="createApp()"
[attr.data-e2e]="'create-button'"
>
{{ 'ACTIONS.CREATE' | translate }}
</button>
</div>
</mat-step>
</mat-horizontal-stepper>
@ -223,7 +280,7 @@
<cnsl-label>{{ 'APP.OIDC.GRANTTYPE' | translate }}</cnsl-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant.type">
{{ ('APP.OIDC.GRANT.' + grant.type) | translate }}
{{ 'APP.OIDC.GRANT.' + grant.type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
@ -251,25 +308,42 @@
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<div class="formfield full-width">
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)" [urisList]="oidcAppRequest.redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
[urisList]="oidcAppRequest.redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true"
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
</div>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="form.invalid"
cdkFocusInitial
type="submit"
>
{{ 'ACTIONS.CREATE' | translate }}
</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -12,13 +12,6 @@ p.desc {
display: block;
}
.app-create-container {
padding-top: 2rem;
.progress-bar {
margin-bottom: 1rem;
}
.abort-container {
display: flex;
align-items: center;
@ -35,6 +28,21 @@ p.desc {
white-space: nowrap;
}
}
.app-create-content {
padding-left: 4.5rem;
.name-formfield {
max-width: 400px;
display: block;
}
.app-create-container {
padding-top: 2rem;
.progress-bar {
margin-bottom: 1rem;
}
}
.margin-right {
@ -87,9 +95,14 @@ p.desc {
.app-create-actions {
margin-top: 1rem;
display: flex;
align-items: center;
.fill-space {
flex: 1;
.bck-button {
margin-right: 1rem;
}
.create-button {
padding: 0.5rem 4rem;
}
}
@ -115,6 +128,6 @@ p.desc {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
float: right;
}
}
}

View File

@ -49,6 +49,9 @@ export class AppCreateComponent implements OnInit, OnDestroy {
public projectId: string = '';
public loading: boolean = false;
public createSteps: number = 4;
public currentCreateStep: number = 1;
public oidcAppRequest: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
public apiAppRequest: AddAPIAppRequest.AsObject = new AddAPIAppRequest().toObject();
@ -270,6 +273,8 @@ export class AppCreateComponent implements OnInit, OnDestroy {
}
public changeStep(event: StepperSelectionEvent): void {
this.currentCreateStep = event.selectedIndex + 1;
if (event.selectedIndex >= 2) {
this.requestRedirectValuesSubject$.next();
}

View File

@ -1,17 +1,32 @@
<cnsl-top-view title="{{project?.projectName}}" [hasActions]="false"
<cnsl-top-view
title="{{ project?.projectName }}"
[hasActions]="false"
docLink="https://docs.zitadel.ch/docs/guides/basics/projects#what-is-a-granted-project"
sub="{{'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate}} {{'ACTIONS.OF' | translate}} <strong>{{project?.projectOwnerName}}</strong>"
sub="{{ 'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate }} {{ 'ACTIONS.OF' | translate }} <strong>{{
project?.projectOwnerName
}}</strong>"
[isActive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE"
[isInactive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE"
stateTooltip="{{'ORG.STATE.'+project.state | translate}}" [hasContributors]="true">
stateTooltip="{{ 'ORG.STATE.' + project?.state | translate }}"
[hasContributors]="true"
>
<p topContent *ngIf="isZitadel" class="granted-project-sub zitadel-warning">
{{ 'PROJECT.PAGES.ZITADELPROJECT' | translate }}
</p>
<cnsl-contributors topContributors class="project-contributors" *ngIf="project" [loading]="loading$ | async"
[totalResult]="totalMemberResult" [membersSubject]="membersSubject" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" (addClicked)="openAddMember()"
(showDetailClicked)="showDetail()" (refreshClicked)="loadMembers()"
[disabled]="(['project.member.write$', 'project.member.write:'+ project.projectId]| hasRole | async) === false">
<cnsl-contributors
topContributors
class="project-contributors"
*ngIf="project"
[loading]="loading$ | async"
[totalResult]="totalMemberResult"
[membersSubject]="membersSubject"
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
(addClicked)="openAddMember()"
(showDetailClicked)="showDetail()"
(refreshClicked)="loadMembers()"
[disabled]="(['project.member.write$', 'project.member.write:' + project.projectId] | hasRole | async) === false"
>
</cnsl-contributors>
<cnsl-info-row topContent *ngIf="project" [grantedProject]="project"></cnsl-info-row>
</cnsl-top-view>
@ -19,14 +34,21 @@
<div class="max-width-container">
<cnsl-meta-layout>
<ng-template cnslHasRole [hasRole]="['user.grant.read', 'user.grant.read:' + grantId]">
<cnsl-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<cnsl-user-grants *ngIf="projectId && grantId" [context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId" [grantId]="grantId"
<cnsl-card
*ngIf="project?.projectId"
title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{ 'GRANTS.PROJECT.DESCRIPTION' | translate }}"
>
<cnsl-user-grants
*ngIf="projectId && grantId"
[context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId"
[grantId]="grantId"
[displayedColumns]="['select', 'user', 'projectId', 'creationDate', 'changeDate', 'roleNamesList', 'actions']"
[disableWrite]="(['user.grant.write$', 'user.grant.write:' + grantId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + grantId] | hasRole | async) === false"
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']">
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']"
>
</cnsl-user-grants>
</cnsl-card>
</ng-template>
@ -34,6 +56,5 @@
<div metainfo>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes>
</div>
</cnsl-meta-layout>
</div>

View File

@ -89,7 +89,7 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
.bulkAddProjectRoles(this.projectId, rolesToAdd)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.ROLESCREATED', true);
this.router.navigate(['projects', this.projectId, 'roles']);
this.router.navigate(['projects', this.projectId], { queryParams: { id: 'roles' } });
})
.catch((error) => {
this.toast.showError(error);

View File

@ -4,9 +4,7 @@
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.PAGES.CREATE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
<span class="abort">{{ 'PROJECT.PAGES.CREATE' | translate }}</span>
</div>
<div class="project-create-content">
@ -15,13 +13,19 @@
<div class="column">
<cnsl-form-field class="formfield" hintLabel="The name is required!">
<cnsl-label>{{ 'PROJECT.NAME' | translate }}</cnsl-label>
<input cnslInput cdkFocusInitial autofocus [(ngModel)]="project.name"
[ngModelOptions]="{ standalone: true }" />
<input cnslInput cdkFocusInitial autofocus [(ngModel)]="project.name" [ngModelOptions]="{ standalone: true }" />
</cnsl-form-field>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="!project.name" cdkFocusInitial
type="submit" [attr.data-e2e]="'continue-button'">
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="!project.name"
cdkFocusInitial
type="submit"
[attr.data-e2e]="'continue-button'"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</form>

View File

@ -11,12 +11,6 @@ h1 {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.project-create-content {

View File

@ -28,9 +28,6 @@ export class ProjectCreateComponent {
breadcrumbService.setBreadcrumb([bread]);
}
public createSteps: number = 1;
public currentCreateStep: number = 1;
public saveProject(): void {
this.mgmtService
.addProject(this.project)

View File

@ -1,30 +1,38 @@
<div class="grid-main-container" *ngIf="projectType$ | async as type">
<div class="loading-sp-wrapper">
<mat-progress-spinner diameter="25" *ngIf="(loading$| async) === false" class="spinner" color="primary">
</mat-progress-spinner>
<div class="loading-sp-wrapper" *ngIf="loading$ | async">
<mat-spinner diameter="25" class="spinner" color="primary"> </mat-spinner>
</div>
<div class="owned-project-grid-container">
<div class="item card" matRipple *ngFor="let item of selection.selected; index as i"
<div
class="item card"
matRipple
*ngFor="let item of selection.selected; index as i"
(click)="navigateToProject(type, item, $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE }"
>
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate"
class="top cnsl-secondary-text">{{'PROJECT.PAGES.LASTMODIFIED' |
translate}}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text"
>{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
>
<div class="name-row">
<span class="name" *ngIf="$any(item).name">{{ $any(item).name }}</span>
<span class="name" *ngIf="$any(item).projectName">{{ $any(item).projectName }}</span>
<div class="state-dot"
[ngClass]="{'active': item.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': item.state === ProjectState.PROJECT_STATE_INACTIVE}">
</div>
<div
class="state-dot"
[ngClass]="{
active: item.state === ProjectState.PROJECT_STATE_ACTIVE,
inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE
}"
></div>
</div>
<span *ngIf="item.details && item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' |
translate}}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span *ngIf="item.details && item.details.creationDate" class="created"
>{{ 'PROJECT.PAGES.CREATEDON' | translate }}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
>
<span class="fill-space"></span>
</div>
@ -33,29 +41,37 @@
</div>
</div>
<div class="owned-project-grid-container">
<div class="item card" matRipple *ngFor="let item of notPinned; index as i"
<div
class="item card"
matRipple
*ngFor="let item of notPinned; index as i"
(click)="navigateToProject(type, $any(item), $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}" [attr.data-e2e]="'grid-card'">
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE }"
[attr.data-e2e]="'grid-card'"
>
<div class="text-part">
<span *ngIf="item.details && item.details.changeDate"
class="top cnsl-secondary-text">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text"
>{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
>
<div class="name-row">
<span class="name" *ngIf="$any(item).name">{{ $any(item).name }}</span>
<span class="name" *ngIf="$any(item).projectName">{{ $any(item).projectName }}</span>
<div class="state-dot"
[ngClass]="{'active': item.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': item.state === ProjectState.PROJECT_STATE_INACTIVE}">
</div>
<div
class="state-dot"
[ngClass]="{
active: item.state === ProjectState.PROJECT_STATE_ACTIVE,
inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE
}"
></div>
</div>
<span class="owning-org" *ngIf="$any(item).projectOwnerName">{{ $any(item).projectOwnerName }}</span>
<span *ngIf="item.details && item.details.creationDate"
class="created cnsl-secondary-text">{{'PROJECT.PAGES.CREATEDON' |
translate}}
{{
item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<span *ngIf="item.details && item.details.creationDate" class="created cnsl-secondary-text"
>{{ 'PROJECT.PAGES.CREATEDON' | translate }}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
>
<span class="fill-space"></span>
</div>
@ -66,8 +82,8 @@
</div>
<p class="n-items cnsl-secondary-text" *ngIf="(loading$ | async) === false && projectList.length === 0">
{{'PROJECT.PAGES.NOITEMS' |
translate}}</p>
{{ 'PROJECT.PAGES.NOITEMS' | translate }}
</p>
<ng-container *ngIf="type === ProjectType.PROJECTTYPE_OWNED">
<ng-template cnslHasRole [hasRole]="['project.create']">
@ -83,16 +99,27 @@
</div>
<ng-template #deleteButton let-key="key">
<button *ngIf="key.id !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn"
(click)="deleteProject($event, key)" class="delete-button" mat-icon-button
[attr.data-e2e]="'delete-project-button'">
<button
*ngIf="key.id !== zitadelProjectId"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
color="warn"
(click)="deleteProject($event, key)"
class="delete-button"
mat-icon-button
[attr.data-e2e]="'delete-project-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>
<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>

View File

@ -25,17 +25,12 @@
}
}
.spinner {
margin: 1rem;
}
.grid-main-container {
position: relative;
.loading-sp-wrapper {
position: absolute;
top: 0;
left: 0;
display: block;
margin: 1rem 0.5rem 0 0.5rem;
}
.owned-project-grid-container {

View File

@ -144,7 +144,6 @@ export class ProjectGridComponent implements OnInit, OnDestroy {
this.loadingSubject.next(false);
})
.catch((error) => {
console.error(error);
this.toast.showError(error);
this.loadingSubject.next(false);
});

View File

@ -10,12 +10,20 @@
<div class="projects-controls">
<div class="project-type-actions">
<button class="type-button" [ngClass]="{'active': (projectType$ | async) === ProjectType.PROJECTTYPE_OWNED}"
(click)="setType(ProjectType.PROJECTTYPE_OWNED)">{{'PROJECT.PAGES.TYPE.OWNED' | translate}}
({{((mgmtService?.ownedProjectsCount | async) ?? 0)}})</button>
<button class="type-button" [ngClass]="{'active': (projectType$ | async) === ProjectType.PROJECTTYPE_GRANTED}"
(click)="setType(ProjectType.PROJECTTYPE_GRANTED)">{{'PROJECT.PAGES.TYPE.GRANTED' | translate}}
({{((mgmtService?.grantedProjectsCount | async) ?? 0)}})</button>
<button
class="type-button"
[ngClass]="{ active: (projectType$ | async) === ProjectType.PROJECTTYPE_OWNED }"
(click)="setType(ProjectType.PROJECTTYPE_OWNED)"
>
{{ 'PROJECT.PAGES.TYPE.OWNED' | translate }} ({{ (mgmtService?.ownedProjectsCount | async) ?? 0 }})
</button>
<button
class="type-button"
[ngClass]="{ active: (projectType$ | async) === ProjectType.PROJECTTYPE_GRANTED }"
(click)="setType(ProjectType.PROJECTTYPE_GRANTED)"
>
{{ 'PROJECT.PAGES.TYPE.GRANTED' | translate }} ({{ (mgmtService?.grantedProjectsCount | async) ?? 0 }})
</button>
</div>
<span class="fill-space"></span>
<button class="grid-btn" (click)="grid = !grid" mat-icon-button [attr.data-e2e]="'toggle-grid'">
@ -24,8 +32,12 @@
</button>
</div>
<cnsl-project-grid *ngIf="grid" [projectType$]="projectType$" [zitadelProjectId]="zitadelProjectId"
(emitAddProject)="addProject()">
<cnsl-project-grid
*ngIf="grid"
[projectType$]="projectType$"
[zitadelProjectId]="zitadelProjectId"
(emitAddProject)="addProject()"
>
</cnsl-project-grid>
<cnsl-project-list *ngIf="!grid" [projectType$]="projectType$" [zitadelProjectId]="zitadelProjectId">

View File

@ -53,6 +53,7 @@ export class ProjectsComponent {
type:
type === ProjectType.PROJECTTYPE_OWNED ? 'owned' : type === ProjectType.PROJECTTYPE_GRANTED ? 'granted' : 'owned',
},
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
});

View File

@ -4,32 +4,35 @@
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<h1 class="abort">{{ 'GRANTS.CREATE.TITLE' | translate }}</h1><span class="abort-2">Step
{{ currentCreateStep }} of
{{ STEPS }}</span>
<h1 class="abort">{{ 'GRANTS.CREATE.TITLE' | translate }}</h1>
<span class="abort-2">Step {{ currentCreateStep }} of {{ STEPS }}</span>
</div>
<div class="user-grant-create-content">
<ng-container *ngIf="currentCreateStep === 1">
<p class="user-grant-create-desc cnsl-secondary-text">
{{ 'PROJECT.GRANT.CREATE.ORG_DESCRIPTION' | translate: org }}
<br>
<br />
{{ 'PROJECT.GRANT.CREATE.ORG_DESCRIPTION_DESC' | translate }}
</p>
<ng-container>
<h2>{{ 'PROJECT.GRANT.CREATE.SEL_USER' | translate }}</h2>
<cnsl-search-user-autocomplete [editState]="context !== UserGrantContext.USER" class="block"
[users]="user ? [user] : []" (selectionChanged)="selectUsers($event)" [target]="UserTarget.SELF">
<cnsl-search-user-autocomplete
[editState]="context !== UserGrantContext.USER"
class="block"
[users]="user ? [user] : []"
(selectionChanged)="selectUsers($event)"
[target]="UserTarget.SELF"
>
</cnsl-search-user-autocomplete>
</ng-container>
<ng-container *ngIf="context && (context === UserGrantContext.USER || context === UserGrantContext.NONE)">
<h2 class="project-search">{{ 'PROJECT.GRANT.CREATE.SEL_PROJECT' | translate }}</h2>
<cnsl-search-project-autocomplete class="block"
(selectionChanged)="selectProject($event.project, $event.type)">
<cnsl-search-project-autocomplete class="block" (selectionChanged)="selectProject($event.project, $event.type)">
</cnsl-search-project-autocomplete>
</ng-container>
</ng-container>
@ -38,19 +41,27 @@
<h1>{{ 'PROJECT.GRANT.CREATE.SEL_ROLES' | translate }}</h1>
<cnsl-card>
{{ $any(project)?.grantId }}
<cnsl-project-roles-table
[displayedColumns]="['select', 'key', 'displayname', 'group', 'creationDate', 'changeDate']"
(changedSelection)="selectRoles($event)"
[projectId]="project?.id ? project.id : grantedProject?.projectId ? grantedProject.projectId : ''"
[grantId]="$any(project)?.grantId ? $any(project)?.grantId : ''">
[grantId]="$any(grantedProject)?.grantId ? $any(grantedProject)?.grantId : ''"
>
</cnsl-project-roles-table>
</cnsl-card>
</ng-container>
<div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1">
<button [disabled]="!org || !(project?.id || grantedProject?.projectId) || userIds.length < 1"
(click)="next()" color="primary" mat-raised-button class="big-button" cdkFocusInitial>
<button
[disabled]="!org || !(project?.id || grantedProject?.projectId) || userIds.length < 1"
(click)="next()"
color="primary"
mat-raised-button
class="big-button"
cdkFocusInitial
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</ng-container>

View File

@ -4,8 +4,7 @@
<a [routerLink]="['/users']" mat-icon-button>
<mat-icon>close</mat-icon>
</a>
<h1 class="abort">{{ 'USER.CREATE.TITLE' | translate }}</h1><span class="abort-2">Step
1 of 1</span>
<h1 class="abort">{{ 'USER.CREATE.TITLE' | translate }}</h1>
</div>
<div class="user-create-main-content">
@ -13,8 +12,10 @@
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="user-create-form">
<div class="user-create-content">
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<p class="user-create-section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<div class="user-create-grid">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<input cnslInput matRipple formControlName="email" required />
<span cnslError *ngIf="email?.invalid && !email?.errors?.required">
@ -24,10 +25,14 @@
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required
[ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" />
<input
cnslInput
formControlName="userName"
required
[ngStyle]="{ 'padding-right': suffixPadding ? suffixPadding : '10px' }"
/>
<span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{ envSuffixLabel }}</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
@ -37,29 +42,29 @@
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
</div>
<div class="user-create-content">
<cnsl-form-field class="formfield">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="firstName" required />
<span cnslError *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="lastName" required />
<span cnslError *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="nickName" />
<span cnslError *ngIf="nickName?.invalid && nickName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
</div>
<div class="email-is-verified">
<mat-checkbox class="block-checkbox" formControlName="isVerified">
@ -77,20 +82,25 @@
<cnsl-password-complexity-view class="complexity-view" [policy]="this.policy" [password]="password">
</cnsl-password-complexity-view>
<form [formGroup]="pwdForm" class="user-create-pwd-form">
<cnsl-form-field class="pwd-field" *ngIf="password" appearance="outline">
<form [formGroup]="pwdForm">
<div class="user-create-grid">
<cnsl-form-field *ngIf="password">
<cnsl-label>{{ 'USER.PASSWORD.NEWINITIAL' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="firstpassword" formControlName="password" type="password" />
<span cnslError *ngIf="password?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="pwd-field" *ngIf="confirmPassword" appearance="outline">
<cnsl-form-field *ngIf="confirmPassword">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="confirmPassword" formControlName="confirmPassword"
type="password" />
<input
cnslInput
autocomplete="off"
name="confirmPassword"
formControlName="confirmPassword"
type="password"
/>
<span cnslError *ngIf="confirmPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
@ -99,12 +109,14 @@
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</div>
</form>
</div>
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<p class="user-create-section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<div class="user-create-grid">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
<mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender">
@ -115,7 +127,7 @@
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</cnsl-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
@ -126,10 +138,11 @@
</span>
</mat-select>
</cnsl-form-field>
</div>
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
<p class="user-create-section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
<input cnslInput formControlName="phone" />
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
@ -138,10 +151,15 @@
</cnsl-form-field>
</div>
<div class="user-create-btn-container">
<button [attr.data-e2e]="'create-button'" color="primary"
[disabled]="userForm.invalid || (this.usePassword && this.pwdForm.invalid)" type="submit"
mat-raised-button>{{ 'ACTIONS.CREATE' |
translate }}</button>
<button
[attr.data-e2e]="'create-button'"
color="primary"
[disabled]="userForm.invalid || (this.usePassword && this.pwdForm.invalid)"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.CREATE' | translate }}
</button>
</div>
</form>
</div>

View File

@ -7,20 +7,47 @@
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.user-create-main-content {
padding-left: 4.5rem;
max-width: 35rem;
@media only screen and (max-width: 500px) {
padding: 0 0.5rem;
}
.user-create-form {
padding-top: 1rem;
.user-create-content {
.user-create-section {
padding: 1rem 0 0 0;
flex-basis: 100%;
font-size: 14px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.user-create-grid {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1rem;
@media only screen and (max-width: 500px) {
grid-template-columns: 1fr;
}
}
.email-is-verified,
.use-password-block {
flex-basis: 100%;
margin-top: 1.5rem;
.block-checkbox {
display: block;
margin: 0.25rem 0;
}
}
}
.user-create-btn-container {
button {
@ -32,49 +59,9 @@
}
}
.user-create-content {
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -0.5rem;
.user-create-section {
padding: 0.5rem;
flex-basis: 100%;
font-size: 0.9rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.formfield {
flex: 1 0 33%;
margin: 0 0.5rem;
}
.email-is-verified,
.use-password-block {
margin: 0 0.5rem;
flex-basis: 100%;
margin-top: 1.5rem;
.block-checkbox {
display: block;
margin: 0.25rem 0;
}
}
}
.pwd-section {
margin: 0 0.5rem;
.section {
padding: 0.5rem 0;
}
.user-create-pwd-form {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
}
}

View File

@ -75,7 +75,7 @@ export class UserCreateComponent implements OnDestroy {
this.loading = true;
this.loadOrg();
this.mgmtService
.getOrgIAMPolicy()
.getDomainPolicy()
.then((resp) => {
if (resp.policy?.userLoginMustBeDomain) {
this.userLoginMustBeDomain = resp.policy.userLoginMustBeDomain;

View File

@ -40,7 +40,9 @@
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
<p class="no-user-error" *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</p>
<div *ngIf="!loading && !user" class="max-width-container">
<p class="no-user-error">{{ 'USER.PAGES.NOUSER' | translate }}</p>
</div>
<div class="max-width-container" *ngIf="user && (['user.write$', 'user.write:' + user.id] | hasRole) as canWrite$">
<cnsl-meta-layout>

View File

@ -76,6 +76,7 @@ export class UserTableComponent implements OnInit {
public ActionKeysType: any = ActionKeysType;
public filterOpen: boolean = false;
private searchQueries: SearchQuery[] = [];
constructor(
private router: Router,
public translate: TranslateService,
@ -109,6 +110,7 @@ export class UserTableComponent implements OnInit {
queryParams: {
type: type === Type.TYPE_HUMAN ? 'human' : type === Type.TYPE_MACHINE ? 'machine' : 'human',
},
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
});
@ -225,12 +227,10 @@ export class UserTableComponent implements OnInit {
}
public refreshPage(): void {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, this.searchQueries);
}
public sortChange(sortState: Sort) {
console.log(sortState.active, sortState.direction);
if (sortState.direction && sortState.active) {
this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`);
this.refreshPage();
@ -241,6 +241,7 @@ export class UserTableComponent implements OnInit {
public applySearchQuery(searchQueries: SearchQuery[]): void {
this.selection.clear();
this.searchQueries = searchQueries;
this.getData(
this.paginator ? this.paginator.pageSize : this.INITIAL_PAGE_SIZE,
this.paginator ? this.paginator.pageIndex * this.paginator.pageSize : 0,

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import {
ActivateLabelPolicyRequest,
ActivateLabelPolicyResponse,
AddCustomOrgIAMPolicyRequest,
AddCustomDomainPolicyRequest,
AddCustomOrgIAMPolicyResponse,
AddIAMMemberRequest,
AddIAMMemberResponse,
@ -23,12 +23,12 @@ import {
DeactivateIDPResponse,
GetCustomDomainClaimedMessageTextRequest,
GetCustomDomainClaimedMessageTextResponse,
GetCustomDomainPolicyRequest,
GetCustomDomainPolicyResponse,
GetCustomInitMessageTextRequest,
GetCustomInitMessageTextResponse,
GetCustomLoginTextsRequest,
GetCustomLoginTextsResponse,
GetCustomOrgIAMPolicyRequest,
GetCustomOrgIAMPolicyResponse,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextResponse,
GetCustomPasswordResetMessageTextRequest,
@ -53,6 +53,8 @@ import {
GetDefaultVerifyEmailMessageTextResponse,
GetDefaultVerifyPhoneMessageTextRequest,
GetDefaultVerifyPhoneMessageTextResponse,
GetDomainPolicyRequest,
GetDomainPolicyResponse,
GetFileSystemNotificationProviderRequest,
GetFileSystemNotificationProviderResponse,
GetIDPByIDRequest,
@ -67,8 +69,6 @@ import {
GetLogNotificationProviderResponse,
GetOIDCSettingsRequest,
GetOIDCSettingsResponse,
GetOrgIAMPolicyRequest,
GetOrgIAMPolicyResponse,
GetPasswordAgePolicyRequest,
GetPasswordAgePolicyResponse,
GetPasswordComplexityPolicyRequest,
@ -130,10 +130,10 @@ import {
RemoveMultiFactorFromLoginPolicyResponse,
RemoveSecondFactorFromLoginPolicyRequest,
RemoveSecondFactorFromLoginPolicyResponse,
ResetCustomDomainPolicyToDefaultRequest,
ResetCustomDomainPolicyToDefaultResponse,
ResetCustomLoginTextsToDefaultRequest,
ResetCustomLoginTextsToDefaultResponse,
ResetCustomOrgIAMPolicyToDefaultRequest,
ResetCustomOrgIAMPolicyToDefaultResponse,
SetCustomLoginTextsRequest,
SetCustomLoginTextsResponse,
SetDefaultDomainClaimedMessageTextRequest,
@ -152,8 +152,10 @@ import {
SetDefaultVerifyPhoneMessageTextResponse,
SetUpOrgRequest,
SetUpOrgResponse,
UpdateCustomOrgIAMPolicyRequest,
UpdateCustomOrgIAMPolicyResponse,
UpdateCustomDomainPolicyRequest,
UpdateCustomDomainPolicyResponse,
UpdateDomainPolicyRequest,
UpdateDomainPolicyResponse,
UpdateIAMMemberRequest,
UpdateIAMMemberResponse,
UpdateIDPJWTConfigRequest,
@ -170,8 +172,6 @@ import {
UpdateLoginPolicyResponse,
UpdateOIDCSettingsRequest,
UpdateOIDCSettingsResponse,
UpdateOrgIAMPolicyRequest,
UpdateOrgIAMPolicyResponse,
UpdatePasswordAgePolicyRequest,
UpdatePasswordAgePolicyResponse,
UpdatePasswordComplexityPolicyRequest,
@ -599,52 +599,35 @@ export class AdminService {
return this.grpcService.admin.updateSecretGenerator(req, null).then((resp) => resp.toObject());
}
/* org iam */
/* org domain policy */
public getCustomOrgIAMPolicy(orgId: string): Promise<GetCustomOrgIAMPolicyResponse.AsObject> {
const req = new GetCustomOrgIAMPolicyRequest();
public getDomainPolicy(): Promise<GetDomainPolicyResponse.AsObject> {
const req = new GetDomainPolicyRequest();
return this.grpcService.admin.getDomainPolicy(req, null).then((resp) => resp.toObject());
}
public updateDomainPolicy(req: UpdateDomainPolicyRequest): Promise<UpdateDomainPolicyResponse.AsObject> {
return this.grpcService.admin.updateDomainPolicy(req, null).then((resp) => resp.toObject());
}
public getCustomDomainPolicy(orgId: string): Promise<GetCustomDomainPolicyResponse.AsObject> {
const req = new GetCustomDomainPolicyRequest();
req.setOrgId(orgId);
return this.grpcService.admin.getCustomOrgIAMPolicy(req, null).then((resp) => resp.toObject());
return this.grpcService.admin.getCustomDomainPolicy(req, null).then((resp) => resp.toObject());
}
public addCustomOrgIAMPolicy(
orgId: string,
userLoginMustBeDomain: boolean,
): Promise<AddCustomOrgIAMPolicyResponse.AsObject> {
const req = new AddCustomOrgIAMPolicyRequest();
public addCustomDomainPolicy(req: AddCustomDomainPolicyRequest): Promise<AddCustomOrgIAMPolicyResponse.AsObject> {
return this.grpcService.admin.addCustomDomainPolicy(req, null).then((resp) => resp.toObject());
}
public updateCustomDomainPolicy(req: UpdateCustomDomainPolicyRequest): Promise<UpdateCustomDomainPolicyResponse.AsObject> {
return this.grpcService.admin.updateCustomDomainPolicy(req, null).then((resp) => resp.toObject());
}
public resetCustomDomainPolicyToDefault(orgId: string): Promise<ResetCustomDomainPolicyToDefaultResponse.AsObject> {
const req = new ResetCustomDomainPolicyToDefaultRequest();
req.setOrgId(orgId);
req.setUserLoginMustBeDomain(userLoginMustBeDomain);
return this.grpcService.admin.addCustomOrgIAMPolicy(req, null).then((resp) => resp.toObject());
}
public updateCustomOrgIAMPolicy(
orgId: string,
userLoginMustBeDomain: boolean,
): Promise<UpdateCustomOrgIAMPolicyResponse.AsObject> {
const req = new UpdateCustomOrgIAMPolicyRequest();
req.setOrgId(orgId);
req.setUserLoginMustBeDomain(userLoginMustBeDomain);
return this.grpcService.admin.updateCustomOrgIAMPolicy(req, null).then((resp) => resp.toObject());
}
public resetCustomOrgIAMPolicyToDefault(orgId: string): Promise<ResetCustomOrgIAMPolicyToDefaultResponse.AsObject> {
const req = new ResetCustomOrgIAMPolicyToDefaultRequest();
req.setOrgId(orgId);
return this.grpcService.admin.resetCustomOrgIAMPolicyToDefault(req, null).then((resp) => resp.toObject());
}
/* admin iam */
public getOrgIAMPolicy(): Promise<GetOrgIAMPolicyResponse.AsObject> {
const req = new GetOrgIAMPolicyRequest();
return this.grpcService.admin.getOrgIAMPolicy(req, null).then((resp) => resp.toObject());
}
public updateOrgIAMPolicy(userLoginMustBeDomain: boolean): Promise<UpdateOrgIAMPolicyResponse.AsObject> {
const req = new UpdateOrgIAMPolicyRequest();
req.setUserLoginMustBeDomain(userLoginMustBeDomain);
return this.grpcService.admin.updateOrgIAMPolicy(req, null).then((resp) => resp.toObject());
return this.grpcService.admin.resetCustomDomainPolicyToDefault(req, null).then((resp) => resp.toObject());
}
/* policies end */

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, finalize, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
@ -83,7 +84,7 @@ import {
} from '../proto/generated/zitadel/auth_pb';
import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
import { Org, OrgQuery } from '../proto/generated/zitadel/org_pb';
import { Org, OrgFieldName, OrgQuery } from '../proto/generated/zitadel/org_pb';
import { Gender, MembershipQuery, User, WebAuthNVerification } from '../proto/generated/zitadel/user_pb';
import { GrpcService } from './grpc.service';
import { StorageKey, StorageLocation, StorageService } from './storage.service';
@ -262,6 +263,8 @@ export class GrpcAuthService {
limit?: number,
offset?: number,
queryList?: OrgQuery[],
sortingColumn?: OrgFieldName,
sortingDirection?: SortDirection,
): Promise<ListMyProjectOrgsResponse.AsObject> {
const req = new ListMyProjectOrgsRequest();
const query = new ListQuery();
@ -274,6 +277,9 @@ export class GrpcAuthService {
if (queryList) {
req.setQueriesList(queryList);
}
// if (sortingColumn) {
// req.setSortingColumn(sortingColumn);
// }
req.setQuery(query);

View File

@ -131,6 +131,8 @@ import {
GetDefaultVerifyEmailMessageTextResponse,
GetDefaultVerifyPhoneMessageTextRequest,
GetDefaultVerifyPhoneMessageTextResponse,
GetDomainPolicyRequest,
GetDomainPolicyResponse,
GetFlowRequest,
GetFlowResponse,
GetGrantedProjectByIDRequest,
@ -155,8 +157,6 @@ import {
GetOIDCInformationResponse,
GetOrgByDomainGlobalRequest,
GetOrgByDomainGlobalResponse,
GetOrgIAMPolicyRequest,
GetOrgIAMPolicyResponse,
GetOrgIDPByIDRequest,
GetOrgIDPByIDResponse,
GetPasswordAgePolicyRequest,
@ -1225,9 +1225,10 @@ export class ManagementService {
const req = new RemoveCustomLabelPolicyLogoDarkRequest();
return this.grpcService.mgmt.removeCustomLabelPolicyLogoDark(req, null).then((resp) => resp.toObject());
}
public getOrgIAMPolicy(): Promise<GetOrgIAMPolicyResponse.AsObject> {
const req = new GetOrgIAMPolicyRequest();
return this.grpcService.mgmt.getOrgIAMPolicy(req, null).then((resp) => resp.toObject());
public getDomainPolicy(): Promise<GetDomainPolicyResponse.AsObject> {
const req = new GetDomainPolicyRequest();
return this.grpcService.mgmt.getDomainPolicy(req, null).then((resp) => resp.toObject());
}
public getPasswordAgePolicy(): Promise<GetPasswordAgePolicyResponse.AsObject> {

View File

@ -818,6 +818,7 @@
"NOTIFICATIONS": "Benachrichtigungen",
"MESSAGETEXTS": "Benachrichtigungstexte",
"IDP": "Identity Provider",
"DOMAIN": "Domain Einstellungen",
"LOGINTEXTS": "Login Interface Texte",
"BRANDING": "Branding",
"PRIVACYPOLICY": "Datenschutzrichtlinie",
@ -827,6 +828,7 @@
"GROUPS": {
"NOTIFICATIONS": "Benachrichtigungen",
"LOGIN": "Login und Zugriff",
"DOMAIN": "Domain",
"TEXTS": "Texte und Sprachen",
"APPEARANCE": "Erscheinungsbild",
"OTHER": "Anderes"
@ -846,6 +848,8 @@
"HOST": "Host",
"USER": "Benutzer",
"PASSWORD": "Passwort",
"SETPASSWORD": "SMTP Passwort setzen",
"PASSWORDSET": "SMTP Passwort erfolgrech gesetzt.",
"TLS": "Transport Layer Security (TLS)",
"SAVED": "Erfolgreich gespeichert.",
"REQUIREDWARN": "Damit Mails von Ihrer Domain verschickt werden können, müssen Sie Ihre SMTP Einstellungen konfigurieren."
@ -949,9 +953,8 @@
"TITLE": "Passwortsperre",
"DESCRIPTION": "Lege eine maximale Anzahl an Passwordwiederholungen fest, nachdem Accounts gesperrt werden sollen."
},
"IAM_POLICY": {
"TITLE": "Zugangseinstellungen IAM",
"DESCRIPTION": "Definiere die Zugangseistellungen für Benutzer."
"DOMAIN_POLICY": {
"TITLE": "Domain Einstellungen"
},
"PRIVATELABELING_POLICY": {
"TITLE": "Branding",
@ -965,6 +968,7 @@
"DESCRIPTIONCREATEADMIN": "Nutzer können sich mit den verfügbaren Idps authentifizieren.",
"DESCRIPTIONCREATEMGMT": "Nutzer können sich mit den verfügbaren Idps authentifizieren. Achtung: Es kann zwischen System- und organisationsspezifischen Providern gewählt werden.",
"ADVANCED": "Erweitert",
"LIFETIMEDURATIONS": "Login Lifetimes",
"SAVED": "Erfolgreich gespeichert."
},
"PRIVACY_POLICY": {
@ -1080,7 +1084,9 @@
"MAXATTEMPTS": "Maximale Anzahl an Versuchen",
"EXPIREWARNDAYS": "Ablauf Warnung nach Tagen",
"MAXAGEDAYS": "Maximale Gültigkeit in Tagen",
"USERLOGINMUSTBEDOMAIN": "Benutzer-Login muss eine Domain sein",
"USERLOGINMUSTBEDOMAIN": "Benutzer Loginname muss die Domain der Organisation beinhalten",
"VALIDATEORGDOMAINS": "Org Domains validieren",
"SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "SMTP Sender Addresse entspricht Instanzdomain",
"ALLOWUSERNAMEPASSWORD": "Benutzername Passwort erlaubt",
"ALLOWEXTERNALIDP": "Externer IDP erlaubt",
"ALLOWREGISTER": "Registrieren erlaubt",
@ -1097,7 +1103,13 @@
"DEFAULTREDIRECTURI": "Default Redirect URI",
"DEFAULTREDIRECTURI_DESC": "Definiert, wohin der Benutzer umgeleitet wird, wenn die Anmeldung ohne App-Kontext gestartet wurde (z. B. von Mail)",
"ERRORMSGPOPUP": "Fehler als Dialog Fenster",
"DISABLEWATERMARK": "Wasserzeichen ausblenden"
"DISABLEWATERMARK": "Wasserzeichen ausblenden",
"PASSWORDCHECKLIFETIME": "Passwort Check Lifetime",
"EXTERNALLOGINCHECKLIFETIME": "Externer Login Check Lifetime",
"MFAINITSKIPLIFETIME": "Multifaktor Init Lifetime",
"SECONDFACTORCHECKLIFETIME": "Zweitfaktor Check Lifetime",
"MULTIFACTORCHECKLIFETIME": "Multifaktor Check Lifetime",
"INHOURS": "Stunden"
},
"RESET": "Auf Instanzeinstellung zurücksetzen",
"CREATECUSTOM": "Benutzerdefinierte Richtlinie erstellen",

View File

@ -818,6 +818,7 @@
"NOTIFICATIONS": "Notification providers and SMTP",
"MESSAGETEXTS": "Message Texts",
"IDP": "Identity Providers",
"DOMAIN": "Domain settings",
"LOGINTEXTS": "Login Interface Texts",
"BRANDING": "Branding",
"PRIVACYPOLICY": "Privacy Policy",
@ -827,6 +828,7 @@
"GROUPS": {
"NOTIFICATIONS": "Notifications",
"LOGIN": "Login and Access",
"DOMAIN": "Domain",
"TEXTS": "Texts and Languages",
"APPEARANCE": "Appearance",
"OTHER": "Other"
@ -846,6 +848,8 @@
"HOST": "Host",
"USER": "User",
"PASSWORD": "Password",
"SETPASSWORD": "Set SMTP Password",
"PASSWORDSET": "SMTP Password was set successfully.",
"TLS": "Transport Layer Security (TLS)",
"SAVED": "Saved successfully!",
"REQUIREDWARN": "To send notifications from your domain, you have to enter your SMTP data."
@ -949,9 +953,8 @@
"TITLE": "Lockout Policy",
"DESCRIPTION": "Set a maximum number of passwordretries, after which accounts will be blocked."
},
"IAM_POLICY": {
"TITLE": "IAM Access Preferences",
"DESCRIPTION": "Define access properties of your users."
"DOMAIN_POLICY": {
"TITLE": "Domain Settings"
},
"PRIVATELABELING_POLICY": {
"TITLE": "Branding",
@ -965,6 +968,7 @@
"DESCRIPTIONCREATEADMIN": "Users can choose from the available identity providers below.",
"DESCRIPTIONCREATEMGMT": "Users can choose from the available identity providers below. Note: You can use System-set providers as well as providers set for your organization only.",
"ADVANCED": "Advanced",
"LIFETIMEDURATIONS": "Login Lifetimes",
"SAVED": "Saved successfully!"
},
"PRIVACY_POLICY": {
@ -1080,7 +1084,9 @@
"MAXATTEMPTS": "Password maximum Attempts",
"EXPIREWARNDAYS": "Expiration Warning after day",
"MAXAGEDAYS": "Max Age in days",
"USERLOGINMUSTBEDOMAIN": "User Login must be Domain",
"USERLOGINMUSTBEDOMAIN": "User Loginname must contain orgdomain",
"VALIDATEORGDOMAINS": "Validate Org domains",
"SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "SMTP Sender Address matches Instance Domain",
"ALLOWUSERNAMEPASSWORD": "Username Password allowed",
"ALLOWEXTERNALIDP": "External IDP allowed",
"ALLOWREGISTER": "Register allowed",
@ -1097,7 +1103,13 @@
"DEFAULTREDIRECTURI": "Default Redirect URI",
"DEFAULTREDIRECTURI_DESC": "Defines where the user will be redirected to if the login has started without an app context (e.g. from mail)",
"ERRORMSGPOPUP": "Show Error in Dialog",
"DISABLEWATERMARK": "Hide Watermark"
"DISABLEWATERMARK": "Hide Watermark",
"PASSWORDCHECKLIFETIME": "Password Check Lifetime",
"EXTERNALLOGINCHECKLIFETIME": "External Login Check Lifetime",
"MFAINITSKIPLIFETIME": "Multifactor Init Lifetime",
"SECONDFACTORCHECKLIFETIME": "Second Factor Check Lifetime",
"MULTIFACTORCHECKLIFETIME": "Multifactor Check Lifetime",
"INHOURS": "hours"
},
"RESET": "Reset to Instance default",
"CREATECUSTOM": "Create Custom Policy",

View File

@ -818,6 +818,7 @@
"NOTIFICATIONS": "Notifiche",
"MESSAGETEXTS": "Testi di notifica",
"IDP": "Identity Providers",
"DOMAIN": "Impostazioni del dominio",
"LOGINTEXTS": "Testi dell'interfaccia login",
"BRANDING": "Branding",
"PRIVACYPOLICY": "Informativa sulla privacy e TOS",
@ -827,6 +828,7 @@
"GROUPS": {
"NOTIFICATIONS": "Notifiche",
"LOGIN": "Accesso e login",
"DOMAIN": "Dominio",
"TEXTS": "Testi e lingue",
"APPEARANCE": "Aussehen",
"OTHER": "Altro"
@ -846,6 +848,8 @@
"HOST": "Host",
"USER": "Utente",
"PASSWORD": "Password",
"SETPASSWORD": "Imposta SMTP Password",
"PASSWORDSET": "SMTP Password impostata con successo.",
"TLS": "Transport Layer Security (TLS)",
"SAVED": "Salvato con successo!",
"REQUIREDWARN": "Per inviare notifiche dal tuo dominio, devi inserire i tuoi dati SMTP."
@ -949,9 +953,8 @@
"TITLE": "Impostazioni di blocco",
"DESCRIPTION": "Imposta un numero massimo di tentativi di password, dopo i quali gli account saranno bloccati."
},
"IAM_POLICY": {
"TITLE": "Impostazioni di accesso IAM",
"DESCRIPTION": "Definisci le propriet\u00e0 di accesso dei tuoi utenti."
"DOMAIN_POLICY": {
"TITLE": "Impostazioni dominio"
},
"PRIVATELABELING_POLICY": {
"TITLE": "Branding",
@ -965,6 +968,7 @@
"DESCRIPTIONCREATEADMIN": "Gli utenti possono scegliere tra gli IDP disponibili qui sotto.",
"DESCRIPTIONCREATEMGMT": "Gli utenti possono scegliere tra gli IDP disponibili qui sotto. Nota: puoi usare i provider impostati nel sistema e quelli impostati della tua organizzazione.",
"ADVANCED": "Impostazioni avanzate",
"LIFETIMEDURATIONS": "Login Lifetimes",
"SAVED": "Salvato con successo!"
},
"PRIVACY_POLICY": {
@ -1080,7 +1084,9 @@
"MAXATTEMPTS": "Massimo numero di tentativi di password",
"EXPIREWARNDAYS": "Avviso scadenza dopo il giorno",
"MAXAGEDAYS": "Lunghezza massima in giorni",
"USERLOGINMUSTBEDOMAIN": "Loginname dell'utente deve contenere il dominio",
"USERLOGINMUSTBEDOMAIN": "Nome utente deve contenere il dominio dell' organizzazione",
"VALIDATEORGDOMAINS": "Verifica domini dell' organizzazione",
"SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "L'indirizzo mittente SMTP corrisponde al dominio dell'istanza",
"ALLOWUSERNAMEPASSWORD": "Autenticazione classica con password consentita",
"ALLOWEXTERNALIDP": "IDP esterno consentito",
"ALLOWREGISTER": "Registrazione consentita",
@ -1097,7 +1103,13 @@
"DEFAULTREDIRECTURI": "Default Redirect URI",
"DEFAULTREDIRECTURI_DESC": "Definisce dove verrà reindirizzato l'utente se l'accesso è stato avviato senza un contesto dell'app (ad es. dall' email)",
"ERRORMSGPOPUP": "Mostra l'errore nella finestra di dialogo",
"DISABLEWATERMARK": "Nascondi la filigrana"
"DISABLEWATERMARK": "Nascondi la filigrana",
"PASSWORDCHECKLIFETIME": "Lifetime verificazione password",
"EXTERNALLOGINCHECKLIFETIME": "Lifetime verificazione login esterno",
"MFAINITSKIPLIFETIME": "Lifetime Initalizzazione Multifattore",
"SECONDFACTORCHECKLIFETIME": "Lifetime Second Factor Lifetime",
"MULTIFACTORCHECKLIFETIME": "Lifetime Multi Factor",
"INHOURS": "ore"
},
"RESET": "Ripristina l'impostazione dell'istanza",
"CREATECUSTOM": "Crea un'impostazione personalizzata",

View File

@ -43,7 +43,6 @@
@import 'src/app/modules/filter/filter.component.scss';
@import 'src/app/modules/policies/message-texts/message-texts.component.scss';
@import 'src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.scss';
@import 'src/app/modules/policies/private-labeling-policy/preview/preview.component.scss';
@import 'src/app/modules/info-row/info-row.component.scss';
@import 'src/app/modules/sidenav/sidenav.component';
@import 'src/app/modules/user-grants/user-grants.component.scss';
@ -104,7 +103,6 @@
@include sidenav-theme($theme);
@include info-section-theme($theme);
@include filter-theme($theme);
@include preview-theme($theme);
@include private-label-theme($theme);
@include project-grid-theme($theme);
@include granted-project-detail-theme($theme);

View File

@ -425,10 +425,6 @@ $custom-typography: mat.define-typography-config(
--success: #10b981;
$border-color: map-get($foreground, divider);
.main-container {
color: map-get($foreground, base);
}
.mat-menu-panel {
background-color: map-get($background, cards);
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);

View File

@ -1,14 +1,10 @@
@use '@angular/material' as mat;
@mixin toast-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
$primary-light-color: mat.get-color-from-palette($primary, 200);
$warn: map-get($theme, warn);
$warn-color: mat.get-color-from-palette($warn, 500);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
// .data-e2e-success {
// background-color: map-get($background, cards) !important;
@ -17,6 +13,6 @@
.data-e2e-failure {
background-color: $warn-color !important;
color: map-get($foreground, text) !important;
color: mat.get-color-from-palette($warn, default-contrast) !important;
}
}