Browse Source

Task/enable bull dashboard in tab of admin control panel (#7030)

* Enable Bull Dashboard in tab

* Eliminate BULL_BOARD_IS_READ_ONLY

* Update changelog
pull/7028/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
073af0c8c2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 1
      apps/api/src/services/configuration/configuration.service.ts
  3. 1
      apps/api/src/services/interfaces/environment.interface.ts
  4. 3
      apps/api/src/services/queues/data-gathering/data-gathering.module.ts
  5. 3
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts
  6. 3
      apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts
  7. 23
      apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
  8. 9
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  9. 63
      apps/client/src/app/pages/admin/admin-page.component.ts
  10. 9
      libs/ui/src/lib/page-tabs/interfaces/interfaces.ts
  11. 49
      libs/ui/src/lib/page-tabs/page-tabs.component.html
  12. 3
      libs/ui/src/lib/page-tabs/page-tabs.component.ts

5
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

1
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 }),

1
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;

3
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({

3
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({

3
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'
}
})
]

23
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, ' ')

9
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -1,15 +1,6 @@
<div class="container">
<div class="row">
<div class="col">
@if (hasPermissionToAccessBullBoard) {
<div class="d-flex justify-content-end mb-3">
<button mat-stroked-button (click)="onOpenBullBoard()">
<span><ng-container i18n>Overview</ng-container></span>
<ion-icon class="ml-2" name="open-outline" />
</button>
</div>
}
<form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select formControlName="status">

63
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');
}
}

9
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[] }
);

49
libs/ui/src/lib/page-tabs/page-tabs.component.html

@ -10,21 +10,40 @@
>
@for (tab of tabs(); track tab) {
@if (tab.showCondition !== false) {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2" [innerHTML]="tab.label"></div>
</a>
@if (tab.onClick) {
<button
class="no-min-width px-3"
mat-tab-link
type="button"
(click)="tab.onClick()"
>
<ng-container
*ngTemplateOutlet="tabContent; context: { $implicit: tab }"
/>
</button>
} @else {
<a
#rla="routerLinkActive"
class="no-min-width px-3"
mat-tab-link
routerLinkActive
[active]="rla.isActive"
[routerLink]="tab.routerLink"
[routerLinkActiveOptions]="{ exact: true }"
>
<ng-container
*ngTemplateOutlet="tabContent; context: { $implicit: tab }"
/>
</a>
}
}
}
</nav>
<ng-template #tabContent let-tab>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2" [innerHTML]="tab.label"></div>
</ng-template>

3
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'

Loading…
Cancel
Save