diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index abe3d9b87..ecc9db781 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -53,6 +53,7 @@ doc/JobCommand.md doc/JobCommandDto.md doc/JobCountsDto.md doc/JobName.md +doc/JobStatusDto.md doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md @@ -60,6 +61,7 @@ doc/OAuthApi.md doc/OAuthCallbackDto.md doc/OAuthConfigDto.md doc/OAuthConfigResponseDto.md +doc/QueueStatusDto.md doc/RemoveAssetsDto.md doc/SearchAlbumResponseDto.md doc/SearchApi.md @@ -170,12 +172,14 @@ lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts_dto.dart lib/model/job_name.dart +lib/model/job_status_dto.dart lib/model/login_credential_dto.dart lib/model/login_response_dto.dart lib/model/logout_response_dto.dart lib/model/o_auth_callback_dto.dart lib/model/o_auth_config_dto.dart lib/model/o_auth_config_response_dto.dart +lib/model/queue_status_dto.dart lib/model/remove_assets_dto.dart lib/model/search_album_response_dto.dart lib/model/search_asset_dto.dart @@ -264,6 +268,7 @@ test/job_command_dto_test.dart test/job_command_test.dart test/job_counts_dto_test.dart test/job_name_test.dart +test/job_status_dto_test.dart test/login_credential_dto_test.dart test/login_response_dto_test.dart test/logout_response_dto_test.dart @@ -271,6 +276,7 @@ test/o_auth_api_test.dart test/o_auth_callback_dto_test.dart test/o_auth_config_dto_test.dart test/o_auth_config_response_dto_test.dart +test/queue_status_dto_test.dart test/remove_assets_dto_test.dart test/search_album_response_dto_test.dart test/search_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e6a30506f..a1dcba3a1 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AllJobStatusResponseDto.md b/mobile/openapi/doc/AllJobStatusResponseDto.md index 306c902ef..e0e8e3795 100644 Binary files a/mobile/openapi/doc/AllJobStatusResponseDto.md and b/mobile/openapi/doc/AllJobStatusResponseDto.md differ diff --git a/mobile/openapi/doc/JobApi.md b/mobile/openapi/doc/JobApi.md index f3a6b81cc..ddfbb7e74 100644 Binary files a/mobile/openapi/doc/JobApi.md and b/mobile/openapi/doc/JobApi.md differ diff --git a/mobile/openapi/doc/JobStatusDto.md b/mobile/openapi/doc/JobStatusDto.md new file mode 100644 index 000000000..ba85c3b25 Binary files /dev/null and b/mobile/openapi/doc/JobStatusDto.md differ diff --git a/mobile/openapi/doc/QueueStatusDto.md b/mobile/openapi/doc/QueueStatusDto.md new file mode 100644 index 000000000..ca3475678 Binary files /dev/null and b/mobile/openapi/doc/QueueStatusDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 4ccf97fad..c30576da8 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/job_api.dart b/mobile/openapi/lib/api/job_api.dart index 85174188a..a3c0e87aa 100644 Binary files a/mobile/openapi/lib/api/job_api.dart and b/mobile/openapi/lib/api/job_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7268a004e..ee3a013ec 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart index 5feb3bfd0..9aea38e6b 100644 Binary files a/mobile/openapi/lib/model/all_job_status_response_dto.dart and b/mobile/openapi/lib/model/all_job_status_response_dto.dart differ diff --git a/mobile/openapi/lib/model/job_status_dto.dart b/mobile/openapi/lib/model/job_status_dto.dart new file mode 100644 index 000000000..481855a23 Binary files /dev/null and b/mobile/openapi/lib/model/job_status_dto.dart differ diff --git a/mobile/openapi/lib/model/queue_status_dto.dart b/mobile/openapi/lib/model/queue_status_dto.dart new file mode 100644 index 000000000..9bb5dea0d Binary files /dev/null and b/mobile/openapi/lib/model/queue_status_dto.dart differ diff --git a/mobile/openapi/test/all_job_status_response_dto_test.dart b/mobile/openapi/test/all_job_status_response_dto_test.dart index 1154f4fa9..319eb2b10 100644 Binary files a/mobile/openapi/test/all_job_status_response_dto_test.dart and b/mobile/openapi/test/all_job_status_response_dto_test.dart differ diff --git a/mobile/openapi/test/job_api_test.dart b/mobile/openapi/test/job_api_test.dart index 6f980417e..7cc1a4b2a 100644 Binary files a/mobile/openapi/test/job_api_test.dart and b/mobile/openapi/test/job_api_test.dart differ diff --git a/mobile/openapi/test/job_status_dto_test.dart b/mobile/openapi/test/job_status_dto_test.dart new file mode 100644 index 000000000..ae353baf0 Binary files /dev/null and b/mobile/openapi/test/job_status_dto_test.dart differ diff --git a/mobile/openapi/test/queue_status_dto_test.dart b/mobile/openapi/test/queue_status_dto_test.dart new file mode 100644 index 000000000..f85eb9da8 Binary files /dev/null and b/mobile/openapi/test/queue_status_dto_test.dart differ diff --git a/server/apps/immich/src/controllers/job.controller.ts b/server/apps/immich/src/controllers/job.controller.ts index a22f110d9..f86b60e1b 100644 --- a/server/apps/immich/src/controllers/job.controller.ts +++ b/server/apps/immich/src/controllers/job.controller.ts @@ -1,4 +1,4 @@ -import { AllJobStatusResponseDto, JobCommandDto, JobIdDto, JobService } from '@app/domain'; +import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto, JobIdDto, JobService } from '@app/domain'; import { Body, Controller, Get, Param, Put, UsePipes, ValidationPipe } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Authenticated } from '../decorators/authenticated.decorator'; @@ -16,7 +16,8 @@ export class JobController { } @Put('/:jobId') - sendJobCommand(@Param() { jobId }: JobIdDto, @Body() dto: JobCommandDto): Promise { - return this.service.handleCommand(jobId, dto); + async sendJobCommand(@Param() { jobId }: JobIdDto, @Body() dto: JobCommandDto): Promise { + await this.service.handleCommand(jobId, dto); + return await this.service.getJobStatus(jobId); } } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 6920d0e9e..dd6e71ac5 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -541,7 +541,14 @@ }, "responses": { "200": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobStatusDto" + } + } + } } }, "tags": [ @@ -4088,32 +4095,62 @@ "paused" ] }, + "QueueStatusDto": { + "type": "object", + "properties": { + "isActive": { + "type": "boolean" + }, + "isPaused": { + "type": "boolean" + } + }, + "required": [ + "isActive", + "isPaused" + ] + }, + "JobStatusDto": { + "type": "object", + "properties": { + "jobCounts": { + "$ref": "#/components/schemas/JobCountsDto" + }, + "queueStatus": { + "$ref": "#/components/schemas/QueueStatusDto" + } + }, + "required": [ + "jobCounts", + "queueStatus" + ] + }, "AllJobStatusResponseDto": { "type": "object", "properties": { "thumbnail-generation-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "metadata-extraction-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "video-conversion-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "object-tagging-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "clip-encoding-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "storage-template-migration-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "background-task-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" }, "search-queue": { - "$ref": "#/components/schemas/JobCountsDto" + "$ref": "#/components/schemas/JobStatusDto" } }, "required": [ diff --git a/server/libs/domain/src/job/job.repository.ts b/server/libs/domain/src/job/job.repository.ts index fbbc02fb6..6f3a5c396 100644 --- a/server/libs/domain/src/job/job.repository.ts +++ b/server/libs/domain/src/job/job.repository.ts @@ -18,6 +18,11 @@ export interface JobCounts { paused: number; } +export interface QueueStatus { + isActive: boolean; + isPaused: boolean; +} + export type JobItem = // Asset Upload | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob } @@ -73,6 +78,6 @@ export interface IJobRepository { pause(name: QueueName): Promise; resume(name: QueueName): Promise; empty(name: QueueName): Promise; - isActive(name: QueueName): Promise; + getQueueStatus(name: QueueName): Promise; getJobCounts(name: QueueName): Promise; } diff --git a/server/libs/domain/src/job/job.service.spec.ts b/server/libs/domain/src/job/job.service.spec.ts index a07e779c9..bb3f62dc0 100644 --- a/server/libs/domain/src/job/job.service.spec.ts +++ b/server/libs/domain/src/job/job.service.spec.ts @@ -25,72 +25,35 @@ describe(JobService.name, () => { waiting: 1, paused: 1, }); + jobMock.getQueueStatus.mockResolvedValue({ + isActive: true, + isPaused: true, + }); + + const expectedJobStatus = { + jobCounts: { + active: 1, + completed: 1, + delayed: 1, + failed: 1, + waiting: 1, + paused: 1, + }, + queueStatus: { + isActive: true, + isPaused: true, + }, + }; await expect(sut.getAllJobsStatus()).resolves.toEqual({ - 'background-task-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'clip-encoding-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'metadata-extraction-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'object-tagging-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'search-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'storage-template-migration-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'thumbnail-generation-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - 'video-conversion-queue': { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, + 'background-task-queue': expectedJobStatus, + 'clip-encoding-queue': expectedJobStatus, + 'metadata-extraction-queue': expectedJobStatus, + 'object-tagging-queue': expectedJobStatus, + 'search-queue': expectedJobStatus, + 'storage-template-migration-queue': expectedJobStatus, + 'thumbnail-generation-queue': expectedJobStatus, + 'video-conversion-queue': expectedJobStatus, }); }); }); @@ -115,7 +78,7 @@ describe(JobService.name, () => { }); it('should not start a job that is already running', async () => { - jobMock.isActive.mockResolvedValue(true); + jobMock.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false }); await expect( sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }), @@ -125,7 +88,7 @@ describe(JobService.name, () => { }); it('should handle a start video conversion command', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }); @@ -133,7 +96,7 @@ describe(JobService.name, () => { }); it('should handle a start storage template migration command', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.STORAGE_TEMPLATE_MIGRATION, { command: JobCommand.START, force: false }); @@ -141,7 +104,7 @@ describe(JobService.name, () => { }); it('should handle a start object tagging command', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.OBJECT_TAGGING, { command: JobCommand.START, force: false }); @@ -149,7 +112,7 @@ describe(JobService.name, () => { }); it('should handle a start clip encoding command', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.CLIP_ENCODING, { command: JobCommand.START, force: false }); @@ -157,7 +120,7 @@ describe(JobService.name, () => { }); it('should handle a start metadata extraction command', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.START, force: false }); @@ -165,7 +128,7 @@ describe(JobService.name, () => { }); it('should handle a start thumbnail generation command', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.THUMBNAIL_GENERATION, { command: JobCommand.START, force: false }); @@ -173,7 +136,7 @@ describe(JobService.name, () => { }); it('should throw a bad request when an invalid queue is used', async () => { - jobMock.isActive.mockResolvedValue(false); + jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await expect( sut.handleCommand(QueueName.BACKGROUND_TASK, { command: JobCommand.START, force: false }), diff --git a/server/libs/domain/src/job/job.service.ts b/server/libs/domain/src/job/job.service.ts index ccb1709aa..fcf969682 100644 --- a/server/libs/domain/src/job/job.service.ts +++ b/server/libs/domain/src/job/job.service.ts @@ -3,7 +3,7 @@ import { assertMachineLearningEnabled } from '../domain.constant'; import { JobCommandDto } from './dto'; import { JobCommand, JobName, QueueName } from './job.constants'; import { IJobRepository } from './job.repository'; -import { AllJobStatusResponseDto } from './response-dto'; +import { AllJobStatusResponseDto, JobStatusDto } from './response-dto'; @Injectable() export class JobService { @@ -29,16 +29,25 @@ export class JobService { } } + async getJobStatus(queueName: QueueName): Promise { + const [jobCounts, queueStatus] = await Promise.all([ + this.jobRepository.getJobCounts(queueName), + this.jobRepository.getQueueStatus(queueName), + ]); + + return { jobCounts, queueStatus }; + } + async getAllJobsStatus(): Promise { const response = new AllJobStatusResponseDto(); for (const queueName of Object.values(QueueName)) { - response[queueName] = await this.jobRepository.getJobCounts(queueName); + response[queueName] = await this.getJobStatus(queueName); } return response; } private async start(name: QueueName, { force }: JobCommandDto): Promise { - const isActive = await this.jobRepository.isActive(name); + const { isActive } = await this.jobRepository.getQueueStatus(name); if (isActive) { throw new BadRequestException(`Job is already running`); } diff --git a/server/libs/domain/src/job/response-dto/all-job-status-response.dto.ts b/server/libs/domain/src/job/response-dto/all-job-status-response.dto.ts index dd0d1fb65..500492311 100644 --- a/server/libs/domain/src/job/response-dto/all-job-status-response.dto.ts +++ b/server/libs/domain/src/job/response-dto/all-job-status-response.dto.ts @@ -16,28 +16,41 @@ export class JobCountsDto { paused!: number; } -export class AllJobStatusResponseDto implements Record { - @ApiProperty({ type: JobCountsDto }) - [QueueName.THUMBNAIL_GENERATION]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.METADATA_EXTRACTION]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.VIDEO_CONVERSION]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.OBJECT_TAGGING]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.CLIP_ENCODING]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.BACKGROUND_TASK]!: JobCountsDto; - - @ApiProperty({ type: JobCountsDto }) - [QueueName.SEARCH]!: JobCountsDto; +export class QueueStatusDto { + isActive!: boolean; + isPaused!: boolean; +} + +export class JobStatusDto { + @ApiProperty({ type: JobCountsDto }) + jobCounts!: JobCountsDto; + + @ApiProperty({ type: QueueStatusDto }) + queueStatus!: QueueStatusDto; +} + +export class AllJobStatusResponseDto implements Record { + @ApiProperty({ type: JobStatusDto }) + [QueueName.THUMBNAIL_GENERATION]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.METADATA_EXTRACTION]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.VIDEO_CONVERSION]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.OBJECT_TAGGING]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.CLIP_ENCODING]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.BACKGROUND_TASK]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.SEARCH]!: JobStatusDto; } diff --git a/server/libs/domain/test/job.repository.mock.ts b/server/libs/domain/test/job.repository.mock.ts index a6c8fff2a..cb347bb09 100644 --- a/server/libs/domain/test/job.repository.mock.ts +++ b/server/libs/domain/test/job.repository.mock.ts @@ -6,7 +6,7 @@ export const newJobRepositoryMock = (): jest.Mocked => { pause: jest.fn(), resume: jest.fn(), queue: jest.fn().mockImplementation(() => Promise.resolve()), - isActive: jest.fn(), + getQueueStatus: jest.fn(), getJobCounts: jest.fn(), }; }; diff --git a/server/libs/infra/src/repositories/job.repository.ts b/server/libs/infra/src/repositories/job.repository.ts index cf39d324c..8619a048d 100644 --- a/server/libs/infra/src/repositories/job.repository.ts +++ b/server/libs/infra/src/repositories/job.repository.ts @@ -7,6 +7,7 @@ import { JobItem, JobName, QueueName, + QueueStatus, } from '@app/domain'; import { InjectQueue } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; @@ -36,9 +37,13 @@ export class JobRepository implements IJobRepository { @InjectQueue(QueueName.SEARCH) private searchIndex: Queue, ) {} - async isActive(name: QueueName): Promise { - const counts = await this.getJobCounts(name); - return !!counts.active; + async getQueueStatus(name: QueueName): Promise { + const queue = this.queueMap[name]; + + return { + isActive: !!(await queue.getActiveCount()), + isPaused: await queue.isPaused(), + }; } pause(name: QueueName) { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 4bf1cd687..2313411d9 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -291,52 +291,52 @@ export interface AlbumResponseDto { export interface AllJobStatusResponseDto { /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'thumbnail-generation-queue': JobCountsDto; + 'thumbnail-generation-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'metadata-extraction-queue': JobCountsDto; + 'metadata-extraction-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'video-conversion-queue': JobCountsDto; + 'video-conversion-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'object-tagging-queue': JobCountsDto; + 'object-tagging-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'clip-encoding-queue': JobCountsDto; + 'clip-encoding-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'storage-template-migration-queue': JobCountsDto; + 'storage-template-migration-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'background-task-queue': JobCountsDto; + 'background-task-queue': JobStatusDto; /** * - * @type {JobCountsDto} + * @type {JobStatusDto} * @memberof AllJobStatusResponseDto */ - 'search-queue': JobCountsDto; + 'search-queue': JobStatusDto; } /** * @@ -1311,6 +1311,25 @@ export const JobName = { export type JobName = typeof JobName[keyof typeof JobName]; +/** + * + * @export + * @interface JobStatusDto + */ +export interface JobStatusDto { + /** + * + * @type {JobCountsDto} + * @memberof JobStatusDto + */ + 'jobCounts': JobCountsDto; + /** + * + * @type {QueueStatusDto} + * @memberof JobStatusDto + */ + 'queueStatus': QueueStatusDto; +} /** * * @export @@ -1467,6 +1486,25 @@ export interface OAuthConfigResponseDto { */ 'autoLaunch'?: boolean; } +/** + * + * @export + * @interface QueueStatusDto + */ +export interface QueueStatusDto { + /** + * + * @type {boolean} + * @memberof QueueStatusDto + */ + 'isActive': boolean; + /** + * + * @type {boolean} + * @memberof QueueStatusDto + */ + 'isPaused': boolean; +} /** * * @export @@ -6270,7 +6308,7 @@ export const JobApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.sendJobCommand(jobId, jobCommandDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -6299,7 +6337,7 @@ export const JobApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: any): AxiosPromise { + sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: any): AxiosPromise { return localVarFp.sendJobCommand(jobId, jobCommandDto, options).then((request) => request(axios, basePath)); }, }; diff --git a/web/src/app.css b/web/src/app.css index f073f352c..d46ca41ce 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -109,8 +109,4 @@ input:focus-visible { display: none; scrollbar-width: none; } - - .job-play-button { - @apply h-full flex flex-col place-items-center place-content-center px-8 text-gray-600 transition-all hover:bg-immich-primary hover:text-white dark:text-gray-200 dark:hover:bg-immich-dark-primary text-sm dark:hover:text-black gap-2; - } } diff --git a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte new file mode 100644 index 000000000..ba98ea1cc --- /dev/null +++ b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/web/src/lib/components/admin-page/jobs/job-tile-status.svelte b/web/src/lib/components/admin-page/jobs/job-tile-status.svelte new file mode 100644 index 000000000..4f31ad6ff --- /dev/null +++ b/web/src/lib/components/admin-page/jobs/job-tile-status.svelte @@ -0,0 +1,16 @@ + + + + +
+ +
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 f22294431..eeb35a24a 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -4,40 +4,49 @@ import Pause from 'svelte-material-icons/Pause.svelte'; import FastForward from 'svelte-material-icons/FastForward.svelte'; import AllInclusive from 'svelte-material-icons/AllInclusive.svelte'; + import Close from 'svelte-material-icons/Close.svelte'; import { locale } from '$lib/stores/preferences.store'; import { createEventDispatcher } from 'svelte'; - import { JobCommand, JobCommandDto, JobCountsDto } from '@api'; + import { JobCommand, JobCommandDto, JobCountsDto, QueueStatusDto } from '@api'; import Badge from '$lib/components/elements/badge.svelte'; + import JobTileButton from './job-tile-button.svelte'; + import JobTileStatus from './job-tile-status.svelte'; export let title: string; export let subtitle: string | undefined = undefined; export let jobCounts: JobCountsDto; + export let queueStatus: QueueStatusDto; export let allowForceCommand = true; - $: isRunning = jobCounts.active > 0 || jobCounts.waiting > 0; - $: waitingCount = jobCounts.waiting + jobCounts.paused; - $: isPause = jobCounts.paused > 0; + $: waitingCount = jobCounts.waiting + jobCounts.paused + jobCounts.delayed; + $: isIdle = !queueStatus.isActive && !queueStatus.isPaused; const dispatch = createEventDispatcher<{ command: JobCommandDto }>(); -
-
-
+
+
+ {#if queueStatus.isPaused} + Paused + {:else if queueStatus.isActive} + Active + {/if} +
{title.toUpperCase()}
{#if jobCounts.failed > 0} - + {jobCounts.failed.toLocaleString($locale)} failed {/if} + {#if jobCounts.delayed > 0} + + {jobCounts.delayed.toLocaleString($locale)} delayed + + {/if}
@@ -69,43 +78,54 @@
-
- {#if isRunning} - - {:else if jobCounts.paused > 0} - +
+ {#if !isIdle} + {#if waitingCount > 0} + dispatch('command', { command: JobCommand.Empty, force: false })} + > + CLEAR + + {/if} + {#if queueStatus.isPaused} + dispatch('command', { command: JobCommand.Resume, force: false })} + > + {@const size = waitingCount > 0 ? '24' : '48'} + + + RESUME + + {:else} + dispatch('command', { command: JobCommand.Pause, force: false })} + > + PAUSE + + {/if} {:else if allowForceCommand} - - + MISSING + {:else} - + {/if}
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 267210b00..b103d4deb 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -1,4 +1,8 @@ diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 263db4df4..e69cb6898 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -5,9 +5,10 @@ import type { PageData } from './$types'; export let data: PageData; - let jobs = data.jobs; let timer: NodeJS.Timer; + $: jobs = data.jobs; + const load = async () => { const { data } = await api.jobApi.getAllJobsStatus(); jobs = data;