Feature/add jobs of queue to admin control panel (#987)
* Add jobs of queue to admin control panel * Update changelogpull/988/head
parent
14a0eeab29
commit
7cf0cdc4ce
@ -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