diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b745e34..510ccddf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extracted the users from the admin control panel endpoint to a dedicated endpoint - Improved the language localization for Italian (`it`) -## 2.106.0-beta.6 +## 2.106.0 - 2024-09-07 ### Added - Set up a performance logging service +- Added a loading indicator to the queue jobs table in the admin control panel +- Added a loading indicator to the users table in the admin control panel - Added the attribute `mode` to the scraper configuration to get quotes instantly ### Changed @@ -38,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue in the view mode toggle of the holdings tab on the home page (experimental) - Fixed an issue on the portfolio activities page by loading the data only once - Fixed an issue in the carousel component for the testimonial section on the landing page +- Fixed the historical market data gathering in the _Yahoo Finance_ service by switching from `historical()` to `chart()` - Handled an exception in the historical market data component of the asset profile details dialog in the admin control panel ## 2.105.0 - 2024-08-21 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 6d201be23..da4b5dd7e 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -17,6 +17,7 @@ import { AdminData, AdminMarketData, AdminMarketDataDetails, + AdminUsers, EnhancedSymbolProfile } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -347,4 +348,11 @@ export class AdminController { ) { return this.adminService.putSetting(key, data.value); } + + @Get('user') + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getUsers(): Promise { + return this.adminService.getUsers(); + } } diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 50b781f54..3f5274285 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -21,6 +21,7 @@ import { AdminMarketData, AdminMarketDataDetails, AdminMarketDataItem, + AdminUsers, AssetProfileIdentifier, EnhancedSymbolProfile, Filter @@ -135,7 +136,6 @@ export class AdminService { settings: await this.propertyService.get(), transactionCount: await this.prismaService.order.count(), userCount: await this.prismaService.user.count(), - users: await this.getUsersWithAnalytics(), version: environment.version }; } @@ -377,6 +377,10 @@ export class AdminService { }; } + public async getUsers(): Promise { + return { users: await this.getUsersWithAnalytics() }; + } + public async patchAssetProfileData({ assetClass, assetSubClass, @@ -546,11 +550,11 @@ export class AdminService { return { marketData, count: marketData.length }; } - private async getUsersWithAnalytics(): Promise { + private async getUsersWithAnalytics(): Promise { let orderBy: any = { createdAt: 'desc' }; - let where; + let where: Prisma.UserWhereInput; if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { orderBy = { diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index e0d88f0c6..a8f7d261e 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -20,6 +20,11 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import { addDays, format, isSameDay } from 'date-fns'; import yahooFinance from 'yahoo-finance2'; +import { ChartResultArray } from 'yahoo-finance2/dist/esm/src/modules/chart'; +import { + HistoricalDividendsResult, + HistoricalHistoryResult +} from 'yahoo-finance2/dist/esm/src/modules/historical'; import { Quote } from 'yahoo-finance2/dist/esm/src/modules/quote'; @Injectable() @@ -60,18 +65,19 @@ export class YahooFinanceService implements DataProviderInterface { } try { - const historicalResult = await yahooFinance.historical( - this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( - symbol - ), - { - events: 'dividends', - interval: granularity === 'month' ? '1mo' : '1d', - period1: format(from, DATE_FORMAT), - period2: format(to, DATE_FORMAT) - } + const historicalResult = this.convertToDividendResult( + await yahooFinance.chart( + this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + symbol + ), + { + events: 'dividends', + interval: granularity === 'month' ? '1mo' : '1d', + period1: format(from, DATE_FORMAT), + period2: format(to, DATE_FORMAT) + } + ) ); - const response: { [date: string]: IDataProviderHistoricalResponse; } = {}; @@ -108,15 +114,17 @@ export class YahooFinanceService implements DataProviderInterface { } try { - const historicalResult = await yahooFinance.historical( - this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( - symbol - ), - { - interval: '1d', - period1: format(from, DATE_FORMAT), - period2: format(to, DATE_FORMAT) - } + const historicalResult = this.convertToHistoricalResult( + await yahooFinance.chart( + this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + symbol + ), + { + interval: '1d', + period1: format(from, DATE_FORMAT), + period2: format(to, DATE_FORMAT) + } + ) ); const response: { @@ -302,6 +310,20 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } + private convertToDividendResult( + result: ChartResultArray + ): HistoricalDividendsResult { + return result.events.dividends.map(({ amount: dividends, date }) => { + return { date, dividends }; + }); + } + + private convertToHistoricalResult( + result: ChartResultArray + ): HistoricalHistoryResult { + return result.quotes; + } + private async getQuotesWithQuoteSummary(aYahooFinanceSymbols: string[]) { const quoteSummaryPromises = aYahooFinanceSymbols.map((symbol) => { return yahooFinance.quoteSummary(symbol).catch(() => { diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts index 820b3651d..e828049bc 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts @@ -51,6 +51,7 @@ export class AdminJobsComponent implements OnDestroy, OnInit { 'status', 'actions' ]; + public isLoading = false; public statusFilterOptions = QUEUE_JOB_STATUS_LIST; public user: User; @@ -138,12 +139,16 @@ export class AdminJobsComponent implements OnDestroy, OnInit { } private fetchJobs(aStatus?: JobStatus[]) { + this.isLoading = true; + this.adminService .fetchJobs({ status: aStatus }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ jobs }) => { this.dataSource = new MatTableDataSource(jobs); + this.isLoading = false; + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html index 9ea2097e2..e194b2b37 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -183,6 +183,16 @@ + @if (isLoading) { + + } diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.module.ts b/apps/client/src/app/components/admin-jobs/admin-jobs.module.ts index fe717b904..cca66a04a 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.module.ts +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.module.ts @@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { AdminJobsComponent } from './admin-jobs.component'; @@ -17,6 +18,7 @@ import { AdminJobsComponent } from './admin-jobs.component'; MatMenuModule, MatSelectModule, MatTableModule, + NgxSkeletonLoaderModule, ReactiveFormsModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] 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 index 0a66977bf..c5264c3b3 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -5,7 +5,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper'; -import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces'; +import { AdminUsers, InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; @@ -24,7 +24,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-users.html' }) export class AdminUsersComponent implements OnDestroy, OnInit { - public dataSource: MatTableDataSource = + public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat: string; public displayedColumns: string[] = []; @@ -32,6 +32,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { public hasPermissionForSubscription: boolean; public hasPermissionToImpersonateAllUsers: boolean; public info: InfoItem; + public isLoading = false; public user: User; private unsubscribeSubject = new Subject(); @@ -93,7 +94,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { } public ngOnInit() { - this.fetchAdminData(); + this.fetchUsers(); } public formatDistanceToNow(aDateString: string) { @@ -118,7 +119,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { .deleteUser(aId) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.fetchAdminData(); + this.fetchUsers(); }); }, confirmType: ConfirmationDialogType.Warn, @@ -141,13 +142,17 @@ export class AdminUsersComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private fetchAdminData() { + private fetchUsers() { + this.isLoading = true; + this.adminService - .fetchAdminData() + .fetchUsers() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ users }) => { this.dataSource = new MatTableDataSource(users); + this.isLoading = false; + 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 index 25ab9053d..b65b7c821 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -245,6 +245,16 @@ > + @if (isLoading) { + + } 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 index 3f4e9f2f7..fcf25c8b5 100644 --- a/apps/client/src/app/components/admin-users/admin-users.module.ts +++ b/apps/client/src/app/components/admin-users/admin-users.module.ts @@ -6,6 +6,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; import { MatTableModule } from '@angular/material/table'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { AdminUsersComponent } from './admin-users.component'; @@ -18,7 +19,8 @@ import { AdminUsersComponent } from './admin-users.component'; GfValueComponent, MatButtonModule, MatMenuModule, - MatTableModule + MatTableModule, + NgxSkeletonLoaderModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index e5ea176d1..4c011e8c1 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -12,6 +12,7 @@ import { AdminJobs, AdminMarketData, AdminMarketDataDetails, + AdminUsers, EnhancedSymbolProfile, Filter } from '@ghostfolio/common/interfaces'; @@ -155,6 +156,10 @@ export class AdminService { return this.http.get('/api/v1/tag'); } + public fetchUsers() { + return this.http.get('/api/v1/admin/user'); + } + public gather7Days() { return this.http.post('/api/v1/admin/gather', {}); } diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index 6b139026b..3dc476df8 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,7 +1,5 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; -import { Role } from '@prisma/client'; - export interface AdminData { exchangeRates: ({ label1: string; @@ -11,15 +9,5 @@ export interface AdminData { settings: { [key: string]: boolean | object | string | string[] }; transactionCount: number; userCount: number; - users: { - accountCount: number; - country: string; - createdAt: Date; - engagement: number; - id: string; - lastActivity: Date; - role: Role; - transactionCount: number; - }[]; version: string; } diff --git a/libs/common/src/lib/interfaces/admin-users.interface.ts b/libs/common/src/lib/interfaces/admin-users.interface.ts new file mode 100644 index 000000000..24eb45c85 --- /dev/null +++ b/libs/common/src/lib/interfaces/admin-users.interface.ts @@ -0,0 +1,14 @@ +import { Role } from '@prisma/client'; + +export interface AdminUsers { + users: { + accountCount: number; + country: string; + createdAt: Date; + engagement: number; + id: string; + lastActivity: Date; + role: Role; + transactionCount: number; + }[]; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index f7224407b..efab780fd 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -7,6 +7,7 @@ import type { AdminMarketData, AdminMarketDataItem } from './admin-market-data.interface'; +import type { AdminUsers } from './admin-users.interface'; import type { AssetProfileIdentifier } from './asset-profile-identifier.interface'; import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; import type { BenchmarkProperty } from './benchmark-property.interface'; @@ -61,6 +62,7 @@ export { AdminMarketData, AdminMarketDataDetails, AdminMarketDataItem, + AdminUsers, AssetProfileIdentifier, Benchmark, BenchmarkMarketDataDetails, diff --git a/package.json b/package.json index 79ed8f8c1..81eb3859f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.106.0-beta.6", + "version": "2.106.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",