fix(console): ui issues (#1670)

* mark required fields

* membership overflow, deactivate btn

* app create show info

* remove padding

* lint scss

* fix count of results

* rm log
This commit is contained in:
Max Peintner 2021-04-27 08:15:53 +02:00 committed by GitHub
parent c4607409a5
commit 10e85d999e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1196 additions and 1154 deletions

View File

@ -7,8 +7,8 @@ import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
export enum ProjectType { export enum ProjectType {
PROJECTTYPE_OWNED = 'OWNED', PROJECTTYPE_OWNED = 'OWNED',
PROJECTTYPE_GRANTED = 'GRANTED', PROJECTTYPE_GRANTED = 'GRANTED',
} }
/** /**
@ -17,68 +17,70 @@ export enum ProjectType {
* (including sorting, pagination, and filtering). * (including sorting, pagination, and filtering).
*/ */
export class ProjectMembersDataSource extends DataSource<Member.AsObject> { export class ProjectMembersDataSource extends DataSource<Member.AsObject> {
public totalResult: number = 0; public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]); public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
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();
constructor(private mgmtService: ManagementService) { constructor(private mgmtService: ManagementService) {
super(); super();
} }
public loadMembers(projectId: string, public loadMembers(projectId: string,
projectType: ProjectType, projectType: ProjectType,
pageIndex: number, pageSize: number, grantId?: string): void { pageIndex: number, pageSize: number, grantId?: string): void {
const offset = pageIndex * pageSize; const offset = pageIndex * pageSize;
this.loadingSubject.next(true); this.loadingSubject.next(true);
const promise: const promise:
Promise<ListProjectMembersResponse.AsObject> | Promise<ListProjectMembersResponse.AsObject> |
Promise<ListProjectGrantMembersResponse.AsObject> Promise<ListProjectGrantMembersResponse.AsObject>
| undefined = | undefined =
projectType === ProjectType.PROJECTTYPE_OWNED ? projectType === ProjectType.PROJECTTYPE_OWNED ?
this.mgmtService.listProjectMembers(projectId, pageSize, offset) : this.mgmtService.listProjectMembers(projectId, pageSize, offset) :
projectType === ProjectType.PROJECTTYPE_GRANTED && grantId ? projectType === ProjectType.PROJECTTYPE_GRANTED && grantId ?
this.mgmtService.listProjectGrantMembers(projectId, this.mgmtService.listProjectGrantMembers(projectId,
grantId, pageSize, offset) : undefined; grantId, pageSize, offset) : undefined;
if (promise) { if (promise) {
from(promise).pipe( from(promise).pipe(
map(resp => { map(resp => {
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult; this.totalResult = resp.details?.totalResult;
} } else {
if (resp.details?.viewTimestamp) { this.totalResult = 0;
this.viewTimestamp = resp.details.viewTimestamp; }
} if (resp.details?.viewTimestamp) {
return resp.resultList; this.viewTimestamp = resp.details.viewTimestamp;
}), }
catchError(() => of([])), return resp.resultList;
finalize(() => this.loadingSubject.next(false)), }),
).subscribe(members => { catchError(() => of([])),
this.membersSubject.next(members); finalize(() => this.loadingSubject.next(false)),
}); ).subscribe(members => {
} this.membersSubject.next(members);
});
} }
}
/** /**
* Connect this data source to the table. The table will only update when * Connect this data source to the table. The table will only update when
* the returned stream emits new items. * the returned stream emits new items.
* @returns A stream of the items to be rendered. * @returns A stream of the items to be rendered.
*/ */
public connect(): Observable<Member.AsObject[]> { public connect(): Observable<Member.AsObject[]> {
return this.membersSubject.asObservable(); return this.membersSubject.asObservable();
} }
/** /**
* Called when the table is being destroyed. Use this function, to clean up * Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect. * any open connections or free any held resources that were set up during connect.
*/ */
public disconnect(): void { public disconnect(): void {
this.membersSubject.complete(); this.membersSubject.complete();
this.loadingSubject.complete(); this.loadingSubject.complete();
} }
} }

View File

@ -4,150 +4,152 @@ import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { catchError, finalize, map } from 'rxjs/operators';
import { ListUserGrantResponse } from 'src/app/proto/generated/zitadel/management_pb'; import { ListUserGrantResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { import {
UserGrant, UserGrant,
UserGrantProjectGrantIDQuery, UserGrantProjectGrantIDQuery,
UserGrantProjectIDQuery, UserGrantProjectIDQuery,
UserGrantQuery, UserGrantQuery,
UserGrantUserIDQuery, UserGrantUserIDQuery,
} from 'src/app/proto/generated/zitadel/user_pb'; } from 'src/app/proto/generated/zitadel/user_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
export enum UserGrantContext { export enum UserGrantContext {
NONE = 'none', NONE = 'none',
USER = 'user', USER = 'user',
OWNED_PROJECT = 'owned', OWNED_PROJECT = 'owned',
GRANTED_PROJECT = 'granted', GRANTED_PROJECT = 'granted',
} }
export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> { export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
public totalResult: number = 0; public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public grantsSubject: BehaviorSubject<UserGrant.AsObject[]> = new BehaviorSubject<UserGrant.AsObject[]>([]); public grantsSubject: BehaviorSubject<UserGrant.AsObject[]> = new BehaviorSubject<UserGrant.AsObject[]>([]);
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();
constructor(private userService: ManagementService) { constructor(private userService: ManagementService) {
super(); super();
} }
public loadGrants( public loadGrants(
context: UserGrantContext, context: UserGrantContext,
pageIndex: number, pageIndex: number,
pageSize: number, pageSize: number,
data: { data: {
projectId?: string; projectId?: string;
grantId?: string; grantId?: string;
userId?: string; userId?: string;
}, },
queries?: UserGrantQuery[], queries?: UserGrantQuery[],
): void { ): void {
switch (context) { switch (context) {
case UserGrantContext.USER: case UserGrantContext.USER:
if (data && data.userId) { if (data && data.userId) {
this.loadingSubject.next(true); this.loadingSubject.next(true);
const userfilter = new UserGrantQuery(); const userfilter = new UserGrantQuery();
const ugUiq = new UserGrantUserIDQuery(); const ugUiq = new UserGrantUserIDQuery();
ugUiq.setUserId(data.userId); ugUiq.setUserId(data.userId);
userfilter.setUserIdQuery(ugUiq); userfilter.setUserIdQuery(ugUiq);
if (queries) { if (queries) {
queries.push(userfilter); queries.push(userfilter);
} else { } else {
queries = [userfilter]; queries = [userfilter];
} }
const promise = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries); const promise = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise); this.loadResponse(promise);
}
break;
case UserGrantContext.OWNED_PROJECT:
if (data && data.projectId) {
this.loadingSubject.next(true);
const projectfilter = new UserGrantQuery();
const ugPfq = new UserGrantProjectIDQuery();
ugPfq.setProjectId(data.projectId);
projectfilter.setProjectIdQuery(ugPfq);
if (queries) {
queries.push(projectfilter);
} else {
queries = [projectfilter];
}
const promise1 = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise1);
}
break;
case UserGrantContext.GRANTED_PROJECT:
if (data && data.grantId && data.projectId) {
this.loadingSubject.next(true);
const grantfilter = new UserGrantQuery();
const uggiq = new UserGrantProjectGrantIDQuery();
uggiq.setProjectGrantId(data.grantId);
grantfilter.setProjectGrantIdQuery(uggiq);
const projectfilter = new UserGrantQuery();
const ugPfq = new UserGrantProjectIDQuery();
ugPfq.setProjectId(data.projectId);
projectfilter.setProjectIdQuery(ugPfq);
if (queries) {
queries.push(grantfilter);
} else {
queries = [grantfilter];
}
const promise2 = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise2);
}
break;
default:
this.loadingSubject.next(true);
const promise3 = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries ?? []);
this.loadResponse(promise3);
break;
} }
} break;
case UserGrantContext.OWNED_PROJECT:
if (data && data.projectId) {
this.loadingSubject.next(true);
private loadResponse(promise: Promise<ListUserGrantResponse.AsObject>): void { const projectfilter = new UserGrantQuery();
from(promise).pipe( const ugPfq = new UserGrantProjectIDQuery();
map(resp => { ugPfq.setProjectId(data.projectId);
if (resp.details?.totalResult) { projectfilter.setProjectIdQuery(ugPfq);
this.totalResult = resp.details.totalResult;
} if (queries) {
if (resp.details?.viewTimestamp) { queries.push(projectfilter);
this.viewTimestamp = resp.details.viewTimestamp; } else {
} queries = [projectfilter];
return resp.resultList; }
}),
catchError(() => of([])), const promise1 = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries);
finalize(() => this.loadingSubject.next(false)), this.loadResponse(promise1);
).subscribe(grants => { }
this.grantsSubject.next(grants); break;
}); case UserGrantContext.GRANTED_PROJECT:
if (data && data.grantId && data.projectId) {
this.loadingSubject.next(true);
const grantfilter = new UserGrantQuery();
const uggiq = new UserGrantProjectGrantIDQuery();
uggiq.setProjectGrantId(data.grantId);
grantfilter.setProjectGrantIdQuery(uggiq);
const projectfilter = new UserGrantQuery();
const ugPfq = new UserGrantProjectIDQuery();
ugPfq.setProjectId(data.projectId);
projectfilter.setProjectIdQuery(ugPfq);
if (queries) {
queries.push(grantfilter);
} else {
queries = [grantfilter];
}
const promise2 = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise2);
}
break;
default:
this.loadingSubject.next(true);
const promise3 = this.userService.listUserGrants(pageSize, pageSize * pageIndex, queries ?? []);
this.loadResponse(promise3);
break;
} }
}
private loadResponse(promise: Promise<ListUserGrantResponse.AsObject>): void {
from(promise).pipe(
map(resp => {
if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult;
} else {
this.totalResult = 0;
}
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details.viewTimestamp;
}
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(grants => {
this.grantsSubject.next(grants);
});
}
/** /**
* Connect this data source to the table. The table will only update when * Connect this data source to the table. The table will only update when
* the returned stream emits new items. * the returned stream emits new items.
* @returns A stream of the items to be rendered. * @returns A stream of the items to be rendered.
*/ */
public connect(): Observable<UserGrant.AsObject[]> { public connect(): Observable<UserGrant.AsObject[]> {
return this.grantsSubject.asObservable(); return this.grantsSubject.asObservable();
} }
/** /**
* Called when the table is being destroyed. Use this function, to clean up * Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect. * any open connections or free any held resources that were set up during connect.
*/ */
public disconnect(): void { public disconnect(): void {
this.grantsSubject.complete(); this.grantsSubject.complete();
this.loadingSubject.complete(); this.loadingSubject.complete();
} }
} }

View File

@ -13,83 +13,85 @@ import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@Component({ @Component({
selector: 'app-iam', selector: 'app-iam',
templateUrl: './iam.component.html', templateUrl: './iam.component.html',
styleUrls: ['./iam.component.scss'], styleUrls: ['./iam.component.scss'],
}) })
export class IamComponent { export class IamComponent {
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
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 totalMemberResult: number = 0; public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> public membersSubject: BehaviorSubject<Member.AsObject[]>
= new BehaviorSubject<Member.AsObject[]>([]); = new BehaviorSubject<Member.AsObject[]>([]);
public PolicyGridType: any = PolicyGridType; public PolicyGridType: any = PolicyGridType;
public features!: Features.AsObject; public features!: Features.AsObject;
constructor(public adminService: AdminService, private dialog: MatDialog, private toast: ToastService, constructor(public adminService: AdminService, private dialog: MatDialog, private toast: ToastService,
private router: Router) { private router: Router) {
this.loadMembers(); this.loadMembers();
this.loadFeatures(); this.loadFeatures();
this.adminService.getDefaultFeatures(); this.adminService.getDefaultFeatures();
} }
public loadMembers(): void { public loadMembers(): void {
this.loadingSubject.next(true); this.loadingSubject.next(true);
from(this.adminService.listIAMMembers(100, 0)).pipe( from(this.adminService.listIAMMembers(100, 0)).pipe(
map(resp => { map(resp => {
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalMemberResult = resp.details.totalResult; this.totalMemberResult = resp.details.totalResult;
} } else {
return resp.resultList; this.totalMemberResult = 0;
}), }
catchError(() => of([])), return resp.resultList;
finalize(() => this.loadingSubject.next(false)), }),
).subscribe(members => { catchError(() => of([])),
this.membersSubject.next(members); finalize(() => this.loadingSubject.next(false)),
}); ).subscribe(members => {
} this.membersSubject.next(members);
});
}
public openAddMember(): void { public openAddMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, { const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: { data: {
creationType: CreationType.IAM, creationType: CreationType.IAM,
}, },
width: '400px', width: '400px',
}); });
dialogRef.afterClosed().subscribe(resp => { dialogRef.afterClosed().subscribe(resp => {
if (resp) { if (resp) {
const users: User.AsObject[] = resp.users; const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles; const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) { if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => { Promise.all(users.map(user => {
return this.adminService.addIAMMember(user.id, roles); return this.adminService.addIAMMember(user.id, roles);
})).then(() => { })).then(() => {
this.toast.showInfo('IAM.TOAST.MEMBERADDED'); this.toast.showInfo('IAM.TOAST.MEMBERADDED');
setTimeout(() => { setTimeout(() => {
this.loadMembers(); this.loadMembers();
}, 1000); }, 1000);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} }
} }
}); });
} }
public showDetail(): void { public showDetail(): void {
this.router.navigate(['iam/members']); this.router.navigate(['iam/members']);
} }
public loadFeatures(): void { public loadFeatures(): void {
this.loadingSubject.next(true); this.loadingSubject.next(true);
this.adminService.getDefaultFeatures().then(resp => { this.adminService.getDefaultFeatures().then(resp => {
if (resp.features) { if (resp.features) {
this.features = resp.features; this.features = resp.features;
} }
}); });
} }
} }

View File

@ -204,7 +204,10 @@ export class OrgDetailComponent implements OnInit {
map(resp => { map(resp => {
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalMemberResult = resp.details?.totalResult; this.totalMemberResult = resp.details?.totalResult;
} else {
this.totalMemberResult = 0;
} }
return resp.resultList; return resp.resultList;
}), }),
catchError(() => of([])), catchError(() => of([])),

View File

@ -3,16 +3,27 @@
</h1> </h1>
<p class="desc">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p> <p class="desc">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
<div mat-dialog-content> <div mat-dialog-content>
<p *ngIf="data.clientId">ClientId: {{data.clientId}}</p> <div class="flex" *ngIf="data.clientId">
<span class="overflow-auto"><span class="desc">ClientId:</span> {{data.clientId}}</span>
<button color="primary" [disabled]="copied == data.clientId" matTooltip="copy to clipboard" appCopyToClipboard
[valueToCopy]="data.clientId" (copiedValue)="this.copied = $event" mat-icon-button>
<i *ngIf="copied != data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied == data.clientId" class="las la-clipboard-check"></i>
</button>
</div>
<div *ngIf="data.clientSecret" class="flex"> <div *ngIf="data.clientSecret; else showNoSecretInfo" class="flex">
<span class="overflow-auto"><span class="desc">ClientSecret:</span> {{data.clientSecret}}</span>
<button color="primary" [disabled]="copied == data.clientSecret" matTooltip="copy to clipboard" <button color="primary" [disabled]="copied == data.clientSecret" matTooltip="copy to clipboard"
appCopyToClipboard [valueToCopy]="data.clientSecret" (copiedValue)="this.copied = $event" mat-icon-button> appCopyToClipboard [valueToCopy]="data.clientSecret" (copiedValue)="this.copied = $event" mat-icon-button>
<i *ngIf="copied != data.clientSecret" class="las la-clipboard"></i> <i *ngIf="copied != data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied == data.clientSecret" class="las la-clipboard-check"></i> <i *ngIf="copied == data.clientSecret" class="las la-clipboard-check"></i>
</button> </button>
<span class="secret">{{data.clientSecret}}</span>
</div> </div>
<ng-template #showNoSecretInfo>
<cnsl-info-section>{{'APP.OIDC.CLIENTSECRET_NOSECRET' | translate}}</cnsl-info-section>
</ng-template>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">

View File

@ -23,11 +23,16 @@
.flex { .flex {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .5rem;
border: 1px solid #ffffff20; border: 1px solid #ffffff20;
border-radius: .5rem; border-radius: .5rem;
justify-content: space-between;
.secret { .overflow-auto {
overflow: auto; overflow: auto;
.desc {
font-size: 14px;
color: var(--grey);
}
} }
} }

View File

@ -15,127 +15,129 @@ 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: 'app-granted-project-detail', selector: 'app-granted-project-detail',
templateUrl: './granted-project-detail.component.html', templateUrl: './granted-project-detail.component.html',
styleUrls: ['./granted-project-detail.component.scss'], styleUrls: ['./granted-project-detail.component.scss'],
}) })
export class GrantedProjectDetailComponent implements OnInit, OnDestroy { export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = ''; public projectId: string = '';
public grantId: string = ''; public grantId: string = '';
public project!: GrantedProject.AsObject; public project!: GrantedProject.AsObject;
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
private subscription?: Subscription; private subscription?: Subscription;
public isZitadel: boolean = false; public isZitadel: boolean = false;
UserGrantContext: any = UserGrantContext; UserGrantContext: any = UserGrantContext;
// members // members
public totalMemberResult: number = 0; public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> public membersSubject: BehaviorSubject<Member.AsObject[]>
= new BehaviorSubject<Member.AsObject[]>([]); = new BehaviorSubject<Member.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private route: ActivatedRoute, private route: ActivatedRoute,
private toast: ToastService, private toast: ToastService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private _location: Location, private _location: Location,
private router: Router, private router: Router,
private dialog: MatDialog, private dialog: MatDialog,
) { ) {
} }
public ngOnInit(): void { public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params)); this.subscription = this.route.params.subscribe(params => this.getData(params));
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.subscription?.unsubscribe(); this.subscription?.unsubscribe();
} }
private async getData({ id, grantId }: Params): Promise<void> { private async getData({ id, grantId }: Params): Promise<void> {
this.projectId = id; this.projectId = id;
this.grantId = grantId; this.grantId = grantId;
this.mgmtService.getIAM().then(iam => { this.mgmtService.getIAM().then(iam => {
this.isZitadel = iam.iamProjectId === this.projectId; this.isZitadel = iam.iamProjectId === this.projectId;
}); });
if (this.projectId && this.grantId) { if (this.projectId && this.grantId) {
this.mgmtService.getGrantedProjectByID(this.projectId, this.grantId).then(proj => { this.mgmtService.getGrantedProjectByID(this.projectId, this.grantId).then(proj => {
if (proj.grantedProject) { if (proj.grantedProject) {
this.project = proj.grantedProject; this.project = proj.grantedProject;
}
}).catch(error => {
this.toast.showError(error);
});
this.loadMembers();
} }
} }).catch(error => {
this.toast.showError(error);
});
public loadMembers(): void { this.loadMembers();
this.loadingSubject.next(true); }
from(this.mgmtService.listProjectGrantMembers(this.projectId, }
this.grantId, 100, 0)).pipe(
map(resp => { public loadMembers(): void {
if (resp.details?.totalResult) { this.loadingSubject.next(true);
this.totalMemberResult = resp.details.totalResult; from(this.mgmtService.listProjectGrantMembers(this.projectId,
} this.grantId, 100, 0)).pipe(
return resp.resultList; map(resp => {
}), if (resp.details?.totalResult) {
catchError(() => of([])), this.totalMemberResult = resp.details.totalResult;
finalize(() => this.loadingSubject.next(false)), } else {
).subscribe(members => { this.totalMemberResult = 0;
this.membersSubject.next(members); }
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
public navigateBack(): void {
this._location.back();
}
public openAddMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: {
creationType: CreationType.PROJECT_GRANTED,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
this.projectId,
this.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadMembers();
}, 1000);
}).catch(error => {
this.toast.showError(error);
}); });
} });
}
}
});
}
public navigateBack(): void { public showDetail(): void {
this._location.back(); this.router.navigate(['granted-projects', this.project.projectId, 'grant', this.grantId, 'members']);
} }
public openAddMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: {
creationType: CreationType.PROJECT_GRANTED,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
this.projectId,
this.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadMembers();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
});
}
public showDetail(): void {
this.router.navigate(['granted-projects', this.project.projectId, 'grant', this.grantId, 'members']);
}
} }

View File

@ -12,107 +12,109 @@ 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: 'app-granted-project-list', selector: 'app-granted-project-list',
templateUrl: './granted-project-list.component.html', templateUrl: './granted-project-list.component.html',
styleUrls: ['./granted-project-list.component.scss'], styleUrls: ['./granted-project-list.component.scss'],
animations: [ animations: [
trigger('list', [ trigger('list', [
transition(':enter', [ transition(':enter', [
query('@animate', query('@animate',
stagger(80, animateChild()), stagger(80, animateChild()),
), ),
]), ]),
]), ]),
trigger('animate', [ trigger('animate', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }), style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })), animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]), ]),
transition(':leave', [ transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }), style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })), animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]), ]),
]), ]),
], ],
}) })
export class GrantedProjectListComponent implements OnInit, OnDestroy { export class GrantedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0; public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public dataSource: MatTableDataSource<GrantedProject.AsObject> = public dataSource: MatTableDataSource<GrantedProject.AsObject> =
new MatTableDataSource<GrantedProject.AsObject>(); new MatTableDataSource<GrantedProject.AsObject>();
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public grantedProjectList: GrantedProject.AsObject[] = []; public grantedProjectList: GrantedProject.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'resourceOwnerName', 'state', 'creationDate', 'changeDate']; public displayedColumns: string[] = ['select', 'name', 'resourceOwnerName', 'state', 'creationDate', 'changeDate'];
public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []); public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
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 grid: boolean = true; public grid: boolean = true;
private subscription?: Subscription; private subscription?: Subscription;
constructor(private router: Router, constructor(private router: Router,
public translate: TranslateService, public translate: TranslateService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private toast: ToastService, private toast: ToastService,
) { } ) { }
public ngOnInit(): void { public ngOnInit(): void {
this.getData(10, 0); this.getData(10, 0);
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.subscription?.unsubscribe(); this.subscription?.unsubscribe();
} }
public isAllSelected(): boolean { public isAllSelected(): boolean {
const numSelected = this.selection.selected.length; const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length; const numRows = this.dataSource.data.length;
return numSelected === numRows; return numSelected === numRows;
} }
public masterToggle(): void { public masterToggle(): void {
this.isAllSelected() ? this.isAllSelected() ?
this.selection.clear() : this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row)); this.dataSource.data.forEach(row => this.selection.select(row));
} }
public changePage(event: PageEvent): void { public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex); this.getData(event.pageSize, event.pageIndex);
} }
public addProject(): void { public addProject(): void {
this.router.navigate(['/projects', 'create']); this.router.navigate(['/projects', 'create']);
} }
private async getData(limit: number, offset: number): Promise<void> { private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true); this.loadingSubject.next(true);
this.mgmtService.listGrantedProjects(limit, offset).then(resp => { this.mgmtService.listGrantedProjects(limit, offset).then(resp => {
this.grantedProjectList = resp.resultList; this.grantedProjectList = resp.resultList;
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult; this.totalResult = resp.details.totalResult;
} } else {
if (resp.details?.viewTimestamp) { this.totalResult = 0;
this.viewTimestamp = resp.details?.viewTimestamp; }
} if (resp.details?.viewTimestamp) {
if (this.totalResult > 5) { this.viewTimestamp = resp.details?.viewTimestamp;
this.grid = false; }
} if (this.totalResult > 5) {
this.dataSource.data = this.grantedProjectList; this.grid = false;
console.log(resp.resultList); }
this.dataSource.data = this.grantedProjectList;
console.log(resp.resultList);
this.loadingSubject.next(false); this.loadingSubject.next(false);
}).catch(error => { }).catch(error => {
console.error(error); console.error(error);
this.toast.showError(error); this.toast.showError(error);
this.loadingSubject.next(false); this.loadingSubject.next(false);
}); });
} }
public refreshPage(): void { public refreshPage(): void {
this.selection.clear(); this.selection.clear();
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize); this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
} }
} }

View File

@ -19,220 +19,222 @@ 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: 'app-owned-project-detail', selector: 'app-owned-project-detail',
templateUrl: './owned-project-detail.component.html', templateUrl: './owned-project-detail.component.html',
styleUrls: ['./owned-project-detail.component.scss'], styleUrls: ['./owned-project-detail.component.scss'],
}) })
export class OwnedProjectDetailComponent implements OnInit, OnDestroy { export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = ''; public projectId: string = '';
public project!: Project.AsObject; public project!: Project.AsObject;
public pageSizeApps: number = 10; public pageSizeApps: number = 10;
public appsDataSource: MatTableDataSource<App.AsObject> = new MatTableDataSource<App.AsObject>(); public appsDataSource: MatTableDataSource<App.AsObject> = new MatTableDataSource<App.AsObject>();
public appsResult!: ListAppsResponse.AsObject; public appsResult!: ListAppsResponse.AsObject;
public appsColumns: string[] = ['name']; public appsColumns: string[] = ['name'];
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
public grid: boolean = true; public grid: boolean = true;
private subscription?: Subscription; private subscription?: Subscription;
public editstate: boolean = false; public editstate: boolean = false;
public isZitadel: boolean = false; public isZitadel: boolean = false;
public UserGrantContext: any = UserGrantContext; public UserGrantContext: any = UserGrantContext;
// members // members
public totalMemberResult: number = 0; public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> public membersSubject: BehaviorSubject<Member.AsObject[]>
= new BehaviorSubject<Member.AsObject[]>([]); = new BehaviorSubject<Member.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public refreshChanges$: EventEmitter<void> = new EventEmitter(); public refreshChanges$: EventEmitter<void> = new EventEmitter();
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private route: ActivatedRoute, private route: ActivatedRoute,
private toast: ToastService, private toast: ToastService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private _location: Location, private _location: Location,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router, private router: Router,
) { } ) { }
public ngOnInit(): void { public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params)); this.subscription = this.route.params.subscribe(params => this.getData(params));
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.subscription?.unsubscribe(); this.subscription?.unsubscribe();
} }
private async getData({ id }: Params): Promise<void> { private async getData({ id }: Params): Promise<void> {
this.projectId = id; this.projectId = id;
this.mgmtService.getIAM().then(iam => { this.mgmtService.getIAM().then(iam => {
this.isZitadel = iam.iamProjectId === this.projectId; this.isZitadel = iam.iamProjectId === this.projectId;
}); });
this.mgmtService.getProjectByID(id).then(resp => { this.mgmtService.getProjectByID(id).then(resp => {
if (resp.project) { if (resp.project) {
this.project = resp.project; this.project = resp.project;
} }
}).catch(error => { }).catch(error => {
console.error(error); console.error(error);
this.toast.showError(error); this.toast.showError(error);
}); });
this.loadMembers(); this.loadMembers();
} }
public loadMembers(): void { public loadMembers(): void {
this.loadingSubject.next(true); this.loadingSubject.next(true);
from(this.mgmtService.listProjectMembers(this.projectId, 100, 0)).pipe( from(this.mgmtService.listProjectMembers(this.projectId, 100, 0)).pipe(
map(resp => { map(resp => {
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalMemberResult = resp.details?.totalResult; this.totalMemberResult = resp.details?.totalResult;
} } else {
return resp.resultList; this.totalMemberResult = 0;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
public changeState(newState: ProjectState): void {
if (newState === ProjectState.PROJECT_STATE_ACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.REACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.REACTIVATE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.REACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.mgmtService.reactivateProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_ACTIVE;
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
});
} else if (newState === ProjectState.PROJECT_STATE_INACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DEACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DEACTIVATE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DEACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.mgmtService.deactivateProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_INACTIVE;
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
});
} }
} return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
public deleteProject(): void { public changeState(newState: ProjectState): void {
const dialogRef = this.dialog.open(WarnDialogComponent, { if (newState === ProjectState.PROJECT_STATE_ACTIVE) {
data: { const dialogRef = this.dialog.open(WarnDialogComponent, {
confirmKey: 'ACTIONS.DELETE', data: {
cancelKey: 'ACTIONS.CANCEL', confirmKey: 'ACTIONS.REACTIVATE',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE', cancelKey: 'ACTIONS.CANCEL',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION', titleKey: 'PROJECT.PAGES.DIALOG.REACTIVATE.TITLE',
}, descriptionKey: 'PROJECT.PAGES.DIALOG.REACTIVATE.DESCRIPTION',
width: '400px', },
}); width: '400px',
dialogRef.afterClosed().subscribe(resp => { });
if (resp) { dialogRef.afterClosed().subscribe(resp => {
this.mgmtService.removeProject(this.projectId).then(() => { if (resp) {
this.toast.showInfo('PROJECT.TOAST.DELETED', true); this.mgmtService.reactivateProject(this.projectId).then(() => {
const params: Params = { this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
'deferredReload': true, this.project.state = ProjectState.PROJECT_STATE_ACTIVE;
};
this.router.navigate(['/projects'], { queryParams: params });
}).catch(error => {
this.toast.showError(error);
});
}
});
}
public saveProject(): void {
const req = new UpdateProjectRequest();
req.setId(this.project.id);
req.setName(this.project.name);
req.setProjectRoleAssertion(this.project.projectRoleAssertion);
req.setProjectRoleCheck(this.project.projectRoleCheck);
this.mgmtService.updateProject(req).then(() => {
this.toast.showInfo('PROJECT.TOAST.UPDATED', true);
this.refreshChanges$.emit(); this.refreshChanges$.emit();
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
});
}
});
} else if (newState === ProjectState.PROJECT_STATE_INACTIVE) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DEACTIVATE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DEACTIVATE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DEACTIVATE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.mgmtService.deactivateProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.project.state = ProjectState.PROJECT_STATE_INACTIVE;
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
});
}
}
public deleteProject(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.mgmtService.removeProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
const params: Params = {
'deferredReload': true,
};
this.router.navigate(['/projects'], { queryParams: params });
}).catch(error => {
this.toast.showError(error);
}); });
} }
});
}
public navigateBack(): void { public saveProject(): void {
this._location.back(); const req = new UpdateProjectRequest();
} req.setId(this.project.id);
req.setName(this.project.name);
req.setProjectRoleAssertion(this.project.projectRoleAssertion);
req.setProjectRoleCheck(this.project.projectRoleCheck);
public updateName(): void { this.mgmtService.updateProject(req).then(() => {
this.saveProject(); this.toast.showInfo('PROJECT.TOAST.UPDATED', true);
this.editstate = false; this.refreshChanges$.emit();
} }).catch(error => {
this.toast.showError(error);
});
}
public openAddMember(): void { public navigateBack(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, { this._location.back();
data: { }
creationType: CreationType.PROJECT_OWNED,
projectId: this.project.id,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => { public updateName(): void {
if (resp) { this.saveProject();
const users: User.AsObject[] = resp.users; this.editstate = false;
const roles: string[] = resp.roles; }
if (users && users.length && roles && roles.length) { public openAddMember(): void {
users.forEach(user => { const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
return this.mgmtService.addProjectMember(this.projectId, user.id, roles) data: {
.then(() => { creationType: CreationType.PROJECT_OWNED,
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true); projectId: this.project.id,
setTimeout(() => { },
this.loadMembers(); width: '400px',
}, 1000); });
}).catch(error => {
this.toast.showError(error);
});
});
}
}
});
}
public showDetail(): void { dialogRef.afterClosed().subscribe(resp => {
this.router.navigate(['projects', this.project.id, 'members']); if (resp) {
} const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectMember(this.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadMembers();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
});
}
public showDetail(): void {
this.router.navigate(['projects', this.project.id, 'members']);
}
} }

View File

@ -11,53 +11,55 @@ import { ManagementService } from 'src/app/services/mgmt.service';
* (including sorting, pagination, and filtering). * (including sorting, pagination, and filtering).
*/ */
export class ProjectGrantsDataSource extends DataSource<GrantedProject.AsObject> { export class ProjectGrantsDataSource extends DataSource<GrantedProject.AsObject> {
public totalResult: number = 0; public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public grantsSubject: BehaviorSubject<GrantedProject.AsObject[]> = new BehaviorSubject<GrantedProject.AsObject[]>([]); public grantsSubject: BehaviorSubject<GrantedProject.AsObject[]> = new BehaviorSubject<GrantedProject.AsObject[]>([]);
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();
constructor(private mgmtService: ManagementService) { constructor(private mgmtService: ManagementService) {
super(); super();
} }
public loadGrants(projectId: string, pageIndex: number, pageSize: number, sortDirection?: string): void { public loadGrants(projectId: string, pageIndex: number, pageSize: number, sortDirection?: string): void {
const offset = pageIndex * pageSize; const offset = pageIndex * pageSize;
this.loadingSubject.next(true); this.loadingSubject.next(true);
from(this.mgmtService.listProjectGrants(projectId, pageSize, offset)).pipe( from(this.mgmtService.listProjectGrants(projectId, pageSize, offset)).pipe(
map(resp => { map(resp => {
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult; this.totalResult = resp.details.totalResult;
} } else {
if (resp.details?.viewTimestamp) { this.totalResult = 0;
this.viewTimestamp = resp.details?.viewTimestamp; }
} if (resp.details?.viewTimestamp) {
return resp.resultList; this.viewTimestamp = resp.details?.viewTimestamp;
}), }
catchError(() => of([])), return resp.resultList;
finalize(() => this.loadingSubject.next(false)), }),
).subscribe(grants => { catchError(() => of([])),
this.grantsSubject.next(grants); finalize(() => this.loadingSubject.next(false)),
}); ).subscribe(grants => {
} this.grantsSubject.next(grants);
});
}
/** /**
* Connect this data source to the table. The table will only update when * Connect this data source to the table. The table will only update when
* the returned stream emits new items. * the returned stream emits new items.
* @returns A stream of the items to be rendered. * @returns A stream of the items to be rendered.
*/ */
public connect(): Observable<GrantedProject.AsObject[]> { public connect(): Observable<GrantedProject.AsObject[]> {
return this.grantsSubject.asObservable(); return this.grantsSubject.asObservable();
} }
/** /**
* Called when the table is being destroyed. Use this function, to clean up * Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect. * any open connections or free any held resources that were set up during connect.
*/ */
public disconnect(): void { public disconnect(): void {
this.grantsSubject.complete(); this.grantsSubject.complete();
this.loadingSubject.complete(); this.loadingSubject.complete();
} }
} }

View File

@ -15,173 +15,175 @@ 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: 'app-owned-project-list', selector: 'app-owned-project-list',
templateUrl: './owned-project-list.component.html', templateUrl: './owned-project-list.component.html',
styleUrls: ['./owned-project-list.component.scss'], styleUrls: ['./owned-project-list.component.scss'],
animations: [ animations: [
trigger('list', [ trigger('list', [
transition(':enter', [ transition(':enter', [
query('@animate', query('@animate',
stagger(80, animateChild()), stagger(80, animateChild()),
), ),
]), ]),
]), ]),
trigger('animate', [ trigger('animate', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }), style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })), animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]), ]),
transition(':leave', [ transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }), style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })), animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]), ]),
]), ]),
], ],
}) })
export class OwnedProjectListComponent implements OnInit, OnDestroy { export class OwnedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0; public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public dataSource: MatTableDataSource<Project.AsObject> = public dataSource: MatTableDataSource<Project.AsObject> =
new MatTableDataSource<Project.AsObject>(); new MatTableDataSource<Project.AsObject>();
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public ownedProjectList: Project.AsObject[] = []; public ownedProjectList: Project.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'state', 'creationDate', 'changeDate', 'actions']; public displayedColumns: string[] = ['select', 'name', 'state', 'creationDate', 'changeDate', 'actions'];
public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []); public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []);
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 grid: boolean = true; public grid: boolean = true;
private subscription?: Subscription; private subscription?: Subscription;
public zitadelProjectId: string = ''; public zitadelProjectId: string = '';
constructor(private router: Router, constructor(private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
public translate: TranslateService, public translate: TranslateService,
private mgmtService: ManagementService, private mgmtService: ManagementService,
private toast: ToastService, private toast: ToastService,
private dialog: MatDialog, private dialog: MatDialog,
) { ) {
this.mgmtService.getIAM().then(iam => { this.mgmtService.getIAM().then(iam => {
this.zitadelProjectId = iam.iamProjectId; this.zitadelProjectId = iam.iamProjectId;
}); });
} }
public ngOnInit(): void { public ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe(params => { this.route.queryParams.pipe(take(1)).subscribe(params => {
this.getData(); this.getData();
if (params.deferredReload) { if (params.deferredReload) {
setTimeout(() => { setTimeout(() => {
this.getData(); this.getData();
}, 2000); }, 2000);
} }
}); });
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.subscription?.unsubscribe(); this.subscription?.unsubscribe();
} }
public isAllSelected(): boolean { public isAllSelected(): boolean {
const numSelected = this.selection.selected.length; const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length; const numRows = this.dataSource.data.length;
return numSelected === numRows; return numSelected === numRows;
} }
public masterToggle(): void { public masterToggle(): void {
this.isAllSelected() ? this.isAllSelected() ?
this.selection.clear() : this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row)); this.dataSource.data.forEach(row => this.selection.select(row));
} }
public changePage(event: PageEvent): void { public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageSize * event.pageIndex); this.getData(event.pageSize, event.pageSize * event.pageIndex);
} }
public addProject(): void { public addProject(): void {
this.router.navigate(['/projects', 'create']); this.router.navigate(['/projects', 'create']);
} }
private async getData(limit?: number, offset?: number): Promise<void> { private async getData(limit?: number, offset?: number): Promise<void> {
this.loadingSubject.next(true); this.loadingSubject.next(true);
this.mgmtService.listProjects(limit, offset).then(resp => { this.mgmtService.listProjects(limit, offset).then(resp => {
this.ownedProjectList = resp.resultList; this.ownedProjectList = resp.resultList;
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult; this.totalResult = resp.details.totalResult;
} } else {
if (this.totalResult > 10) { this.totalResult = 0;
this.grid = false; }
} if (this.totalResult > 10) {
if (resp.details?.viewTimestamp) { this.grid = false;
this.viewTimestamp = resp.details?.viewTimestamp; }
} if (resp.details?.viewTimestamp) {
this.dataSource.data = this.ownedProjectList; this.viewTimestamp = resp.details?.viewTimestamp;
this.loadingSubject.next(false); }
this.dataSource.data = this.ownedProjectList;
this.loadingSubject.next(false);
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.loadingSubject.next(false);
});
this.ownedProjectList = [];
}
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.mgmtService.reactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public deactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.mgmtService.deactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public refreshPage(): void {
this.selection.clear();
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public deleteProject(item: Project.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (this.zitadelProjectId && resp && item.id !== this.zitadelProjectId) {
this.mgmtService.removeProject(item.id).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => { }).catch(error => {
console.error(error); this.toast.showError(error);
this.toast.showError(error);
this.loadingSubject.next(false);
}); });
}
this.ownedProjectList = []; });
} }
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.mgmtService.reactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public deactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.mgmtService.deactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public refreshPage(): void {
this.selection.clear();
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public deleteProject(item: Project.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (this.zitadelProjectId && resp && item.id !== this.zitadelProjectId) {
this.mgmtService.removeProject(item.id).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
} }

View File

@ -5,7 +5,7 @@
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="form"> <form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="form">
<div class="content"> <div class="content">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required /> <input cnslInput formControlName="userName" required />
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required"> <span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
@ -15,7 +15,7 @@
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="name" required /> <input cnslInput formControlName="name" required />
<span cnsl-error *ngIf="name?.invalid && name?.errors?.required"> <span cnsl-error *ngIf="name?.invalid && name?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}

View File

@ -5,7 +5,7 @@
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="form"> <form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="form">
<div class="content"> <div class="content">
<p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p> <p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<input cnslInput matRipple formControlName="email" required /> <input cnslInput matRipple formControlName="email" required />
<span cnsl-error *ngIf="email?.invalid && !email?.errors?.required"> <span cnsl-error *ngIf="email?.invalid && !email?.errors?.required">
@ -15,7 +15,7 @@
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required <input cnslInput formControlName="userName" required
[ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" /> [ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" />
@ -30,21 +30,21 @@
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<div class="content"> <div class="content">
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="firstName" required /> <input cnslInput formControlName="firstName" required />
<span cnsl-error *ngIf="firstName?.invalid && firstName?.errors?.required"> <span cnsl-error *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="lastName" required /> <input cnslInput formControlName="lastName" required />
<span cnsl-error *ngIf="lastName?.invalid && lastName?.errors?.required"> <span cnsl-error *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="nickName" /> <input cnslInput formControlName="nickName" />
<span cnsl-error *ngIf="nickName?.invalid && nickName?.errors?.required"> <span cnsl-error *ngIf="nickName?.invalid && nickName?.errors?.required">
@ -54,7 +54,7 @@
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p> <p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
<mat-select formControlName="gender"> <mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender"> <mat-option *ngFor="let gender of genders" [value]="gender">
@ -65,7 +65,7 @@
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</cnsl-label>
<mat-select formControlName="preferredLanguage"> <mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language"> <mat-option *ngFor="let language of languages" [value]="language">
@ -79,7 +79,7 @@
<p class="section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p> <p class="section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
<cnsl-form-field class="formfield" appearance="fill"> <cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
<input cnslInput formControlName="phone" /> <input cnslInput formControlName="phone" />
<span cnsl-error *ngIf="phone?.invalid && phone?.errors?.required"> <span cnsl-error *ngIf="phone?.invalid && phone?.errors?.required">

View File

@ -13,109 +13,111 @@ import { ManagementService } from '../../../../services/mgmt.service';
import { ToastService } from '../../../../services/toast.service'; import { ToastService } from '../../../../services/toast.service';
@Component({ @Component({
selector: 'app-external-idps', selector: 'app-external-idps',
templateUrl: './external-idps.component.html', templateUrl: './external-idps.component.html',
styleUrls: ['./external-idps.component.scss'], styleUrls: ['./external-idps.component.scss'],
}) })
export class ExternalIdpsComponent implements OnInit { export class ExternalIdpsComponent implements OnInit {
@Input() service!: GrpcAuthService | ManagementService; @Input() service!: GrpcAuthService | ManagementService;
@Input() userId!: string; @Input() userId!: string;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public totalResult: number = 0; public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public dataSource: MatTableDataSource<IDPUserLink.AsObject> public dataSource: MatTableDataSource<IDPUserLink.AsObject>
= new MatTableDataSource<IDPUserLink.AsObject>(); = new MatTableDataSource<IDPUserLink.AsObject>();
public selection: SelectionModel<IDPUserLink.AsObject> public selection: SelectionModel<IDPUserLink.AsObject>
= new SelectionModel<IDPUserLink.AsObject>(true, []); = new SelectionModel<IDPUserLink.AsObject>(true, []);
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();
@Input() public displayedColumns: string[] = ['idpConfigId', 'idpName', 'externalUserId', 'externalUserDisplayName', 'actions']; @Input() public displayedColumns: string[] = ['idpConfigId', 'idpName', 'externalUserId', 'externalUserDisplayName', 'actions'];
constructor(private toast: ToastService, private dialog: MatDialog) { } constructor(private toast: ToastService, private dialog: MatDialog) { }
ngOnInit(): void { ngOnInit(): void {
this.getData(10, 0); this.getData(10, 0);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex * event.pageSize);
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
let promise;
if (this.service instanceof ManagementService) {
promise = (this.service as ManagementService).listHumanLinkedIDPs(this.userId, limit, offset);
} else if (this.service instanceof GrpcAuthService) {
promise = (this.service as GrpcAuthService).listMyLinkedIDPs(limit, offset);
} }
public isAllSelected(): boolean { if (promise) {
const numSelected = this.selection.selected.length; promise.then(resp => {
const numRows = this.dataSource.data.length; this.dataSource.data = resp.resultList;
return numSelected === numRows; if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details.viewTimestamp;
}
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
} else {
this.totalResult = 0;
}
this.loadingSubject.next(false);
}).catch((error: any) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} }
}
public masterToggle(): void { public refreshPage(): void {
this.isAllSelected() ? this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
this.selection.clear() : }
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void { public removeExternalIdp(idp: IDPUserLink.AsObject): void {
this.getData(event.pageSize, event.pageIndex * event.pageSize); const dialogRef = this.dialog.open(WarnDialogComponent, {
} data: {
confirmKey: 'ACTIONS.REMOVE',
private async getData(limit: number, offset: number): Promise<void> { cancelKey: 'ACTIONS.CANCEL',
this.loadingSubject.next(true); titleKey: 'USER.EXTERNALIDP.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.EXTERNALIDP.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
let promise; let promise;
if (this.service instanceof ManagementService) { if (this.service instanceof ManagementService) {
promise = (this.service as ManagementService).listHumanLinkedIDPs(this.userId, limit, offset); promise = (this.service as ManagementService)
.removeHumanLinkedIDP(idp.providedUserId, idp.idpId, idp.userId);
} else if (this.service instanceof GrpcAuthService) { } else if (this.service instanceof GrpcAuthService) {
promise = (this.service as GrpcAuthService).listMyLinkedIDPs(limit, offset); promise = (this.service as GrpcAuthService)
.removeMyLinkedIDP(idp.providedUserId, idp.idpId);
} }
if (promise) { if (promise) {
promise.then(resp => { promise.then(_ => {
this.dataSource.data = resp.resultList; setTimeout(() => {
if (resp.details?.viewTimestamp) { this.refreshPage();
this.viewTimestamp = resp.details.viewTimestamp; }, 1000);
} }).catch((error: any) => {
if (resp.details?.totalResult) { this.toast.showError(error);
this.totalResult = resp.details?.totalResult; });
}
this.loadingSubject.next(false);
}).catch((error: any) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} }
} }
});
public refreshPage(): void { }
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public removeExternalIdp(idp: IDPUserLink.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.REMOVE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.EXTERNALIDP.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.EXTERNALIDP.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
let promise;
if (this.service instanceof ManagementService) {
promise = (this.service as ManagementService)
.removeHumanLinkedIDP(idp.providedUserId, idp.idpId, idp.userId);
} else if (this.service instanceof GrpcAuthService) {
promise = (this.service as GrpcAuthService)
.removeMyLinkedIDP(idp.providedUserId, idp.idpId);
}
if (promise) {
promise.then(_ => {
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch((error: any) => {
this.toast.showError(error);
});
}
}
});
}
} }

View File

@ -63,6 +63,7 @@ export class MembershipsComponent implements OnInit {
} else { } else {
this.mgmtService.listUserMemberships(userId, 100, 0, []).then(resp => { this.mgmtService.listUserMemberships(userId, 100, 0, []).then(resp => {
this.memberships = resp.resultList; this.memberships = resp.resultList;
this.totalResult = resp.details?.totalResult || 0;
this.loading = false; this.loading = false;
}); });
} }

View File

@ -19,7 +19,7 @@
(click)="changeState(UserState.USER_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' | (click)="changeState(UserState.USER_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' |
translate}}</button> translate}}</button>
<button class="state-button" mat-stroked-button color="warn" <button class="state-button" mat-stroked-button color="warn"
*ngIf="user?.state !== UserState.USER_STATE_ACTIVE" *ngIf="user?.state == UserState.USER_STATE_INACTIVE"
(click)="changeState(UserState.USER_STATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' | translate}}</button> (click)="changeState(UserState.USER_STATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' | translate}}</button>
</ng-template> </ng-template>
</div> </div>

View File

@ -12,242 +12,244 @@ import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.com
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb'; import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { import {
DisplayNameQuery, DisplayNameQuery,
EmailQuery, EmailQuery,
FirstNameQuery, FirstNameQuery,
LastNameQuery, LastNameQuery,
SearchQuery, SearchQuery,
Type, Type,
TypeQuery, TypeQuery,
User, User,
UserNameQuery, UserNameQuery,
UserState, UserState,
} from 'src/app/proto/generated/zitadel/user_pb'; } 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';
enum UserListSearchKey { enum UserListSearchKey {
FIRST_NAME, FIRST_NAME,
LAST_NAME, LAST_NAME,
DISPLAY_NAME, DISPLAY_NAME,
USER_NAME, USER_NAME,
EMAIL, EMAIL,
} }
@Component({ @Component({
selector: 'app-user-table', selector: 'app-user-table',
templateUrl: './user-table.component.html', templateUrl: './user-table.component.html',
styleUrls: ['./user-table.component.scss'], styleUrls: ['./user-table.component.scss'],
animations: [ animations: [
enterAnimations, enterAnimations,
], ],
}) })
export class UserTableComponent implements OnInit { export class UserTableComponent implements OnInit {
public userSearchKey: UserListSearchKey | undefined = undefined; public userSearchKey: UserListSearchKey | undefined = undefined;
public Type: any = Type; public Type: any = Type;
@Input() type: Type = Type.TYPE_HUMAN; @Input() type: Type = Type.TYPE_HUMAN;
@Input() refreshOnPreviousRoutes: string[] = []; @Input() refreshOnPreviousRoutes: string[] = [];
@Input() disabled: boolean = false; @Input() disabled: boolean = false;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild('input') public filter!: Input; @ViewChild('input') public filter!: Input;
public viewTimestamp!: Timestamp.AsObject; public viewTimestamp!: Timestamp.AsObject;
public totalResult: number = 0; public totalResult: number = 0;
public dataSource: MatTableDataSource<User.AsObject> = new MatTableDataSource<User.AsObject>(); public dataSource: MatTableDataSource<User.AsObject> = new MatTableDataSource<User.AsObject>();
public selection: SelectionModel<User.AsObject> = new SelectionModel<User.AsObject>(true, []); public selection: SelectionModel<User.AsObject> = new SelectionModel<User.AsObject>(true, []);
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();
@Input() public displayedColumns: string[] = ['select', 'displayName', 'username', 'email', 'state', 'actions']; @Input() public displayedColumns: string[] = ['select', 'displayName', 'username', 'email', 'state', 'actions'];
@Output() public changedSelection: EventEmitter<Array<User.AsObject>> = new EventEmitter(); @Output() public changedSelection: EventEmitter<Array<User.AsObject>> = new EventEmitter();
public UserState: any = UserState; public UserState: any = UserState;
public UserListSearchKey: any = UserListSearchKey; public UserListSearchKey: any = UserListSearchKey;
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private userService: ManagementService, private userService: ManagementService,
private toast: ToastService, private toast: ToastService,
private dialog: MatDialog, private dialog: MatDialog,
private route: ActivatedRoute, private route: ActivatedRoute,
) { ) {
this.selection.changed.subscribe(() => { this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected); this.changedSelection.emit(this.selection.selected);
}); });
} }
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe(params => { this.route.queryParams.pipe(take(1)).subscribe(params => {
this.getData(10, 0, this.type); this.getData(10, 0, this.type);
if (params.deferredReload) { if (params.deferredReload) {
setTimeout(() => {
this.getData(10, 0, this.type);
}, 2000);
}
});
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.selection.clear();
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type);
}
public deactivateSelectedUsers(): void {
Promise.all(this.selection.selected.map(value => {
return this.userService.deactivateUser(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.selection.clear();
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
public reactivateSelectedUsers(): void {
Promise.all(this.selection.selected.map(value => {
return this.userService.reactivateUser(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
this.selection.clear();
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
private async getData(limit: number, offset: number, type: Type, searchValue?: string): Promise<void> {
this.loadingSubject.next(true);
const query = new SearchQuery();
const typeQuery = new TypeQuery();
typeQuery.setType(type);
query.setTypeQuery(typeQuery);
if (searchValue && this.userSearchKey !== undefined) {
switch (this.userSearchKey) {
case UserListSearchKey.DISPLAY_NAME:
const dNQuery = new DisplayNameQuery();
dNQuery.setDisplayName(searchValue);
dNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setDisplayNameQuery(dNQuery);
break;
case UserListSearchKey.USER_NAME:
const uNQuery = new UserNameQuery();
uNQuery.setUserName(searchValue);
uNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setUserNameQuery(uNQuery);
break;
case UserListSearchKey.FIRST_NAME:
const fNQuery = new FirstNameQuery();
fNQuery.setFirstName(searchValue);
fNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setFirstNameQuery(fNQuery);
break;
case UserListSearchKey.FIRST_NAME:
const lNQuery = new LastNameQuery();
lNQuery.setLastName(searchValue);
lNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setLastNameQuery(lNQuery);
break;
case UserListSearchKey.EMAIL:
const eQuery = new EmailQuery();
eQuery.setEmailAddress(searchValue);
eQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setEmailQuery(eQuery);
break;
}
}
this.userService.listUsers(limit, offset, [query]).then(resp => {
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
}
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details?.viewTimestamp;
}
this.dataSource.data = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public refreshPage(): void {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
}
public applyFilter(event: Event): void {
this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value;
this.getData(
this.paginator.pageSize,
this.paginator.pageIndex * this.paginator.pageSize,
this.type,
filterValue,
);
}
public setFilter(key: UserListSearchKey): void {
setTimeout(() => { setTimeout(() => {
if (this.filter) { this.getData(10, 0, this.type);
(this.filter as any).nativeElement.focus(); }, 2000);
} }
}, 100); });
}
if (this.userSearchKey !== key) { public isAllSelected(): boolean {
this.userSearchKey = key; const numSelected = this.selection.selected.length;
} else { const numRows = this.dataSource.data.length;
this.userSearchKey = undefined; return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.selection.clear();
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type);
}
public deactivateSelectedUsers(): void {
Promise.all(this.selection.selected.map(value => {
return this.userService.deactivateUser(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.selection.clear();
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
public reactivateSelectedUsers(): void {
Promise.all(this.selection.selected.map(value => {
return this.userService.reactivateUser(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
this.selection.clear();
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
private async getData(limit: number, offset: number, type: Type, searchValue?: string): Promise<void> {
this.loadingSubject.next(true);
const query = new SearchQuery();
const typeQuery = new TypeQuery();
typeQuery.setType(type);
query.setTypeQuery(typeQuery);
if (searchValue && this.userSearchKey !== undefined) {
switch (this.userSearchKey) {
case UserListSearchKey.DISPLAY_NAME:
const dNQuery = new DisplayNameQuery();
dNQuery.setDisplayName(searchValue);
dNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setDisplayNameQuery(dNQuery);
break;
case UserListSearchKey.USER_NAME:
const uNQuery = new UserNameQuery();
uNQuery.setUserName(searchValue);
uNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setUserNameQuery(uNQuery);
break;
case UserListSearchKey.FIRST_NAME:
const fNQuery = new FirstNameQuery();
fNQuery.setFirstName(searchValue);
fNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setFirstNameQuery(fNQuery);
break;
case UserListSearchKey.FIRST_NAME:
const lNQuery = new LastNameQuery();
lNQuery.setLastName(searchValue);
lNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setLastNameQuery(lNQuery);
break;
case UserListSearchKey.EMAIL:
const eQuery = new EmailQuery();
eQuery.setEmailAddress(searchValue);
eQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setEmailQuery(eQuery);
break;
}
}
this.userService.listUsers(limit, offset, [query]).then(resp => {
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
} else {
this.totalResult = 0;
}
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details?.viewTimestamp;
}
this.dataSource.data = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public refreshPage(): void {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
}
public applyFilter(event: Event): void {
this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value;
this.getData(
this.paginator.pageSize,
this.paginator.pageIndex * this.paginator.pageSize,
this.type,
filterValue,
);
}
public setFilter(key: UserListSearchKey): void {
setTimeout(() => {
if (this.filter) {
(this.filter as any).nativeElement.focus();
}
}, 100);
if (this.userSearchKey !== key) {
this.userSearchKey = key;
} else {
this.userSearchKey = undefined;
this.refreshPage();
}
}
public deleteUser(user: User.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.userService.removeUser(user.id).then(() => {
setTimeout(() => {
this.refreshPage(); this.refreshPage();
} }, 1000);
} this.toast.showInfo('USER.TOAST.DELETED', true);
}).catch(error => {
public deleteUser(user: User.AsObject): void { this.toast.showError(error);
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
}); });
}
dialogRef.afterClosed().subscribe(resp => { });
if (resp) { }
this.userService.removeUser(user.id).then(() => {
setTimeout(() => {
this.refreshPage();
}, 1000);
this.toast.showInfo('USER.TOAST.DELETED', true);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
} }

View File

@ -1078,6 +1078,7 @@
"TITLE": "OIDC-Konfiguration", "TITLE": "OIDC-Konfiguration",
"CLIENTID": "Client ID", "CLIENTID": "Client ID",
"CLIENTSECRET": "Client Secret", "CLIENTSECRET": "Client Secret",
"CLIENTSECRET_NOSECRET":"Bei ihrem gewählten Authentication Flow wird kein Secret benötigt und steht daher nicht zur Verfügung.",
"CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird.", "CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird.",
"REGENERATESECRET": "Client Secret neu generieren", "REGENERATESECRET": "Client Secret neu generieren",
"DEVMODE": "Entwicklermodus", "DEVMODE": "Entwicklermodus",

View File

@ -1079,6 +1079,7 @@
"TITLE": "OIDC Configuration", "TITLE": "OIDC Configuration",
"CLIENTID": "Client ID", "CLIENTID": "Client ID",
"CLIENTSECRET": "Client Secret", "CLIENTSECRET": "Client Secret",
"CLIENTSECRET_NOSECRET":"With your chosen authentication flow, no secret is required and is therefore not available.",
"CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed.", "CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed.",
"REGENERATESECRET": "Regenerate Client Secret", "REGENERATESECRET": "Regenerate Client Secret",
"DEVMODE": "Development Mode", "DEVMODE": "Development Mode",