diff --git a/backend/.sqlx/query-051acd49542618e3be5654dce94567a02e87be23a4c0863e3b2d53b5c2cb6749.json b/backend/.sqlx/query-051acd49542618e3be5654dce94567a02e87be23a4c0863e3b2d53b5c2cb6749.json new file mode 100644 index 0000000000..c7c7260ed4 --- /dev/null +++ b/backend/.sqlx/query-051acd49542618e3be5654dce94567a02e87be23a4c0863e3b2d53b5c2cb6749.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE schedule SET on_success = NULL, on_success_extra_args = NULL WHERE workspace_id = $1 RETURNING path", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "051acd49542618e3be5654dce94567a02e87be23a4c0863e3b2d53b5c2cb6749" +} diff --git a/backend/.sqlx/query-31fe5d2965f7b25dea785f8be529a9b2c4c83c910fd7e2a08f4d95ae195ab3ed.json b/backend/.sqlx/query-31fe5d2965f7b25dea785f8be529a9b2c4c83c910fd7e2a08f4d95ae195ab3ed.json new file mode 100644 index 0000000000..85c0412f3b --- /dev/null +++ b/backend/.sqlx/query-31fe5d2965f7b25dea785f8be529a9b2c4c83c910fd7e2a08f4d95ae195ab3ed.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE schedule SET on_success = $1, on_success_extra_args = $2 WHERE workspace_id = $3 RETURNING path", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Json", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "31fe5d2965f7b25dea785f8be529a9b2c4c83c910fd7e2a08f4d95ae195ab3ed" +} diff --git a/backend/.sqlx/query-4422b7183ede17a9cbde4afae41be4da5447020e39398deedbcca9121492834a.json b/backend/.sqlx/query-4422b7183ede17a9cbde4afae41be4da5447020e39398deedbcca9121492834a.json index 1163e930c9..3e8ba47b99 100644 --- a/backend/.sqlx/query-4422b7183ede17a9cbde4afae41be4da5447020e39398deedbcca9121492834a.json +++ b/backend/.sqlx/query-4422b7183ede17a9cbde4afae41be4da5447020e39398deedbcca9121492834a.json @@ -135,6 +135,16 @@ }, { "ordinal": 26, + "name": "on_success", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "on_success_extra_args", + "type_info": "Json" + }, + { + "ordinal": 28, "name": "jobs", "type_info": "JsonArray" } @@ -173,6 +183,8 @@ false, true, true, + true, + true, null ] }, diff --git a/backend/migrations/20240906151147_add_success_handler.down.sql b/backend/migrations/20240906151147_add_success_handler.down.sql new file mode 100644 index 0000000000..33b6e62539 --- /dev/null +++ b/backend/migrations/20240906151147_add_success_handler.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER TABLE schedule DROP COLUMN on_success, DROP COLUMN on_success_extra_args; \ No newline at end of file diff --git a/backend/migrations/20240906151147_add_success_handler.up.sql b/backend/migrations/20240906151147_add_success_handler.up.sql new file mode 100644 index 0000000000..3465ed5ea5 --- /dev/null +++ b/backend/migrations/20240906151147_add_success_handler.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE schedule ADD COLUMN on_success VARCHAR(1000), ADD COLUMN on_success_extra_args json; \ No newline at end of file diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 5da17cded7..e2745c56d1 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -2981,6 +2981,8 @@ async fn test_script_schedule_handlers(db: Pool) { on_recovery: Some("script/f/system/schedule_recovery_handler".to_string()), on_recovery_times: None, on_recovery_extra_args: None, + on_success: None, + on_success_extra_args: None, path: "f/system/failing_script_schedule".to_string(), script_path: "f/system/failing_script".to_string(), timezone: "UTC".to_string(), @@ -3048,6 +3050,8 @@ async fn test_script_schedule_handlers(db: Pool) { on_recovery: Some("script/f/system/schedule_recovery_handler".to_string()), on_recovery_times: None, on_recovery_extra_args: None, + on_success: None, + on_success_extra_args: None, timezone: "UTC".to_string(), schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(), ws_error_handler_muted: None, @@ -3126,6 +3130,8 @@ async fn test_flow_schedule_handlers(db: Pool) { on_recovery: Some("script/f/system/schedule_recovery_handler".to_string()), on_recovery_times: None, on_recovery_extra_args: None, + on_success: None, + on_success_extra_args: None, path: "f/system/failing_flow_schedule".to_string(), script_path: "f/system/failing_flow".to_string(), timezone: "UTC".to_string(), @@ -3194,6 +3200,8 @@ async fn test_flow_schedule_handlers(db: Pool) { on_recovery: Some("script/f/system/schedule_recovery_handler".to_string()), on_recovery_times: None, on_recovery_extra_args: None, + on_success: None, + on_success_extra_args: None, timezone: "UTC".to_string(), schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(), ws_error_handler_muted: None, diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index ff8e9dadd7..a7ac1752b8 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -6800,7 +6800,7 @@ paths: properties: handler_type: type: string - enum: ["error", "recovery"] + enum: ["error", "recovery", "success"] override_existing: type: boolean path: @@ -10474,6 +10474,10 @@ components: type: number on_recovery_extra_args: $ref: "#/components/schemas/ScriptArgs" + on_success: + type: string + on_success_extra_args: + $ref: "#/components/schemas/ScriptArgs" ws_error_handler_muted: type: boolean retry: @@ -10552,6 +10556,10 @@ components: type: number on_recovery_extra_args: $ref: "#/components/schemas/ScriptArgs" + on_success: + type: string + on_success_extra_args: + $ref: "#/components/schemas/ScriptArgs" ws_error_handler_muted: type: boolean retry: @@ -10597,6 +10605,10 @@ components: type: number on_recovery_extra_args: $ref: "#/components/schemas/ScriptArgs" + on_success: + type: string + on_success_extra_args: + $ref: "#/components/schemas/ScriptArgs" ws_error_handler_muted: type: boolean retry: diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api/src/schedule.rs index d52d441608..7a18976b03 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api/src/schedule.rs @@ -69,6 +69,8 @@ pub struct NewSchedule { pub on_recovery: Option, pub on_recovery_times: Option, pub on_recovery_extra_args: Option, + pub on_success: Option, + pub on_success_extra_args: Option, pub ws_error_handler_muted: Option, pub retry: Option, pub tag: Option, @@ -92,6 +94,7 @@ pub struct ErrorOrRecoveryHandler { pub enum HandlerType { Error, Recovery, + Success, } async fn check_path_conflict<'c>( @@ -133,6 +136,13 @@ async fn create_schedule( )); } + #[cfg(not(feature = "enterprise"))] + if ns.on_success.is_some() { + return Err(Error::BadRequest( + "on_success is only available in enterprise version".to_string(), + )); + } + #[cfg(not(feature = "enterprise"))] if ns.on_failure_times.is_some() && ns.on_failure_times.unwrap() > 1 { return Err(Error::BadRequest( @@ -158,9 +168,10 @@ async fn create_schedule( "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \ is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, \ on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, \ + on_success, on_success_extra_args, \ ws_error_handler_muted, retry, summary, no_flow_overlap, tag, paused_until \ ) VALUES ( \ - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23 \ + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25 \ ) RETURNING *") .bind(&w_id) .bind(&ns.path) @@ -179,6 +190,8 @@ async fn create_schedule( .bind(&ns.on_recovery) .bind(&ns.on_recovery_times) .bind(&ns.on_recovery_extra_args) + .bind(&ns.on_success) + .bind(&ns.on_success_extra_args) .bind(&ns.ws_error_handler_muted.unwrap_or(false)) .bind(&ns.retry) .bind(&ns.summary) @@ -248,9 +261,10 @@ async fn edit_schedule( let schedule = sqlx::query_as::<_, Schedule>( "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, \ on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, \ - on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, \ - no_flow_overlap = $14, tag = $15, paused_until = $16 - WHERE path = $17 AND workspace_id = $18 RETURNING *") + on_recovery_extra_args = $10, on_success = $11, on_success_extra_args = $12, \ + ws_error_handler_muted = $13, retry = $14, summary = $15, \ + no_flow_overlap = $16, tag = $17, paused_until = $18 + WHERE path = $19 AND workspace_id = $20 RETURNING *") .bind(&es.schedule) .bind(&es.timezone) .bind(&es.args) @@ -261,6 +275,8 @@ async fn edit_schedule( .bind(&es.on_recovery) .bind(&es.on_recovery_times) .bind(&es.on_recovery_extra_args) + .bind(&es.on_success) + .bind(&es.on_success_extra_args) .bind(&es.ws_error_handler_muted.unwrap_or(false)) .bind(&es.retry) .bind(&es.summary) @@ -376,6 +392,8 @@ pub struct ScheduleWJobs { pub on_recovery: Option, pub on_recovery_times: Option, pub on_recovery_extra_args: Option, + pub on_success: Option, + pub on_success_extra_args: Option, pub ws_error_handler_muted: bool, pub retry: Option, pub jobs: Option>, @@ -680,6 +698,18 @@ async fn set_default_error_handler( (key, None) } } + HandlerType::Success => { + let key = format!("default_success_handler_{}", w_id); + if let Some(payload_path) = payload.path.as_ref() { + let value = serde_json::json!({ + "successHandlerPath": payload_path, + "successHandlerExtraArgs": payload.extra_args, + }); + (key, Some(value)) + } else { + (key, None) + } + } }; if let Some(value_content) = value { @@ -733,6 +763,25 @@ async fn set_default_error_handler( .await?; } } + HandlerType::Success => { + if payload.path.is_some() { + updated_schedules = sqlx::query_scalar!( + "UPDATE schedule SET on_success = $1, on_success_extra_args = $2 WHERE workspace_id = $3 RETURNING path", + payload.path, + payload.extra_args, + w_id, + ) + .fetch_all(&db) + .await?; + } else { + updated_schedules = sqlx::query_scalar!( + "UPDATE schedule SET on_success = NULL, on_success_extra_args = NULL WHERE workspace_id = $1 RETURNING path", + w_id, + ) + .fetch_all(&db) + .await?; + } + } } for updated_schedule_path in updated_schedules { handle_deployment_metadata( @@ -790,6 +839,8 @@ pub struct EditSchedule { pub on_recovery: Option, pub on_recovery_times: Option, pub on_recovery_extra_args: Option, + pub on_success: Option, + pub on_success_extra_args: Option, pub ws_error_handler_muted: Option, pub retry: Option, pub no_flow_overlap: Option, diff --git a/backend/windmill-common/src/schedule.rs b/backend/windmill-common/src/schedule.rs index f369a619ff..ed74274756 100644 --- a/backend/windmill-common/src/schedule.rs +++ b/backend/windmill-common/src/schedule.rs @@ -42,7 +42,10 @@ pub struct Schedule { pub on_recovery_times: Option, #[serde(skip_serializing_if = "Option::is_none")] pub on_recovery_extra_args: Option>>, - + #[serde(skip_serializing_if = "Option::is_none")] + pub on_success: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub on_success_extra_args: Option>>, pub ws_error_handler_muted: bool, #[serde(skip_serializing_if = "Option::is_none")] pub retry: Option, diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index dddb6cdc6b..134d91180b 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -1376,7 +1376,25 @@ async fn apply_schedule_handlers< } } else { #[cfg(feature = "enterprise")] - if let Some(on_recovery_path) = schedule.on_recovery.clone() { + if let Some(ref on_success_path) = schedule.on_success { + handle_successful_schedule( + db, + rsmq.clone(), + job_id, + &schedule.path, + script_path, + schedule.is_flow, + w_id, + on_success_path, + result, + started_at, + schedule.on_success_extra_args.clone(), + ) + .await?; + } + + #[cfg(feature = "enterprise")] + if let Some(ref on_recovery_path) = schedule.on_recovery.clone() { let tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into(); let times = schedule.on_recovery_times.unwrap_or(1).max(1); let past_jobs = sqlx::query_as::<_, CompletedJobSubset>( @@ -1636,6 +1654,86 @@ async fn handle_recovered_schedule< Ok(()) } +async fn handle_successful_schedule< + 'a, + 'c, + T: Serialize + Send + Sync, + R: rsmq_async::RsmqConnection + Clone + Send + 'c, +>( + db: &Pool, + rsmq: Option, + job_id: Uuid, + schedule_path: &str, + script_path: &str, + is_flow: bool, + w_id: &str, + on_success_path: &str, + successful_job_result: Json<&'a T>, + successful_job_started_at: DateTime, + extra_args: Option>>, +) -> windmill_common::error::Result<()> { + let (payload, tag) = get_payload_tag_from_prefixed_path(on_success_path, db, w_id).await?; + + let mut extra = HashMap::new(); + extra.insert("schedule_path".to_string(), to_raw_value(&schedule_path)); + extra.insert("path".to_string(), to_raw_value(&script_path)); + extra.insert("is_flow".to_string(), to_raw_value(&is_flow)); + extra.insert( + "success_result".to_string(), + serde_json::from_str::>( + &serde_json::to_string(&successful_job_result).unwrap(), + ) + .unwrap_or_else(|_| serde_json::value::RawValue::from_string("{}".to_string()).unwrap()), + ); + extra.insert( + "success_started_at".to_string(), + to_raw_value(&successful_job_started_at), + ); + if let Some(args_v) = extra_args { + if let Ok(args_m) = serde_json::from_str::>>(args_v.get()) { + extra.extend(args_m); + } else { + return Err(error::Error::ExecutionErr( + "args of scripts needs to be dict".to_string(), + )); + } + } + + let tx = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq); + let (uuid, tx) = push( + &db, + tx, + w_id, + payload, + PushArgs { extra: Some(extra), args: &HashMap::new() }, + SCHEDULE_RECOVERY_HANDLER_USERNAME, + SCHEDULE_RECOVERY_HANDLER_USER_EMAIL, + ERROR_HANDLER_USER_GROUP.to_string(), + None, + None, + Some(job_id), + Some(job_id), + None, + false, + false, + None, + true, + tag, + None, + None, + None, + None, + ) + .await?; + tracing::info!( + "Pushed on_success job {} for {} to queue", + uuid, + schedule_path + ); + tx.commit().await?; + Ok(()) +} + pub async fn pull( db: &Pool, rsmq: Option, diff --git a/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte b/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte index 2ada375bcf..059d9231b1 100644 --- a/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte +++ b/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte @@ -22,8 +22,9 @@ const slackRecoveryHandler = hubPaths.slackRecoveryHandler const slackHandlerScriptPath = hubPaths.slackErrorHandler + const slackSuccessHandler = hubPaths.slackSuccessHandler - export let errorOrRecovery: 'error' | 'recovery' + export let errorOrRecovery: 'error' | 'recovery' | 'success' export let isEditable: boolean export let slackToggleText: string = 'Enable' export let showScriptHelpText: boolean = false @@ -135,10 +136,12 @@ scriptPath.startsWith('hub/') && scriptPath.endsWith('/workspace-or-schedule-error-handler-slack') ) - } else { + } else if (errorOrRecovery == 'recovery') { return ( scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-recovery-handler-slack') ) + } else { + return scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-success-handler-slack') } } @@ -256,6 +259,8 @@ handlerPath = slackHandlerScriptPath } else if (e.detail && errorOrRecovery === 'recovery') { handlerPath = slackRecoveryHandler + } else if (e.detail && errorOrRecovery === 'success') { + handlerPath = slackSuccessHandler } else { handlerPath = undefined } diff --git a/frontend/src/lib/components/ScheduleEditorInner.svelte b/frontend/src/lib/components/ScheduleEditorInner.svelte index dfa6d979e4..2291b12622 100644 --- a/frontend/src/lib/components/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/ScheduleEditorInner.svelte @@ -30,7 +30,8 @@ import Label from './Label.svelte' import DateTimeInput from './DateTimeInput.svelte' - let optionTabSelected: 'error_handler' | 'recovery_handler' | 'retries' = 'error_handler' + let optionTabSelected: 'error_handler' | 'recovery_handler' | 'success_handler' | 'retries' = + 'error_handler' let is_flow: boolean = false let initialPath = '' @@ -51,6 +52,11 @@ let recoveryHandlerSelected: 'custom' | 'slack' = 'slack' let recoveryHandlerItemKind: 'flow' | 'script' = 'script' let recoveryHandlerExtraArgs: Record = {} + let successHandlerPath: string | undefined = undefined + let successHandlerCustomInitialPath: string | undefined = undefined + let successHandlerSelected: 'custom' | 'slack' = 'slack' + let successHandlerItemKind: 'flow' | 'script' = 'script' + let successHandlerExtraArgs: Record = {} let failedTimes = 1 let failedExact = false let recoveredTimes = 1 @@ -95,6 +101,7 @@ showPauseUntil = false let defaultErrorHandlerMaybe = undefined let defaultRecoveryHandlerMaybe = undefined + let defaultSuccessHandlerMaybe = undefined if ($workspaceStore) { defaultErrorHandlerMaybe = (await SettingService.getGlobal({ key: 'default_error_handler_' + $workspaceStore! @@ -102,6 +109,9 @@ defaultRecoveryHandlerMaybe = (await SettingService.getGlobal({ key: 'default_recovery_handler_' + $workspaceStore! })) as any + defaultSuccessHandlerMaybe = (await SettingService.getGlobal({ + key: 'default_success_handler_' + $workspaceStore! + })) as any } edit = false @@ -152,6 +162,21 @@ recoveryHandlerSelected = 'slack' recoveredTimes = 1 } + if (defaultSuccessHandlerMaybe !== undefined && defaultSuccessHandlerMaybe !== null) { + let splitted = (defaultSuccessHandlerMaybe['successHandlerPath'] as string).split('/') + successHandlerItemKind = splitted[0] as 'flow' | 'script' + successHandlerPath = splitted.slice(1)?.join('/') + successHandlerExtraArgs = defaultSuccessHandlerMaybe['successHandlerExtraArgs'] + successHandlerCustomInitialPath = successHandlerPath + successHandlerSelected = isSlackHandler('success', successHandlerPath) ? 'slack' : 'custom' + recoveredTimes = defaultSuccessHandlerMaybe['recoveredTimes'] + } else { + successHandlerPath = undefined + successHandlerItemKind = 'script' + successHandlerExtraArgs = {} + successHandlerCustomInitialPath = undefined + successHandlerSelected = 'slack' + } timezone = Intl.DateTimeFormat().resolvedOptions().timeZone } finally { drawerLoading = false @@ -255,6 +280,33 @@ } } + async function saveAsDefaultSuccessHandler(overrideExisting: boolean) { + if (!$enterpriseLicense) { + sendUserToast(`Setting default success handler is an enterprise edition feature`, true) + return + } + if ($workspaceStore) { + await ScheduleService.setDefaultErrorOrRecoveryHandler({ + workspace: $workspaceStore!, + requestBody: { + handler_type: 'success', + override_existing: overrideExisting, + path: + successHandlerPath === undefined + ? undefined + : `${successHandlerItemKind}/${successHandlerPath}`, + extra_args: successHandlerExtraArgs, + number_of_occurence: recoveredTimes + } + }) + if (successHandlerPath !== undefined) { + sendUserToast(`Default success handler saved to ${successHandlerPath}`, false) + } else { + sendUserToast(`Default success handler reset`, false) + } + } + } + let can_write = true async function loadSchedule(): Promise { loading = true @@ -312,6 +364,20 @@ recoveryHandlerSelected = 'slack' recoveryHandlerExtraArgs = {} } + if (s.on_success) { + let splitted = s.on_success.split('/') + successHandlerItemKind = splitted[0] as 'flow' | 'script' + successHandlerPath = splitted.slice(1)?.join('/') + successHandlerCustomInitialPath = successHandlerPath + successHandlerExtraArgs = s.on_success_extra_args ?? {} + successHandlerSelected = isSlackHandler('success', successHandlerPath) ? 'slack' : 'custom' + } else { + successHandlerPath = undefined + successHandlerItemKind = 'script' + successHandlerCustomInitialPath = undefined + successHandlerSelected = 'slack' + successHandlerExtraArgs = {} + } args = s.args ?? {} can_write = canWrite(s.path, s.extra_perms, $userStore) tag = s.tag @@ -328,6 +394,9 @@ if (recoveryHandlerPath !== undefined && isSlackHandler('recovery', recoveryHandlerPath)) { recoveryHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token' } + if (successHandlerPath !== undefined && isSlackHandler('success', successHandlerPath)) { + successHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token' + } if (edit) { await ScheduleService.updateSchedule({ workspace: $workspaceStore!, @@ -345,6 +414,10 @@ : undefined, on_recovery_times: recoveredTimes, on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {}, + on_success: successHandlerPath + ? `${successHandlerItemKind}/${successHandlerPath}` + : undefined, + on_success_extra_args: successHandlerPath ? successHandlerExtraArgs : {}, ws_error_handler_muted: wsErrorHandlerMuted, retry: retry, summary: summary != '' ? summary : undefined, @@ -374,6 +447,10 @@ : undefined, on_recovery_times: recoveredTimes, on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {}, + on_success: successHandlerPath + ? `${successHandlerItemKind}/${successHandlerPath}` + : undefined, + on_success_extra_args: successHandlerPath ? successHandlerExtraArgs : {}, ws_error_handler_muted: wsErrorHandlerMuted, retry: retry, summary: summary != '' ? summary : undefined, @@ -388,16 +465,18 @@ drawer.closeDrawer() } - function isSlackHandler(isSlackHandler: 'error' | 'recovery', scriptPath: string) { + function isSlackHandler(isSlackHandler: 'error' | 'recovery' | 'success', scriptPath: string) { if (isSlackHandler == 'error') { return ( scriptPath.startsWith('hub/') && scriptPath.endsWith('/workspace-or-schedule-error-handler-slack') ) - } else { + } else if (isSlackHandler == 'recovery') { return ( scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-recovery-handler-slack') ) + } else { + return scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-success-handler-slack') } } @@ -631,6 +710,7 @@ Error Handler Recovery Handler + Success Handler {#if itemKind === 'script'} Retries Custom tag @@ -778,7 +858,7 @@ bind:handlerPath={recoveryHandlerPath} customInitialScriptPath={recoveryHandlerCustomInitialPath} slackToggleText="Alert channel when error recovered" - customScriptTemplate="/scripts/add?hub=hub%2F2421%2Fwindmill%2Fschedule_recovery_handler_template" + customScriptTemplate="/scripts/add?hub=hub%2F2794%2Fwindmill%2Fschedule_recovery_handler_template" bind:customHandlerKind={recoveryHandlerItemKind} bind:handlerExtraArgs={recoveryHandlerExtraArgs} > @@ -830,6 +910,68 @@ + {:else if optionTabSelected === 'success_handler'} +
+ +
+ {#if !$enterpriseLicense}(ee only){/if} +
+
+ +
+ defaults + saveAsDefaultSuccessHandler(false) + }, + { + displayName: 'Override all existing', + type: 'delete', + action: () => saveAsDefaultSuccessHandler(true) + } + ]} + > + + + Set as default + + +
+
+ + + +
+
The following args will be passed to the success handler: +
    +
  • path: The path of the script or flow that succeeded.
  • +
  • is_flow: Whether the runnable is a flow.
  • +
  • schedule_path: The path of the schedule.
  • +
  • success_result: The result of the successful job
  • +
  • success_started_at: The start datetime of the successful job
  • +
+
+
+
+
+
+
{:else if optionTabSelected === 'retries'}
diff --git a/frontend/src/lib/hub.ts b/frontend/src/lib/hub.ts index f5ff47ff1a..caa49e5948 100644 --- a/frontend/src/lib/hub.ts +++ b/frontend/src/lib/hub.ts @@ -62,6 +62,7 @@ type HubPaths = { gitSyncTest: string slackErrorHandler: string slackRecoveryHandler: string + slackSuccessHandler: string slackReport: string discordReport: string smtpReport: string diff --git a/frontend/src/lib/hubPaths.json b/frontend/src/lib/hubPaths.json index 5d274614a0..443ff85b24 100644 --- a/frontend/src/lib/hubPaths.json +++ b/frontend/src/lib/hubPaths.json @@ -2,7 +2,9 @@ "gitSync": "hub/8931/sync-script-to-git-repo-windmill", "gitSyncTest": "hub/8944/git-repo-test-read-write-windmill", "slackErrorHandler": "hub/6512/workspace-or-schedule-error-handler-slack", - "slackRecoveryHandler": "hub/2430/slack/schedule-recovery-handler-slack", + "slackRecoveryHandler": "hub/9067/slack/schedule-recovery-handler-slack", + "slackRecoveryHandler_0": "hub/2430/slack/schedule-recovery-handler-slack", + "slackSuccessHandler": "hub/9069/slack/schedule-success-handler-slack", "slackReport": "hub/7836/slack", "discordReport": "hub/7838/discord", "smtpReport": "hub/7837/smtp"