Browse Source

Feature/set up user detail dialog in admin control panel (#5819)

* Set up user detail dialog

* Update changelog
pull/5835/head
Harsh Santwani 6 days ago
committed by GitHub
parent
commit
482b97ba9b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 70
      apps/client/src/app/components/admin-users/admin-users.component.ts
  3. 12
      apps/client/src/app/components/admin-users/admin-users.html
  4. 7
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  5. 7
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.scss
  6. 52
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  7. 32
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Extended the export functionality by the user account’s performance calculation type - Extended the export functionality by the user account’s performance calculation type
- Added a user detail dialog to the users section of the admin control panel
### Changed ### Changed

70
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -19,6 +19,7 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { import {
MatPaginator, MatPaginator,
@ -26,6 +27,7 @@ import {
PageEvent PageEvent
} from '@angular/material/paginator'; } from '@angular/material/paginator';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { import {
differenceInSeconds, differenceInSeconds,
@ -37,8 +39,10 @@ import {
contractOutline, contractOutline,
ellipsisHorizontal, ellipsisHorizontal,
keyOutline, keyOutline,
personOutline,
trashOutline trashOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -49,6 +53,8 @@ import { AdminService } from '../../services/admin.service';
import { DataService } from '../../services/data.service'; import { DataService } from '../../services/data.service';
import { ImpersonationStorageService } from '../../services/impersonation-storage.service'; import { ImpersonationStorageService } from '../../services/impersonation-storage.service';
import { UserService } from '../../services/user/user.service'; import { UserService } from '../../services/user/user.service';
import { UserDetailDialogParams } from '../user-detail-dialog/interfaces/interfaces';
import { GfUserDetailDialogComponent } from '../user-detail-dialog/user-detail-dialog.component';
@Component({ @Component({
imports: [ imports: [
@ -71,6 +77,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
public dataSource = new MatTableDataSource<AdminUsers['users'][0]>(); public dataSource = new MatTableDataSource<AdminUsers['users'][0]>();
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string;
public displayedColumns: string[] = []; public displayedColumns: string[] = [];
public getEmojiFlag = getEmojiFlag; public getEmojiFlag = getEmojiFlag;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
@ -87,11 +94,16 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission( this.hasPermissionForSubscription = hasPermission(
@ -121,6 +133,14 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
]; ];
} }
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['userDetailDialog'] && params['userId']) {
this.openUserDetailDialog(params['userId']);
}
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -138,7 +158,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
} }
}); });
addIcons({ contractOutline, ellipsisHorizontal, keyOutline, trashOutline }); addIcons({
contractOutline,
ellipsisHorizontal,
keyOutline,
personOutline,
trashOutline
});
} }
public ngOnInit() { public ngOnInit() {
@ -161,6 +187,12 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
return ''; return '';
} }
public onChangePage(page: PageEvent) {
this.fetchUsers({
pageIndex: page.pageIndex
});
}
public onDeleteUser(aId: string) { public onDeleteUser(aId: string) {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
@ -212,9 +244,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
window.location.reload(); window.location.reload();
} }
public onChangePage(page: PageEvent) { public onOpenUserDetailDialog(userId: string) {
this.fetchUsers({ this.router.navigate([], {
pageIndex: page.pageIndex queryParams: { userId, userDetailDialog: true }
}); });
} }
@ -245,4 +277,34 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
private openUserDetailDialog(userId: string) {
const userData = this.dataSource.data.find(({ id }) => {
return id === userId;
});
if (!userData) {
this.router.navigate(['.'], { relativeTo: this.route });
return;
}
const dialogRef = this.dialog.open(GfUserDetailDialogComponent, {
autoFocus: false,
data: {
userData,
deviceType: this.deviceType,
locale: this.user?.settings?.locale
} as UserDetailDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : '60vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.fetchUsers();
this.router.navigate(['.'], { relativeTo: this.route });
});
}
} }

12
apps/client/src/app/components/admin-users/admin-users.html

@ -216,6 +216,15 @@
<ion-icon name="ellipsis-horizontal" /> <ion-icon name="ellipsis-horizontal" />
</button> </button>
<mat-menu #userMenu="matMenu" xPosition="before"> <mat-menu #userMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onOpenUserDetailDialog(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="person-outline" />
<span i18n>View Details</span>
</span>
</button>
@if (hasPermissionToImpersonateAllUsers) { @if (hasPermissionToImpersonateAllUsers) {
<button mat-menu-item (click)="onImpersonateUser(element.id)"> <button mat-menu-item (click)="onImpersonateUser(element.id)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -255,8 +264,9 @@
></tr> ></tr>
<tr <tr
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
class="mat-mdc-row" class="cursor-pointer mat-mdc-row"
mat-row mat-row
(click)="onOpenUserDetailDialog(row.id)"
></tr> ></tr>
</table> </table>
</div> </div>

7
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -0,0 +1,7 @@
import { AdminUsers } from '@ghostfolio/common/interfaces';
export interface UserDetailDialogParams {
deviceType: string;
locale: string;
userData: AdminUsers['users'][0];
}

7
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.scss

@ -0,0 +1,7 @@
:host {
display: block;
.mat-mdc-dialog-content {
max-height: unset;
}
}

52
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -0,0 +1,52 @@
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnDestroy
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { UserDetailDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfValueComponent,
MatButtonModule,
MatDialogModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-user-detail-dialog',
styleUrls: ['./user-detail-dialog.component.scss'],
templateUrl: './user-detail-dialog.html'
})
export class GfUserDetailDialogComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
public constructor(
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams,
public dialogRef: MatDialogRef<GfUserDetailDialogComponent>
) {}
public onClose() {
this.dialogRef.close();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

32
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -0,0 +1,32 @@
<gf-dialog-header
position="center"
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="mb-3 row">
<div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="data.userData.id"
>User ID</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[isDate]="true"
[locale]="data.locale"
[value]="data.userData.createdAt"
>Registration Date</gf-value
>
</div>
</div>
</div>
</div>
<gf-dialog-footer
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>
Loading…
Cancel
Save