mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
19 changed files with 301 additions and 11 deletions
@ -0,0 +1,41 @@ |
|||||
|
import { AdminJobs } from '@ghostfolio/common/interfaces'; |
||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
||||
|
import type { RequestWithUser } from '@ghostfolio/common/types'; |
||||
|
import { |
||||
|
Controller, |
||||
|
Get, |
||||
|
HttpException, |
||||
|
Inject, |
||||
|
UseGuards |
||||
|
} from '@nestjs/common'; |
||||
|
import { REQUEST } from '@nestjs/core'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
||||
|
|
||||
|
import { QueueService } from './queue.service'; |
||||
|
|
||||
|
@Controller('admin/queue') |
||||
|
export class QueueController { |
||||
|
public constructor( |
||||
|
private readonly queueService: QueueService, |
||||
|
@Inject(REQUEST) private readonly request: RequestWithUser |
||||
|
) {} |
||||
|
|
||||
|
@Get('jobs') |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async getJobs(): Promise<AdminJobs> { |
||||
|
if ( |
||||
|
!hasPermission( |
||||
|
this.request.user.permissions, |
||||
|
permissions.accessAdminControl |
||||
|
) |
||||
|
) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return this.queueService.getJobs({}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { QueueController } from './queue.controller'; |
||||
|
import { QueueService } from './queue.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [QueueController], |
||||
|
imports: [DataGatheringModule], |
||||
|
providers: [QueueService] |
||||
|
}) |
||||
|
export class QueueModule {} |
@ -0,0 +1,32 @@ |
|||||
|
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config'; |
||||
|
import { AdminJobs } from '@ghostfolio/common/interfaces'; |
||||
|
import { InjectQueue } from '@nestjs/bull'; |
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { Queue } from 'bull'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class QueueService { |
||||
|
public constructor( |
||||
|
@InjectQueue(DATA_GATHERING_QUEUE) |
||||
|
private readonly dataGatheringQueue: Queue |
||||
|
) {} |
||||
|
|
||||
|
public async getJobs({ |
||||
|
limit = 1000 |
||||
|
}: { |
||||
|
limit?: number; |
||||
|
}): Promise<AdminJobs> { |
||||
|
const jobs = await this.dataGatheringQueue.getJobs([ |
||||
|
'active', |
||||
|
'completed', |
||||
|
'delayed', |
||||
|
'failed', |
||||
|
'paused', |
||||
|
'waiting' |
||||
|
]); |
||||
|
|
||||
|
return { |
||||
|
jobs: jobs.slice(0, limit) |
||||
|
}; |
||||
|
} |
||||
|
} |
@ -0,0 +1,75 @@ |
|||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
OnDestroy, |
||||
|
OnInit |
||||
|
} from '@angular/core'; |
||||
|
import { AdminService } from '@ghostfolio/client/services/admin.service'; |
||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
||||
|
import { getDateWithTimeFormatString } from '@ghostfolio/common/helper'; |
||||
|
import { AdminJobs, User } from '@ghostfolio/common/interfaces'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
selector: 'gf-admin-jobs', |
||||
|
styleUrls: ['./admin-jobs.scss'], |
||||
|
templateUrl: './admin-jobs.html' |
||||
|
}) |
||||
|
export class AdminJobsComponent implements OnDestroy, OnInit { |
||||
|
public defaultDateTimeFormat: string; |
||||
|
public jobs: AdminJobs['jobs'] = []; |
||||
|
public user: User; |
||||
|
|
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
/** |
||||
|
* @constructor |
||||
|
*/ |
||||
|
public constructor( |
||||
|
private adminService: AdminService, |
||||
|
private changeDetectorRef: ChangeDetectorRef, |
||||
|
private userService: UserService |
||||
|
) { |
||||
|
this.userService.stateChanged |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((state) => { |
||||
|
if (state?.user) { |
||||
|
this.user = state.user; |
||||
|
|
||||
|
this.defaultDateTimeFormat = getDateWithTimeFormatString( |
||||
|
this.user.settings.locale |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initializes the controller |
||||
|
*/ |
||||
|
public ngOnInit() { |
||||
|
this.fetchJobs(); |
||||
|
} |
||||
|
|
||||
|
public onViewStacktrace(aStacktrace: AdminJobs['jobs'][0]['stacktrace']) { |
||||
|
alert(JSON.stringify(aStacktrace, null, ' ')); |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
private fetchJobs() { |
||||
|
this.adminService |
||||
|
.fetchJobs() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(({ jobs }) => { |
||||
|
this.jobs = jobs; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,74 @@ |
|||||
|
<div class="container"> |
||||
|
<div class="row"> |
||||
|
<div class="col"> |
||||
|
<table class="gf-table w-100"> |
||||
|
<thead> |
||||
|
<tr class="mat-header-row"> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>#</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Type</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Created</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Finished</th> |
||||
|
<th class="mat-header-cell px-1 py-2" i18n>Status</th> |
||||
|
<th class="mat-header-cell px-1 py-2"></th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<ng-container *ngFor="let job of jobs"> |
||||
|
<tr class="mat-row"> |
||||
|
<td class="mat-cell px-1 py-2">{{ job.id }}</td> |
||||
|
<td class="mat-cell px-1 py-2">{{ job.name }}</td> |
||||
|
<td class="mat-cell px-1 py-2">{{ job.data?.dataSource }}</td> |
||||
|
<td class="mat-cell px-1 py-2">{{ job.data?.symbol }}</td> |
||||
|
<td class="mat-cell px-1 py-2"> |
||||
|
{{ job.timestamp | date: defaultDateTimeFormat }} |
||||
|
</td> |
||||
|
<td class="mat-cell px-1 py-2"> |
||||
|
{{ job.finishedOn | date: defaultDateTimeFormat }} |
||||
|
</td> |
||||
|
<td class="mat-cell px-1 py-2"> |
||||
|
<ion-icon |
||||
|
*ngIf="job.finishedOn" |
||||
|
class="text-success" |
||||
|
name="checkmark-circle-outline" |
||||
|
></ion-icon> |
||||
|
<ng-container *ngIf="!job.finishedOn"> |
||||
|
<ion-icon |
||||
|
*ngIf="job.stacktrace?.length >= 1" |
||||
|
class="text-danger" |
||||
|
name="alert-circle-outline" |
||||
|
></ion-icon> |
||||
|
<ion-icon |
||||
|
*ngIf="job.stacktrace?.length < 1" |
||||
|
name="time-outline" |
||||
|
></ion-icon> |
||||
|
</ng-container> |
||||
|
</td> |
||||
|
<td class="mat-cell px-1 py-2"> |
||||
|
<button |
||||
|
class="mx-1 no-min-width px-2" |
||||
|
mat-button |
||||
|
[matMenuTriggerFor]="accountMenu" |
||||
|
(click)="$event.stopPropagation()" |
||||
|
> |
||||
|
<ion-icon name="ellipsis-vertical"></ion-icon> |
||||
|
</button> |
||||
|
<mat-menu #accountMenu="matMenu" xPosition="before"> |
||||
|
<button |
||||
|
i18n |
||||
|
mat-menu-item |
||||
|
[disabled]="job.stacktrace?.length < 1" |
||||
|
(click)="onViewStacktrace(job.stacktrace)" |
||||
|
> |
||||
|
View Stacktrace |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</ng-container> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,13 @@ |
|||||
|
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 { AdminJobsComponent } from './admin-jobs.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [AdminJobsComponent], |
||||
|
imports: [CommonModule, MatButtonModule, MatMenuModule], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
||||
|
}) |
||||
|
export class GfAdminJobsModule {} |
@ -0,0 +1,5 @@ |
|||||
|
@import '~apps/client/src/styles/ghostfolio-style'; |
||||
|
|
||||
|
:host { |
||||
|
display: block; |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
import { Job } from 'bull'; |
||||
|
|
||||
|
export interface AdminJobs { |
||||
|
jobs: Job<any>[]; |
||||
|
} |
Loading…
Reference in new issue