From 982183600daf17b7a18543dbf1ceedee3cc20f54 Mon Sep 17 00:00:00 2001 From: Clement Ong Date: Tue, 5 Dec 2023 10:07:20 +0800 Subject: [PATCH] feat(web): clear failed jobs (#5423) * add clear failed jobs button * refactor: clean up code * chore: open api --------- Co-authored-by: Jason Rasmussen --- cli/src/api/open-api/api.ts | 3 ++- mobile/openapi/lib/model/job_command.dart | 3 +++ server/immich-openapi-specs.json | 3 ++- server/src/domain/job/job.constants.ts | 1 + server/src/domain/job/job.service.ts | 6 ++++++ .../src/domain/repositories/job.repository.ts | 5 +++++ .../src/infra/repositories/job.repository.ts | 15 ++++++++++++++- .../test/repositories/job.repository.mock.ts | 1 + server/test/test-utils.ts | 1 + web/src/api/open-api/api.ts | 3 ++- .../components/admin-page/jobs/job-tile.svelte | 18 +++++++++++++++--- web/src/lib/components/elements/badge.svelte | 2 +- .../components/elements/buttons/button.svelte | 3 ++- 13 files changed, 55 insertions(+), 9 deletions(-) diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index ac5ea101e..54cc92149 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -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]; diff --git a/mobile/openapi/lib/model/job_command.dart b/mobile/openapi/lib/model/job_command.dart index f1fc8986b..363db01e8 100644 --- a/mobile/openapi/lib/model/job_command.dart +++ b/mobile/openapi/lib/model/job_command.dart @@ -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 = [ @@ -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'); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 356399813..55b4f81ab 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -7548,7 +7548,8 @@ "start", "pause", "resume", - "empty" + "empty", + "clear-failed" ], "type": "string" }, diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index a7f467784..b7fe0d1dd 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -18,6 +18,7 @@ export enum JobCommand { PAUSE = 'pause', RESUME = 'resume', EMPTY = 'empty', + CLEAR_FAILED = 'clear-failed', } export enum JobName { diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 975163117..4082d5e90 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -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); diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index 7b9deabbd..f3cbedec9 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -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; resume(name: QueueName): Promise; empty(name: QueueName): Promise; + clear(name: QueueName, type: QueueCleanType): Promise; getQueueStatus(name: QueueName): Promise; getJobCounts(name: QueueName): Promise; } diff --git a/server/src/infra/repositories/job.repository.ts b/server/src/infra/repositories/job.repository.ts index 067ba9bbf..4d802cd4b 100644 --- a/server/src/infra/repositories/job.repository.ts +++ b/server/src/infra/repositories/job.repository.ts @@ -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 { return this.getQueue(name).getJobCounts( 'active', diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts index fe794d1dc..967c6a804 100644 --- a/server/test/repositories/job.repository.mock.ts +++ b/server/test/repositories/job.repository.mock.ts @@ -13,5 +13,6 @@ export const newJobRepositoryMock = (): jest.Mocked => { queue: jest.fn().mockImplementation(() => Promise.resolve()), getQueueStatus: jest.fn(), getJobCounts: jest.fn(), + clear: jest.fn(), }; }; diff --git a/server/test/test-utils.ts b/server/test/test-utils.ts index dc7c1b698..0ef80e858 100644 --- a/server/test/test-utils.ts +++ b/server/test/test-utils.ts @@ -76,6 +76,7 @@ export const testApp = { getQueueStatus: jest.fn(), getJobCounts: jest.fn(), pause: jest.fn(), + clear: jest.fn(), } as IJobRepository) .compile(); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index ac5ea101e..54cc92149 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -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]; diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index 1be188cd2..464ba9c08 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -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 @@
{#if jobCounts.failed > 0} - {jobCounts.failed.toLocaleString($locale)} failed + + {jobCounts.failed.toLocaleString($locale)} failed + + {/if} - {#if jobCounts.delayed > 0} + {#if jobCounts.delayed > 0 || true} - {jobCounts.delayed.toLocaleString($locale)} delayed + + {jobCounts.delayed.toLocaleString($locale)} delayed + {/if}
diff --git a/web/src/lib/components/elements/badge.svelte b/web/src/lib/components/elements/badge.svelte index 3b75bbc2e..da305e40f 100644 --- a/web/src/lib/components/elements/badge.svelte +++ b/web/src/lib/components/elements/badge.svelte @@ -14,7 +14,7 @@ @@ -46,6 +46,7 @@ }; const sizeClasses: Record = { + 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',