mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
feat: add email workspace error handler if smtp is configured (#6267)
* detecting trigger kind on job error * v0 done * done * updat .sqlx and add mgrations * fix: set back vs code settings and unused * fix unused import * add feature flag * update ref * nits * nits * done * update repo ref * remove * add isCloudHosted * update sqlx * big * fix * use extra args instead of new col * nits * update .sqllx * update ref * update email * fix * fix * fix * nits * update script and use env base_internal_url * update sqlx and fix query * remove * fix unused import * update ref
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = NULL\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "04f8d738b1073b8c58db0965e8fdcdb872dce2c1872359c1bebb553a29ba1637"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0d8415a79d8312db4d46da46fdf348ab17dee7b8358b6719fa399511962bc3e9"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n workspace_settings\n SET\n error_handler = $1,\n error_handler_extra_args = $2,\n error_handler_muted_on_cancel = $3\n WHERE \n workspace_id = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2549197c6750bb1a20041b2d7c2654a788a32a9282ed314c2b3bcafe6550efca"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n email = $9,\n can_return_message = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"JsonbArray",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "35b211d19e53da4b64b0bd097284de3236ab939e47a1fc2b15ffc9607b552f8d"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\" \n FROM postgres_trigger\n WHERE workspace_id = $1",
|
||||
"query": "\n SELECT \n workspace_id,\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n replication_slot_name,\n publication_name,\n postgres_resource_path,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n postgres_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -120,5 +120,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bf1fba73d3109aae48bda524cfafdf7f5b9b0e56396aed1b83a2c235222e8983"
|
||||
"hash": "3ebf788b86d6aed59b8c61c08f1d9323e6c66107a1b7b37dadbbb7b91339e9d5"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE kafka_trigger SET kafka_resource_path = $1, group_id = $2, topics = $3, script_path = $4, path = $5, is_flow = $6, edited_by = $7, email = $8, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $11, error_handler_args = $12, retry = $13\n WHERE workspace_id = $9 AND path = $10",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3f91433fc076f47dd3516b40c9337981c329ddc31132882f037eaf2709aa8805"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15\n WHERE workspace_id = $11 AND path = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"JsonbArray",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4479beaefc36afba093e5113bb330a87979623d69b94f274863318ed351a1dac"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n kafka_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -121,5 +121,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6489342f52a9e1e90466bad48e0a6079fcabd696b02d57e3dbe652d28b84f008"
|
||||
"hash": "494d49e92dce7a1cd7074156704ccbbff020a2042409fc318e045852323cef5e"
|
||||
}
|
||||
+2
-1
@@ -223,7 +223,8 @@
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"mqtt"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4bfa42153df81184e02ffc7ab6c8c591605211c070e52b2919cf9d7dede0ea16"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\" \n FROM kafka_trigger\n WHERE workspace_id = $1",
|
||||
"query": "SELECT \n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM kafka_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -120,5 +120,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8f511fd837a99fa37a95781e50f8a83eaf93651f82c36c9795637d3ad761d003"
|
||||
"hash": "4c6fbed80ba44defe3d125c258bc3449b02925c8caf85d9203529471479fdde8"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE nats_trigger SET nats_resource_path = $1, subjects = $2, stream_name = $3, consumer_name = $4, use_jetstream = $5, script_path = $6, path = $7, is_flow = $8, edited_by = $9, email = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15\n WHERE workspace_id = $11 AND path = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "570a4684bbdb390e4652301aa8d2d09b6e09b258f748d48cf2d188465621ee04"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM nats_trigger\n WHERE workspace_id = $1",
|
||||
"query": "\n SELECT \n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n nats_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -132,5 +132,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5af0299841a0313d0cc270e98b04b5b5968184ae53dc076b1a9bc76af35923da"
|
||||
"hash": "711e665344873454428d84e35f300edb7e0ebad5de40ba5645c1ce3d4a6b84be"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_config, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, ducklake, git_sync, deploy_ui, default_app, default_scripts, mute_critical_alerts, color, operator_settings, git_app_installations FROM workspace_settings WHERE workspace_id = $1",
|
||||
"query": "\n SELECT \n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n auto_invite_domain,\n auto_invite_operator,\n auto_add,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n error_handler,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations\n FROM \n workspace_settings\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -186,5 +186,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6e60068b582af46afbb93010355cb5e7cc038042a6fa7089f3f73cd137a63956"
|
||||
"hash": "8960b73f0d3cbfa0729d24ecbe3f635592feee6a5724fdff662c6ac1f3c6ddc8"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n summary,\n description,\n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM \n http_trigger\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -183,5 +183,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "09f43081a8b253d059d9d4302daa139cee6f055cdbafc213b2f992667f8ed578"
|
||||
"hash": "89be9b595a892c82f2a9f3cfa4da213e360ebe8827d5b4680d3c227fb8ddec82"
|
||||
}
|
||||
+25
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26,\n CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -98,10 +98,32 @@
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"TextArray",
|
||||
"Uuid"
|
||||
"Uuid",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b7c3a66c3831eb5d145ff00807badae57bef81be051f150df754fd1444d7356d"
|
||||
"hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597"
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET error_handler = $1, error_handler_extra_args = $2, error_handler_muted_on_cancel = $3 WHERE workspace_id = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d21e5be1ac26db926b7196316dae0c9fa82a865b95bb55de5370e4ee34889ba3"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET error_handler = NULL, error_handler_extra_args = NULL WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d9308c0154e029f7568b1d021368ca07f4867d192aa58b3888ae532d1c040828"
|
||||
}
|
||||
+1
-1
@@ -77,7 +77,7 @@ mcp = ["windmill-api/mcp"]
|
||||
mqtt_trigger = ["windmill-api/mqtt_trigger"]
|
||||
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
|
||||
gcp_trigger = ["windmill-api/gcp_trigger"]
|
||||
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
|
||||
smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"]
|
||||
license = ["windmill-api/license"]
|
||||
oauth2 = ["windmill-api/oauth2"]
|
||||
zip = ["windmill-api/zip"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dbc87a4966118e5486226d1a1863e4d97ba82ecc
|
||||
db17bc0b22a51192aeadd8a0eab44108b0725a5b
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'mqtt';
|
||||
@@ -2440,7 +2440,6 @@ paths:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
error_handler_muted_on_cancel:
|
||||
type: boolean
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -2449,6 +2448,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/workspaces/edit_large_file_storage_config:
|
||||
post:
|
||||
summary: edit large file storage settings
|
||||
@@ -15202,6 +15202,14 @@ components:
|
||||
- success
|
||||
- duration_ms
|
||||
|
||||
ErrorHandler:
|
||||
type: string
|
||||
enum:
|
||||
- custom
|
||||
- slack
|
||||
- teams
|
||||
- email
|
||||
|
||||
NewSchedule:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::{
|
||||
users::fetch_api_authed,
|
||||
utils::{check_scopes, non_empty_str, ExpiringCacheEntry},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::response::Response;
|
||||
use axum::{
|
||||
@@ -197,38 +196,37 @@ async fn list_triggers(
|
||||
) -> error::JsonResult<Vec<HttpTrigger>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
|
||||
let mut sqlb = SqlBuilder::select_from("http_trigger")
|
||||
.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"route_path",
|
||||
"route_path_key",
|
||||
"workspaced_route",
|
||||
"wrap_body",
|
||||
"raw_string",
|
||||
"script_path",
|
||||
"summary",
|
||||
"description",
|
||||
"is_flow",
|
||||
"http_method",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"is_async",
|
||||
"authentication_method",
|
||||
"static_asset_config",
|
||||
"is_static_website",
|
||||
"authentication_resource_path",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
let mut sqlb = SqlBuilder::select_from("http_trigger");
|
||||
sqlb.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"route_path",
|
||||
"route_path_key",
|
||||
"workspaced_route",
|
||||
"wrap_body",
|
||||
"raw_string",
|
||||
"script_path",
|
||||
"summary",
|
||||
"description",
|
||||
"is_flow",
|
||||
"http_method",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"is_async",
|
||||
"authentication_method",
|
||||
"static_asset_config",
|
||||
"is_static_website",
|
||||
"authentication_resource_path",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page);
|
||||
if let Some(path) = lst.path {
|
||||
sqlb.and_where_eq("script_path", "?".bind(&path));
|
||||
}
|
||||
@@ -391,7 +389,7 @@ async fn create_trigger_inner(
|
||||
new_http_trigger.is_static_website,
|
||||
new_http_trigger.error_handler_path,
|
||||
new_http_trigger.error_handler_args as _,
|
||||
new_http_trigger.retry as _,
|
||||
new_http_trigger.retry as _
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -16,6 +16,7 @@ use http::{HeaderMap, HeaderName};
|
||||
use itertools::Itertools;
|
||||
use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::Value;
|
||||
use sqlx::Pool;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
@@ -24,6 +25,8 @@ use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tower::ServiceBuilder;
|
||||
#[cfg(feature = "smtp")]
|
||||
use windmill_common::auth::is_super_admin_email;
|
||||
use windmill_common::auth::TOKEN_PREFIX_LEN;
|
||||
use windmill_common::error::JsonResult;
|
||||
use windmill_common::flow_status::{JobResult, RestartedFrom};
|
||||
@@ -33,6 +36,8 @@ use windmill_common::jobs::{
|
||||
};
|
||||
use windmill_common::utils::WarnAfterExt;
|
||||
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
|
||||
#[cfg(feature = "smtp")]
|
||||
use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
|
||||
|
||||
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
|
||||
use windmill_common::variables::get_workspace_key;
|
||||
@@ -66,7 +71,9 @@ use tower_http::cors::{Any, CorsLayer};
|
||||
use urlencoding::encode;
|
||||
use windmill_audit::audit_oss::{audit_log, AuditAuthor};
|
||||
use windmill_audit::ActionKind;
|
||||
|
||||
use windmill_common::worker::to_raw_value;
|
||||
|
||||
use windmill_common::{
|
||||
cache,
|
||||
db::UserDB,
|
||||
@@ -85,7 +92,7 @@ use windmill_common::{
|
||||
|
||||
use windmill_common::{
|
||||
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
|
||||
get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, BASE_URL,
|
||||
get_script_info_for_hash, utils::empty_as_none, FlowVersionInfo, ScriptHashInfo, BASE_URL,
|
||||
};
|
||||
use windmill_queue::{
|
||||
cancel_job, get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs,
|
||||
@@ -234,6 +241,10 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
.route("/run/dependencies", post(run_dependencies_job))
|
||||
.route("/run/flow_dependencies", post(run_flow_dependencies_job))
|
||||
.route(
|
||||
"/send_email_with_instance_smtp",
|
||||
post(send_email_with_instance_smtp),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn workspace_unauthed_service() -> Router {
|
||||
@@ -1037,6 +1048,240 @@ impl<'a> GetQuery<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp", feature = "enterprise"))]
|
||||
async fn send_workspace_trigger_failure_email_notification(
|
||||
db: &Pool<Postgres>,
|
||||
w_id: &str,
|
||||
job_id: &Uuid,
|
||||
trigger_path: Option<&str>,
|
||||
runnable_path: Option<&str>,
|
||||
email_recipients: &Vec<String>,
|
||||
error: &Value,
|
||||
) -> Result<String, Error> {
|
||||
let smtp_config = match load_smtp_config(db).await? {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
tracing::info!(
|
||||
"SMTP not configured, skipping workspace trigger failure email notification"
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"SMTP not configured, skipping workspace trigger failure email notification"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
let runnable_path = runnable_path.as_deref().unwrap_or("Unknown");
|
||||
|
||||
let (trigger_kind, trigger_path) = if let Some(trigger_path) = trigger_path.as_deref() {
|
||||
match trigger_path.split_once('/') {
|
||||
Some((trigger_kind, trigger_path)) => {
|
||||
tracing::debug!(
|
||||
"Workspace trigger job {} is a {:?} trigger",
|
||||
&job_id,
|
||||
trigger_kind
|
||||
);
|
||||
(Some(trigger_kind), Some(trigger_path))
|
||||
}
|
||||
_ => (None, None),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let base_url = BASE_URL.read().await;
|
||||
let job_url = format!("{}/run/{}?workspace={}", base_url, &job_id, w_id);
|
||||
let trigger_kind_str = trigger_kind.unwrap_or("Unknown").to_string().to_uppercase();
|
||||
|
||||
let subject = format!(
|
||||
"Windmill Job Failed: {} in workspace {}",
|
||||
runnable_path, w_id
|
||||
);
|
||||
|
||||
let error_details = serde_json::to_string_pretty(&error)
|
||||
.unwrap_or_else(|_| format!("Unable to serialize error: {:?}", error));
|
||||
|
||||
let trigger_info = if trigger_kind.is_some() && trigger_path.is_some() {
|
||||
format!(
|
||||
r#"
|
||||
<div class="section">
|
||||
<span class="label">Trigger path:</span> {}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Trigger Type:</span> {}
|
||||
</div>"#,
|
||||
trigger_path.unwrap(),
|
||||
trigger_kind_str
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let email_title = if trigger_kind.is_some() {
|
||||
format!("Windmill Trigger Job {} Failed", &job_id)
|
||||
} else {
|
||||
format!("Windmill Job {} Failed", &job_id)
|
||||
};
|
||||
|
||||
let content = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; color: #333; }}
|
||||
h1 {{ color: #c0392b; }}
|
||||
.section {{ margin-bottom: 20px; }}
|
||||
.label {{ font-weight: bold; }}
|
||||
pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; }}
|
||||
a.button {{
|
||||
display: inline-block;
|
||||
padding: 10px 15px;
|
||||
margin-top: 10px;
|
||||
color: #fff;
|
||||
background-color: #2980b9;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{}</h1>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Workspace:</span> {}
|
||||
</div>
|
||||
{}
|
||||
<div class="section">
|
||||
<span class="label">Script/Flow Path:</span> {}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Job ID:</span> {}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<span class="label">Error Details:</span>
|
||||
<pre>{}</pre>
|
||||
</div>
|
||||
|
||||
<a href="{}" class="button">View Job Details</a>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
email_title,
|
||||
w_id,
|
||||
trigger_info,
|
||||
runnable_path,
|
||||
&job_id,
|
||||
error_details,
|
||||
job_url
|
||||
);
|
||||
|
||||
if let Err(e) = send_email_html(
|
||||
&subject,
|
||||
&content,
|
||||
email_recipients.to_owned(),
|
||||
smtp_config,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let err_msg = format!(
|
||||
"Failed to send workspace email notification for trigger failure (trigger kind: {}, job ID: {}): {}",
|
||||
trigger_kind_str,
|
||||
&job_id,
|
||||
e
|
||||
);
|
||||
return Err(Error::internal_err(err_msg));
|
||||
}
|
||||
let success_msg = format!(
|
||||
"Job ID '{}' failed. An email with error details has been sent to {:?}.",
|
||||
&job_id, email_recipients
|
||||
);
|
||||
tracing::info!("{}", &success_msg);
|
||||
Ok(success_msg)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SendEmail {
|
||||
job_id: Uuid,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
trigger_path: Option<String>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
runnable_path: Option<String>,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
email_recipients: Option<Vec<String>>,
|
||||
error: Value,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp"))]
|
||||
async fn send_email_with_instance_smtp(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(send_email): Json<SendEmail>,
|
||||
) -> error::Result<Json<String>> {
|
||||
use windmill_common::jobs::EMAIL_ERROR_HANDLER_USER_EMAIL;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::warn!(
|
||||
"Workspace trigger failure email notification is not available for cloud hosted Windmill",
|
||||
);
|
||||
return Err(anyhow::anyhow!("Feature not supported in cloud hosted windmill").into());
|
||||
}
|
||||
|
||||
if send_email.email_recipients.is_none() {
|
||||
use windmill_common::utils::report_critical_error;
|
||||
|
||||
tracing::error!("No recipient to send the error");
|
||||
report_critical_error(
|
||||
"No recipient to send the error".to_string(),
|
||||
db.clone(),
|
||||
Some(&w_id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
return Err(anyhow::anyhow!("No recipient to send the error").into());
|
||||
}
|
||||
|
||||
if authed.email == EMAIL_ERROR_HANDLER_USER_EMAIL
|
||||
|| is_super_admin_email(&db, &authed.email).await?
|
||||
{
|
||||
let resp = send_workspace_trigger_failure_email_notification(
|
||||
&db,
|
||||
&w_id,
|
||||
&send_email.job_id,
|
||||
send_email.trigger_path.as_deref(),
|
||||
send_email.runnable_path.as_deref(),
|
||||
&send_email.email_recipients.unwrap(),
|
||||
&send_email.error,
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(Json(resp));
|
||||
}
|
||||
|
||||
return Err(Error::NotAuthorized(
|
||||
"Only super admin or whitelisted token can access email workspace error handler feature"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "smtp")))]
|
||||
async fn send_email_with_instance_smtp(
|
||||
_authed: ApiAuthed,
|
||||
Extension(_db): Extension<DB>,
|
||||
Path(_w_id): Path<String>,
|
||||
Json(_send_email): Json<SendEmail>,
|
||||
) -> error::Result<Json<String>> {
|
||||
tracing::warn!("SMTP is not enabled, skipping workspace trigger failure email notification",);
|
||||
|
||||
return Err(anyhow::anyhow!("SMTP is not enabled").into());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn get_logs_from_store(
|
||||
log_offset: i32,
|
||||
|
||||
@@ -644,35 +644,34 @@ pub async fn list_mqtt_triggers(
|
||||
) -> error::JsonResult<Vec<MqttTrigger>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
|
||||
let mut sqlb = SqlBuilder::select_from("mqtt_trigger")
|
||||
.fields(&[
|
||||
"mqtt_resource_path",
|
||||
"subscribe_topics",
|
||||
"v3_config",
|
||||
"v5_config",
|
||||
"client_version",
|
||||
"client_id",
|
||||
"workspace_id",
|
||||
"path",
|
||||
"script_path",
|
||||
"is_flow",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"server_id",
|
||||
"last_server_ping",
|
||||
"extra_perms",
|
||||
"error",
|
||||
"enabled",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
let mut sqlb = SqlBuilder::select_from("mqtt_trigger");
|
||||
sqlb.fields(&[
|
||||
"mqtt_resource_path",
|
||||
"subscribe_topics",
|
||||
"v3_config",
|
||||
"v5_config",
|
||||
"client_version",
|
||||
"client_id",
|
||||
"workspace_id",
|
||||
"path",
|
||||
"script_path",
|
||||
"is_flow",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"server_id",
|
||||
"last_server_ping",
|
||||
"extra_perms",
|
||||
"error",
|
||||
"enabled",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page);
|
||||
if let Some(path) = lst.path {
|
||||
sqlb.and_where_eq("script_path", "?".bind(&path));
|
||||
}
|
||||
@@ -760,7 +759,9 @@ pub async fn update_mqtt_trigger(
|
||||
Json(mqtt_trigger): Json<EditMqttTrigger>,
|
||||
) -> error::Result<String> {
|
||||
let workspace_path = path.to_path();
|
||||
check_scopes(&authed, || format!("mqtt_triggers:write:{}", workspace_path))?;
|
||||
check_scopes(&authed, || {
|
||||
format!("mqtt_triggers:write:{}", workspace_path)
|
||||
})?;
|
||||
|
||||
let EditMqttTrigger {
|
||||
mqtt_resource_path,
|
||||
|
||||
@@ -374,7 +374,9 @@ pub async fn create_postgres_trigger(
|
||||
Path(w_id): Path<String>,
|
||||
Json(new_postgres_trigger): Json<NewPostgresTrigger>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
check_scopes(&authed, || format!("postgres_triggers:write:{}", new_postgres_trigger.path))?;
|
||||
check_scopes(&authed, || {
|
||||
format!("postgres_triggers:write:{}", new_postgres_trigger.path)
|
||||
})?;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
return Err(error::Error::BadRequest(
|
||||
@@ -515,32 +517,31 @@ pub async fn list_postgres_triggers(
|
||||
) -> error::JsonResult<Vec<PostgresTrigger>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
|
||||
let mut sqlb = SqlBuilder::select_from("postgres_trigger")
|
||||
.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"script_path",
|
||||
"is_flow",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"server_id",
|
||||
"last_server_ping",
|
||||
"extra_perms",
|
||||
"error",
|
||||
"enabled",
|
||||
"postgres_resource_path",
|
||||
"replication_slot_name",
|
||||
"publication_name",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
let mut sqlb = SqlBuilder::select_from("postgres_trigger");
|
||||
sqlb.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"script_path",
|
||||
"is_flow",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"server_id",
|
||||
"last_server_ping",
|
||||
"extra_perms",
|
||||
"error",
|
||||
"enabled",
|
||||
"postgres_resource_path",
|
||||
"replication_slot_name",
|
||||
"publication_name",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page);
|
||||
if let Some(path) = lst.path {
|
||||
sqlb.and_where_eq("script_path", "?".bind(&path));
|
||||
}
|
||||
@@ -1204,7 +1205,9 @@ pub async fn update_postgres_trigger(
|
||||
Json(postgres_trigger): Json<EditPostgresTrigger>,
|
||||
) -> Result<String> {
|
||||
let workspace_path = path.to_path();
|
||||
check_scopes(&authed, || format!("postgres_triggers:write:{}", workspace_path))?;
|
||||
check_scopes(&authed, || {
|
||||
format!("postgres_triggers:write:{}", workspace_path)
|
||||
})?;
|
||||
|
||||
let EditPostgresTrigger {
|
||||
replication_slot_name,
|
||||
@@ -1298,7 +1301,7 @@ pub async fn update_postgres_trigger(
|
||||
workspace_path,
|
||||
error_handler_path,
|
||||
error_handler_args as _,
|
||||
retry as _,
|
||||
retry as _
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -32,7 +32,7 @@ use serde::Deserialize;
|
||||
use windmill_common::ee_oss::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel};
|
||||
use windmill_common::error::to_anyhow;
|
||||
use windmill_common::{
|
||||
email_oss::send_email,
|
||||
email_oss::send_email_plain_text,
|
||||
error::{self, JsonResult, Result},
|
||||
get_database_url,
|
||||
global_settings::{
|
||||
@@ -105,7 +105,7 @@ pub async fn test_email(
|
||||
let to = test_email.to;
|
||||
|
||||
let client_timeout = Duration::from_secs(3);
|
||||
send_email(
|
||||
send_email_plain_text(
|
||||
"Test email from Windmill",
|
||||
"Test email content",
|
||||
vec![to],
|
||||
|
||||
@@ -24,7 +24,9 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, to_anyhow, JsonResult},
|
||||
triggers::TriggerKind,
|
||||
utils::{not_found_if_none, paginate, report_critical_error, Pagination, StripPath},
|
||||
utils::{
|
||||
empty_as_none, not_found_if_none, paginate, report_critical_error, Pagination, StripPath,
|
||||
},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
INSTANCE_NAME,
|
||||
};
|
||||
@@ -64,6 +66,7 @@ struct NewWebsocketTrigger {
|
||||
initial_messages: Option<Vec<Box<RawValue>>>,
|
||||
url_runnable_args: Option<Box<RawValue>>,
|
||||
can_return_message: bool,
|
||||
#[serde(default, deserialize_with = "empty_as_none")]
|
||||
error_handler_path: Option<String>,
|
||||
error_handler_args: Option<SqlxJson<HashMap<String, Box<RawValue>>>>,
|
||||
retry: Option<SqlxJson<windmill_common::flows::Retry>>,
|
||||
@@ -152,13 +155,34 @@ async fn list_websocket_triggers(
|
||||
) -> error::JsonResult<Vec<WebsocketTrigger>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(Pagination { per_page: lst.per_page, page: lst.page });
|
||||
let mut sqlb = SqlBuilder::select_from("websocket_trigger")
|
||||
.field("*")
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
let mut sqlb = SqlBuilder::select_from("websocket_trigger");
|
||||
|
||||
sqlb.fields(&[
|
||||
"workspace_id",
|
||||
"path",
|
||||
"url",
|
||||
"script_path",
|
||||
"is_flow",
|
||||
"edited_by",
|
||||
"email",
|
||||
"edited_at",
|
||||
"server_id",
|
||||
"last_server_ping",
|
||||
"extra_perms",
|
||||
"error",
|
||||
"enabled",
|
||||
"filters",
|
||||
"initial_messages",
|
||||
"url_runnable_args",
|
||||
"can_return_message",
|
||||
"error_handler_path",
|
||||
"error_handler_args",
|
||||
"retry",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page);
|
||||
if let Some(path) = lst.path {
|
||||
sqlb.and_where_eq("script_path", "?".bind(&path));
|
||||
}
|
||||
@@ -188,14 +212,39 @@ async fn get_websocket_trigger(
|
||||
check_scopes(&authed, || format!("websocket_triggers:read:{}", path))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let trigger = sqlx::query_as::<_, WebsocketTrigger>(
|
||||
r#"SELECT *
|
||||
FROM websocket_trigger
|
||||
WHERE workspace_id = $1 AND path = $2"#,
|
||||
r#"
|
||||
SELECT
|
||||
workspace_id,
|
||||
path,
|
||||
url,
|
||||
script_path,
|
||||
is_flow,
|
||||
edited_by,
|
||||
email,
|
||||
edited_at,
|
||||
server_id,
|
||||
last_server_ping,
|
||||
extra_perms,
|
||||
error,
|
||||
enabled,
|
||||
filters,
|
||||
initial_messages,
|
||||
url_runnable_args,
|
||||
can_return_message,
|
||||
error_handler_path,
|
||||
error_handler_args,
|
||||
retry,
|
||||
FROM
|
||||
websocket_trigger
|
||||
WHERE
|
||||
workspace_id = $1 AND path = $2
|
||||
"#,
|
||||
)
|
||||
.bind(w_id)
|
||||
.bind(path)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let trigger = not_found_if_none(trigger, "Trigger", path)?;
|
||||
@@ -317,8 +366,29 @@ async fn update_websocket_trigger(
|
||||
|
||||
// important to update server_id to NULL to stop current websocket listener
|
||||
sqlx::query!(
|
||||
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL, error_handler_path = $13, error_handler_args = $14, retry = $15
|
||||
WHERE workspace_id = $11 AND path = $12",
|
||||
"
|
||||
UPDATE
|
||||
websocket_trigger
|
||||
SET
|
||||
url = $1,
|
||||
script_path = $2,
|
||||
path = $3,
|
||||
is_flow = $4,
|
||||
filters = $5,
|
||||
initial_messages = $6,
|
||||
url_runnable_args = $7,
|
||||
edited_by = $8,
|
||||
email = $9,
|
||||
can_return_message = $10,
|
||||
edited_at = now(),
|
||||
server_id = NULL,
|
||||
error = NULL,
|
||||
error_handler_path = $13,
|
||||
error_handler_args = $14,
|
||||
retry = $15
|
||||
WHERE
|
||||
workspace_id = $11 AND path = $12
|
||||
",
|
||||
ct.url,
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
@@ -333,9 +403,10 @@ async fn update_websocket_trigger(
|
||||
path,
|
||||
ct.error_handler_path,
|
||||
ct.error_handler_args as _,
|
||||
ct.retry as _,
|
||||
ct.retry as _
|
||||
)
|
||||
.execute(&mut *tx).await?;
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
@@ -546,18 +617,45 @@ async fn listen_to_unlistened_websockets(
|
||||
db: &DB,
|
||||
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
match sqlx::query_as::<_, WebsocketTrigger>(
|
||||
r#"SELECT *
|
||||
FROM websocket_trigger
|
||||
WHERE enabled IS TRUE AND (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')"#
|
||||
let websocket_triggers = sqlx::query_as::<_, WebsocketTrigger>(
|
||||
r#"
|
||||
SELECT
|
||||
workspace_id,
|
||||
path,
|
||||
url,
|
||||
script_path,
|
||||
is_flow,
|
||||
edited_by,
|
||||
email,
|
||||
edited_at,
|
||||
server_id,
|
||||
last_server_ping,
|
||||
extra_perms,
|
||||
error,
|
||||
enabled,
|
||||
filters,
|
||||
initial_messages,
|
||||
url_runnable_args,
|
||||
can_return_message,
|
||||
error_handler_path,
|
||||
error_handler_args,
|
||||
retry
|
||||
FROM websocket_trigger
|
||||
WHERE
|
||||
enabled IS TRUE
|
||||
AND (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')
|
||||
"#,
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
|
||||
match websocket_triggers {
|
||||
Ok(mut triggers) => {
|
||||
triggers.shuffle(&mut rand::rng());
|
||||
for trigger in triggers {
|
||||
trigger.maybe_listen_to_websocket(db.clone(), killpill_rx.resubscribe()).await;
|
||||
trigger
|
||||
.maybe_listen_to_websocket(db.clone(), killpill_rx.resubscribe())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -381,6 +381,10 @@ pub struct EditErrorHandler {
|
||||
pub error_handler_muted_on_cancel: Option<bool>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EMAIL_REGEXP: Regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
|
||||
}
|
||||
|
||||
async fn list_pending_invites(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -457,15 +461,51 @@ async fn get_settings(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let settings = sqlx::query_as!(
|
||||
WorkspaceSettings,
|
||||
"SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_config, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, ducklake, git_sync, deploy_ui, default_app, default_scripts, mute_critical_alerts, color, operator_settings, git_app_installations FROM workspace_settings WHERE workspace_id = $1",
|
||||
r#"
|
||||
SELECT
|
||||
workspace_id,
|
||||
slack_team_id,
|
||||
teams_team_id,
|
||||
teams_team_name,
|
||||
slack_name,
|
||||
slack_command_script,
|
||||
teams_command_script,
|
||||
slack_email,
|
||||
auto_invite_domain,
|
||||
auto_invite_operator,
|
||||
auto_add,
|
||||
customer_id,
|
||||
plan,
|
||||
webhook,
|
||||
deploy_to,
|
||||
ai_config,
|
||||
error_handler,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel,
|
||||
large_file_storage,
|
||||
ducklake,
|
||||
git_sync,
|
||||
deploy_ui,
|
||||
default_app,
|
||||
default_scripts,
|
||||
mute_critical_alerts,
|
||||
color,
|
||||
operator_settings,
|
||||
git_app_installations
|
||||
FROM
|
||||
workspace_settings
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("getting settings: {e:#}")))?;
|
||||
tx.commit().await?;
|
||||
let settings = not_found_if_none(settings, "workspace settings", &w_id)?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let settings = not_found_if_none(settings, "workspace settings", &w_id)?;
|
||||
Ok(Json(settings))
|
||||
}
|
||||
|
||||
@@ -1645,8 +1685,46 @@ async fn edit_error_handler(
|
||||
.await?;
|
||||
|
||||
if let Some(error_handler) = &ee.error_handler {
|
||||
match ee.error_handler_extra_args.as_ref() {
|
||||
Some(extra_args) if extra_args.is_object() => {
|
||||
let Ok(email_recipients) = serde_json::from_value::<Option<Vec<String>>>(
|
||||
extra_args["email_recipients"].to_owned(),
|
||||
) else {
|
||||
return Err(Error::BadRequest(
|
||||
"Field `email_recipients` expected to be JSON array".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(email_recipients) = email_recipients {
|
||||
for email in email_recipients {
|
||||
if !EMAIL_REGEXP.is_match(&email) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Invalid email format: {}",
|
||||
email
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
"Field `error_handler_extra_args` expected to be JSON object".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET error_handler = $1, error_handler_extra_args = $2, error_handler_muted_on_cancel = $3 WHERE workspace_id = $4",
|
||||
r#"
|
||||
UPDATE
|
||||
workspace_settings
|
||||
SET
|
||||
error_handler = $1,
|
||||
error_handler_extra_args = $2,
|
||||
error_handler_muted_on_cancel = $3
|
||||
WHERE
|
||||
workspace_id = $4
|
||||
"#,
|
||||
error_handler,
|
||||
ee.error_handler_extra_args,
|
||||
ee.error_handler_muted_on_cancel.unwrap_or(false),
|
||||
@@ -1656,12 +1734,22 @@ async fn edit_error_handler(
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET error_handler = NULL, error_handler_extra_args = NULL WHERE workspace_id = $1",
|
||||
&w_id,
|
||||
r#"
|
||||
UPDATE
|
||||
workspace_settings
|
||||
SET
|
||||
error_handler = NULL,
|
||||
error_handler_extra_args = NULL,
|
||||
error_handler_muted_on_cancel = NULL
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
|
||||
@@ -564,8 +564,10 @@ pub(crate) async fn tarball_workspace(
|
||||
error_handler_path,
|
||||
error_handler_args as "error_handler_args: _",
|
||||
retry as "retry: _"
|
||||
FROM http_trigger
|
||||
WHERE workspace_id = $1
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
)
|
||||
@@ -649,7 +651,7 @@ pub(crate) async fn tarball_workspace(
|
||||
enabled,
|
||||
error_handler_path,
|
||||
error_handler_args as "error_handler_args: _",
|
||||
retry as "retry: _"
|
||||
retry as "retry: _"
|
||||
FROM kafka_trigger
|
||||
WHERE workspace_id = $1"#,
|
||||
&w_id
|
||||
@@ -760,7 +762,8 @@ pub(crate) async fn tarball_workspace(
|
||||
{
|
||||
let nats_triggers = sqlx::query_as!(
|
||||
crate::nats_triggers_oss::NatsTrigger,
|
||||
r#"SELECT
|
||||
r#"
|
||||
SELECT
|
||||
workspace_id,
|
||||
path,
|
||||
nats_resource_path,
|
||||
@@ -781,8 +784,11 @@ pub(crate) async fn tarball_workspace(
|
||||
error_handler_path,
|
||||
error_handler_args as "error_handler_args: _",
|
||||
retry as "retry: _"
|
||||
FROM nats_trigger
|
||||
WHERE workspace_id = $1"#,
|
||||
FROM
|
||||
nats_trigger
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -801,7 +807,8 @@ pub(crate) async fn tarball_workspace(
|
||||
{
|
||||
let postgres_triggers = sqlx::query_as!(
|
||||
crate::postgres_triggers::PostgresTrigger,
|
||||
r#"SELECT
|
||||
r#"
|
||||
SELECT
|
||||
workspace_id,
|
||||
path,
|
||||
script_path,
|
||||
@@ -819,9 +826,12 @@ pub(crate) async fn tarball_workspace(
|
||||
postgres_resource_path,
|
||||
error_handler_path,
|
||||
error_handler_args as "error_handler_args: _",
|
||||
retry as "retry: _"
|
||||
FROM postgres_trigger
|
||||
WHERE workspace_id = $1"#,
|
||||
retry as "retry: _"
|
||||
FROM
|
||||
postgres_trigger
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
|
||||
@@ -14,8 +14,6 @@ use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use crate::{jwt::decode_without_verify, worker::HttpClient};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref BASE_INTERNAL_URL: String =
|
||||
std::env::var("BASE_INTERNAL_URL").unwrap_or("http://localhost:8080".to_string());
|
||||
pub static ref AGENT_TOKEN: String = std::env::var("AGENT_TOKEN").unwrap_or_default();
|
||||
pub static ref DECODED_AGENT_TOKEN: Option<AgentAuth> = {
|
||||
if AGENT_TOKEN.is_empty() {
|
||||
|
||||
@@ -15,3 +15,25 @@ pub async fn send_email(
|
||||
) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub async fn send_email_html(
|
||||
_subject: &str,
|
||||
_content: &str,
|
||||
_to: Vec<String>,
|
||||
_smtp: Smtp,
|
||||
_client_timeout: Option<tokio::time::Duration>,
|
||||
) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub async fn send_email_plain_text(
|
||||
_subject: &str,
|
||||
_content: &str,
|
||||
_to: Vec<String>,
|
||||
_smtp: Smtp,
|
||||
_client_timeout: Option<tokio::time::Duration>,
|
||||
) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ use uuid::Uuid;
|
||||
pub const ENTRYPOINT_OVERRIDE: &str = "_ENTRYPOINT_OVERRIDE";
|
||||
pub const LARGE_LOG_THRESHOLD_SIZE: usize = 9000;
|
||||
|
||||
pub const EMAIL_ERROR_HANDLER_USER_EMAIL: &str = "email_error_handler@windmill.dev";
|
||||
|
||||
use crate::{
|
||||
apps::AppScriptId,
|
||||
auth::is_super_admin_email,
|
||||
|
||||
@@ -135,6 +135,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
pub static ref BASE_INTERNAL_URL: String = std::env::var("BASE_INTERNAL_URL").unwrap_or("http://localhost:8000".to_string());
|
||||
pub static ref HUB_BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new(DEFAULT_HUB_BASE_URL.to_string()));
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ use std::sync::atomic::Ordering;
|
||||
use crate::worker::CLOUD_HOSTED;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
|
||||
@@ -26,13 +26,7 @@ use uuid::Uuid;
|
||||
use windmill_macros::annotations;
|
||||
|
||||
use crate::{
|
||||
agent_workers::{PingJobStatusResponse, BASE_INTERNAL_URL},
|
||||
cache::{unwrap_or_error, RawNode, RawScript},
|
||||
error::{self, to_anyhow},
|
||||
global_settings::CUSTOM_TAGS_SETTING,
|
||||
indexer::TantivyIndexerSettings,
|
||||
server::Smtp,
|
||||
KillpillSender, DB,
|
||||
agent_workers::PingJobStatusResponse, cache::{unwrap_or_error, RawNode, RawScript}, error::{self, to_anyhow}, global_settings::CUSTOM_TAGS_SETTING, indexer::TantivyIndexerSettings, server::Smtp, KillpillSender, BASE_INTERNAL_URL, DB
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
|
||||
@@ -15,6 +15,7 @@ enterprise = ["windmill-common/enterprise"]
|
||||
cloud = []
|
||||
benchmark = ["windmill-common/benchmark"]
|
||||
prometheus = ["dep:prometheus"]
|
||||
smtp = []
|
||||
|
||||
[dependencies]
|
||||
windmill-audit.workspace = true
|
||||
|
||||
@@ -33,8 +33,11 @@ use windmill_common::add_time;
|
||||
use windmill_common::auth::JobPerms;
|
||||
#[cfg(feature = "benchmark")]
|
||||
use windmill_common::bench::BenchmarkIter;
|
||||
use windmill_common::jobs::EMAIL_ERROR_HANDLER_USER_EMAIL;
|
||||
use windmill_common::utils::now_from_db;
|
||||
use windmill_common::worker::{Connection, SCRIPT_TOKEN_EXPIRY};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::BASE_URL;
|
||||
use windmill_common::{
|
||||
auth::{fetch_authed_from_permissioned_as, permissioned_as_to_username},
|
||||
cache::{self, FlowData},
|
||||
@@ -63,16 +66,12 @@ use windmill_common::{
|
||||
use backon::ConstantBuilder;
|
||||
use backon::{BackoffBuilder, Retryable};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::BASE_URL;
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
|
||||
|
||||
use crate::flow_status::{update_flow_status_in_progress, update_workflow_as_code_status};
|
||||
use crate::jobs_oss::update_concurrency_counter;
|
||||
use crate::schedule::{get_schedule_opt, push_scheduled_job};
|
||||
use crate::tags::per_workspace_tag;
|
||||
#[cfg(feature = "cloud")]
|
||||
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
lazy_static::lazy_static! {
|
||||
@@ -121,6 +120,7 @@ const MAX_FREE_CONCURRENT_RUNS: i32 = 30;
|
||||
|
||||
const ERROR_HANDLER_USERNAME: &str = "error_handler";
|
||||
const SCHEDULE_ERROR_HANDLER_USERNAME: &str = "schedule_error_handler";
|
||||
const GLOBAL_ERROR_HANDLER_USERNAME: &str = "global";
|
||||
#[cfg(feature = "enterprise")]
|
||||
const SCHEDULE_RECOVERY_HANDLER_USERNAME: &str = "schedule_recovery_handler";
|
||||
const ERROR_HANDLER_USER_GROUP: &str = "g/error_handler";
|
||||
@@ -1360,6 +1360,7 @@ async fn apply_completed_job_error_handlers<T: Serialize + Send + Sync + Validab
|
||||
"Sending error of job {} to error handlers (if any)",
|
||||
queued_job.id
|
||||
);
|
||||
|
||||
if let Err(e) = send_error_to_global_handler(&queued_job, db, Json(&result)).await {
|
||||
tracing::error!(
|
||||
"Could not run global error handler for job {}: {}",
|
||||
@@ -1378,11 +1379,16 @@ async fn apply_completed_job_error_handlers<T: Serialize + Send + Sync + Validab
|
||||
{
|
||||
match err {
|
||||
Error::QuotaExceeded(_) => {}
|
||||
_ => {
|
||||
err => {
|
||||
tracing::error!(
|
||||
"Could not run workspace error handler for job {}: {}",
|
||||
&queued_job.id,
|
||||
err
|
||||
);
|
||||
let base_url = BASE_URL.read().await;
|
||||
let w_id: &String = &queued_job.workspace_id;
|
||||
report_critical_error(format!(
|
||||
"Could not push workspace error handler for failed job ({base_url}/run/{}?workspace={w_id}): {}",
|
||||
"Failed to push workspace error handler job to handle failed job ({base_url}/run/{}?workspace={w_id}): {}",
|
||||
queued_job.id,
|
||||
err
|
||||
), db.clone(), Some(&w_id), None)
|
||||
@@ -1485,8 +1491,16 @@ pub async fn report_error_to_workspace_handler_or_critical_side_channel(
|
||||
error_message: String,
|
||||
) -> () {
|
||||
let w_id = &queued_job.workspace_id;
|
||||
let (error_handler, error_handler_extra_args) = sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>)>(
|
||||
"SELECT error_handler, error_handler_extra_args FROM workspace_settings WHERE workspace_id = $1",
|
||||
let row_result = sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>)>(
|
||||
r#"
|
||||
SELECT
|
||||
error_handler,
|
||||
error_handler_extra_args
|
||||
FROM
|
||||
workspace_settings
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(db)
|
||||
@@ -1495,6 +1509,8 @@ pub async fn report_error_to_workspace_handler_or_critical_side_channel(
|
||||
.flatten()
|
||||
.unwrap_or((None, None));
|
||||
|
||||
let (error_handler, error_handler_extra_args) = row_result;
|
||||
|
||||
if let Some(error_handler) = error_handler {
|
||||
if let Err(err) = push_error_handler(
|
||||
db,
|
||||
@@ -1538,8 +1554,18 @@ pub async fn send_error_to_workspace_handler<'a, 'c, T: Serialize + Send + Sync>
|
||||
result: Json<&'a T>,
|
||||
) -> Result<(), Error> {
|
||||
let w_id = &queued_job.workspace_id;
|
||||
let (error_handler, error_handler_extra_args, error_handler_muted_on_cancel) = sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>, bool)>(
|
||||
"SELECT error_handler, error_handler_extra_args, error_handler_muted_on_cancel FROM workspace_settings WHERE workspace_id = $1",
|
||||
|
||||
let row_result = sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>, bool)>(
|
||||
r#"
|
||||
SELECT
|
||||
error_handler,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel
|
||||
FROM
|
||||
workspace_settings
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(db)
|
||||
@@ -1547,6 +1573,8 @@ pub async fn send_error_to_workspace_handler<'a, 'c, T: Serialize + Send + Sync>
|
||||
.context("fetching error handler info from workspace_settings")?
|
||||
.ok_or_else(|| Error::internal_err(format!("no workspace settings for id {w_id}")))?;
|
||||
|
||||
let (error_handler, error_handler_extra_args, error_handler_muted_on_cancel) = row_result;
|
||||
|
||||
if is_canceled && error_handler_muted_on_cancel {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1598,7 +1626,6 @@ pub async fn send_error_to_workspace_handler<'a, 'c, T: Serialize + Send + Sync>
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1846,6 +1873,71 @@ async fn apply_schedule_handlers<'a, 'c, T: Serialize + Send + Sync>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const ERROR_HANDLER_PATH_TEAMS: &str = "/workspace-or-schedule-error-handler-teams";
|
||||
pub const ERROR_HANDLER_PATH_SLACK: &str = "/workspace-or-schedule-error-handler-slack";
|
||||
pub const ERROR_HANDLER_PATH_EMAIL: &str = "/workspace-or-error-handler-email";
|
||||
|
||||
enum ErrorHandlerType {
|
||||
Custom,
|
||||
Teams,
|
||||
Slack,
|
||||
Email,
|
||||
}
|
||||
|
||||
impl ErrorHandlerType {
|
||||
fn from_error_handler_path(error_handler_path: &str) -> Option<ErrorHandlerType> {
|
||||
let error_handler_path = if error_handler_path.starts_with("script/") {
|
||||
error_handler_path.strip_prefix("script/").unwrap()
|
||||
} else if error_handler_path.starts_with("flow/") {
|
||||
error_handler_path.strip_prefix("flow/").unwrap()
|
||||
} else {
|
||||
error_handler_path
|
||||
};
|
||||
|
||||
if let Some(from_hub) = error_handler_path.strip_prefix("hub/") {
|
||||
let handler_type = if from_hub.ends_with(ERROR_HANDLER_PATH_TEAMS) {
|
||||
ErrorHandlerType::Teams
|
||||
} else if from_hub.ends_with(ERROR_HANDLER_PATH_SLACK) {
|
||||
ErrorHandlerType::Slack
|
||||
} else if from_hub.ends_with(ERROR_HANDLER_PATH_EMAIL) {
|
||||
ErrorHandlerType::Email
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
return Some(handler_type);
|
||||
}
|
||||
|
||||
Some(ErrorHandlerType::Custom)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_email_and_permissioned_as(
|
||||
error_handler_path: &str,
|
||||
is_global_error_handler: bool,
|
||||
is_schedule_error_handler: bool,
|
||||
) -> (&'static str, String) {
|
||||
let res = if is_global_error_handler {
|
||||
(SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SECRET_EMAIL.to_string())
|
||||
} else if is_schedule_error_handler {
|
||||
(
|
||||
SCHEDULE_ERROR_HANDLER_USER_EMAIL,
|
||||
ERROR_HANDLER_USER_GROUP.to_string(),
|
||||
)
|
||||
} else {
|
||||
let handler_type = ErrorHandlerType::from_error_handler_path(error_handler_path);
|
||||
|
||||
let email = match handler_type {
|
||||
Some(ErrorHandlerType::Email) => EMAIL_ERROR_HANDLER_USER_EMAIL,
|
||||
_ => ERROR_HANDLER_USER_EMAIL,
|
||||
};
|
||||
|
||||
(email, ERROR_HANDLER_USER_GROUP.to_string())
|
||||
};
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
|
||||
db: &Pool<Postgres>,
|
||||
job_id: Uuid,
|
||||
@@ -1881,6 +1973,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
|
||||
extra.insert("is_flow".to_string(), to_raw_value(&is_flow));
|
||||
extra.insert("started_at".to_string(), to_raw_value(&started_at));
|
||||
extra.insert("email".to_string(), to_raw_value(&email));
|
||||
|
||||
if let Some(failed_times) = failed_times {
|
||||
extra.insert("failed_times".to_string(), to_raw_value(&failed_times));
|
||||
}
|
||||
@@ -1902,17 +1995,11 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
|
||||
on_behalf_of.email.as_str(),
|
||||
on_behalf_of.permissioned_as.clone(),
|
||||
)
|
||||
} else if is_global_error_handler {
|
||||
(SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SECRET_EMAIL.to_string())
|
||||
} else if is_schedule_error_handler {
|
||||
(
|
||||
SCHEDULE_ERROR_HANDLER_USER_EMAIL,
|
||||
ERROR_HANDLER_USER_GROUP.to_string(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
ERROR_HANDLER_USER_EMAIL,
|
||||
ERROR_HANDLER_USER_GROUP.to_string(),
|
||||
get_email_and_permissioned_as(
|
||||
on_failure_path,
|
||||
is_global_error_handler,
|
||||
is_schedule_error_handler,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1924,7 +2011,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
|
||||
payload,
|
||||
PushArgs { extra: Some(extra), args: &result },
|
||||
if is_global_error_handler {
|
||||
"global"
|
||||
GLOBAL_ERROR_HANDLER_USERNAME
|
||||
} else if is_schedule_error_handler {
|
||||
SCHEDULE_ERROR_HANDLER_USERNAME
|
||||
} else {
|
||||
@@ -2172,6 +2259,25 @@ pub enum JobTriggerKind {
|
||||
Gcp,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobTriggerKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let kind = match self {
|
||||
JobTriggerKind::Webhook => "webhook",
|
||||
JobTriggerKind::Http => "http",
|
||||
JobTriggerKind::Websocket => "websocket",
|
||||
JobTriggerKind::Kafka => "kafka",
|
||||
JobTriggerKind::Email => "email",
|
||||
JobTriggerKind::Nats => "nats",
|
||||
JobTriggerKind::Mqtt => "mqtt",
|
||||
JobTriggerKind::Sqs => "sqs",
|
||||
JobTriggerKind::Postgres => "postgres",
|
||||
JobTriggerKind::Schedule => "schedule",
|
||||
JobTriggerKind::Gcp => "gcp",
|
||||
};
|
||||
write!(f, "{}", kind)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MiniPulledJob {
|
||||
pub workspace_id: String,
|
||||
@@ -4532,6 +4638,12 @@ pub async fn push<'c, 'd>(
|
||||
// tracing::error!("Could not insert job_perms for job {job_id}: {err:#}");
|
||||
// }
|
||||
|
||||
let trigger_kind = if schedule_path.is_some() {
|
||||
Some(JobTriggerKind::Schedule)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"WITH inserted_job AS (
|
||||
INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,
|
||||
@@ -4540,8 +4652,7 @@ pub async fn push<'c, 'd>(
|
||||
flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,
|
||||
cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,
|
||||
$19, $20, $38, $21, $22, $23, $24, $25, $26,
|
||||
CASE WHEN $14::VARCHAR IS NOT NULL THEN 'schedule'::job_trigger_kind END,
|
||||
$19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,
|
||||
($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)
|
||||
),
|
||||
inserted_runtime AS (
|
||||
@@ -4596,7 +4707,8 @@ pub async fn push<'c, 'd>(
|
||||
job_authed.is_operator,
|
||||
folders.as_slice(),
|
||||
job_authed.groups.as_slice(),
|
||||
root_job.or(parent_job)
|
||||
root_job.or(parent_job),
|
||||
trigger_kind as Option<JobTriggerKind>,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(1)
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import MsTeamsIcon from '$lib/components/icons/MSTeamsIcon.svelte'
|
||||
import { emptySchema, emptyString, sendUserToast, tryEvery } from '$lib/utils'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import MultiSelect from '$lib/components/select/MultiSelect.svelte'
|
||||
import {
|
||||
FlowService,
|
||||
JobService,
|
||||
@@ -18,17 +20,17 @@
|
||||
WorkspaceService,
|
||||
type Flow
|
||||
} from '$lib/gen'
|
||||
import type { ListAvailableTeamsChannelsResponse } from '$lib/gen/types.gen'
|
||||
import type { ErrorHandler, ListAvailableTeamsChannelsResponse } from '$lib/gen/types.gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { hubBaseUrlStore } from '$lib/stores'
|
||||
|
||||
import { CheckCircle2, Loader2, RotateCw, XCircle, RefreshCcw } from 'lucide-svelte'
|
||||
import { hubPaths } from '$lib/hub'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
|
||||
const slackRecoveryHandler = hubPaths.slackRecoveryHandler
|
||||
const slackHandlerScriptPath = hubPaths.slackErrorHandler
|
||||
const slackSuccessHandler = hubPaths.slackSuccessHandler
|
||||
|
||||
const teamsRecoveryHandler = hubPaths.teamsRecoveryHandler
|
||||
const teamsHandlerScriptPath = hubPaths.teamsErrorHandler
|
||||
const teamsSuccessHandler = hubPaths.teamsSuccessHandler
|
||||
@@ -38,7 +40,7 @@
|
||||
isEditable: boolean
|
||||
toggleText?: string
|
||||
showScriptHelpText?: boolean
|
||||
handlerSelected: 'custom' | 'slack' | 'teams'
|
||||
handlerSelected: ErrorHandler
|
||||
handlerPath: string | undefined
|
||||
handlerExtraArgs: Record<string, any>
|
||||
customScriptTemplate: string
|
||||
@@ -51,18 +53,17 @@
|
||||
isEditable,
|
||||
toggleText = 'Enable',
|
||||
showScriptHelpText = false,
|
||||
handlerSelected = $bindable(),
|
||||
handlerSelected = $bindable('custom'),
|
||||
handlerPath = $bindable(),
|
||||
handlerExtraArgs = $bindable(),
|
||||
customScriptTemplate,
|
||||
customHandlerKind = $bindable('script'),
|
||||
customTabTooltip
|
||||
customTabTooltip,
|
||||
}: Props = $props()
|
||||
|
||||
let customHandlerSchema: Schema | undefined = $state()
|
||||
let slackHandlerSchema: Schema | undefined = $state()
|
||||
let isFetching: boolean = $state(false)
|
||||
|
||||
let teams_channels: ListAvailableTeamsChannelsResponse = $state([])
|
||||
let teams_team_name: string | undefined = $state(undefined)
|
||||
|
||||
@@ -71,6 +72,8 @@
|
||||
|
||||
let connectionTestJob: { uuid: string; is_success: boolean; in_progress: boolean } | undefined =
|
||||
$state()
|
||||
const EMAIL_RECIPIENTS_KEY = 'email_recipients'
|
||||
const CHANNEL_KEY = 'channel'
|
||||
|
||||
async function loadSlackResources() {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
@@ -222,6 +225,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isEmailHandler(scriptPath: string | undefined) {
|
||||
if (!scriptPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
return scriptPath.startsWith('hub/') && scriptPath.endsWith('/workspace-or-error-handler-email')
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($workspaceStore) {
|
||||
loadSlackResources()
|
||||
@@ -237,22 +248,27 @@
|
||||
}
|
||||
})
|
||||
|
||||
let lastHandlerSelected: 'slack' | 'teams' | 'custom' | undefined = $state(undefined)
|
||||
let channelCache = $state({
|
||||
let lastHandlerSelected: ErrorHandler | undefined = $state(undefined)
|
||||
let handlerCache = $state({
|
||||
slack: undefined as string | undefined,
|
||||
teams: undefined as string | undefined
|
||||
teams: undefined as string | undefined,
|
||||
email: undefined as string[] | undefined
|
||||
})
|
||||
$effect(() => {
|
||||
if (lastHandlerSelected !== handlerSelected && lastHandlerSelected !== undefined) {
|
||||
if (lastHandlerSelected === 'teams' || lastHandlerSelected === 'slack') {
|
||||
channelCache[lastHandlerSelected] = handlerExtraArgs['channel']
|
||||
if (lastHandlerSelected != 'custom') {
|
||||
const key = lastHandlerSelected === 'email' ? EMAIL_RECIPIENTS_KEY : CHANNEL_KEY
|
||||
handlerCache[lastHandlerSelected] = handlerExtraArgs[key]
|
||||
}
|
||||
|
||||
if (handlerSelected === 'custom') {
|
||||
handlerExtraArgs['channel'] = ''
|
||||
handlerExtraArgs[CHANNEL_KEY] = ''
|
||||
handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = []
|
||||
handlerPath = undefined
|
||||
} else if (handlerSelected === 'email') {
|
||||
handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = handlerCache[handlerSelected] ?? []
|
||||
} else {
|
||||
handlerExtraArgs['channel'] = channelCache[handlerSelected] ?? ''
|
||||
handlerExtraArgs[CHANNEL_KEY] = handlerCache[handlerSelected] ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +279,7 @@
|
||||
handlerPath &&
|
||||
!isSlackHandler(handlerPath) &&
|
||||
!isTeamsHandler(handlerPath) &&
|
||||
!isEmailHandler(handlerPath) &&
|
||||
loadHandlerScriptArgs(handlerPath, [
|
||||
'path',
|
||||
'workspace_id',
|
||||
@@ -302,12 +319,19 @@
|
||||
'slack'
|
||||
]).then((schema) => (slackHandlerSchema = schema))
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (handlerSelected === 'email') {
|
||||
handlerPath = hubPaths.emailErrorHandler
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Tabs bind:selected={handlerSelected} class="mt-2 mb-4">
|
||||
<Tab value="slack" disabled={!isEditable}>Slack</Tab>
|
||||
<Tab value="teams" disabled={!isEditable}>Teams</Tab>
|
||||
<Tab value="email" disabled={!isEditable}>Email</Tab>
|
||||
<Tab value="custom" disabled={!isEditable}>
|
||||
Custom
|
||||
{@render customTabTooltip?.()}
|
||||
@@ -334,8 +358,10 @@
|
||||
size="xs"
|
||||
href={customScriptTemplate}
|
||||
disabled={!isEditable}
|
||||
target="_blank">Create from template</Button
|
||||
target="_blank"
|
||||
>
|
||||
Create from template
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showScriptHelpText}
|
||||
@@ -563,4 +589,46 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if handlerSelected === 'email'}
|
||||
{#if isCloudHosted()}
|
||||
<Alert type="info" title="Email notifications are not available in Cloud">
|
||||
Email notifications for trigger failures are only available in self-hosted Windmill instances.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4 my-4">
|
||||
<Description>
|
||||
Configure email addresses to receive notifications when jobs fail. This feature requires
|
||||
SMTP to be configured.
|
||||
</Description>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 my-4">
|
||||
<MultiSelect
|
||||
items={[] as { label: string; value: string }[]}
|
||||
bind:value={
|
||||
() => handlerExtraArgs[EMAIL_RECIPIENTS_KEY] ?? [],
|
||||
(recipients) => (handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = recipients)
|
||||
}
|
||||
placeholder="Enter email addresses..."
|
||||
onCreateItem={(email) => {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
if (!emailRegex.test(email)) {
|
||||
sendUserToast('Invalid email format', true)
|
||||
return
|
||||
}
|
||||
const currentArray = handlerExtraArgs[EMAIL_RECIPIENTS_KEY] ?? []
|
||||
handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = [...currentArray, email]
|
||||
}}
|
||||
class="w-full"
|
||||
/>
|
||||
{#if handlerExtraArgs[EMAIL_RECIPIENTS_KEY]?.length > 0}
|
||||
<span class="text-sm text-tertiary">
|
||||
{handlerExtraArgs[EMAIL_RECIPIENTS_KEY]?.length} email{handlerExtraArgs[
|
||||
EMAIL_RECIPIENTS_KEY
|
||||
]?.length === 1
|
||||
? ''
|
||||
: 's'} configured
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Retry } from '$lib/gen'
|
||||
import type { ErrorHandler, Retry } from '$lib/gen'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
@@ -19,7 +19,7 @@
|
||||
optionTabSelected: 'error_handler' | 'retries' | string
|
||||
itemKind: 'script' | 'flow'
|
||||
can_write: boolean
|
||||
errorHandlerSelected: 'custom' | 'slack' | 'teams'
|
||||
errorHandlerSelected: ErrorHandler
|
||||
error_handler_path: string | undefined
|
||||
error_handler_args: Record<string, any>
|
||||
retry: Retry | undefined
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
type DeliveryType,
|
||||
type PushConfig,
|
||||
type SubscriptionMode,
|
||||
type Retry
|
||||
type Retry,
|
||||
type ErrorHandler
|
||||
} from '$lib/gen'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
@@ -52,11 +53,10 @@
|
||||
let deploymentLoading = $state(false)
|
||||
let base_endpoint = $derived(`${window.location.origin}${base}`)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
let {
|
||||
useDrawer = true,
|
||||
description = undefined,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
HttpTriggerService,
|
||||
VariableService,
|
||||
type AuthenticationMethod,
|
||||
type ErrorHandler,
|
||||
type HttpTrigger,
|
||||
type NewHttpTrigger,
|
||||
type Retry
|
||||
@@ -108,8 +109,7 @@
|
||||
let initialConfig: NewHttpTrigger | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
let optionTabSelected: 'request_options' | 'error_handler' | 'retries' = $state('request_options')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
const routeConfig = $derived.by(getRouteConfig)
|
||||
const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
|
||||
@@ -284,7 +284,6 @@
|
||||
}
|
||||
extraPerms = cfg?.extra_perms ?? undefined
|
||||
can_write = canWrite(path, cfg?.extra_perms ?? {}, $userStore)
|
||||
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
retry = cfg?.retry
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function saveHttpRouteFromCfg(
|
||||
summary: routeCfg.summary,
|
||||
error_handler_path: routeCfg.error_handler_path,
|
||||
error_handler_args: routeCfg.error_handler_path ? routeCfg.error_handler_args : undefined,
|
||||
retry: routeCfg.retry
|
||||
retry: routeCfg.retry,
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { KafkaTriggerService, type Retry } from '$lib/gen'
|
||||
import { KafkaTriggerService, type ErrorHandler, type Retry } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -78,7 +78,7 @@
|
||||
let kafkaCfg: Record<string, any> = $state({})
|
||||
let deploymentLoading = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function saveKafkaTriggerFromCfg(
|
||||
? {
|
||||
error_handler_path: cfg.error_handler_path,
|
||||
error_handler_args: cfg.error_handler_path ? cfg.error_handler_args : undefined,
|
||||
retry: cfg.retry
|
||||
retry: cfg.retry,
|
||||
}
|
||||
: {}
|
||||
const requestBody: EditKafkaTrigger = {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
type MqttV3Config,
|
||||
type MqttV5Config,
|
||||
type MqttSubscribeTopic,
|
||||
type Retry
|
||||
type Retry,
|
||||
type ErrorHandler
|
||||
} from '$lib/gen'
|
||||
import MqttEditorConfigSection from './MqttEditorConfigSection.svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
@@ -91,7 +92,7 @@
|
||||
let isValid: boolean = $state(false)
|
||||
let initialConfig: Record<string, any> | undefined = {}
|
||||
let deploymentLoading = $state(false)
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function saveMqttTriggerFromCfg(
|
||||
? {
|
||||
error_handler_path: cfg.error_handler_path,
|
||||
error_handler_args: cfg.error_handler_path ? cfg.error_handler_args : undefined,
|
||||
retry: cfg.retry
|
||||
retry: cfg.retry,
|
||||
}
|
||||
: {}
|
||||
const requestBody: EditMqttTrigger = {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { NatsTriggerService, type Retry } from '$lib/gen'
|
||||
import { NatsTriggerService, type ErrorHandler, type Retry } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -82,7 +82,7 @@
|
||||
let deploymentLoading = $state(false)
|
||||
let isValid = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function saveNatsTriggerFromCfg(
|
||||
? {
|
||||
error_handler_path: cfg.error_handler_path,
|
||||
error_handler_args: cfg.error_handler_path ? cfg.error_handler_args : undefined,
|
||||
retry: cfg.retry
|
||||
retry: cfg.retry,
|
||||
}
|
||||
: {}
|
||||
const requestBody: EditNatsTrigger = {
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { PostgresTriggerService, type Language, type Relations, type Retry } from '$lib/gen'
|
||||
import {
|
||||
PostgresTriggerService,
|
||||
type ErrorHandler,
|
||||
type Language,
|
||||
type Relations,
|
||||
type Retry
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, emptyStringTrimmed, sendUserToast } from '$lib/utils'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -106,7 +112,7 @@
|
||||
let creatingPublication: boolean = $state(false)
|
||||
let pg14: boolean = $derived(postgresVersion.startsWith('14'))
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function savePostgresTriggerFromCfg(
|
||||
? {
|
||||
error_handler_path: config.error_handler_path,
|
||||
error_handler_args: config.error_handler_path ? config.error_handler_args : undefined,
|
||||
retry: config.retry
|
||||
retry: config.retry,
|
||||
}
|
||||
: {}
|
||||
const requestBody: EditPostgresTrigger = {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
type Flow,
|
||||
SettingService,
|
||||
type Retry,
|
||||
type Schedule
|
||||
type Schedule,
|
||||
type ErrorHandler
|
||||
} from '$lib/gen'
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, formatCron, sendUserToast, cronV1toV2 } from '$lib/utils'
|
||||
@@ -66,14 +67,14 @@
|
||||
let errorHandleritemKind: 'flow' | 'script' = $state('script')
|
||||
let wsErrorHandlerMuted: boolean = $state(false)
|
||||
let errorHandlerPath: string | undefined = $state(undefined)
|
||||
let errorHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let errorHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let recoveryHandlerPath: string | undefined = $state(undefined)
|
||||
let recoveryHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack')
|
||||
let recoveryHandlerSelected: ErrorHandler = $state('slack')
|
||||
let recoveryHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let recoveryHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let successHandlerPath: string | undefined = $state(undefined)
|
||||
let successHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack')
|
||||
let successHandlerSelected: ErrorHandler = $state('slack')
|
||||
let successHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let successHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let failedTimes = $state(1)
|
||||
@@ -515,7 +516,7 @@
|
||||
function getHandlerType(
|
||||
isHandler: 'error' | 'recovery' | 'success',
|
||||
scriptPath: string
|
||||
): 'custom' | 'slack' | 'teams' {
|
||||
): ErrorHandler {
|
||||
const handlerMap = {
|
||||
error: {
|
||||
teams: '/workspace-or-schedule-error-handler-teams',
|
||||
@@ -533,7 +534,7 @@
|
||||
|
||||
for (const [type, suffix] of Object.entries(handlerMap[isHandler])) {
|
||||
if (scriptPath.startsWith('hub/') && scriptPath.endsWith(suffix)) {
|
||||
return type as 'custom' | 'slack' | 'teams'
|
||||
return type as ErrorHandler
|
||||
}
|
||||
}
|
||||
return 'custom'
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { SqsTriggerService, type AwsAuthResourceType, type Retry } from '$lib/gen'
|
||||
import {
|
||||
SqsTriggerService,
|
||||
type AwsAuthResourceType,
|
||||
type ErrorHandler,
|
||||
type Retry
|
||||
} from '$lib/gen'
|
||||
import SqsTriggerEditorConfigSection from './SqsTriggerEditorConfigSection.svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
@@ -79,7 +84,7 @@
|
||||
let initialConfig: Record<string, any> | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function saveSqsTriggerFromCfg(
|
||||
? {
|
||||
error_handler_path: cfg.error_handler_path,
|
||||
error_handler_args: cfg.error_handler_path ? cfg.error_handler_args : undefined,
|
||||
retry: cfg.retry
|
||||
retry: cfg.retry,
|
||||
}
|
||||
: {}
|
||||
const requestBody: EditSqsTrigger = {
|
||||
|
||||
@@ -4,7 +4,13 @@ import NatsIcon from '$lib/components/icons/NatsIcon.svelte'
|
||||
import MqttIcon from '$lib/components/icons/MqttIcon.svelte'
|
||||
import AwsIcon from '$lib/components/icons/AwsIcon.svelte'
|
||||
import GoogleCloudIcon from '$lib/components/icons/GoogleCloudIcon.svelte'
|
||||
import type { CaptureTriggerKind, Flow, NewScript, TriggersCount } from '$lib/gen/types.gen'
|
||||
import type {
|
||||
CaptureTriggerKind,
|
||||
ErrorHandler,
|
||||
Flow,
|
||||
NewScript,
|
||||
TriggersCount
|
||||
} from '$lib/gen/types.gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import SchedulePollIcon from '../icons/SchedulePollIcon.svelte'
|
||||
import { type TriggerKind } from '$lib/components/triggers'
|
||||
@@ -543,14 +549,15 @@ export function filterDraftTriggers(
|
||||
return newSavedValue
|
||||
}
|
||||
|
||||
export function getHandlerType(scriptPath: string): 'custom' | 'slack' | 'teams' {
|
||||
export function getHandlerType(scriptPath: string): ErrorHandler {
|
||||
const handlerMap = {
|
||||
teams: '/workspace-or-schedule-error-handler-teams',
|
||||
slack: '/workspace-or-schedule-error-handler-slack'
|
||||
slack: '/workspace-or-schedule-error-handler-slack',
|
||||
email: '/workspace-or-error-handler-email'
|
||||
}
|
||||
for (const [type, suffix] of Object.entries(handlerMap)) {
|
||||
if (scriptPath.startsWith('hub/') && scriptPath.endsWith(suffix)) {
|
||||
return type as 'slack' | 'teams'
|
||||
return type as ErrorHandler
|
||||
}
|
||||
}
|
||||
return 'custom'
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
type Script,
|
||||
type ScriptArgs,
|
||||
type WebsocketTriggerInitialMessage,
|
||||
type Retry
|
||||
type Retry,
|
||||
type ErrorHandler
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptySchema, emptyString, sendUserToast } from '$lib/utils'
|
||||
@@ -99,7 +100,7 @@
|
||||
let deploymentLoading = $state(false)
|
||||
let isValid = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: 'slack' | 'teams' | 'custom' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function saveWebsocketTriggerFromCfg(
|
||||
error_handler_args: triggerCfg.error_handler_path
|
||||
? triggerCfg.error_handler_args
|
||||
: undefined,
|
||||
retry: triggerCfg.retry
|
||||
retry: triggerCfg.retry,
|
||||
}
|
||||
: {}
|
||||
const requestBody: EditWebsocketTrigger = {
|
||||
|
||||
@@ -93,6 +93,7 @@ type HubPaths = {
|
||||
teamsErrorHandler: string
|
||||
teamsRecoveryHandler: string
|
||||
teamsSuccessHandler: string
|
||||
emailErrorHandler: string
|
||||
}
|
||||
|
||||
export const hubPaths = JSON.parse(rawHubPaths) as HubPaths
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"slackErrorHandler": "hub/19741/workspace-or-schedule-error-handler-slack",
|
||||
"slackErrorHandler_0": "hub/9079/workspace-or-schedule-error-handler-slack",
|
||||
"slackErrorHandler_1": "hub/9206/workspace-or-schedule-error-handler-slack",
|
||||
"emailErrorHandler": "hub/19795/workspace-or-error-handler-email",
|
||||
"slackRecoveryHandler": "hub/9080/slack/schedule-recovery-handler-slack",
|
||||
"slackSuccessHandler": "hub/9072/slack/schedule-success-handler-slack",
|
||||
"teamsErrorHandler": "hub/19742/workspace-or-schedule-error-handler-teams",
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
WorkspaceService,
|
||||
ResourceService,
|
||||
SettingService,
|
||||
type AIConfig
|
||||
type AIConfig,
|
||||
|
||||
type ErrorHandler
|
||||
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
@@ -49,6 +52,7 @@
|
||||
import StorageSettings from '$lib/components/workspaceSettings/StorageSettings.svelte'
|
||||
import GitSyncSection from '$lib/components/git_sync/GitSyncSection.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { getHandlerType } from '$lib/components/triggers/utils'
|
||||
import DucklakeSettings, {
|
||||
convertDucklakeSettingsFromBackend,
|
||||
type DucklakeSettingsType
|
||||
@@ -66,7 +70,7 @@
|
||||
let customer_id: string | undefined = $state(undefined)
|
||||
let webhook: string | undefined = $state(undefined)
|
||||
let workspaceToDeployTo: string | undefined = $state(undefined)
|
||||
let errorHandlerSelected: 'custom' | 'slack' | 'teams' = $state('slack')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let errorHandlerScriptPath: string | undefined = $state(undefined)
|
||||
let errorHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let errorHandlerExtraArgs: Record<string, any> = $state({})
|
||||
@@ -252,14 +256,7 @@
|
||||
if (emptyString($enterpriseLicense)) {
|
||||
errorHandlerSelected = 'custom'
|
||||
} else {
|
||||
errorHandlerSelected = emptyString(errorHandlerScriptPath)
|
||||
? 'custom'
|
||||
: errorHandlerScriptPath.startsWith('hub/') &&
|
||||
errorHandlerScriptPath.endsWith('/workspace-or-schedule-error-handler-slack')
|
||||
? 'slack'
|
||||
: errorHandlerScriptPath.endsWith('/workspace-or-schedule-error-handler-teams')
|
||||
? 'teams'
|
||||
: 'custom'
|
||||
errorHandlerSelected = getHandlerType(errorHandlerScriptPath)
|
||||
}
|
||||
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
|
||||
workspaceDefaultAppPath = settings.default_app
|
||||
@@ -321,7 +318,7 @@
|
||||
requestBody: {
|
||||
error_handler: `${errorHandlerItemKind}/${errorHandlerScriptPath}`,
|
||||
error_handler_extra_args: errorHandlerExtraArgs,
|
||||
error_handler_muted_on_cancel: errorHandlerMutedOnCancel
|
||||
error_handler_muted_on_cancel: errorHandlerMutedOnCancel,
|
||||
}
|
||||
})
|
||||
sendUserToast(`workspace error handler set to ${errorHandlerScriptPath}`)
|
||||
@@ -331,7 +328,7 @@
|
||||
requestBody: {
|
||||
error_handler: undefined,
|
||||
error_handler_extra_args: undefined,
|
||||
error_handler_muted_on_cancel: undefined
|
||||
error_handler_muted_on_cancel: undefined,
|
||||
}
|
||||
})
|
||||
sendUserToast(`workspace error handler removed`)
|
||||
|
||||
Reference in New Issue
Block a user