feat: disabling/enabling email triggers (#7171)

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
hugocasa
2025-11-19 00:04:12 +01:00
committed by GitHub
parent f2dbf6d20d
commit 8ae266b6a9
23 changed files with 133 additions and 77 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE local_part = $1 \n AND workspaced_local_part = FALSE\n ",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE local_part = $1 \n AND workspaced_local_part = FALSE\n AND enabled IS TRUE\n ",
"describe": {
"columns": [
{
@@ -66,5 +66,5 @@
true
]
},
"hash": "668edc2f84eccf5db6b8daa3ed97e25e46a37c50a9d1a6eca5a277fb58a2e7b4"
"hash": "628d303956d55d22e5ac64496b66010d4476d5c222184f09e5a189975a37b941"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11\n )\n ",
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry,\n enabled\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11, $12\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -15,10 +15,11 @@
"Varchar",
"Varchar",
"Jsonb",
"Jsonb"
"Jsonb",
"Bool"
]
},
"nullable": []
},
"hash": "d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94"
"hash": "6c568509908c1833d9e6f58f739b8a186fc91f94d4fcc40eeb9914084e4ef60c"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8\n WHERE \n workspace_id = $9 AND path = $10\n ",
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8,\n enabled = $9\n WHERE \n workspace_id = $10 AND path = $11\n ",
"describe": {
"columns": [],
"parameters": {
@@ -13,11 +13,12 @@
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f"
"hash": "a704283ff62ac1cd6db489ca3f84d6994d75c6b9e5f61daf54d298a2fdcfd9af"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n http_method = $7,\n static_asset_config = $8,\n edited_by = $9,\n email = $10,\n request_type = $11,\n authentication_method = $12,\n summary = $13,\n description = $14,\n edited_at = now(),\n is_static_website = $15,\n error_handler_path = $16,\n error_handler_args = $17,\n retry = $18\n WHERE\n workspace_id = $19 AND\n path = $20\n ",
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n enabled = $7,\n http_method = $8,\n static_asset_config = $9,\n edited_by = $10,\n email = $11,\n request_type = $12,\n authentication_method = $13,\n summary = $14,\n description = $15,\n edited_at = now(),\n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE\n workspace_id = $20 AND\n path = $21\n ",
"describe": {
"columns": [],
"parameters": {
@@ -11,6 +11,7 @@
"Varchar",
"Varchar",
"Bool",
"Bool",
{
"Custom": {
"name": "http_method",
@@ -67,5 +68,5 @@
},
"nullable": []
},
"hash": "4c64bd0e364f536597db83161b5a27ff58b5ec7148bc94807423593bcfa27b90"
"hash": "ada1a14b4f25e41742df153b07e8bdf5b5feac9c6fa9de38bd24104bac8539d2"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE workspace_id = $1 \n AND local_part = $2 \n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n ",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE workspace_id = $1 \n AND local_part = $2 \n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n AND enabled IS TRUE\n ",
"describe": {
"columns": [
{
@@ -68,5 +68,5 @@
true
]
},
"hash": "bbd51f4f0a8bb2db5d6b634f2d32f5f7f7f57390a0e4b6e7cad45c9147fd6356"
"hash": "df6972fb16a2364f10379fe37e125ae228697602e3769c479582470dc0b9488b"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10\n WHERE \n workspace_id = $11 AND path = $12\n ",
"query": "\n UPDATE email_trigger \n SET \n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10,\n enabled = $11\n WHERE \n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -15,11 +15,12 @@
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00"
"hash": "fe12006498b9e7aece6b104fc5d9a80eeac12c6ca07a0e8950941ba796b0ba79"
}
+1 -1
View File
@@ -1 +1 @@
6694dfbc62ff69570743f028aa19c543ae846e6e
6009dffc0e9549463e34d748ad54c401cbe46ffd
+29 -1
View File
@@ -11998,6 +11998,33 @@ paths:
application/json:
schema:
type: boolean
/w/{workspace}/email_triggers/setenabled/{path}:
post:
summary: enable/disable email trigger
operationId: setEmailTriggerEnabled
tags:
- email_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: email trigger enable/disable
content:
text/plain:
schema:
type: string
/groups/list:
get:
@@ -18612,7 +18639,8 @@ components:
$ref: "#/components/schemas/ScriptArgs"
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
enabled:
type: boolean
required:
- path
- script_path
@@ -28,7 +28,6 @@ impl TriggerCrud for EmailTrigger {
const TABLE_NAME: &'static str = "";
const TRIGGER_TYPE: &'static str = "";
const SUPPORTS_ENABLED: bool = true;
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/email_triggers";
@@ -25,7 +25,6 @@ impl TriggerCrud for GcpTrigger {
const TABLE_NAME: &'static str = "";
const TRIGGER_TYPE: &'static str = "";
const SUPPORTS_ENABLED: bool = false;
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/gcp_triggers";
+4 -14
View File
@@ -50,7 +50,6 @@ pub trait TriggerCrud: Send + Sync + 'static {
const TABLE_NAME: &'static str;
const TRIGGER_TYPE: &'static str;
const SUPPORTS_ENABLED: bool;
const SUPPORTS_SERVER_STATE: bool;
const SUPPORTS_TEST_CONNECTION: bool;
const ROUTE_PREFIX: &'static str;
@@ -144,12 +143,9 @@ pub trait TriggerCrud: Send + Sync + 'static {
"email",
"edited_at",
"extra_perms",
"enabled",
];
if Self::SUPPORTS_ENABLED {
fields.push("enabled");
}
if Self::SUPPORTS_SERVER_STATE {
fields.extend_from_slice(&["server_id", "last_server_ping", "error"]);
}
@@ -325,12 +321,9 @@ pub trait TriggerCrud: Send + Sync + 'static {
"email",
"edited_at",
"extra_perms",
"enabled",
];
if Self::SUPPORTS_ENABLED {
fields.push("enabled");
}
if Self::SUPPORTS_SERVER_STATE {
fields.extend_from_slice(&["server_id", "last_server_ping", "error"]);
}
@@ -380,11 +373,8 @@ pub fn trigger_routes<T: TriggerCrud + 'static>() -> Router {
.route("/get/*path", get(get_trigger::<T>))
.route("/update/*path", post(update_trigger::<T>))
.route("/delete/*path", delete(delete_trigger::<T>))
.route("/exists/*path", get(exists_trigger::<T>));
if T::SUPPORTS_ENABLED {
router = router.route("/setenabled/*path", post(set_enabled_trigger::<T>));
}
.route("/exists/*path", get(exists_trigger::<T>))
.route("/setenabled/*path", post(set_enabled_trigger::<T>));
if T::SUPPORTS_TEST_CONNECTION {
router = router.route("/test", post(test_connection::<T>));
@@ -358,7 +358,6 @@ impl TriggerCrud for HttpTrigger {
const TABLE_NAME: &'static str = "http_trigger";
const TRIGGER_TYPE: &'static str = "http";
const SUPPORTS_ENABLED: bool = true;
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/http_triggers";
@@ -543,22 +542,23 @@ impl TriggerCrud for HttpTrigger {
script_path = $4,
path = $5,
is_flow = $6,
http_method = $7,
static_asset_config = $8,
edited_by = $9,
email = $10,
request_type = $11,
authentication_method = $12,
summary = $13,
description = $14,
enabled = $7,
http_method = $8,
static_asset_config = $9,
edited_by = $10,
email = $11,
request_type = $12,
authentication_method = $13,
summary = $14,
description = $15,
edited_at = now(),
is_static_website = $15,
error_handler_path = $16,
error_handler_args = $17,
retry = $18
is_static_website = $16,
error_handler_path = $17,
error_handler_args = $18,
retry = $19
WHERE
workspace_id = $19 AND
path = $20
workspace_id = $20 AND
path = $21
"#,
trigger.config.wrap_body,
trigger.config.raw_string,
@@ -566,6 +566,7 @@ impl TriggerCrud for HttpTrigger {
trigger.base.script_path,
trigger.base.path,
trigger.base.is_flow,
trigger.base.enabled.unwrap_or(true),
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&authed.username,
@@ -28,7 +28,6 @@ impl TriggerCrud for KafkaTrigger {
const TABLE_NAME: &'static str = "";
const TRIGGER_TYPE: &'static str = "";
const SUPPORTS_ENABLED: bool = false;
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/kafka_triggers";
@@ -26,7 +26,6 @@ impl TriggerCrud for MqttTrigger {
const TABLE_NAME: &'static str = "mqtt_trigger";
const TRIGGER_TYPE: &'static str = "mqtt";
const SUPPORTS_ENABLED: bool = true;
const SUPPORTS_SERVER_STATE: bool = true;
const SUPPORTS_TEST_CONNECTION: bool = true;
const ROUTE_PREFIX: &'static str = "/mqtt_triggers";
@@ -25,7 +25,6 @@ impl TriggerCrud for NatsTrigger {
const TABLE_NAME: &'static str = "";
const TRIGGER_TYPE: &'static str = "";
const SUPPORTS_ENABLED: bool = false;
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/nats_triggers";
@@ -47,7 +47,6 @@ impl TriggerCrud for PostgresTrigger {
const TABLE_NAME: &'static str = "postgres_trigger";
const TRIGGER_TYPE: &'static str = "postgres";
const SUPPORTS_ENABLED: bool = true;
const SUPPORTS_SERVER_STATE: bool = true;
const SUPPORTS_TEST_CONNECTION: bool = true;
const ROUTE_PREFIX: &'static str = "/postgres_triggers";
@@ -64,7 +63,6 @@ impl TriggerCrud for PostgresTrigger {
DeployedObject::PostgresTrigger { path }
}
async fn create_trigger(
&self,
db: &DB,
@@ -26,7 +26,6 @@ impl TriggerCrud for SqsTrigger {
const TABLE_NAME: &'static str = "";
const TRIGGER_TYPE: &'static str = "";
const SUPPORTS_ENABLED: bool = false;
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/sqs_triggers";
@@ -30,7 +30,6 @@ impl TriggerCrud for WebsocketTrigger {
const TABLE_NAME: &'static str = "websocket_trigger";
const TRIGGER_TYPE: &'static str = "websocket";
const SUPPORTS_ENABLED: bool = true;
const SUPPORTS_SERVER_STATE: bool = true;
const SUPPORTS_TEST_CONNECTION: bool = true;
const ROUTE_PREFIX: &'static str = "/websocket_triggers";
@@ -65,6 +65,7 @@
let error_handler_path: string | undefined = $state()
let error_handler_args: Record<string, any> = $state({})
let retry: Retry | undefined = $state()
let enabled = $state(false)
// Component references
let drawer = $state<Drawer | undefined>(undefined)
let initialConfig: NewEmailTrigger | undefined = undefined
@@ -72,7 +73,7 @@
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
let errorHandlerSelected: ErrorHandler = $state('slack')
const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
const routeConfig = $derived.by(getEmailTriggerConfig)
const emailConfig = $derived.by(getEmailTriggerConfig)
const captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
const saveDisabled = $derived(
drawerLoading || !can_write || pathError != '' || !isValid || emptyString(script_path)
@@ -141,6 +142,7 @@
error_handler_args = defaultValues?.error_handler_args ?? {}
retry = defaultValues?.retry ?? undefined
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
enabled = defaultValues?.enabled ?? false
} finally {
clearTimeout(loader)
drawerLoading = false
@@ -161,6 +163,7 @@
error_handler_args = cfg?.error_handler_args ?? {}
retry = cfg?.retry
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
enabled = cfg?.enabled ?? false
}
async function loadTrigger(defaultConfig?: Partial<EmailTrigger>): Promise<void> {
@@ -179,11 +182,11 @@
async function triggerScript(): Promise<void> {
if (customSaveBehavior) {
customSaveBehavior(routeConfig)
customSaveBehavior(emailConfig)
drawer?.closeDrawer()
} else {
deploymentLoading = true
const saveCfg = routeConfig
const saveCfg = emailConfig
const isSaved = await saveEmailTriggerFromCfg(
initialPath,
saveCfg,
@@ -210,17 +213,30 @@
extra_perms: extraPerms,
error_handler_path,
error_handler_args,
retry
retry,
enabled
}
return nCfg
}
async function handleToggleEnabled(newEnabled: boolean) {
enabled = newEnabled
if (!trigger?.draftConfig) {
await EmailTriggerService.setEmailTriggerEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: newEnabled }
})
sendUserToast(`${newEnabled ? 'enabled' : 'disabled'} email trigger ${initialPath}`)
}
}
// Update config for captures
function getCaptureConfig() {
const newCaptureConfig = {
local_part: routeConfig.local_part,
path: routeConfig.path
local_part: emailConfig.local_part,
path: emailConfig.path
}
//
return newCaptureConfig
@@ -233,7 +249,7 @@
$effect(() => {
if (!drawerLoading) {
handleConfigChange(routeConfig, initialConfig, saveDisabled, edit, onConfigChange)
handleConfigChange(emailConfig, initialConfig, saveDisabled, edit, onConfigChange)
}
})
</script>
@@ -338,8 +354,9 @@
{trigger}
permissions={drawerLoading || !can_write ? 'none' : can_write && isAdmin ? 'create' : 'write'}
{saveDisabled}
enabled={undefined}
{enabled}
{allowDraft}
onToggleEnabled={handleToggleEnabled}
{edit}
isLoading={deploymentLoading}
onUpdate={triggerScript}
@@ -15,21 +15,22 @@ export function getEmailAddress(
export async function saveEmailTriggerFromCfg(
initialPath: string,
routeCfg: Record<string, any>,
emailCfg: Record<string, any>,
edit: boolean,
workspace: string,
isAdmin: boolean,
usedTriggerKinds: Writable<string[]>
): Promise<boolean> {
const requestBody: NewEmailTrigger = {
path: routeCfg.path,
script_path: routeCfg.script_path,
local_part: routeCfg.local_part,
is_flow: routeCfg.is_flow,
workspaced_local_part: routeCfg.workspaced_local_part,
error_handler_path: routeCfg.error_handler_path,
error_handler_args: routeCfg.error_handler_path ? routeCfg.error_handler_args : undefined,
retry: routeCfg.retry
path: emailCfg.path,
script_path: emailCfg.script_path,
local_part: emailCfg.local_part,
is_flow: emailCfg.is_flow,
workspaced_local_part: emailCfg.workspaced_local_part,
error_handler_path: emailCfg.error_handler_path,
error_handler_args: emailCfg.error_handler_path ? emailCfg.error_handler_args : undefined,
retry: emailCfg.retry,
enabled: emailCfg.enabled
}
try {
if (edit) {
@@ -38,16 +39,16 @@ export async function saveEmailTriggerFromCfg(
path: initialPath,
requestBody: {
...requestBody,
local_part: isAdmin || !edit ? routeCfg.local_part : undefined
local_part: isAdmin || !edit ? emailCfg.local_part : undefined
}
})
sendUserToast(`Route ${routeCfg.path} updated`)
sendUserToast(`Email trigger ${emailCfg.path} updated`)
} else {
await EmailTriggerService.createEmailTrigger({
workspace: workspace,
requestBody: requestBody
requestBody: { ...requestBody, enabled: true }
})
sendUserToast(`Route ${routeCfg.path} created`)
sendUserToast(`Email trigger ${emailCfg.path} created`)
}
if (!get(usedTriggerKinds).includes('email')) {
usedTriggerKinds.update((t) => [...t, 'email'])
@@ -236,7 +236,7 @@
s3FileUploadRawMode = defaultValues?.s3FileUploadRawMode ?? false
path = defaultValues?.path ?? ''
initialPath = ''
enabled = defaultValues?.enabled ?? true
enabled = defaultValues?.enabled ?? false
dirtyPath = false
is_static_website = defaultValues?.is_static_website ?? false
workspaced_route = defaultValues?.workspaced_route ?? false
@@ -77,7 +77,7 @@ export async function saveHttpRouteFromCfg(
} else {
await HttpTriggerService.createHttpTrigger({
workspace: workspace,
requestBody: requestBody
requestBody: { ...requestBody, enabled: true }
})
sendUserToast(`Route ${routeCfg.path} created`)
}
@@ -193,6 +193,23 @@
}
}
async function setTriggerEnabled(path: string, enabled: boolean): Promise<void> {
try {
await EmailTriggerService.setEmailTriggerEnabled({
path,
workspace: $workspaceStore!,
requestBody: { enabled }
})
} catch (err) {
sendUserToast(
`Cannot ` + (enabled ? 'enable' : 'disable') + ` email trigger: ${err.body}`,
true
)
} finally {
loadTriggers()
}
}
onMount(() => {
loadQueryFilters()
})
@@ -274,7 +291,7 @@
<div class="text-center text-sm text-primary mt-2"> No email triggers </div>
{:else if items?.length}
<div class="border rounded-md divide-y">
{#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_local_part, path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, marked, local_part } (path)}
{#each items.slice(0, nbDisplayed) as { workspace_id, workspaced_local_part, path, edited_by, edited_at, script_path, is_flow, extra_perms, canWrite, marked, local_part, enabled } (path)}
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
{@const emailAddress = getEmailAddress(
local_part,
@@ -317,6 +334,13 @@
<div class="hidden lg:flex flex-row gap-1 items-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
<Toggle
checked={enabled}
disabled={!canWrite}
on:change={(e) => {
setTriggerEnabled(path, e.detail)
}}
/>
<div class="flex gap-2 items-center justify-end">
<Button