feat: schedule success handler (#4346)

This commit is contained in:
HugoCasa
2024-09-06 19:03:09 +02:00
committed by GitHub
parent 2a5bafecb8
commit a009c8df23
14 changed files with 398 additions and 14 deletions
@@ -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"
}
@@ -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"
}
@@ -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
]
},
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE schedule DROP COLUMN on_success, DROP COLUMN on_success_extra_args;
@@ -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;
+8
View File
@@ -2981,6 +2981,8 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
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<Postgres>) {
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<Postgres>) {
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<Postgres>) {
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,
+13 -1
View File
@@ -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:
+55 -4
View File
@@ -69,6 +69,8 @@ pub struct NewSchedule {
pub on_recovery: Option<String>,
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub on_success: Option<String>,
pub on_success_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: Option<bool>,
pub retry: Option<serde_json::Value>,
pub tag: Option<String>,
@@ -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<String>,
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub on_success: Option<String>,
pub on_success_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: bool,
pub retry: Option<serde_json::Value>,
pub jobs: Option<Vec<serde_json::Value>>,
@@ -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<String>,
pub on_recovery_times: Option<i32>,
pub on_recovery_extra_args: Option<serde_json::Value>,
pub on_success: Option<String>,
pub on_success_extra_args: Option<serde_json::Value>,
pub ws_error_handler_muted: Option<bool>,
pub retry: Option<serde_json::Value>,
pub no_flow_overlap: Option<bool>,
+4 -1
View File
@@ -42,7 +42,10 @@ pub struct Schedule {
pub on_recovery_times: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_recovery_extra_args: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_success: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_success_extra_args: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
pub ws_error_handler_muted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry: Option<serde_json::Value>,
+99 -1
View File
@@ -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<Postgres>,
rsmq: Option<R>,
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<Utc>,
extra_args: Option<Json<Box<RawValue>>>,
) -> 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::<Box<RawValue>>(
&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::<HashMap<String, Box<RawValue>>>(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<R: rsmq_async::RsmqConnection + Send + Clone>(
db: &Pool<Postgres>,
rsmq: Option<R>,
@@ -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
}
@@ -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<string, any> = {}
let successHandlerPath: string | undefined = undefined
let successHandlerCustomInitialPath: string | undefined = undefined
let successHandlerSelected: 'custom' | 'slack' = 'slack'
let successHandlerItemKind: 'flow' | 'script' = 'script'
let successHandlerExtraArgs: Record<string, any> = {}
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<void> {
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 @@
<Tabs bind:selected={optionTabSelected}>
<Tab value="error_handler">Error Handler</Tab>
<Tab value="recovery_handler">Recovery Handler</Tab>
<Tab value="success_handler">Success Handler</Tab>
{#if itemKind === 'script'}
<Tab value="retries">Retries</Tab>
<Tab value="tag">Custom tag</Tab>
@@ -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 @@
</div>
</div>
</Section>
{:else if optionTabSelected === 'success_handler'}
<Section label="Success handler">
<svelte:fragment slot="header">
<div class="flex flex-row gap-2">
{#if !$enterpriseLicense}<span class="text-normal text-2xs">(ee only)</span
>{/if}
</div>
</svelte:fragment>
<svelte:fragment slot="action">
<div class="flex flex-row items-center text-tertiary text-2xs gap-2">
defaults
<Dropdown
items={[
{
displayName: `Override future schedules only`,
action: () => saveAsDefaultSuccessHandler(false)
},
{
displayName: 'Override all existing',
type: 'delete',
action: () => saveAsDefaultSuccessHandler(true)
}
]}
>
<svelte:fragment>
<Save size={12} class="mr-1" />
Set as default
</svelte:fragment>
</Dropdown>
</div>
</svelte:fragment>
<ErrorOrRecoveryHandler
isEditable={can_write && !emptyString($enterpriseLicense)}
errorOrRecovery="success"
bind:handlerSelected={successHandlerSelected}
bind:handlerPath={successHandlerPath}
customInitialScriptPath={successHandlerCustomInitialPath}
slackToggleText="Alert channel when successful"
customScriptTemplate="/scripts/add?hub=hub%2F9071%2Fwindmill%2Fschedule_success_handler_template"
bind:customHandlerKind={successHandlerItemKind}
bind:handlerExtraArgs={successHandlerExtraArgs}
>
<svelte:fragment slot="custom-tab-tooltip">
<Tooltip>
<div class="flex gap-20 items-start mt-3">
<div class=" text-sm"
>The following args will be passed to the success handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that succeeded.</li>
<li><b>is_flow</b>: Whether the runnable is a flow.</li>
<li><b>schedule_path</b>: The path of the schedule.</li>
<li><b>success_result</b>: The result of the successful job</li>
<li
><b>success_started_at</b>: The start datetime of the successful job</li
>
</ul>
</div>
</div>
</Tooltip>
</svelte:fragment>
</ErrorOrRecoveryHandler>
</Section>
{:else if optionTabSelected === 'retries'}
<Section label="Retries">
<svelte:fragment slot="header">
+1
View File
@@ -62,6 +62,7 @@ type HubPaths = {
gitSyncTest: string
slackErrorHandler: string
slackRecoveryHandler: string
slackSuccessHandler: string
slackReport: string
discordReport: string
smtpReport: string
+3 -1
View File
@@ -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"