Browse Source

Filter and delete jobs

pull/991/head
Thomas 3 years ago
parent
commit
8613fbaff0
  1. 30
      apps/api/src/app/admin/queue/queue.controller.ts
  2. 38
      apps/api/src/app/admin/queue/queue.service.ts
  3. 31
      apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
  4. 32
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  5. 11
      apps/client/src/app/components/admin-jobs/admin-jobs.module.ts
  6. 33
      apps/client/src/app/services/admin.service.ts
  7. 10
      libs/common/src/lib/config.ts

30
apps/api/src/app/admin/queue/queue.controller.ts

@ -8,10 +8,12 @@ import {
HttpException, HttpException,
Inject, Inject,
Param, Param,
Query,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { JobStatus } from 'bull';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { QueueService } from './queue.service'; import { QueueService } from './queue.service';
@ -23,9 +25,32 @@ export class QueueController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@Delete('job')
@UseGuards(AuthGuard('jwt'))
public async deleteJobs(
@Query('status') filterByStatus?: string
): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined;
return this.queueService.deleteJobs({ status });
}
@Get('job') @Get('job')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getJobs(): Promise<AdminJobs> { public async getJobs(
@Query('status') filterByStatus?: string
): Promise<AdminJobs> {
if ( if (
!hasPermission( !hasPermission(
this.request.user.permissions, this.request.user.permissions,
@ -38,7 +63,8 @@ export class QueueController {
); );
} }
return this.queueService.getJobs({}); const status = <JobStatus[]>filterByStatus?.split(',') ?? undefined;
return this.queueService.getJobs({ status });
} }
@Delete('job/:id') @Delete('job/:id')

38
apps/api/src/app/admin/queue/queue.service.ts

@ -1,8 +1,11 @@
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config'; import {
DATA_GATHERING_QUEUE,
QUEUE_JOB_STATUS_LIST
} from '@ghostfolio/common/config';
import { AdminJobs } from '@ghostfolio/common/interfaces'; import { AdminJobs } from '@ghostfolio/common/interfaces';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Queue } from 'bull'; import { JobStatus, Queue } from 'bull';
@Injectable() @Injectable()
export class QueueService { export class QueueService {
@ -15,19 +18,30 @@ export class QueueService {
return (await this.dataGatheringQueue.getJob(aId))?.remove(); return (await this.dataGatheringQueue.getJob(aId))?.remove();
} }
public async deleteJobs({
status = QUEUE_JOB_STATUS_LIST
}: {
status?: JobStatus[];
}) {
const jobs = await this.dataGatheringQueue.getJobs(status);
for (const job of jobs) {
try {
await job.remove();
} catch (error) {
Logger.warn(error, 'QueueService');
}
}
}
public async getJobs({ public async getJobs({
limit = 1000 limit = 1000,
status = QUEUE_JOB_STATUS_LIST
}: { }: {
limit?: number; limit?: number;
status?: JobStatus[];
}): Promise<AdminJobs> { }): Promise<AdminJobs> {
const jobs = await this.dataGatheringQueue.getJobs([ const jobs = await this.dataGatheringQueue.getJobs(status);
'active',
'completed',
'delayed',
'failed',
'paused',
'waiting'
]);
const jobsWithState = await Promise.all( const jobsWithState = await Promise.all(
jobs.slice(0, limit).map(async (job) => { jobs.slice(0, limit).map(async (job) => {

31
apps/client/src/app/components/admin-jobs/admin-jobs.component.ts

@ -5,10 +5,13 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { QUEUE_JOB_STATUS_LIST } from '@ghostfolio/common/config';
import { getDateWithTimeFormatString } from '@ghostfolio/common/helper'; import { getDateWithTimeFormatString } from '@ghostfolio/common/helper';
import { AdminJobs, User } from '@ghostfolio/common/interfaces'; import { AdminJobs, User } from '@ghostfolio/common/interfaces';
import { JobStatus } from 'bull';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -20,7 +23,9 @@ import { takeUntil } from 'rxjs/operators';
}) })
export class AdminJobsComponent implements OnDestroy, OnInit { export class AdminJobsComponent implements OnDestroy, OnInit {
public defaultDateTimeFormat: string; public defaultDateTimeFormat: string;
public filterForm: FormGroup;
public jobs: AdminJobs['jobs'] = []; public jobs: AdminJobs['jobs'] = [];
public statusFilterOptions = QUEUE_JOB_STATUS_LIST;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -31,6 +36,7 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
public constructor( public constructor(
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private formBuilder: FormBuilder,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -50,6 +56,17 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
* Initializes the controller * Initializes the controller
*/ */
public ngOnInit() { public ngOnInit() {
this.filterForm = this.formBuilder.group({
status: []
});
this.filterForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
const currentFilter = this.filterForm.get('status').value;
this.fetchJobs(currentFilter ? [currentFilter] : undefined);
});
this.fetchJobs(); this.fetchJobs();
} }
@ -62,6 +79,16 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
}); });
} }
public onDeleteJobs() {
this.adminService
.deleteJobs({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
const currentFilter = this.filterForm.get('status').value;
this.fetchJobs(currentFilter ? [currentFilter] : undefined);
});
}
public onViewStacktrace(aStacktrace: AdminJobs['jobs'][0]['stacktrace']) { public onViewStacktrace(aStacktrace: AdminJobs['jobs'][0]['stacktrace']) {
alert(JSON.stringify(aStacktrace, null, ' ')); alert(JSON.stringify(aStacktrace, null, ' '));
} }
@ -71,9 +98,9 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private fetchJobs() { private fetchJobs(aStatus?: JobStatus[]) {
this.adminService this.adminService
.fetchJobs() .fetchJobs({ status: aStatus })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ jobs }) => { .subscribe(({ jobs }) => {
this.jobs = jobs; this.jobs = jobs;

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

@ -1,6 +1,26 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field appearance="outline" class="flex-grow-1">
<mat-select formControlName="status">
<mat-option></mat-option>
<mat-option
*ngFor="let statusFilterOption of statusFilterOptions"
[value]="statusFilterOption"
>{{ statusFilterOption }}</mat-option
>
</mat-select>
</mat-form-field>
<button
class="ml-1"
color="warn"
mat-flat-button
(click)="onDeleteJobs()"
>
<span i18n>Delete Jobs</span>
</button>
</form>
<table class="gf-table w-100"> <table class="gf-table w-100">
<thead> <thead>
<tr class="mat-header-row"> <tr class="mat-header-row">
@ -47,6 +67,10 @@
{{ job.finishedOn | date: defaultDateTimeFormat }} {{ job.finishedOn | date: defaultDateTimeFormat }}
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<ion-icon
*ngIf="job.state === 'active'"
name="play-outline"
></ion-icon>
<ion-icon <ion-icon
*ngIf="job.state === 'completed'" *ngIf="job.state === 'completed'"
class="text-success" class="text-success"
@ -61,6 +85,14 @@
class="text-danger" class="text-danger"
name="alert-circle-outline" name="alert-circle-outline"
></ion-icon> ></ion-icon>
<ion-icon
*ngIf="job.state === 'paused'"
name="pause-outline"
></ion-icon>
<ion-icon
*ngIf="job.state === 'waiting'"
name="cafe-outline"
></ion-icon>
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
<button <button

11
apps/client/src/app/components/admin-jobs/admin-jobs.module.ts

@ -1,13 +1,22 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { AdminJobsComponent } from './admin-jobs.component'; import { AdminJobsComponent } from './admin-jobs.component';
@NgModule({ @NgModule({
declarations: [AdminJobsComponent], declarations: [AdminJobsComponent],
imports: [CommonModule, MatButtonModule, MatMenuModule], imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatMenuModule,
MatSelectModule,
ReactiveFormsModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfAdminJobsModule {} export class GfAdminJobsModule {}

33
apps/client/src/app/services/admin.service.ts

@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
@ -9,6 +9,7 @@ import {
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { JobStatus } from 'bull';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { Observable, map } from 'rxjs'; import { Observable, map } from 'rxjs';
@ -22,6 +23,18 @@ export class AdminService {
return this.http.delete<void>(`/api/v1/admin/queue/job/${aId}`); return this.http.delete<void>(`/api/v1/admin/queue/job/${aId}`);
} }
public deleteJobs({ status }: { status?: JobStatus[] }) {
let params = new HttpParams();
if (status?.length > 0) {
params = params.append('status', status.join(','));
}
return this.http.delete<void>('/api/v1/admin/queue/job', {
params
});
}
public deleteProfileData({ dataSource, symbol }: UniqueAsset) { public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
return this.http.delete<void>( return this.http.delete<void>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}` `/api/v1/admin/profile-data/${dataSource}/${symbol}`
@ -47,20 +60,28 @@ export class AdminService {
); );
} }
public fetchJobs() { public fetchJobs({ status }: { status?: JobStatus[] }) {
return this.http.get<AdminJobs>(`/api/v1/admin/queue/job`); let params = new HttpParams();
if (status?.length > 0) {
params = params.append('status', status.join(','));
}
return this.http.get<AdminJobs>('/api/v1/admin/queue/job', {
params
});
} }
public gather7Days() { public gather7Days() {
return this.http.post<void>(`/api/v1/admin/gather`, {}); return this.http.post<void>('/api/v1/admin/gather', {});
} }
public gatherMax() { public gatherMax() {
return this.http.post<void>(`/api/v1/admin/gather/max`, {}); return this.http.post<void>('/api/v1/admin/gather/max', {});
} }
public gatherProfileData() { public gatherProfileData() {
return this.http.post<void>(`/api/v1/admin/gather/profile-data`, {}); return this.http.post<void>('/api/v1/admin/gather/profile-data', {});
} }
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {

10
libs/common/src/lib/config.ts

@ -1,4 +1,5 @@
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { JobStatus } from 'bull';
import { ToggleOption } from './types'; import { ToggleOption } from './types';
@ -60,4 +61,13 @@ export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG'; export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE'; export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE';
export const QUEUE_JOB_STATUS_LIST = <JobStatus[]>[
'active',
'completed',
'delayed',
'failed',
'paused',
'waiting'
];
export const UNKNOWN_KEY = 'UNKNOWN'; export const UNKNOWN_KEY = 'UNKNOWN';

Loading…
Cancel
Save