feat(web): clear failed jobs (#5423)

* add clear failed jobs button

* refactor: clean up code

* chore: open api

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Clement Ong 2023-12-05 10:07:20 +08:00 committed by GitHub
parent 933c24ea6f
commit 982183600d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 55 additions and 9 deletions

View file

@ -1785,7 +1785,8 @@ export const JobCommand = {
Start: 'start',
Pause: 'pause',
Resume: 'resume',
Empty: 'empty'
Empty: 'empty',
ClearFailed: 'clear-failed'
} as const;
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];

View file

@ -27,6 +27,7 @@ class JobCommand {
static const pause = JobCommand._(r'pause');
static const resume = JobCommand._(r'resume');
static const empty = JobCommand._(r'empty');
static const clearFailed = JobCommand._(r'clear-failed');
/// List of all possible values in this [enum][JobCommand].
static const values = <JobCommand>[
@ -34,6 +35,7 @@ class JobCommand {
pause,
resume,
empty,
clearFailed,
];
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
@ -76,6 +78,7 @@ class JobCommandTypeTransformer {
case r'pause': return JobCommand.pause;
case r'resume': return JobCommand.resume;
case r'empty': return JobCommand.empty;
case r'clear-failed': return JobCommand.clearFailed;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');

View file

@ -7548,7 +7548,8 @@
"start",
"pause",
"resume",
"empty"
"empty",
"clear-failed"
],
"type": "string"
},

View file

@ -18,6 +18,7 @@ export enum JobCommand {
PAUSE = 'pause',
RESUME = 'resume',
EMPTY = 'empty',
CLEAR_FAILED = 'clear-failed',
}
export enum JobName {

View file

@ -10,6 +10,7 @@ import {
ISystemConfigRepository,
JobHandler,
JobItem,
QueueCleanType,
} from '../repositories';
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
import { JobCommand, JobName, QueueName } from './job.constants';
@ -49,6 +50,11 @@ export class JobService {
case JobCommand.EMPTY:
await this.jobRepository.empty(queueName);
break;
case JobCommand.CLEAR_FAILED:
const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.FAILED);
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
break;
}
return this.getJobStatus(queueName);

View file

@ -26,6 +26,10 @@ export interface QueueStatus {
isPaused: boolean;
}
export enum QueueCleanType {
FAILED = 'failed',
}
export type JobItem =
// Transcoding
| { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob }
@ -120,6 +124,7 @@ export interface IJobRepository {
pause(name: QueueName): Promise<void>;
resume(name: QueueName): Promise<void>;
empty(name: QueueName): Promise<void>;
clear(name: QueueName, type: QueueCleanType): Promise<string[]>;
getQueueStatus(name: QueueName): Promise<QueueStatus>;
getJobCounts(name: QueueName): Promise<JobCounts>;
}

View file

@ -1,4 +1,13 @@
import { IJobRepository, JobCounts, JobItem, JobName, JOBS_TO_QUEUE, QueueName, QueueStatus } from '@app/domain';
import {
IJobRepository,
JobCounts,
JobItem,
JobName,
JOBS_TO_QUEUE,
QueueCleanType,
QueueName,
QueueStatus,
} from '@app/domain';
import { getQueueToken } from '@nestjs/bullmq';
import { Injectable, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@ -91,6 +100,10 @@ export class JobRepository implements IJobRepository {
return this.getQueue(name).drain();
}
clear(name: QueueName, type: QueueCleanType) {
return this.getQueue(name).clean(0, 1000, type);
}
getJobCounts(name: QueueName): Promise<JobCounts> {
return this.getQueue(name).getJobCounts(
'active',

View file

@ -13,5 +13,6 @@ export const newJobRepositoryMock = (): jest.Mocked<IJobRepository> => {
queue: jest.fn().mockImplementation(() => Promise.resolve()),
getQueueStatus: jest.fn(),
getJobCounts: jest.fn(),
clear: jest.fn(),
};
};

View file

@ -76,6 +76,7 @@ export const testApp = {
getQueueStatus: jest.fn(),
getJobCounts: jest.fn(),
pause: jest.fn(),
clear: jest.fn(),
} as IJobRepository)
.compile();

View file

@ -1785,7 +1785,8 @@ export const JobCommand = {
Start: 'start',
Pause: 'pause',
Resume: 'resume',
Empty: 'empty'
Empty: 'empty',
ClearFailed: 'clear-failed'
} as const;
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];

View file

@ -5,6 +5,7 @@
import Badge from '$lib/components/elements/badge.svelte';
import JobTileButton from './job-tile-button.svelte';
import JobTileStatus from './job-tile-status.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import {
mdiAlertCircle,
@ -55,12 +56,23 @@
<div class="flex gap-2">
{#if jobCounts.failed > 0}
<Badge color="primary">
<span class="text-sm">
{jobCounts.failed.toLocaleString($locale)} failed
</span>
<Button
size="tiny"
shadow={false}
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
>
<Icon path={mdiClose} size="18" />
</Button>
</Badge>
{/if}
{#if jobCounts.delayed > 0}
{#if jobCounts.delayed > 0 || true}
<Badge color="secondary">
<span class="text-sm">
{jobCounts.delayed.toLocaleString($locale)} delayed
</span>
</Badge>
{/if}
</div>

View file

@ -14,7 +14,7 @@
</script>
<span
class="inline-block h-min whitespace-nowrap px-4 pb-[0.55em] pt-[0.55em] text-center align-baseline text-xs leading-none {colorClasses[
class="inline-block h-min whitespace-nowrap px-3 py-1 text-center align-baseline text-xs leading-none {colorClasses[
color
]}"
class:rounded-md={rounded === true}

View file

@ -11,7 +11,7 @@
| 'transparent-gray'
| 'dark-gray'
| 'overlay-primary';
export type Size = 'icon' | 'link' | 'sm' | 'base' | 'lg';
export type Size = 'tiny' | 'icon' | 'link' | 'sm' | 'base' | 'lg';
export type Rounded = 'lg' | '3xl' | 'full' | false;
export type Shadow = 'md' | false;
</script>
@ -46,6 +46,7 @@
};
const sizeClasses: Record<Size, string> = {
tiny: 'p-0 ml-2 mr-0 align-top',
icon: 'p-2.5',
link: 'p-2 font-medium',
sm: 'px-4 py-2 text-sm font-medium',