Migrated users table in Admin Control to mat-table (#2469)
* Migrated users table in Admin Control to mat-table * Update changelog
This commit is contained in:
parent
73ac4b4197
commit
c9878c9050
@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Added a chart to the account detail dialog
|
- Added a chart to the account detail dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed the users table in the admin control panel to an `@angular/material` data table
|
||||||
|
|
||||||
## 2.12.0 - 2023-10-17
|
## 2.12.0 - 2023-10-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
@ -20,13 +21,15 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-users.html'
|
templateUrl: './admin-users.html'
|
||||||
})
|
})
|
||||||
export class AdminUsersComponent implements OnDestroy, OnInit {
|
export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||||
|
public dataSource: MatTableDataSource<AdminData['users'][0]> =
|
||||||
|
new MatTableDataSource();
|
||||||
public defaultDateFormat: string;
|
public defaultDateFormat: string;
|
||||||
|
public displayedColumns: string[] = [];
|
||||||
public getEmojiFlag = getEmojiFlag;
|
public getEmojiFlag = getEmojiFlag;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToImpersonateAllUsers: boolean;
|
public hasPermissionToImpersonateAllUsers: boolean;
|
||||||
public info: InfoItem;
|
public info: InfoItem;
|
||||||
public user: User;
|
public user: User;
|
||||||
public users: AdminData['users'];
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -44,6 +47,29 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
|||||||
permissions.enableSubscription
|
permissions.enableSubscription
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.hasPermissionForSubscription) {
|
||||||
|
this.displayedColumns = [
|
||||||
|
'index',
|
||||||
|
'user',
|
||||||
|
'country',
|
||||||
|
'registration',
|
||||||
|
'accounts',
|
||||||
|
'activities',
|
||||||
|
'engagementPerDay',
|
||||||
|
'lastRequest',
|
||||||
|
'actions'
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this.displayedColumns = [
|
||||||
|
'index',
|
||||||
|
'user',
|
||||||
|
'registration',
|
||||||
|
'accounts',
|
||||||
|
'activities',
|
||||||
|
'actions'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -118,7 +144,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
|
|||||||
.fetchAdminData()
|
.fetchAdminData()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ users }) => {
|
.subscribe(({ users }) => {
|
||||||
this.users = users;
|
this.dataSource = new MatTableDataSource(users);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -2,136 +2,232 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="users">
|
<div class="users">
|
||||||
<table class="gf-table">
|
<table class="gf-table" mat-table [dataSource]="dataSource">
|
||||||
<thead>
|
<ng-container matColumnDef="index">
|
||||||
<tr class="mat-mdc-header-row">
|
<th
|
||||||
<th class="mat-mdc-header-cell px-1 py-2 text-right">#</th>
|
*matHeaderCellDef
|
||||||
<th class="mat-mdc-header-cell px-1 py-2" i18n>User</th>
|
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||||
<th
|
mat-header-cell
|
||||||
*ngIf="hasPermissionForSubscription"
|
|
||||||
class="mat-mdc-header-cell px-1 py-2"
|
|
||||||
>
|
|
||||||
<ng-container i18n>Country</ng-container>
|
|
||||||
</th>
|
|
||||||
<th class="mat-mdc-header-cell px-1 py-2">
|
|
||||||
<ng-container i18n>Registration</ng-container>
|
|
||||||
</th>
|
|
||||||
<th class="mat-mdc-header-cell px-1 py-2 text-right">
|
|
||||||
<ng-container i18n>Accounts</ng-container>
|
|
||||||
</th>
|
|
||||||
<th class="mat-mdc-header-cell px-1 py-2 text-right">
|
|
||||||
<ng-container i18n>Activities</ng-container>
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
*ngIf="hasPermissionForSubscription"
|
|
||||||
class="mat-mdc-header-cell px-1 py-2 text-right"
|
|
||||||
>
|
|
||||||
<ng-container i18n>Engagement per Day</ng-container>
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
*ngIf="hasPermissionForSubscription"
|
|
||||||
class="mat-mdc-header-cell px-1 py-2"
|
|
||||||
i18n
|
|
||||||
>
|
|
||||||
Last Request
|
|
||||||
</th>
|
|
||||||
<th class="mat-mdc-header-cell px-1 py-2"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
*ngFor="let userItem of users; let i = index"
|
|
||||||
class="mat-mdc-row"
|
|
||||||
>
|
>
|
||||||
<td class="mat-mdc-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
#
|
||||||
<td class="mat-mdc-cell px-1 py-2">
|
</th>
|
||||||
<div class="d-flex align-items-center">
|
<td
|
||||||
<span class="d-none d-sm-inline-block text-monospace"
|
*matCellDef="let element; let i=index"
|
||||||
>{{ userItem.id }}</span
|
class="mat-mdc-cell px-1 py-2 text-right"
|
||||||
>
|
mat-cell
|
||||||
<span class="d-inline-block d-sm-none text-monospace"
|
>
|
||||||
>{{ (userItem.id | slice:0:5) + '...' }}</span
|
{{ i + 1 }}
|
||||||
>
|
</td>
|
||||||
<gf-premium-indicator
|
</ng-container>
|
||||||
*ngIf="userItem?.subscription?.type === 'Premium'"
|
|
||||||
class="ml-1"
|
<ng-container matColumnDef="user">
|
||||||
[enableLink]="false"
|
<th
|
||||||
[title]="'Expires ' + formatDistanceToNow(userItem.subscription.expiresAt) + ' (' + (userItem.subscription.expiresAt | date: defaultDateFormat) + ')'"
|
*matHeaderCellDef
|
||||||
></gf-premium-indicator>
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
</div>
|
i18n
|
||||||
</td>
|
mat-header-cell
|
||||||
<td
|
>
|
||||||
*ngIf="hasPermissionForSubscription"
|
User
|
||||||
class="mat-mdc-cell px-1 py-2"
|
</th>
|
||||||
>
|
<td
|
||||||
<span class="h5" [title]="userItem.country"
|
*matCellDef="let element"
|
||||||
>{{ getEmojiFlag(userItem.country) }}</span
|
class="mat-mdc-cell px-1 py-2"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="d-none d-sm-inline-block text-monospace"
|
||||||
|
>{{ element.id }}</span
|
||||||
>
|
>
|
||||||
</td>
|
<span class="d-inline-block d-sm-none text-monospace"
|
||||||
<td class="mat-mdc-cell px-1 py-2">
|
>{{ (element.id | slice:0:5) + '...' }}</span
|
||||||
{{ formatDistanceToNow(userItem.createdAt) }}
|
>
|
||||||
</td>
|
<gf-premium-indicator
|
||||||
<td class="mat-mdc-cell px-1 py-2 text-right">
|
*ngIf="element?.subscription?.type === 'Premium'"
|
||||||
<gf-value
|
class="ml-1"
|
||||||
class="d-inline-block justify-content-end"
|
[enableLink]="false"
|
||||||
[locale]="user?.settings?.locale"
|
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'"
|
||||||
[value]="userItem.accountCount"
|
></gf-premium-indicator>
|
||||||
></gf-value>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-mdc-cell px-1 py-2 text-right">
|
</ng-container>
|
||||||
<gf-value
|
|
||||||
class="d-inline-block justify-content-end"
|
<ng-container
|
||||||
[locale]="user?.settings?.locale"
|
*ngIf="hasPermissionForSubscription"
|
||||||
[value]="userItem.transactionCount"
|
matColumnDef="country"
|
||||||
></gf-value>
|
>
|
||||||
</td>
|
<th
|
||||||
<td
|
*matHeaderCellDef
|
||||||
*ngIf="hasPermissionForSubscription"
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
class="mat-mdc-cell px-1 py-2 text-right"
|
mat-header-cell
|
||||||
|
>
|
||||||
|
<ng-container i18n>Country</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<span class="h5" [title]="element.country"
|
||||||
|
>{{ getEmojiFlag(element.country) }}</span
|
||||||
>
|
>
|
||||||
<gf-value
|
</td>
|
||||||
class="d-inline-block justify-content-end"
|
</ng-container>
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[precision]="0"
|
<ng-container matColumnDef="registration">
|
||||||
[value]="userItem.engagement"
|
<th
|
||||||
></gf-value>
|
*matHeaderCellDef
|
||||||
</td>
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
<td
|
mat-header-cell
|
||||||
*ngIf="hasPermissionForSubscription"
|
>
|
||||||
class="mat-mdc-cell px-1 py-2"
|
<ng-container i18n>Registration</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
{{ formatDistanceToNow(element.createdAt) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="accounts">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
<ng-container i18n>Accounts</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2 text-right"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="element.accountCount"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="activities">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
<ng-container i18n>Activities</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2 text-right"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="element.transactionCount"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container
|
||||||
|
*ngIf="hasPermissionForSubscription"
|
||||||
|
matColumnDef="engagementPerDay"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
<ng-container i18n>Engagement per Day</ng-container>
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2 text-right"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[precision]="0"
|
||||||
|
[value]="element.engagement"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container
|
||||||
|
*ngIf="hasPermissionForSubscription"
|
||||||
|
matColumnDef="lastRequest"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
Last Request
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
{{ formatDistanceToNow(element.lastActivity) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="mat-mdc-header-cell px-1 py-2"
|
||||||
|
mat-header-cell
|
||||||
|
></th>
|
||||||
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="mat-mdc-cell px-1 py-2"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="userMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
{{ formatDistanceToNow(userItem.lastActivity) }}
|
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||||
</td>
|
</button>
|
||||||
<td class="mat-mdc-cell px-1 py-2">
|
<mat-menu #userMenu="matMenu" xPosition="before">
|
||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
*ngIf="hasPermissionToImpersonateAllUsers"
|
||||||
mat-button
|
mat-menu-item
|
||||||
[matMenuTriggerFor]="userMenu"
|
(click)="onImpersonateUser(element.id)"
|
||||||
(click)="$event.stopPropagation()"
|
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
<ion-icon class="mr-2" name="contract-outline"></ion-icon>
|
||||||
|
<span i18n>Impersonate User</span>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #userMenu="matMenu" xPosition="before">
|
<button
|
||||||
<button
|
mat-menu-item
|
||||||
*ngIf="hasPermissionToImpersonateAllUsers"
|
[disabled]="element.id === user?.id"
|
||||||
mat-menu-item
|
(click)="onDeleteUser(element.id)"
|
||||||
(click)="onImpersonateUser(userItem.id)"
|
>
|
||||||
>
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
<ion-icon class="mr-2" name="contract-outline"></ion-icon>
|
<span i18n>Delete User</span>
|
||||||
<span i18n>Impersonate User</span>
|
</button>
|
||||||
</button>
|
</mat-menu>
|
||||||
<button
|
</td>
|
||||||
mat-menu-item
|
</ng-container>
|
||||||
[disabled]="userItem.id === user?.id"
|
|
||||||
(click)="onDeleteUser(userItem.id)"
|
<tr
|
||||||
>
|
*matHeaderRowDef="displayedColumns"
|
||||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
class="mat-mdc-header-row"
|
||||||
<span i18n>Delete User</span>
|
mat-header-row
|
||||||
</button>
|
></tr>
|
||||||
</mat-menu>
|
<tr
|
||||||
</td>
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
</tr>
|
class="mat-mdc-row"
|
||||||
</tbody>
|
mat-row
|
||||||
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
@ -15,7 +16,8 @@ import { AdminUsersComponent } from './admin-users.component';
|
|||||||
GfPremiumIndicatorModule,
|
GfPremiumIndicatorModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule
|
MatMenuModule,
|
||||||
|
MatTableModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user