diff --git a/CHANGELOG.md b/CHANGELOG.md index 2141ce5a1..d402bf8f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for a click handler in the page tabs component + ### Changed +- Enabled the _Bull Dashboard_ tab in the admin control panel (experimental) - Upgraded `bull-board` from version `7.1.5` to `7.2.1` ## 3.10.0 - 2026-06-13 diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index b19508d3e..5f9d1055d 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -30,7 +30,6 @@ export class ConfigurationService { API_KEY_FINANCIAL_MODELING_PREP: str({ default: '' }), API_KEY_OPEN_FIGI: str({ default: '' }), API_KEY_RAPID_API: str({ default: '' }), - BULL_BOARD_IS_READ_ONLY: bool({ default: true }), CACHE_QUOTES_TTL: num({ default: ms('1 minute') }), CACHE_TTL: num({ default: CACHE_TTL_NO_CACHE }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index eb3ac86a3..57c58898e 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -10,7 +10,6 @@ export interface Environment extends CleanedEnvAccessors { API_KEY_FINANCIAL_MODELING_PREP: string; API_KEY_OPEN_FIGI: string; API_KEY_RAPID_API: string; - BULL_BOARD_IS_READ_ONLY: boolean; CACHE_QUOTES_TTL: number; CACHE_TTL: number; DATA_SOURCE_EXCHANGE_RATES: string; diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts index 5672df5e8..5ac6c40c0 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts @@ -23,8 +23,7 @@ import { DataGatheringProcessor } from './data-gathering.processor'; adapter: BullAdapter, name: DATA_GATHERING_QUEUE, options: { - displayName: 'Data Gathering', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + displayName: 'Data Gathering' } }), BullModule.registerQueue({ diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index c90f826f6..0da529821 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -29,8 +29,7 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; adapter: BullAdapter, name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, options: { - displayName: 'Portfolio Snapshot Computation', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + displayName: 'Portfolio Snapshot Computation' } }), BullModule.registerQueue({ diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts index d6f6d5ccd..6ef14e29c 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts @@ -20,8 +20,7 @@ import { StatisticsGatheringService } from './statistics-gathering.service'; adapter: BullAdapter, name: STATISTICS_GATHERING_QUEUE, options: { - displayName: 'Statistics Gathering', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + displayName: 'Statistics Gathering' } }) ] 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 b4c228881..fd90bff2c 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 @@ -1,8 +1,5 @@ -import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { - BULL_BOARD_COOKIE_NAME, - BULL_BOARD_ROUTE, DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_LOW, DATA_GATHERING_QUEUE_PRIORITY_MEDIUM, @@ -10,7 +7,6 @@ import { } from '@ghostfolio/common/config'; import { getDateWithTimeFormatString } from '@ghostfolio/common/helper'; import { AdminJobs, User } from '@ghostfolio/common/interfaces'; -import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { AdminService } from '@ghostfolio/ui/services'; @@ -106,7 +102,6 @@ export class GfAdminJobsComponent implements OnInit { 'actions' ]; - protected hasPermissionToAccessBullBoard = false; protected isLoading = false; protected readonly statusFilterOptions = QUEUE_JOB_STATUS_LIST; @@ -116,7 +111,6 @@ export class GfAdminJobsComponent implements OnInit { private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); private readonly notificationService = inject(NotificationService); - private readonly tokenStorageService = inject(TokenStorageService); private readonly userService = inject(UserService); public constructor() { @@ -129,11 +123,6 @@ export class GfAdminJobsComponent implements OnInit { this.defaultDateTimeFormat = getDateWithTimeFormatString( this.user.settings.locale ); - - this.hasPermissionToAccessBullBoard = hasPermission( - this.user.permissions, - permissions.accessAdminControlBullBoard - ); } }); @@ -193,18 +182,6 @@ export class GfAdminJobsComponent implements OnInit { }); } - protected onOpenBullBoard() { - const token = this.tokenStorageService.getToken(); - - document.cookie = [ - `${BULL_BOARD_COOKIE_NAME}=${encodeURIComponent(token)}`, - 'path=/', - 'SameSite=Strict' - ].join('; '); - - window.open(BULL_BOARD_ROUTE, '_blank'); - } - protected onViewData(aData: AdminJobs['jobs'][0]['data']) { this.notificationService.alert({ title: JSON.stringify(aData, null, ' ') 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 d57704b86..e615db31b 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -1,15 +1,6 @@
- @if (hasPermissionToAccessBullBoard) { -
- -
- } -
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 b933ff058..c5da0fb0c 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -1,10 +1,19 @@ +import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { + BULL_BOARD_COOKIE_NAME, + BULL_BOARD_ROUTE +} from '@ghostfolio/common/config'; +import { User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { GfPageTabsComponent, TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { Component, OnInit } from '@angular/core'; +import { Component, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { flashOutline, @@ -21,10 +30,23 @@ import { styleUrls: ['./admin-page.scss'], templateUrl: './admin-page.html' }) -export class AdminPageComponent implements OnInit { +export class AdminPageComponent { public tabs: TabConfiguration[] = []; + private user: User; + + private readonly tokenStorageService = inject(TokenStorageService); + private readonly userService = inject(UserService); + public constructor() { + this.userService.stateChanged + .pipe(takeUntilDestroyed()) + .subscribe((state) => { + this.user = state?.user; + + this.initializeTabs(); + }); + addIcons({ flashOutline, peopleOutline, @@ -34,7 +56,12 @@ export class AdminPageComponent implements OnInit { }); } - public ngOnInit() { + private initializeTabs() { + const hasPermissionToAccessBullBoard = hasPermission( + this.user?.permissions, + permissions.accessAdminControlBullBoard + ); + this.tabs = [ { iconName: 'reader-outline', @@ -51,11 +78,19 @@ export class AdminPageComponent implements OnInit { label: internalRoutes.adminControl.subRoutes.marketData.title, routerLink: internalRoutes.adminControl.subRoutes.marketData.routerLink }, - { - iconName: 'flash-outline', - label: internalRoutes.adminControl.subRoutes.jobs.title, - routerLink: internalRoutes.adminControl.subRoutes.jobs.routerLink - }, + hasPermissionToAccessBullBoard + ? { + iconName: 'flash-outline', + label: $localize`Job Queue`, + onClick: () => { + this.onOpenBullBoard(); + } + } + : { + iconName: 'flash-outline', + label: internalRoutes.adminControl.subRoutes.jobs.title, + routerLink: internalRoutes.adminControl.subRoutes.jobs.routerLink + }, { iconName: 'people-outline', label: internalRoutes.adminControl.subRoutes.users.title, @@ -63,4 +98,16 @@ export class AdminPageComponent implements OnInit { } ]; } + + private onOpenBullBoard() { + const token = this.tokenStorageService.getToken(); + + document.cookie = [ + `${BULL_BOARD_COOKIE_NAME}=${encodeURIComponent(token)}`, + 'path=/', + 'SameSite=Strict' + ].join('; '); + + window.open(BULL_BOARD_ROUTE, '_blank'); + } } diff --git a/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts b/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts index 7b18b26ec..3d44d2870 100644 --- a/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts +++ b/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts @@ -1,6 +1,11 @@ -export interface TabConfiguration { +interface BaseTabConfiguration { iconName: string; label: string; - routerLink: string[]; showCondition?: boolean; } + +export type TabConfiguration = BaseTabConfiguration & + ( + | { onClick: () => void; routerLink?: never } + | { onClick?: never; routerLink: string[] } + ); diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.html b/libs/ui/src/lib/page-tabs/page-tabs.component.html index fa9af9b11..28843148a 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.html +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.html @@ -10,21 +10,40 @@ > @for (tab of tabs(); track tab) { @if (tab.showCondition !== false) { - - -
-
+ @if (tab.onClick) { + + } @else { + + + + } } } + + + +
+
diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.ts b/libs/ui/src/lib/page-tabs/page-tabs.component.ts index 61c2caf05..a6ab9cb18 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.ts +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.ts @@ -1,3 +1,4 @@ +import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -13,7 +14,7 @@ import { TabConfiguration } from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [IonIcon, MatTabsModule, RouterModule], + imports: [IonIcon, MatTabsModule, NgTemplateOutlet, RouterModule], selector: 'gf-page-tabs', styleUrls: ['./page-tabs.component.scss'], templateUrl: './page-tabs.component.html'