mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: schedule success handler (#4346)
This commit is contained in:
+22
@@ -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"
|
||||
}
|
||||
+24
@@ -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"
|
||||
}
|
||||
+12
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -62,6 +62,7 @@ type HubPaths = {
|
||||
gitSyncTest: string
|
||||
slackErrorHandler: string
|
||||
slackRecoveryHandler: string
|
||||
slackSuccessHandler: string
|
||||
slackReport: string
|
||||
discordReport: string
|
||||
smtpReport: string
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user