Compare commits

...

5 Commits

Author SHA1 Message Date
Ben Beckford
024b7a0930 chore(server): update sql and lint 2026-07-13 04:34:29 -07:00
Ben Beckford
ba848dcd99 chore: include id in workflow log entries 2026-07-13 04:25:42 -07:00
Ben Beckford
792c8f2691 chore: update sql and openapi patch 2026-07-13 04:22:17 -07:00
Ben Beckford
3b79b471cb feat: workflow logging 2026-07-13 04:03:47 -07:00
Weblate (bot)
352c8086f9 chore(web): update translations (#29774)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ml/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Aiixi Bear <bearshen1019@outlook.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alaksandr Łopart <laporte0723@gmail.com>
Co-authored-by: Alexis Rossfelder <rossfelderalexis@gmail.com>
Co-authored-by: AntonPalmqvist <apq@users.noreply.hosted.weblate.org>
Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Biepa <github@flippie.de>
Co-authored-by: Branko Kokanovic <branko@kokanovic.org>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Dusan Hlavaty <dhlavaty@gmail.com>
Co-authored-by: Emil Warelius <emilwarelius@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Frank Paul Silye <frankps@gmail.com>
Co-authored-by: Grzegorz Leks <grzegorz.leks@gmail.com>
Co-authored-by: Gyutaek Kang <gtkang2000@naver.com>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Hosted Weblate user 157208 <benjamin3.hanke@gmail.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Huy Trieu <thetop1102@gmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jesus Barba <jesusbarba11@gmail.com>
Co-authored-by: JiZPaper <JiZPaper@gmail.com>
Co-authored-by: Jins K Joy <mailjinskjoy@gmail.com>
Co-authored-by: Jonas Franke <90050350+JonasFranke@users.noreply.github.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Jorge Montejo <jorgemon.lopez@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Ketan Avhad <ketaavha@gmail.com>
Co-authored-by: Kevin wang <jiqi963@gmail.com>
Co-authored-by: Klenner Martins Barros <klenne.al@gmail.com>
Co-authored-by: Liviu Roman <contact@liviuroman.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: Marc Portabella Navarro <portabella.marc@gmail.com>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Matteo De Carli <matteo.de.carli01@gmail.com>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: MrKoby07 <hirmerkorbinian122@gmail.com>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nico Sendan <nico@sendan.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Ohne Sonne <ohne_sonne_1312@protonmail.com>
Co-authored-by: Okie <leopetrovic11@gmail.com>
Co-authored-by: Olaf Nielsen <solluh@mail.de>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: PiTrick <dev@pitrick.link>
Co-authored-by: Rubber Ducky <rd1945@gmail.com>
Co-authored-by: Sebastian <sebastiankiwidk@gmail.com>
Co-authored-by: Simone Ognibene <ognibene2001@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: TV Box <realceday.tvbox@gmail.com>
Co-authored-by: TheDevIsHere <bagigio2005@gmail.com>
Co-authored-by: Tiago <tiagojoaquimsamsung@gmail.com>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Tom Golden <tmgldn@users.noreply.hosted.weblate.org>
Co-authored-by: Tomislav Renić <trenic@gmail.com>
Co-authored-by: Tushar Harsora <tusharharsora95@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vallabh Sharma <srivallabhsharma@gmail.com>
Co-authored-by: Yusuf Arda <yusufarda7813@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bonjur <ivanpercsity08@gmail.com>
Co-authored-by: canxunwangluo <canxunwangluo@163.com>
Co-authored-by: kylo32 <kylo32@gmail.com>
Co-authored-by: nachtpfoetchen <nachtpfoetchen@posteo.de>
Co-authored-by: pluwen chen <pluwen@gmail.com>
Co-authored-by: puv <dev@puv.pm>
Co-authored-by: rubes <mail@armd.one>
Co-authored-by: vatahome <vataszilamer@pm.me>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2026-07-09 20:01:50 +00:00
33 changed files with 1207 additions and 31 deletions

View File

@@ -1787,6 +1787,7 @@
"restore_all": "Restore all",
"restore_user": "Restore user",
"restored_asset": "Restored asset",
"result": "Result",
"resume": "Resume",
"resume_paused_jobs": "Resume {count, plural, one {# paused job} other {# paused jobs}}",
"review_duplicates": "Review duplicates",
@@ -2273,6 +2274,13 @@
"workflow_info": "Workflow info",
"workflow_json": "Workflow JSON",
"workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.",
"workflow_logging_completed": "Completed",
"workflow_logging_disable": "Disable logging",
"workflow_logging_disabled_description": "Logging is currently disabled for this workflow.",
"workflow_logging_enable": "Enable logging",
"workflow_logging_error_step": "Error on step #{step}",
"workflow_logging_halted": "Halted",
"workflow_logging_halted_step": "Halted on step #{step}",
"workflow_name": "Workflow name",
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
"workflow_summary": "Workflow summary",

View File

@@ -431,8 +431,8 @@
"transcoding_realtime_description": "Permet au transcodage d'être réalisé en temps réel durant la diffusion du flux de la vidéo. Active la bascule automatique entre résolutions, mais peut entraîner une latence importante de lecture et des microcoupures en fonction des capacités du serveur.",
"transcoding_realtime_enabled": "Activer la transcodification en temps réel",
"transcoding_realtime_enabled_description": "Si désactivé, le serveur refusera de démarrer de nouvelles sessions de transcodifications en temps réel.",
"transcoding_realtime_resolutions": "Définitions",
"transcoding_realtime_resolutions_description": "Les définitions proposées pour le transcodage en temps réel. Des définitions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
"transcoding_realtime_resolutions": "Résolutions",
"transcoding_realtime_resolutions_description": "Les résolutions proposées pour le transcodage en temps réel. Des résolutions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
"transcoding_realtime_video_codecs": "Codecs vidéo",
"transcoding_realtime_video_codecs_description": "Les codecs vidéo proposés pour le transcodage en temps réel. Les clients choisissent la meilleure option qu'ils supportent durant la lecture. AV1 est plus efficace que HEVC, qui est plus efficace que H.264. En utilisant l'accélération matérielle, sélectionnez seulement les codecs que l'accélérateur peut encoder. En utilisant le transcodage logiciel, notez que H.264 est plus rapide que AV1, qui est plus rapide que HEVC.",
"transcoding_reference_frames": "Trames de référence",

View File

@@ -41,6 +41,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
'SyncAssetV1': {'isEdited': false},
'ServerFeaturesDto': {'ocr': false, 'realtimeTranscoding': false},
'MemoriesResponse': {'duration': 5},
'WorkflowResponseDto': {'logging': false},
};
void upgradeDto(dynamic value, String targetType) {

View File

@@ -341,6 +341,7 @@ Class | Method | HTTP request | Description
*WorkflowsApi* | [**deleteWorkflow**](doc//WorkflowsApi.md#deleteworkflow) | **DELETE** /workflows/{id} | Delete a workflow
*WorkflowsApi* | [**getWorkflow**](doc//WorkflowsApi.md#getworkflow) | **GET** /workflows/{id} | Retrieve a workflow
*WorkflowsApi* | [**getWorkflowForShare**](doc//WorkflowsApi.md#getworkflowforshare) | **GET** /workflows/{id}/share | Retrieve a workflow
*WorkflowsApi* | [**getWorkflowLogs**](doc//WorkflowsApi.md#getworkflowlogs) | **GET** /workflows/{id}/logs | Retrieve a workflow
*WorkflowsApi* | [**getWorkflowTriggers**](doc//WorkflowsApi.md#getworkflowtriggers) | **GET** /workflows/triggers | List all workflow triggers
*WorkflowsApi* | [**searchWorkflows**](doc//WorkflowsApi.md#searchworkflows) | **GET** /workflows | List all workflows
*WorkflowsApi* | [**updateWorkflow**](doc//WorkflowsApi.md#updateworkflow) | **PUT** /workflows/{id} | Update a workflow
@@ -711,7 +712,10 @@ Class | Method | HTTP request | Description
- [VideoCodec](doc//VideoCodec.md)
- [VideoContainer](doc//VideoContainer.md)
- [WorkflowCreateDto](doc//WorkflowCreateDto.md)
- [WorkflowLogEntryDto](doc//WorkflowLogEntryDto.md)
- [WorkflowLogEntryDtoLastStep](doc//WorkflowLogEntryDtoLastStep.md)
- [WorkflowResponseDto](doc//WorkflowResponseDto.md)
- [WorkflowResult](doc//WorkflowResult.md)
- [WorkflowShareResponseDto](doc//WorkflowShareResponseDto.md)
- [WorkflowShareStepDto](doc//WorkflowShareStepDto.md)
- [WorkflowStepDto](doc//WorkflowStepDto.md)

View File

@@ -432,7 +432,10 @@ part 'model/version_check_state_response_dto.dart';
part 'model/video_codec.dart';
part 'model/video_container.dart';
part 'model/workflow_create_dto.dart';
part 'model/workflow_log_entry_dto.dart';
part 'model/workflow_log_entry_dto_last_step.dart';
part 'model/workflow_response_dto.dart';
part 'model/workflow_result.dart';
part 'model/workflow_share_response_dto.dart';
part 'model/workflow_share_step_dto.dart';
part 'model/workflow_step_dto.dart';

View File

@@ -239,6 +239,95 @@ class WorkflowsApi {
return null;
}
/// Retrieve a workflow
///
/// Retrieve a workflow details without ids, default values, etc.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [DateTime] before:
/// Filter by runs before a date/time
///
/// * [int] limit:
/// Maximum number of logs
///
/// * [WorkflowResult] result:
/// Filter by run result
Future<Response> getWorkflowLogsWithHttpInfo(String id, { DateTime? before, int? limit, WorkflowResult? result, Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/workflows/{id}/logs'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (before != null) {
queryParams.addAll(_queryParams('', 'before', before));
}
if (limit != null) {
queryParams.addAll(_queryParams('', 'limit', limit));
}
if (result != null) {
queryParams.addAll(_queryParams('', 'result', result));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Retrieve a workflow
///
/// Retrieve a workflow details without ids, default values, etc.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [DateTime] before:
/// Filter by runs before a date/time
///
/// * [int] limit:
/// Maximum number of logs
///
/// * [WorkflowResult] result:
/// Filter by run result
Future<List<WorkflowLogEntryDto>?> getWorkflowLogs(String id, { DateTime? before, int? limit, WorkflowResult? result, Future<void>? abortTrigger, }) async {
final response = await getWorkflowLogsWithHttpInfo(id, before: before, limit: limit, result: result, abortTrigger: abortTrigger,);
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<WorkflowLogEntryDto>') as List)
.cast<WorkflowLogEntryDto>()
.toList(growable: false);
}
return null;
}
/// List all workflow triggers
///
/// Retrieve a list of all available workflow triggers.
@@ -308,12 +397,15 @@ class WorkflowsApi {
/// * [String] id:
/// Workflow ID
///
/// * [bool] logging:
/// Workflow logs run results
///
/// * [String] name:
/// Workflow name
///
/// * [WorkflowTrigger] trigger:
/// Workflow trigger type
Future<Response> searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
Future<Response> searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, bool? logging, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/workflows';
@@ -333,6 +425,9 @@ class WorkflowsApi {
if (id != null) {
queryParams.addAll(_queryParams('', 'id', id));
}
if (logging != null) {
queryParams.addAll(_queryParams('', 'logging', logging));
}
if (name != null) {
queryParams.addAll(_queryParams('', 'name', name));
}
@@ -370,13 +465,16 @@ class WorkflowsApi {
/// * [String] id:
/// Workflow ID
///
/// * [bool] logging:
/// Workflow logs run results
///
/// * [String] name:
/// Workflow name
///
/// * [WorkflowTrigger] trigger:
/// Workflow trigger type
Future<List<WorkflowResponseDto>?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
final response = await searchWorkflowsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, trigger: trigger, abortTrigger: abortTrigger,);
Future<List<WorkflowResponseDto>?> searchWorkflows({ String? description, bool? enabled, String? id, bool? logging, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
final response = await searchWorkflowsWithHttpInfo(description: description, enabled: enabled, id: id, logging: logging, name: name, trigger: trigger, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -909,8 +909,14 @@ class ApiClient {
return VideoContainerTypeTransformer().decode(value);
case 'WorkflowCreateDto':
return WorkflowCreateDto.fromJson(value);
case 'WorkflowLogEntryDto':
return WorkflowLogEntryDto.fromJson(value);
case 'WorkflowLogEntryDtoLastStep':
return WorkflowLogEntryDtoLastStep.fromJson(value);
case 'WorkflowResponseDto':
return WorkflowResponseDto.fromJson(value);
case 'WorkflowResult':
return WorkflowResultTypeTransformer().decode(value);
case 'WorkflowShareResponseDto':
return WorkflowShareResponseDto.fromJson(value);
case 'WorkflowShareStepDto':

View File

@@ -214,6 +214,9 @@ String parameterToString(dynamic value) {
if (value is VideoContainer) {
return VideoContainerTypeTransformer().encode(value).toString();
}
if (value is WorkflowResult) {
return WorkflowResultTypeTransformer().encode(value).toString();
}
if (value is WorkflowTrigger) {
return WorkflowTriggerTypeTransformer().encode(value).toString();
}

View File

@@ -172,6 +172,7 @@ class Permission {
static const workflowPeriodRead = Permission._(r'workflow.read');
static const workflowPeriodUpdate = Permission._(r'workflow.update');
static const workflowPeriodDelete = Permission._(r'workflow.delete');
static const workflowPeriodLogs = Permission._(r'workflow.logs');
static const adminUserPeriodCreate = Permission._(r'adminUser.create');
static const adminUserPeriodRead = Permission._(r'adminUser.read');
static const adminUserPeriodUpdate = Permission._(r'adminUser.update');
@@ -330,6 +331,7 @@ class Permission {
workflowPeriodRead,
workflowPeriodUpdate,
workflowPeriodDelete,
workflowPeriodLogs,
adminUserPeriodCreate,
adminUserPeriodRead,
adminUserPeriodUpdate,
@@ -523,6 +525,7 @@ class PermissionTypeTransformer {
case r'workflow.read': return Permission.workflowPeriodRead;
case r'workflow.update': return Permission.workflowPeriodUpdate;
case r'workflow.delete': return Permission.workflowPeriodDelete;
case r'workflow.logs': return Permission.workflowPeriodLogs;
case r'adminUser.create': return Permission.adminUserPeriodCreate;
case r'adminUser.read': return Permission.adminUserPeriodRead;
case r'adminUser.update': return Permission.adminUserPeriodUpdate;

View File

@@ -15,6 +15,7 @@ class WorkflowCreateDto {
WorkflowCreateDto({
this.description = const Optional.absent(),
this.enabled = const Optional.absent(),
this.logging = const Optional.absent(),
this.name = const Optional.absent(),
this.steps = const Optional.present(const []),
required this.trigger,
@@ -32,6 +33,15 @@ class WorkflowCreateDto {
///
Optional<bool?> enabled;
/// Workflow logs run results
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Optional<bool?> logging;
/// Workflow name
Optional<String?> name;
@@ -43,6 +53,7 @@ class WorkflowCreateDto {
bool operator ==(Object other) => identical(this, other) || other is WorkflowCreateDto &&
other.description == description &&
other.enabled == enabled &&
other.logging == logging &&
other.name == name &&
_deepEquality.equals(other.steps, steps) &&
other.trigger == trigger;
@@ -52,12 +63,13 @@ class WorkflowCreateDto {
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(enabled == null ? 0 : enabled!.hashCode) +
(logging == null ? 0 : logging!.hashCode) +
(name == null ? 0 : name!.hashCode) +
(steps.hashCode) +
(trigger.hashCode);
@override
String toString() => 'WorkflowCreateDto[description=$description, enabled=$enabled, name=$name, steps=$steps, trigger=$trigger]';
String toString() => 'WorkflowCreateDto[description=$description, enabled=$enabled, logging=$logging, name=$name, steps=$steps, trigger=$trigger]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -69,6 +81,10 @@ class WorkflowCreateDto {
final value = this.enabled.value;
json[r'enabled'] = value;
}
if (this.logging.isPresent) {
final value = this.logging.value;
json[r'logging'] = value;
}
if (this.name.isPresent) {
final value = this.name.value;
json[r'name'] = value;
@@ -92,6 +108,7 @@ class WorkflowCreateDto {
return WorkflowCreateDto(
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
enabled: json.containsKey(r'enabled') ? Optional.present(mapValueOfType<bool>(json, r'enabled')) : const Optional.absent(),
logging: json.containsKey(r'logging') ? Optional.present(mapValueOfType<bool>(json, r'logging')) : const Optional.absent(),
name: json.containsKey(r'name') ? Optional.present(mapValueOfType<String>(json, r'name')) : const Optional.absent(),
steps: json.containsKey(r'steps') ? Optional.present(WorkflowStepDto.listFromJson(json[r'steps'])) : const Optional.absent(),
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,

View File

@@ -0,0 +1,150 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 WorkflowLogEntryDto {
/// Returns a new [WorkflowLogEntryDto] instance.
WorkflowLogEntryDto({
required this.at,
required this.id,
this.lastStep = const Optional.absent(),
required this.result,
this.triggerDataId = const Optional.absent(),
});
/// Workflow run date/time
String at;
/// Workflow log entry ID
String id;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Optional<WorkflowLogEntryDtoLastStep?> lastStep;
WorkflowResult result;
/// Workflow trigger data ID
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Optional<String?> triggerDataId;
@override
bool operator ==(Object other) => identical(this, other) || other is WorkflowLogEntryDto &&
other.at == at &&
other.id == id &&
other.lastStep == lastStep &&
other.result == result &&
other.triggerDataId == triggerDataId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(at.hashCode) +
(id.hashCode) +
(lastStep == null ? 0 : lastStep!.hashCode) +
(result.hashCode) +
(triggerDataId == null ? 0 : triggerDataId!.hashCode);
@override
String toString() => 'WorkflowLogEntryDto[at=$at, id=$id, lastStep=$lastStep, result=$result, triggerDataId=$triggerDataId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'at'] = this.at;
json[r'id'] = this.id;
if (this.lastStep.isPresent) {
final value = this.lastStep.value;
json[r'lastStep'] = value;
}
json[r'result'] = this.result;
if (this.triggerDataId.isPresent) {
final value = this.triggerDataId.value;
json[r'triggerDataId'] = value;
}
return json;
}
/// Returns a new [WorkflowLogEntryDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static WorkflowLogEntryDto? fromJson(dynamic value) {
upgradeDto(value, "WorkflowLogEntryDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return WorkflowLogEntryDto(
at: mapValueOfType<String>(json, r'at')!,
id: mapValueOfType<String>(json, r'id')!,
lastStep: json.containsKey(r'lastStep') ? Optional.present(WorkflowLogEntryDtoLastStep.fromJson(json[r'lastStep'])) : const Optional.absent(),
result: WorkflowResult.fromJson(json[r'result'])!,
triggerDataId: json.containsKey(r'triggerDataId') ? Optional.present(mapValueOfType<String>(json, r'triggerDataId')) : const Optional.absent(),
);
}
return null;
}
static List<WorkflowLogEntryDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <WorkflowLogEntryDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = WorkflowLogEntryDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, WorkflowLogEntryDto> mapFromJson(dynamic json) {
final map = <String, WorkflowLogEntryDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = WorkflowLogEntryDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of WorkflowLogEntryDto-objects as value to a dart map
static Map<String, List<WorkflowLogEntryDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<WorkflowLogEntryDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = WorkflowLogEntryDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'at',
'id',
'result',
};
}

View File

@@ -0,0 +1,112 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 WorkflowLogEntryDtoLastStep {
/// Returns a new [WorkflowLogEntryDtoLastStep] instance.
WorkflowLogEntryDtoLastStep({
required this.index,
required this.method,
});
/// Index of the step in the workflow
///
/// Minimum value: 0
/// Maximum value: 9007199254740991
int index;
/// Method of the step
String method;
@override
bool operator ==(Object other) => identical(this, other) || other is WorkflowLogEntryDtoLastStep &&
other.index == index &&
other.method == method;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(index.hashCode) +
(method.hashCode);
@override
String toString() => 'WorkflowLogEntryDtoLastStep[index=$index, method=$method]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'index'] = this.index;
json[r'method'] = this.method;
return json;
}
/// Returns a new [WorkflowLogEntryDtoLastStep] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static WorkflowLogEntryDtoLastStep? fromJson(dynamic value) {
upgradeDto(value, "WorkflowLogEntryDtoLastStep");
if (value is Map) {
final json = value.cast<String, dynamic>();
return WorkflowLogEntryDtoLastStep(
index: mapValueOfType<int>(json, r'index')!,
method: mapValueOfType<String>(json, r'method')!,
);
}
return null;
}
static List<WorkflowLogEntryDtoLastStep> listFromJson(dynamic json, {bool growable = false,}) {
final result = <WorkflowLogEntryDtoLastStep>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = WorkflowLogEntryDtoLastStep.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, WorkflowLogEntryDtoLastStep> mapFromJson(dynamic json) {
final map = <String, WorkflowLogEntryDtoLastStep>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = WorkflowLogEntryDtoLastStep.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of WorkflowLogEntryDtoLastStep-objects as value to a dart map
static Map<String, List<WorkflowLogEntryDtoLastStep>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<WorkflowLogEntryDtoLastStep>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = WorkflowLogEntryDtoLastStep.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'index',
'method',
};
}

View File

@@ -17,6 +17,7 @@ class WorkflowResponseDto {
required this.description,
required this.enabled,
required this.id,
required this.logging,
required this.name,
this.steps = const [],
required this.trigger,
@@ -35,6 +36,9 @@ class WorkflowResponseDto {
/// Workflow ID
String id;
/// Workflow logs run results
bool logging;
/// Workflow name
String? name;
@@ -52,6 +56,7 @@ class WorkflowResponseDto {
other.description == description &&
other.enabled == enabled &&
other.id == id &&
other.logging == logging &&
other.name == name &&
_deepEquality.equals(other.steps, steps) &&
other.trigger == trigger &&
@@ -64,13 +69,14 @@ class WorkflowResponseDto {
(description == null ? 0 : description!.hashCode) +
(enabled.hashCode) +
(id.hashCode) +
(logging.hashCode) +
(name == null ? 0 : name!.hashCode) +
(steps.hashCode) +
(trigger.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'WorkflowResponseDto[createdAt=$createdAt, description=$description, enabled=$enabled, id=$id, name=$name, steps=$steps, trigger=$trigger, updatedAt=$updatedAt]';
String toString() => 'WorkflowResponseDto[createdAt=$createdAt, description=$description, enabled=$enabled, id=$id, logging=$logging, name=$name, steps=$steps, trigger=$trigger, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -82,6 +88,7 @@ class WorkflowResponseDto {
}
json[r'enabled'] = this.enabled;
json[r'id'] = this.id;
json[r'logging'] = this.logging;
if (this.name != null) {
json[r'name'] = this.name;
} else {
@@ -106,6 +113,7 @@ class WorkflowResponseDto {
description: mapValueOfType<String>(json, r'description'),
enabled: mapValueOfType<bool>(json, r'enabled')!,
id: mapValueOfType<String>(json, r'id')!,
logging: mapValueOfType<bool>(json, r'logging')!,
name: mapValueOfType<String>(json, r'name'),
steps: WorkflowStepDto.listFromJson(json[r'steps']),
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
@@ -161,6 +169,7 @@ class WorkflowResponseDto {
'description',
'enabled',
'id',
'logging',
'name',
'steps',
'trigger',

View File

@@ -0,0 +1,88 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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;
/// Workflow run result
class WorkflowResult {
/// Instantiate a new enum with the provided [value].
const WorkflowResult._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const completed = WorkflowResult._(r'completed');
static const halted = WorkflowResult._(r'halted');
static const error = WorkflowResult._(r'error');
/// List of all possible values in this [enum][WorkflowResult].
static const values = <WorkflowResult>[
completed,
halted,
error,
];
static WorkflowResult? fromJson(dynamic value) => WorkflowResultTypeTransformer().decode(value);
static List<WorkflowResult> listFromJson(dynamic json, {bool growable = false,}) {
final result = <WorkflowResult>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = WorkflowResult.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [WorkflowResult] to String,
/// and [decode] dynamic data back to [WorkflowResult].
class WorkflowResultTypeTransformer {
factory WorkflowResultTypeTransformer() => _instance ??= const WorkflowResultTypeTransformer._();
const WorkflowResultTypeTransformer._();
String encode(WorkflowResult data) => data.value;
/// Decodes a [dynamic value][data] to a WorkflowResult.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
WorkflowResult? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'completed': return WorkflowResult.completed;
case r'halted': return WorkflowResult.halted;
case r'error': return WorkflowResult.error;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [WorkflowResultTypeTransformer] instance.
static WorkflowResultTypeTransformer? _instance;
}

View File

@@ -15,6 +15,7 @@ class WorkflowUpdateDto {
WorkflowUpdateDto({
this.description = const Optional.absent(),
this.enabled = const Optional.absent(),
this.logging = const Optional.absent(),
this.name = const Optional.absent(),
this.steps = const Optional.present(const []),
this.trigger = const Optional.absent(),
@@ -32,6 +33,15 @@ class WorkflowUpdateDto {
///
Optional<bool?> enabled;
/// Workflow logs run results
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Optional<bool?> logging;
/// Workflow name
Optional<String?> name;
@@ -49,6 +59,7 @@ class WorkflowUpdateDto {
bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto &&
other.description == description &&
other.enabled == enabled &&
other.logging == logging &&
other.name == name &&
_deepEquality.equals(other.steps, steps) &&
other.trigger == trigger;
@@ -58,12 +69,13 @@ class WorkflowUpdateDto {
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(enabled == null ? 0 : enabled!.hashCode) +
(logging == null ? 0 : logging!.hashCode) +
(name == null ? 0 : name!.hashCode) +
(steps.hashCode) +
(trigger == null ? 0 : trigger!.hashCode);
@override
String toString() => 'WorkflowUpdateDto[description=$description, enabled=$enabled, name=$name, steps=$steps, trigger=$trigger]';
String toString() => 'WorkflowUpdateDto[description=$description, enabled=$enabled, logging=$logging, name=$name, steps=$steps, trigger=$trigger]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -75,6 +87,10 @@ class WorkflowUpdateDto {
final value = this.enabled.value;
json[r'enabled'] = value;
}
if (this.logging.isPresent) {
final value = this.logging.value;
json[r'logging'] = value;
}
if (this.name.isPresent) {
final value = this.name.value;
json[r'name'] = value;
@@ -101,6 +117,7 @@ class WorkflowUpdateDto {
return WorkflowUpdateDto(
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
enabled: json.containsKey(r'enabled') ? Optional.present(mapValueOfType<bool>(json, r'enabled')) : const Optional.absent(),
logging: json.containsKey(r'logging') ? Optional.present(mapValueOfType<bool>(json, r'logging')) : const Optional.absent(),
name: json.containsKey(r'name') ? Optional.present(mapValueOfType<String>(json, r'name')) : const Optional.absent(),
steps: json.containsKey(r'steps') ? Optional.present(WorkflowStepDto.listFromJson(json[r'steps'])) : const Optional.absent(),
trigger: json.containsKey(r'trigger') ? Optional.present(WorkflowTrigger.fromJson(json[r'trigger'])) : const Optional.absent(),

View File

@@ -15854,6 +15854,15 @@
"type": "string"
}
},
{
"name": "logging",
"required": false,
"in": "query",
"description": "Workflow logs run results",
"schema": {
"type": "boolean"
}
},
{
"name": "name",
"required": false,
@@ -16167,6 +16176,95 @@
"x-immich-state": "Deprecated"
}
},
"/workflows/{id}/logs": {
"get": {
"description": "Retrieve a workflow details without ids, default values, etc.",
"operationId": "getWorkflowLogs",
"parameters": [
{
"name": "before",
"required": false,
"in": "query",
"description": "Filter by runs before a date/time",
"schema": {
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"example": "2024-01-01T00:00:00.000Z",
"type": "string"
}
},
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
}
},
{
"name": "limit",
"required": false,
"in": "query",
"description": "Maximum number of logs",
"schema": {
"maximum": 9007199254740991,
"exclusiveMinimum": true,
"default": 50,
"type": "integer",
"minimum": 0
}
},
{
"name": "result",
"required": false,
"in": "query",
"description": "Filter by run result",
"schema": {
"$ref": "#/components/schemas/WorkflowResult"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/WorkflowLogEntryDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Retrieve a workflow",
"tags": [
"Workflows"
],
"x-immich-history": [
{
"version": "v3.0.0",
"state": "Added"
}
],
"x-immich-permission": "workflow.logs"
}
},
"/workflows/{id}/share": {
"get": {
"description": "Retrieve a workflow details without ids, default values, etc.",
@@ -21015,6 +21113,7 @@
"workflow.read",
"workflow.update",
"workflow.delete",
"workflow.logs",
"adminUser.create",
"adminUser.read",
"adminUser.update",
@@ -27855,6 +27954,10 @@
"description": "Workflow enabled",
"type": "boolean"
},
"logging": {
"description": "Workflow logs run results",
"type": "boolean"
},
"name": {
"description": "Workflow name",
"nullable": true,
@@ -27876,6 +27979,56 @@
],
"type": "object"
},
"WorkflowLogEntryDto": {
"properties": {
"at": {
"description": "Workflow run date/time",
"type": "string"
},
"id": {
"description": "Workflow log entry ID",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
},
"lastStep": {
"description": "Last step ran, if the workflow ended early",
"properties": {
"index": {
"description": "Index of the step in the workflow",
"exclusiveMinimum": true,
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"method": {
"description": "Method of the step",
"type": "string"
}
},
"required": [
"method",
"index"
],
"type": "object"
},
"result": {
"$ref": "#/components/schemas/WorkflowResult"
},
"triggerDataId": {
"description": "Workflow trigger data ID",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"type": "string"
}
},
"required": [
"at",
"id",
"result"
],
"type": "object"
},
"WorkflowResponseDto": {
"properties": {
"createdAt": {
@@ -27897,6 +28050,10 @@
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
},
"logging": {
"description": "Workflow logs run results",
"type": "boolean"
},
"name": {
"description": "Workflow name",
"nullable": true,
@@ -27923,6 +28080,7 @@
"description",
"enabled",
"id",
"logging",
"name",
"steps",
"trigger",
@@ -27930,6 +28088,15 @@
],
"type": "object"
},
"WorkflowResult": {
"description": "Workflow run result",
"enum": [
"completed",
"halted",
"error"
],
"type": "string"
},
"WorkflowShareResponseDto": {
"properties": {
"description": {
@@ -28054,6 +28221,10 @@
"description": "Workflow enabled",
"type": "boolean"
},
"logging": {
"description": "Workflow logs run results",
"type": "boolean"
},
"name": {
"description": "Workflow name",
"nullable": true,

View File

@@ -2791,6 +2791,8 @@ export type WorkflowResponseDto = {
enabled: boolean;
/** Workflow ID */
id: string;
/** Workflow logs run results */
logging: boolean;
/** Workflow name */
name: string | null;
/** Workflow steps */
@@ -2805,6 +2807,8 @@ export type WorkflowCreateDto = {
description?: string | null;
/** Workflow enabled */
enabled?: boolean;
/** Workflow logs run results */
logging?: boolean;
/** Workflow name */
name?: string | null;
steps?: WorkflowStepDto[];
@@ -2822,12 +2826,30 @@ export type WorkflowUpdateDto = {
description?: string | null;
/** Workflow enabled */
enabled?: boolean;
/** Workflow logs run results */
logging?: boolean;
/** Workflow name */
name?: string | null;
steps?: WorkflowStepDto[];
/** Workflow trigger type */
trigger?: WorkflowTrigger;
};
export type WorkflowLogEntryDto = {
/** Workflow run date/time */
at: string;
/** Workflow log entry ID */
id: string;
/** Last step ran, if the workflow ended early */
lastStep?: {
/** Index of the step in the workflow */
index: number;
/** Method of the step */
method: string;
};
result: WorkflowResult;
/** Workflow trigger data ID */
triggerDataId?: string;
};
export type WorkflowShareStepDto = {
/** Step configuration */
config: {
@@ -6977,10 +6999,11 @@ export function getUniqueOriginalPaths(opts?: Oazapfts.RequestOpts) {
/**
* List all workflows
*/
export function searchWorkflows({ description, enabled, id, name, trigger }: {
export function searchWorkflows({ description, enabled, id, logging, name, trigger }: {
description?: string;
enabled?: boolean;
id?: string;
logging?: boolean;
name?: string;
trigger?: WorkflowTrigger;
}, opts?: Oazapfts.RequestOpts) {
@@ -6991,6 +7014,7 @@ export function searchWorkflows({ description, enabled, id, name, trigger }: {
description,
enabled,
id,
logging,
name,
trigger
}))}`, {
@@ -7063,6 +7087,26 @@ export function updateWorkflow({ id, workflowUpdateDto }: {
body: workflowUpdateDto
})));
}
/**
* Retrieve a workflow
*/
export function getWorkflowLogs({ before, id, limit, result }: {
before?: string;
id: string;
limit?: number;
result?: WorkflowResult;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: WorkflowLogEntryDto[];
}>(`/workflows/${encodeURIComponent(id)}/logs${QS.query(QS.explode({
before,
limit,
result
}))}`, {
...opts
}));
}
/**
* Retrieve a workflow
*/
@@ -7310,6 +7354,7 @@ export enum Permission {
WorkflowRead = "workflow.read",
WorkflowUpdate = "workflow.update",
WorkflowDelete = "workflow.delete",
WorkflowLogs = "workflow.logs",
AdminUserCreate = "adminUser.create",
AdminUserRead = "adminUser.read",
AdminUserUpdate = "adminUser.update",
@@ -7686,6 +7731,11 @@ export enum AssetOrderBy {
TakenAt = "takenAt",
CreatedAt = "createdAt"
}
export enum WorkflowResult {
Completed = "completed",
Halted = "halted",
Error = "error"
}
export enum ReleaseType {
Major = "major",
Premajor = "premajor",

View File

@@ -4,6 +4,8 @@ import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import {
WorkflowCreateDto,
WorkflowGetLogsDto,
WorkflowLogEntryDto,
WorkflowResponseDto,
WorkflowSearchDto,
WorkflowShareResponseDto,
@@ -113,4 +115,19 @@ export class WorkflowController {
deleteWorkflow(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
@Get(':id/logs')
@Authenticated({ permission: Permission.WorkflowLogs })
@Endpoint({
summary: 'Retrieve a workflow',
description: 'Retrieve a workflow details without ids, default values, etc.',
history: HistoryBuilder.v3(),
})
getWorkflowLogs(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Query() dto: WorkflowGetLogsDto,
): Promise<WorkflowLogEntryDto[]> {
return this.service.getLogs(auth, id, dto);
}
}

View File

@@ -1,6 +1,7 @@
import type { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
import { createZodDto } from 'nestjs-zod';
import { WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
import { WorkflowResultSchema, WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
import { isoDatetimeToDate } from 'src/validation';
import z from 'zod';
const WorkflowTriggerResponseSchema = z
@@ -17,6 +18,7 @@ const WorkflowSearchSchema = z
name: z.string().optional().describe('Workflow name'),
description: z.string().optional().describe('Workflow description'),
enabled: z.boolean().optional().describe('Workflow enabled'),
logging: z.boolean().optional().describe('Workflow logs run results'),
})
.meta({ id: 'WorkflowSearchDto' });
@@ -42,6 +44,7 @@ const WorkflowCreateSchema = z
name: z.string().nullable().optional().describe('Workflow name'),
description: z.string().nullable().optional().describe('Workflow description'),
enabled: z.boolean().optional().describe('Workflow enabled'),
logging: z.boolean().optional().describe('Workflow logs run results'),
steps: z.array(WorkflowStepSchema).optional(),
})
.meta({ id: 'WorkflowCreateDto' });
@@ -52,6 +55,7 @@ const WorkflowUpdateSchema = z
name: z.string().nullable().optional().describe('Workflow name'),
description: z.string().nullable().optional().describe('Workflow description'),
enabled: z.boolean().optional().describe('Workflow enabled'),
logging: z.boolean().optional().describe('Workflow logs run results'),
steps: z.array(WorkflowStepSchema).optional(),
})
.meta({ id: 'WorkflowUpdateDto' });
@@ -65,6 +69,7 @@ const WorkflowResponseSchema = z
createdAt: z.string().describe('Creation date'),
updatedAt: z.string().describe('Update date'),
enabled: z.boolean().describe('Workflow enabled'),
logging: z.boolean().describe('Workflow logs run results'),
steps: z.array(WorkflowStepSchema).describe('Workflow steps'),
})
.meta({ id: 'WorkflowResponseDto' });
@@ -78,12 +83,36 @@ const WorkflowShareResponseSchema = z
})
.meta({ id: 'WorkflowShareResponseDto' });
const WorkflowLogEntrySchema = z
.object({
id: z.uuidv4().describe('Workflow log entry ID'),
at: z.string().describe('Workflow run date/time'),
result: WorkflowResultSchema.describe('Workflow run result'),
triggerDataId: z.uuid().optional().describe('Workflow trigger data ID'),
lastStep: z
.object({
method: z.string().describe('Method of the step'),
index: z.int().positive().describe('Index of the step in the workflow'),
})
.optional()
.describe('Last step ran, if the workflow ended early'),
})
.meta({ id: 'WorkflowLogEntryDto' });
const WorkflowGetLogsSchema = z.object({
result: WorkflowResultSchema.optional().describe('Filter by run result'),
before: isoDatetimeToDate.optional().describe('Filter by runs before a date/time'),
limit: z.coerce.number().int().positive().default(50).describe('Maximum number of logs'),
});
export class WorkflowTriggerResponseDto extends createZodDto(WorkflowTriggerResponseSchema) {}
export class WorkflowSearchDto extends createZodDto(WorkflowSearchSchema) {}
export class WorkflowCreateDto extends createZodDto(WorkflowCreateSchema) {}
export class WorkflowUpdateDto extends createZodDto(WorkflowUpdateSchema) {}
export class WorkflowResponseDto extends createZodDto(WorkflowResponseSchema) {}
export class WorkflowShareResponseDto extends createZodDto(WorkflowShareResponseSchema) {}
export class WorkflowLogEntryDto extends createZodDto(WorkflowLogEntrySchema) {}
export class WorkflowGetLogsDto extends createZodDto(WorkflowGetLogsSchema) {}
type Workflow = {
id: string;
@@ -93,6 +122,7 @@ type Workflow = {
name: string | null;
description: string | null;
enabled: boolean;
logging: boolean;
};
type WorkflowStep = {
@@ -107,6 +137,7 @@ export const mapWorkflow = (workflow: Workflow & { steps: WorkflowStep[] }): Wor
id: workflow.id,
enabled: workflow.enabled,
trigger: workflow.trigger,
logging: workflow.logging,
name: workflow.name,
description: workflow.description,
createdAt: workflow.createdAt.toISOString(),

View File

@@ -298,6 +298,7 @@ export enum Permission {
WorkflowRead = 'workflow.read',
WorkflowUpdate = 'workflow.update',
WorkflowDelete = 'workflow.delete',
WorkflowLogs = 'workflow.logs',
AdminUserCreate = 'adminUser.create',
AdminUserRead = 'adminUser.read',
@@ -1234,3 +1235,14 @@ export enum CalendarHeatmapType {
Upload = 'Upload',
Taken = 'Taken',
}
export enum WorkflowResult {
Completed = 'completed',
Halted = 'halted',
Error = 'error',
}
export const WorkflowResultSchema = z
.enum(WorkflowResult)
.describe('Workflow run result')
.meta({ id: 'WorkflowResult' });

View File

@@ -9,6 +9,7 @@ select
"workflow"."enabled",
"workflow"."createdAt",
"workflow"."updatedAt",
"workflow"."logging",
(
select
coalesce(json_agg(agg), '[]')
@@ -41,6 +42,7 @@ select
"workflow"."enabled",
"workflow"."createdAt",
"workflow"."updatedAt",
"workflow"."logging",
(
select
coalesce(json_agg(agg), '[]')
@@ -69,6 +71,7 @@ select
"workflow"."id",
"workflow"."name",
"workflow"."trigger",
"workflow"."logging",
(
select
coalesce(json_agg(agg), '[]')
@@ -96,6 +99,40 @@ where
"id" = $2
and "enabled" = $3
-- WorkflowRepository.getLogs
select
"workflow_log"."id",
"workflow_log"."createdAt",
"workflow_log"."halted",
"workflow_log"."error",
"workflow_log"."workflowId",
"workflow_log"."workflowStepId",
"workflow_log"."triggerDataId",
(
select
to_json(obj)
from
(
select
"plugin_method"."pluginId",
"plugin_method"."name" as "methodName",
"workflow_step"."order"
from
"workflow_step"
inner join "plugin_method" on "plugin_method"."id" = "workflow_step"."pluginMethodId"
where
"workflow_step"."id" = "workflow_log"."workflowStepId"
) as obj
) as "step"
from
"workflow_log"
where
"workflow_log"."workflowId" = $1
order by
"workflow_log"."createdAt" desc
limit
$2
-- WorkflowRepository.delete
delete from "workflow"
where

View File

@@ -4,8 +4,10 @@ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { DummyValue, GenerateSql } from 'src/decorators';
import { WorkflowSearchDto } from 'src/dtos/workflow.dto';
import { WorkflowGetLogsDto, WorkflowSearchDto } from 'src/dtos/workflow.dto';
import { WorkflowResult } from 'src/enum';
import { DB } from 'src/schema';
import { WorkflowLogTable } from 'src/schema/tables/workflow-log.table';
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
import { WorkflowTable } from 'src/schema/tables/workflow.table';
@@ -26,6 +28,7 @@ export class WorkflowRepository {
'workflow.enabled',
'workflow.createdAt',
'workflow.updatedAt',
'workflow.logging',
])
.select((eb) => [
jsonArrayFrom(
@@ -64,7 +67,7 @@ export class WorkflowRepository {
getForWorkflowRun(id: string) {
return this.db
.selectFrom('workflow')
.select(['workflow.id', 'workflow.name', 'workflow.trigger'])
.select(['workflow.id', 'workflow.name', 'workflow.trigger', 'workflow.logging'])
.select((eb) => [
jsonArrayFrom(
eb
@@ -97,6 +100,9 @@ export class WorkflowRepository {
update(id: string, dto: Updateable<WorkflowTable>, steps?: WorkflowStepUpsert[]) {
return this.db.transaction().execute(async (tx) => {
if (dto.logging === false) {
await tx.deleteFrom('workflow_log').where('workflowId', '=', id).execute();
}
if (Object.values(dto).some((prop) => prop !== undefined)) {
await tx.updateTable('workflow').set(dto).where('id', '=', id).executeTakeFirstOrThrow();
}
@@ -104,6 +110,44 @@ export class WorkflowRepository {
});
}
@GenerateSql({ params: [DummyValue.UUID, { result: undefined }] })
getLogs(id: string, dto: WorkflowGetLogsDto) {
return this.db
.selectFrom('workflow_log')
.select([
'workflow_log.id',
'workflow_log.createdAt',
'workflow_log.halted',
'workflow_log.error',
'workflow_log.workflowId',
'workflow_log.workflowStepId',
'workflow_log.triggerDataId',
])
.where('workflow_log.workflowId', '=', id)
.select((eb) => [
jsonObjectFrom(
eb
.selectFrom('workflow_step')
.whereRef('workflow_step.id', '=', 'workflow_log.workflowStepId')
.innerJoin('plugin_method', 'plugin_method.id', 'workflow_step.pluginMethodId')
.select(['plugin_method.pluginId', 'plugin_method.name as methodName', 'workflow_step.order']),
).as('step'),
])
.$if(dto.result === WorkflowResult.Error, (qb) => qb.where('workflow_log.error', '=', true))
.$if(dto.result === WorkflowResult.Halted, (qb) => qb.where('workflow_log.halted', '=', true))
.$if(dto.result === WorkflowResult.Completed, (qb) =>
qb.where('workflow_log.halted', '=', false).where('workflow_log.error', '=', false),
)
.$if(dto.before !== undefined, (qb) => qb.where('workflow_log.createdAt', '<', dto.before!))
.orderBy('workflow_log.createdAt', 'desc')
.limit(dto.limit)
.execute();
}
log(dto: Insertable<WorkflowLogTable>) {
return this.db.insertInto('workflow_log').values(dto).execute();
}
async updateStep(id: string, dto: Updateable<WorkflowStepTable>) {
await this.db.updateTable('workflow_step').where('workflow_step.id', '=', id).set(dto).execute();
}

View File

@@ -86,6 +86,7 @@ import {
VideoStreamSessionTable,
VideoStreamVariantTable,
} from 'src/schema/tables/video-stream.table';
import { WorkflowLogTable } from 'src/schema/tables/workflow-log.table';
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
import { WorkflowTable } from 'src/schema/tables/workflow.table';
@@ -277,4 +278,5 @@ export interface DB {
workflow: WorkflowTable;
workflow_step: WorkflowStepTable;
workflow_log: WorkflowLogTable;
}

View File

@@ -0,0 +1,24 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "workflow" ADD "logging" boolean NOT NULL DEFAULT false;`.execute(db);
await sql`CREATE TABLE "workflow_log" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"workflowId" uuid NOT NULL,
"error" boolean NOT NULL,
"halted" boolean NOT NULL,
"workflowStepId" uuid,
"triggerDataId" uuid,
CONSTRAINT "workflow_log_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflow" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "workflow_log_workflowStepId_fkey" FOREIGN KEY ("workflowStepId") REFERENCES "workflow_step" ("id") ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT "workflow_log_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "workflow_log_workflowId_idx" ON "workflow_log" ("workflowId");`.execute(db);
await sql`CREATE INDEX "workflow_log_workflowStepId_idx" ON "workflow_log" ("workflowStepId");`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "workflow" DROP COLUMN "logging";`.execute(db);
await sql`DROP TABLE "workflow_log";`.execute(db);
}

View File

@@ -0,0 +1,35 @@
import {
Column,
CreateDateColumn,
ForeignKeyColumn,
Generated,
PrimaryGeneratedColumn,
Table,
Timestamp,
} from '@immich/sql-tools';
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
import { WorkflowTable } from 'src/schema/tables/workflow.table';
@Table('workflow_log')
export class WorkflowLogTable {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@ForeignKeyColumn(() => WorkflowTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', index: true })
workflowId!: string;
@Column({ type: 'boolean' })
error!: boolean;
@Column({ type: 'boolean' })
halted!: boolean;
@ForeignKeyColumn(() => WorkflowStepTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true })
workflowStepId!: string | null;
@Column({ type: 'uuid', nullable: true })
triggerDataId!: string | null;
}

View File

@@ -41,4 +41,7 @@ export class WorkflowTable {
@Column({ type: 'boolean', default: true })
enabled!: Generated<boolean>;
@Column({ type: 'boolean', default: false })
logging!: Generated<boolean>;
}

View File

@@ -36,7 +36,7 @@ const dummy = () => {
};
type ExecuteOptions<T extends WorkflowType> = {
read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData<T> }>;
read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData<T>; entityId?: string }>;
write: (auth: AuthDto, changes: WorkflowChanges<T>) => Promise<void>;
};
@@ -331,6 +331,7 @@ export class WorkflowExecutionService extends BaseService {
return {
data: { asset } as any,
authUserId: asset.ownerId,
entityId: asset.id,
};
},
write: async (auth, changes) => {
@@ -398,11 +399,12 @@ export class WorkflowExecutionService extends BaseService {
return;
}
try {
const { read, write } = handler;
const readResult = await read(type);
let data = readResult.data;
for (const step of workflow.steps) {
const { read, write } = handler;
const readResult = await read(type);
let data = readResult.data;
for (const step of workflow.steps) {
try {
const payload: WorkflowEventPayload<typeof type> = {
trigger: workflow.trigger,
type,
@@ -453,14 +455,45 @@ export class WorkflowExecutionService extends BaseService {
const shouldContinue = result?.workflow?.continue ?? true;
if (!shouldContinue) {
break;
}
}
if (workflow.logging) {
await this.workflowRepository.log({
workflowId,
error: false,
halted: true,
workflowStepId: step.id,
triggerDataId: readResult.entityId,
});
}
this.logger.debug(`Workflow ${workflowId} executed successfully`);
} catch (error) {
this.logger.error(`Error executing workflow ${workflowId}:`, error);
return JobStatus.Failed;
this.logger.debug(`Workflow ${workflowId} stopped on step ${step.id}`);
return;
}
} catch (error) {
this.logger.error(`Error executing workflow ${workflowId}:`, error);
if (workflow.logging) {
await this.workflowRepository.log({
workflowId,
error: true,
halted: false,
workflowStepId: step.id,
triggerDataId: readResult.entityId,
});
}
return JobStatus.Failed;
}
}
if (workflow.logging) {
await this.workflowRepository.log({
workflowId,
error: false,
halted: false,
triggerDataId: readResult.entityId,
});
}
this.logger.debug(`Workflow ${workflowId} executed successfully`);
}
}

View File

@@ -5,13 +5,15 @@ import {
mapWorkflow,
mapWorkflowShare,
WorkflowCreateDto,
WorkflowGetLogsDto,
WorkflowLogEntryDto,
WorkflowResponseDto,
WorkflowSearchDto,
WorkflowShareResponseDto,
WorkflowTriggerResponseDto,
WorkflowUpdateDto,
} from 'src/dtos/workflow.dto';
import { Permission } from 'src/enum';
import { Permission, WorkflowResult } from 'src/enum';
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
import { BaseService } from 'src/services/base.service';
import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow';
@@ -82,6 +84,23 @@ export class WorkflowService extends BaseService {
await this.workflowRepository.delete(id);
}
async getLogs(auth: AuthDto, id: string, dto: WorkflowGetLogsDto): Promise<WorkflowLogEntryDto[]> {
await this.requireAccess({ auth, permission: Permission.WorkflowLogs, ids: [id] });
const logs = await this.workflowRepository.getLogs(id, dto);
return logs.map((entry) => ({
id: entry.id,
at: entry.createdAt.toISOString(),
result: entry.error ? WorkflowResult.Error : entry.halted ? WorkflowResult.Halted : WorkflowResult.Completed,
triggerDataId: entry.triggerDataId ?? undefined,
lastStep: entry.step
? {
index: entry.step.order,
method: `${entry.step.pluginId}#${entry.step.methodName}`,
}
: undefined,
}));
}
private async resolveAndValidateSteps<T extends { method: string }>(steps: T[], trigger: WorkflowTrigger) {
const methods = await this.pluginRepository.getForValidation();
const results: Array<T & { pluginMethod: PluginMethodSearchResponse }> = [];

View File

@@ -324,7 +324,8 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
case Permission.WorkflowRead:
case Permission.WorkflowUpdate:
case Permission.WorkflowDelete: {
case Permission.WorkflowDelete:
case Permission.WorkflowLogs: {
return access.workflow.checkOwnerAccess(auth.user.id, ids);
}

View File

@@ -0,0 +1,169 @@
<script lang="ts">
import { type WorkflowLogEntryDto, type WorkflowResponseDto, WorkflowResult, getWorkflowLogs } from '@immich/sdk';
import {
Modal,
ModalBody,
Table,
TableHeader,
TableHeading,
TableBody,
TableRow,
TableCell,
HStack,
Icon,
VStack,
Button,
Select,
type SelectOption,
} from '@immich/ui';
import { mdiHistory, mdiOpenInNew } from '@mdi/js';
import { t } from 'svelte-i18n';
import { locale } from '$lib/stores/preferences.store';
import { DateTime } from 'luxon';
import { Route } from '$lib/route';
import { handleUpdateWorkflow } from '$lib/services/workflow.service';
type Props = {
workflow: WorkflowResponseDto;
onClose: () => void;
};
let { workflow, onClose }: Props = $props();
let entries: WorkflowLogEntryDto[] = $state([]);
let placeholder: HTMLElement | undefined = $state();
let filter: WorkflowResult | undefined = $state();
let before: string | undefined = $state();
let hasNext = $state(true);
let loading = $state(false);
const setLogging = (logging: boolean) =>
handleUpdateWorkflow(workflow.id, { logging }).then((success) => {
if (success) {
workflow = { ...workflow, logging };
reset();
}
});
const reset = () => {
entries = [];
hasNext = true;
before = undefined;
};
const setFilter = (option: SelectOption<WorkflowResult>) => {
reset();
filter = option.value;
void getLogs();
};
const getLogs = async () => {
if (!hasNext || loading) {
return;
}
loading = true;
const limit = 50;
const results = await getWorkflowLogs({ id: workflow.id, result: filter, before, limit });
entries.push(...results);
if (results.length < limit) {
hasNext = false;
} else {
before = results.at(-1)?.at;
}
loading = false;
};
const observer = new IntersectionObserver((entries) => {
const entry = entries.find((entry) => entry.target === placeholder);
if (entry?.isIntersecting) {
void getLogs();
}
});
$effect(() => {
if (placeholder) {
observer.disconnect();
observer.observe(placeholder);
}
});
</script>
<Modal title={$t('logs')} icon={mdiHistory} {onClose} size="medium">
<ModalBody>
{#if workflow.logging}
<Table striped>
<TableHeader>
<TableHeading>{$t('date')}</TableHeading>
<TableHeading>{$t('result')}</TableHeading>
</TableHeader>
<TableBody class="max-h-100">
{#each entries as entry (entry.id)}
<TableRow>
<TableCell>
<HStack class="justify-center">
<p>
{DateTime.fromISO(entry.at).toLocaleString(DateTime.DATETIME_MED, {
locale: $locale,
})}
</p>
{#if entry.triggerDataId}
<a href={Route.viewAsset({ id: entry.triggerDataId })} target="_blank">
<Icon icon={mdiOpenInNew} size="20" class="text-primary" />
</a>
{/if}
</HStack>
</TableCell>
<TableCell>
<HStack class="justify-center">
{#if entry.result === WorkflowResult.Completed}
<p class="rounded-full bg-green-700 px-3 py-1 text-xs text-white">
{$t('workflow_logging_completed')}
</p>
{:else if entry.result === WorkflowResult.Halted}
<p class="rounded-full bg-gray-600 px-3 py-1 text-xs text-white">
{#if entry.lastStep}
{$t('workflow_logging_halted_step', { values: { step: entry.lastStep.index + 1 } })}
{:else}
{$t('workflow_logging_halted')}
{/if}
</p>
{:else}
<p class="rounded-full bg-red-500 px-3 py-1 text-xs text-white">
{#if entry.lastStep}
{$t('workflow_logging_error_step', { values: { step: entry.lastStep.index + 1 } })}
{:else}
{$t('error')}
{/if}
</p>
{/if}
</HStack>
</TableCell>
</TableRow>
{/each}
{#if hasNext}
<TableRow><TableCell><div bind:this={placeholder}>...</div></TableCell></TableRow>
{/if}
</TableBody>
</Table>
<div class="mt-5 flex gap-5">
<Select
onSelect={setFilter}
class="flex-1"
placeholder={$t('filter')}
options={[
{ value: WorkflowResult.Completed, label: $t('workflow_logging_completed') },
{ value: WorkflowResult.Halted, label: $t('workflow_logging_halted') },
{ value: WorkflowResult.Error, label: $t('error') },
]}
/>
<Button class="flex-1" onclick={() => setLogging(false)}>{$t('workflow_logging_disable')}</Button>
</div>
{:else}
<VStack class="gap-5 py-5">
<p class="text-md text-center">{$t('workflow_logging_disabled_description')}</p>
<Button onclick={() => setLogging(true)}>{$t('workflow_logging_enable')}</Button>
</VStack>
{/if}
</ModalBody>
</Modal>

View File

@@ -17,6 +17,7 @@ import {
mdiDeleteOutline,
mdiDownload,
mdiFileDocumentMultipleOutline,
mdiHistory,
mdiPause,
mdiPencil,
mdiPlay,
@@ -26,6 +27,7 @@ import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { eventManager } from '$lib/managers/event-manager.svelte';
import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte';
import WorkflowLogsModal from '$lib/modals/WorkflowLogsModal.svelte';
import WorkflowTemplatePickerModal from '$lib/modals/WorkflowTemplatePickerModal.svelte';
import { Route } from '$lib/route';
import { copyToClipboard, downloadJson } from '$lib/utils';
@@ -117,7 +119,13 @@ export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowRespo
onAction: () => handleDeleteWorkflow(workflow),
};
return { CopyJson, Download, Duplicate, ToggleEnabled, Edit, Delete };
const Logs: ActionItem = {
title: $t('check_logs'),
icon: mdiHistory,
onAction: () => modalManager.show(WorkflowLogsModal, { workflow }),
};
return { CopyJson, Download, Duplicate, ToggleEnabled, Edit, Delete, Logs };
};
export const getWorkflowShowSchemaAction = (

View File

@@ -75,7 +75,7 @@
{:else}
<div class="my-6 flex flex-col gap-3">
{#each workflows as workflow (workflow.id)}
{@const { ToggleEnabled, Duplicate, Edit, Delete } = getWorkflowActions($t, workflow)}
{@const { ToggleEnabled, Duplicate, Logs, Edit, Delete } = getWorkflowActions($t, workflow)}
<Card class="group shadow-none transition-colors hover:border-primary">
<CardHeader>
@@ -120,6 +120,7 @@
ToggleEnabled,
Edit,
Duplicate,
Logs,
getWorkflowShowSchemaAction($t, expandedIds.has(workflow.id), () => onToggleExpand(workflow.id)),
MenuItemType.Divider,
Delete,

View File

@@ -261,7 +261,7 @@
$effect(() => console.log(steps));
const { Download, Duplicate, CopyJson, Delete } = $derived(
const { Download, Duplicate, CopyJson, Delete, Logs } = $derived(
getWorkflowActions($t, { ...savedWorkflow, name, description, enabled, trigger, steps }),
);
</script>
@@ -276,7 +276,7 @@
{onClose}
translations={{ close: $t('back') }}
closeIcon={mdiArrowLeft}
actions={[Duplicate, CopyJson, Download, Delete].map((item) => ({ ...item, color: undefined }))}
actions={[Logs, Duplicate, CopyJson, Download, Delete].map((item) => ({ ...item, color: undefined }))}
>
<ControlBarHeader>
<ControlBarTitle>{data.workflow.name}</ControlBarTitle>