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; width: 32px;
line-height: 1rem; line-height: 1rem;
border-radius: 50%; border-radius: 50%;
-webkit-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: 2px 0 7px -1px rgba(33, 34, 36, 0.5); -moz-box-shadow: 1px 0 3px -1px rgba(33, 34, 36, 0.5);
box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5); box-shadow: 1px 0 3px -1px rgba(33, 34, 36, 0.5);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;

View File

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

View File

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

View File

@ -1,31 +1,18 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()"> <cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
<div class="filter-row" id="filtercomp"> <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"> <div class="email-query">
<mat-checkbox id="name" class="cb" [checked]="getSubFilter(SubQuery.NAME)" <mat-checkbox
(change)="changeCheckbox(SubQuery.NAME, $event )">{{'FILTER.PROJECTNAME' | translate}} id="name"
class="cb"
[checked]="getSubFilter(SubQuery.NAME)"
(change)="changeCheckbox(SubQuery.NAME, $event)"
>{{ 'FILTER.PROJECTNAME' | translate }}
</mat-checkbox> </mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.NAME) as eq"> <div class="subquery" *ngIf="getSubFilter(SubQuery.NAME) as eq">
<cnsl-form-field class="filter-select-method"> <cnsl-form-field class="filter-select-method">
<mat-select [value]="eq.getMethod()" (selectionChange)="setMethod(eq, $event)"> <mat-select [value]="eq.getMethod()" (selectionChange)="setMethod(eq, $event)">
<mat-option *ngFor="let method of methods" [value]="method"> <mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate}} {{ 'FILTER.METHODS.' + method | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>

View File

@ -50,7 +50,7 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
}); });
this.searchQueries = projectQueries.filter((q) => q !== undefined) as ProjectQuery[]; 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.showFilter = true;
// this.filterOpen.emit(true); // this.filterOpen.emit(true);
} }
@ -60,15 +60,6 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) { public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
if (event.checked) { if (event.checked) {
switch (subquery) { 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: case SubQuery.NAME:
const nq = new ProjectNameQuery(); const nq = new ProjectNameQuery();
nq.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE); nq.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
@ -82,14 +73,6 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
} }
} else { } else {
switch (subquery) { 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: case SubQuery.NAME:
const index_dn = this.searchQueries.findIndex((q) => (q as ProjectQuery).toObject().nameQuery !== undefined); const index_dn = this.searchQueries.findIndex((q) => (q as ProjectQuery).toObject().nameQuery !== undefined);
if (index_dn > -1) { if (index_dn > -1) {
@ -103,28 +86,15 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public setValue(subquery: SubQuery, query: any, event: any) { public setValue(subquery: SubQuery, query: any, event: any) {
const value = event?.target?.value ?? event.value; const value = event?.target?.value ?? event.value;
switch (subquery) { switch (subquery) {
// case SubQuery.RESOURCEOWNER:
// (query as ProjectResourceOwnerQuery).setResourceOwner(value);
// this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
// break;
case SubQuery.NAME: case SubQuery.NAME:
(query as ProjectNameQuery).setName(value); (query as ProjectNameQuery).setName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
} }
this.filterCount$.next(this.filterCount);
} }
public getSubFilter(subquery: SubQuery): any { public getSubFilter(subquery: SubQuery): any {
switch (subquery) { 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: case SubQuery.NAME:
const dn = this.searchQueries.find((q) => (q as ProjectQuery).toObject().nameQuery !== undefined); const dn = this.searchQueries.find((q) => (q as ProjectQuery).toObject().nameQuery !== undefined);
if (dn) { if (dn) {
@ -137,15 +107,13 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public setMethod(query: any, event: any) { public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value); (query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
} }
public emitFilter(): void { public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false; this.showFilter = false;
this.filterOpen.emit(false); this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
} }
public resetFilter(): void { public resetFilter(): void {
@ -153,7 +121,7 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
this.emitFilter(); this.emitFilter();
} }
public get filterCount(): number { public get filterCounter(): number {
return this.searchQueries.length; 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="filter-row" id="filtercomp">
<div class="name-query"> <div class="name-query">
<mat-checkbox <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.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.showFilter = true;
// this.filterOpen.emit(true); // this.filterOpen.emit(true);
} }
@ -170,23 +170,21 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
switch (subquery) { switch (subquery) {
case SubQuery.DISPLAYNAME: case SubQuery.DISPLAYNAME:
(query as DisplayNameQuery).setDisplayName(event?.target?.value); (query as DisplayNameQuery).setDisplayName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
case SubQuery.USERNAME: case SubQuery.USERNAME:
(query as UserNameQuery).setUserName(event?.target?.value); (query as UserNameQuery).setUserName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
case SubQuery.ORGNAME: case SubQuery.ORGNAME:
(query as UserGrantOrgNameQuery).setOrgName(event?.target?.value); (query as UserGrantOrgNameQuery).setOrgName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
case SubQuery.PROJECTNAME: case SubQuery.PROJECTNAME:
(query as UserGrantProjectNameQuery).setProjectName(event?.target?.value); (query as UserGrantProjectNameQuery).setProjectName(event?.target?.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
} }
this.filterCount$.next(this.filterCount);
} }
public getSubFilter(subquery: SubQuery): any { public getSubFilter(subquery: SubQuery): any {
@ -225,23 +223,17 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
public setMethod(query: any, event: any) { public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value); (query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
} }
public emitFilter(): void { public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false; this.showFilter = false;
this.filterOpen.emit(false); this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
} }
public resetFilter(): void { public resetFilter(): void {
this.searchQueries = []; this.searchQueries = [];
this.emitFilter(); 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="filter-row" id="filtercomp">
<div class="name-query"> <div class="name-query">
<mat-checkbox id="state" class="cb" [checked]="getSubFilter(SubQuery.STATE)" <mat-checkbox
(change)="changeCheckbox(SubQuery.STATE, $event )">{{'FILTER.STATE' | translate}} id="state"
class="cb"
[checked]="getSubFilter(SubQuery.STATE)"
(change)="changeCheckbox(SubQuery.STATE, $event)"
>{{ 'FILTER.STATE' | translate }}
</mat-checkbox> </mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.STATE) as sq"> <div class="subquery" *ngIf="getSubFilter(SubQuery.STATE) as sq">
<span class="nomethod cnsl-secondary-text"> <span class="nomethod cnsl-secondary-text">
@ -10,9 +14,9 @@
</span> </span>
<cnsl-form-field class="filter-select-value" appearance="outline"> <cnsl-form-field class="filter-select-value" appearance="outline">
<mat-select [value]="sq.getState()" (selectionChange)="setValue(SubQuery.STATE,sq, $event)"> <mat-select [value]="sq.getState()" (selectionChange)="setValue(SubQuery.STATE, sq, $event)">
<mat-option *ngFor="let state of states" [value]="state"> <mat-option *ngFor="let state of states" [value]="state">
{{ 'USER.STATE.'+state | translate }} {{ 'USER.STATE.' + state | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
@ -20,61 +24,75 @@
</div> </div>
<div class="name-query"> <div class="name-query">
<mat-checkbox id="displayname" class="cb" [checked]="getSubFilter(SubQuery.DISPLAYNAME)" <mat-checkbox
(change)="changeCheckbox(SubQuery.DISPLAYNAME, $event )">{{'FILTER.DISPLAYNAME' | translate}} id="displayname"
class="cb"
[checked]="getSubFilter(SubQuery.DISPLAYNAME)"
(change)="changeCheckbox(SubQuery.DISPLAYNAME, $event)"
>{{ 'FILTER.DISPLAYNAME' | translate }}
</mat-checkbox> </mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.DISPLAYNAME) as dnq"> <div class="subquery" *ngIf="getSubFilter(SubQuery.DISPLAYNAME) as dnq">
<cnsl-form-field class="filter-select-method" appearance="outline"> <cnsl-form-field class="filter-select-method" appearance="outline">
<mat-select [value]="dnq.getMethod()" (selectionChange)="setMethod(dnq, $event)"> <mat-select [value]="dnq.getMethod()" (selectionChange)="setMethod(dnq, $event)">
<mat-option *ngFor="let method of methods" [value]="method"> <mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate }} {{ 'FILTER.METHODS.' + method | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="filter-input-value" appearance="outline"> <cnsl-form-field class="filter-input-value" appearance="outline">
<input cnslInput name="value" [value]="dnq.getDisplayName()" <input
(change)="setValue(SubQuery.DISPLAYNAME, dnq, $event)" /> cnslInput
name="value"
[value]="dnq.getDisplayName()"
(change)="setValue(SubQuery.DISPLAYNAME, dnq, $event)"
/>
</cnsl-form-field> </cnsl-form-field>
</div> </div>
</div> </div>
<div class="email-query"> <div class="email-query">
<mat-checkbox id="email" class="cb" [checked]="getSubFilter(SubQuery.EMAIL)" <mat-checkbox
(change)="changeCheckbox(SubQuery.EMAIL, $event )">{{'FILTER.EMAIL' | translate}} id="email"
class="cb"
[checked]="getSubFilter(SubQuery.EMAIL)"
(change)="changeCheckbox(SubQuery.EMAIL, $event)"
>{{ 'FILTER.EMAIL' | translate }}
</mat-checkbox> </mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.EMAIL) as eq"> <div class="subquery" *ngIf="getSubFilter(SubQuery.EMAIL) as eq">
<cnsl-form-field class="filter-select-method" appearance="outline"> <cnsl-form-field class="filter-select-method" appearance="outline">
<mat-select [value]="eq.getMethod()" (selectionChange)="setMethod(eq, $event)"> <mat-select [value]="eq.getMethod()" (selectionChange)="setMethod(eq, $event)">
<mat-option *ngFor="let method of methods" [value]="method"> <mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate}} {{ 'FILTER.METHODS.' + method | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="filter-input-value" appearance="outline"> <cnsl-form-field class="filter-input-value" appearance="outline">
<input cnslInput name="value" [value]="eq.getEmailAddress()" <input cnslInput name="value" [value]="eq.getEmailAddress()" (change)="setValue(SubQuery.EMAIL, eq, $event)" />
(change)="setValue(SubQuery.EMAIL, eq, $event)" />
</cnsl-form-field> </cnsl-form-field>
</div> </div>
</div> </div>
<div class="usernane-query"> <div class="usernane-query">
<mat-checkbox id="username" class="cb" [checked]="getSubFilter(SubQuery.USERNAME)" <mat-checkbox
(change)="changeCheckbox(SubQuery.USERNAME, $event )">{{'FILTER.USERNAME' | translate}} id="username"
class="cb"
[checked]="getSubFilter(SubQuery.USERNAME)"
(change)="changeCheckbox(SubQuery.USERNAME, $event)"
>{{ 'FILTER.USERNAME' | translate }}
</mat-checkbox> </mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.USERNAME) as unq"> <div class="subquery" *ngIf="getSubFilter(SubQuery.USERNAME) as unq">
<cnsl-form-field class="filter-select-method" appearance="outline"> <cnsl-form-field class="filter-select-method" appearance="outline">
<mat-select [value]="unq.getMethod()" (selectionChange)="setMethod(unq, $event)"> <mat-select [value]="unq.getMethod()" (selectionChange)="setMethod(unq, $event)">
<mat-option *ngFor="let method of methods" [value]="method"> <mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate}} {{ 'FILTER.METHODS.' + method | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="filter-input-value" appearance="outline"> <cnsl-form-field class="filter-input-value" appearance="outline">
<input cnslInput name="value" [value]="unq.getUserName()" <input cnslInput name="value" [value]="unq.getUserName()" (change)="setValue(SubQuery.USERNAME, unq, $event)" />
(change)="setValue(SubQuery.USERNAME, unq, $event)" />
</cnsl-form-field> </cnsl-form-field>
</div> </div>
</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.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.showFilter = true;
// this.filterOpen.emit(true); // this.filterOpen.emit(true);
} }
@ -181,23 +181,21 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
switch (subquery) { switch (subquery) {
case SubQuery.STATE: case SubQuery.STATE:
(query as StateQuery).setState(value); (query as StateQuery).setState(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
case SubQuery.DISPLAYNAME: case SubQuery.DISPLAYNAME:
(query as DisplayNameQuery).setDisplayName(value); (query as DisplayNameQuery).setDisplayName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
case SubQuery.EMAIL: case SubQuery.EMAIL:
(query as EmailQuery).setEmailAddress(value); (query as EmailQuery).setEmailAddress(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
case SubQuery.USERNAME: case SubQuery.USERNAME:
(query as UserNameQuery).setUserName(value); (query as UserNameQuery).setUserName(value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; break;
} }
this.filterCount$.next(this.filterCount);
} }
public getSubFilter(subquery: SubQuery): any { public getSubFilter(subquery: SubQuery): any {
@ -235,23 +233,17 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
public setMethod(query: any, event: any) { public setMethod(query: any, event: any) {
(query as UserNameQuery).setMethod(event.value); (query as UserNameQuery).setMethod(event.value);
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
} }
public emitFilter(): void { public emitFilter(): void {
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false; this.showFilter = false;
this.filterOpen.emit(false); this.filterOpen.emit(false);
this.filterCount$.next(this.filterCount);
} }
public resetFilter(): void { public resetFilter(): void {
this.searchQueries = []; this.searchQueries = [];
this.emitFilter(); this.emitFilter();
} }
public get filterCount(): number {
return this.searchQueries.length;
}
} }

View File

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

View File

@ -1,7 +1,7 @@
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay'; 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 { 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 { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb'; import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
@ -24,15 +24,16 @@ type FilterSearchQueryAsObject =
styleUrls: ['./filter.component.scss'], styleUrls: ['./filter.component.scss'],
}) })
export class FilterComponent implements OnDestroy { 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 filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
@Output() public resetted: EventEmitter<void> = new EventEmitter(); @Output() public resetted: EventEmitter<void> = new EventEmitter();
@Output() public trigger: 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 showFilter: boolean = false;
public methods: TextQueryMethod[] = [ public methods: TextQueryMethod[] = [
@ -59,7 +60,6 @@ export class FilterComponent implements OnDestroy {
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.filterCount$.complete();
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
@ -83,6 +83,7 @@ export class FilterComponent implements OnDestroy {
queryParams: { queryParams: {
['filter']: JSON.stringify(filters), ['filter']: JSON.stringify(filters),
}, },
replaceUrl: true,
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
skipLocationChange: false, skipLocationChange: false,
}); });

View File

@ -68,9 +68,16 @@ export class HeaderComponent implements OnDestroy {
} }
public get isOnInstance(): boolean { public get isOnInstance(): boolean {
return ( const pages: string[] = [
['/instance', '/views', '/orgs', '/settings', '/failed-events', '/instance/members'].includes(this.router.url) || '/instance',
this.router.url.includes('/settings') '/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" <cnsl-refresh-table
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async"> *ngIf="dataSource"
(refreshed)="changePage()"
[dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp"
[selection]="selection"
[loading]="dataSource?.loading$ | async"
>
<ng-container actions *ngIf="selection.hasValue()"> <ng-container actions *ngIf="selection.hasValue()">
<ng-content select="[selectactions]"></ng-content> <ng-content select="[selectactions]"></ng-content>
</ng-container> </ng-container>
@ -13,17 +18,32 @@
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource"> <table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef> <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()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"> [indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox> </mat-checkbox>
</th> </th>
<td class="selection" mat-cell *matCellDef="let row"> <td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()" <mat-checkbox
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"> [disabled]="!canWrite"
<cnsl-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar" color="primary"
[name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [avatarUrl]="row.avatarUrl|| ''" (click)="$event.stopPropagation()"
[forColor]="row?.preferredLoginName" [size]="32"> (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> </cnsl-avatar>
<ng-template #cog> <ng-template #cog>
<cnsl-avatar [forColor]="row.preferredLoginName" [isMachine]="true"> <cnsl-avatar [forColor]="row.preferredLoginName" [isMachine]="true">
@ -35,27 +55,30 @@
</ng-container> </ng-container>
<ng-container matColumnDef="userId"> <ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.USERID' | translate }}</th>
<td mat-cell *matCellDef="let member"> <td mat-cell *matCellDef="let member">
{{member.userId}} </td> {{ member.userId }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="displayName"> <ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.DISPLAYNAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.DISPLAYNAME' | translate }}</th>
<td mat-cell *matCellDef="let member"> <td mat-cell *matCellDef="let member">
{{member.displayName}} </td> {{ member.displayName }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="loginname"> <ng-container matColumnDef="loginname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LOGINNAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.LOGINNAME' | translate }}</th>
<td mat-cell *matCellDef="let member"> <td mat-cell *matCellDef="let member">
{{member.preferredLoginName}} </td> {{ member.preferredLoginName }}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="email"> <ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th> <th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.MEMBER.EMAIL' | translate }}</th>
<td mat-cell *matCellDef="let member"> <td mat-cell *matCellDef="let member">
{{member.email}} {{ member.email }}
</td> </td>
</ng-container> </ng-container>
@ -63,24 +86,36 @@
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let member" class="member-action-tr"> <td mat-cell *matCellDef="let member" class="member-action-tr">
<cnsl-table-actions [hasActions]="true"> <cnsl-table-actions [hasActions]="true">
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" <button
(click)="triggerDeleteMember(member)" mat-icon-button><i class="las la-trash"></i></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]"> <button menuActions mat-menu-item [routerLink]="['/users', member.userId]">
{{'ACTIONS.TABLE.SHOWUSER' | translate : ({value: member.displayName})}} {{ 'ACTIONS.TABLE.SHOWUSER' | translate: { value: member.displayName } }}
</button> </button>
</cnsl-table-actions> </cnsl-table-actions>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="roles"> <ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef class="role-row"> {{ 'ROLESLABEL' | translate }} </th> <th mat-header-cell *matHeaderCellDef class="role-row">{{ 'ROLESLABEL' | translate }}</th>
<td mat-cell *matCellDef="let member" class="role-row"> <td mat-cell *matCellDef="let member" class="role-row">
<mat-chip-list class="cnsl-chip-list" aria-label="role selection"> <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" <mat-chip
(removed)="removeRole(member, role)"> 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> <div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
<span>{{role | roletransform}}</span> <span>{{ role | roletransform }}</span>
<button matChipRemove> <button matChipRemove>
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
</button> </button>
@ -90,13 +125,23 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight pointer" (click)="addRole(member)" mat-row <tr
*matRowDef="let member; columns: displayedColumns;"> class="highlight pointer"
</tr> (click)="addRole(member)"
mat-row
*matRowDef="let member; columns: displayedColumns"
></tr>
</table> </table>
</div> </div>
<cnsl-paginator *ngIf="dataSource" class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp" <cnsl-paginator
[pageSize]="INITIALPAGESIZE" [length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]" *ngIf="dataSource"
(page)="changePage($event)"> class="paginator"
#paginator
[timestamp]="dataSource?.viewTimestamp"
[pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult"
[pageSizeOptions]="[25, 50, 100, 250]"
(page)="changePage($event)"
>
</cnsl-paginator> </cnsl-paginator>
</cnsl-refresh-table> </cnsl-refresh-table>

View File

@ -15,7 +15,14 @@
</a> </a>
</ng-template> </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"> <ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef> <th class="selection" mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.ACTIVE' | translate }} {{ 'ORG.PAGES.ACTIVE' | translate }}
@ -51,6 +58,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<!-- mat-sort-header -->
<th mat-header-cell *matHeaderCellDef> <th mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.NAME' | translate }} {{ 'ORG.PAGES.NAME' | translate }}
</th> </th>

View File

@ -1,9 +1,11 @@
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { Component, Input, ViewChild } from '@angular/core'; import { Component, Input, ViewChild } from '@angular/core';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, takeUntil } from 'rxjs'; 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 { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -38,6 +40,7 @@ export class OrgTableComponent {
public filterOpen: boolean = false; public filterOpen: boolean = false;
public OrgState: any = OrgState; public OrgState: any = OrgState;
public copied: string = ''; public copied: string = '';
@ViewChild(MatSort) public sort!: MatSort;
private searchQueries: OrgQuery[] = []; private searchQueries: OrgQuery[] = [];
private destroy$: Subject<void> = new Subject(); private destroy$: Subject<void> = new Subject();
@ -48,7 +51,12 @@ export class OrgTableComponent {
}); });
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$)); 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.requestOrgs$.next({ limit: this.initialLimit, offset: 0, queries: this.searchQueries });
this.authService.getActiveOrg().then((org) => (this.activeOrg = org)); this.authService.getActiveOrg().then((org) => (this.activeOrg = org));
@ -60,7 +68,17 @@ export class OrgTableComponent {
public loadOrgs(request: Request): Observable<Org.AsObject[]> { public loadOrgs(request: Request): Observable<Org.AsObject[]> {
this.loadingSubject.next(true); 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) => { map((resp) => {
this.timestamp = resp.details?.viewTimestamp; this.timestamp = resp.details?.viewTimestamp;
this.totalResult = resp.details?.totalResult ?? 0; 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 { public applySearchQuery(searchQueries: OrgQuery[]): void {
this.searchQueries = searchQueries; this.searchQueries = searchQueries;
this.requestOrgs$.next({ 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 { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { Subscription } from 'rxjs'; 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 { GetOrgIAMPolicyResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_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 { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.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 { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
selector: 'cnsl-org-iam-policy', selector: 'cnsl-domain-policy',
templateUrl: './org-iam-policy.component.html', templateUrl: './domain-policy.component.html',
styleUrls: ['./org-iam-policy.component.scss'], styleUrls: ['./domain-policy.component.scss'],
}) })
export class OrgIamPolicyComponent implements OnInit, OnDestroy { export class DomainPolicyComponent implements OnInit, OnDestroy {
private managementService!: ManagementService; private managementService!: ManagementService;
@Input() public serviceType!: PolicyComponentServiceType; @Input() public serviceType!: PolicyComponentServiceType;
public iamData!: OrgIAMPolicy.AsObject; public domainData!: DomainPolicy.AsObject;
public loading: boolean = false;
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
private org!: Org.AsObject; private org!: Org.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType; 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 { ngOnInit(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) { if (this.serviceType === PolicyComponentServiceType.MGMT) {
@ -40,22 +51,32 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
} }
public fetchData(): void { public fetchData(): void {
this.getData().then((resp) => { this.loading = true;
if (resp?.policy) { this.getData()
this.iamData = resp.policy; .then((resp) => {
} this.loading = false;
}); if (resp?.policy) {
this.domainData = resp.policy;
}
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
} }
private async getData(): Promise<GetCustomOrgIAMPolicyResponse.AsObject | GetOrgIAMPolicyResponse.AsObject | any> { 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) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
return this.managementService.getOrgIAMPolicy(); return this.managementService.getDomainPolicy();
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
if (this.org?.id) { return this.adminService.getCustomDomainPolicy(this.org.id);
return this.adminService.getCustomOrgIAMPolicy(this.org.id);
}
break;
default: default:
return Promise.reject(); return Promise.reject();
} }
@ -64,23 +85,33 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
public savePolicy(): void { public savePolicy(): void {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: 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 this.adminService
.addCustomOrgIAMPolicy(this.org.id, this.iamData.userLoginMustBeDomain) .addCustomDomainPolicy(req)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);
}); });
break; break;
} else { } 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 this.adminService
.updateCustomOrgIAMPolicy(this.org.id, this.iamData.userLoginMustBeDomain) .updateCustomDomainPolicy(req)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);
@ -88,12 +119,15 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
break; break;
} }
case PolicyComponentServiceType.ADMIN: 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 this.adminService
.updateOrgIAMPolicy(this.iamData.userLoginMustBeDomain) .updateDomainPolicy(req)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData();
}) })
.catch((error) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);
@ -105,7 +139,7 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
public removePolicy(): void { public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) { if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.adminService this.adminService
.resetCustomOrgIAMPolicyToDefault(this.org.id) .resetCustomDomainPolicyToDefault(this.org.id)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true); this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => { setTimeout(() => {
@ -119,8 +153,8 @@ export class OrgIamPolicyComponent implements OnInit, OnDestroy {
} }
public get isDefault(): boolean { public get isDefault(): boolean {
if (this.iamData && this.serviceType === PolicyComponentServiceType.MGMT) { if (this.domainData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.iamData as OrgIAMPolicy.AsObject).isDefault; return (this.domainData as OrgIAMPolicy.AsObject).isDefault;
} else { } else {
return false; return false;
} }

View File

@ -2,8 +2,9 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon'; 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 { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; 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 { CardModule } from '../../card/card.module';
import { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { OrgIamPolicyRoutingModule } from './org-iam-policy-routing.module'; import { DomainPolicyComponent } from './domain-policy.component';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
@NgModule({ @NgModule({
declarations: [OrgIamPolicyComponent], declarations: [DomainPolicyComponent],
imports: [ imports: [
OrgIamPolicyRoutingModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
CardModule, CardModule,
InputModule, InputModule,
MatButtonModule, MatButtonModule,
HasRolePipeModule, HasRolePipeModule,
MatSlideToggleModule,
MatIconModule, MatIconModule,
HasRoleModule, HasRoleModule,
MatProgressSpinnerModule,
MatTooltipModule, MatTooltipModule,
InfoSectionModule, InfoSectionModule,
MatCheckboxModule,
TranslateModule, TranslateModule,
DetailLayoutModule, 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> <p class="cnsl-secondary-text">{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}</p>
<div *ngIf="loginData" class="login-policy-row"> <div *ngIf="loginData" class="login-policy-row">
<mat-slide-toggle <mat-checkbox card-actions class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.forceMfa">
card-actions
class="login-policy-toggle"
color="primary"
ngDefaultControl
[(ngModel)]="loginData.forceMfa"
>
{{ 'POLICY.DATA.FORCEMFA' | translate }} {{ 'POLICY.DATA.FORCEMFA' | translate }}
</mat-slide-toggle> </mat-checkbox>
</div> </div>
<cnsl-card class="max-card-width"> <cnsl-card class="max-card-width">
<cnsl-mfa-table <cnsl-mfa-table
@ -77,11 +71,60 @@
<br /> <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> <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"> <div class="login-policy-row">
<mat-slide-toggle <mat-checkbox
class="login-policy-toggle" class="login-policy-toggle"
color="primary" color="primary"
matTooltip="{{ 'POLICY.DATA.FORCEMFA_DESC' | translate }}" matTooltip="{{ 'POLICY.DATA.FORCEMFA_DESC' | translate }}"
@ -89,16 +132,16 @@
[(ngModel)]="loginData.allowUsernamePassword" [(ngModel)]="loginData.allowUsernamePassword"
> >
{{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate }} {{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate }}
</mat-slide-toggle> </mat-checkbox>
<!-- <cnsl-info-section class="info"> <!-- <cnsl-info-section class="info">
{{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate }} {{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate }}
</cnsl-info-section> --> </cnsl-info-section> -->
</div> </div>
<div class="login-policy-row"> <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 }} {{ 'POLICY.DATA.ALLOWREGISTER' | translate }}
</mat-slide-toggle> </mat-checkbox>
<!-- <ng-template #regInfo> <!-- <ng-template #regInfo>
<cnsl-info-section class="info"> <cnsl-info-section class="info">
@ -107,9 +150,9 @@
</ng-template> --> </ng-template> -->
</div> </div>
<div class="login-policy-row"> <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 }} {{ 'POLICY.DATA.ALLOWEXTERNALIDP' | translate }}
</mat-slide-toggle> </mat-checkbox>
<!-- <ng-template #idpInfo> <!-- <ng-template #idpInfo>
<cnsl-info-section class="info"> <cnsl-info-section class="info">
@ -119,9 +162,9 @@
</div> </div>
<div class="login-policy-row"> <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 }} {{ 'POLICY.DATA.HIDEPASSWORDRESET' | translate }}
</mat-slide-toggle> </mat-checkbox>
<!-- <ng-template #passwordResetInfo> <!-- <ng-template #passwordResetInfo>
<cnsl-info-section class="info"> <cnsl-info-section class="info">
@ -131,9 +174,14 @@
</div> </div>
<div class="login-policy-row"> <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 }} {{ 'POLICY.DATA.IGNOREUNKNOWNUSERNAMES' | translate }}
</mat-slide-toggle> </mat-checkbox>
</div> </div>
<div class="login-policy-row"> <div class="login-policy-row">
@ -142,19 +190,13 @@
<input cnslInput placeholder="https://" [(ngModel)]="loginData.defaultRedirectUri" /> <input cnslInput placeholder="https://" [(ngModel)]="loginData.defaultRedirectUri" />
</cnsl-form-field> </cnsl-form-field>
</div> </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> </div>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT"> <ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault">
<ng-template cnslHasRole [hasRole]="['policy.delete']"> <ng-template cnslHasRole [hasRole]="['policy.delete']">
<button <button
*ngIf="!isDefault"
color="primary" color="primary"
class="loginpolicy-reset-button"
matTooltip="{{ 'POLICY.RESET' | translate }}" matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn" color="warn"
(click)="removePolicy()" (click)="removePolicy()"
@ -164,3 +206,11 @@
</button> </button>
</ng-template> </ng-template>
</ng-container> </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; 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-content {
.login-policy-row { .login-policy-row {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@ -26,6 +40,11 @@
} }
} }
.loginpolicy-reset-button {
margin: 1rem 0;
display: block;
}
.login-policy-btn-container { .login-policy-btn-container {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@ -1,4 +1,6 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core'; 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 { import {
GetLoginPolicyResponse as AdminGetLoginPolicyResponse, GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
UpdateLoginPolicyRequest, UpdateLoginPolicyRequest,
@ -37,16 +39,54 @@ export class LoginPolicyComponent implements OnInit {
public loading: boolean = false; public loading: boolean = false;
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public PasswordlessType: any = PasswordlessType; public PasswordlessType: any = PasswordlessType;
public lifetimeForm!: FormGroup;
constructor(private toast: ToastService, private injector: Injector) {} 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 { private fetchData(): void {
this.getData().then((resp) => { this.getData()
if (resp.policy) { .then((resp) => {
this.loginData = resp.policy; console.log(resp);
this.loading = false;
} 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 { public ngOnInit(): void {
@ -88,6 +128,22 @@ export class LoginPolicyComponent implements OnInit {
mgmtreq.setForceMfa(this.loginData.forceMfa); mgmtreq.setForceMfa(this.loginData.forceMfa);
mgmtreq.setPasswordlessType(this.loginData.passwordlessType); mgmtreq.setPasswordlessType(this.loginData.passwordlessType);
mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset); 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.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri); mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
@ -107,6 +163,21 @@ export class LoginPolicyComponent implements OnInit {
adminreq.setForceMfa(this.loginData.forceMfa); adminreq.setForceMfa(this.loginData.forceMfa);
adminreq.setPasswordlessType(this.loginData.passwordlessType); adminreq.setPasswordlessType(this.loginData.passwordlessType);
adminreq.setHidePasswordReset(this.loginData.hidePasswordReset); 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.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
adminreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri); adminreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
// adminreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime); // adminreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime);
@ -153,4 +224,24 @@ export class LoginPolicyComponent implements OnInit {
return false; 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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@ -30,6 +31,7 @@ import { MfaTableComponent } from './mfa-table/mfa-table.component';
InfoSectionModule, InfoSectionModule,
FormsModule, FormsModule,
CardModule, CardModule,
ReactiveFormsModule,
InputModule, InputModule,
MatIconModule, MatIconModule,
MatButtonModule, MatButtonModule,
@ -37,6 +39,7 @@ import { MfaTableComponent } from './mfa-table/mfa-table.component';
HasRoleModule, HasRoleModule,
MatDialogModule, MatDialogModule,
HasRolePipeModule, HasRolePipeModule,
MatCheckboxModule,
MatTooltipModule, MatTooltipModule,
DetailLayoutModule, DetailLayoutModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,

View File

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

View File

@ -18,6 +18,10 @@
margin: 1rem 0; margin: 1rem 0;
} }
.set-password-btn {
margin-bottom: 1rem;
}
.general-btn-container { .general-btn-container {
display: flex; display: flex;
justify-content: flex-start; 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 { InfoSectionType } from '../../info-section/info-section.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component'; import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { SMTPPasswordDialogComponent } from './smtp-password-dialog/smtp-password-dialog.component';
@Component({ @Component({
selector: 'cnsl-notification-settings', selector: 'cnsl-notification-settings',
@ -25,7 +26,12 @@ export class NotificationSettingsComponent implements OnInit {
public smsProviders: SMSProvider.AsObject[] = []; public smsProviders: SMSProvider.AsObject[] = [];
public logNotificationProvider!: DebugNotificationProvider.AsObject; public logNotificationProvider!: DebugNotificationProvider.AsObject;
public fileNotificationProvider!: 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 form!: FormGroup;
public SMSProviderConfigState: any = SMSProviderConfigState; public SMSProviderConfigState: any = SMSProviderConfigState;
@ -45,7 +51,6 @@ export class NotificationSettingsComponent implements OnInit {
tls: [true, [Validators.required]], tls: [true, [Validators.required]],
host: ['', [Validators.required]], host: ['', [Validators.required]],
user: ['', [Validators.required]], user: ['', [Validators.required]],
password: ['', [Validators.required]],
}); });
} }
@ -54,47 +59,61 @@ export class NotificationSettingsComponent implements OnInit {
} }
private fetchData(): void { private fetchData(): void {
this.smtpLoading = true;
this.service this.service
.getSMTPConfig() .getSMTPConfig()
.then((smtpConfig) => { .then((smtpConfig) => {
this.smtpLoading = false;
if (smtpConfig.smtpConfig) { if (smtpConfig.smtpConfig) {
this.form.patchValue(smtpConfig.smtpConfig); this.form.patchValue(smtpConfig.smtpConfig);
} }
}) })
.catch((error) => { .catch((error) => {
this.smtpLoading = false;
if (error && error.code === 5) { if (error && error.code === 5) {
console.log(error); console.log(error);
} }
}); });
this.service.listSMSProviders().then((smsProviders) => { this.smsProvidersLoading = true;
if (smsProviders.resultList) { this.service
this.smsProviders = smsProviders.resultList; .listSMSProviders()
console.log(this.smsProviders); .then((smsProviders) => {
} this.smsProvidersLoading = false;
}); if (smsProviders.resultList) {
this.smsProviders = smsProviders.resultList;
}
})
.catch((error) => {
this.smsProvidersLoading = false;
this.toast.showError(error);
});
this.logProviderLoading = true;
this.service this.service
.getLogNotificationProvider() .getLogNotificationProvider()
.then((logNotificationProvider) => { .then((logNotificationProvider) => {
this.logProviderLoading = false;
if (logNotificationProvider.provider) { if (logNotificationProvider.provider) {
this.logNotificationProvider = logNotificationProvider.provider; this.logNotificationProvider = logNotificationProvider.provider;
} }
}) })
.catch((error) => { .catch((error) => {
this.logProviderLoading = false;
this.toast.showError(error); this.toast.showError(error);
}); });
this.fileProviderLoading = true;
this.service this.service
.getFileSystemNotificationProvider() .getFileSystemNotificationProvider()
.then((fileNotificationProvider) => { .then((fileNotificationProvider) => {
this.fileProviderLoading = false;
if (fileNotificationProvider.provider) { if (fileNotificationProvider.provider) {
console.log(fileNotificationProvider);
this.fileNotificationProvider = fileNotificationProvider.provider; this.fileNotificationProvider = fileNotificationProvider.provider;
} }
}) })
.catch((error) => { .catch((error) => {
console.log('hehe'); this.fileProviderLoading = false;
this.toast.showError(error); this.toast.showError(error);
}); });
} }
@ -107,16 +126,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setTls(this.tls?.value ?? false); req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? ''); req.setUser(this.user?.value ?? '');
return this.service.updateSMTPConfig(req).then(() => { return this.service.updateSMTPConfig(req).catch(this.toast.showError);
let passwordReq: UpdateSMTPConfigPasswordRequest;
if (this.password) {
passwordReq = new UpdateSMTPConfigPasswordRequest();
passwordReq.setPassword(this.password.value);
return this.service.updateSMTPConfigPassword(passwordReq);
} else {
return;
}
});
} }
public savePolicy(): void { public savePolicy(): void {
@ -125,7 +135,6 @@ export class NotificationSettingsComponent implements OnInit {
prom prom
.then(() => { .then(() => {
this.toast.showInfo('SETTING.SMTP.SAVED', true); this.toast.showInfo('SETTING.SMTP.SAVED', true);
this.loading = true;
setTimeout(() => { setTimeout(() => {
this.fetchData(); this.fetchData();
}, 2000); }, 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 { public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio); return this.smsProviders.find((p) => p.twilio);
} }
@ -178,8 +209,4 @@ export class NotificationSettingsComponent implements OnInit {
public get host(): AbstractControl | null { public get host(): AbstractControl | null {
return this.form.get('host'); 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 { InputModule } from '../../input/input.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component'; import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component'; import { NotificationSettingsComponent } from './notification-settings.component';
import { SMTPPasswordDialogComponent } from './smtp-password-dialog/smtp-password-dialog.component';
@NgModule({ @NgModule({
declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent], declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent, SMTPPasswordDialogComponent],
imports: [ imports: [
CommonModule, CommonModule,
CardModule, 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> </div>
<div class="row"> <div class="row">
<mat-slide-toggle <mat-checkbox
class="slide-toggle" class="slide-toggle"
color="primary" color="primary"
name="hasNumber" name="hasNumber"
@ -52,10 +52,10 @@
<mat-icon class="icon" svgIcon="mdi_numeric"></mat-icon> <mat-icon class="icon" svgIcon="mdi_numeric"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASNUMBER' | translate }}</span> <span class="left-desc">{{ 'POLICY.DATA.HASNUMBER' | translate }}</span>
</div> </div>
</mat-slide-toggle> </mat-checkbox>
</div> </div>
<div class="row"> <div class="row">
<mat-slide-toggle <mat-checkbox
class="slide-toggle" class="slide-toggle"
color="primary" color="primary"
name="hasSymbol" name="hasSymbol"
@ -67,10 +67,10 @@
<mat-icon class="icon" svgIcon="mdi_symbol"></mat-icon> <mat-icon class="icon" svgIcon="mdi_symbol"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASSYMBOL' | translate }}</span> <span class="left-desc">{{ 'POLICY.DATA.HASSYMBOL' | translate }}</span>
</div> </div>
</mat-slide-toggle> </mat-checkbox>
</div> </div>
<div class="row"> <div class="row">
<mat-slide-toggle <mat-checkbox
class="slide-toggle" class="slide-toggle"
color="primary" color="primary"
name="hasLowercase" name="hasLowercase"
@ -82,10 +82,10 @@
<mat-icon class="icon" svgIcon="mdi_format-letter-case-lower"></mat-icon> <mat-icon class="icon" svgIcon="mdi_format-letter-case-lower"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASLOWERCASE' | translate }}</span> <span class="left-desc">{{ 'POLICY.DATA.HASLOWERCASE' | translate }}</span>
</div> </div>
</mat-slide-toggle> </mat-checkbox>
</div> </div>
<div class="row"> <div class="row">
<mat-slide-toggle <mat-checkbox
class="slide-toggle" class="slide-toggle"
color="primary" color="primary"
name="hasUppercase" name="hasUppercase"
@ -97,7 +97,7 @@
<mat-icon class="icon" svgIcon="mdi_format-letter-case-upper"></mat-icon> <mat-icon class="icon" svgIcon="mdi_format-letter-case-upper"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASUPPERCASE' | translate }}</span> <span class="left-desc">{{ 'POLICY.DATA.HASUPPERCASE' | translate }}</span>
</div> </div>
</mat-slide-toggle> </mat-checkbox>
</div> </div>
</div> </div>
</cnsl-card> </cnsl-card>

View File

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

View File

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

View File

@ -1,19 +1,31 @@
<div [attr.data-e2e]="'color'"> <div [attr.data-e2e]="'color'">
<p class="name cnsl-secondary-text">{{name}}</p> <p class="name cnsl-secondary-text">{{ name }}</p>
<div class="wrapper"> <div class="wrapper">
<button [disabled]="disabled" class="color-shadow" [style.background-color]="previewColor" <button
(click)="isOpen = !isOpen" cdkOverlayOrigin #trigger="cdkOverlayOrigin" [disabled]="disabled"
matTooltip="{{'ACTIONS.SET' | translate}}"> </button> 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> <i class="las la-hashtag"></i>
<span class="hex">{{previewColorCropped}}</span> <span class="hex">{{ previewColorCropped }}</span>
</div> </div>
</div> </div>
</div> </div>
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOpen]="isOpen" <ng-template
(overlayOutsideClick)="isOpen = false"> cdkConnectedOverlay
<color-chrome class="picker" [color]="previewColor" (onChangeComplete)="changeComplete($event)"></color-chrome> (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> </ng-template>

View File

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

View File

@ -123,7 +123,12 @@ export class ColorComponent implements OnInit {
} }
public changeComplete(event: ColorEvent): void { 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 { public get previewColorCropped(): string {

View File

@ -1,17 +1,26 @@
@use '@angular/material' as mat; @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: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500); $primary-color: mat.get-color-from-palette($primary, 500);
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background); $background: map-get($theme, background);
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$p-border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
.preview { .preview {
pointer-events: none; pointer-events: none;
border-radius: 0.5rem; border-radius: 0.5rem;
transform: scale(0.9); transform: scale(0.9);
color: map-get($foreground, base);
* { * {
pointer-events: none; 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"> <mat-button-toggle [value]="View.PREVIEW">
<div class="toggle-row"> <div class="toggle-row">
<span>{{ 'POLICY.PRIVATELABELING.VIEWS.PREVIEW' | translate }}</span> <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 *ngIf="view === View.PREVIEW" class="current-dot"></div>
</div> </div>
</mat-button-toggle> </mat-button-toggle>
@ -71,10 +75,6 @@
</button> </button>
</div> </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"> <div *ngIf="previewData && data" class="lab-policy-content">
<mat-accordion class="settings"> <mat-accordion class="settings">
<mat-expansion-panel class="expansion"> <mat-expansion-panel class="expansion">
@ -223,7 +223,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.BACKGROUNDDARK" [colorType]="ColorType.BACKGROUNDDARK"
(previewChanged)="previewData.backgroundColorDark = $event; savePolicy()" (previewChanged)="previewData.backgroundColorDark !== $event ? setDarkBackgroundColorAndSave($event) : null"
name="Background Color" name="Background Color"
[color]="data.backgroundColorDark" [color]="data.backgroundColorDark"
[previewColor]="previewData.backgroundColorDark" [previewColor]="previewData.backgroundColorDark"
@ -234,7 +234,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.PRIMARY" [colorType]="ColorType.PRIMARY"
(previewChanged)="previewData.primaryColorDark = $event; savePolicy()" (previewChanged)="previewData.primaryColorDark !== $event ? setDarkPrimaryColorAndSave($event) : null"
name="Primary Color" name="Primary Color"
[color]="data.primaryColorDark" [color]="data.primaryColorDark"
[previewColor]="previewData.primaryColorDark" [previewColor]="previewData.primaryColorDark"
@ -246,7 +246,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.WARN" [colorType]="ColorType.WARN"
(previewChanged)="previewData.warnColorDark = $event; savePolicy()" (previewChanged)="previewData.warnColorDark !== $event ? setDarkWarnColorAndSave($event) : null"
name="Warn Color" name="Warn Color"
[color]="data.warnColorDark" [color]="data.warnColorDark"
[previewColor]="previewData.warnColorDark" [previewColor]="previewData.warnColorDark"
@ -258,7 +258,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.FONTDARK" [colorType]="ColorType.FONTDARK"
(previewChanged)="previewData.fontColorDark = $event; savePolicy()" (previewChanged)="previewData.fontColorDark !== $event ? setDarkFontColorAndSave($event) : null"
name="Font Color" name="Font Color"
[color]="data.fontColorDark" [color]="data.fontColorDark"
[previewColor]="previewData.fontColorDark" [previewColor]="previewData.fontColorDark"
@ -274,7 +274,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.BACKGROUNDLIGHT" [colorType]="ColorType.BACKGROUNDLIGHT"
(previewChanged)="previewData.backgroundColor = $event; savePolicy()" (previewChanged)="previewData.backgroundColor !== $event ? setBackgroundColorAndSave($event) : null"
name="Background Color" name="Background Color"
[color]="data.backgroundColor" [color]="data.backgroundColor"
[previewColor]="previewData.backgroundColor" [previewColor]="previewData.backgroundColor"
@ -285,7 +285,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.PRIMARY" [colorType]="ColorType.PRIMARY"
(previewChanged)="previewData.primaryColor = $event; savePolicy()" (previewChanged)="previewData.primaryColor !== $event ? setPrimaryColorAndSave($event) : null"
name="Primary Color" name="Primary Color"
[color]="data.primaryColor" [color]="data.primaryColor"
[previewColor]="previewData.primaryColor" [previewColor]="previewData.primaryColor"
@ -298,7 +298,7 @@
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.WARN" [colorType]="ColorType.WARN"
name="Warn Color" name="Warn Color"
(previewChanged)="previewData.warnColor = $event; savePolicy()" (previewChanged)="previewData.warnColor !== $event ? setWarnColorAndSave($event) : null"
[color]="data.warnColor" [color]="data.warnColor"
[previewColor]="previewData.warnColor" [previewColor]="previewData.warnColor"
></cnsl-color> ></cnsl-color>
@ -308,7 +308,7 @@
<cnsl-color <cnsl-color
[disabled]="view === View.CURRENT" [disabled]="view === View.CURRENT"
[colorType]="ColorType.FONTLIGHT" [colorType]="ColorType.FONTLIGHT"
(previewChanged)="previewData.fontColor = $event; savePolicy()" (previewChanged)="previewData.fontColor !== $event ? setFontColorAndSave($event) : null"
name="Font Color" name="Font Color"
[color]="data.fontColor" [color]="data.fontColor"
[previewColor]="previewData.fontColor" [previewColor]="previewData.fontColor"
@ -420,6 +420,7 @@
[refresh]="refreshPreview" [refresh]="refreshPreview"
[theme]="theme" [theme]="theme"
class="preview" class="preview"
[ngClass]="{ darkmode: theme === Theme.DARK, lightmode: theme === Theme.LIGHT }"
[policy]="view === View.PREVIEW ? previewData : data" [policy]="view === View.PREVIEW ? previewData : data"
> >
</cnsl-preview> </cnsl-preview>

View File

@ -1,5 +1,7 @@
@use '@angular/material' as mat; @use '@angular/material' as mat;
@import './preview/preview.component.scss';
@mixin private-label-theme($theme) { @mixin private-label-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500); $primary-color: mat.get-color-from-palette($primary, 500);
@ -68,6 +70,12 @@
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.info-i {
font-size: 1.2rem;
margin-left: 0.5rem;
margin-right: 0;
}
.current-dot { .current-dot {
height: 8px; height: 8px;
width: 8px; width: 8px;
@ -314,6 +322,16 @@
min-height: 600px; min-height: 600px;
width: fit-content; width: fit-content;
margin: auto; 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 { public overwriteValues(req: AddCustomLabelPolicyRequest | UpdateCustomLabelPolicyRequest): void {
req.setBackgroundColorDark(this.previewData.backgroundColorDark); req.setBackgroundColorDark(this.previewData.backgroundColorDark);
req.setBackgroundColor(this.previewData.backgroundColor); req.setBackgroundColor(this.previewData.backgroundColor);

View File

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

View File

@ -103,6 +103,19 @@ export class ProjectMembersComponent {
this.grantId, 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> <cnsl-password-lockout-policy [serviceType]="serviceType"></cnsl-password-lockout-policy>
</ng-container> </ng-container>
<ng-container *ngIf="currentSetting === 'login'"> <ng-container *ngIf="currentSetting === 'login'">
<!-- <cnsl-org-iam-policy [serviceType]="serviceType"></cnsl-org-iam-policy> -->
<cnsl-login-policy [serviceType]="serviceType"></cnsl-login-policy> <cnsl-login-policy [serviceType]="serviceType"></cnsl-login-policy>
</ng-container> </ng-container>
<ng-container *ngIf="currentSetting === 'domain'">
<cnsl-domain-policy [serviceType]="serviceType"></cnsl-domain-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'idp'"> <ng-container *ngIf="currentSetting === 'idp'">
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings> <cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
</ng-container> </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 { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../card/card.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 { GeneralSettingsModule } from '../policies/general-settings/general-settings.module';
import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module'; import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module';
import { LoginPolicyModule } from '../policies/login-policy/login-policy.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 { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module'; import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.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 { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module'; import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
import { PrivacyPolicyModule } from '../policies/privacy-policy/privacy-policy.module'; import { PrivacyPolicyModule } from '../policies/privacy-policy/privacy-policy.module';
@ -37,7 +37,7 @@ import { SettingsListComponent } from './settings-list.component';
PrivacyPolicyModule, PrivacyPolicyModule,
MessageTextsPolicyModule, MessageTextsPolicyModule,
LoginTextsPolicyModule, LoginTextsPolicyModule,
OrgIamPolicyModule, DomainPolicyModule,
TranslateModule, TranslateModule,
HasRolePipeModule, HasRolePipeModule,
NotificationSettingsModule, NotificationSettingsModule,

View File

@ -19,10 +19,12 @@ export const LOGIN: SidenavSetting = {
id: 'login', id: 'login',
i18nKey: 'SETTINGS.LIST.LOGIN', i18nKey: 'SETTINGS.LIST.LOGIN',
groupI18nKey: 'SETTINGS.GROUPS.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 = { export const LOCKOUT: SidenavSetting = {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,275 +1,349 @@
<div class="app-create-container"> <div class="max-width-container">
<div class="abort-container"> <div class="enlarged-container">
<button (click)="close()" mat-icon-button> <div class="abort-container">
<mat-icon>close</mat-icon> <button (click)="close()" mat-icon-button>
</button> <mat-icon>close</mat-icon>
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span> </button>
</div> <span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span
><span class="abort-2">Step {{ currentCreateStep }} of {{ createSteps }}</span>
</div>
<h1>{{'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate}}</h1> <div class="app-create-content">
<p class="desc cnsl-secondary-text">{{'APP.PAGES.CREATE_OIDC_DESC_SUB' | translate}}</p> <h1>{{ 'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate }}</h1>
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<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-checkbox class="proswitch" color="primary" [(ngModel)]="devmode"> <mat-horizontal-stepper
{{'APP.OIDC.PROSWITCH' | translate}} class="stepper"
</mat-checkbox> *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>
<mat-horizontal-stepper class="stepper" *ngIf="!devmode" linear #stepper labelPosition="bottom" <p class="step-title">{{ 'APP.OIDC.TITLEFIRST' | translate }}</p>
(selectionChange)="changeStep($event)"> <cnsl-form-field appearance="outline" class="name-formfield">
<mat-step [stepControl]="firstFormGroup" [editable]="true"> <cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<form [formGroup]="firstFormGroup"> <input cnslInput cdkFocusInitial formControlName="name" />
<ng-template matStepLabel>{{'APP.OIDC.NAMEANDTYPESECTION' | translate}}</ng-template> <span cnslError *ngIf="name?.errors?.required">{{ 'PROJECT.APP.NAMEREQUIRED' | translate }}</span>
</cnsl-form-field>
<p class="step-title">{{'APP.OIDC.TITLEFIRST' | translate}}</p> <p class="step-title">{{ 'APP.OIDC.TYPETITLE' | translate }}</p>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput cdkFocusInitial formControlName="name" />
<span cnslError *ngIf="name?.errors?.required">{{'PROJECT.APP.NAMEREQUIRED' | translate}}</span>
</cnsl-form-field>
<p class="step-title">{{'APP.OIDC.TYPETITLE' | translate}}</p> <cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
</cnsl-type-radio>
<div class="app-create-actions">
<button
mat-raised-button
[disabled]="firstFormGroup.invalid"
color="primary"
matStepperNext
[attr.data-e2e]="'continue-button-nameandtype'"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</form>
</mat-step>
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value"> <!-- skip for native applications -->
</cnsl-type-radio> <mat-step
<div class="app-create-actions"> *ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE"
<span class="fill-space"></span> [stepControl]="secondFormGroup"
<button mat-raised-button [disabled]="firstFormGroup.invalid" color="primary" matStepperNext [editable]="true"
[attr.data-e2e]="'continue-button-nameandtype'">{{'ACTIONS.CONTINUE' | translate}}</button> >
</div> <form [formGroup]="secondFormGroup">
</form> <ng-template matStepLabel>{{ 'APP.AUTHMETHODSECTION' | translate }}</ng-template>
</mat-step>
<!-- skip for native applications --> <cnsl-auth-method-radio
<mat-step *ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE" [stepControl]="secondFormGroup" [authMethods]="authMethods"
[editable]="true"> [selected]="authMethod?.value"
<form [formGroup]="secondFormGroup"> [isOIDC]="appType?.value?.createType === AppCreateType.OIDC"
<ng-template matStepLabel>{{'APP.AUTHMETHODSECTION' | translate}}</ng-template> (selectedMethod)="authMethod?.setValue($event)"
>
</cnsl-auth-method-radio>
<cnsl-auth-method-radio [authMethods]="authMethods" [selected]="authMethod?.value" <div class="app-create-actions">
[isOIDC]="appType?.value?.createType === AppCreateType.OIDC" (selectedMethod)="authMethod?.setValue($event)"> <button class="bck-button" mat-stroked-button matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
</cnsl-auth-method-radio> <button
mat-raised-button
color="primary"
[disabled]="secondFormGroup.invalid"
matStepperNext
[attr.data-e2e]="'continue-button-authmethod'"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</form>
</mat-step>
<div class="app-create-actions"> <!-- show redirect step only for OIDC apps -->
<button mat-stroked-button color="primary" matStepperPrevious>{{'ACTIONS.BACK' | <mat-step *ngIf="appType?.value?.createType === AppCreateType.OIDC" [editable]="true">
translate}}</button> <ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
<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>
</div>
</form>
</mat-step>
<!-- show redirect step only for OIDC apps --> <p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
<mat-step *ngIf="appType?.value?.createType === AppCreateType.OIDC" [editable]="true"> <p
<ng-template matStepLabel>{{'APP.OIDC.REDIRECTSECTION' | translate}}</ng-template> 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>
<p class="step-title">{{'APP.OIDC.REDIRECTTITLE' | translate}}</p> <cnsl-redirect-uris
<p class="step-description cnsl-secondary-text" class="redirect-section"
*ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"> [canWrite]="true"
{{'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate}}</p> [isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB"> (changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
{{'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate}}</p> [urisList]="oidcAppRequest.redirectUrisList"
[getValues]="requestRedirectValuesSubject$"
<cnsl-redirect-uris class="redirect-section" [canWrite]="true" title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" >
(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>
<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">
</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>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{'APP.OIDC.OVERVIEWSECTION'| translate}}</ng-template>
<p class="step-title">{{'APP.OIDC.OVERVIEWTITLE' | translate}}</p>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.NAME' | translate }}
</span>
<span class="right">
{{oidcAppRequest.name}}
</span>
</div>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{'APP.OIDC.APPTYPE.'+oidcAppRequest.appType | translate}}
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.GRANT' | translate }}
</span>
<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>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</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>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{'APP.OIDC.AUTHMETHOD.'+oidcAppRequest?.authMethodType | translate}}
</span>
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.REDIRECT' | translate }}
</span>
<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>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
</span>
<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>]
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{'APP.API.AUTHMETHOD.'+apiAppRequest?.authMethodType | translate}}
</span>
</span>
</div>
</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>
</div>
</mat-step>
</mat-horizontal-stepper>
<div *ngIf="devmode" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" [attr.data-e2e]="'create-app-wizzard-3'">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let appType of appTypes" [value]="appType">
{{ appType.titleI18nKey | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<cnsl-form-field appearance="outline" class="formfield">
<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 }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type.type">
{{ 'APP.OIDC.RESPONSE.'+type.type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</ng-container>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of authMethodTypes" [value]="type.type">
<span *ngIf="type.oidc">{{ 'APP.OIDC.AUTHMETHOD.'+type.type | translate }}</span>
<span *ngIf="type.api">{{ 'APP.API.AUTHMETHOD.'+type.type | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<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> </cnsl-redirect-uris>
<cnsl-redirect-uris class="redirect-section" [canWrite]="true" <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>
<cnsl-redirect-uris
class="redirect-section"
[canWrite]="true"
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)" (changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
[urisList]="oidcAppRequest.postLogoutRedirectUrisList" [urisList]="oidcAppRequest.postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"> [getValues]="requestRedirectValuesSubject$"
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris> </cnsl-redirect-uris>
</div>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial <div class="app-create-actions">
type="submit"> <button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
{{ 'ACTIONS.SAVE' | translate }} <button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'">
</button> {{ 'ACTIONS.CONTINUE' | translate }}
</form> </button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'APP.OIDC.OVERVIEWSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.OVERVIEWTITLE' | translate }}</p>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.NAME' | translate }}
</span>
<span class="right">
{{ oidcAppRequest.name }}
</span>
</div>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.appType | translate }}
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.GRANT' | translate }}
</span>
<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
>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</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
>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest?.authMethodType | translate }}
</span>
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.REDIRECT' | translate }}
</span>
<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
>]
</span>
</div>
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
</span>
<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
>]
</span>
</div>
</ng-container>
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
<div class="row cnsl-secondary-text">
<span class="left">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{ 'APP.API.AUTHMETHOD.' + apiAppRequest?.authMethodType | translate }}
</span>
</span>
</div>
</ng-container>
<div class="app-create-actions">
<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>
<div *ngIf="devmode" class="dev">
<form [formGroup]="form" (ngSubmit)="createApp()" [attr.data-e2e]="'create-app-wizzard-3'">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let appType of appTypes" [value]="appType">
{{ appType.titleI18nKey | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<cnsl-form-field appearance="outline" class="formfield">
<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 }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type.type">
{{ 'APP.OIDC.RESPONSE.' + type.type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</ng-container>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of authMethodTypes" [value]="type.type">
<span *ngIf="type.oidc">{{ 'APP.OIDC.AUTHMETHOD.' + type.type | translate }}</span>
<span *ngIf="type.api">{{ 'APP.API.AUTHMETHOD.' + type.type | translate }}</span>
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<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>
<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"
>
</cnsl-redirect-uris>
</div>
</div>
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="form.invalid"
cdkFocusInitial
type="submit"
>
{{ 'ACTIONS.CREATE' | translate }}
</button>
</form>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -12,109 +12,122 @@ p.desc {
display: block; display: block;
} }
.app-create-container { .abort-container {
padding-top: 2rem; display: flex;
align-items: center;
margin-bottom: 2rem;
.progress-bar { .abort {
margin-bottom: 1rem; font-size: 1.2rem;
margin-left: 2rem;
} }
.abort-container { .abort-2 {
display: flex; font-size: 1.2rem;
align-items: center; margin-left: 2rem;
margin-bottom: 2rem; white-space: nowrap;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
} }
} }
.margin-right { .app-create-content {
margin-right: 0.5rem; padding-left: 4.5rem;
}
.full-width { .name-formfield {
width: 100%;
}
.stepper {
background: inherit !important;
margin: 0 -1.5rem;
.formfield {
max-width: 400px; max-width: 400px;
display: block;
} }
.step-title { .app-create-container {
font-size: 1rem; padding-top: 2rem;
text-transform: uppercase;
letter-spacing: 0.05em; .progress-bar {
margin-bottom: 1rem;
}
} }
.step-description { .margin-right {
font-size: 0.9rem; margin-right: 0.5rem;
} }
}
.checkbox-container { .full-width {
display: flex; width: 100%;
flex-direction: column;
.checkbox {
margin: 0.5rem 0;
} }
}
.row { .stepper {
display: flex; background: inherit !important;
justify-content: space-between; margin: 0 -1.5rem;
.left,
.right {
margin-bottom: 0.5rem;
font-size: 14px;
}
}
.app-create-actions {
margin-top: 1rem;
display: flex;
.fill-space {
flex: 1;
}
}
.dev {
.content {
display: flex;
margin: 0 -0.5rem;
flex-wrap: wrap;
flex-direction: row;
.formfield { .formfield {
flex: 1; max-width: 400px;
box-sizing: border-box; }
margin: 0 0.5rem;
&.full-width { .step-title {
flex-basis: 80%; font-size: 1rem;
} text-transform: uppercase;
letter-spacing: 0.05em;
}
.step-description {
font-size: 0.9rem;
} }
} }
.continue-button { .checkbox-container {
margin-top: 3rem; display: flex;
display: block; flex-direction: column;
padding: 0.5rem 4rem;
float: right; .checkbox {
margin: 0.5rem 0;
}
}
.row {
display: flex;
justify-content: space-between;
.left,
.right {
margin-bottom: 0.5rem;
font-size: 14px;
}
}
.app-create-actions {
margin-top: 1rem;
display: flex;
align-items: center;
.bck-button {
margin-right: 1rem;
}
.create-button {
padding: 0.5rem 4rem;
}
}
.dev {
.content {
display: flex;
margin: 0 -0.5rem;
flex-wrap: wrap;
flex-direction: row;
.formfield {
flex: 1;
box-sizing: border-box;
margin: 0 0.5rem;
&.full-width {
flex-basis: 80%;
}
}
}
.continue-button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
}
} }
} }

View File

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

View File

@ -1,32 +1,54 @@
<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" 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" [isActive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE"
[isInactive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE" [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"> <p topContent *ngIf="isZitadel" class="granted-project-sub zitadel-warning">
{{'PROJECT.PAGES.ZITADELPROJECT' | translate}} {{ 'PROJECT.PAGES.ZITADELPROJECT' | translate }}
</p> </p>
<cnsl-contributors topContributors class="project-contributors" *ngIf="project" [loading]="loading$ | async" <cnsl-contributors
[totalResult]="totalMemberResult" [membersSubject]="membersSubject" title="{{ 'PROJECT.MEMBER.TITLE' | translate }}" topContributors
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}" (addClicked)="openAddMember()" class="project-contributors"
(showDetailClicked)="showDetail()" (refreshClicked)="loadMembers()" *ngIf="project"
[disabled]="(['project.member.write$', 'project.member.write:'+ project.projectId]| hasRole | async) === false"> [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-contributors>
<cnsl-info-row topContent *ngIf="project" [grantedProject]="project"></cnsl-info-row> <cnsl-info-row topContent *ngIf="project" [grantedProject]="project"></cnsl-info-row>
</cnsl-top-view> </cnsl-top-view>
<div class="max-width-container"> <div class="max-width-container">
<cnsl-meta-layout> <cnsl-meta-layout>
<ng-template cnslHasRole [hasRole]="['user.grant.read', 'user.grant.read:'+grantId]"> <ng-template cnslHasRole [hasRole]="['user.grant.read', 'user.grant.read:' + grantId]">
<cnsl-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}" <cnsl-card
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}"> *ngIf="project?.projectId"
<cnsl-user-grants *ngIf="projectId && grantId" [context]="UserGrantContext.GRANTED_PROJECT" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
[projectId]="projectId" [grantId]="grantId" description="{{ 'GRANTS.PROJECT.DESCRIPTION' | translate }}"
[displayedColumns]="['select','user', 'projectId', 'creationDate','changeDate', 'roleNamesList','actions']" >
[disableWrite]="(['user.grant.write$','user.grant.write:'+grantId] | hasRole | async) === false" <cnsl-user-grants
[disableDelete]="(['user.grant.delete$','user.grant.delete:'+grantId] | hasRole | async) === false" *ngIf="projectId && grantId"
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{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}}']"
>
</cnsl-user-grants> </cnsl-user-grants>
</cnsl-card> </cnsl-card>
</ng-template> </ng-template>
@ -34,6 +56,5 @@
<div metainfo> <div metainfo>
<cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes> <cnsl-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.projectId"></cnsl-changes>
</div> </div>
</cnsl-meta-layout> </cnsl-meta-layout>
</div> </div>

View File

@ -89,7 +89,7 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
.bulkAddProjectRoles(this.projectId, rolesToAdd) .bulkAddProjectRoles(this.projectId, rolesToAdd)
.then(() => { .then(() => {
this.toast.showInfo('PROJECT.TOAST.ROLESCREATED', true); 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) => { .catch((error) => {
this.toast.showError(error); this.toast.showError(error);

View File

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

View File

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

View File

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

View File

@ -1,79 +1,95 @@
<div class="grid-main-container" *ngIf="projectType$ | async as type"> <div class="grid-main-container" *ngIf="projectType$ | async as type">
<div class="loading-sp-wrapper"> <div class="loading-sp-wrapper" *ngIf="loading$ | async">
<mat-progress-spinner diameter="25" *ngIf="(loading$| async) === false" class="spinner" color="primary"> <mat-spinner diameter="25" class="spinner" color="primary"> </mat-spinner>
</mat-progress-spinner>
</div> </div>
<div class="owned-project-grid-container"> <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)" (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"> <div class="text-part">
<span *ngIf="item.details && item.details.changeDate" <span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text"
class="top cnsl-secondary-text">{{'PROJECT.PAGES.LASTMODIFIED' | >{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}
translate}} {{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> >
<div class="name-row"> <div class="name-row">
<span class="name" *ngIf="$any(item).name">{{ $any(item).name }}</span> <span class="name" *ngIf="$any(item).name">{{ $any(item).name }}</span>
<span class="name" *ngIf="$any(item).projectName">{{ $any(item).projectName }}</span> <span class="name" *ngIf="$any(item).projectName">{{ $any(item).projectName }}</span>
<div class="state-dot" <div
[ngClass]="{'active': item.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': item.state === ProjectState.PROJECT_STATE_INACTIVE}"> class="state-dot"
</div> [ngClass]="{
active: item.state === ProjectState.PROJECT_STATE_ACTIVE,
inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE
}"
></div>
</div> </div>
<span *ngIf="item.details && item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | <span *ngIf="item.details && item.details.creationDate" class="created"
translate}} >{{ 'PROJECT.PAGES.CREATEDON' | translate }}
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> {{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template> <template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{ key: item }"></template>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{ key: item }"></template>
</div> </div>
</div> </div>
<div class="owned-project-grid-container"> <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)" (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"> <div class="text-part">
<span *ngIf="item.details && item.details.changeDate" <span *ngIf="item.details && item.details.changeDate" class="top cnsl-secondary-text"
class="top cnsl-secondary-text">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} >{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}
{{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> {{ item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
>
<div class="name-row"> <div class="name-row">
<span class="name" *ngIf="$any(item).name">{{ $any(item).name }}</span> <span class="name" *ngIf="$any(item).name">{{ $any(item).name }}</span>
<span class="name" *ngIf="$any(item).projectName">{{ $any(item).projectName }}</span> <span class="name" *ngIf="$any(item).projectName">{{ $any(item).projectName }}</span>
<div class="state-dot" <div
[ngClass]="{'active': item.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': item.state === ProjectState.PROJECT_STATE_INACTIVE}"> class="state-dot"
</div> [ngClass]="{
active: item.state === ProjectState.PROJECT_STATE_ACTIVE,
inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE
}"
></div>
</div> </div>
<span class="owning-org" *ngIf="$any(item).projectOwnerName">{{$any(item).projectOwnerName}}</span> <span class="owning-org" *ngIf="$any(item).projectOwnerName">{{ $any(item).projectOwnerName }}</span>
<span *ngIf="item.details && item.details.creationDate" <span *ngIf="item.details && item.details.creationDate" class="created cnsl-secondary-text"
class="created cnsl-secondary-text">{{'PROJECT.PAGES.CREATEDON' | >{{ 'PROJECT.PAGES.CREATEDON' | translate }}
translate}} {{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span
{{ >
item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
<ng-container *ngIf="type === ProjectType.PROJECTTYPE_OWNED"> <ng-container *ngIf="type === ProjectType.PROJECTTYPE_OWNED">
<template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{key: item}"></template> <template [ngTemplateOutlet]="deleteButton" [ngTemplateOutletContext]="{ key: item }"></template>
</ng-container> </ng-container>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: item}"></template> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{ key: item }"></template>
</div> </div>
<p class="n-items cnsl-secondary-text" *ngIf="(loading$ | async) === false && projectList.length === 0"> <p class="n-items cnsl-secondary-text" *ngIf="(loading$ | async) === false && projectList.length === 0">
{{'PROJECT.PAGES.NOITEMS' | {{ 'PROJECT.PAGES.NOITEMS' | translate }}
translate}}</p> </p>
<ng-container *ngIf="type === ProjectType.PROJECTTYPE_OWNED"> <ng-container *ngIf="type === ProjectType.PROJECTTYPE_OWNED">
<ng-template cnslHasRole [hasRole]="['project.create']"> <ng-template cnslHasRole [hasRole]="['project.create']">
<div class="add-project-button card" matRipple (click)="addItem()"> <div class="add-project-button card" matRipple (click)="addItem()">
<mat-icon class="icon">add</mat-icon> <mat-icon class="icon">add</mat-icon>
<span>{{'PROJECT.PAGES.ADDNEW' | translate}}</span> <span>{{ 'PROJECT.PAGES.ADDNEW' | translate }}</span>
<cnsl-action-keys [doNotUseContrast]="true" [withoutMargin]="true" (actionTriggered)="addItem()"> <cnsl-action-keys [doNotUseContrast]="true" [withoutMargin]="true" (actionTriggered)="addItem()">
</cnsl-action-keys> </cnsl-action-keys>
</div> </div>
@ -83,16 +99,27 @@
</div> </div>
<ng-template #deleteButton let-key="key"> <ng-template #deleteButton let-key="key">
<button *ngIf="key.id !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" <button
(click)="deleteProject($event, key)" class="delete-button" mat-icon-button *ngIf="key.id !== zitadelProjectId"
[attr.data-e2e]="'delete-project-button'"> 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> <i class="las la-trash"></i>
</button> </button>
</ng-template> </ng-template>
<ng-template #toggleButton let-key="key"> <ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: selection.isSelected(key)}" <button
(click)="toggle(key,$event)" class="edit-button" mat-icon-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 *ngIf="selection.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon> <mat-icon svgIcon="mdi_pin_outline" *ngIf="!selection.isSelected(key)"></mat-icon>
</button> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
<div class="max-width-container"> <div class="max-width-container">
<div class="enlarged-container"> <div class="enlarged-container">
<div class="abort-container"> <div class="abort-container">
<a [routerLink]="[ '/users']" mat-icon-button> <a [routerLink]="['/users']" mat-icon-button>
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</a> </a>
<h1 class="abort">{{ 'USER.CREATE.TITLE' | translate }}</h1><span class="abort-2">Step <h1 class="abort">{{ 'USER.CREATE.TITLE' | translate }}</h1>
1 of 1</span>
</div> </div>
<div class="user-create-main-content"> <div class="user-create-main-content">
@ -13,63 +12,69 @@
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="user-create-form"> <form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="user-create-form">
<div class="user-create-content"> <div class="user-create-content">
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p> <p class="user-create-section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<input cnslInput matRipple formControlName="email" required />
<span cnslError *ngIf="email?.invalid && !email?.errors?.required">
{{ 'USER.VALIDATION.NOTANEMAIL' | translate }}
</span>
<span cnslError *ngIf="email?.invalid && email?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<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"> <div class="user-create-grid">
{{ 'USER.VALIDATION.REQUIRED' | translate }} <cnsl-form-field>
</span> <cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator"> <input cnslInput matRipple formControlName="email" required />
{{ 'USER.VALIDATION.NOEMAIL' | translate }} <span cnslError *ngIf="email?.invalid && !email?.errors?.required">
</span> {{ 'USER.VALIDATION.NOTANEMAIL' | translate }}
</cnsl-form-field> </span>
</div> <span cnslError *ngIf="email?.invalid && email?.errors?.required">
<div class="user-create-content"> {{ 'USER.VALIDATION.REQUIRED' | translate }}
<cnsl-form-field class="formfield"> </span>
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label> </cnsl-form-field>
<input cnslInput formControlName="firstName" required /> <cnsl-form-field>
<span cnslError *ngIf="firstName?.invalid && firstName?.errors?.required"> <cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
{{ 'USER.VALIDATION.REQUIRED' | translate }} <input
</span> cnslInput
</cnsl-form-field> formControlName="userName"
<cnsl-form-field class="formfield"> required
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label> [ngStyle]="{ 'padding-right': suffixPadding ? suffixPadding : '10px' }"
<input cnslInput formControlName="lastName" required /> />
<span cnslError *ngIf="lastName?.invalid && lastName?.errors?.required"> <span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{ envSuffixLabel }}</span>
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> <span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
</cnsl-form-field> {{ 'USER.VALIDATION.REQUIRED' | translate }}
<cnsl-form-field class="formfield"> </span>
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label> <span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
<input cnslInput formControlName="nickName" /> {{ 'USER.VALIDATION.NOEMAIL' | translate }}
<span cnslError *ngIf="nickName?.invalid && nickName?.errors?.required"> </span>
{{ 'USER.VALIDATION.REQUIRED' | translate }} </cnsl-form-field>
</span>
</cnsl-form-field> <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>
<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>
<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"> <div class="email-is-verified">
<mat-checkbox class="block-checkbox" formControlName="isVerified"> <mat-checkbox class="block-checkbox" formControlName="isVerified">
{{'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate}} {{ 'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox class="block-checkbox" [(ngModel)]="usePassword" [ngModelOptions]="{standalone: true}"> <mat-checkbox class="block-checkbox" [(ngModel)]="usePassword" [ngModelOptions]="{ standalone: true }">
{{'ORG.PAGES.USEPASSWORD' | translate}} {{ 'ORG.PAGES.USEPASSWORD' | translate }}
</mat-checkbox> </mat-checkbox>
<cnsl-info-section class="full-width desc"> <cnsl-info-section class="full-width desc">
<span>{{'USER.CREATE.INITMAILDESCRIPTION' | translate}}</span> <span>{{ 'USER.CREATE.INITMAILDESCRIPTION' | translate }}</span>
</cnsl-info-section> </cnsl-info-section>
</div> </div>
@ -77,59 +82,67 @@
<cnsl-password-complexity-view class="complexity-view" [policy]="this.policy" [password]="password"> <cnsl-password-complexity-view class="complexity-view" [policy]="this.policy" [password]="password">
</cnsl-password-complexity-view> </cnsl-password-complexity-view>
<form [formGroup]="pwdForm" class="user-create-pwd-form"> <form [formGroup]="pwdForm">
<cnsl-form-field class="pwd-field" *ngIf="password" appearance="outline"> <div class="user-create-grid">
<cnsl-label>{{ 'USER.PASSWORD.NEWINITIAL' | translate }}</cnsl-label> <cnsl-form-field *ngIf="password">
<input cnslInput autocomplete="off" name="firstpassword" formControlName="password" type="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"> <span cnslError *ngIf="password?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</cnsl-form-field>
<cnsl-form-field *ngIf="confirmPassword">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
<input
cnslInput
autocomplete="off"
name="confirmPassword"
formControlName="confirmPassword"
type="password"
/>
</cnsl-form-field> <span cnslError *ngIf="confirmPassword?.errors?.required">
<cnsl-form-field class="pwd-field" *ngIf="confirmPassword" appearance="outline"> {{ 'USER.VALIDATION.REQUIRED' | translate }}
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label> </span>
<input cnslInput autocomplete="off" name="confirmPassword" formControlName="confirmPassword" <span cnslError *ngIf="confirmPassword?.errors?.notequal">
type="password" /> {{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
<span cnslError *ngIf="confirmPassword?.errors?.required"> </cnsl-form-field>
{{ 'USER.VALIDATION.REQUIRED' | translate }} </div>
</span>
<span cnslError *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</form> </form>
</div> </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-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label> <cnsl-form-field>
<mat-select formControlName="gender"> <cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
<mat-option *ngFor="let gender of genders" [value]="gender"> <mat-select formControlName="gender">
{{ 'GENDERS.'+gender | translate }} <mat-option *ngFor="let gender of genders" [value]="gender">
</mat-option> {{ 'GENDERS.' + gender | translate }}
</mat-select> </mat-option>
<span cnslError *ngIf="gender?.invalid && gender?.errors?.required"> </mat-select>
{{ 'USER.VALIDATION.REQUIRED' | translate }} <span cnslError *ngIf="gender?.invalid && gender?.errors?.required">
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</cnsl-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'LANGUAGES.'+language | translate }}
</mat-option>
<span cnslError *ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</mat-select> </cnsl-form-field>
</cnsl-form-field> <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">
{{ 'LANGUAGES.' + language | translate }}
</mat-option>
<span cnslError *ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</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> <cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
<input cnslInput formControlName="phone" /> <input cnslInput formControlName="phone" />
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required"> <span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
@ -138,10 +151,15 @@
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<div class="user-create-btn-container"> <div class="user-create-btn-container">
<button [attr.data-e2e]="'create-button'" color="primary" <button
[disabled]="userForm.invalid || (this.usePassword && this.pwdForm.invalid)" type="submit" [attr.data-e2e]="'create-button'"
mat-raised-button>{{ 'ACTIONS.CREATE' | color="primary"
translate }}</button> [disabled]="userForm.invalid || (this.usePassword && this.pwdForm.invalid)"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.CREATE' | translate }}
</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -7,20 +7,47 @@
font-size: 1.2rem; font-size: 1.2rem;
margin-left: 2rem; margin-left: 2rem;
} }
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
} }
.user-create-main-content { .user-create-main-content {
padding-left: 4.5rem; padding-left: 4.5rem;
max-width: 35rem; max-width: 35rem;
@media only screen and (max-width: 500px) {
padding: 0 0.5rem;
}
.user-create-form { .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 { .user-create-btn-container {
button { 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 { .pwd-section {
margin: 0 0.5rem;
.section { .section {
padding: 0.5rem 0; 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.loading = true;
this.loadOrg(); this.loadOrg();
this.mgmtService this.mgmtService
.getOrgIAMPolicy() .getDomainPolicy()
.then((resp) => { .then((resp) => {
if (resp.policy?.userLoginMustBeDomain) { if (resp.policy?.userLoginMustBeDomain) {
this.userLoginMustBeDomain = 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> <mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
</div> </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$"> <div class="max-width-container" *ngIf="user && (['user.write$', 'user.write:' + user.id] | hasRole) as canWrite$">
<cnsl-meta-layout> <cnsl-meta-layout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -818,6 +818,7 @@
"NOTIFICATIONS": "Notification providers and SMTP", "NOTIFICATIONS": "Notification providers and SMTP",
"MESSAGETEXTS": "Message Texts", "MESSAGETEXTS": "Message Texts",
"IDP": "Identity Providers", "IDP": "Identity Providers",
"DOMAIN": "Domain settings",
"LOGINTEXTS": "Login Interface Texts", "LOGINTEXTS": "Login Interface Texts",
"BRANDING": "Branding", "BRANDING": "Branding",
"PRIVACYPOLICY": "Privacy Policy", "PRIVACYPOLICY": "Privacy Policy",
@ -827,6 +828,7 @@
"GROUPS": { "GROUPS": {
"NOTIFICATIONS": "Notifications", "NOTIFICATIONS": "Notifications",
"LOGIN": "Login and Access", "LOGIN": "Login and Access",
"DOMAIN": "Domain",
"TEXTS": "Texts and Languages", "TEXTS": "Texts and Languages",
"APPEARANCE": "Appearance", "APPEARANCE": "Appearance",
"OTHER": "Other" "OTHER": "Other"
@ -846,6 +848,8 @@
"HOST": "Host", "HOST": "Host",
"USER": "User", "USER": "User",
"PASSWORD": "Password", "PASSWORD": "Password",
"SETPASSWORD": "Set SMTP Password",
"PASSWORDSET": "SMTP Password was set successfully.",
"TLS": "Transport Layer Security (TLS)", "TLS": "Transport Layer Security (TLS)",
"SAVED": "Saved successfully!", "SAVED": "Saved successfully!",
"REQUIREDWARN": "To send notifications from your domain, you have to enter your SMTP data." "REQUIREDWARN": "To send notifications from your domain, you have to enter your SMTP data."
@ -949,9 +953,8 @@
"TITLE": "Lockout Policy", "TITLE": "Lockout Policy",
"DESCRIPTION": "Set a maximum number of passwordretries, after which accounts will be blocked." "DESCRIPTION": "Set a maximum number of passwordretries, after which accounts will be blocked."
}, },
"IAM_POLICY": { "DOMAIN_POLICY": {
"TITLE": "IAM Access Preferences", "TITLE": "Domain Settings"
"DESCRIPTION": "Define access properties of your users."
}, },
"PRIVATELABELING_POLICY": { "PRIVATELABELING_POLICY": {
"TITLE": "Branding", "TITLE": "Branding",
@ -965,6 +968,7 @@
"DESCRIPTIONCREATEADMIN": "Users can choose from the available identity providers below.", "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.", "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", "ADVANCED": "Advanced",
"LIFETIMEDURATIONS": "Login Lifetimes",
"SAVED": "Saved successfully!" "SAVED": "Saved successfully!"
}, },
"PRIVACY_POLICY": { "PRIVACY_POLICY": {
@ -1080,7 +1084,9 @@
"MAXATTEMPTS": "Password maximum Attempts", "MAXATTEMPTS": "Password maximum Attempts",
"EXPIREWARNDAYS": "Expiration Warning after day", "EXPIREWARNDAYS": "Expiration Warning after day",
"MAXAGEDAYS": "Max Age in days", "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", "ALLOWUSERNAMEPASSWORD": "Username Password allowed",
"ALLOWEXTERNALIDP": "External IDP allowed", "ALLOWEXTERNALIDP": "External IDP allowed",
"ALLOWREGISTER": "Register allowed", "ALLOWREGISTER": "Register allowed",
@ -1097,7 +1103,13 @@
"DEFAULTREDIRECTURI": "Default Redirect URI", "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)", "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", "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", "RESET": "Reset to Instance default",
"CREATECUSTOM": "Create Custom Policy", "CREATECUSTOM": "Create Custom Policy",

View File

@ -818,6 +818,7 @@
"NOTIFICATIONS": "Notifiche", "NOTIFICATIONS": "Notifiche",
"MESSAGETEXTS": "Testi di notifica", "MESSAGETEXTS": "Testi di notifica",
"IDP": "Identity Providers", "IDP": "Identity Providers",
"DOMAIN": "Impostazioni del dominio",
"LOGINTEXTS": "Testi dell'interfaccia login", "LOGINTEXTS": "Testi dell'interfaccia login",
"BRANDING": "Branding", "BRANDING": "Branding",
"PRIVACYPOLICY": "Informativa sulla privacy e TOS", "PRIVACYPOLICY": "Informativa sulla privacy e TOS",
@ -827,6 +828,7 @@
"GROUPS": { "GROUPS": {
"NOTIFICATIONS": "Notifiche", "NOTIFICATIONS": "Notifiche",
"LOGIN": "Accesso e login", "LOGIN": "Accesso e login",
"DOMAIN": "Dominio",
"TEXTS": "Testi e lingue", "TEXTS": "Testi e lingue",
"APPEARANCE": "Aussehen", "APPEARANCE": "Aussehen",
"OTHER": "Altro" "OTHER": "Altro"
@ -846,6 +848,8 @@
"HOST": "Host", "HOST": "Host",
"USER": "Utente", "USER": "Utente",
"PASSWORD": "Password", "PASSWORD": "Password",
"SETPASSWORD": "Imposta SMTP Password",
"PASSWORDSET": "SMTP Password impostata con successo.",
"TLS": "Transport Layer Security (TLS)", "TLS": "Transport Layer Security (TLS)",
"SAVED": "Salvato con successo!", "SAVED": "Salvato con successo!",
"REQUIREDWARN": "Per inviare notifiche dal tuo dominio, devi inserire i tuoi dati SMTP." "REQUIREDWARN": "Per inviare notifiche dal tuo dominio, devi inserire i tuoi dati SMTP."
@ -949,9 +953,8 @@
"TITLE": "Impostazioni di blocco", "TITLE": "Impostazioni di blocco",
"DESCRIPTION": "Imposta un numero massimo di tentativi di password, dopo i quali gli account saranno bloccati." "DESCRIPTION": "Imposta un numero massimo di tentativi di password, dopo i quali gli account saranno bloccati."
}, },
"IAM_POLICY": { "DOMAIN_POLICY": {
"TITLE": "Impostazioni di accesso IAM", "TITLE": "Impostazioni dominio"
"DESCRIPTION": "Definisci le propriet\u00e0 di accesso dei tuoi utenti."
}, },
"PRIVATELABELING_POLICY": { "PRIVATELABELING_POLICY": {
"TITLE": "Branding", "TITLE": "Branding",
@ -965,6 +968,7 @@
"DESCRIPTIONCREATEADMIN": "Gli utenti possono scegliere tra gli IDP disponibili qui sotto.", "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.", "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", "ADVANCED": "Impostazioni avanzate",
"LIFETIMEDURATIONS": "Login Lifetimes",
"SAVED": "Salvato con successo!" "SAVED": "Salvato con successo!"
}, },
"PRIVACY_POLICY": { "PRIVACY_POLICY": {
@ -1080,7 +1084,9 @@
"MAXATTEMPTS": "Massimo numero di tentativi di password", "MAXATTEMPTS": "Massimo numero di tentativi di password",
"EXPIREWARNDAYS": "Avviso scadenza dopo il giorno", "EXPIREWARNDAYS": "Avviso scadenza dopo il giorno",
"MAXAGEDAYS": "Lunghezza massima in giorni", "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", "ALLOWUSERNAMEPASSWORD": "Autenticazione classica con password consentita",
"ALLOWEXTERNALIDP": "IDP esterno consentito", "ALLOWEXTERNALIDP": "IDP esterno consentito",
"ALLOWREGISTER": "Registrazione consentita", "ALLOWREGISTER": "Registrazione consentita",
@ -1097,7 +1103,13 @@
"DEFAULTREDIRECTURI": "Default Redirect URI", "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)", "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", "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", "RESET": "Ripristina l'impostazione dell'istanza",
"CREATECUSTOM": "Crea un'impostazione personalizzata", "CREATECUSTOM": "Crea un'impostazione personalizzata",

View File

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

View File

@ -425,10 +425,6 @@ $custom-typography: mat.define-typography-config(
--success: #10b981; --success: #10b981;
$border-color: map-get($foreground, divider); $border-color: map-get($foreground, divider);
.main-container {
color: map-get($foreground, base);
}
.mat-menu-panel { .mat-menu-panel {
background-color: map-get($background, cards); background-color: map-get($background, cards);
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 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; @use '@angular/material' as mat;
@mixin toast-theme($theme) { @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: map-get($theme, warn);
$warn-color: mat.get-color-from-palette($warn, 500); $warn-color: mat.get-color-from-palette($warn, 500);
$background: map-get($theme, background); $background: map-get($theme, background);
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
// .data-e2e-success { // .data-e2e-success {
// background-color: map-get($background, cards) !important; // background-color: map-get($background, cards) !important;
@ -17,6 +13,6 @@
.data-e2e-failure { .data-e2e-failure {
background-color: $warn-color !important; background-color: $warn-color !important;
color: map-get($foreground, text) !important; color: mat.get-color-from-palette($warn, default-contrast) !important;
} }
} }