mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
3 years ago
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