mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -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',
 | 
					    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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user