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:
parent
933c24ea6f
commit
982183600d
13 changed files with 55 additions and 9 deletions
3
cli/src/api/open-api/api.ts
generated
3
cli/src/api/open-api/api.ts
generated
|
@ -1785,7 +1785,8 @@ export const JobCommand = {
|
||||||
Start: 'start',
|
Start: 'start',
|
||||||
Pause: 'pause',
|
Pause: 'pause',
|
||||||
Resume: 'resume',
|
Resume: 'resume',
|
||||||
Empty: 'empty'
|
Empty: 'empty',
|
||||||
|
ClearFailed: 'clear-failed'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
||||||
|
|
3
mobile/openapi/lib/model/job_command.dart
generated
3
mobile/openapi/lib/model/job_command.dart
generated
|
@ -27,6 +27,7 @@ class JobCommand {
|
||||||
static const pause = JobCommand._(r'pause');
|
static const pause = JobCommand._(r'pause');
|
||||||
static const resume = JobCommand._(r'resume');
|
static const resume = JobCommand._(r'resume');
|
||||||
static const empty = JobCommand._(r'empty');
|
static const empty = JobCommand._(r'empty');
|
||||||
|
static const clearFailed = JobCommand._(r'clear-failed');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][JobCommand].
|
/// List of all possible values in this [enum][JobCommand].
|
||||||
static const values = <JobCommand>[
|
static const values = <JobCommand>[
|
||||||
|
@ -34,6 +35,7 @@ class JobCommand {
|
||||||
pause,
|
pause,
|
||||||
resume,
|
resume,
|
||||||
empty,
|
empty,
|
||||||
|
clearFailed,
|
||||||
];
|
];
|
||||||
|
|
||||||
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
|
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
|
||||||
|
@ -76,6 +78,7 @@ class JobCommandTypeTransformer {
|
||||||
case r'pause': return JobCommand.pause;
|
case r'pause': return JobCommand.pause;
|
||||||
case r'resume': return JobCommand.resume;
|
case r'resume': return JobCommand.resume;
|
||||||
case r'empty': return JobCommand.empty;
|
case r'empty': return JobCommand.empty;
|
||||||
|
case r'clear-failed': return JobCommand.clearFailed;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|
|
@ -7548,7 +7548,8 @@
|
||||||
"start",
|
"start",
|
||||||
"pause",
|
"pause",
|
||||||
"resume",
|
"resume",
|
||||||
"empty"
|
"empty",
|
||||||
|
"clear-failed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@ export enum JobCommand {
|
||||||
PAUSE = 'pause',
|
PAUSE = 'pause',
|
||||||
RESUME = 'resume',
|
RESUME = 'resume',
|
||||||
EMPTY = 'empty',
|
EMPTY = 'empty',
|
||||||
|
CLEAR_FAILED = 'clear-failed',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum JobName {
|
export enum JobName {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
JobHandler,
|
JobHandler,
|
||||||
JobItem,
|
JobItem,
|
||||||
|
QueueCleanType,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
||||||
import { JobCommand, JobName, QueueName } from './job.constants';
|
import { JobCommand, JobName, QueueName } from './job.constants';
|
||||||
|
@ -49,6 +50,11 @@ export class JobService {
|
||||||
case JobCommand.EMPTY:
|
case JobCommand.EMPTY:
|
||||||
await this.jobRepository.empty(queueName);
|
await this.jobRepository.empty(queueName);
|
||||||
break;
|
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);
|
return this.getJobStatus(queueName);
|
||||||
|
|
|
@ -26,6 +26,10 @@ export interface QueueStatus {
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum QueueCleanType {
|
||||||
|
FAILED = 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
export type JobItem =
|
export type JobItem =
|
||||||
// Transcoding
|
// Transcoding
|
||||||
| { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob }
|
| { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob }
|
||||||
|
@ -120,6 +124,7 @@ export interface IJobRepository {
|
||||||
pause(name: QueueName): Promise<void>;
|
pause(name: QueueName): Promise<void>;
|
||||||
resume(name: QueueName): Promise<void>;
|
resume(name: QueueName): Promise<void>;
|
||||||
empty(name: QueueName): Promise<void>;
|
empty(name: QueueName): Promise<void>;
|
||||||
|
clear(name: QueueName, type: QueueCleanType): Promise<string[]>;
|
||||||
getQueueStatus(name: QueueName): Promise<QueueStatus>;
|
getQueueStatus(name: QueueName): Promise<QueueStatus>;
|
||||||
getJobCounts(name: QueueName): Promise<JobCounts>;
|
getJobCounts(name: QueueName): Promise<JobCounts>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { getQueueToken } from '@nestjs/bullmq';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
@ -91,6 +100,10 @@ export class JobRepository implements IJobRepository {
|
||||||
return this.getQueue(name).drain();
|
return this.getQueue(name).drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clear(name: QueueName, type: QueueCleanType) {
|
||||||
|
return this.getQueue(name).clean(0, 1000, type);
|
||||||
|
}
|
||||||
|
|
||||||
getJobCounts(name: QueueName): Promise<JobCounts> {
|
getJobCounts(name: QueueName): Promise<JobCounts> {
|
||||||
return this.getQueue(name).getJobCounts(
|
return this.getQueue(name).getJobCounts(
|
||||||
'active',
|
'active',
|
||||||
|
|
|
@ -13,5 +13,6 @@ export const newJobRepositoryMock = (): jest.Mocked<IJobRepository> => {
|
||||||
queue: jest.fn().mockImplementation(() => Promise.resolve()),
|
queue: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
getQueueStatus: jest.fn(),
|
getQueueStatus: jest.fn(),
|
||||||
getJobCounts: jest.fn(),
|
getJobCounts: jest.fn(),
|
||||||
|
clear: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -76,6 +76,7 @@ export const testApp = {
|
||||||
getQueueStatus: jest.fn(),
|
getQueueStatus: jest.fn(),
|
||||||
getJobCounts: jest.fn(),
|
getJobCounts: jest.fn(),
|
||||||
pause: jest.fn(),
|
pause: jest.fn(),
|
||||||
|
clear: jest.fn(),
|
||||||
} as IJobRepository)
|
} as IJobRepository)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
|
3
web/src/api/open-api/api.ts
generated
3
web/src/api/open-api/api.ts
generated
|
@ -1785,7 +1785,8 @@ export const JobCommand = {
|
||||||
Start: 'start',
|
Start: 'start',
|
||||||
Pause: 'pause',
|
Pause: 'pause',
|
||||||
Resume: 'resume',
|
Resume: 'resume',
|
||||||
Empty: 'empty'
|
Empty: 'empty',
|
||||||
|
ClearFailed: 'clear-failed'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import Badge from '$lib/components/elements/badge.svelte';
|
import Badge from '$lib/components/elements/badge.svelte';
|
||||||
import JobTileButton from './job-tile-button.svelte';
|
import JobTileButton from './job-tile-button.svelte';
|
||||||
import JobTileStatus from './job-tile-status.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 Icon from '$lib/components/elements/icon.svelte';
|
||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
|
@ -55,12 +56,23 @@
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{#if jobCounts.failed > 0}
|
{#if jobCounts.failed > 0}
|
||||||
<Badge color="primary">
|
<Badge color="primary">
|
||||||
{jobCounts.failed.toLocaleString($locale)} failed
|
<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>
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
{#if jobCounts.delayed > 0}
|
{#if jobCounts.delayed > 0 || true}
|
||||||
<Badge color="secondary">
|
<Badge color="secondary">
|
||||||
{jobCounts.delayed.toLocaleString($locale)} delayed
|
<span class="text-sm">
|
||||||
|
{jobCounts.delayed.toLocaleString($locale)} delayed
|
||||||
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<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
|
color
|
||||||
]}"
|
]}"
|
||||||
class:rounded-md={rounded === true}
|
class:rounded-md={rounded === true}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
| 'transparent-gray'
|
| 'transparent-gray'
|
||||||
| 'dark-gray'
|
| 'dark-gray'
|
||||||
| 'overlay-primary';
|
| '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 Rounded = 'lg' | '3xl' | 'full' | false;
|
||||||
export type Shadow = 'md' | false;
|
export type Shadow = 'md' | false;
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,6 +46,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
|
tiny: 'p-0 ml-2 mr-0 align-top',
|
||||||
icon: 'p-2.5',
|
icon: 'p-2.5',
|
||||||
link: 'p-2 font-medium',
|
link: 'p-2 font-medium',
|
||||||
sm: 'px-4 py-2 text-sm font-medium',
|
sm: 'px-4 py-2 text-sm font-medium',
|
||||||
|
|
Loading…
Reference in a new issue