From a24a094407e6ac1c92af9e930d3cae368dd19eb0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 28 Nov 2021 12:34:10 +0100 Subject: [PATCH] Feature/introduce tabs to admin control panel (#494) * Add tabs * Update changelog --- CHANGELOG.md | 6 + .../admin-overview.component.ts | 150 +++++++++++++ .../admin-overview/admin-overview.html | 107 +++++++++ .../admin-overview/admin-overview.module.ts | 17 ++ .../admin-overview/admin-overview.scss | 17 ++ .../admin-users/admin-users.component.ts | 82 +++++++ .../components/admin-users/admin-users.html | 86 +++++++ .../admin-users/admin-users.module.ts | 14 ++ .../components/admin-users/admin-users.scss | 18 ++ .../pages/admin/admin-page-routing.module.ts | 13 +- .../app/pages/admin/admin-page.component.ts | 149 +----------- .../src/app/pages/admin/admin-page.html | 212 ++---------------- .../src/app/pages/admin/admin-page.module.ts | 8 +- .../src/app/pages/admin/admin-page.scss | 38 ++-- 14 files changed, 556 insertions(+), 361 deletions(-) create mode 100644 apps/client/src/app/components/admin-overview/admin-overview.component.ts create mode 100644 apps/client/src/app/components/admin-overview/admin-overview.html create mode 100644 apps/client/src/app/components/admin-overview/admin-overview.module.ts create mode 100644 apps/client/src/app/components/admin-overview/admin-overview.scss create mode 100644 apps/client/src/app/components/admin-users/admin-users.component.ts create mode 100644 apps/client/src/app/components/admin-users/admin-users.html create mode 100644 apps/client/src/app/components/admin-users/admin-users.module.ts create mode 100644 apps/client/src/app/components/admin-users/admin-users.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0321da8..8879ea365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added tabs to the admin control panel + ## 1.81.0 - 27.11.2021 ### Added diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts new file mode 100644 index 000000000..5dd86f23d --- /dev/null +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -0,0 +1,150 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { CacheService } from '@ghostfolio/client/services/cache.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; +import { User } from '@ghostfolio/common/interfaces'; +import { + differenceInSeconds, + formatDistanceToNowStrict, + isValid, + parseISO +} from 'date-fns'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'gf-admin-overview', + styleUrls: ['./admin-overview.scss'], + templateUrl: './admin-overview.html' +}) +export class AdminOverviewComponent implements OnDestroy, OnInit { + public dataGatheringInProgress: boolean; + public dataGatheringProgress: number; + public defaultDateFormat = DEFAULT_DATE_FORMAT; + public exchangeRates: { label1: string; label2: string; value: number }[]; + public lastDataGathering: string; + public transactionCount: number; + public userCount: number; + public user: User; + + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor( + private adminService: AdminService, + private cacheService: CacheService, + private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, + private userService: UserService + ) {} + + /** + * Initializes the controller + */ + public ngOnInit() { + this.fetchAdminData(); + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + } + }); + } + + public onFlushCache() { + this.cacheService + .flush() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); + } + + public onGatherMax() { + const confirmation = confirm( + 'This action may take some time. Do you want to proceed?' + ); + + if (confirmation === true) { + this.adminService + .gatherMax() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); + } + } + + public onGatherProfileData() { + this.adminService + .gatherProfileData() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + + public formatDistanceToNow(aDateString: string) { + if (aDateString) { + const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { + addSuffix: true + }); + + return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < + 60 + ? 'just now' + : distanceString; + } + + return ''; + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private fetchAdminData() { + this.dataService + .fetchAdminData() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe( + ({ + dataGatheringProgress, + exchangeRates, + lastDataGathering, + transactionCount, + userCount + }) => { + this.dataGatheringProgress = dataGatheringProgress; + this.exchangeRates = exchangeRates; + + if (isValid(parseISO(lastDataGathering?.toString()))) { + this.lastDataGathering = formatDistanceToNowStrict( + new Date(lastDataGathering), + { + addSuffix: true + } + ); + } else if (lastDataGathering === 'IN_PROGRESS') { + this.dataGatheringInProgress = true; + } else { + this.lastDataGathering = 'Starting soon...'; + } + + this.transactionCount = transactionCount; + this.userCount = userCount; + + this.changeDetectorRef.markForCheck(); + } + ); + } +} diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html new file mode 100644 index 000000000..2f6c0aa4d --- /dev/null +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -0,0 +1,107 @@ +
+
+
+ + +
+
Exchange Rates
+
+ + + + + + + + +
+ + {{ exchangeRate.label1 }}= + + {{ exchangeRate.label2 }}
+
+
+
+
Data Gathering
+
+
+ {{ lastDataGathering }} + In Progress ({{ dataGatheringProgress | percent : '1.2-2' + }}) +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
User Count
+
{{ userCount }}
+
+
+
Transaction Count
+
+ + {{ transactionCount }} ({{ transactionCount / userCount | number + : '1.2-2' }} per User) + +
+
+
+
+
+
+
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.module.ts b/apps/client/src/app/components/admin-overview/admin-overview.module.ts new file mode 100644 index 000000000..88109ea8c --- /dev/null +++ b/apps/client/src/app/components/admin-overview/admin-overview.module.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { CacheService } from '@ghostfolio/client/services/cache.service'; +import { GfValueModule } from '@ghostfolio/ui/value'; + +import { AdminOverviewComponent } from './admin-overview.component'; + +@NgModule({ + declarations: [AdminOverviewComponent], + exports: [], + imports: [CommonModule, GfValueModule, MatButtonModule, MatCardModule], + providers: [CacheService], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class AdminOverviewModule {} diff --git a/apps/client/src/app/components/admin-overview/admin-overview.scss b/apps/client/src/app/components/admin-overview/admin-overview.scss new file mode 100644 index 000000000..ed7f589cd --- /dev/null +++ b/apps/client/src/app/components/admin-overview/admin-overview.scss @@ -0,0 +1,17 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; + + .mat-flat-button { + ::ng-deep { + .mat-button-wrapper { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } + } + } +} diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts new file mode 100644 index 000000000..5a5a96a70 --- /dev/null +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -0,0 +1,82 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { AdminData } from '@ghostfolio/common/interfaces'; +import { + differenceInSeconds, + formatDistanceToNowStrict, + parseISO +} from 'date-fns'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'gf-admin-users', + styleUrls: ['./admin-users.scss'], + templateUrl: './admin-users.html' +}) +export class AdminUsersComponent implements OnDestroy, OnInit { + public users: AdminData['users']; + + private unsubscribeSubject = new Subject(); + + /** + * @constructor + */ + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService + ) {} + + /** + * Initializes the controller + */ + public ngOnInit() { + this.fetchAdminData(); + } + + public formatDistanceToNow(aDateString: string) { + if (aDateString) { + const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { + addSuffix: true + }); + + return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < + 60 + ? 'just now' + : distanceString; + } + + return ''; + } + + public onDeleteUser(aId: string) { + const confirmation = confirm('Do you really want to delete this user?'); + + if (confirmation) { + this.dataService + .deleteUser(aId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchAdminData(); + } + }); + } + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private fetchAdminData() { + this.dataService + .fetchAdminData() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ users }) => { + this.users = users; + + this.changeDetectorRef.markForCheck(); + }); + } +} diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html new file mode 100644 index 000000000..21be11265 --- /dev/null +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -0,0 +1,86 @@ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
#User + Registration + + Accounts + + Transactions + + Engagement per Day + Last Activitiy
{{ i + 1 }} +
+ {{ userItem.alias || userItem.id }} + {{ userItem.alias || (userItem.id | slice:0:5) + + '...' }} + +
+
+ {{ formatDistanceToNow(userItem.createdAt) }} + + {{ userItem.accountCount }} + + {{ userItem.transactionCount }} + + {{ userItem.engagement | number: '1.0-0' }} + + {{ formatDistanceToNow(userItem.lastActivity) }} + + + + + +
+
+
+
+
diff --git a/apps/client/src/app/components/admin-users/admin-users.module.ts b/apps/client/src/app/components/admin-users/admin-users.module.ts new file mode 100644 index 000000000..fbc8b4870 --- /dev/null +++ b/apps/client/src/app/components/admin-users/admin-users.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; + +import { AdminUsersComponent } from './admin-users.component'; + +@NgModule({ + declarations: [AdminUsersComponent], + exports: [], + imports: [CommonModule, MatButtonModule, MatMenuModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class AdminUsersModule {} diff --git a/apps/client/src/app/components/admin-users/admin-users.scss b/apps/client/src/app/components/admin-users/admin-users.scss new file mode 100644 index 000000000..a3916f727 --- /dev/null +++ b/apps/client/src/app/components/admin-users/admin-users.scss @@ -0,0 +1,18 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; + + .users { + overflow-x: auto; + + table { + min-width: 100%; + + .mat-row, + .mat-header-row { + width: 100%; + } + } + } +} diff --git a/apps/client/src/app/pages/admin/admin-page-routing.module.ts b/apps/client/src/app/pages/admin/admin-page-routing.module.ts index 1362cec53..a483fefe7 100644 --- a/apps/client/src/app/pages/admin/admin-page-routing.module.ts +++ b/apps/client/src/app/pages/admin/admin-page-routing.module.ts @@ -1,11 +1,22 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component'; +import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AdminPageComponent } from './admin-page.component'; const routes: Routes = [ - { path: '', component: AdminPageComponent, canActivate: [AuthGuard] } + { + path: '', + component: AdminPageComponent, + canActivate: [AuthGuard], + children: [ + { path: '', redirectTo: 'overview', pathMatch: 'full' }, + { path: 'overview', component: AdminOverviewComponent }, + { path: 'users', component: AdminUsersComponent } + ] + } ]; @NgModule({ diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index b61562192..9865e23c3 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -1,169 +1,26 @@ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { AdminService } from '@ghostfolio/client/services/admin.service'; -import { CacheService } from '@ghostfolio/client/services/cache.service'; -import { DataService } from '@ghostfolio/client/services/data.service'; -import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; -import { AdminData, User } from '@ghostfolio/common/interfaces'; -import { - differenceInSeconds, - formatDistanceToNowStrict, - isValid, - parseISO -} from 'date-fns'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, selector: 'gf-admin-page', styleUrls: ['./admin-page.scss'], templateUrl: './admin-page.html' }) export class AdminPageComponent implements OnDestroy, OnInit { - public dataGatheringInProgress: boolean; - public dataGatheringProgress: number; - public defaultDateFormat = DEFAULT_DATE_FORMAT; - public exchangeRates: { label1: string; label2: string; value: number }[]; - public lastDataGathering: string; - public transactionCount: number; - public userCount: number; - public user: User; - public users: AdminData['users']; - private unsubscribeSubject = new Subject(); /** * @constructor */ - public constructor( - private adminService: AdminService, - private cacheService: CacheService, - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private userService: UserService - ) {} + public constructor() {} /** * Initializes the controller */ - public ngOnInit() { - this.fetchAdminData(); - - this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((state) => { - if (state?.user) { - this.user = state.user; - } - }); - } - - public onFlushCache() { - this.cacheService - .flush() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); - }); - } - - public onGatherMax() { - const confirmation = confirm( - 'This action may take some time. Do you want to proceed?' - ); - - if (confirmation === true) { - this.adminService - .gatherMax() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); - }); - } - } - - public onGatherProfileData() { - this.adminService - .gatherProfileData() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => {}); - } - - public formatDistanceToNow(aDateString: string) { - if (aDateString) { - const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { - addSuffix: true - }); - - return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < - 60 - ? 'just now' - : distanceString; - } - - return ''; - } - - public onDeleteUser(aId: string) { - const confirmation = confirm('Do you really want to delete this user?'); - - if (confirmation) { - this.dataService - .deleteUser(aId) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe({ - next: () => { - this.fetchAdminData(); - } - }); - } - } + public ngOnInit() {} public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } - - private fetchAdminData() { - this.dataService - .fetchAdminData() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe( - ({ - dataGatheringProgress, - exchangeRates, - lastDataGathering, - transactionCount, - userCount, - users - }) => { - this.dataGatheringProgress = dataGatheringProgress; - this.exchangeRates = exchangeRates; - this.users = users; - - if (isValid(parseISO(lastDataGathering?.toString()))) { - this.lastDataGathering = formatDistanceToNowStrict( - new Date(lastDataGathering), - { - addSuffix: true - } - ); - } else if (lastDataGathering === 'IN_PROGRESS') { - this.dataGatheringInProgress = true; - } else { - this.lastDataGathering = 'Starting soon...'; - } - - this.transactionCount = transactionCount; - this.userCount = userCount; - - this.changeDetectorRef.markForCheck(); - } - ); - } } diff --git a/apps/client/src/app/pages/admin/admin-page.html b/apps/client/src/app/pages/admin/admin-page.html index c98f010a3..544d0cc1a 100644 --- a/apps/client/src/app/pages/admin/admin-page.html +++ b/apps/client/src/app/pages/admin/admin-page.html @@ -1,195 +1,17 @@ -
-
-
-

- Admin Control Panel -

- - -
-
Exchange Rates
-
- - - - - - - - -
- - {{ exchangeRate.label1 }}= - - {{ exchangeRate.label2 }}
-
-
-
-
Data Gathering
-
-
- {{ lastDataGathering }} - In Progress ({{ dataGatheringProgress | percent : '1.2-2' - }}) -
-
-
- -
-
- -
-
- -
-
-
-
-
-
User Count
-
{{ userCount }}
-
-
-
Transaction Count
-
- - {{ transactionCount }} ({{ transactionCount / userCount | number - : '1.2-2' }} per User) - -
-
-
-
-
-
-
-
-

Users

-
- - - - - - - - - - - - - - - - - - - - - - - - - -
#User - Registration - - Accounts - - Transactions - - Engagement per Day - Last Activitiy
{{ i + 1 }} -
- {{ userItem.alias || userItem.id }} - {{ userItem.alias || (userItem.id | slice:0:5) + - '...' }} - -
-
- {{ formatDistanceToNow(userItem.createdAt) }} - - {{ userItem.accountCount }} - - {{ userItem.transactionCount }} - - {{ userItem.engagement | number: '1.0-0' }} - - {{ formatDistanceToNow(userItem.lastActivity) }} - - - - - -
-
-
-
-
+ + + diff --git a/apps/client/src/app/pages/admin/admin-page.module.ts b/apps/client/src/app/pages/admin/admin-page.module.ts index d67d08bcd..d89267071 100644 --- a/apps/client/src/app/pages/admin/admin-page.module.ts +++ b/apps/client/src/app/pages/admin/admin-page.module.ts @@ -3,6 +3,9 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatMenuModule } from '@angular/material/menu'; +import { MatTabsModule } from '@angular/material/tabs'; +import { AdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module'; +import { AdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -13,12 +16,15 @@ import { AdminPageComponent } from './admin-page.component'; declarations: [AdminPageComponent], exports: [], imports: [ + AdminOverviewModule, AdminPageRoutingModule, + AdminUsersModule, CommonModule, GfValueModule, MatButtonModule, MatCardModule, - MatMenuModule + MatMenuModule, + MatTabsModule ], providers: [CacheService], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/apps/client/src/app/pages/admin/admin-page.scss b/apps/client/src/app/pages/admin/admin-page.scss index bb3bac7ca..a7de7023b 100644 --- a/apps/client/src/app/pages/admin/admin-page.scss +++ b/apps/client/src/app/pages/admin/admin-page.scss @@ -2,29 +2,31 @@ :host { color: rgb(var(--dark-primary-text)); - display: block; + display: flex; + flex-direction: column; + height: calc(100vh - 5rem); + overflow-y: auto; - .users { - overflow-x: auto; + padding-bottom: env(safe-area-inset-bottom); + padding-bottom: constant(safe-area-inset-bottom); - table { - min-width: 100%; + ::ng-deep { + gf-admin-overview, + gf-admin-users { + flex: 1 1 auto; + overflow-y: auto; + } + + .mat-tab-header { + border-bottom: 0; - .mat-row, - .mat-header-row { - width: 100%; + .mat-ink-bar { + visibility: hidden !important; } - } - } - .mat-flat-button { - ::ng-deep { - .mat-button-wrapper { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; + .mat-tab-label-active { + color: rgba(var(--palette-primary-500), 1); + opacity: 1; } } }