From b06ddec2d5a61fa8e09b61607e3caf8103b80512 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 1 Apr 2023 22:46:07 +0200 Subject: [PATCH] feat(server/web): jobs clear button + queue status (#2144) * feat(server/web): jobs clear button + queue status * adjust design and colors * Adjust some styling * show status next to buttons instead of on top * Update rounded corner for badge --------- Co-authored-by: Alex Tran --- mobile/openapi/.openapi-generator/FILES | 6 + mobile/openapi/README.md | 2 + mobile/openapi/doc/AllJobStatusResponseDto.md | 16 +-- mobile/openapi/doc/JobApi.md | 9 +- mobile/openapi/doc/JobStatusDto.md | 16 +++ mobile/openapi/doc/QueueStatusDto.md | 16 +++ mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api/job_api.dart | 10 +- mobile/openapi/lib/api_client.dart | 4 + .../model/all_job_status_response_dto.dart | 32 ++--- mobile/openapi/lib/model/job_status_dto.dart | 119 ++++++++++++++++++ .../openapi/lib/model/queue_status_dto.dart | 119 ++++++++++++++++++ .../all_job_status_response_dto_test.dart | 16 +-- mobile/openapi/test/job_api_test.dart | 2 +- mobile/openapi/test/job_status_dto_test.dart | 32 +++++ .../openapi/test/queue_status_dto_test.dart | 32 +++++ .../immich/src/controllers/job.controller.ts | 7 +- server/immich-openapi-specs.json | 55 ++++++-- server/libs/domain/src/job/job.repository.ts | 7 +- .../libs/domain/src/job/job.service.spec.ts | 107 ++++++---------- server/libs/domain/src/job/job.service.ts | 15 ++- .../all-job-status-response.dto.ts | 61 +++++---- .../libs/domain/test/job.repository.mock.ts | 2 +- .../infra/src/repositories/job.repository.ts | 11 +- web/src/api/open-api/api.ts | 74 ++++++++--- web/src/app.css | 4 - .../admin-page/jobs/job-tile-button.svelte | 21 ++++ .../admin-page/jobs/job-tile-status.svelte | 16 +++ .../admin-page/jobs/job-tile.svelte | 100 +++++++++------ .../admin-page/jobs/jobs-panel.svelte | 28 ++--- web/src/lib/components/elements/badge.svelte | 20 ++- web/src/routes/admin/jobs-status/+page.svelte | 3 +- 32 files changed, 722 insertions(+), 242 deletions(-) create mode 100644 mobile/openapi/doc/JobStatusDto.md create mode 100644 mobile/openapi/doc/QueueStatusDto.md create mode 100644 mobile/openapi/lib/model/job_status_dto.dart create mode 100644 mobile/openapi/lib/model/queue_status_dto.dart create mode 100644 mobile/openapi/test/job_status_dto_test.dart create mode 100644 mobile/openapi/test/queue_status_dto_test.dart create mode 100644 web/src/lib/components/admin-page/jobs/job-tile-button.svelte create mode 100644 web/src/lib/components/admin-page/jobs/job-tile-status.svelte 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 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -200,12 +200,14 @@ Class | Method | HTTP request | Description - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) - [JobName](doc//JobName.md) + - [JobStatusDto](doc//JobStatusDto.md) - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md) + - [QueueStatusDto](doc//QueueStatusDto.md) - [RemoveAssetsDto](doc//RemoveAssetsDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAssetDto](doc//SearchAssetDto.md) diff --git a/mobile/openapi/doc/AllJobStatusResponseDto.md b/mobile/openapi/doc/AllJobStatusResponseDto.md index 306c902ef..e0e8e3795 100644 --- a/mobile/openapi/doc/AllJobStatusResponseDto.md +++ b/mobile/openapi/doc/AllJobStatusResponseDto.md @@ -8,14 +8,14 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**thumbnailGenerationQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**metadataExtractionQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**videoConversionQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**objectTaggingQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**clipEncodingQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**storageTemplateMigrationQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**backgroundTaskQueue** | [**JobCountsDto**](JobCountsDto.md) | | -**searchQueue** | [**JobCountsDto**](JobCountsDto.md) | | +**thumbnailGenerationQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**metadataExtractionQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**videoConversionQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**objectTaggingQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**clipEncodingQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**storageTemplateMigrationQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**backgroundTaskQueue** | [**JobStatusDto**](JobStatusDto.md) | | +**searchQueue** | [**JobStatusDto**](JobStatusDto.md) | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/JobApi.md b/mobile/openapi/doc/JobApi.md index f3a6b81cc..ddfbb7e74 100644 --- a/mobile/openapi/doc/JobApi.md +++ b/mobile/openapi/doc/JobApi.md @@ -63,7 +63,7 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **sendJobCommand** -> sendJobCommand(jobId, jobCommandDto) +> JobStatusDto sendJobCommand(jobId, jobCommandDto) @@ -88,7 +88,8 @@ final jobId = ; // JobName | final jobCommandDto = JobCommandDto(); // JobCommandDto | try { - api_instance.sendJobCommand(jobId, jobCommandDto); + final result = api_instance.sendJobCommand(jobId, jobCommandDto); + print(result); } catch (e) { print('Exception when calling JobApi->sendJobCommand: $e\n'); } @@ -103,7 +104,7 @@ Name | Type | Description | Notes ### Return type -void (empty response body) +[**JobStatusDto**](JobStatusDto.md) ### Authorization @@ -112,7 +113,7 @@ void (empty response body) ### HTTP request headers - **Content-Type**: application/json - - **Accept**: Not defined + - **Accept**: application/json [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/JobStatusDto.md b/mobile/openapi/doc/JobStatusDto.md new file mode 100644 index 000000000..ba85c3b25 --- /dev/null +++ b/mobile/openapi/doc/JobStatusDto.md @@ -0,0 +1,16 @@ +# openapi.model.JobStatusDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**jobCounts** | [**JobCountsDto**](JobCountsDto.md) | | +**queueStatus** | [**QueueStatusDto**](QueueStatusDto.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/QueueStatusDto.md b/mobile/openapi/doc/QueueStatusDto.md new file mode 100644 index 000000000..ca3475678 --- /dev/null +++ b/mobile/openapi/doc/QueueStatusDto.md @@ -0,0 +1,16 @@ +# openapi.model.QueueStatusDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**isActive** | **bool** | | +**isPaused** | **bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 4ccf97fad..c30576da8 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -86,12 +86,14 @@ part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; part 'model/job_name.dart'; +part 'model/job_status_dto.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; part 'model/o_auth_config_response_dto.dart'; +part 'model/queue_status_dto.dart'; part 'model/remove_assets_dto.dart'; part 'model/search_album_response_dto.dart'; part 'model/search_asset_dto.dart'; diff --git a/mobile/openapi/lib/api/job_api.dart b/mobile/openapi/lib/api/job_api.dart index 85174188a..a3c0e87aa 100644 --- a/mobile/openapi/lib/api/job_api.dart +++ b/mobile/openapi/lib/api/job_api.dart @@ -102,10 +102,18 @@ class JobApi { /// * [JobName] jobId (required): /// /// * [JobCommandDto] jobCommandDto (required): - Future sendJobCommand(JobName jobId, JobCommandDto jobCommandDto,) async { + Future sendJobCommand(JobName jobId, JobCommandDto jobCommandDto,) async { final response = await sendJobCommandWithHttpInfo(jobId, jobCommandDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'JobStatusDto',) as JobStatusDto; + + } + return null; } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7268a004e..ee3a013ec 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -280,6 +280,8 @@ class ApiClient { return JobCountsDto.fromJson(value); case 'JobName': return JobNameTypeTransformer().decode(value); + case 'JobStatusDto': + return JobStatusDto.fromJson(value); case 'LoginCredentialDto': return LoginCredentialDto.fromJson(value); case 'LoginResponseDto': @@ -292,6 +294,8 @@ class ApiClient { return OAuthConfigDto.fromJson(value); case 'OAuthConfigResponseDto': return OAuthConfigResponseDto.fromJson(value); + case 'QueueStatusDto': + return QueueStatusDto.fromJson(value); case 'RemoveAssetsDto': return RemoveAssetsDto.fromJson(value); case 'SearchAlbumResponseDto': 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 --- a/mobile/openapi/lib/model/all_job_status_response_dto.dart +++ b/mobile/openapi/lib/model/all_job_status_response_dto.dart @@ -23,21 +23,21 @@ class AllJobStatusResponseDto { required this.searchQueue, }); - JobCountsDto thumbnailGenerationQueue; + JobStatusDto thumbnailGenerationQueue; - JobCountsDto metadataExtractionQueue; + JobStatusDto metadataExtractionQueue; - JobCountsDto videoConversionQueue; + JobStatusDto videoConversionQueue; - JobCountsDto objectTaggingQueue; + JobStatusDto objectTaggingQueue; - JobCountsDto clipEncodingQueue; + JobStatusDto clipEncodingQueue; - JobCountsDto storageTemplateMigrationQueue; + JobStatusDto storageTemplateMigrationQueue; - JobCountsDto backgroundTaskQueue; + JobStatusDto backgroundTaskQueue; - JobCountsDto searchQueue; + JobStatusDto searchQueue; @override bool operator ==(Object other) => identical(this, other) || other is AllJobStatusResponseDto && @@ -97,14 +97,14 @@ class AllJobStatusResponseDto { }()); return AllJobStatusResponseDto( - thumbnailGenerationQueue: JobCountsDto.fromJson(json[r'thumbnail-generation-queue'])!, - metadataExtractionQueue: JobCountsDto.fromJson(json[r'metadata-extraction-queue'])!, - videoConversionQueue: JobCountsDto.fromJson(json[r'video-conversion-queue'])!, - objectTaggingQueue: JobCountsDto.fromJson(json[r'object-tagging-queue'])!, - clipEncodingQueue: JobCountsDto.fromJson(json[r'clip-encoding-queue'])!, - storageTemplateMigrationQueue: JobCountsDto.fromJson(json[r'storage-template-migration-queue'])!, - backgroundTaskQueue: JobCountsDto.fromJson(json[r'background-task-queue'])!, - searchQueue: JobCountsDto.fromJson(json[r'search-queue'])!, + thumbnailGenerationQueue: JobStatusDto.fromJson(json[r'thumbnail-generation-queue'])!, + metadataExtractionQueue: JobStatusDto.fromJson(json[r'metadata-extraction-queue'])!, + videoConversionQueue: JobStatusDto.fromJson(json[r'video-conversion-queue'])!, + objectTaggingQueue: JobStatusDto.fromJson(json[r'object-tagging-queue'])!, + clipEncodingQueue: JobStatusDto.fromJson(json[r'clip-encoding-queue'])!, + storageTemplateMigrationQueue: JobStatusDto.fromJson(json[r'storage-template-migration-queue'])!, + backgroundTaskQueue: JobStatusDto.fromJson(json[r'background-task-queue'])!, + searchQueue: JobStatusDto.fromJson(json[r'search-queue'])!, ); } return null; 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 --- /dev/null +++ b/mobile/openapi/lib/model/job_status_dto.dart @@ -0,0 +1,119 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class JobStatusDto { + /// Returns a new [JobStatusDto] instance. + JobStatusDto({ + required this.jobCounts, + required this.queueStatus, + }); + + JobCountsDto jobCounts; + + QueueStatusDto queueStatus; + + @override + bool operator ==(Object other) => identical(this, other) || other is JobStatusDto && + other.jobCounts == jobCounts && + other.queueStatus == queueStatus; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (jobCounts.hashCode) + + (queueStatus.hashCode); + + @override + String toString() => 'JobStatusDto[jobCounts=$jobCounts, queueStatus=$queueStatus]'; + + Map toJson() { + final json = {}; + json[r'jobCounts'] = this.jobCounts; + json[r'queueStatus'] = this.queueStatus; + return json; + } + + /// Returns a new [JobStatusDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static JobStatusDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "JobStatusDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "JobStatusDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return JobStatusDto( + jobCounts: JobCountsDto.fromJson(json[r'jobCounts'])!, + queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = JobStatusDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = JobStatusDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of JobStatusDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = JobStatusDto.listFromJson(entry.value, growable: growable,); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'jobCounts', + 'queueStatus', + }; +} + 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 --- /dev/null +++ b/mobile/openapi/lib/model/queue_status_dto.dart @@ -0,0 +1,119 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class QueueStatusDto { + /// Returns a new [QueueStatusDto] instance. + QueueStatusDto({ + required this.isActive, + required this.isPaused, + }); + + bool isActive; + + bool isPaused; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueStatusDto && + other.isActive == isActive && + other.isPaused == isPaused; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isActive.hashCode) + + (isPaused.hashCode); + + @override + String toString() => 'QueueStatusDto[isActive=$isActive, isPaused=$isPaused]'; + + Map toJson() { + final json = {}; + json[r'isActive'] = this.isActive; + json[r'isPaused'] = this.isPaused; + return json; + } + + /// Returns a new [QueueStatusDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueStatusDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), 'Required key "QueueStatusDto[$key]" is missing from JSON.'); + assert(json[key] != null, 'Required key "QueueStatusDto[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return QueueStatusDto( + isActive: mapValueOfType(json, r'isActive')!, + isPaused: mapValueOfType(json, r'isPaused')!, + ); + } + return null; + } + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueStatusDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueStatusDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueStatusDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueStatusDto.listFromJson(entry.value, growable: growable,); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isActive', + 'isPaused', + }; +} + 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 --- a/mobile/openapi/test/all_job_status_response_dto_test.dart +++ b/mobile/openapi/test/all_job_status_response_dto_test.dart @@ -16,42 +16,42 @@ void main() { // final instance = AllJobStatusResponseDto(); group('test AllJobStatusResponseDto', () { - // JobCountsDto thumbnailGenerationQueue + // JobStatusDto thumbnailGenerationQueue test('to test the property `thumbnailGenerationQueue`', () async { // TODO }); - // JobCountsDto metadataExtractionQueue + // JobStatusDto metadataExtractionQueue test('to test the property `metadataExtractionQueue`', () async { // TODO }); - // JobCountsDto videoConversionQueue + // JobStatusDto videoConversionQueue test('to test the property `videoConversionQueue`', () async { // TODO }); - // JobCountsDto objectTaggingQueue + // JobStatusDto objectTaggingQueue test('to test the property `objectTaggingQueue`', () async { // TODO }); - // JobCountsDto clipEncodingQueue + // JobStatusDto clipEncodingQueue test('to test the property `clipEncodingQueue`', () async { // TODO }); - // JobCountsDto storageTemplateMigrationQueue + // JobStatusDto storageTemplateMigrationQueue test('to test the property `storageTemplateMigrationQueue`', () async { // TODO }); - // JobCountsDto backgroundTaskQueue + // JobStatusDto backgroundTaskQueue test('to test the property `backgroundTaskQueue`', () async { // TODO }); - // JobCountsDto searchQueue + // JobStatusDto searchQueue test('to test the property `searchQueue`', () async { // TODO }); diff --git a/mobile/openapi/test/job_api_test.dart b/mobile/openapi/test/job_api_test.dart index 6f980417e..7cc1a4b2a 100644 --- a/mobile/openapi/test/job_api_test.dart +++ b/mobile/openapi/test/job_api_test.dart @@ -26,7 +26,7 @@ void main() { // // - //Future sendJobCommand(JobName jobId, JobCommandDto jobCommandDto) async + //Future sendJobCommand(JobName jobId, JobCommandDto jobCommandDto) async test('test sendJobCommand', () async { // TODO }); 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 --- /dev/null +++ b/mobile/openapi/test/job_status_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for JobStatusDto +void main() { + // final instance = JobStatusDto(); + + group('test JobStatusDto', () { + // JobCountsDto jobCounts + test('to test the property `jobCounts`', () async { + // TODO + }); + + // QueueStatusDto queueStatus + test('to test the property `queueStatus`', () async { + // TODO + }); + + + }); + +} 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 --- /dev/null +++ b/mobile/openapi/test/queue_status_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for QueueStatusDto +void main() { + // final instance = QueueStatusDto(); + + group('test QueueStatusDto', () { + // bool isActive + test('to test the property `isActive`', () async { + // TODO + }); + + // bool isPaused + test('to test the property `isPaused`', () async { + // TODO + }); + + + }); + +} 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;