feat: console flat navigation, settings (#3581)

* instance routing

* instance naming

* org list

* rm isonsystem

* breadcrumb  type

* routing

* instance members

* fragment refresh org

* settings pages

* settings list, sidenav grouping, i18n

* org-settings, policy changes

* lint

* grid

* rename grid

* fallback to general

* cleanup

* general settings, remove cards

* sidenav for settings, label policy

* i18n

* header, nav backbuild

* general, project nav rehaul

* login text background adapt

* org nav anim

* org, instance settings, fix policy layout, roles

* i18n, active route for project

* lint
This commit is contained in:
Max Peintner
2022-05-09 15:01:36 +02:00
committed by GitHub
parent 94e420bb24
commit 06e3330d2e
188 changed files with 4046 additions and 3639 deletions

View File

@@ -13,6 +13,10 @@ const routes: Routes = [
loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule), loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule),
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
{
path: 'orgs',
loadChildren: () => import('./pages/org-list/org-list.module').then((m) => m.OrgListModule),
},
{ {
path: 'granted-projects', path: 'granted-projects',
loadChildren: () => loadChildren: () =>
@@ -41,8 +45,8 @@ const routes: Routes = [
], ],
}, },
{ {
path: 'system', path: 'instance',
loadChildren: () => import('./pages/iam/iam.module').then((m) => m.IamModule), loadChildren: () => import('./pages/instance/instance.module').then((m) => m.InstanceModule),
canActivate: [AuthGuard, RoleGuard], canActivate: [AuthGuard, RoleGuard],
data: { data: {
roles: ['iam.read', 'iam.write'], roles: ['iam.read', 'iam.write'],
@@ -59,7 +63,7 @@ const routes: Routes = [
}, },
{ {
path: 'org', path: 'org',
loadChildren: () => import('./pages/orgs/orgs.module').then((m) => m.OrgsModule), loadChildren: () => import('./pages/orgs/org.module').then((m) => m.OrgModule),
canActivate: [AuthGuard, RoleGuard], canActivate: [AuthGuard, RoleGuard],
data: { data: {
roles: ['org.read'], roles: ['org.read'],
@@ -140,6 +144,14 @@ const routes: Routes = [
roles: ['iam.read'], roles: ['iam.read'],
}, },
}, },
{
path: 'settings',
loadChildren: () => import('./pages/instance-settings/instance-settings.module').then((m) => m.InstanceSettingsModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['iam.read', 'iam.write'],
},
},
{ {
path: 'domains', path: 'domains',
loadChildren: () => import('./pages/domains/domains.module').then((m) => m.DomainsModule), loadChildren: () => import('./pages/domains/domains.module').then((m) => m.DomainsModule),
@@ -148,6 +160,14 @@ const routes: Routes = [
roles: ['org.read'], roles: ['org.read'],
}, },
}, },
{
path: 'org-settings',
loadChildren: () => import('./pages/org-settings/org-settings.module').then((m) => m.OrgSettingsModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['iam.read', 'iam.write'],
},
},
{ {
path: 'signedout', path: 'signedout',
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule), loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),

View File

@@ -22,7 +22,7 @@
position: relative; position: relative;
.router-container { .router-container {
padding: 0 2rem; padding: 0 2rem 50px 2rem;
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
padding: 0 1rem; padding: 0 1rem;

View File

@@ -349,7 +349,7 @@ export class AppComponent implements OnInit, OnDestroy {
public changedOrg(org: Org.AsObject): void { public changedOrg(org: Org.AsObject): void {
this.loadPrivateLabelling(); this.loadPrivateLabelling();
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => { this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
this.router.navigate(['/']); this.router.navigate(['/org'],{fragment: org.id} );
}); });
} }

View File

@@ -17,19 +17,6 @@
<span class="u-name">{{ user.human?.profile?.displayName ? user.human?.profile?.displayName : 'A' }}</span> <span class="u-name">{{ user.human?.profile?.displayName ? user.human?.profile?.displayName : 'A' }}</span>
<span class="u-email" *ngIf="user.preferredLoginName">{{ user.preferredLoginName }}</span> <span class="u-email" *ngIf="user.preferredLoginName">{{ user.preferredLoginName }}</span>
<a [routerLink]="['/system']" class="iamuser" *ngIf="iamuser" (click)="close()">
<span class="label">{{ 'MENU.INSTANCE' | translate }}</span>
<a class="iambtn">
<i class="las la-cog"></i>
</a>
</a>
<a [routerLink]="['/org']" class="iamuser" *ngIf="isOnSystem" (click)="close()">
<span class="label">{{ 'MENU.ORGANIZATION' | translate }}</span>
<a class="iambtn">
<i class="las la-cog"></i>
</a>
</a>
<button (click)="editUserProfile()" mat-stroked-button>{{ 'USER.EDITACCOUNT' | translate }}</button> <button (click)="editUserProfile()" mat-stroked-button>{{ 'USER.EDITACCOUNT' | translate }}</button>
<div class="l-accounts"> <div class="l-accounts">

View File

@@ -50,40 +50,6 @@
margin-top: 0; margin-top: 0;
} }
.iamuser {
position: absolute;
border-radius: 50vw;
right: 1rem;
top: 1rem;
font-size: 12px;
line-height: 12px;
font-weight: 600;
background-color: $primary-color;
color: mat.get-color-from-palette($primary, default-contrast);
flex-direction: row;
align-items: center;
text-decoration: none;
padding: 0 4px;
display: none;
.label {
margin: 0.5rem 1rem;
}
.iambtn {
margin: 0 0 0 4px;
color: mat.get-color-from-palette($primary, default-contrast);
}
@media only screen and (max-width: 600px) {
display: flex;
.label {
margin: 0.5rem 0 0.5rem 0.5rem;
}
}
}
button { button {
border-radius: 50vh; border-radius: 50vh;
margin: 0.5rem; margin: 0.5rem;

View File

@@ -78,11 +78,4 @@ export class AccountsCardComponent implements OnInit {
this.authService.signout(); this.authService.signout();
this.closedCard.emit(); this.closedCard.emit();
} }
public get isOnSystem(): boolean {
return (
['/system', '/views', '/failed-events', '/system/members'].includes(this.router.url) ||
new RegExp('/system/policy/*').test(this.router.url)
);
}
} }

View File

@@ -16,7 +16,6 @@
padding: 2rem; padding: 2rem;
background-color: map-get($background, footer) !important; background-color: map-get($background, footer) !important;
border-top: 1px solid map-get($foreground, divider); border-top: 1px solid map-get($foreground, divider);
margin-top: 50px;
display: flex; display: flex;
transition: background-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); transition: background-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);

View File

@@ -37,7 +37,7 @@
</ng-template> </ng-template>
<ng-container *ngFor="let bread of breadcrumbService.breadcrumbs$ | async as bc; index as i"> <ng-container *ngFor="let bread of breadcrumbService.breadcrumbs$ | async as bc; index as i">
<ng-container *ngIf="bread.type === BreadcrumbType.IAM"> <ng-container *ngIf="bread.type === BreadcrumbType.INSTANCE">
<ng-template cnslHasRole [hasRole]="['iam.read']"> <ng-template cnslHasRole [hasRole]="['iam.read']">
<svg <svg
class="slash hide-on-small" class="slash hide-on-small"
@@ -132,7 +132,7 @@
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="bread.type !== BreadcrumbType.IAM && bread.type !== BreadcrumbType.ORG"> <ng-container *ngIf="bread.type !== BreadcrumbType.INSTANCE && bread.type !== BreadcrumbType.ORG">
<svg <svg
class="slash" class="slash"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -176,8 +176,8 @@
<div class="system-rel" *ngIf="!isOnMe"> <div class="system-rel" *ngIf="!isOnMe">
<a <a
id="systembutton" id="systembutton"
*ngIf="!isOnSystem && (['iam.read$', 'iam.write$'] | hasRole | async)" *ngIf="!isOnInstance && (['iam.read$', 'iam.write$'] | hasRole | async)"
[routerLink]="['/system']" [routerLink]="['/instance']"
class="iam-settings cnsl-action-button" class="iam-settings cnsl-action-button"
mat-stroked-button mat-stroked-button
> >
@@ -186,7 +186,7 @@
</a> </a>
<a <a
id="orgbutton" id="orgbutton"
*ngIf="isOnSystem && (['org.read'] | hasRole | async)" *ngIf="isOnInstance && (['org.read'] | hasRole | async)"
[routerLink]="['/org']" [routerLink]="['/org']"
class="org-settings cnsl-action-button" class="org-settings cnsl-action-button"
mat-stroked-button mat-stroked-button

View File

@@ -63,14 +63,14 @@ export class HeaderComponent implements OnDestroy {
this.changedActiveOrg.emit(org); this.changedActiveOrg.emit(org);
} }
public get isOnSystem(): boolean {
return (
['/system', '/views', '/failed-events', '/system/members'].includes(this.router.url) ||
new RegExp('/system/policy/*').test(this.router.url)
);
}
public get isOnMe(): boolean { public get isOnMe(): boolean {
return this.router.url === '/users/me'; return this.router.url === '/users/me';
} }
public get isOnInstance(): boolean {
return (
['/instance', '/views', '/orgs', '/settings', '/failed-events', '/instance/members'].includes(this.router.url) ||
this.router.url.includes('/settings')
);
}
} }

View File

@@ -85,9 +85,9 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
]; ];
const iamBread = new Breadcrumb({ const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM, type: BreadcrumbType.INSTANCE,
name: 'System', name: 'Instance',
routerLink: ['/system'], routerLink: ['/instance'],
}); });
breadcrumbService.setBreadcrumb([iamBread]); breadcrumbService.setBreadcrumb([iamBread]);
break; break;
@@ -98,16 +98,11 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
OIDCMappingField.OIDC_MAPPING_FIELD_EMAIL, OIDCMappingField.OIDC_MAPPING_FIELD_EMAIL,
]; ];
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = { const bread: Breadcrumb = {
type: BreadcrumbType.ORG, type: BreadcrumbType.ORG,
routerLink: ['/org'], routerLink: ['/org'],
}; };
breadcrumbService.setBreadcrumb([iambread, bread]); breadcrumbService.setBreadcrumb([bread]);
break; break;
} }
}); });

View File

@@ -2,7 +2,7 @@
[loading]="loading$ | async" [loading]="loading$ | async"
(refreshed)="refreshPage()" (refreshed)="refreshPage()"
[dataSize]="dataSource.data.length" [dataSize]="dataSource.data.length"
[emitRefreshOnPreviousRoutes]="['/system/idp/create']" [emitRefreshOnPreviousRoutes]="['/instance/idp/create']"
[timestamp]="idpResult?.details?.viewTimestamp" [timestamp]="idpResult?.details?.viewTimestamp"
[selection]="selection" [selection]="selection"
[hideRefresh]="true" [hideRefresh]="true"
@@ -14,7 +14,17 @@
class="margin-right bg-state inactive" class="margin-right bg-state inactive"
mat-stroked-button mat-stroked-button
*ngIf="selection.hasValue()" *ngIf="selection.hasValue()"
[disabled]="disabled" [disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.idp.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'org.idp.write'
: ''
]
| hasRole
| async) === false
"
> >
{{ 'IDP.DEACTIVATE' | translate }} {{ 'IDP.DEACTIVATE' | translate }}
</button> </button>
@@ -24,12 +34,38 @@
class="bg-state active" class="bg-state active"
mat-stroked-button mat-stroked-button
*ngIf="selection.hasValue()" *ngIf="selection.hasValue()"
[disabled]="disabled" [disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.idp.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'org.idp.write'
: ''
]
| hasRole
| async) === false
"
> >
{{ 'IDP.ACTIVATE' | translate }} {{ 'IDP.ACTIVATE' | translate }}
</button> </button>
<a [routerLink]="createRouterLink" class="cnsl-action-button" color="primary" mat-raised-button [disabled]="disabled"> <a
[routerLink]="createRouterLink"
class="cnsl-action-button"
color="primary"
mat-raised-button
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.idp.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'org.idp.write'
: ''
]
| hasRole
| async) === false
"
>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</div> </div>
@@ -193,7 +229,22 @@
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr
mat-header-row
*matHeaderRowDef="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.idp.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'org.idp.write'
: ''
]
| hasRole
| async)
? displayedColumnsWithActions
: displayedColumns
"
></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr> <tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table> </table>
</div> </div>

View File

@@ -24,7 +24,6 @@ import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
export class IdpTableComponent implements OnInit { export class IdpTableComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType; @Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService; @Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<IDP.AsObject> = new MatTableDataSource<IDP.AsObject>(); public dataSource: MatTableDataSource<IDP.AsObject> = new MatTableDataSource<IDP.AsObject>();
public selection: SelectionModel<IDP.AsObject> = new SelectionModel<IDP.AsObject>(true, []); public selection: SelectionModel<IDP.AsObject> = new SelectionModel<IDP.AsObject>(true, []);
@@ -55,10 +54,6 @@ export class IdpTableComponent implements OnInit {
if (this.serviceType === PolicyComponentServiceType.MGMT) { if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.displayedColumns = ['availability', 'name', 'type', 'owner', 'creationDate', 'changeDate', 'state']; this.displayedColumns = ['availability', 'name', 'type', 'owner', 'creationDate', 'changeDate', 'state'];
} }
if (!this.disabled) {
this.displayedColumns.push('actions');
}
} }
public isAllSelected(): boolean { public isAllSelected(): boolean {
@@ -195,7 +190,7 @@ export class IdpTableComponent implements OnInit {
public get createRouterLink(): RouterLink | any { public get createRouterLink(): RouterLink | any {
if (this.service instanceof AdminService) { if (this.service instanceof AdminService) {
return ['/system', 'idp', 'create']; return ['/instance', 'idp', 'create'];
} else if (this.service instanceof ManagementService) { } else if (this.service instanceof ManagementService) {
return ['/org', 'idp', 'create']; return ['/org', 'idp', 'create'];
} }
@@ -207,13 +202,13 @@ export class IdpTableComponent implements OnInit {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
switch (row.owner) { switch (row.owner) {
case IDPOwnerType.IDP_OWNER_TYPE_SYSTEM: case IDPOwnerType.IDP_OWNER_TYPE_SYSTEM:
return ['/system', 'idp', row.id]; return ['/instance', 'idp', row.id];
case IDPOwnerType.IDP_OWNER_TYPE_ORG: case IDPOwnerType.IDP_OWNER_TYPE_ORG:
return ['/org', 'idp', row.id]; return ['/org', 'idp', row.id];
} }
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
return ['/system', 'idp', row.id]; return ['/instance', 'idp', row.id];
} }
} }
} }
@@ -289,4 +284,8 @@ export class IdpTableComponent implements OnInit {
public isEnabled(idp: IDP.AsObject): boolean { public isEnabled(idp: IDP.AsObject): boolean {
return this.idps.findIndex((i) => i.idpId === idp.id) > -1; return this.idps.findIndex((i) => i.idpId === idp.id) > -1;
} }
public get displayedColumnsWithActions(): string[] {
return ['actions', ...this.displayedColumns];
}
} }

View File

@@ -101,24 +101,19 @@ export class IdpComponent implements OnDestroy {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = { const bread: Breadcrumb = {
type: BreadcrumbType.ORG, type: BreadcrumbType.ORG,
routerLink: ['/org'], routerLink: ['/org'],
}; };
breadcrumbService.setBreadcrumb([iambread, bread]); breadcrumbService.setBreadcrumb([bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
const iamBread = new Breadcrumb({ const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM, type: BreadcrumbType.INSTANCE,
name: 'System', name: 'Instance',
routerLink: ['/system'], routerLink: ['/instance'],
}); });
breadcrumbService.setBreadcrumb([iamBread]); breadcrumbService.setBreadcrumb([iamBread]);
break; break;
@@ -419,7 +414,7 @@ export class IdpComponent implements OnDestroy {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
return ['/org', 'policy', 'login']; return ['/org', 'policy', 'login'];
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
return ['/system', 'policy', 'login']; return ['/instance', 'policy', 'login'];
} }
} }

View File

@@ -23,8 +23,8 @@ export class KeyboardShortcutsComponent {
public get isNotOnSystem(): boolean { public get isNotOnSystem(): boolean {
return !( return !(
['/system', '/views', '/failed-events'].includes(this.router.url) || ['/instance', '/views', '/failed-events'].includes(this.router.url) ||
new RegExp('/system/policy/*').test(this.router.url) new RegExp('/instance/policy/*').test(this.router.url)
); );
} }
} }

View File

@@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { IamMembersDataSource } from 'src/app/pages/iam/iam-members/iam-members-datasource'; import { InstanceMembersDataSource } from 'src/app/pages/instance/instance-members/instance-members-datasource';
import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource'; import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource';
import { import {
ProjectGrantMembersDataSource, ProjectGrantMembersDataSource,
@@ -21,7 +21,7 @@ type MemberDatasource =
| OrgMembersDataSource | OrgMembersDataSource
| ProjectMembersDataSource | ProjectMembersDataSource
| ProjectGrantMembersDataSource | ProjectGrantMembersDataSource
| IamMembersDataSource; | InstanceMembersDataSource;
@Component({ @Component({
selector: 'cnsl-members-table', selector: 'cnsl-members-table',
@@ -121,7 +121,7 @@ export class MembersTableComponent implements OnInit, OnDestroy {
public masterToggle(): void { public masterToggle(): void {
this.isAllSelected() this.isAllSelected()
? this.selection.clear() ? this.selection.clear()
: this.dataSource.membersSubject.value.forEach((row) => this.selection.select(row)); : this.dataSource.membersSubject.value.forEach((row: Member.AsObject) => this.selection.select(row));
} }
public changePage(event?: PageEvent): any { public changePage(event?: PageEvent): any {

View File

@@ -160,31 +160,10 @@ export class MembershipsTableComponent implements OnInit, OnDestroy {
}); });
} else if (membership.iam) { } else if (membership.iam) {
// only shown on auth user // only shown on auth user
this.router.navigate(['/system/members']); this.router.navigate(['/instance/members']);
} }
} }
// public canNavigateToEdit(membership: Membership.AsObject): Observable<boolean> {
// if (membership.orgId && !membership.projectId && !membership.projectGrantId) {
// return this.authService.isAllowed(['org.member.read:' + membership.orgId]);
// } else if (membership.projectGrantId && membership.details?.resourceOwner) {
// return from(this.authService.getActiveOrg(membership.details?.resourceOwner)).pipe(
// switchMap(() => this.authService.isAllowed(['project.grant.member.read:' + membership.projectId])),
// catchError(() => of(false)),
// );
// } else if (membership.projectId && membership.details?.resourceOwner) {
// return from(this.authService.getActiveOrg(membership.details?.resourceOwner)).pipe(
// take(1),
// switchMap(() => this.authService.isAllowed(['project.member.read:' + membership.projectId])),
// catchError(() => of(false)),
// );
// } else if (membership.iam) {
// return this.authService.isAllowed(['iam.member.read']);
// } else {
// return of(false);
// }
// }
private startOrgContextWorkflow(membershipOrg: Org.AsObject, currentOrg?: Org.AsObject | null): void { private startOrgContextWorkflow(membershipOrg: Org.AsObject, currentOrg?: Org.AsObject | null): void {
if (!currentOrg || (membershipOrg.id && currentOrg.id && currentOrg.id !== membershipOrg.id)) { if (!currentOrg || (membershipOrg.id && currentOrg.id && currentOrg.id !== membershipOrg.id)) {
setTimeout(() => { setTimeout(() => {

View File

@@ -7,27 +7,36 @@
*ngIf=" *ngIf="
breadc[breadc.length - 1] && breadc[breadc.length - 1] &&
!breadc[breadc.length - 1].hideNav && !breadc[breadc.length - 1].hideNav &&
breadc[breadc.length - 1].type !== BreadcrumbType.GRANTEDPROJECT &&
breadc[breadc.length - 1].type !== BreadcrumbType.APP &&
breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER
" "
[ngSwitch]="breadc[breadc.length - 1].type" [ngSwitch]="breadc[0].type"
> >
<div class="nav-row" @navrow> <div class="nav-row" @navrow>
<ng-container *ngSwitchCase="BreadcrumbType.IAM"> <ng-container *ngSwitchCase="BreadcrumbType.INSTANCE">
<div class="nav-row-abs" @navroworg> <div class="nav-row-abs" @navroworg>
<ng-template cnslHasRole [hasRole]="['iam.read']"> <ng-template cnslHasRole [hasRole]="['iam.read']">
<a <a
class="nav-item" class="nav-item"
[routerLinkActiveOptions]="{ exact: false }" [routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']" [routerLinkActive]="['active']"
[routerLink]="['/system']" [routerLink]="['/instance']"
> >
<div class="c_label"> <div class="c_label">
<span> {{ 'MENU.INSTANCEOVERVIEW' | translate }} </span> <span> {{ 'MENU.INSTANCEOVERVIEW' | translate }} </span>
</div> </div>
</a> </a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: true }"
[routerLinkActive]="['active']"
[routerLink]="['/orgs']"
>
<div class="c_label">
<span> {{ 'MENU.ORGS' | translate }} </span>
</div>
</a>
<a <a
class="nav-item" class="nav-item"
[routerLinkActiveOptions]="{ exact: false }" [routerLinkActiveOptions]="{ exact: false }"
@@ -49,6 +58,17 @@
<span> {{ 'MENU.FAILEDEVENTS' | translate }} </span> <span> {{ 'MENU.FAILEDEVENTS' | translate }} </span>
</div> </div>
</a> </a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/settings']"
>
<div class="c_label">
<span> {{ 'MENU.SETTINGS' | translate }} </span>
</div>
</a>
</ng-template> </ng-template>
<template [ngTemplateOutlet]="shortcutKeyRef"></template> <template [ngTemplateOutlet]="shortcutKeyRef"></template>
@@ -56,7 +76,7 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="BreadcrumbType.ORG"> <ng-container *ngSwitchCase="BreadcrumbType.ORG">
<div class="nav-row-abs" @navroworg> <div class="nav-row-abs" @navrowproject>
<a <a
class="nav-item" class="nav-item"
[routerLinkActive]="['active']" [routerLinkActive]="['active']"
@@ -142,13 +162,24 @@
<span class="label">{{ 'MENU.DOMAINS' | translate }}</span> <span class="label">{{ 'MENU.DOMAINS' | translate }}</span>
</a> </a>
</ng-template> </ng-template>
<ng-template cnslHasRole [hasRole]="['org.read']">
<a
class="nav-item"
[routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: true }"
[routerLink]="['/org-settings']"
>
<span class="label">{{ 'MENU.SETTINGS' | translate }}</span>
</a>
</ng-template>
</ng-container> </ng-container>
<template [ngTemplateOutlet]="shortcutKeyRef"></template> <template [ngTemplateOutlet]="shortcutKeyRef"></template>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="BreadcrumbType.PROJECT"> <!-- <ng-container *ngSwitchCase="BreadcrumbType.PROJECT">
<div *ngIf="breadc[breadc.length - 1]?.param?.value" class="nav-row-abs" @navrowproject> <div *ngIf="breadc[breadc.length - 1]?.param?.value" class="nav-row-abs" @navrowproject>
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']"> <ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
<a <a
@@ -207,7 +238,7 @@
<template [ngTemplateOutlet]="shortcutKeyRef"></template> <template [ngTemplateOutlet]="shortcutKeyRef"></template>
</div> </div>
</ng-container> </ng-container> -->
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@@ -1,48 +1,74 @@
<div class="org-context-card" cdkTrapFocus> <div class="org-context-card" cdkTrapFocus>
<div class="spinner-w"> <div class="spinner-w">
<mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent"> <mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent"> </mat-spinner>
</mat-spinner>
</div> </div>
<div class="filter-wrapper"> <div class="filter-wrapper">
<input cnslInput class="filter-input" [formControl]="filterControl" autocomplete="off" <input
(click)="$event.stopPropagation()" placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input> cnslInput
class="filter-input"
[formControl]="filterControl"
autocomplete="off"
(click)="$event.stopPropagation()"
placeholder="{{ 'ORG.PAGES.FILTERPLACEHOLDER' | translate }}"
#input
/>
</div> </div>
<div class="org-wrapper"> <div class="org-wrapper">
<button class="org-button-with-pin" mat-button <button
[ngClass]="{'active': pinnedorg.id === org?.id, 'border-bottom':pinned.selected.length && i === pinned.selected.length -1}" class="org-button-with-pin"
[disabled]="!pinnedorg.id" *ngFor="let pinnedorg of pinned.selected; index as i" mat-button
(click)="setActiveOrg(pinnedorg)"> [ngClass]="{
active: pinnedorg.id === org?.id,
'border-bottom': pinned.selected.length && i === pinned.selected.length - 1
}"
[disabled]="!pinnedorg.id"
*ngFor="let pinnedorg of pinned.selected; index as i"
(click)="setActiveOrg(pinnedorg)"
>
<div class="org-flex-row"> <div class="org-flex-row">
<span class="org-span">{{pinnedorg?.name ? pinnedorg.name : 'NO NAME'}}</span> <span class="org-span">{{ pinnedorg?.name ? pinnedorg.name : 'NO NAME' }}</span>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: pinnedorg}"></template> <template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{ key: pinnedorg }"></template>
</div> </div>
</button> </button>
<ng-container *ngFor="let temporg of orgs$ | async"> <ng-container *ngFor="let temporg of orgs$ | async">
<button *ngIf="!pinned.isSelected(temporg)" class="org-button-with-pin" mat-button <button
[ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id" (click)="setActiveOrg(temporg)"> *ngIf="!pinned.isSelected(temporg)"
<div class="org-flex-row"><span class="org-span">{{temporg?.name ? temporg.name : 'NO NAME'}}</span> class="org-button-with-pin"
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{key: temporg}"></template> mat-button
[ngClass]="{ active: temporg.id === org?.id }"
[disabled]="!temporg.id"
(click)="setActiveOrg(temporg)"
>
<div class="org-flex-row">
<span class="org-span">{{ temporg?.name ? temporg.name : 'NO NAME' }}</span>
<template [ngTemplateOutlet]="toggleButton" [ngTemplateOutletContext]="{ key: temporg }"></template>
</div> </div>
</button> </button>
</ng-container> </ng-container>
</div> </div>
<button mat-button class="show-all" [routerLink]="[ '/org/overview' ]" (click)="closedCard.emit()">{{'MENU.SHOWORGS' | <button mat-button class="show-all" [routerLink]="['/orgs']" (click)="closedCard.emit()">
translate}}</button> {{ 'MENU.SHOWORGS' | translate }}
</button>
<ng-template cnslHasRole [hasRole]="['org.create','iam.write']"> <ng-template cnslHasRole [hasRole]="['org.create', 'iam.write']">
<button mat-button [routerLink]="[ '/org/create' ]" (click)="closedCard.emit()"> <button mat-button [routerLink]="['/org/create']" (click)="closedCard.emit()">
<mat-icon class="avatar">add</mat-icon> <mat-icon class="avatar">add</mat-icon>
{{'MENU.NEWORG' | translate}} {{ 'MENU.NEWORG' | translate }}
</button> </button>
</ng-template> </ng-template>
</div> </div>
<ng-template #toggleButton let-key="key"> <ng-template #toggleButton let-key="key">
<button matTooltip="{{'ACTIONS.PIN' | translate}}" [ngClass]="{ selected: pinned.isSelected(key)}" <button
(click)="toggle(key,$event)" class="edit-button" mat-icon-button> matTooltip="{{ 'ACTIONS.PIN' | translate }}"
[ngClass]="{ selected: pinned.isSelected(key) }"
(click)="toggle(key, $event)"
class="edit-button"
mat-icon-button
>
<mat-icon *ngIf="pinned.isSelected(key)" svgIcon="mdi_pin"></mat-icon> <mat-icon *ngIf="pinned.isSelected(key)" svgIcon="mdi_pin"></mat-icon>
<mat-icon svgIcon="mdi_pin_outline" *ngIf="!pinned.isSelected(key)"></mat-icon> <mat-icon svgIcon="mdi_pin_outline" *ngIf="!pinned.isSelected(key)"></mat-icon>
</button> </button>

View File

@@ -0,0 +1,106 @@
<cnsl-refresh-table
*ngIf="dataSource"
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
[loading]="loading$ | async"
>
<cnsl-filter-org actions (filterChanged)="applySearchQuery($any($event))" (filterOpen)="filterOpen = $event">
</cnsl-filter-org>
<ng-template actions cnslHasRole [hasRole]="['org.create', 'iam.write']">
<a [routerLink]="['/org', 'create']" color="primary" mat-raised-button class="cnsl-action-button">
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink(['/org', 'create'])"> </cnsl-action-keys>
</a>
</ng-template>
<table [dataSource]="dataSource" mat-table class="table" aria-label="Elements">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.ACTIVE' | translate }}
</th>
<td class="selection" mat-cell *matCellDef="let org">
<mat-radio-button (change)="selectOrg(org)" color="primary" [checked]="org.id === activeOrg.id"> </mat-radio-button>
</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.ID' | translate }}</th>
<td mat-cell *matCellDef="let org" (change)="selectOrg(org)">{{ org.id }}</td>
</ng-container>
<ng-container matColumnDef="primaryDomain">
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.PRIMARYDOMAIN' | translate }}</th>
<td mat-cell *matCellDef="let org">
<span>{{ org.primaryDomain }}</span>
<button
color="primary"
class="cpy-button"
mat-icon-button
[disabled]="copied === org.primaryDomain"
[matTooltip]="(copied !== org.primaryDomain ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="org.primaryDomain"
(copiedValue)="copied = $event"
>
<i *ngIf="copied !== org.primaryDomain" class="las la-clipboard"></i>
<i *ngIf="copied === org.primaryDomain" class="las la-clipboard-check"></i>
</button>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.NAME' | translate }}
</th>
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">{{ org.name }}</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.STATE' | translate }}</th>
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
<span
class="state"
[ngClass]="{
active: org.state === OrgState.ORG_STATE_ACTIVE,
inactive: org.state === OrgState.ORG_STATE_INACTIVE
}"
*ngIf="org.state"
>{{ 'ORG.STATE.' + org.state | translate }}</span
>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.CREATIONDATE' | translate }}
</th>
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
{{ org.details?.creationDate | timestampToDate | localizedDate: 'fromNow' }}
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.DATECHANGED' | translate }}
</th>
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
{{ org.details?.changeDate | timestampToDate | localizedDate: 'fromNow' }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<cnsl-paginator
#paginator
class="paginator"
[timestamp]="timestamp"
[length]="totalResult || 0"
[pageSize]="initialLimit"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"
></cnsl-paginator>
</cnsl-refresh-table>

View File

@@ -1,11 +1,3 @@
h1 {
margin: 0;
}
.org-desc {
font-size: 14px;
}
td { td {
cursor: pointer; cursor: pointer;

View File

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

View File

@@ -1,34 +1,30 @@
import { Component, Input, ViewChild } from '@angular/core'; import { Component, Input, ViewChild } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
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, from, Observable, of } from 'rxjs'; import { BehaviorSubject, catchError, finalize, from, map, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { enterAnimations } from 'src/app/animations';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { Org, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb'; import { Org, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
enum OrgListSearchKey { enum OrgListSearchKey {
NAME = 'NAME', NAME = 'NAME',
} }
@Component({ @Component({
selector: 'cnsl-org-list', selector: 'cnsl-org-table',
templateUrl: './org-list.component.html', templateUrl: './org-table.component.html',
styleUrls: ['./org-list.component.scss'], styleUrls: ['./org-table.component.scss'],
animations: [enterAnimations],
}) })
export class OrgListComponent { export class OrgTableComponent {
public orgSearchKey: OrgListSearchKey | undefined = undefined; public orgSearchKey: OrgListSearchKey | undefined = undefined;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild('input') public filter!: Input; @ViewChild('input') public filter!: Input;
public dataSource!: MatTableDataSource<Org.AsObject>; public dataSource!: MatTableDataSource<Org.AsObject>;
public displayedColumns: string[] = ['select', 'name', 'state', 'primaryDomain', 'creationDate', 'changeDate']; public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public activeOrg!: Org.AsObject; public activeOrg!: Org.AsObject;
@@ -39,20 +35,9 @@ export class OrgListComponent {
public filterOpen: boolean = false; public filterOpen: boolean = false;
public OrgState: any = OrgState; public OrgState: any = OrgState;
public copied: string = ''; public copied: string = '';
constructor(private authService: GrpcAuthService, private router: Router, breadcrumbService: BreadcrumbService) { constructor(private authService: GrpcAuthService, private router: Router) {
this.loadOrgs(this.initialLimit, 0); this.loadOrgs(this.initialLimit, 0);
this.authService.getActiveOrg().then((org) => (this.activeOrg = org)); this.authService.getActiveOrg().then((org) => (this.activeOrg = org));
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iamBread, bread]);
} }
public loadOrgs(limit: number, offset: number, queries?: OrgQuery[]): void { public loadOrgs(limit: number, offset: number, queries?: OrgQuery[]): void {

View File

@@ -7,25 +7,24 @@ import { MatRadioModule } from '@angular/material/radio';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { FilterOrgModule } from 'src/app/modules/filter-org/filter-org.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { OrgListRoutingModule } from './org-list-routing.module'; import { ActionKeysModule } from '../action-keys/action-keys.module';
import { OrgListComponent } from './org-list.component'; import { FilterOrgModule } from '../filter-org/filter-org.module';
import { InputModule } from '../input/input.module';
import { PaginatorModule } from '../paginator/paginator.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { OrgTableComponent } from './org-table.component';
@NgModule({ @NgModule({
declarations: [OrgListComponent], declarations: [OrgTableComponent],
imports: [ imports: [
CommonModule, CommonModule,
OrgListRoutingModule,
MatTableModule, MatTableModule,
TranslateModule, TranslateModule,
RefreshTableModule, RefreshTableModule,
@@ -37,6 +36,7 @@ import { OrgListComponent } from './org-list.component';
MatIconModule, MatIconModule,
PaginatorModule, PaginatorModule,
HasRoleModule, HasRoleModule,
RouterModule,
MatButtonModule, MatButtonModule,
MatTooltipModule, MatTooltipModule,
CopyToClipboardModule, CopyToClipboardModule,
@@ -44,5 +44,6 @@ import { OrgListComponent } from './org-list.component';
InputModule, InputModule,
FormsModule, FormsModule,
], ],
exports: [OrgTableComponent],
}) })
export class OrgListModule {} export class OrgTableModule {}

View File

@@ -0,0 +1,17 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<h2>{{ 'SETTING.DEFAULTLANGUAGE' | translate }}</h2>
<cnsl-form-field class="default-language" label="Default Language" required="true">
<cnsl-label>{{ 'SETTING.DEFAULTLANGUAGE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="defaultLanguage">
<mat-option *ngFor="let lang of defaultLanguageOptions" [value]="lang">
{{ lang }} - {{ 'SETTING.LANGUAGE.' + lang | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<div class="general-btn-container">
<button class="save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@@ -0,0 +1,17 @@
.spinner-wr {
margin: 0.5rem 0;
}
.default-language {
max-width: 400px;
display: block;
}
.general-btn-container {
display: flex;
justify-content: flex-start;
.save-button {
display: block;
}
}

View File

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

View File

@@ -0,0 +1,88 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { SetDefaultLanguageResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'cnsl-general-settings',
templateUrl: './general-settings.component.html',
styleUrls: ['./general-settings.component.scss'],
})
export class GeneralSettingsComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public service!: ManagementService | AdminService;
public defaultLanguage: string = '';
public defaultLanguageOptions: string[] = [];
public loading: boolean = false;
constructor(private injector: Injector, private toast: ToastService) {}
ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
this.fetchData();
}
private fetchData(): void {
if (this.serviceType === PolicyComponentServiceType.ADMIN) {
(this.service as AdminService).getDefaultLanguage().then((langResp) => {
this.defaultLanguage = langResp.language;
});
(this.service as AdminService).getSupportedLanguages().then((supportedResp) => {
this.defaultLanguageOptions = supportedResp.languagesList;
});
}
}
private updateData(): Promise<SetDefaultLanguageResponse.AsObject> | void {
if (this.serviceType === PolicyComponentServiceType.ADMIN) {
return (this.service as AdminService).setDefaultLanguage(this.defaultLanguage);
} else {
return;
}
}
public savePolicy(): void {
const prom = this.updateData();
if (prom) {
prom
.then(() => {
this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService)
.resetLoginPolicyToDefault()
.then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
}
}
}

View File

@@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { GeneralSettingsComponent } from './general-settings.component';
@NgModule({
declarations: [GeneralSettingsComponent],
imports: [
CommonModule,
CardModule,
FormsModule,
MatButtonModule,
FormFieldModule,
MatProgressSpinnerModule,
MatSelectModule,
TranslateModule,
],
exports: [GeneralSettingsComponent],
})
export class GeneralSettingsModule {}

View File

@@ -0,0 +1,3 @@
<h2>{{ 'IDP.LIST.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'IDP.LIST.DESCRIPTION' | translate }}</p>
<cnsl-idp-table [service]="service" [serviceType]="serviceType"> </cnsl-idp-table>

View File

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

View File

@@ -0,0 +1,29 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'cnsl-idp-settings',
templateUrl: './idp-settings.component.html',
styleUrls: ['./idp-settings.component.scss'],
})
export class IdpSettingsComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(private injector: Injector) {}
ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
}
}

View File

@@ -0,0 +1,15 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from '../../card/card.module';
import { IdpTableModule } from '../../idp-table/idp-table.module';
import { IdpSettingsComponent } from './idp-settings.component';
@NgModule({
declarations: [IdpSettingsComponent],
imports: [CommonModule, CardModule, IdpTableModule, MatProgressSpinnerModule, TranslateModule],
exports: [IdpSettingsComponent],
})
export class IdpSettingsModule {}

View File

@@ -1,93 +1,24 @@
<cnsl-detail-layout <!-- <cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section> -->
[hasBackButton]="true"
[title]="'POLICY.LOGIN_POLICY.TITLE' | translate"
[description]="
(serviceType === PolicyComponentServiceType.MGMT
? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEMGMT'
: PolicyComponentServiceType.ADMIN
? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEADMIN'
: ''
) | translate
"
>
<p class="policy-applied-to" sub>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section> <div class="spinner-wr">
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner> <mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div> </div>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT"> <h2>{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}</h2>
<ng-template cnslHasRole [hasRole]="['policy.delete']"> <p class="cnsl-secondary-text">{{ 'MFA.LIST.MULTIFACTORDESCRIPTION' | translate }}</p>
<button
*ngIf="!isDefault"
color="primary"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
<ng-template cnslHasRole [hasRole]="['policy.write']"> <div class="login-policy-row" *ngIf="loginData">
<button <cnsl-form-field class="passwordless-allowed" label="Access Code" required="true">
*ngIf="isDefault"
color="primary"
matTooltip="{{ 'POLICY.CREATECUSTOM' | translate }}"
(click)="savePolicy()"
mat-raised-button
>
{{ 'POLICY.CREATECUSTOM' | translate }}
</button>
</ng-template>
</ng-container>
<ng-template cnslHasRole [hasRole]="['org.idp.read']">
<cnsl-card title="{{ 'IDP.LIST.TITLE' | translate }}" description="{{ 'IDP.LIST.DESCRIPTION' | translate }}">
<cnsl-idp-table
[service]="service"
[serviceType]="serviceType"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.idp.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'org.idp.write'
: ''
]
| hasRole
| async) === false
"
>
</cnsl-idp-table>
</cnsl-card>
</ng-template>
<cnsl-card
title="{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}"
description="{{ 'MFA.LIST.MULTIFACTORDESCRIPTION' | translate }}"
[expanded]="true"
>
<div class="login-policy-row">
<cnsl-form-field *ngIf="loginData" class="passwordless-allowed" label="Access Code" required="true">
<cnsl-label>{{ 'LOGINPOLICY.PASSWORDLESS' | translate }}</cnsl-label> <cnsl-label>{{ 'LOGINPOLICY.PASSWORDLESS' | translate }}</cnsl-label>
<mat-select [(ngModel)]="loginData.passwordlessType" [disabled]="disabled"> <mat-select [(ngModel)]="loginData.passwordlessType">
<mat-option *ngFor="let pt of passwordlessTypes" [value]="pt"> <mat-option *ngFor="let pt of passwordlessTypes" [value]="pt">
{{ 'LOGINPOLICY.PASSWORDLESSTYPE.' + pt | translate }} {{ 'LOGINPOLICY.PASSWORDLESSTYPE.' + pt | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<cnsl-card class="max-card-width">
<cnsl-mfa-table <cnsl-mfa-table
[service]="service" [service]="service"
[serviceType]="serviceType" [serviceType]="serviceType"
@@ -106,25 +37,25 @@
" "
> >
</cnsl-mfa-table> </cnsl-mfa-table>
</cnsl-card> </cnsl-card>
<cnsl-card <br />
*ngIf="loginData"
title="{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}" <h2>{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}</h2>
description="{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}" <p class="cnsl-secondary-text">{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}</p>
[expanded]="true"
> <div *ngIf="loginData" class="login-policy-row">
<mat-slide-toggle <mat-slide-toggle
card-actions card-actions
class="force-mfa-toggle" class="login-policy-toggle"
color="primary" color="primary"
[disabled]="disabled"
ngDefaultControl ngDefaultControl
[(ngModel)]="loginData.forceMfa" [(ngModel)]="loginData.forceMfa"
> >
{{ 'POLICY.DATA.FORCEMFA' | translate }} {{ 'POLICY.DATA.FORCEMFA' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div>
<cnsl-card class="max-card-width">
<cnsl-mfa-table <cnsl-mfa-table
[service]="service" [service]="service"
[serviceType]="serviceType" [serviceType]="serviceType"
@@ -142,95 +73,81 @@
" "
> >
</cnsl-mfa-table> </cnsl-mfa-table>
</cnsl-card> </cnsl-card>
<cnsl-card title="{{ 'POLICY.LOGIN_POLICY.ADVANCED' | translate }}"> <br />
<div class="login-policy-content" *ngIf="loginData">
<h2>{{ 'POLICY.LOGIN_POLICY.ADVANCED' | translate }}</h2>
<cnsl-card class="max-card-width login-policy-content" *ngIf="loginData">
<div class="login-policy-row"> <div class="login-policy-row">
<mat-slide-toggle <mat-slide-toggle
class="login-policy-toggle"
color="primary" color="primary"
matTooltip="{{ 'POLICY.DATA.FORCEMFA_DESC' | translate }}" matTooltip="{{ 'POLICY.DATA.FORCEMFA_DESC' | translate }}"
[disabled]="disabled"
ngDefaultControl ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword" [(ngModel)]="loginData.allowUsernamePassword"
> >
{{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate }} {{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<ng-template #usernameInfo> <!-- <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> -->
</ng-template>
</div> </div>
<div class="login-policy-row"> <div class="login-policy-row">
<mat-slide-toggle <mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.allowRegister">
class="login-policy-toggle"
color="primary"
[disabled]="disabled"
ngDefaultControl
[(ngModel)]="loginData.allowRegister"
>
{{ 'POLICY.DATA.ALLOWREGISTER' | translate }} {{ 'POLICY.DATA.ALLOWREGISTER' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<ng-template #regInfo> <!-- <ng-template #regInfo>
<cnsl-info-section class="info"> <cnsl-info-section class="info">
{{ 'POLICY.DATA.ALLOWREGISTER_DESC' | translate }} {{ 'POLICY.DATA.ALLOWREGISTER_DESC' | translate }}
</cnsl-info-section> </cnsl-info-section>
</ng-template> </ng-template> -->
</div> </div>
<div class="login-policy-row"> <div class="login-policy-row">
<mat-slide-toggle <mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.allowExternalIdp">
class="login-policy-toggle"
color="primary"
[disabled]="disabled"
ngDefaultControl
[(ngModel)]="loginData.allowExternalIdp"
>
{{ 'POLICY.DATA.ALLOWEXTERNALIDP' | translate }} {{ 'POLICY.DATA.ALLOWEXTERNALIDP' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<ng-template #idpInfo> <!-- <ng-template #idpInfo>
<cnsl-info-section class="info"> <cnsl-info-section class="info">
{{ 'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate }} {{ 'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate }}
</cnsl-info-section> </cnsl-info-section>
</ng-template> </ng-template> -->
</div> </div>
<div class="login-policy-row"> <div class="login-policy-row">
<mat-slide-toggle <mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.hidePasswordReset">
class="login-policy-toggle"
color="primary"
[disabled]="disabled"
ngDefaultControl
[(ngModel)]="loginData.hidePasswordReset"
>
{{ 'POLICY.DATA.HIDEPASSWORDRESET' | translate }} {{ 'POLICY.DATA.HIDEPASSWORDRESET' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<ng-template #passwordResetInfo> <!-- <ng-template #passwordResetInfo>
<cnsl-info-section class="info"> <cnsl-info-section class="info">
{{ 'POLICY.DATA.HIDEPASSWORDRESET_DESC' | translate }} {{ 'POLICY.DATA.HIDEPASSWORDRESET_DESC' | translate }}
</cnsl-info-section> </cnsl-info-section>
</ng-template> </ng-template> -->
</div> </div>
</div> </cnsl-card>
</cnsl-card>
<div class="login-policy-btn-container"> <div class="login-policy-btn-container">
<button <button class="login-policy-save-button" (click)="savePolicy()" color="primary" type="submit" mat-raised-button>
[disabled]="disabled"
class="login-policy-save-button"
(click)="savePolicy()"
color="primary"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
<cnsl-policy-grid class="grid" [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="security"> <ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT">
</cnsl-policy-grid> <ng-template cnslHasRole [hasRole]="['policy.delete']">
</cnsl-detail-layout> <button
*ngIf="!isDefault"
color="primary"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
</ng-container>

View File

@@ -2,9 +2,9 @@
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.policy-applied-to { .max-card-width {
margin: -1rem 0 0 0; max-width: 400px;
font-size: 14px; display: block;
} }
.passwordless-allowed { .passwordless-allowed {
@@ -12,15 +12,9 @@
display: block; display: block;
} }
.force-mfa-toggle {
margin-right: 1rem;
}
.login-policy-content { .login-policy-content {
padding-top: 1rem;
.login-policy-row { .login-policy-row {
padding-bottom: 1.5rem; padding-bottom: 0.5rem;
.login-policy-toggle { .login-policy-toggle {
margin: 0.3rem 0; margin: 0.3rem 0;
@@ -34,10 +28,9 @@
.login-policy-btn-container { .login-policy-btn-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
.login-policy-save-button { .login-policy-save-button {
margin-bottom: 3rem;
display: block; display: block;
} }
} }

View File

@@ -1,7 +1,4 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { import {
GetLoginPolicyResponse as AdminGetLoginPolicyResponse, GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
UpdateLoginPolicyRequest, UpdateLoginPolicyRequest,
@@ -11,16 +8,12 @@ import {
AddCustomLoginPolicyRequest, AddCustomLoginPolicyRequest,
GetLoginPolicyResponse as MgmtGetLoginPolicyResponse, GetLoginPolicyResponse as MgmtGetLoginPolicyResponse,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { LoginPolicy, PasswordlessType } from 'src/app/proto/generated/zitadel/policy_pb'; import { LoginPolicy, PasswordlessType } 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 { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.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 { InfoSectionType } from '../../info-section/info-section.component'; import { InfoSectionType } from '../../info-section/info-section.component';
import { GridPolicy, LOGIN_POLICY } from '../../policy-grid/policies';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { LoginMethodComponentType } from './mfa-table/mfa-table.component'; import { LoginMethodComponentType } from './mfa-table/mfa-table.component';
@@ -29,7 +22,7 @@ import { LoginMethodComponentType } from './mfa-table/mfa-table.component';
templateUrl: './login-policy.component.html', templateUrl: './login-policy.component.html',
styleUrls: ['./login-policy.component.scss'], styleUrls: ['./login-policy.component.scss'],
}) })
export class LoginPolicyComponent implements OnDestroy { export class LoginPolicyComponent implements OnInit {
public LoginMethodComponentType: any = LoginMethodComponentType; public LoginMethodComponentType: any = LoginMethodComponentType;
public passwordlessTypes: Array<PasswordlessType> = [ public passwordlessTypes: Array<PasswordlessType> = [
PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED, PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED,
@@ -37,30 +30,26 @@ export class LoginPolicyComponent implements OnDestroy {
]; ];
public loginData!: LoginPolicy.AsObject; public loginData!: LoginPolicy.AsObject;
private sub: Subscription = new Subscription();
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public loading: boolean = false; public loading: boolean = false;
public disabled: boolean = true;
public currentPolicy: GridPolicy = LOGIN_POLICY;
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public orgName: string = '';
public PasswordlessType: any = PasswordlessType; public PasswordlessType: any = PasswordlessType;
constructor( constructor(private toast: ToastService, private injector: Injector) {}
private route: ActivatedRoute,
private toast: ToastService, private fetchData(): void {
private injector: Injector, this.getData().then((resp) => {
breadcrumbService: BreadcrumbService, if (resp.policy) {
private storageService: StorageService, this.loginData = resp.policy;
) { this.loading = false;
this.sub = this.route.data }
.pipe( });
switchMap((data) => { }
this.serviceType = data.serviceType;
public ngOnInit(): void {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
@@ -68,22 +57,6 @@ export class LoginPolicyComponent implements OnDestroy {
PasswordlessType.PASSWORDLESS_TYPE_ALLOWED, PasswordlessType.PASSWORDLESS_TYPE_ALLOWED,
PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED, PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED,
]; ];
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
@@ -91,36 +64,9 @@ export class LoginPolicyComponent implements OnDestroy {
PasswordlessType.PASSWORDLESS_TYPE_ALLOWED, PasswordlessType.PASSWORDLESS_TYPE_ALLOWED,
PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED, PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED,
]; ];
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
} }
return this.route.params;
}),
)
.subscribe(() => {
this.fetchData(); this.fetchData();
});
}
private fetchData(): void {
this.getData().then((resp) => {
if (resp.policy) {
this.loginData = resp.policy;
this.loading = false;
this.disabled = this.isDefault;
}
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
} }
private async getData(): Promise<AdminGetLoginPolicyResponse.AsObject | MgmtGetLoginPolicyResponse.AsObject> { private async getData(): Promise<AdminGetLoginPolicyResponse.AsObject | MgmtGetLoginPolicyResponse.AsObject> {

View File

@@ -13,12 +13,10 @@ 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';
import { CardModule } from 'src/app/modules/card/card.module'; import { CardModule } from 'src/app/modules/card/card.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { IdpTableModule } from 'src/app/modules/idp-table/idp-table.module';
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
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 { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { LoginPolicyRoutingModule } from './login-policy-routing.module'; import { LoginPolicyRoutingModule } from './login-policy-routing.module';
import { LoginPolicyComponent } from './login-policy.component'; import { LoginPolicyComponent } from './login-policy.component';
import { DialogAddTypeComponent } from './mfa-table/dialog-add-type/dialog-add-type.component'; import { DialogAddTypeComponent } from './mfa-table/dialog-add-type/dialog-add-type.component';
@@ -41,12 +39,11 @@ import { MfaTableComponent } from './mfa-table/mfa-table.component';
HasRolePipeModule, HasRolePipeModule,
MatTooltipModule, MatTooltipModule,
DetailLayoutModule, DetailLayoutModule,
IdpTableModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatSelectModule, MatSelectModule,
MatRippleModule, MatRippleModule,
TranslateModule, TranslateModule,
PolicyGridModule,
], ],
exports: [LoginPolicyComponent],
}) })
export class LoginPolicyModule {} export class LoginPolicyModule {}

View File

@@ -5,19 +5,29 @@
<div class="login-policy-mfa-list"> <div class="login-policy-mfa-list">
<div class="mfa" *ngFor="let mfa of mfas"> <div class="mfa" *ngFor="let mfa of mfas">
<span> <span>
{{(componentType === LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.': {{
LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}} (componentType === LoginMethodComponentType.SecondFactor
? 'MFA.SECONDFACTORTYPES.'
: LoginMethodComponentType.MultiFactor
? 'MFA.MULTIFACTORTYPES.'
: '') + mfa | translate
}}
</span> </span>
<button color="warn" *ngIf="!disabled" mat-icon-button (click)="removeMfa(mfa)" class="rm"> <button color="warn" *ngIf="!disabled" mat-icon-button (click)="removeMfa(mfa)" class="rm">
<i matTooltip="{{'ACTIONS.REMOVE' | translate}}" class="las la-times-circle"></i> <i matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" class="las la-times-circle"></i>
</button> </button>
</div> </div>
<div class="mfa-list-btns"> <div class="mfa-list-btns">
<button mat-raised-button color="primary" class="new-mfa cnsl-action-button" [disabled]="disabled" <button
(click)="!disabled ? addMfa(): null"> mat-stroked-button
color="primary"
class="new-mfa cnsl-action-button"
[disabled]="disabled"
(click)="!disabled ? addMfa() : null"
>
<mat-icon class="icon">add</mat-icon> <mat-icon class="icon">add</mat-icon>
<span>{{'ACTIONS.ADD' | translate}}</span> <span>{{ 'ACTIONS.ADD' | translate }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -13,6 +13,7 @@
justify-content: space-between; justify-content: space-between;
padding: 0.5rem 0; padding: 0.5rem 0;
min-height: 40px; min-height: 40px;
max-width: 400px;
border-bottom: 1px solid map-get($foreground, dividers); border-bottom: 1px solid map-get($foreground, dividers);
&:last-child { &:last-child {
@@ -42,7 +43,8 @@
.mfa-list-btns { .mfa-list-btns {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
max-width: 400px;
.new-mfa { .new-mfa {
margin-top: 0.5rem; margin-top: 0.5rem;

View File

@@ -1,16 +1,6 @@
<cnsl-detail-layout <h2>{{ 'POLICY.LOGIN_TEXTS.TITLE' | translate }}</h2>
[hasBackButton]="true" <p class="cnsl-secondary-text">{{ 'POLICY.LOGIN_TEXTS.DESCRIPTION' | translate }}</p>
[title]="'POLICY.LOGIN_TEXTS.TITLE' | translate" <div class="date">
[description]="'POLICY.LOGIN_TEXTS.DESCRIPTION' | translate"
>
<p class="policy-applied-to" sub>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<div class="date">
<div> <div>
<p class="newer-title" *ngIf="newerVersionExists">{{ 'POLICY.LOGIN_TEXTS.NEWERVERSIONEXISTS' | translate }}</p> <p class="newer-title" *ngIf="newerVersionExists">{{ 'POLICY.LOGIN_TEXTS.NEWERVERSIONEXISTS' | translate }}</p>
<p *ngIf="newerPolicyChangeDate && newerVersionExists"> <p *ngIf="newerPolicyChangeDate && newerVersionExists">
@@ -26,8 +16,8 @@
<i class="las la-sync-alt"></i> <i class="las la-sync-alt"></i>
{{ 'ACTIONS.REFRESH' | translate }} {{ 'ACTIONS.REFRESH' | translate }}
</button> </button>
</div> </div>
<form *ngIf="form" class="top-actions" [formGroup]="form"> <form *ngIf="form" class="top-actions" [formGroup]="form">
<cnsl-form-field class="keys" appearance="outline"> <cnsl-form-field class="keys" appearance="outline">
<cnsl-label>{{ 'POLICY.LOGIN_TEXTS.KEYNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'POLICY.LOGIN_TEXTS.KEYNAME' | translate }}</cnsl-label>
<mat-select formControlName="currentSubMap" name="currentSubMap"> <mat-select formControlName="currentSubMap" name="currentSubMap">
@@ -52,11 +42,11 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
</form> </form>
<div class="divider"></div> <div class="divider"></div>
<div class="content"> <div class="content">
<cnsl-edit-text <cnsl-edit-text
label="one" label="one"
[disabled]="(canWrite$ | async) === false" [disabled]="(canWrite$ | async) === false"
@@ -64,9 +54,9 @@
[current$]="getCustomInitMessageTextMap$" [current$]="getCustomInitMessageTextMap$"
(changedValues)="updateCurrentValues($event)" (changedValues)="updateCurrentValues($event)"
></cnsl-edit-text> ></cnsl-edit-text>
</div> </div>
<div class="actions"> <div class="actions">
<!-- *ngIf="totalCustomPolicy && totalCustomPolicy.isDefault === false" --> <!-- *ngIf="totalCustomPolicy && totalCustomPolicy.isDefault === false" -->
<button <button
class="reset-button" class="reset-button"
@@ -88,7 +78,4 @@
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text"></cnsl-policy-grid>
</cnsl-detail-layout>

View File

@@ -32,18 +32,16 @@
} }
.top-actions { .top-actions {
display: flex; display: grid;
margin: 0 -0.5rem; grid-template-columns: 1fr 1fr 1fr 1fr;
flex-wrap: wrap; column-gap: 1rem;
.keys { .keys {
flex: 1;
margin: 0 0.5rem;
min-width: 150px; min-width: 150px;
grid-column: span 3;
} }
.language { .language {
margin: 0 0.5rem;
min-width: 150px; min-width: 150px;
.lighter { .lighter {

View File

@@ -1,10 +1,9 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, from, interval, Observable, of, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, from, interval, Observable, of, Subject, Subscription } from 'rxjs';
import { map, pairwise, startWith, switchMap, takeUntil } from 'rxjs/operators'; import { map, pairwise, startWith, takeUntil } from 'rxjs/operators';
import { import {
GetCustomLoginTextsRequest as AdminGetCustomLoginTextsRequest, GetCustomLoginTextsRequest as AdminGetCustomLoginTextsRequest,
GetDefaultLoginTextsRequest as AdminGetDefaultLoginTextsRequest, GetDefaultLoginTextsRequest as AdminGetDefaultLoginTextsRequest,
@@ -15,16 +14,12 @@ import {
GetDefaultLoginTextsRequest, GetDefaultLoginTextsRequest,
SetCustomLoginTextsRequest, SetCustomLoginTextsRequest,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.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 { InfoSectionType } from '../../info-section/info-section.component'; import { InfoSectionType } from '../../info-section/info-section.component';
import { GridPolicy, LOGIN_TEXTS_POLICY } from '../../policy-grid/policies';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { mapRequestValues } from './helper'; import { mapRequestValues } from './helper';
@@ -97,7 +92,7 @@ const REQUESTMAP = {
templateUrl: './login-texts.component.html', templateUrl: './login-texts.component.html',
styleUrls: ['./login-texts.component.scss'], styleUrls: ['./login-texts.component.scss'],
}) })
export class LoginTextsComponent implements OnDestroy { export class LoginTextsComponent implements OnInit, OnDestroy {
public currentPolicyChangeDate!: Timestamp.AsObject | undefined; public currentPolicyChangeDate!: Timestamp.AsObject | undefined;
public newerPolicyChangeDate!: Timestamp.AsObject | undefined; public newerPolicyChangeDate!: Timestamp.AsObject | undefined;
@@ -108,7 +103,7 @@ export class LoginTextsComponent implements OnDestroy {
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public KeyNamesArray: string[] = KeyNamesArray; public KeyNamesArray: string[] = KeyNamesArray;
public LOCALES: string[] = ['en', 'de', 'it']; public LOCALES: string[] = ['en', 'de', 'it'];
@@ -116,11 +111,9 @@ export class LoginTextsComponent implements OnDestroy {
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public updateRequest!: SetCustomLoginTextsRequest; public updateRequest!: SetCustomLoginTextsRequest;
public currentPolicy: GridPolicy = LOGIN_TEXTS_POLICY;
public destroy$: Subject<void> = new Subject(); public destroy$: Subject<void> = new Subject();
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public orgName: string = '';
public form: FormGroup = new FormGroup({ public form: FormGroup = new FormGroup({
currentSubMap: new FormControl('emailVerificationDoneText'), currentSubMap: new FormControl('emailVerificationDoneText'),
locale: new FormControl('en'), locale: new FormControl('en'),
@@ -135,76 +128,10 @@ export class LoginTextsComponent implements OnDestroy {
]); ]);
constructor( constructor(
private authService: GrpcAuthService, private authService: GrpcAuthService,
private route: ActivatedRoute,
private injector: Injector, private injector: Injector,
private dialog: MatDialog, private dialog: MatDialog,
private toast: ToastService, private toast: ToastService,
private storageService: StorageService,
breadcrumbService: BreadcrumbService,
) { ) {
this.sub = this.route.data
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData();
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData();
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break;
}
return this.route.params;
}),
)
.subscribe(() => {
interval(10000)
.pipe(
// debounceTime(5000),
takeUntil(this.destroy$),
)
.subscribe((x) => {
this.checkForChanges();
});
});
this.form.valueChanges this.form.valueChanges
.pipe(startWith({ currentSubMap: 'emailVerificationDoneText', locale: 'en' }), pairwise(), takeUntil(this.destroy$)) .pipe(startWith({ currentSubMap: 'emailVerificationDoneText', locale: 'en' }), pairwise(), takeUntil(this.destroy$))
.subscribe((pair) => { .subscribe((pair) => {
@@ -225,6 +152,38 @@ export class LoginTextsComponent implements OnDestroy {
}); });
} }
ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData();
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
this.service.getSupportedLanguages().then((lang) => {
this.LOCALES = lang.languagesList;
});
this.loadData();
break;
}
interval(10000)
.pipe(
// debounceTime(5000),
takeUntil(this.destroy$),
)
.subscribe((x) => {
this.checkForChanges();
});
}
public getDefaultValues(req: any): Promise<any> { public getDefaultValues(req: any): Promise<any> {
return this.service.getDefaultLoginTexts(req).then((res) => { return this.service.getDefaultLoginTexts(req).then((res) => {
if (res.customText) { if (res.customText) {

View File

@@ -16,10 +16,10 @@ import { HasRoleModule } from '../../../directives/has-role/has-role.module';
import { DetailLayoutModule } from '../../../modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from '../../../modules/detail-layout/detail-layout.module';
import { InputModule } from '../../../modules/input/input.module'; import { InputModule } from '../../../modules/input/input.module';
import { HasRolePipeModule } from '../../../pipes/has-role-pipe/has-role-pipe.module'; import { HasRolePipeModule } from '../../../pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../../card/card.module';
import { EditTextModule } from '../../edit-text/edit-text.module'; import { EditTextModule } from '../../edit-text/edit-text.module';
import { FormFieldModule } from '../../form-field/form-field.module'; import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module'; import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { LoginTextsRoutingModule } from './login-texts-routing.module'; import { LoginTextsRoutingModule } from './login-texts-routing.module';
import { LoginTextsComponent } from './login-texts.component'; import { LoginTextsComponent } from './login-texts.component';
@@ -48,9 +48,11 @@ import { LoginTextsComponent } from './login-texts.component';
TextFieldModule, TextFieldModule,
MatDialogModule, MatDialogModule,
WarnDialogModule, WarnDialogModule,
PolicyGridModule, CardModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
LocalizedDatePipeModule, LocalizedDatePipeModule,
], ],
exports: [LoginTextsComponent],
}) })
export class LoginTextsPolicyModule {} export class LoginTextsPolicyModule {}

View File

@@ -1,17 +1,7 @@
<cnsl-detail-layout <h2>{{ 'POLICY.MESSAGE_TEXTS.TITLE' | translate }}</h2>
[hasBackButton]="true" <p class="cnsl-secondary-text">{{ 'POLICY.MESSAGE_TEXTS.DESCRIPTION' | translate }}</p>
[title]="'POLICY.MESSAGE_TEXTS.TITLE' | translate"
[description]="'POLICY.MESSAGE_TEXTS.DESCRIPTION' | translate"
>
<p class="policy-applied-to" sub>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<div class="message-texts-top-actions"> <div class="message-texts-top-actions">
<cnsl-form-field class="type"> <cnsl-form-field class="type">
<cnsl-label>{{ 'POLICY.MESSAGE_TEXTS.TYPE' | translate }}</cnsl-label> <cnsl-label>{{ 'POLICY.MESSAGE_TEXTS.TYPE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="currentType" name="currentSubMap" (selectionChange)="changedCurrentType()"> <mat-select [(ngModel)]="currentType" name="currentSubMap" (selectionChange)="changedCurrentType()">
@@ -36,9 +26,9 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<div class="content"> <div class="content">
<cnsl-edit-text <cnsl-edit-text
[chips]="chips[currentType]" [chips]="chips[currentType]"
[disabled]="(canWrite$ | async) === false" [disabled]="(canWrite$ | async) === false"
@@ -47,9 +37,9 @@
[current$]="getCustomInitMessageTextMap$" [current$]="getCustomInitMessageTextMap$"
(changedValues)="updateCurrentValues($event)" (changedValues)="updateCurrentValues($event)"
></cnsl-edit-text> ></cnsl-edit-text>
</div> </div>
<div class="actions"> <div class="actions">
<button <button
class="reset-button" class="reset-button"
*ngIf="(getCustomInitMessageTextMap$ | async) && (getCustomInitMessageTextMap$ | async)?.isDefault === false" *ngIf="(getCustomInitMessageTextMap$ | async) && (getCustomInitMessageTextMap$ | async)?.isDefault === false"
@@ -71,7 +61,4 @@
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text"></cnsl-policy-grid>
</cnsl-detail-layout>

View File

@@ -1,9 +1,7 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs'; import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { import {
GetCustomPasswordResetMessageTextRequest as AdminGetCustomPasswordResetMessageTextRequest, GetCustomPasswordResetMessageTextRequest as AdminGetCustomPasswordResetMessageTextRequest,
GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest, GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest,
@@ -35,17 +33,14 @@ import {
SetCustomVerifyEmailMessageTextRequest, SetCustomVerifyEmailMessageTextRequest,
SetCustomVerifyPhoneMessageTextRequest, SetCustomVerifyPhoneMessageTextRequest,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { MessageCustomText } from 'src/app/proto/generated/zitadel/text_pb'; import { MessageCustomText } from 'src/app/proto/generated/zitadel/text_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.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 { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service'; 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 { GridPolicy, MESSAGE_TEXTS_POLICY } from '../../policy-grid/policies';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@@ -279,7 +274,7 @@ const REQUESTMAP = {
templateUrl: './message-texts.component.html', templateUrl: './message-texts.component.html',
styleUrls: ['./message-texts.component.scss'], styleUrls: ['./message-texts.component.scss'],
}) })
export class MessageTextsComponent implements OnDestroy { export class MessageTextsComponent implements OnInit, OnDestroy {
public getDefaultInitMessageTextMap$: Observable<{ [key: string]: string }> = of({}); public getDefaultInitMessageTextMap$: Observable<{ [key: string]: string }> = of({});
public getCustomInitMessageTextMap$: BehaviorSubject<{ [key: string]: string | boolean }> = new BehaviorSubject({}); // boolean because of isDefault public getCustomInitMessageTextMap$: BehaviorSubject<{ [key: string]: string | boolean }> = new BehaviorSubject({}); // boolean because of isDefault
@@ -287,7 +282,7 @@ export class MessageTextsComponent implements OnDestroy {
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public MESSAGETYPES: any = MESSAGETYPES; public MESSAGETYPES: any = MESSAGETYPES;
@@ -392,8 +387,6 @@ export class MessageTextsComponent implements OnDestroy {
public locale: string = 'en'; public locale: string = 'en';
public LOCALES: string[] = ['en', 'de', 'it']; public LOCALES: string[] = ['en', 'de', 'it'];
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public currentPolicy: GridPolicy = MESSAGE_TEXTS_POLICY;
public orgName: string = '';
public canWrite$: Observable<boolean> = this.authService.isAllowed([ public canWrite$: Observable<boolean> = this.authService.isAllowed([
this.serviceType === PolicyComponentServiceType.ADMIN this.serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write' ? 'iam.policy.write'
@@ -404,17 +397,13 @@ export class MessageTextsComponent implements OnDestroy {
constructor( constructor(
private authService: GrpcAuthService, private authService: GrpcAuthService,
private route: ActivatedRoute,
private toast: ToastService, private toast: ToastService,
private injector: Injector, private injector: Injector,
private dialog: MatDialog, private dialog: MatDialog,
private storageService: StorageService, private storageService: StorageService,
breadcrumbService: BreadcrumbService, ) {}
) {
this.sub = this.route.data ngOnInit(): void {
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
@@ -422,21 +411,6 @@ export class MessageTextsComponent implements OnDestroy {
this.LOCALES = lang.languagesList; this.LOCALES = lang.languagesList;
}); });
this.loadData(this.currentType); this.loadData(this.currentType);
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
@@ -444,21 +418,8 @@ export class MessageTextsComponent implements OnDestroy {
this.LOCALES = lang.languagesList; this.LOCALES = lang.languagesList;
}); });
this.loadData(this.currentType); this.loadData(this.currentType);
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
} }
return this.route.params;
}),
)
.subscribe();
} }
public getDefaultValues(type: MESSAGETYPES, req: any): Promise<any> { public getDefaultValues(type: MESSAGETYPES, req: any): Promise<any> {

View File

@@ -13,10 +13,10 @@ import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
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 { EditTextModule } from '../../edit-text/edit-text.module'; import { EditTextModule } from '../../edit-text/edit-text.module';
import { FormFieldModule } from '../../form-field/form-field.module'; import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { MessageTextsRoutingModule } from './message-texts-routing.module'; import { MessageTextsRoutingModule } from './message-texts-routing.module';
import { MessageTextsComponent } from './message-texts.component'; import { MessageTextsComponent } from './message-texts.component';
@@ -37,12 +37,13 @@ import { MessageTextsComponent } from './message-texts.component';
HasRolePipeModule, HasRolePipeModule,
MatTooltipModule, MatTooltipModule,
TranslateModule, TranslateModule,
CardModule,
MatTooltipModule, MatTooltipModule,
MatSelectModule, MatSelectModule,
DetailLayoutModule, DetailLayoutModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
PolicyGridModule,
TextFieldModule, TextFieldModule,
], ],
exports: [MessageTextsComponent],
}) })
export class MessageTextsPolicyModule {} export class MessageTextsPolicyModule {}

View File

@@ -1,19 +1,8 @@
<cnsl-detail-layout <h2>{{ 'POLICY.IAM_POLICY.TITLE' | translate }}</h2>
[hasBackButton]="true" <p class="cnsl-secondary-text">{{ 'POLICY.IAM_POLICY.DESCRIPTION' | translate }}</p>
[title]="'POLICY.IAM_POLICY.TITLE' | translate" <cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
[description]="'POLICY.IAM_POLICY.DESCRIPTION' | translate"
>
<p class="policy-applied-to" sub>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section> <ng-template cnslHasRole [hasRole]="['iam.policy.delete']">
<ng-template cnslHasRole [hasRole]="['iam.policy.delete']">
<button <button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault" *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}" matTooltip="{{ 'POLICY.RESET' | translate }}"
@@ -23,9 +12,9 @@
> >
{{ 'POLICY.RESET' | translate }} {{ 'POLICY.RESET' | translate }}
</button> </button>
</ng-template> </ng-template>
<div class="content" *ngIf="iamData"> <div class="content" *ngIf="iamData">
<div class="row"> <div class="row">
<mat-slide-toggle <mat-slide-toggle
color="primary" color="primary"
@@ -37,9 +26,9 @@
{{ 'POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate }} {{ 'POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
</div> </div>
<div class="btn-container"> <div class="btn-container">
<button <button
(click)="savePolicy()" (click)="savePolicy()"
[disabled]="(['iam.policy.write'] | hasRole | async) === false" [disabled]="(['iam.policy.write'] | hasRole | async) === false"
@@ -49,7 +38,4 @@
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="login"></cnsl-policy-grid>
</cnsl-detail-layout>

View File

@@ -36,12 +36,10 @@
.btn-container { .btn-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
width: 100%; width: 100%;
margin-bottom: 50px;
button { button {
margin-top: 3rem;
display: block; display: block;
} }
} }

View File

@@ -1,18 +1,13 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { GetCustomOrgIAMPolicyResponse } from 'src/app/proto/generated/zitadel/admin_pb'; import { GetCustomOrgIAMPolicyResponse } 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 { 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 { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageKey, 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 { GridPolicy, IAM_POLICY } from '../../policy-grid/policies';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
@@ -20,9 +15,9 @@ import { PolicyComponentServiceType } from '../policy-component-types.enum';
templateUrl: './org-iam-policy.component.html', templateUrl: './org-iam-policy.component.html',
styleUrls: ['./org-iam-policy.component.scss'], styleUrls: ['./org-iam-policy.component.scss'],
}) })
export class OrgIamPolicyComponent implements OnDestroy { export class OrgIamPolicyComponent implements OnInit, OnDestroy {
private managementService!: ManagementService; private managementService!: ManagementService;
public serviceType!: PolicyComponentServiceType; @Input() public serviceType!: PolicyComponentServiceType;
public iamData!: OrgIAMPolicy.AsObject; public iamData!: OrgIAMPolicy.AsObject;
@@ -30,50 +25,14 @@ export class OrgIamPolicyComponent implements OnDestroy {
private org!: Org.AsObject; private org!: Org.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public currentPolicy: GridPolicy = IAM_POLICY;
public orgName: string = '';
constructor( constructor(private toast: ToastService, private injector: Injector, private adminService: AdminService) {}
private route: ActivatedRoute,
private toast: ToastService, ngOnInit(): void {
private storage: StorageService,
private injector: Injector,
private adminService: AdminService,
private storageService: StorageService,
breadcrumbService: BreadcrumbService,
) {
const temporg = this.storage.getItem(StorageKey.organization, StorageLocation.session) as Org.AsObject;
if (temporg) {
this.org = temporg;
}
this.sub = this.route.data
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
if (this.serviceType === PolicyComponentServiceType.MGMT) { if (this.serviceType === PolicyComponentServiceType.MGMT) {
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
this.managementService = this.injector.get(ManagementService as Type<ManagementService>); this.managementService = this.injector.get(ManagementService as Type<ManagementService>);
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
} }
return this.route.params;
}),
)
.subscribe((_) => {
this.fetchData(); this.fetchData();
});
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
@@ -103,7 +62,6 @@ export class OrgIamPolicyComponent implements OnDestroy {
} }
public savePolicy(): void { public savePolicy(): void {
console.log(this.iamData);
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
if ((this.iamData as OrgIAMPolicy.AsObject).isDefault) { if ((this.iamData as OrgIAMPolicy.AsObject).isDefault) {

View File

@@ -11,8 +11,8 @@ import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
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 { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { OrgIamPolicyRoutingModule } from './org-iam-policy-routing.module'; import { OrgIamPolicyRoutingModule } from './org-iam-policy-routing.module';
import { OrgIamPolicyComponent } from './org-iam-policy.component'; import { OrgIamPolicyComponent } from './org-iam-policy.component';
@@ -22,6 +22,7 @@ import { OrgIamPolicyComponent } from './org-iam-policy.component';
OrgIamPolicyRoutingModule, OrgIamPolicyRoutingModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
CardModule,
InputModule, InputModule,
MatButtonModule, MatButtonModule,
HasRolePipeModule, HasRolePipeModule,
@@ -32,7 +33,7 @@ import { OrgIamPolicyComponent } from './org-iam-policy.component';
InfoSectionModule, InfoSectionModule,
TranslateModule, TranslateModule,
DetailLayoutModule, DetailLayoutModule,
PolicyGridModule,
], ],
exports: [OrgIamPolicyComponent],
}) })
export class OrgIamPolicyModule {} export class OrgIamPolicyModule {}

View File

@@ -8,7 +8,6 @@ import {
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { PasswordAgePolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { PasswordAgePolicy } 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 { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@@ -28,12 +27,7 @@ export class PasswordAgePolicyComponent implements OnDestroy {
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor( constructor(private route: ActivatedRoute, private toast: ToastService, private injector: Injector) {
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
breadcrumbService: BreadcrumbService,
) {
this.sub = this.route.data this.sub = this.route.data
.pipe( .pipe(
switchMap((data) => { switchMap((data) => {
@@ -41,27 +35,9 @@ export class PasswordAgePolicyComponent implements OnDestroy {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
} }

View File

@@ -1,22 +1,15 @@
<cnsl-detail-layout <!-- <cnsl-card
[hasBackButton]="true" title="{{ 'POLICY.PWD_COMPLEXITY.TITLE' | translate }}"
[title]="'POLICY.PWD_COMPLEXITY.TITLE' | translate" description="{{ 'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate }}"
[description]="'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate" > -->
> <h2>{{ 'POLICY.PWD_COMPLEXITY.TITLE' | translate }}</h2>
<p class="policy-applied-to" sub> <p class="cnsl-secondary-text">{{ 'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate }}</p>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<div class="spinner-wr"> <div *ngIf="loading" class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner> <mat-spinner diameter="30" color="primary"></mat-spinner>
</div> </div>
<ng-template cnslHasRole [hasRole]="['policy.delete']"> <ng-template cnslHasRole [hasRole]="['policy.delete']">
<button <button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault" *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}" matTooltip="{{ 'POLICY.RESET' | translate }}"
@@ -26,13 +19,11 @@
> >
{{ 'POLICY.RESET' | translate }} {{ 'POLICY.RESET' | translate }}
</button> </button>
</ng-template> </ng-template>
<div *ngIf="complexityData" class="complexity-content"> <cnsl-card *ngIf="complexityData">
<div class="complexity-content">
<div class="row"> <div class="row">
<mat-icon class="icon" svgIcon="mdi_counter"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.MINLENGTH' | translate }}</span>
<span class="fill-space"></span>
<div class="length-wrapper"> <div class="length-wrapper">
<button mat-icon-button (click)="decrementLength()" [disabled]="(['policy.write'] | hasRole | async) === false"> <button mat-icon-button (click)="decrementLength()" [disabled]="(['policy.write'] | hasRole | async) === false">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
@@ -42,62 +33,76 @@
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</div> </div>
<div class="number-toggle-row">
<mat-icon class="icon" svgIcon="mdi_counter"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.MINLENGTH' | translate }}</span>
</div>
</div> </div>
<div class="row"> <div class="row">
<mat-icon class="icon" svgIcon="mdi_numeric"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASNUMBER' | translate }}</span>
<span class="fill-space"></span>
<mat-slide-toggle <mat-slide-toggle
class="slide-toggle"
color="primary" color="primary"
name="hasNumber" name="hasNumber"
ngDefaultControl ngDefaultControl
[(ngModel)]="complexityData.hasNumber" [(ngModel)]="complexityData.hasNumber"
[disabled]="(['policy.write'] | hasRole | async) === false" [disabled]="(['policy.write'] | hasRole | async) === false"
> >
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_numeric"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASNUMBER' | translate }}</span>
</div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="row"> <div class="row">
<mat-icon class="icon" svgIcon="mdi_symbol"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASSYMBOL' | translate }}</span>
<span class="fill-space"></span>
<mat-slide-toggle <mat-slide-toggle
class="slide-toggle"
color="primary" color="primary"
name="hasSymbol" name="hasSymbol"
ngDefaultControl ngDefaultControl
[(ngModel)]="complexityData.hasSymbol" [(ngModel)]="complexityData.hasSymbol"
[disabled]="(['policy.write'] | hasRole | async) === false" [disabled]="(['policy.write'] | hasRole | async) === false"
> >
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_symbol"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASSYMBOL' | translate }}</span>
</div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="row"> <div class="row">
<mat-icon class="icon" svgIcon="mdi_format-letter-case-lower"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASLOWERCASE' | translate }}</span>
<span class="fill-space"></span>
<mat-slide-toggle <mat-slide-toggle
class="slide-toggle"
color="primary" color="primary"
name="hasLowercase" name="hasLowercase"
ngDefaultControl ngDefaultControl
[(ngModel)]="complexityData.hasLowercase" [(ngModel)]="complexityData.hasLowercase"
[disabled]="(['policy.write'] | hasRole | async) === false" [disabled]="(['policy.write'] | hasRole | async) === false"
> >
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_format-letter-case-lower"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASLOWERCASE' | translate }}</span>
</div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="row"> <div class="row">
<mat-icon class="icon" svgIcon="mdi_format-letter-case-upper"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASUPPERCASE' | translate }}</span>
<span class="fill-space"></span>
<mat-slide-toggle <mat-slide-toggle
class="slide-toggle"
color="primary" color="primary"
name="hasUppercase" name="hasUppercase"
ngDefaultControl ngDefaultControl
[(ngModel)]="complexityData.hasUppercase" [(ngModel)]="complexityData.hasUppercase"
[disabled]="(['policy.write'] | hasRole | async) === false" [disabled]="(['policy.write'] | hasRole | async) === false"
> >
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_format-letter-case-upper"></mat-icon>
<span class="left-desc">{{ 'POLICY.DATA.HASUPPERCASE' | translate }}</span>
</div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
</div> </div>
</cnsl-card>
<div class="btn-container"> <div class="btn-container">
<button <button
(click)="savePolicy()" (click)="savePolicy()"
[disabled]="(['policy.write'] | hasRole | async) === false" [disabled]="(['policy.write'] | hasRole | async) === false"
@@ -107,7 +112,5 @@
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
<!-- </cnsl-card> -->
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="security"></cnsl-policy-grid>
</cnsl-detail-layout>

View File

@@ -8,7 +8,6 @@
} }
.complexity-content { .complexity-content {
padding-top: 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@@ -18,6 +17,11 @@
align-items: center; align-items: center;
padding: 0.3rem 0; padding: 0.3rem 0;
.number-toggle-row {
display: flex;
align-items: center;
margin-left: 1rem;
.icon { .icon {
margin-right: 1rem; margin-right: 1rem;
} }
@@ -25,6 +29,25 @@
.left-desc { .left-desc {
font-size: 0.9rem; font-size: 0.9rem;
} }
}
.slide-toggle {
margin-left: 1.75rem;
.slide-toggle-row {
display: flex;
align-items: center;
margin-left: 1.5rem;
.icon {
margin-right: 1rem;
}
.left-desc {
font-size: 0.9rem;
}
}
}
.fill-space { .fill-space {
flex: 1; flex: 1;
@@ -40,12 +63,9 @@
.btn-container { .btn-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
width: 100%;
margin-bottom: 50px;
button { button {
margin-top: 3rem;
display: block; display: block;
} }
} }

View File

@@ -1,93 +1,46 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { import {
GetPasswordComplexityPolicyResponse as AdminGetPasswordComplexityPolicyResponse, GetPasswordComplexityPolicyResponse as AdminGetPasswordComplexityPolicyResponse,
} from 'src/app/proto/generated/zitadel/admin_pb'; } from 'src/app/proto/generated/zitadel/admin_pb';
import { import {
GetPasswordComplexityPolicyResponse as MgmtGetPasswordComplexityPolicyResponse, GetPasswordComplexityPolicyResponse as MgmtGetPasswordComplexityPolicyResponse,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { PasswordComplexityPolicy } 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 { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.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 { InfoSectionType } from '../../info-section/info-section.component'; import { InfoSectionType } from '../../info-section/info-section.component';
import { COMPLEXITY_POLICY, GridPolicy } from '../../policy-grid/policies';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
selector: 'cnsl-password-policy', selector: 'cnsl-password-complexity-policy',
templateUrl: './password-complexity-policy.component.html', templateUrl: './password-complexity-policy.component.html',
styleUrls: ['./password-complexity-policy.component.scss'], styleUrls: ['./password-complexity-policy.component.scss'],
}) })
export class PasswordComplexityPolicyComponent implements OnDestroy { export class PasswordComplexityPolicyComponent implements OnInit {
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
public complexityData!: PasswordComplexityPolicy.AsObject; public complexityData!: PasswordComplexityPolicy.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public loading: boolean = false; public loading: boolean = false;
public currentPolicy: GridPolicy = COMPLEXITY_POLICY;
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public orgName: string = ''; constructor(private toast: ToastService, private injector: Injector) {}
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
private storageService: StorageService,
breadcrumbService: BreadcrumbService,
) {
this.sub = this.route.data
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
public ngOnInit(): void {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
break; break;
} }
return this.route.params;
}),
)
.subscribe(() => {
this.fetchData(); this.fetchData();
});
} }
public fetchData(): void { public fetchData(): void {
@@ -101,10 +54,6 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
}); });
} }
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private async getData(): Promise< private async getData(): Promise<
MgmtGetPasswordComplexityPolicyResponse.AsObject | AdminGetPasswordComplexityPolicyResponse.AsObject MgmtGetPasswordComplexityPolicyResponse.AsObject | AdminGetPasswordComplexityPolicyResponse.AsObject
> { > {

View File

@@ -12,8 +12,8 @@ import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
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 { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { PasswordComplexityPolicyRoutingModule } from './password-complexity-policy-routing.module'; import { PasswordComplexityPolicyRoutingModule } from './password-complexity-policy-routing.module';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
@@ -32,9 +32,10 @@ import { PasswordComplexityPolicyComponent } from './password-complexity-policy.
HasRolePipeModule, HasRolePipeModule,
TranslateModule, TranslateModule,
DetailLayoutModule, DetailLayoutModule,
CardModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
PolicyGridModule,
InfoSectionModule, InfoSectionModule,
], ],
exports: [PasswordComplexityPolicyComponent],
}) })
export class PasswordComplexityPolicyModule {} export class PasswordComplexityPolicyModule {}

View File

@@ -1,34 +1,15 @@
<cnsl-detail-layout <!-- <cnsl-card
[hasBackButton]="true" title="{{ 'POLICY.PWD_LOCKOUT.TITLE' | translate }}"
[title]="'POLICY.PWD_LOCKOUT.TITLE' | translate" description="{{ 'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate }}"
[description]="'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate" > -->
> <h2>{{ 'POLICY.PWD_LOCKOUT.TITLE' | translate }}</h2>
<p class="policy-applied-to" sub> <p class="cnsl-secondary-text">{{ 'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate }}</p>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<cnsl-info-section class="default" *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<ng-template cnslHasRole [hasRole]="['policy.delete']"> <cnsl-info-section class="default" *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<button
[disabled]="(['policy.write'] | hasRole | async) === false"
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="resetPolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
<div class="content" *ngIf="lockoutData"> <cnsl-card *ngIf="lockoutData">
<div class="lockout-content">
<div class="row"> <div class="row">
<span class="left-desc">{{ 'POLICY.DATA.MAXATTEMPTS' | translate }}</span>
<span class="fill-space"></span>
<div class="length-wrapper"> <div class="length-wrapper">
<button [disabled]="(['policy.write'] | hasRole | async) === false" mat-icon-button (click)="decrementMaxAttempts()"> <button [disabled]="(['policy.write'] | hasRole | async) === false" mat-icon-button (click)="decrementMaxAttempts()">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
@@ -38,10 +19,16 @@
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</div> </div>
</div>
</div>
<div class="btn-container"> <div class="number-toggle-row">
<span class="left-desc">{{ 'POLICY.DATA.MAXATTEMPTS' | translate }}</span>
<span class="fill-space"></span>
</div>
</div>
</div>
</cnsl-card>
<div class="btn-container">
<button <button
(click)="savePolicy()" (click)="savePolicy()"
[disabled]="(['policy.write'] | hasRole | async) === false" [disabled]="(['policy.write'] | hasRole | async) === false"
@@ -51,7 +38,17 @@
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="security"></cnsl-policy-grid> <ng-template cnslHasRole [hasRole]="['policy.delete']">
</cnsl-detail-layout> <button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="resetPolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
</div>
<!-- </cnsl-card> -->

View File

@@ -8,8 +8,7 @@
font-size: 14px; font-size: 14px;
} }
.content { .lockout-content {
padding-top: 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@@ -19,9 +18,15 @@
align-items: center; align-items: center;
padding: 0.3rem 0; padding: 0.3rem 0;
.number-toggle-row {
margin-left: 1rem;
display: flex;
align-items: center;
.left-desc { .left-desc {
font-size: 0.9rem; font-size: 0.9rem;
} }
}
.fill-space { .fill-space {
flex: 1; flex: 1;
@@ -36,11 +41,11 @@
.btn-container { .btn-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
width: 100%; width: 100%;
button { button {
margin-top: 3rem;
display: block; display: block;
margin-right: 1rem;
} }
} }

View File

@@ -1,22 +1,16 @@
import { Component, Injector, Input, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { GetLockoutPolicyResponse as AdminGetPasswordLockoutPolicyResponse } from 'src/app/proto/generated/zitadel/admin_pb'; import { GetLockoutPolicyResponse as AdminGetPasswordLockoutPolicyResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { import {
GetLockoutPolicyResponse as MgmtGetPasswordLockoutPolicyResponse, GetLockoutPolicyResponse as MgmtGetPasswordLockoutPolicyResponse,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { LockoutPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { LockoutPolicy } 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 { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.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 { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service'; 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 { GridPolicy, LOCKOUT_POLICY } from '../../policy-grid/policies';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
@@ -24,77 +18,33 @@ import { PolicyComponentServiceType } from '../policy-component-types.enum';
templateUrl: './password-lockout-policy.component.html', templateUrl: './password-lockout-policy.component.html',
styleUrls: ['./password-lockout-policy.component.scss'], styleUrls: ['./password-lockout-policy.component.scss'],
}) })
export class PasswordLockoutPolicyComponent implements OnDestroy { export class PasswordLockoutPolicyComponent implements OnInit {
@Input() public service!: ManagementService | AdminService; @Input() public service!: ManagementService | AdminService;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public lockoutForm!: FormGroup; public lockoutForm!: FormGroup;
public lockoutData!: LockoutPolicy.AsObject; public lockoutData!: LockoutPolicy.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public currentPolicy: GridPolicy = LOCKOUT_POLICY;
public orgName: string = '';
constructor( constructor(private toast: ToastService, private injector: Injector, private storageService: StorageService) {}
private route: ActivatedRoute,
breadcrumbService: BreadcrumbService,
private toast: ToastService,
private injector: Injector,
private storageService: StorageService,
) {
this.sub = this.route.data
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
public ngOnInit(): void {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
} }
return this.route.params;
}),
)
.subscribe(() => {
this.fetchData(); this.fetchData();
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
} }
private fetchData(): void { private fetchData(): void {
console.log(this.serviceType);
this.getData().then((resp) => { this.getData().then((resp) => {
console.log(resp);
if (resp.policy) { if (resp.policy) {
this.lockoutData = resp.policy; this.lockoutData = resp.policy;
} }

View File

@@ -11,8 +11,8 @@ import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
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 { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { PasswordLockoutPolicyRoutingModule } from './password-lockout-policy-routing.module'; import { PasswordLockoutPolicyRoutingModule } from './password-lockout-policy-routing.module';
import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component'; import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
@@ -29,10 +29,11 @@ import { PasswordLockoutPolicyComponent } from './password-lockout-policy.compon
MatIconModule, MatIconModule,
HasRoleModule, HasRoleModule,
MatTooltipModule, MatTooltipModule,
CardModule,
TranslateModule, TranslateModule,
DetailLayoutModule, DetailLayoutModule,
PolicyGridModule,
InfoSectionModule, InfoSectionModule,
], ],
exports: [PasswordLockoutPolicyComponent],
}) })
export class PasswordLockoutPolicyModule {} export class PasswordLockoutPolicyModule {}

View File

@@ -1,15 +1,3 @@
export enum PolicyComponentType {
LOCKOUT = 'lockout',
AGE = 'age',
COMPLEXITY = 'complexity',
IAM = 'iam',
LOGIN = 'login',
PRIVATELABEL = 'privatelabel',
MESSAGETEXTS = 'messagetexts',
LOGINTEXTS = 'logintexts',
PRIVACYPOLICY = 'privacypolicy',
}
export enum PolicyComponentServiceType { export enum PolicyComponentServiceType {
MGMT = 'mgmt', MGMT = 'mgmt',
ADMIN = 'admin', ADMIN = 'admin',

View File

@@ -1,22 +1,10 @@
<cnsl-detail-layout <h2>{{ 'POLICY.PRIVACY_POLICY.TITLE' | translate }}</h2>
[hasBackButton]="true" <p class="cnsl-secondary-text">{{ 'POLICY.PRIVACY_POLICY.DESCRIPTION' | translate }}</p>
[title]="'POLICY.PRIVACY_POLICY.TITLE' | translate"
[description]="'POLICY.PRIVACY_POLICY.DESCRIPTION' | translate"
>
<p class="policy-applied-to" sub>
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="orgName; else iam">{{ orgName }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section> <cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<div class="divider"></div> <div>
<form *ngIf="form" [formGroup]="form" class="policy-content">
<div>
<form *ngIf="form" [formGroup]="form" class="content">
<cnsl-form-field class="privacy-policy-formfield"> <cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.TOSLINK' | translate }}</cnsl-label> <cnsl-label>{{ 'POLICY.PRIVACY_POLICY.TOSLINK' | translate }}</cnsl-label>
<input cnslInput name="tosLink" formControlName="tosLink" /> <input cnslInput name="tosLink" formControlName="tosLink" />
@@ -35,9 +23,9 @@
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ key: 'helpLink' }"></template> <template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ key: 'helpLink' }"></template>
</cnsl-form-field> </cnsl-form-field>
</form> </form>
</div> </div>
<div class="actions"> <div class="policy-actions">
<button <button
*ngIf="privacyPolicy && privacyPolicy.isDefault === false" *ngIf="privacyPolicy && privacyPolicy.isDefault === false"
class="reset-button" class="reset-button"
@@ -59,10 +47,7 @@
> >
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</div> </div>
<cnsl-policy-grid [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text"></cnsl-policy-grid>
</cnsl-detail-layout>
<ng-template #templateRef let-key="key"> <ng-template #templateRef let-key="key">
<div class="chips"> <div class="chips">

View File

@@ -2,29 +2,7 @@
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.policy-applied-to { .policy-content {
margin: -1rem 0 0 0;
font-size: 14px;
}
.top-actions {
display: flex;
margin: 0 -0.5rem;
flex-wrap: wrap;
.keys {
flex: 1;
margin: 0 0.5rem;
min-width: 150px;
}
}
.centerline {
display: flex;
align-items: center;
}
.content {
padding-top: 1rem; padding-top: 1rem;
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -40,7 +18,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
opacity: 0; opacity: 0;
margin: 0 -0.25rem; margin: 0;
transition: all 0.2s ease; transition: all 0.2s ease;
.chip { .chip {
@@ -49,7 +27,7 @@
font-size: 12px; font-size: 12px;
background: #5282c1; background: #5282c1;
color: white; color: white;
margin: 0.25rem; margin-right: 0.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -79,22 +57,14 @@
} }
} }
.divider { .policy-actions {
width: 100%;
height: 1px;
background-color: rgba(#81868a, 0.5);
margin: 1.5rem 0 0 0;
}
.actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
margin: 0 -0.25rem;
.save-button, .save-button,
.reset-button { .reset-button {
display: block; display: block;
margin: 0 0.25rem 3rem 0.25rem; margin: 0 1rem 0 0;
} }
.reset-button { .reset-button {

View File

@@ -1,9 +1,8 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { import {
GetPrivacyPolicyResponse as AdminGetPrivacyPolicyResponse, GetPrivacyPolicyResponse as AdminGetPrivacyPolicyResponse,
UpdatePrivacyPolicyRequest, UpdatePrivacyPolicyRequest,
@@ -13,18 +12,14 @@ import {
GetPrivacyPolicyResponse, GetPrivacyPolicyResponse,
UpdateCustomPrivacyPolicyRequest, UpdateCustomPrivacyPolicyRequest,
} from 'src/app/proto/generated/zitadel/management_pb'; } from 'src/app/proto/generated/zitadel/management_pb';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { PrivacyPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { PrivacyPolicy } 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 { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.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 { InfoSectionType } from '../../info-section/info-section.component'; import { InfoSectionType } from '../../info-section/info-section.component';
import { CnslLinks } from '../../links/links.component'; import { CnslLinks } from '../../links/links.component';
import { GridPolicy, PRIVACY_POLICY } from '../../policy-grid/policies';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@@ -33,19 +28,17 @@ import { PolicyComponentServiceType } from '../policy-component-types.enum';
templateUrl: './privacy-policy.component.html', templateUrl: './privacy-policy.component.html',
styleUrls: ['./privacy-policy.component.scss'], styleUrls: ['./privacy-policy.component.scss'],
}) })
export class PrivacyPolicyComponent implements OnDestroy { export class PrivacyPolicyComponent implements OnInit, OnDestroy {
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public nextLinks: CnslLinks[] = []; public nextLinks: CnslLinks[] = [];
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public privacyPolicy: PrivacyPolicy.AsObject | undefined = undefined; public privacyPolicy: PrivacyPolicy.AsObject | undefined = undefined;
public form!: FormGroup; public form!: FormGroup;
public currentPolicy: GridPolicy = PRIVACY_POLICY;
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
public orgName: string = '';
public canWrite$: Observable<boolean> = this.authService.isAllowed([ public canWrite$: Observable<boolean> = this.authService.isAllowed([
this.serviceType === PolicyComponentServiceType.ADMIN this.serviceType === PolicyComponentServiceType.ADMIN
@@ -60,13 +53,10 @@ export class PrivacyPolicyComponent implements OnDestroy {
constructor( constructor(
private authService: GrpcAuthService, private authService: GrpcAuthService,
private route: ActivatedRoute,
private injector: Injector, private injector: Injector,
private dialog: MatDialog, private dialog: MatDialog,
private toast: ToastService, private toast: ToastService,
private fb: FormBuilder, private fb: FormBuilder,
private storageService: StorageService,
breadcrumbService: BreadcrumbService,
) { ) {
this.form = this.fb.group({ this.form = this.fb.group({
tosLink: ['', []], tosLink: ['', []],
@@ -81,48 +71,19 @@ export class PrivacyPolicyComponent implements OnDestroy {
this.form.disable(); this.form.disable();
} }
}); });
}
this.route.data ngOnInit(): void {
.pipe(
switchMap((data) => {
this.serviceType = data.serviceType;
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.loadData(); this.loadData();
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) {
this.orgName = org.name;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
this.loadData(); this.loadData();
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break; break;
} }
return this.route.params;
}),
)
.subscribe();
} }
public addChip(formControlName: string, value: string): void { public addChip(formControlName: string, value: string): void {

View File

@@ -15,9 +15,9 @@ import { HasRoleModule } from '../../../directives/has-role/has-role.module';
import { DetailLayoutModule } from '../../../modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from '../../../modules/detail-layout/detail-layout.module';
import { InputModule } from '../../../modules/input/input.module'; import { InputModule } from '../../../modules/input/input.module';
import { HasRolePipeModule } from '../../../pipes/has-role-pipe/has-role-pipe.module'; import { HasRolePipeModule } from '../../../pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module'; import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module'; import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { PrivacyPolicyRoutingModule } from './privacy-policy-routing.module'; import { PrivacyPolicyRoutingModule } from './privacy-policy-routing.module';
import { PrivacyPolicyComponent } from './privacy-policy.component'; import { PrivacyPolicyComponent } from './privacy-policy.component';
@@ -39,14 +39,15 @@ import { PrivacyPolicyComponent } from './privacy-policy.component';
HasRolePipeModule, HasRolePipeModule,
MatTooltipModule, MatTooltipModule,
TranslateModule, TranslateModule,
CardModule,
MatTooltipModule, MatTooltipModule,
DetailLayoutModule, DetailLayoutModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
TextFieldModule, TextFieldModule,
MatDialogModule, MatDialogModule,
WarnDialogModule, WarnDialogModule,
PolicyGridModule,
InfoSectionModule, InfoSectionModule,
], ],
exports: [PrivacyPolicyComponent],
}) })
export class PrivacyPolicyModule {} export class PrivacyPolicyModule {}

View File

@@ -1,13 +1,5 @@
<cnsl-detail-layout [hasBackButton]="true" [title]="'POLICY.PRIVATELABELING.TITLE' | translate"> <h2>{{ 'POLICY.PRIVATELABELING.TITLE' | translate }}</h2>
<p class="policy-applied-to" sub> <div class="privatelabeling-policy">
{{ 'POLICY.APPLIEDTO' | translate }}:
<strong *ngIf="org; else iam">{{ org.name }}</strong>
<ng-template #iam
><strong>{{ 'MENU.INSTANCE' | translate }}</strong>
</ng-template>
</p>
<div class="privatelabeling-policy">
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section> <cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<div class="spinner-wr"> <div class="spinner-wr">
@@ -434,8 +426,4 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<cnsl-policy-grid class="grid" [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="text">
</cnsl-policy-grid>
</div>
</cnsl-detail-layout>

View File

@@ -1,10 +1,5 @@
@use '@angular/material' as mat; @use '@angular/material' as mat;
.policy-applied-to {
margin: -1rem 0 0 0;
font-size: 14px;
}
@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);
@@ -97,7 +92,6 @@
padding-top: 2rem; padding-top: 2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 -1rem;
@media only screen and (min-width: 950px) { @media only screen and (min-width: 950px) {
flex-direction: row; flex-direction: row;
@@ -109,12 +103,14 @@
min-width: 400px; min-width: 400px;
} }
} }
.settings {
margin-right: 1rem;
}
} }
.settings { .settings {
flex: 1; flex: 1;
min-width: 350px;
margin: 0 1rem;
.expansion { .expansion {
box-shadow: none; box-shadow: none;
@@ -298,13 +294,16 @@
} }
.preview-wrapper { .preview-wrapper {
margin: 0 1rem;
flex: 2; flex: 2;
position: relative; position: relative;
background-color: #00000010; background-color: #00000010;
border: 1px solid $border-color; border: 1px solid $border-color;
box-sizing: border-box; box-sizing: border-box;
@media only screen and (min-width: 950px) {
margin-left: 1rem;
}
.col { .col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,8 +1,7 @@
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Component, EventEmitter, Injector, OnDestroy, OnInit, Type } from '@angular/core'; import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { import {
GetLabelPolicyResponse as AdminGetLabelPolicyResponse, GetLabelPolicyResponse as AdminGetLabelPolicyResponse,
GetPreviewLabelPolicyResponse as AdminGetPreviewLabelPolicyResponse, GetPreviewLabelPolicyResponse as AdminGetPreviewLabelPolicyResponse,
@@ -18,15 +17,12 @@ import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { LabelPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { LabelPolicy } 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 { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service'; import { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service'; import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
import { ThemeService } from 'src/app/services/theme.service'; import { ThemeService } from 'src/app/services/theme.service';
import { ToastService } from 'src/app/services/toast.service'; 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 { GridPolicy, PRIVATELABEL_POLICY } from '../../policy-grid/policies';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
export enum Theme { export enum Theme {
@@ -59,7 +55,7 @@ const MAX_ALLOWED_SIZE = 0.5 * 1024 * 1024;
export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy { export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
public theme: Theme = Theme.LIGHT; public theme: Theme = Theme.LIGHT;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; @Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
public previewData!: LabelPolicy.AsObject; public previewData!: LabelPolicy.AsObject;
@@ -84,66 +80,17 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
public refreshPreview: EventEmitter<void> = new EventEmitter(); public refreshPreview: EventEmitter<void> = new EventEmitter();
public org!: Org.AsObject; public org!: Org.AsObject;
public currentPolicy: GridPolicy = PRIVATELABEL_POLICY;
public InfoSectionType: any = InfoSectionType; public InfoSectionType: any = InfoSectionType;
private destroy$: Subject<void> = new Subject(); private destroy$: Subject<void> = new Subject();
public view: View = View.PREVIEW; public view: View = View.PREVIEW;
constructor( constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
private toast: ToastService, private toast: ToastService,
private injector: Injector, private injector: Injector,
private assetService: AssetService, private assetService: AssetService,
private storageService: StorageService, private storageService: StorageService,
private themeService: ThemeService, private themeService: ThemeService,
breadcrumbService: BreadcrumbService, ) {}
) {
this.route.data
.pipe(
takeUntil(this.destroy$),
switchMap((data) => {
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
if (org) {
this.org = org;
}
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
const iamBread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'System',
routerLink: ['/system'],
});
breadcrumbService.setBreadcrumb([iamBread]);
break;
}
return this.route.params;
}),
)
.subscribe(() => {
this.fetchData();
});
}
public toggleHoverLogo(theme: Theme, isHovering: boolean): void { public toggleHoverLogo(theme: Theme, isHovering: boolean): void {
if (theme === Theme.DARK) { if (theme === Theme.DARK) {
@@ -194,6 +141,24 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy {
this.theme = Theme.LIGHT; this.theme = Theme.LIGHT;
} }
}); });
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
if (org) {
this.org = org;
}
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
this.fetchData();
} }
public onDropFont(filelist: FileList | null): Promise<any> | void { public onDropFont(filelist: FileList | null): Promise<any> | void {

View File

@@ -14,10 +14,10 @@ import { ColorChromeModule } from 'ngx-color/chrome';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DropzoneModule } from '../../../directives/dropzone/dropzone.module'; import { DropzoneModule } from '../../../directives/dropzone/dropzone.module';
import { CardModule } from '../../card/card.module';
import { DetailLayoutModule } from '../../detail-layout/detail-layout.module'; import { DetailLayoutModule } from '../../detail-layout/detail-layout.module';
import { InfoSectionModule } from '../../info-section/info-section.module'; import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module'; import { InputModule } from '../../input/input.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { ColorComponent } from './color/color.component'; import { ColorComponent } from './color/color.component';
import { PreviewComponent } from './preview/preview.component'; import { PreviewComponent } from './preview/preview.component';
import { PrivateLabelingPolicyRoutingModule } from './private-labeling-policy-routing.module'; import { PrivateLabelingPolicyRoutingModule } from './private-labeling-policy-routing.module';
@@ -34,6 +34,7 @@ import { PrivateLabelingPolicyComponent } from './private-labeling-policy.compon
MatButtonModule, MatButtonModule,
MatButtonToggleModule, MatButtonToggleModule,
OverlayModule, OverlayModule,
CardModule,
MatIconModule, MatIconModule,
HasRoleModule, HasRoleModule,
MatSlideToggleModule, MatSlideToggleModule,
@@ -42,9 +43,9 @@ import { PrivateLabelingPolicyComponent } from './private-labeling-policy.compon
DetailLayoutModule, DetailLayoutModule,
DropzoneModule, DropzoneModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
PolicyGridModule,
MatExpansionModule, MatExpansionModule,
InfoSectionModule, InfoSectionModule,
], ],
exports: [PrivateLabelingPolicyComponent],
}) })
export class PrivateLabelingPolicyModule {} export class PrivateLabelingPolicyModule {}

View File

@@ -1,121 +0,0 @@
import { PolicyComponentType } from '../policies/policy-component-types.enum';
export interface GridPolicy {
i18nTitle: string;
i18nDesc: string;
iamRouterLink: any;
orgRouterLink: any;
iamWithRole: string[];
orgWithRole: string[];
tags: string[];
icon?: string;
svgIcon?: string;
color: string;
}
export const COMPLEXITY_POLICY: GridPolicy = {
i18nTitle: 'POLICY.PWD_COMPLEXITY.TITLE',
i18nDesc: 'POLICY.PWD_COMPLEXITY.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.COMPLEXITY],
orgRouterLink: ['/org', 'policy', PolicyComponentType.COMPLEXITY],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['login', 'security'],
svgIcon: 'mdi_textbox_password',
color: 'yellow',
};
export const LOCKOUT_POLICY: GridPolicy = {
i18nTitle: 'POLICY.PWD_LOCKOUT.TITLE',
i18nDesc: 'POLICY.PWD_LOCKOUT.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.LOCKOUT],
orgRouterLink: ['/org', 'policy', PolicyComponentType.LOCKOUT],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['login', 'security'],
icon: 'las la-lock',
color: 'yellow',
};
export const IAM_POLICY = {
i18nTitle: 'POLICY.IAM_POLICY.TITLE',
i18nDesc: 'POLICY.IAM_POLICY.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.IAM],
orgRouterLink: ['/org', 'policy', PolicyComponentType.IAM],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['iam.policy.read'],
tags: ['login'],
icon: 'las la-sign-in-alt',
color: 'purple',
};
export const LOGIN_POLICY = {
i18nTitle: 'POLICY.LOGIN_POLICY.TITLE',
i18nDesc: 'POLICY.LOGIN_POLICY.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.LOGIN],
orgRouterLink: ['/org', 'policy', PolicyComponentType.LOGIN],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['login', 'security'],
icon: 'las la-sign-in-alt',
color: 'green',
};
export const PRIVATELABEL_POLICY = {
i18nTitle: 'POLICY.PRIVATELABELING.TITLE',
i18nDesc: 'POLICY.PRIVATELABELING.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.PRIVATELABEL],
orgRouterLink: ['/org', 'policy', PolicyComponentType.PRIVATELABEL],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['login', 'appearance'],
icon: 'las la-swatchbook',
color: 'blue',
};
export const PRIVACY_POLICY = {
i18nTitle: 'POLICY.PRIVACY_POLICY.TITLE',
i18nDesc: 'POLICY.PRIVACY_POLICY.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.PRIVACYPOLICY],
orgRouterLink: ['/org', 'policy', PolicyComponentType.PRIVACYPOLICY],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['documents', 'text'],
icon: 'las la-file-contract',
color: 'black',
};
export const MESSAGE_TEXTS_POLICY = {
i18nTitle: 'POLICY.MESSAGE_TEXTS.TITLE',
i18nDesc: 'POLICY.MESSAGE_TEXTS.DESCRIPTION',
iamRouterLink: ['/system', 'policy', PolicyComponentType.MESSAGETEXTS],
orgRouterLink: ['/org', 'policy', PolicyComponentType.MESSAGETEXTS],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['appearance', 'text'],
icon: 'las la-paragraph',
color: 'red',
};
export const LOGIN_TEXTS_POLICY = {
i18nTitle: 'POLICY.LOGIN_TEXTS.TITLE',
i18nDesc: 'POLICY.LOGIN_TEXTS.DESCRIPTION_SHORT',
iamRouterLink: ['/system', 'policy', PolicyComponentType.LOGINTEXTS],
orgRouterLink: ['/org', 'policy', PolicyComponentType.LOGINTEXTS],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['appearance', 'text'],
icon: 'las la-paragraph',
color: 'red',
};
export const POLICIES: GridPolicy[] = [
COMPLEXITY_POLICY,
LOCKOUT_POLICY,
IAM_POLICY,
LOGIN_POLICY,
PRIVATELABEL_POLICY,
PRIVACY_POLICY,
MESSAGE_TEXTS_POLICY,
LOGIN_TEXTS_POLICY,
];

View File

@@ -1,41 +0,0 @@
<h2>{{'POLICY.TITLE' | translate}}</h2>
<p class="top-desc cnsl-secondary-text">{{'POLICY.DESCRIPTION' | translate}}</p>
<div class="tags" *ngIf="tags">
<span class="tag" [ngClass]="{'active': tag === tagForFilter}"
(click)="tagForFilter !== tag ? tagForFilter = tag : tagForFilter = ''" *ngFor="let tag of tags"><i
class="las la-hashtag"></i>{{tag}}</span>
</div>
<div class="row-lyt">
<ng-container *ngFor="let policy of filteredPolicies">
<ng-template cnslHasRole
[hasRole]="type === PolicyComponentServiceType.ADMIN ? policy.iamWithRole : type === PolicyComponentServiceType.MGMT ? policy.orgWithRole : []">
<div class="p-item card" @policy [attr.data-e2e]="'policy-card'">
<div class="avatar {{policy.color}}">
<mat-icon *ngIf="policy.svgIcon" class="mat-icon" [svgIcon]="policy.svgIcon"></mat-icon>
<i *ngIf="policy.icon" class="icon {{policy.icon}}"></i>
</div>
<div class="title">
<span>{{policy.i18nTitle | translate}}</span>
</div>
<p class="desc cnsl-secondary-text">
{{policy.i18nDesc | translate}}</p>
<span class="fill-space"></span>
<div class="tags" *ngIf="policy.tags">
<span class="tag cnsl-secondary-text" *ngFor="let tag of policy.tags"
(click)="tagForFilter !== tag ? tagForFilter = tag : tagForFilter = ''"><i
class="las la-hashtag"></i>{{tag}}</span>
</div>
<div class="btn-wrapper">
<button
[routerLink]="type === PolicyComponentServiceType.ADMIN ? policy.iamRouterLink : type === PolicyComponentServiceType.MGMT ? policy.orgRouterLink : null"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</div>
</div>
</ng-template>
</ng-container>
</div>

View File

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

View File

@@ -1,56 +0,0 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Component, Input } from '@angular/core';
import { PolicyComponentServiceType, PolicyComponentType } from 'src/app/modules/policies/policy-component-types.enum';
import { GridPolicy, POLICIES } from './policies';
@Component({
selector: 'cnsl-policy-grid',
templateUrl: './policy-grid.component.html',
styleUrls: ['./policy-grid.component.scss'],
animations: [
trigger('policy', [
transition(':enter', [
style({
opacity: 0.5,
}),
animate(
'.15s ease-in-out',
style({
opacity: 1,
}),
),
]),
transition(':leave', [
style({
opacity: 1,
}),
animate(
'.15s ease-in-out',
style({
opacity: 0.5,
}),
),
]),
]),
],
})
export class PolicyGridComponent {
@Input() public type!: PolicyComponentServiceType;
@Input() public tag: string = '';
public PolicyComponentType: any = PolicyComponentType;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public POLICIES: GridPolicy[] = POLICIES;
public tags: Set<string> = new Set(POLICIES.map((p) => p.tags).flat());
@Input() public tagForFilter: string = '';
@Input() public currentPolicy!: GridPolicy;
public get filteredPolicies(): GridPolicy[] {
if (this.tagForFilter) {
return POLICIES.filter((p) => p !== this.currentPolicy && p.tags.includes(this.tagForFilter));
} else {
return POLICIES.filter((p) => p !== this.currentPolicy);
}
}
}

View File

@@ -71,11 +71,6 @@ export class ProjectMembersComponent {
this.mgmtService.getIAM().then((iam) => { this.mgmtService.getIAM().then((iam) => {
const isZitadel = iam.iamProjectId === (this.project as Project.AsObject).id; const isZitadel = iam.iamProjectId === (this.project as Project.AsObject).id;
const breadcrumbs = [ const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({ new Breadcrumb({
type: BreadcrumbType.ORG, type: BreadcrumbType.ORG,
routerLink: ['/org'], routerLink: ['/org'],

View File

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

View File

@@ -5,18 +5,20 @@ import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@Component({ @Component({
selector: 'cnsl-project-role-detail', selector: 'cnsl-project-role-detail-dialog',
templateUrl: './project-role-detail.component.html', templateUrl: './project-role-detail-dialog.component.html',
styleUrls: ['./project-role-detail.component.scss'], styleUrls: ['./project-role-detail-dialog.component.scss'],
}) })
export class ProjectRoleDetailComponent { export class ProjectRoleDetailDialogComponent {
public projectId: string = ''; public projectId: string = '';
public formGroup!: FormGroup; public formGroup!: FormGroup;
constructor(private mgmtService: ManagementService, private toast: ToastService, constructor(
public dialogRef: MatDialogRef<ProjectRoleDetailComponent>, private mgmtService: ManagementService,
@Inject(MAT_DIALOG_DATA) public data: any) { private toast: ToastService,
public dialogRef: MatDialogRef<ProjectRoleDetailDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
this.projectId = data.projectId; this.projectId = data.projectId;
this.formGroup = new FormGroup({ this.formGroup = new FormGroup({
key: new FormControl({ value: '', disabled: true }, [Validators.required]), key: new FormControl({ value: '', disabled: true }, [Validators.required]),
@@ -29,11 +31,13 @@ export class ProjectRoleDetailComponent {
submitForm(): void { submitForm(): void {
if (this.formGroup.valid && this.key?.value && this.group?.value && this.displayName?.value) { if (this.formGroup.valid && this.key?.value && this.group?.value && this.displayName?.value) {
this.mgmtService.updateProjectRole(this.projectId, this.key.value, this.displayName.value, this.group.value) this.mgmtService
.updateProjectRole(this.projectId, this.key.value, this.displayName.value, this.group.value)
.then(() => { .then(() => {
this.toast.showInfo('PROJECT.TOAST.ROLECHANGED', true); this.toast.showInfo('PROJECT.TOAST.ROLECHANGED', true);
this.dialogRef.close(true); this.dialogRef.close(true);
}).catch(error => { })
.catch((error) => {
this.toast.showError(error); this.toast.showError(error);
}); });
} }

View File

@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatRadioModule } from '@angular/material/radio';
import { TranslateModule } from '@ngx-translate/core';
import { InfoSectionModule } from '../info-section/info-section.module';
import { InputModule } from '../input/input.module';
import { ProjectRoleDetailDialogComponent } from './project-role-detail-dialog.component';
@NgModule({
declarations: [ProjectRoleDetailDialogComponent],
imports: [
CommonModule,
TranslateModule,
InputModule,
MatButtonModule,
FormsModule,
ReactiveFormsModule,
MatRadioModule,
InfoSectionModule,
],
exports: [ProjectRoleDetailDialogComponent],
})
export class ProjectRoleDetailDialogModule {}

View File

@@ -7,10 +7,8 @@ import { Role } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import {
ProjectRoleDetailComponent,
} from '../../pages/projects/owned-projects/project-roles/project-role-detail/project-role-detail.component';
import { PaginatorComponent } from '../paginator/paginator.component'; import { PaginatorComponent } from '../paginator/paginator.component';
import { ProjectRoleDetailDialogComponent } from '../project-role-detail-dialog/project-role-detail-dialog.component';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
import { ProjectRolesDataSource } from './project-roles-table-datasource'; import { ProjectRolesDataSource } from './project-roles-table-datasource';
@@ -126,7 +124,7 @@ export class ProjectRolesTableComponent implements OnInit {
} }
public openDetailDialog(role: Role.AsObject): void { public openDetailDialog(role: Role.AsObject): void {
this.dialog.open(ProjectRoleDetailComponent, { this.dialog.open(ProjectRoleDetailDialogComponent, {
data: { data: {
role, role,
projectId: this.projectId, projectId: this.projectId,

View File

@@ -21,6 +21,7 @@ import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/
import { ActionKeysModule } from '../action-keys/action-keys.module'; import { ActionKeysModule } from '../action-keys/action-keys.module';
import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.module'; import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.module';
import { ProjectRoleDetailDialogModule } from '../project-role-detail-dialog/project-role-detail-dialog.module';
import { TableActionsModule } from '../table-actions/table-actions.module'; import { TableActionsModule } from '../table-actions/table-actions.module';
import { ProjectRolesTableComponent } from './project-roles-table.component'; import { ProjectRolesTableComponent } from './project-roles-table.component';
@@ -46,6 +47,7 @@ import { ProjectRolesTableComponent } from './project-roles-table.component';
HasRolePipeModule, HasRolePipeModule,
TranslateModule, TranslateModule,
TableActionsModule, TableActionsModule,
ProjectRoleDetailDialogModule,
MatMenuModule, MatMenuModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
RefreshTableModule, RefreshTableModule,

View File

@@ -3,26 +3,36 @@
<div class="found-user-row" *ngFor="let user of users; index as i"> <div class="found-user-row" *ngFor="let user of users; index as i">
<div class="circle"> <div class="circle">
<cnsl-avatar <cnsl-avatar
*ngIf="user.human && user.human.profile && user.human.profile.displayName && user.human.profile.firstName && user.human.profile.lastName; else cog" *ngIf="
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human.profile?.avatarUrl || ''" user.human &&
[forColor]="user.preferredLoginName" [size]="32"> user.human.profile &&
user.human.profile.displayName &&
user.human.profile.firstName &&
user.human.profile.lastName;
else cog
"
class="avatar"
[name]="user.human.profile.displayName"
[avatarUrl]="user.human.profile?.avatarUrl || ''"
[forColor]="user.preferredLoginName"
[size]="32"
>
</cnsl-avatar> </cnsl-avatar>
<ng-template #cog> <ng-template #cog>
<cnsl-avatar class="avatar" [forColor]="user.preferredLoginName" [isMachine]="true" [size]="32"> <cnsl-avatar class="avatar" [forColor]="user.preferredLoginName" [isMachine]="true" [size]="32"> </cnsl-avatar>
</cnsl-avatar>
</ng-template> </ng-template>
</div> </div>
<div class="user-name-column" *ngIf="user.human"> <div class="user-name-column" *ngIf="user.human">
<span>{{user.human?.profile?.displayName}}</span> <span>{{ user.human?.profile?.displayName }}</span>
<span class="smaller cnsl-secondary-text">{{user.preferredLoginName}}</span> <span class="smaller cnsl-secondary-text">{{ user.preferredLoginName }}</span>
</div> </div>
<div class="user-name-column" *ngIf="user.machine"> <div class="user-name-column" *ngIf="user.machine">
<span>{{user.machine.name}}</span> <span>{{ user.machine.name }}</span>
<span class="smaller cnsl-secondary-text">{{user.preferredLoginName}}</span> <span class="smaller cnsl-secondary-text">{{ user.preferredLoginName }}</span>
</div> </div>
<span class="fill-space"></span> <span class="fill-space"></span>
<button class="search-user-dl-btn" matTooltip="{{'ACTIONS.REMOVE' | translate}}" mat-icon-button color="warn"> <button class="search-user-dl-btn" matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" mat-icon-button color="warn">
<i class="las la-minus-circle" (click)="users.splice(i, 1)"></i> <i class="las la-minus-circle" (click)="users.splice(i, 1)"></i>
</button> </button>
</div> </div>
@@ -37,13 +47,26 @@
<ng-container *ngIf="target && target === UserTarget.SELF"> <ng-container *ngIf="target && target === UserTarget.SELF">
<div class="line"> <div class="line">
<cnsl-form-field class="user-create-form-field more-space"> <cnsl-form-field class="user-create-form-field more-space">
<cnsl-label>{{'USER.SEARCH.ADDITIONAL' | translate}}</cnsl-label> <cnsl-label>{{ 'USER.SEARCH.ADDITIONAL' | translate }}</cnsl-label>
<input cnslInput *ngIf="singleOutput" type="text" placeholder="Search for the user loginname" #usernameInput <input
[formControl]="myControl" [matAutocomplete]="auto" /> cnslInput
*ngIf="singleOutput"
type="text"
placeholder="Search for the user loginname"
#usernameInput
[formControl]="myControl"
[matAutocomplete]="auto"
/>
<input *ngIf="!singleOutput" cnslInput #usernameInput [formControl]="myControl" placeholder="John Doe" <input
[matAutocomplete]="auto" /> *ngIf="!singleOutput"
cnslInput
#usernameInput
[formControl]="myControl"
placeholder="johndoe@domain.com"
[matAutocomplete]="auto"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn"> <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading"> <mat-option *ngIf="isLoading" class="is-loading">
@@ -53,9 +76,20 @@
<div class="user-option"> <div class="user-option">
<div class="circle"> <div class="circle">
<cnsl-avatar <cnsl-avatar
*ngIf="user.human && user.human.profile && user.human.profile.displayName && user.human.profile.firstName && user.human.profile.lastName; else cog" *ngIf="
class="avatar" [name]="user.human.profile.displayName" user.human &&
[avatarUrl]="user.human.profile?.avatarUrl || ''" [forColor]="user.preferredLoginName" [size]="32"> user.human.profile &&
user.human.profile.displayName &&
user.human.profile.firstName &&
user.human.profile.lastName;
else cog
"
class="avatar"
[name]="user.human.profile.displayName"
[avatarUrl]="user.human.profile?.avatarUrl || ''"
[forColor]="user.preferredLoginName"
[size]="32"
>
</cnsl-avatar> </cnsl-avatar>
<ng-template #cog> <ng-template #cog>
<cnsl-avatar class="avatar" [forColor]="user.preferredLoginName" [isMachine]="true" [size]="32"> <cnsl-avatar class="avatar" [forColor]="user.preferredLoginName" [isMachine]="true" [size]="32">
@@ -63,19 +97,21 @@
</ng-template> </ng-template>
</div> </div>
<div class="user-option-column"> <div class="user-option-column">
<span>{{(user.human && user.human.profile && user.human.profile.displayName) ? <span>{{
user.human.profile.displayName : user.human && user.human.profile && user.human.profile.displayName
user.machine?.name}}</span> ? user.human.profile.displayName
: user.machine?.name
}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<span class="smaller cnsl-secondary-text">{{user.preferredLoginName}}</span> <span class="smaller cnsl-secondary-text">{{ user.preferredLoginName }}</span>
</div> </div>
</div> </div>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<span class="target-desc"> <span class="target-desc">
{{'USER.TARGET.SELF'| translate}} {{ 'USER.TARGET.SELF' | translate }}
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a> <a (click)="changeTarget()">{{ 'USER.TARGET.CLICKHERE' | translate }}</a>
</span> </span>
</cnsl-form-field> </cnsl-form-field>
</div> </div>
@@ -84,11 +120,11 @@
<ng-container *ngIf="target && target === UserTarget.EXTERNAL"> <ng-container *ngIf="target && target === UserTarget.EXTERNAL">
<div class="line"> <div class="line">
<cnsl-form-field class="user-create-form-field more-space"> <cnsl-form-field class="user-create-form-field more-space">
<cnsl-label>{{'USER.SEARCH.ADDITIONAL-EXTERNAL' | translate}}</cnsl-label> <cnsl-label>{{ 'USER.SEARCH.ADDITIONAL-EXTERNAL' | translate }}</cnsl-label>
<input cnslInput type="text" [formControl]="globalLoginNameControl" placeholder="example@externaldomain.com" /> <input cnslInput type="text" [formControl]="globalLoginNameControl" placeholder="example@externaldomain.com" />
<span class="target-desc"> <span class="target-desc">
{{(target === UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate}} {{ (target === UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate }}
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a> <a (click)="changeTarget()">{{ 'USER.TARGET.CLICKHERE' | translate }}</a>
</span> </span>
</cnsl-form-field> </cnsl-form-field>

View File

@@ -17,7 +17,7 @@ import { from, of, Subject } from 'rxjs';
import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators';
import { ListUsersResponse } from 'src/app/proto/generated/zitadel/management_pb'; import { ListUsersResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { DisplayNameQuery, SearchQuery, User } from 'src/app/proto/generated/zitadel/user_pb'; import { SearchQuery, User, UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@@ -78,11 +78,11 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
switchMap((value) => { switchMap((value) => {
const query = new SearchQuery(); const query = new SearchQuery();
const dnQuery = new DisplayNameQuery(); const unQuery = new UserNameQuery();
dnQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE); unQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
dnQuery.setDisplayName(value); unQuery.setUserName(value);
query.setDisplayNameQuery(dnQuery); query.setUserNameQuery(unQuery);
if (this.target === UserTarget.SELF) { if (this.target === UserTarget.SELF) {
return from(this.userService.listUsers(10, 0, [query])); return from(this.userService.listUsers(10, 0, [query]));

View File

@@ -0,0 +1,62 @@
export interface SettingLinks {
i18nTitle: string;
i18nDesc: string;
iamRouterLink: any;
orgRouterLink: any;
queryParams: any;
iamWithRole: string[];
orgWithRole: string[];
icon?: string;
svgIcon?: string;
color: string;
}
export const LOGIN_GROUP: SettingLinks = {
i18nTitle: 'SETTINGS.GROUPS.LOGIN',
i18nDesc: 'POLICY.LOGIN_POLICY.DESCRIPTION',
iamRouterLink: ['/settings'],
orgRouterLink: ['/org-settings'],
queryParams: { id: 'login' },
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
icon: 'las la-sign-in-alt',
color: 'green',
};
export const APPEARANCE_GROUP: SettingLinks = {
i18nTitle: 'SETTINGS.GROUPS.APPEARANCE',
i18nDesc: 'POLICY.PRIVATELABELING.DESCRIPTION',
iamRouterLink: ['/settings'],
orgRouterLink: ['/org-settings'],
queryParams: { id: 'branding' },
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
icon: 'las la-swatchbook',
color: 'blue',
};
export const PRIVACY_POLICY: SettingLinks = {
i18nTitle: 'SETTINGS.LIST.PRIVACYPOLICY',
i18nDesc: 'POLICY.PRIVACY_POLICY.DESCRIPTION',
iamRouterLink: ['/settings'],
orgRouterLink: ['/org-settings'],
queryParams: { id: 'privacypolicy' },
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
icon: 'las la-file-contract',
color: 'black',
};
export const NOTIFICATION_GROUP: SettingLinks = {
i18nTitle: 'SETTINGS.GROUPS.NOTIFICATIONS',
i18nDesc: '',
iamRouterLink: ['/settings'],
orgRouterLink: ['/org-settings'],
queryParams: { id: 'notifications' },
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
icon: 'las la-bell',
color: 'red',
};
export const SETTINGLINKS: SettingLinks[] = [LOGIN_GROUP, APPEARANCE_GROUP, PRIVACY_POLICY, NOTIFICATION_GROUP];

View File

@@ -0,0 +1,50 @@
<h2>{{ 'POLICY.TITLE' | translate }}</h2>
<p class="top-desc cnsl-secondary-text">{{ 'POLICY.DESCRIPTION' | translate }}</p>
<div class="row-lyt" [ngClass]="{ more: type === PolicyComponentServiceType.ADMIN }">
<ng-container *ngFor="let setting of SETTINGS">
<ng-template
cnslHasRole
[hasRole]="
type === PolicyComponentServiceType.ADMIN
? setting.iamWithRole
: type === PolicyComponentServiceType.MGMT
? setting.orgWithRole
: []
"
>
<div class="p-item card" @policy [attr.data-e2e]="'policy-card'">
<div class="avatar {{ setting.color }}">
<mat-icon *ngIf="setting.svgIcon" class="mat-icon" [svgIcon]="setting.svgIcon"></mat-icon>
<i *ngIf="setting.icon" class="icon {{ setting.icon }}"></i>
</div>
<div class="title">
<span>{{ setting.i18nTitle | translate }}</span>
</div>
<p class="desc cnsl-secondary-text">
{{ setting.i18nDesc ? (setting.i18nDesc | translate) : '' }}
</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<a
[routerLink]="
type === PolicyComponentServiceType.ADMIN
? setting.iamRouterLink
: type === PolicyComponentServiceType.MGMT
? setting.orgRouterLink
: null
"
[queryParams]="setting.queryParams"
mat-stroked-button
>
{{ 'POLICY.BTN_EDIT' | translate }}
</a>
</div>
</div>
</ng-template>
</ng-container>
</div>

View File

@@ -9,28 +9,10 @@ h2 {
font-size: 14px; font-size: 14px;
} }
.tags {
display: flex;
flex-wrap: wrap;
align-items: center;
margin: 0 -0.5rem 1rem -0.5rem;
.tag {
display: flex;
align-items: center;
margin: 0 0.5rem;
cursor: pointer;
&:hover,
&.active {
color: var(--color-main);
}
}
}
.row-lyt { .row-lyt {
margin: 0; margin: 0;
display: grid; display: grid;
margin-top: 1.5rem;
row-gap: 1rem; row-gap: 1rem;
column-gap: 1rem; column-gap: 1rem;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
@@ -39,10 +21,26 @@ h2 {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
@media only screen and (max-width: 450px) { @media only screen and (max-width: 500px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
&.more {
grid-template-columns: 1fr 1fr 1fr 1fr;
@media only screen and (max-width: 1300px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media only screen and (max-width: 850px) {
grid-template-columns: 1fr 1fr;
}
@media only screen and (max-width: 500px) {
grid-template-columns: 1fr;
}
}
.p-item { .p-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -137,16 +135,6 @@ h2 {
flex: 1; flex: 1;
} }
.tags {
.tag {
font-size: 13px;
i {
font-size: 18px;
}
}
}
.btn-wrapper { .btn-wrapper {
display: flex; display: flex;
} }

View File

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

View File

@@ -0,0 +1,43 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Component, Input } from '@angular/core';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { SETTINGLINKS, SettingLinks } from './settinglinks';
@Component({
selector: 'cnsl-settings-grid',
templateUrl: './settings-grid.component.html',
styleUrls: ['./settings-grid.component.scss'],
animations: [
trigger('policy', [
transition(':enter', [
style({
opacity: 0.5,
}),
animate(
'.15s ease-in-out',
style({
opacity: 1,
}),
),
]),
transition(':leave', [
style({
opacity: 1,
}),
animate(
'.15s ease-in-out',
style({
opacity: 0.5,
}),
),
]),
]),
],
})
export class SettingsGridComponent {
@Input() public type!: PolicyComponentServiceType;
@Input() public tag: string = '';
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public SETTINGS: SettingLinks[] = SETTINGLINKS;
}

View File

@@ -9,10 +9,10 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
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 { InfoSectionModule } from '../info-section/info-section.module'; import { InfoSectionModule } from '../info-section/info-section.module';
import { PolicyGridComponent } from './policy-grid.component'; import { SettingsGridComponent } from './settings-grid.component';
@NgModule({ @NgModule({
declarations: [PolicyGridComponent], declarations: [SettingsGridComponent],
imports: [ imports: [
CommonModule, CommonModule,
HasRolePipeModule, HasRolePipeModule,
@@ -24,6 +24,6 @@ import { PolicyGridComponent } from './policy-grid.component';
MatTooltipModule, MatTooltipModule,
InfoSectionModule, InfoSectionModule,
], ],
exports: [PolicyGridComponent], exports: [SettingsGridComponent],
}) })
export class PolicyGridModule {} export class SettingsGridModule {}

View File

@@ -0,0 +1,37 @@
<cnsl-sidenav
[title]="title"
[description]="description"
[indented]="true"
[(ngModel)]="currentSetting"
[settingsList]="settingsList"
queryParam="id"
>
<ng-container *ngIf="currentSetting === 'general' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-general-settings [serviceType]="serviceType"></cnsl-general-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'complexity'">
<cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'lockout'">
<cnsl-password-lockout-policy [serviceType]="serviceType"></cnsl-password-lockout-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'login'">
<!-- <cnsl-org-iam-policy [serviceType]="serviceType"></cnsl-org-iam-policy> -->
<cnsl-login-policy [serviceType]="serviceType"></cnsl-login-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'idp'">
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'branding'">
<cnsl-private-labeling-policy [serviceType]="serviceType"></cnsl-private-labeling-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'messagetexts'">
<cnsl-message-texts [serviceType]="serviceType"></cnsl-message-texts>
</ng-container>
<ng-container *ngIf="currentSetting === 'logintexts'">
<cnsl-login-texts [serviceType]="serviceType"></cnsl-login-texts>
</ng-container>
<ng-container *ngIf="currentSetting === 'privacypolicy'">
<cnsl-privacy-policy [serviceType]="PolicyComponentServiceType.ADMIN"></cnsl-privacy-policy>
</ng-container>
</cnsl-sidenav>

View File

@@ -0,0 +1,3 @@
.divider {
margin: 2rem 0;
}

View File

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

View File

@@ -0,0 +1,31 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { SidenavSetting } from '../sidenav/sidenav.component';
@Component({
selector: 'cnsl-settings-list',
templateUrl: './settings-list.component.html',
styleUrls: ['./settings-list.component.scss'],
})
export class SettingsListComponent implements OnChanges {
@Input() public title: string = '';
@Input() public description: string = '';
@Input() public serviceType!: PolicyComponentServiceType;
@Input() public selectedId: string = '';
@Input() public settingsList: SidenavSetting[] = [];
public currentSetting: string | undefined = '';
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.selectedId?.currentValue) {
this.currentSetting =
this.settingsList && this.settingsList.find((l) => l.id === changes.selectedId.currentValue)
? changes.selectedId.currentValue
: '';
} else {
this.currentSetting = this.settingsList ? this.settingsList[0].id : '';
}
}
}

View File

@@ -0,0 +1,43 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../card/card.module';
import { GeneralSettingsModule } from '../policies/general-settings/general-settings.module';
import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module';
import { LoginPolicyModule } from '../policies/login-policy/login-policy.module';
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { OrgIamPolicyModule } from '../policies/org-iam-policy/org-iam-policy.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
import { PrivacyPolicyModule } from '../policies/privacy-policy/privacy-policy.module';
import { PrivateLabelingPolicyModule } from '../policies/private-labeling-policy/private-labeling-policy.module';
import { SidenavModule } from '../sidenav/sidenav.module';
import { SettingsListComponent } from './settings-list.component';
@NgModule({
declarations: [SettingsListComponent],
imports: [
CommonModule,
FormsModule,
SidenavModule,
LoginPolicyModule,
CardModule,
PasswordComplexityPolicyModule,
PasswordLockoutPolicyModule,
PrivateLabelingPolicyModule,
GeneralSettingsModule,
IdpSettingsModule,
PrivacyPolicyModule,
MessageTextsPolicyModule,
LoginTextsPolicyModule,
OrgIamPolicyModule,
TranslateModule,
HasRolePipeModule,
],
exports: [SettingsListComponent],
})
export class SettingsListModule {}

View File

@@ -0,0 +1,66 @@
import { SidenavSetting } from '../sidenav/sidenav.component';
export const GENERAL: SidenavSetting = {
id: 'general',
i18nKey: 'SETTINGS.LIST.GENERAL',
};
export const LOGIN: SidenavSetting = {
id: 'login',
i18nKey: 'SETTINGS.LIST.LOGIN',
groupI18nKey: 'SETTINGS.GROUPS.LOGIN',
// requiredRoles: {
// [PolicyComponentServiceType.ADMIN]: true,
// [PolicyComponentServiceType.MGMT]: true,
// }
};
export const LOCKOUT: SidenavSetting = {
id: 'lockout',
i18nKey: 'SETTINGS.LIST.LOCKOUT',
groupI18nKey: 'SETTINGS.GROUPS.LOGIN',
};
export const COMPLEXITY: SidenavSetting = {
id: 'complexity',
i18nKey: 'SETTINGS.LIST.COMPLEXITY',
groupI18nKey: 'SETTINGS.GROUPS.LOGIN',
};
export const IDP: SidenavSetting = { id: 'idp', i18nKey: 'SETTINGS.LIST.IDP', groupI18nKey: 'SETTINGS.GROUPS.LOGIN' };
export const NOTIFICATIONPROVIDERS: SidenavSetting = {
id: 'notificationproviders',
i18nKey: 'SETTINGS.LIST.NOTIFICATIONPROVIDERS',
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
};
export const NOTIFICATIONS: SidenavSetting = {
id: 'notifications',
i18nKey: 'SETTINGS.LIST.NOTIFICATIONS',
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
};
export const MESSAGETEXTS: SidenavSetting = {
id: 'messagetexts',
i18nKey: 'SETTINGS.LIST.MESSAGETEXTS',
groupI18nKey: 'SETTINGS.GROUPS.APPEARANCE',
};
export const LOGINTEXTS: SidenavSetting = {
id: 'logintexts',
i18nKey: 'SETTINGS.LIST.LOGINTEXTS',
groupI18nKey: 'SETTINGS.GROUPS.APPEARANCE',
};
export const PRIVACYPOLICY: SidenavSetting = {
id: 'privacypolicy',
i18nKey: 'SETTINGS.LIST.PRIVACYPOLICY',
groupI18nKey: 'SETTINGS.GROUPS.OTHER',
};
export const BRANDING: SidenavSetting = {
id: 'branding',
i18nKey: 'SETTINGS.LIST.BRANDING',
groupI18nKey: 'SETTINGS.GROUPS.APPEARANCE',
};

View File

@@ -7,7 +7,7 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.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 { StorageLocation, StorageService } from 'src/app/services/storage.service';
import { POLICIES } from '../policy-grid/policies'; import { SETTINGLINKS } from '../settings-grid/settinglinks';
export interface ShortcutItem { export interface ShortcutItem {
id: string; id: string;
@@ -108,8 +108,8 @@ export class ShortcutsComponent implements OnDestroy {
const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session); const org: Org.AsObject | null = this.storageService.getItem('organization', StorageLocation.session);
if (org && org.id) { if (org && org.id) {
this.org = org; this.org = org;
}
this.loadProjectShortcuts(); this.loadProjectShortcuts();
}
}); });
} }
@@ -132,7 +132,7 @@ export class ShortcutsComponent implements OnDestroy {
}); });
const routesShortcuts = [PROFILE_SHORTCUT, CREATE_ORG, CREATE_PROJECT, CREATE_USER]; const routesShortcuts = [PROFILE_SHORTCUT, CREATE_ORG, CREATE_PROJECT, CREATE_USER];
const policyShortcuts = POLICIES.map((p) => { const settingsShortcuts = SETTINGLINKS.map((p) => {
const policy: ShortcutItem = { const policy: ShortcutItem = {
id: p.i18nTitle, id: p.i18nTitle,
type: ShortcutType.POLICY, type: ShortcutType.POLICY,
@@ -148,7 +148,7 @@ export class ShortcutsComponent implements OnDestroy {
return policy; return policy;
}); });
this.ALL_SHORTCUTS = [...routesShortcuts, ...policyShortcuts, ...mapped]; this.ALL_SHORTCUTS = [...routesShortcuts, ...settingsShortcuts, ...mapped];
this.loadShortcuts(this.org); this.loadShortcuts(this.org);
} }
}); });

View File

@@ -1,6 +1,9 @@
<div class="sidenav-container"> <div class="sidenav-container">
<div class="sidenav-settings-list"> <div class="sidenav-settings-list" [ngClass]="{ indented: indented }">
<div class="sidenav-sticky-rel"> <div class="sidenav-sticky-rel">
<h1 *ngIf="title">{{ title }}</h1>
<p *ngIf="description" class="cnsl-secondary-text">{{ description }}</p>
<button <button
*ngIf="currentSetting !== undefined" *ngIf="currentSetting !== undefined"
(click)="value = undefined" (click)="value = undefined"
@@ -10,8 +13,19 @@
<i class="las la-angle-left"></i> <i class="las la-angle-left"></i>
<span>{{ 'USER.SETTINGS.TITLE' | translate }}</span> <span>{{ 'USER.SETTINGS.TITLE' | translate }}</span>
</button> </button>
<ng-container *ngFor="let setting of settingsList">
<ng-container *ngFor="let setting of settingsList; index as i">
<ng-container> <ng-container>
<span
class="sidenav-setting-group hide-on-mobile"
[ngClass]="{ show: currentSetting === undefined }"
*ngIf="
(setting.groupI18nKey && i > 0 && setting.groupI18nKey !== settingsList[i - 1].groupI18nKey) ||
(i === 0 && setting.groupI18nKey)
"
>{{ setting.groupI18nKey | translate }}</span
>
<button <button
(click)="value = setting.id" (click)="value = setting.id"
class="sidenav-setting-list-element hide-on-mobile" class="sidenav-setting-list-element hide-on-mobile"
@@ -34,8 +48,6 @@
</div> </div>
<div class="sidenav-content"> <div class="sidenav-content">
<div class="max-width-container">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
</div>
</div> </div>

View File

@@ -2,10 +2,10 @@
@mixin sidenav-theme($theme) { @mixin sidenav-theme($theme) {
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
.sidenav-container { .sidenav-container {
padding-top: 1rem;
display: grid; display: grid;
position: relative; position: relative;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -13,12 +13,54 @@
.sidenav-settings-list { .sidenav-settings-list {
position: relative; position: relative;
&.indented {
margin-right: 2rem;
.sidenav-sticky-rel {
margin-left: -2rem;
padding-left: 2rem;
background: map-get($background, footer);
}
}
.sidenav-sticky-rel { .sidenav-sticky-rel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: sticky; position: sticky;
top: 50px; top: 36px;
padding: 1rem 2rem 1rem 0; padding: 20px 2rem 20px 0;
.sidenav-setting-group {
font-size: 11px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: if($is-dark-theme, #ffffff60, #00000060);
margin-top: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
padding-top: 1rem;
border-top: 1px solid if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
&:nth-child(2) {
border-top: none;
padding-top: 0;
margin-top: 0;
}
&.hide-on-mobile {
@media only screen and (max-width: 500px) {
display: none;
}
}
&.show {
display: none;
@media only screen and (max-width: 500px) {
display: flex;
}
}
}
.sidenav-setting-list-element { .sidenav-setting-list-element {
border: none; border: none;

View File

@@ -1,32 +1,65 @@
import { Component, forwardRef, Input } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
export interface SidenavSetting { export interface SidenavSetting {
id: string; id: string;
i18nKey: string; i18nKey: string;
groupI18nKey?: string;
requiredRoles?: { [serviceType in PolicyComponentServiceType]: string[] };
} }
@Component({ @Component({
selector: 'cnsl-sidenav', selector: 'cnsl-sidenav',
templateUrl: './sidenav.component.html', templateUrl: './sidenav.component.html',
styleUrls: ['./sidenav.component.scss'], styleUrls: ['./sidenav.component.scss'],
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SidenavComponent), multi: true }], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: SidenavComponent, multi: true }],
}) })
export class SidenavComponent { export class SidenavComponent implements ControlValueAccessor, OnInit {
@Input() public currentSetting: string | undefined = 'general'; @Input() public title: string = '';
@Input() public description: string = '';
@Input() public indented: boolean = false;
@Input() public currentSetting?: string | undefined = undefined;
@Input() public settingsList: SidenavSetting[] = []; @Input() public settingsList: SidenavSetting[] = [];
@Input() public queryParam: string = '';
constructor() {} constructor(private router: Router, private route: ActivatedRoute) {}
private onChange: any = () => {}; ngOnInit(): void {
private onTouch: any = () => {}; if (!this.value) {
this.value = this.settingsList[0].id;
}
}
private onChange = (current: string | undefined) => {};
private onTouch = (current: string | undefined) => {};
@Input() get value(): string | undefined {
return this.currentSetting;
}
set value(setting: string | undefined) { set value(setting: string | undefined) {
this.currentSetting = setting; this.currentSetting = setting;
if (setting || setting === undefined) {
this.onChange(setting); this.onChange(setting);
this.onTouch(setting); this.onTouch(setting);
} }
if (this.queryParam && setting) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
[this.queryParam]: setting,
},
queryParamsHandling: 'merge',
skipLocationChange: false,
});
}
}
public writeValue(value: any) { public writeValue(value: any) {
this.value = value; this.value = value;
} }

View File

@@ -1,13 +1,15 @@
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 { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { SidenavComponent } from './sidenav.component'; import { SidenavComponent } from './sidenav.component';
@NgModule({ @NgModule({
declarations: [SidenavComponent], declarations: [SidenavComponent],
imports: [CommonModule, MatIconModule, TranslateModule], imports: [CommonModule, FormsModule, RouterModule, MatIconModule, TranslateModule],
exports: [SidenavComponent], exports: [SidenavComponent],
}) })
export class SidenavModule {} export class SidenavModule {}

View File

@@ -84,7 +84,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
'actions', 'actions',
]; ];
public ngOnInit(): void { ngOnInit(): void {
this.dataSource = new UserGrantsDataSource(this.userService); this.dataSource = new UserGrantsDataSource(this.userService);
switch (this.context) { switch (this.context) {

Some files were not shown because too many files have changed in this diff Show More