mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	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
					
				
							
								
								
									
										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',
 | 
			
		||||
    Pause: 'pause',
 | 
			
		||||
    Resume: 'resume',
 | 
			
		||||
    Empty: 'empty'
 | 
			
		||||
    Empty: 'empty',
 | 
			
		||||
    ClearFailed: 'clear-failed'
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
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 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');
 | 
			
		||||
 | 
			
		||||
@ -7548,7 +7548,8 @@
 | 
			
		||||
          "start",
 | 
			
		||||
          "pause",
 | 
			
		||||
          "resume",
 | 
			
		||||
          "empty"
 | 
			
		||||
          "empty",
 | 
			
		||||
          "clear-failed"
 | 
			
		||||
        ],
 | 
			
		||||
        "type": "string"
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ export enum JobCommand {
 | 
			
		||||
  PAUSE = 'pause',
 | 
			
		||||
  RESUME = 'resume',
 | 
			
		||||
  EMPTY = 'empty',
 | 
			
		||||
  CLEAR_FAILED = 'clear-failed',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum JobName {
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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(),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,7 @@ export const testApp = {
 | 
			
		||||
        getQueueStatus: jest.fn(),
 | 
			
		||||
        getJobCounts: jest.fn(),
 | 
			
		||||
        pause: jest.fn(),
 | 
			
		||||
        clear: jest.fn(),
 | 
			
		||||
      } as IJobRepository)
 | 
			
		||||
      .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',
 | 
			
		||||
    Pause: 'pause',
 | 
			
		||||
    Resume: 'resume',
 | 
			
		||||
    Empty: 'empty'
 | 
			
		||||
    Empty: 'empty',
 | 
			
		||||
    ClearFailed: 'clear-failed'
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user