mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: email triggers (#6548)
* feat: email triggers
* Change down migration to drop email_trigger table
* email triggers UI
* bug fix
* Apply suggestion from @ellipsis-dev[bot]
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
* cli and git sync
* Revert "cli and git sync"
This reverts commit 220fd50d13.
---------
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
+2
-1
@@ -38,7 +38,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT script_path, is_flow, workspace_id, edited_by, email, path FROM email_trigger WHERE workspace_id = $1 AND local_part = $2 AND (workspaced_local_part = TRUE OR $3 IS TRUE)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "23fb2099fe211c9c5388f28097ed8635198a4144a3415121800f5df52b2a133f"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1 \n FROM email_trigger \n WHERE \n ((workspaced_local_part IS TRUE AND workspace_id || '-' || local_part = $1) \n OR (workspaced_local_part IS FALSE AND local_part = $1))\n AND ($2::TEXT IS NULL OR path != $2)\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "28a5caa6e8a8a95d228908cc83e9109986a0803d50164a259aabcdee81014d07"
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT script_path, is_flow, workspace_id, edited_by, email, path FROM email_trigger WHERE local_part = $1 AND workspaced_local_part = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3700706bb0408d6593be7f15f8aee4d6a023109e5dcfee7009a25ddf5db5e28d"
|
||||
}
|
||||
+2
-1
@@ -22,7 +22,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n owner, \n email,\n path,\n is_flow\n FROM \n capture_config\n WHERE \n workspace_id = $1\n AND trigger_config->>'local_part' = $2\n AND trigger_kind = 'email'::TRIGGER_KIND \n AND last_client_ping > NOW() - INTERVAL '10 seconds'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4c70aac8f83e9b76628a65df351c16cb630aa963d20898b084b043a59bfc4ba8"
|
||||
}
|
||||
+4
-2
@@ -30,7 +30,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -66,7 +67,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1 \n FROM email_trigger \n WHERE \n workspace_id = $1\n AND local_part = $2 \n AND ($3::TEXT IS NULL OR path != $3)\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "54ee00c98dc50ca3da0e2195a78e6df77fd1e8c969667b267aed3347335c91ab"
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6fafc23924eded970689040bd4a94d4d23ebee4f2b7d37bb54c47edb7720be00"
|
||||
}
|
||||
+2
-1
@@ -25,7 +25,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n workspaced_route = $1,\n wrap_body = $2,\n raw_string = $3,\n authentication_resource_path = $4,\n script_path = $5, \n path = $6, \n is_flow = $7, \n http_method = $8, \n static_asset_config = $9, \n edited_by = $10, \n email = $11, \n is_async = $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 ",
|
||||
"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 is_async = $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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
@@ -57,5 +56,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6ca3ae85ddeb2334c9ae4bd7682daef744a1b10b4f47bc7355a97011bc366aae"
|
||||
"hash": "a7a23229d6915d7fdeea8073d31be6e7d9f7a8581bdbb5914f2c5b49f37dbc36"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acb094aef60bba9083087264d65034fce38417099f15e8312be72a386f10bc1f"
|
||||
}
|
||||
+2
-1
@@ -22,7 +22,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\"\n ",
|
||||
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\",\n EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS \"email_used!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "gcp_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "email_used!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,8 +62,9 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "aa0215d4174c1aeda8631bcd582c895329d2daf722d360fbcbdef6f04bb1400f"
|
||||
"hash": "d1d9a3184f3470949840cc07414d08641527a4fe329204c65c49ab82d9ee4afb"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d328d00e5393b0e8d2c4b3674221fabbae580fc404e0d4481fd8d7fb51272c94"
|
||||
}
|
||||
+2
-1
@@ -22,7 +22,8 @@
|
||||
"postgres",
|
||||
"sqs",
|
||||
"mqtt",
|
||||
"gcp"
|
||||
"gcp",
|
||||
"default_email"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
28adb8c6253e798056fceedeed5dac3eaf64c568
|
||||
29e2930269c744fe6136334f8b159afc9dd26bab
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE email_trigger;
|
||||
@@ -0,0 +1,68 @@
|
||||
-- Add up migration script here
|
||||
|
||||
CREATE TABLE email_trigger (
|
||||
path VARCHAR(255) NOT NULL,
|
||||
local_part VARCHAR(255) NOT NULL,
|
||||
workspaced_local_part BOOLEAN NOT NULL,
|
||||
script_path VARCHAR(255) NOT NULL,
|
||||
is_flow BOOLEAN NOT NULL,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
edited_by VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
extra_perms JSONB NOT NULL DEFAULT '{}',
|
||||
error_handler_path VARCHAR(255) NULL,
|
||||
error_handler_args JSONB NULL,
|
||||
retry JSONB NULL,
|
||||
PRIMARY KEY (path, workspace_id)
|
||||
);
|
||||
|
||||
GRANT ALL ON email_trigger TO windmill_user;
|
||||
GRANT ALL ON email_trigger TO windmill_admin;
|
||||
|
||||
ALTER TABLE email_trigger ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY admin_policy ON email_trigger FOR ALL TO windmill_admin USING (true);
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user_select ON email_trigger FOR SELECT TO windmill_user
|
||||
USING (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
|
||||
CREATE POLICY see_folder_extra_perms_user_insert ON email_trigger FOR INSERT TO windmill_user
|
||||
WITH CHECK (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
CREATE POLICY see_folder_extra_perms_user_update ON email_trigger FOR UPDATE TO windmill_user
|
||||
USING (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
CREATE POLICY see_folder_extra_perms_user_delete ON email_trigger FOR DELETE TO windmill_user
|
||||
USING (SPLIT_PART(email_trigger.path, '/', 1) = 'f' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_own ON email_trigger FOR ALL TO windmill_user
|
||||
USING (SPLIT_PART(email_trigger.path, '/', 1) = 'u' AND SPLIT_PART(email_trigger.path, '/', 2) = current_setting('session.user'));
|
||||
CREATE POLICY see_member ON email_trigger FOR ALL TO windmill_user
|
||||
USING (SPLIT_PART(email_trigger.path, '/', 1) = 'g' AND SPLIT_PART(email_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
|
||||
|
||||
CREATE POLICY see_extra_perms_user_select ON email_trigger FOR SELECT TO windmill_user
|
||||
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
|
||||
CREATE POLICY see_extra_perms_user_insert ON email_trigger FOR INSERT TO windmill_user
|
||||
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
CREATE POLICY see_extra_perms_user_update ON email_trigger FOR UPDATE TO windmill_user
|
||||
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
CREATE POLICY see_extra_perms_user_delete ON email_trigger FOR DELETE TO windmill_user
|
||||
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_select ON email_trigger FOR SELECT TO windmill_user
|
||||
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
|
||||
CREATE POLICY see_extra_perms_groups_insert ON email_trigger FOR INSERT TO windmill_user
|
||||
WITH CHECK (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
CREATE POLICY see_extra_perms_groups_update ON email_trigger FOR UPDATE TO windmill_user
|
||||
USING (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
CREATE POLICY see_extra_perms_groups_delete ON email_trigger FOR DELETE TO windmill_user
|
||||
USING (exists(
|
||||
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND value::boolean));
|
||||
|
||||
ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'default_email';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
UPDATE capture SET trigger_kind = 'email' WHERE trigger_kind = 'default_email';
|
||||
UPDATE capture_config SET trigger_kind = 'email' WHERE trigger_kind = 'default_email';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
UPDATE capture SET trigger_kind = 'default_email' WHERE trigger_kind = 'email';
|
||||
UPDATE capture_config SET trigger_kind = 'default_email' WHERE trigger_kind = 'email';
|
||||
@@ -2907,6 +2907,8 @@ paths:
|
||||
type: boolean
|
||||
sqs_used:
|
||||
type: boolean
|
||||
email_used:
|
||||
type: boolean
|
||||
required:
|
||||
- http_routes_used
|
||||
- websocket_used
|
||||
@@ -2916,6 +2918,7 @@ paths:
|
||||
- mqtt_used
|
||||
- gcp_used
|
||||
- sqs_used
|
||||
- email_used
|
||||
/w/{workspace}/users/list:
|
||||
get:
|
||||
summary: list users
|
||||
@@ -10914,6 +10917,171 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/email_triggers/create:
|
||||
post:
|
||||
summary: create email trigger
|
||||
operationId: createEmailTrigger
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: new email trigger
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewEmailTrigger"
|
||||
responses:
|
||||
"201":
|
||||
description: email trigger created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/email_triggers/update/{path}:
|
||||
post:
|
||||
summary: update email trigger
|
||||
operationId: updateEmailTrigger
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
requestBody:
|
||||
description: updated trigger
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EditEmailTrigger"
|
||||
responses:
|
||||
"200":
|
||||
description: email trigger updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/email_triggers/delete/{path}:
|
||||
delete:
|
||||
summary: delete email trigger
|
||||
operationId: deleteEmailTrigger
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: email trigger deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/email_triggers/get/{path}:
|
||||
get:
|
||||
summary: get email trigger
|
||||
operationId: getEmailTrigger
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: email trigger retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EmailTrigger"
|
||||
|
||||
/w/{workspace}/email_triggers/list:
|
||||
get:
|
||||
summary: list email triggers
|
||||
operationId: listEmailTriggers
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
required: true
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: path
|
||||
description: filter by path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: is_flow
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: path_start
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: email trigger list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/EmailTrigger"
|
||||
|
||||
/w/{workspace}/email_triggers/exists/{path}:
|
||||
get:
|
||||
summary: does email trigger exists
|
||||
operationId: existsEmailTrigger
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: email trigger exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/w/{workspace}/email_triggers/local_part_exists:
|
||||
post:
|
||||
summary: does email local part exists
|
||||
operationId: existsEmailLocalPart
|
||||
tags:
|
||||
- email_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: email local part exists request
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
local_part:
|
||||
type: string
|
||||
workspaced_local_part:
|
||||
type: boolean
|
||||
trigger_path:
|
||||
type: string
|
||||
|
||||
required:
|
||||
- local_part
|
||||
responses:
|
||||
"200":
|
||||
description: email local part exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/groups/list:
|
||||
get:
|
||||
summary: list instance groups
|
||||
@@ -11999,6 +12167,7 @@ paths:
|
||||
mqtt_trigger,
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
]
|
||||
responses:
|
||||
"200":
|
||||
@@ -12043,6 +12212,7 @@ paths:
|
||||
mqtt_trigger,
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
@@ -12098,6 +12268,7 @@ paths:
|
||||
mqtt_trigger,
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
@@ -16131,6 +16302,8 @@ components:
|
||||
type: number
|
||||
email_count:
|
||||
type: number
|
||||
default_email_count:
|
||||
type: number
|
||||
websocket_count:
|
||||
type: number
|
||||
postgres_count:
|
||||
@@ -17086,6 +17259,75 @@ components:
|
||||
- subjects
|
||||
- is_flow
|
||||
|
||||
EmailTrigger:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TriggerExtraProperty"
|
||||
type: object
|
||||
properties:
|
||||
local_part:
|
||||
type: string
|
||||
workspaced_local_part:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
|
||||
required:
|
||||
- local_part
|
||||
|
||||
NewEmailTrigger:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
script_path:
|
||||
type: string
|
||||
local_part:
|
||||
type: string
|
||||
workspaced_local_part:
|
||||
type: boolean
|
||||
is_flow:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
- local_part
|
||||
- is_flow
|
||||
|
||||
EditEmailTrigger:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
script_path:
|
||||
type: string
|
||||
local_part:
|
||||
type: string
|
||||
workspaced_local_part:
|
||||
type: boolean
|
||||
is_flow:
|
||||
type: boolean
|
||||
error_handler_path:
|
||||
type: string
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
- is_flow
|
||||
|
||||
Group:
|
||||
type: object
|
||||
properties:
|
||||
@@ -18141,7 +18383,7 @@ components:
|
||||
CaptureTriggerKind:
|
||||
type: string
|
||||
enum:
|
||||
[webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt, gcp]
|
||||
[webhook, http, websocket, kafka, default_email, nats, postgres, sqs, mqtt, gcp, email]
|
||||
|
||||
Capture:
|
||||
type: object
|
||||
|
||||
@@ -146,6 +146,12 @@ struct HttpTriggerConfig {
|
||||
wrap_body: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct EmailTriggerConfig {
|
||||
local_part: String,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct KafkaTriggerConfig {
|
||||
@@ -241,6 +247,8 @@ enum TriggerConfig {
|
||||
Mqtt(MqttTriggerConfig),
|
||||
#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))]
|
||||
Gcp(GcpTriggerConfig),
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))]
|
||||
Email(EmailTriggerConfig),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
||||
@@ -23,7 +23,7 @@ use windmill_common::{
|
||||
utils::{not_found_if_none, StripPath},
|
||||
};
|
||||
|
||||
const KINDS: [&str; 14] = [
|
||||
const KINDS: [&str; 18] = [
|
||||
"script",
|
||||
"group_",
|
||||
"resource",
|
||||
@@ -37,7 +37,11 @@ const KINDS: [&str; 14] = [
|
||||
"websocket_trigger",
|
||||
"kafka_trigger",
|
||||
"nats_trigger",
|
||||
"mqtt_trigger"
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"gcp_trigger",
|
||||
"sqs_trigger",
|
||||
"email_trigger",
|
||||
];
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -131,7 +135,7 @@ async fn add_granular_acl(
|
||||
&w_id,
|
||||
DeployedObject::Folder { path: format!("f/{}", path) },
|
||||
Some(format!("Folder '{}' changed permissions", path)),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -243,7 +247,7 @@ async fn remove_granular_acl(
|
||||
&w_id,
|
||||
DeployedObject::Folder { path: format!("f/{}", path) },
|
||||
Some(format!("Folder '{}' changed permissions", path)),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ pub enum ScopeDomain {
|
||||
SqsTriggers,
|
||||
GcpTriggers,
|
||||
PostgresTriggers,
|
||||
EmailTriggers,
|
||||
|
||||
// System domains
|
||||
Audit,
|
||||
@@ -308,6 +309,7 @@ impl ScopeDomain {
|
||||
Self::SqsTriggers => "sqs_triggers",
|
||||
Self::GcpTriggers => "gcp_triggers",
|
||||
Self::PostgresTriggers => "postgres_triggers",
|
||||
Self::EmailTriggers => "email_triggers",
|
||||
Self::Audit => "audit",
|
||||
Self::Settings => "settings",
|
||||
Self::Workers => "workers",
|
||||
@@ -354,6 +356,7 @@ impl ScopeDomain {
|
||||
"sqs_triggers" => Some(Self::SqsTriggers),
|
||||
"gcp_triggers" => Some(Self::GcpTriggers),
|
||||
"postgres_triggers" => Some(Self::PostgresTriggers),
|
||||
"email_triggers" => Some(Self::EmailTriggers),
|
||||
"audit" => Some(Self::Audit),
|
||||
"settings" => Some(Self::Settings),
|
||||
"workers" => Some(Self::Workers),
|
||||
|
||||
@@ -27,6 +27,7 @@ fn build_trigger_scope_domains() -> Vec<ScopeDomain> {
|
||||
("sqs_triggers", "AWS SQS"),
|
||||
("gcp_triggers", "GCP Pub/Sub"),
|
||||
("postgres_triggers", "PostgreSQL"),
|
||||
("email_triggers", "Email"),
|
||||
];
|
||||
|
||||
TRIGGER_DOMAINS
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#[cfg(not(feature = "private"))]
|
||||
use crate::triggers::TriggerData;
|
||||
|
||||
#[allow(unused)]
|
||||
#[cfg(feature = "private")]
|
||||
pub use super::handler_ee::*;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
use {
|
||||
super::EmailTrigger,
|
||||
crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
triggers::TriggerCrud,
|
||||
},
|
||||
axum::async_trait,
|
||||
sqlx::PgConnection,
|
||||
windmill_common::error::{Error, Result},
|
||||
windmill_git_sync::DeployedObject,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
#[async_trait]
|
||||
impl TriggerCrud for EmailTrigger {
|
||||
type Trigger = ();
|
||||
type TriggerConfig = ();
|
||||
type TriggerConfigRequest = ();
|
||||
type TestConnectionConfig = ();
|
||||
|
||||
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 = "/email_triggers";
|
||||
const DEPLOYMENT_NAME: &'static str = "";
|
||||
const IS_CLOUD_HOSTED: bool = false;
|
||||
|
||||
fn get_deployed_object(path: String) -> DeployedObject {
|
||||
DeployedObject::EmailTrigger { path }
|
||||
}
|
||||
|
||||
async fn create_trigger(
|
||||
&self,
|
||||
_db: &DB,
|
||||
_tx: &mut PgConnection,
|
||||
_authed: &ApiAuthed,
|
||||
_w_id: &str,
|
||||
_trigger: TriggerData<Self::TriggerConfigRequest>,
|
||||
) -> Result<()> {
|
||||
Err(Error::BadRequest(
|
||||
"Email triggers are not available in open source version".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_trigger(
|
||||
&self,
|
||||
_db: &DB,
|
||||
_executor: &mut PgConnection,
|
||||
_authed: &ApiAuthed,
|
||||
_workspace_id: &str,
|
||||
_path: &str,
|
||||
_trigger: TriggerData<Self::TriggerConfigRequest>,
|
||||
) -> Result<()> {
|
||||
Err(Error::BadRequest(
|
||||
"Email triggers are not available in open source version".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#[cfg(feature = "private")]
|
||||
mod handler_ee;
|
||||
pub mod handler_oss;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
mod mod_ee;
|
||||
#[cfg(feature = "private")]
|
||||
pub use mod_ee::*;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct EmailTrigger;
|
||||
@@ -231,8 +231,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
edited_by = $3,
|
||||
edited_at = now(),
|
||||
server_id = NULL,
|
||||
error = NULL,
|
||||
last_server_ping = NULL
|
||||
error = NULL
|
||||
WHERE
|
||||
workspace_id = $4 AND
|
||||
path = $5
|
||||
@@ -737,6 +736,16 @@ pub fn generate_trigger_routers() -> Router {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))]
|
||||
{
|
||||
use crate::triggers::email::EmailTrigger;
|
||||
|
||||
router = router.nest(
|
||||
EmailTrigger::ROUTE_PREFIX,
|
||||
complete_trigger_routes(EmailTrigger),
|
||||
);
|
||||
}
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
@@ -752,6 +761,7 @@ pub struct TriggersCount {
|
||||
http_routes_count: i64,
|
||||
webhook_count: i64,
|
||||
email_count: i64,
|
||||
default_email_count: i64,
|
||||
websocket_count: i64,
|
||||
kafka_count: i64,
|
||||
nats_count: i64,
|
||||
@@ -872,6 +882,18 @@ pub async fn get_triggers_count_internal(
|
||||
};
|
||||
#[cfg(not(all(feature = "gcp_trigger", feature = "enterprise", feature = "private")))]
|
||||
let gcp_count = 0;
|
||||
|
||||
#[cfg(all(feature = "smtp", feature = "enterprise", feature = "private"))]
|
||||
let email_count = {
|
||||
use crate::triggers::email::EmailTrigger;
|
||||
let count = EmailTrigger
|
||||
.trigger_count(&mut tx, w_id, is_flow, path)
|
||||
.await;
|
||||
count
|
||||
};
|
||||
#[cfg(not(all(feature = "smtp", feature = "enterprise", feature = "private")))]
|
||||
let email_count = 0;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let webhook_count = (if is_flow {
|
||||
@@ -890,7 +912,7 @@ pub async fn get_triggers_count_internal(
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let email_count = (if is_flow {
|
||||
let default_email_count = (if is_flow {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
w_id,
|
||||
@@ -911,6 +933,7 @@ pub async fn get_triggers_count_internal(
|
||||
schedule_count,
|
||||
http_routes_count,
|
||||
webhook_count,
|
||||
default_email_count,
|
||||
email_count,
|
||||
websocket_count,
|
||||
kafka_count,
|
||||
|
||||
@@ -458,8 +458,7 @@ impl TriggerCrud for HttpTrigger {
|
||||
}
|
||||
|
||||
let route_path_key =
|
||||
check_if_route_exist(db, &trigger.config, workspace_id, Some(path))
|
||||
.await?;
|
||||
check_if_route_exist(db, &trigger.config, workspace_id, Some(path)).await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -524,31 +523,29 @@ impl TriggerCrud for HttpTrigger {
|
||||
UPDATE
|
||||
http_trigger
|
||||
SET
|
||||
workspaced_route = $1,
|
||||
wrap_body = $2,
|
||||
raw_string = $3,
|
||||
authentication_resource_path = $4,
|
||||
script_path = $5,
|
||||
path = $6,
|
||||
is_flow = $7,
|
||||
http_method = $8,
|
||||
static_asset_config = $9,
|
||||
edited_by = $10,
|
||||
email = $11,
|
||||
is_async = $12,
|
||||
authentication_method = $13,
|
||||
summary = $14,
|
||||
description = $15,
|
||||
wrap_body = $1,
|
||||
raw_string = $2,
|
||||
authentication_resource_path = $3,
|
||||
script_path = $4,
|
||||
path = $5,
|
||||
is_flow = $6,
|
||||
http_method = $7,
|
||||
static_asset_config = $8,
|
||||
edited_by = $9,
|
||||
email = $10,
|
||||
is_async = $11,
|
||||
authentication_method = $12,
|
||||
summary = $13,
|
||||
description = $14,
|
||||
edited_at = now(),
|
||||
is_static_website = $16,
|
||||
error_handler_path = $17,
|
||||
error_handler_args = $18,
|
||||
retry = $19
|
||||
is_static_website = $15,
|
||||
error_handler_path = $16,
|
||||
error_handler_args = $17,
|
||||
retry = $18
|
||||
WHERE
|
||||
workspace_id = $20 AND
|
||||
path = $21
|
||||
workspace_id = $19 AND
|
||||
path = $20
|
||||
"#,
|
||||
trigger.config.workspaced_route,
|
||||
trigger.config.wrap_body,
|
||||
trigger.config.raw_string,
|
||||
trigger.config.authentication_resource_path,
|
||||
|
||||
@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::{types::Json as SqlxJson, FromRow};
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
|
||||
#[cfg(all(feature = "smtp", feature = "enterprise", feature = "private"))]
|
||||
pub mod email;
|
||||
#[cfg(all(feature = "gcp_trigger", feature = "enterprise", feature = "private"))]
|
||||
pub mod gcp;
|
||||
#[cfg(feature = "http_trigger")]
|
||||
|
||||
@@ -2012,6 +2012,7 @@ struct UsedTriggers {
|
||||
pub mqtt_used: bool,
|
||||
pub sqs_used: bool,
|
||||
pub gcp_used: bool,
|
||||
pub email_used: bool,
|
||||
}
|
||||
|
||||
async fn get_used_triggers(
|
||||
@@ -2031,7 +2032,8 @@ async fn get_used_triggers(
|
||||
EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS "postgres_used!",
|
||||
EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS "mqtt_used!",
|
||||
EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS "sqs_used!",
|
||||
EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS "gcp_used!"
|
||||
EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS "gcp_used!",
|
||||
EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS "email_used!"
|
||||
"#,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -5,13 +5,14 @@ use std::fmt;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, EnumIter)]
|
||||
#[sqlx(type_name = "TRIGGER_KIND", rename_all = "lowercase")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[sqlx(type_name = "TRIGGER_KIND", rename_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TriggerKind {
|
||||
Webhook,
|
||||
Http,
|
||||
Websocket,
|
||||
Kafka,
|
||||
DefaultEmail,
|
||||
Email,
|
||||
Nats,
|
||||
Mqtt,
|
||||
@@ -28,6 +29,7 @@ impl TriggerKind {
|
||||
TriggerKind::Websocket => "websocket".to_string(),
|
||||
TriggerKind::Kafka => "kafka".to_string(),
|
||||
TriggerKind::Email => "email".to_string(),
|
||||
TriggerKind::DefaultEmail => "email".to_string(), // to the user we also show kind email for default email
|
||||
TriggerKind::Nats => "nats".to_string(),
|
||||
TriggerKind::Mqtt => "mqtt".to_string(),
|
||||
TriggerKind::Sqs => "sqs".to_string(),
|
||||
@@ -45,6 +47,7 @@ impl fmt::Display for TriggerKind {
|
||||
TriggerKind::Websocket => "websocket",
|
||||
TriggerKind::Kafka => "kafka",
|
||||
TriggerKind::Email => "email",
|
||||
TriggerKind::DefaultEmail => "default_email",
|
||||
TriggerKind::Nats => "nats",
|
||||
TriggerKind::Mqtt => "mqtt",
|
||||
TriggerKind::Sqs => "sqs",
|
||||
|
||||
@@ -34,6 +34,7 @@ pub enum DeployedObject {
|
||||
MqttTrigger { path: String },
|
||||
SqsTrigger { path: String },
|
||||
GcpTrigger { path: String },
|
||||
EmailTrigger { path: String },
|
||||
Settings { setting_type: String },
|
||||
Key { key_type: String },
|
||||
}
|
||||
@@ -59,6 +60,7 @@ impl DeployedObject {
|
||||
DeployedObject::MqttTrigger { path } => path.to_owned(),
|
||||
DeployedObject::SqsTrigger { path } => path.to_owned(),
|
||||
DeployedObject::GcpTrigger { path } => path.to_owned(),
|
||||
DeployedObject::EmailTrigger { path } => path.to_owned(),
|
||||
DeployedObject::Settings { .. } => "settings.yaml".to_string(),
|
||||
DeployedObject::Key { .. } => "encryption_key.yaml".to_string(),
|
||||
}
|
||||
@@ -95,6 +97,7 @@ impl DeployedObject {
|
||||
DeployedObject::MqttTrigger { .. } => None,
|
||||
DeployedObject::SqsTrigger { .. } => None,
|
||||
DeployedObject::GcpTrigger { .. } => None,
|
||||
DeployedObject::EmailTrigger { .. } => None,
|
||||
DeployedObject::Settings { .. } => None,
|
||||
DeployedObject::Key { .. } => None,
|
||||
}
|
||||
|
||||
@@ -1011,7 +1011,10 @@ async fn run_agent(
|
||||
let output_value = match content {
|
||||
Some(content_str) => match has_output_properties {
|
||||
true => serde_json::from_str::<Box<RawValue>>(&content_str).map_err(|_e| {
|
||||
Error::internal_err(format!("Failed to parse structured output: {}", content_str))
|
||||
Error::internal_err(format!(
|
||||
"Failed to parse structured output: {}",
|
||||
content_str
|
||||
))
|
||||
})?,
|
||||
false => to_raw_value(&content_str),
|
||||
},
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
new Triggers(
|
||||
[
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
{ type: 'default_email', path: '', isDraft: false },
|
||||
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
|
||||
],
|
||||
selectedTriggerIndexFromUrl,
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
NatsTriggerService,
|
||||
MqttTriggerService,
|
||||
SqsTriggerService,
|
||||
GcpTriggerService
|
||||
GcpTriggerService,
|
||||
EmailTriggerService
|
||||
} from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
@@ -52,6 +53,7 @@
|
||||
| 'mqtt_trigger'
|
||||
| 'sqs_trigger'
|
||||
| 'gcp_trigger'
|
||||
| 'email_trigger'
|
||||
let meta: Meta | undefined = $state(undefined)
|
||||
interface Props {
|
||||
fullNamePlaceholder?: string | undefined
|
||||
@@ -290,6 +292,11 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: path
|
||||
})
|
||||
} else if (kind === 'email_trigger') {
|
||||
return await EmailTriggerService.existsEmailTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: path
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
new Triggers(
|
||||
[
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
{ type: 'default_email', path: '', isDraft: false },
|
||||
...(script.draft_triggers ?? [])
|
||||
],
|
||||
undefined,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| 'sqs_trigger'
|
||||
| 'postgres_trigger'
|
||||
| 'gcp_trigger'
|
||||
| 'email_trigger'
|
||||
let kind: Kind
|
||||
|
||||
let path: string = $state('')
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import { Tabs, Tab } from '$lib/components/common'
|
||||
import {
|
||||
CalendarCheck2,
|
||||
MailIcon,
|
||||
Route,
|
||||
Terminal,
|
||||
Webhook,
|
||||
Unplug,
|
||||
PlugZap,
|
||||
Database
|
||||
} from 'lucide-svelte'
|
||||
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon } from '../icons'
|
||||
import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte'
|
||||
|
||||
interface Props {
|
||||
triggerSelected?:
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'kafka'
|
||||
| 'postgres'
|
||||
| 'nats'
|
||||
| 'sqs'
|
||||
| 'mqtt'
|
||||
| 'gcp'
|
||||
| 'scheduledPoll'
|
||||
simplfiedPoll?: boolean
|
||||
eventStreamType?: 'kafka' | 'nats' | 'sqs' | 'mqtt' | 'gcp'
|
||||
webhooks?: import('svelte').Snippet
|
||||
routes?: import('svelte').Snippet
|
||||
emails?: import('svelte').Snippet
|
||||
schedules?: import('svelte').Snippet
|
||||
websockets?: import('svelte').Snippet
|
||||
postgres?: import('svelte').Snippet
|
||||
kafka?: import('svelte').Snippet
|
||||
nats?: import('svelte').Snippet
|
||||
sqs?: import('svelte').Snippet
|
||||
mqtt?: import('svelte').Snippet
|
||||
gcp?: import('svelte').Snippet
|
||||
cli?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
triggerSelected = $bindable('webhooks'),
|
||||
simplfiedPoll = false,
|
||||
eventStreamType = $bindable('kafka'),
|
||||
webhooks,
|
||||
routes,
|
||||
emails,
|
||||
schedules,
|
||||
websockets,
|
||||
postgres,
|
||||
kafka,
|
||||
nats,
|
||||
sqs,
|
||||
mqtt,
|
||||
gcp,
|
||||
cli
|
||||
}: Props = $props()
|
||||
|
||||
run(() => {
|
||||
if (
|
||||
triggerSelected === 'kafka' ||
|
||||
triggerSelected === 'nats' ||
|
||||
triggerSelected === 'sqs' ||
|
||||
triggerSelected === 'mqtt' ||
|
||||
triggerSelected === 'gcp'
|
||||
) {
|
||||
eventStreamType = triggerSelected
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
{#if !simplfiedPoll}
|
||||
<div class="flex flex-col h-full">
|
||||
<Tabs bind:selected={triggerSelected} wrapperClass="flex-none w-full">
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Webhook size={12} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedules">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<CalendarCheck2 size={12} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Route size={12} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="websockets">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
WebSockets
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="postgres">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Database size={12} />
|
||||
Postgres
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="kafka" otherValues={['nats', 'sqs', 'mqtt', 'gcp']}>
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<PlugZap size={12} />
|
||||
Event streams
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="emails">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<MailIcon size={12} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Terminal size={12} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
|
||||
{#snippet content()}
|
||||
<div class="min-h-0 grow overflow-y-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
{@render webhooks?.()}
|
||||
{:else if triggerSelected === 'routes'}
|
||||
{@render routes?.()}
|
||||
{:else if triggerSelected === 'emails'}
|
||||
{@render emails?.()}
|
||||
{:else if triggerSelected === 'schedules'}
|
||||
{@render schedules?.()}
|
||||
{:else if triggerSelected === 'websockets'}
|
||||
{@render websockets?.()}
|
||||
{:else if triggerSelected === 'postgres'}
|
||||
{@render postgres?.()}
|
||||
{:else if triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs' || triggerSelected === 'mqtt' || triggerSelected === 'gcp'}
|
||||
<div class="m-1.5">
|
||||
<ToggleButtonGroup bind:selected={eventStreamType}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} {item} />
|
||||
<ToggleButton value="nats" label="NATS" icon={NatsIcon} {item} />
|
||||
<ToggleButton value="mqtt" label="MQTT" icon={MqttIcon} {item} />
|
||||
<ToggleButton value="sqs" label="SQS" icon={AwsIcon} {item} />
|
||||
<ToggleButton value="gcp" label="GCP Pub/Sub" icon={GoogleCloudIcon} {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if eventStreamType === 'kafka'}
|
||||
{@render kafka?.()}
|
||||
{:else if eventStreamType === 'nats'}
|
||||
{@render nats?.()}
|
||||
{:else if eventStreamType === 'sqs'}
|
||||
{@render sqs?.()}
|
||||
{:else if eventStreamType === 'mqtt'}
|
||||
{@render mqtt?.()}
|
||||
{:else if eventStreamType === 'gcp'}
|
||||
{@render gcp?.()}
|
||||
{/if}
|
||||
{:else if triggerSelected === 'cli'}
|
||||
{@render cli?.()}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
</div>
|
||||
{:else}
|
||||
{@render schedules?.()}
|
||||
{/if}
|
||||
@@ -1,84 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import UserSettings from '../UserSettings.svelte'
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import Skeleton from '../common/skeleton/Skeleton.svelte'
|
||||
import TriggerTokens from '../triggers/TriggerTokens.svelte'
|
||||
import Description from '../Description.svelte'
|
||||
import Section from '../Section.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import EmailTriggerConfigSection from './EmailTriggerConfigSection.svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let token: string
|
||||
export let scopes: string[] = []
|
||||
export let isFlow: boolean = false
|
||||
export let hash: string | undefined = undefined
|
||||
export let path: string
|
||||
|
||||
let emailDomain: string | null = null
|
||||
let triggerTokens: TriggerTokens | undefined = undefined
|
||||
|
||||
let loading = true
|
||||
async function getEmailDomain() {
|
||||
emailDomain =
|
||||
((await SettingService.getGlobal({
|
||||
key: 'email_domain'
|
||||
})) as any) ?? 'mail.test.com'
|
||||
loading = false
|
||||
}
|
||||
|
||||
getEmailDomain()
|
||||
|
||||
$: emailDomain && dispatch('email-domain', emailDomain)
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<UserSettings
|
||||
bind:this={userSettings}
|
||||
on:tokenCreated={(e) => {
|
||||
token = e.detail
|
||||
triggerTokens?.listTokens()
|
||||
}}
|
||||
newTokenWorkspace={$workspaceStore}
|
||||
newTokenLabel={`email-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<Section label="Email trigger" class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/advanced/email_triggers">
|
||||
Email triggers execute scripts and flows when emails are sent to specific addresses. Each
|
||||
trigger has its own unique email address that can be used to invoke the script or flow.
|
||||
</Description>
|
||||
{#if loading}
|
||||
<Skeleton layout={[[18]]} />
|
||||
{:else}
|
||||
{#if emailDomain}
|
||||
<EmailTriggerConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" type="warning">
|
||||
Ask an instance superadmin to setup the instance for email triggering (<a
|
||||
target="_blank"
|
||||
href="https://windmill.dev/docs/advanced/email_triggers">docs</a
|
||||
>) and to set the email domain in the instance settings.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="Community Edition limitations" type="warning" size="xs">
|
||||
Email triggers on Windmill Community Edition are limited to 100 emails per day.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<TriggerTokens bind:this={triggerTokens} {isFlow} {path} labelPrefix="email" />
|
||||
{/if}
|
||||
</Section>
|
||||
@@ -56,6 +56,7 @@
|
||||
websocket: { icon: Unplug, countKey: 'websocket_count' },
|
||||
postgres: { icon: Database, countKey: 'postgres_count' },
|
||||
kafka: { icon: KafkaIcon, countKey: 'kafka_count', disabled: !$enterpriseLicense },
|
||||
default_email: { icon: Mail, countKey: 'default_email_count' },
|
||||
email: { icon: Mail, countKey: 'email_count' },
|
||||
nats: { icon: NatsIcon, countKey: 'nats_count', disabled: !$enterpriseLicense },
|
||||
mqtt: { icon: MqttIcon, countKey: 'mqtt_count', disabled: !$enterpriseLicense },
|
||||
@@ -72,11 +73,12 @@
|
||||
'websocket',
|
||||
'postgres',
|
||||
'kafka',
|
||||
'email',
|
||||
'default_email',
|
||||
'nats',
|
||||
'mqtt',
|
||||
'sqs',
|
||||
'gcp',
|
||||
'email',
|
||||
'poll',
|
||||
'cli'
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
DollarSignIcon,
|
||||
HomeIcon,
|
||||
LayoutDashboardIcon,
|
||||
MailIcon,
|
||||
PlayIcon,
|
||||
Route,
|
||||
Search,
|
||||
@@ -144,6 +145,13 @@
|
||||
action: (newtab: boolean = false) => gotoPage('/mqtt_triggers', newtab),
|
||||
icon: MqttIcon,
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{
|
||||
search_id: 'nav:email_triggers',
|
||||
label: 'Go to Email triggers',
|
||||
action: (newtab: boolean = false) => gotoPage('/email_triggers', newtab),
|
||||
icon: MailIcon,
|
||||
disabled: $userStore?.operator
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -114,6 +114,11 @@
|
||||
id: 'triggers',
|
||||
href: `${base}/mqtt_triggers`
|
||||
},
|
||||
{
|
||||
label: 'Email triggers',
|
||||
id: 'triggers',
|
||||
href: `${base}/email_triggers`
|
||||
},
|
||||
{
|
||||
label: 'Audit logs',
|
||||
id: 'audit_logs',
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
Unplug,
|
||||
AlertCircle,
|
||||
Database,
|
||||
Pyramid
|
||||
Pyramid,
|
||||
MailIcon
|
||||
} from 'lucide-svelte'
|
||||
import UserMenu from './UserMenu.svelte'
|
||||
import DiscordIcon from '../icons/brands/Discord.svelte'
|
||||
@@ -261,6 +262,15 @@
|
||||
kind: 'mqtt',
|
||||
aiId: 'sidebar-menu-link-mqtt',
|
||||
aiDescription: 'Button to navigate to MQTT triggers'
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
href: '/email_triggers',
|
||||
icon: MailIcon,
|
||||
disabled: $userStore?.operator,
|
||||
kind: 'email',
|
||||
aiId: 'sidebar-menu-link-email',
|
||||
aiDescription: 'Button to navigate to Email triggers'
|
||||
}
|
||||
])
|
||||
let triggerMenuLinks = $derived([
|
||||
|
||||
@@ -44,6 +44,7 @@ export function setScheduledPollSchedule(
|
||||
export type TriggerKind =
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'default_emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
@@ -61,6 +62,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg
|
||||
return 'webhooks'
|
||||
case 'email':
|
||||
return 'emails'
|
||||
case 'default_email':
|
||||
return 'default_emails'
|
||||
case 'http':
|
||||
return 'routes'
|
||||
case 'websocket':
|
||||
|
||||
@@ -85,6 +85,12 @@
|
||||
icon: triggerIconMap.gcp,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
action: () => onAddDraftTrigger?.('email'),
|
||||
icon: triggerIconMap.email,
|
||||
extra: cloudHosted ? extra : undefined
|
||||
},
|
||||
{
|
||||
displayName: 'Scheduled Poll',
|
||||
action: (e) => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { ConnectionInfo } from '../common/alert/ConnectionIndicator.svelte'
|
||||
import type { CaptureInfo } from './CaptureSection.svelte'
|
||||
import WebhooksCapture from './webhook/WebhooksCapture.svelte'
|
||||
import EmailTriggerCaptures from '../details/EmailTriggerCaptures.svelte'
|
||||
import DefaultEmailCapture from './email/DefaultEmailCapture.svelte'
|
||||
import WebsocketCapture from './websocket/WebsocketCapture.svelte'
|
||||
import PostgresCapture from './postgres/PostgresCapture.svelte'
|
||||
import KafkaCapture from './kafka/KafkaCapture.svelte'
|
||||
@@ -15,6 +15,7 @@
|
||||
import MqttCapture from './mqtt/MqttCapture.svelte'
|
||||
import SqsCapture from './sqs/SqsCapture.svelte'
|
||||
import GcpCapture from './gcp/GcpCapture.svelte'
|
||||
import EmailCapture from './email/EmailCapture.svelte'
|
||||
|
||||
interface Props {
|
||||
isFlow: boolean
|
||||
@@ -238,8 +239,8 @@
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'email'}
|
||||
<EmailTriggerCaptures
|
||||
{:else if captureType === 'default_email'}
|
||||
<DefaultEmailCapture
|
||||
{path}
|
||||
{isFlow}
|
||||
emailDomain={data?.emailDomain}
|
||||
@@ -323,6 +324,21 @@
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'email'}
|
||||
<EmailCapture
|
||||
local_part={args.local_part}
|
||||
emailDomain={data?.emailDomain}
|
||||
{isValid}
|
||||
{captureInfo}
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
{captureLoading}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{#if trigger.type === 'default_email'}
|
||||
<span
|
||||
class="ml-2 bg-blue-50 dark:bg-blue-900/40 px-1.5 py-0.5 rounded text-xs text-blue-700 dark:text-blue-100 whitespace-nowrap"
|
||||
>
|
||||
Default
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if trigger.isPrimary}
|
||||
<span
|
||||
class="ml-2 bg-blue-50 dark:bg-blue-900/40 px-1.5 py-0.5 rounded text-xs text-blue-700 dark:text-blue-100 whitespace-nowrap"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
|
||||
).filter((x) => x.label && x.label.startsWith(labelPrefix + '-'))
|
||||
if (labelPrefix == 'email') {
|
||||
$triggersCount = { ...($triggersCount ?? {}), email_count: tokens?.length }
|
||||
$triggersCount = { ...($triggersCount ?? {}), default_email_count: tokens?.length }
|
||||
} else {
|
||||
$triggersCount = { ...($triggersCount ?? {}), webhook_count: tokens?.length }
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
MqttTriggerService,
|
||||
HttpTriggerService,
|
||||
GcpTriggerService,
|
||||
SqsTriggerService
|
||||
SqsTriggerService,
|
||||
EmailTriggerService
|
||||
} from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
@@ -105,7 +106,8 @@
|
||||
gcp: () => GcpTriggerService.deleteGcpTrigger,
|
||||
sqs: () => SqsTriggerService.deleteSqsTrigger,
|
||||
mqtt: () => MqttTriggerService.deleteMqttTrigger,
|
||||
http: () => HttpTriggerService.deleteHttpTrigger
|
||||
http: () => HttpTriggerService.deleteHttpTrigger,
|
||||
email: () => EmailTriggerService.deleteEmailTrigger
|
||||
}
|
||||
|
||||
const deleteHandler = deleteHandlers[triggerType as keyof typeof deleteHandlers]
|
||||
@@ -225,6 +227,14 @@
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
} else if (triggerType === 'email') {
|
||||
await triggersState.fetchEmailTriggers(
|
||||
triggersCount,
|
||||
$workspaceStore,
|
||||
currentPath,
|
||||
isFlow,
|
||||
$userStore
|
||||
)
|
||||
}
|
||||
|
||||
triggersState.selectedTriggerIndex = triggersState.triggers.findIndex(
|
||||
@@ -285,7 +295,7 @@
|
||||
onDeleteDraft={deleteTrigger}
|
||||
onReset={handleResetDraft}
|
||||
webhookToken={$triggersCount?.webhook_count}
|
||||
emailToken={$triggersCount?.email_count}
|
||||
emailToken={$triggersCount?.default_email_count}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -359,8 +369,8 @@
|
||||
onReset={() => {
|
||||
handleResetDraft(triggersState.selectedTriggerIndex)
|
||||
}}
|
||||
on:email-domain={({ detail }) => {
|
||||
emailDomain = detail
|
||||
onEmailDomain={(domain) => {
|
||||
emailDomain = domain
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
>
|
||||
{`${webhookToken} token${webhookToken > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
{:else if trigger.type === 'email' && emailToken}
|
||||
{:else if trigger.type === 'default_email' && emailToken}
|
||||
<span
|
||||
class="ml-2 text-xs rounded-md bg-tertiary/50 group-hover:bg-primary text-primary-inverse px-1.5 py-0.5"
|
||||
>
|
||||
@@ -101,7 +101,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !['email', 'webhook', 'cli'].includes(trigger.type)}
|
||||
{#if !['default_email', 'webhook', 'cli'].includes(trigger.type)}
|
||||
{#if trigger.isDraft}
|
||||
<DeleteTriggerButton {trigger} onDelete={() => onDeleteDraft?.(index)} small />
|
||||
{:else if !!trigger.draftConfig && !trigger.isDraft}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import RoutesPanel from './http/RoutesPanel.svelte'
|
||||
import WebhooksPanel from './webhook/WebhooksPanel.svelte'
|
||||
import EmailTriggerPanel from '../details/EmailTriggerPanel.svelte'
|
||||
import EmailTriggerPanel from './email/EmailTriggerPanel.svelte'
|
||||
import DefaultEmailPanel from './email/DefaultEmailPanel.svelte'
|
||||
import SchedulePanel from '$lib/components/SchedulePanel.svelte'
|
||||
import PostgresTriggersPanel from './postgres/PostgresTriggersPanel.svelte'
|
||||
import KafkaTriggerPanel from './kafka/KafkaTriggersPanel.svelte'
|
||||
@@ -35,6 +36,7 @@
|
||||
onUpdate?: (path: string) => void
|
||||
onDelete?: () => void
|
||||
onReset?: () => void
|
||||
onEmailDomain: (domain: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -48,6 +50,7 @@
|
||||
args,
|
||||
newItem,
|
||||
schema,
|
||||
onEmailDomain,
|
||||
...props
|
||||
}: Props = $props()
|
||||
</script>
|
||||
@@ -71,14 +74,14 @@
|
||||
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
|
||||
{newItem}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'email'}
|
||||
<EmailTriggerPanel
|
||||
{:else if selectedTrigger.type === 'default_email'}
|
||||
<DefaultEmailPanel
|
||||
token=""
|
||||
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{isFlow}
|
||||
{hash}
|
||||
on:email-domain
|
||||
{onEmailDomain}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'schedule'}
|
||||
<SchedulePanel
|
||||
@@ -153,6 +156,16 @@
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'email'}
|
||||
<EmailTriggerPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
{customLabel}
|
||||
{onEmailDomain}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'poll'}
|
||||
<ScheduledPollPanel />
|
||||
{:else if selectedTrigger.type === 'cli'}
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base32 } from 'rfc4648'
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../triggers/CaptureSection.svelte'
|
||||
import ClipboardPanel from '../../details/ClipboardPanel.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
interface Props {
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="email"
|
||||
captureType="default_email"
|
||||
disabled={false}
|
||||
{captureInfo}
|
||||
{captureLoading}
|
||||
+6
-13
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
@@ -9,20 +7,15 @@
|
||||
import { SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON } from '$lib/consts'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import ClipboardPanel from '../../details/ClipboardPanel.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { base32 } from 'rfc4648'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
|
||||
let requestType: 'hash' | 'path' = $state('path')
|
||||
|
||||
function emailAddress(
|
||||
requestType: 'hash' | 'path',
|
||||
path: string,
|
||||
hash: string | undefined,
|
||||
isFlow: boolean,
|
||||
token: string
|
||||
) {
|
||||
function emailAddress() {
|
||||
const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.')
|
||||
const plainPrefix = `${$workspaceStore}+${
|
||||
(requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
|
||||
@@ -40,7 +33,7 @@
|
||||
isFlow?: boolean
|
||||
hash?: string | undefined
|
||||
path: string
|
||||
userSettings: any
|
||||
userSettings: UserSettings
|
||||
emailDomain?: string | null
|
||||
email?: string
|
||||
}
|
||||
@@ -55,8 +48,8 @@
|
||||
email = $bindable('')
|
||||
}: Props = $props()
|
||||
|
||||
run(() => {
|
||||
email = emailAddress(requestType, path, hash, isFlow, token)
|
||||
$effect(() => {
|
||||
email = emailAddress()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import UserSettings from '../../UserSettings.svelte'
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../../HighlightTheme.svelte'
|
||||
import Alert from '../../common/alert/Alert.svelte'
|
||||
import Skeleton from '../../common/skeleton/Skeleton.svelte'
|
||||
import TriggerTokens from '../TriggerTokens.svelte'
|
||||
import Description from '../../Description.svelte'
|
||||
import Section from '../../Section.svelte'
|
||||
import DefaultEmailConfigSection from './DefaultEmailConfigSection.svelte'
|
||||
import { getEmailDomain } from './utils'
|
||||
|
||||
let userSettings: UserSettings | undefined = $state(undefined)
|
||||
interface Props {
|
||||
token: string
|
||||
scopes?: string[]
|
||||
isFlow?: boolean
|
||||
hash?: string | undefined
|
||||
path: string
|
||||
onEmailDomain: (domain: string) => void
|
||||
}
|
||||
|
||||
let {
|
||||
token = $bindable(),
|
||||
scopes = [],
|
||||
isFlow = false,
|
||||
hash = undefined,
|
||||
path,
|
||||
onEmailDomain
|
||||
}: Props = $props()
|
||||
|
||||
let emailDomain: string | null = $state(null)
|
||||
let triggerTokens: TriggerTokens | undefined = $state(undefined)
|
||||
|
||||
let loading = $state(true)
|
||||
|
||||
getEmailDomain().then((domain) => {
|
||||
emailDomain = domain
|
||||
loading = false
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (emailDomain) {
|
||||
onEmailDomain(emailDomain)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<UserSettings
|
||||
bind:this={userSettings}
|
||||
on:tokenCreated={(e) => {
|
||||
token = e.detail
|
||||
triggerTokens?.listTokens()
|
||||
}}
|
||||
newTokenWorkspace={$workspaceStore}
|
||||
newTokenLabel={`email-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<Section label="Default email trigger" class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/advanced/email_triggers">
|
||||
Default email trigger is a partially fixed email address that can be used to trigger a script or
|
||||
flow. The email address is composed of the encoded workspace and script or flow path as well as
|
||||
the token.
|
||||
</Description>
|
||||
{#if loading}
|
||||
<Skeleton layout={[[18]]} />
|
||||
{:else}
|
||||
{#if emailDomain}
|
||||
<DefaultEmailConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" type="warning">
|
||||
Ask an instance superadmin to setup the instance for email triggering (<a
|
||||
target="_blank"
|
||||
href="https://windmill.dev/docs/advanced/email_triggers">docs</a
|
||||
>) and to set the email domain in the instance settings.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="Community Edition limitations" type="warning" size="xs">
|
||||
Email triggers on Windmill Community Edition are limited to 100 emails per day.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<TriggerTokens bind:this={triggerTokens} {isFlow} {path} labelPrefix="email" />
|
||||
{/if}
|
||||
</Section>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
// import { page } from '$app/stores'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
|
||||
interface Props {
|
||||
local_part: string | undefined
|
||||
emailDomain: string | null
|
||||
captureInfo?: CaptureInfo | undefined
|
||||
isValid?: boolean | undefined
|
||||
hasPreprocessor?: boolean
|
||||
isFlow?: boolean
|
||||
captureLoading?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
local_part,
|
||||
emailDomain = null,
|
||||
captureInfo = undefined,
|
||||
isValid = undefined,
|
||||
hasPreprocessor = false,
|
||||
isFlow = false,
|
||||
captureLoading = false
|
||||
}: Props = $props()
|
||||
|
||||
let captureEmail = $derived(`capture+${$workspaceStore}-${local_part}@${emailDomain}`)
|
||||
</script>
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="email"
|
||||
disabled={isValid === false}
|
||||
{captureInfo}
|
||||
{captureLoading}
|
||||
on:captureToggle
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
{hasPreprocessor}
|
||||
{isFlow}
|
||||
>
|
||||
{#snippet description()}
|
||||
{#if captureInfo.active}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Send an email to the test address below to simulate an email trigger.
|
||||
</p>
|
||||
{:else}
|
||||
<p in:fade={{ duration: 100, delay: 50 }} out:fade={{ duration: 50 }}>
|
||||
Start capturing to listen to email events on this test address.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
<Label label="Test email address" disabled={!captureInfo.active}>
|
||||
<ClipboardPanel content={captureEmail} disabled={!captureInfo.active} />
|
||||
</Label>
|
||||
</CaptureSection>
|
||||
{/if}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte'
|
||||
import EmailTriggerEditorInner from './EmailTriggerEditorInner.svelte'
|
||||
import type { EditEmailTrigger, EmailTrigger } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
onUpdate?: (cfg?: Record<string, any>) => void
|
||||
customSaveBehavior?: (cfg: EmailTrigger | EditEmailTrigger) => void
|
||||
}
|
||||
|
||||
let { onUpdate = undefined, customSaveBehavior }: Props = $props()
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
open = true
|
||||
await tick()
|
||||
drawer?.openEdit(ePath, isFlow)
|
||||
}
|
||||
|
||||
export async function openNew(
|
||||
is_flow: boolean,
|
||||
initial_script_path?: string,
|
||||
defaultValues?: Record<string, any>
|
||||
) {
|
||||
open = true
|
||||
await tick()
|
||||
drawer?.openNew(is_flow, initial_script_path, defaultValues)
|
||||
}
|
||||
|
||||
let drawer: EmailTriggerEditorInner | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<EmailTriggerEditorInner {customSaveBehavior} {onUpdate} bind:this={drawer} />
|
||||
{/if}
|
||||
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
// import { page } from '$app/stores'
|
||||
import { getEmailAddress, getEmailDomain } from './utils'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { EmailTriggerService } from '$lib/gen'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
interface Props {
|
||||
initialTriggerPath?: string | undefined
|
||||
dirtyLocalPart?: boolean
|
||||
local_part: string | undefined
|
||||
can_write?: boolean
|
||||
headless?: boolean
|
||||
workspaced_local_part?: boolean
|
||||
isValid?: boolean
|
||||
isDraftOnly?: boolean
|
||||
showTestingBadge?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
initialTriggerPath = undefined,
|
||||
dirtyLocalPart = $bindable(false),
|
||||
local_part = $bindable(),
|
||||
can_write = false,
|
||||
headless = false,
|
||||
workspaced_local_part = $bindable(false),
|
||||
isValid = $bindable(false),
|
||||
isDraftOnly = true,
|
||||
showTestingBadge = false
|
||||
}: Props = $props()
|
||||
|
||||
let validateTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
let addressError: string = $state('')
|
||||
async function validateEmailAddress(
|
||||
localPart: string | undefined,
|
||||
workspaced_local_part: boolean
|
||||
): Promise<void> {
|
||||
if (validateTimeout) {
|
||||
clearTimeout(validateTimeout)
|
||||
}
|
||||
validateTimeout = setTimeout(async () => {
|
||||
if (!localPart || !/^[a-z0-9._]{1,64}$/.test(localPart)) {
|
||||
addressError =
|
||||
'Local part not valid, only accepts lowercase alphanumeric characters, dots and underscores, and must be between 1 and 64 characters'
|
||||
} else if (await emailTriggerExists(localPart, workspaced_local_part)) {
|
||||
addressError = 'Email address already taken'
|
||||
} else {
|
||||
addressError = ''
|
||||
}
|
||||
validateTimeout = undefined
|
||||
}, 500)
|
||||
}
|
||||
async function emailTriggerExists(local_part: string, workspaced_local_part: boolean) {
|
||||
return await EmailTriggerService.existsEmailLocalPart({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
local_part,
|
||||
trigger_path: initialTriggerPath,
|
||||
workspaced_local_part: workspaced_local_part
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$effect.pre(() => {
|
||||
;[local_part, workspaced_local_part]
|
||||
untrack(() => {
|
||||
validateEmailAddress(local_part, workspaced_local_part)
|
||||
})
|
||||
})
|
||||
|
||||
$effect.pre(() => {
|
||||
isValid = addressError === ''
|
||||
})
|
||||
|
||||
let emailDomain: string | null = $state(null)
|
||||
getEmailDomain().then((domain) => {
|
||||
emailDomain = domain
|
||||
})
|
||||
|
||||
let fullEmailAddress = $derived(
|
||||
getEmailAddress(local_part, workspaced_local_part, $workspaceStore ?? '', emailDomain ?? '')
|
||||
)
|
||||
|
||||
$effect.pre(() => {
|
||||
local_part === undefined && (local_part = '')
|
||||
})
|
||||
|
||||
let userIsAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
let userCanEditConfig = $derived(userIsAdmin || isDraftOnly) // User can edit config if they are admin or if the trigger is a draft which will not be saved
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Section label="Email" {headless}>
|
||||
{#snippet header()}
|
||||
{#if showTestingBadge}
|
||||
<TestingBadge />
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#if !userCanEditConfig && isDraftOnly}
|
||||
<Alert type="info" title="Admin only" collapsible size="xs">
|
||||
Email triggers can only be edited by workspace admins
|
||||
</Alert>
|
||||
<div class="my-2"></div>
|
||||
{/if}
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<label class="block grow w-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-secondary text-sm flex items-center gap-1 w-full justify-between">
|
||||
<div>
|
||||
Local part
|
||||
<Required required={true} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
bind:value={local_part}
|
||||
disabled={!userCanEditConfig || !can_write}
|
||||
class={addressError === ''
|
||||
? ''
|
||||
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
|
||||
oninput={() => {
|
||||
dirtyLocalPart = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<ClipboardPanel content={fullEmailAddress} />
|
||||
|
||||
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5"
|
||||
>{dirtyLocalPart ? addressError : ''}</div
|
||||
>
|
||||
{#if !isCloudHosted()}
|
||||
<div class="mt-1">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={workspaced_local_part}
|
||||
disabled={!can_write || !userCanEditConfig}
|
||||
on:change={() => {
|
||||
workspaced_local_part = !workspaced_local_part
|
||||
dirtyLocalPart = true
|
||||
}}
|
||||
options={{
|
||||
right: 'Prefix with workspace',
|
||||
rightTooltip:
|
||||
'Prefixes the email address with the workspace ID (e.g., ${workspace_id}-${local_part}@). Note: deploying the email trigger to another workspace updates the email address workspace prefix accordingly.',
|
||||
rightDocumentationLink:
|
||||
'https://www.windmill.dev/docs/advanced/email_triggers#workspace-prefix'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
@@ -0,0 +1,384 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import {
|
||||
EmailTriggerService,
|
||||
type ErrorHandler,
|
||||
type EmailTrigger,
|
||||
type NewEmailTrigger,
|
||||
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'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import EmailTriggerEditorConfigSection from './EmailTriggerEditorConfigSection.svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import { getHandlerType, handleConfigChange } from '../utils'
|
||||
import { untrack } from 'svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import { saveEmailTriggerFromCfg } from './utils'
|
||||
|
||||
let {
|
||||
useDrawer = true,
|
||||
hideTarget = false,
|
||||
description = undefined,
|
||||
isEditor = false,
|
||||
customLabel = undefined,
|
||||
allowDraft = false,
|
||||
isDeployed = false,
|
||||
onConfigChange = undefined,
|
||||
onCaptureConfigChange = undefined,
|
||||
onUpdate = undefined,
|
||||
onDelete = undefined,
|
||||
onReset = undefined,
|
||||
trigger = undefined,
|
||||
customSaveBehavior = undefined
|
||||
} = $props()
|
||||
|
||||
// Form data state
|
||||
let initialPath = $state('')
|
||||
let edit = $state(true)
|
||||
let itemKind = $state<'flow' | 'script'>('script')
|
||||
let is_flow = $state(false)
|
||||
let script_path = $state('')
|
||||
let initialScriptPath = $state('')
|
||||
let fixedScriptPath = $state('')
|
||||
let path = $state('')
|
||||
let pathError = $state('')
|
||||
let isValid = $state(false)
|
||||
let dirtyLocalPart = $state(false)
|
||||
let dirtyPath = $state(false)
|
||||
let local_part = $state('')
|
||||
let workspaced_local_part = $state(false)
|
||||
let drawerLoading = $state(true)
|
||||
let showLoader = $state(false)
|
||||
let can_write = $state(true)
|
||||
let extraPerms = $state<Record<string, boolean> | undefined>(undefined)
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let retry: Retry | undefined = $state()
|
||||
// Component references
|
||||
let drawer = $state<Drawer | undefined>(undefined)
|
||||
let initialConfig: NewEmailTrigger | undefined = undefined
|
||||
let deploymentLoading = $state(false)
|
||||
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 captureConfig = $derived.by(isEditor ? getCaptureConfig : () => ({}))
|
||||
const saveDisabled = $derived(
|
||||
drawerLoading || !can_write || pathError != '' || !isValid || emptyString(script_path)
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
is_flow = itemKind === 'flow'
|
||||
})
|
||||
|
||||
export async function openEdit(
|
||||
ePath: string,
|
||||
isFlow: boolean,
|
||||
defaultConfig?: Partial<NewEmailTrigger>
|
||||
) {
|
||||
drawerLoading = true
|
||||
let loader = setTimeout(() => {
|
||||
showLoader = true
|
||||
}, 100) // if loading takes less than 100ms, we don't show the loader
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
initialPath = ePath
|
||||
path = ePath
|
||||
itemKind = isFlow ? 'flow' : 'script'
|
||||
edit = true
|
||||
dirtyPath = false
|
||||
dirtyLocalPart = false
|
||||
await loadTrigger(defaultConfig)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load email trigger: ${err}`, true)
|
||||
} finally {
|
||||
if (!defaultConfig) {
|
||||
// If the email trigger is loaded from the backend, we to set the initial config
|
||||
initialConfig = structuredClone($state.snapshot(getEmailTriggerConfig()))
|
||||
}
|
||||
clearTimeout(loader)
|
||||
drawerLoading = false
|
||||
showLoader = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function openNew(
|
||||
nis_flow: boolean,
|
||||
fixedScriptPath_?: string,
|
||||
defaultValues?: Partial<EmailTrigger>
|
||||
) {
|
||||
drawerLoading = true
|
||||
let loader = setTimeout(() => {
|
||||
showLoader = true
|
||||
}, 100) // if loading takes less than 100ms, we don't show the loader
|
||||
try {
|
||||
drawer?.openDrawer()
|
||||
is_flow = defaultValues?.is_flow ?? nis_flow
|
||||
edit = false
|
||||
itemKind = nis_flow ? 'flow' : 'script'
|
||||
local_part = defaultValues?.local_part ?? ''
|
||||
dirtyLocalPart = false
|
||||
initialScriptPath = ''
|
||||
fixedScriptPath = fixedScriptPath_ ?? ''
|
||||
script_path = fixedScriptPath
|
||||
path = defaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
dirtyPath = false
|
||||
|
||||
workspaced_local_part = defaultValues?.workspaced_local_part ?? false
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
} finally {
|
||||
clearTimeout(loader)
|
||||
drawerLoading = false
|
||||
showLoader = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadTriggerConfig(cfg?: Partial<EmailTrigger>): void {
|
||||
script_path = cfg?.script_path ?? ''
|
||||
initialScriptPath = cfg?.script_path ?? ''
|
||||
is_flow = cfg?.is_flow ?? false
|
||||
path = cfg?.path ?? ''
|
||||
local_part = cfg?.local_part ?? ''
|
||||
workspaced_local_part = cfg?.workspaced_local_part ?? false
|
||||
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
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
}
|
||||
|
||||
async function loadTrigger(defaultConfig?: Partial<EmailTrigger>): Promise<void> {
|
||||
if (defaultConfig) {
|
||||
loadTriggerConfig(defaultConfig)
|
||||
return
|
||||
} else {
|
||||
const s = await EmailTriggerService.getEmailTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
|
||||
loadTriggerConfig(s)
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerScript(): Promise<void> {
|
||||
if (customSaveBehavior) {
|
||||
customSaveBehavior(routeConfig)
|
||||
drawer?.closeDrawer()
|
||||
} else {
|
||||
deploymentLoading = true
|
||||
const saveCfg = routeConfig
|
||||
const isSaved = await saveEmailTriggerFromCfg(
|
||||
initialPath,
|
||||
saveCfg,
|
||||
edit,
|
||||
$workspaceStore!,
|
||||
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
|
||||
usedTriggerKinds
|
||||
)
|
||||
if (isSaved) {
|
||||
onUpdate(saveCfg.path)
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
deploymentLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function getEmailTriggerConfig(): NewEmailTrigger {
|
||||
const nCfg = {
|
||||
script_path,
|
||||
is_flow,
|
||||
path,
|
||||
local_part,
|
||||
workspaced_local_part,
|
||||
extra_perms: extraPerms,
|
||||
error_handler_path,
|
||||
error_handler_args,
|
||||
retry
|
||||
}
|
||||
|
||||
return nCfg
|
||||
}
|
||||
|
||||
// Update config for captures
|
||||
function getCaptureConfig() {
|
||||
const newCaptureConfig = {
|
||||
local_part: routeConfig.local_part,
|
||||
path: routeConfig.path
|
||||
}
|
||||
//
|
||||
return newCaptureConfig
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const args = [captureConfig, isValid] as const
|
||||
untrack(() => onCaptureConfigChange?.(...args))
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!drawerLoading) {
|
||||
handleConfigChange(routeConfig, initialConfig, saveDisabled, edit, onConfigChange)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#snippet config()}
|
||||
{#if drawerLoading}
|
||||
{#if showLoader}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col gap-12">
|
||||
<Section label="Metadata">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:dirty={dirtyPath}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
checkInitialPathExistence={!edit}
|
||||
namePlaceholder="email_trigger"
|
||||
kind="email_trigger"
|
||||
hideUser
|
||||
disableEditing={!can_write}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{#if !hideTarget}
|
||||
<Section label="Target">
|
||||
<p class="text-xs mt-3 mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered<Required required={true} />
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row mb-2">
|
||||
<ScriptPicker
|
||||
disabled={fixedScriptPath != '' || !can_write}
|
||||
initialPath={fixedScriptPath || initialScriptPath}
|
||||
kinds={['script']}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
clearable
|
||||
/>
|
||||
|
||||
{#if emptyString(script_path)}
|
||||
<Button
|
||||
btnClasses="ml-4 mt-2"
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow' ? '/flows/add?hub=72' : '/scripts/add?hub=hub%2F19813'}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<EmailTriggerEditorConfigSection
|
||||
initialTriggerPath={initialPath}
|
||||
bind:local_part
|
||||
bind:isValid
|
||||
bind:workspaced_local_part
|
||||
bind:dirtyLocalPart
|
||||
{can_write}
|
||||
showTestingBadge={isEditor}
|
||||
isDraftOnly={trigger ? trigger.isDraft : false}
|
||||
/>
|
||||
|
||||
<Section label="Advanced" collapsable>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="min-h-96">
|
||||
<Tabs bind:selected={optionTabSelected}>
|
||||
<Tab value="error_handler">Error Handler</Tab>
|
||||
<Tab value="retries">Retries</Tab>
|
||||
</Tabs>
|
||||
<div class="mt-4">
|
||||
<TriggerRetriesAndErrorHandler
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet saveButton()}
|
||||
{#if !drawerLoading}
|
||||
<TriggerEditorToolbar
|
||||
{trigger}
|
||||
permissions={drawerLoading || !can_write ? 'none' : can_write && isAdmin ? 'create' : 'write'}
|
||||
{saveDisabled}
|
||||
enabled={undefined}
|
||||
{allowDraft}
|
||||
{edit}
|
||||
isLoading={deploymentLoading}
|
||||
onUpdate={triggerScript}
|
||||
{onReset}
|
||||
{onDelete}
|
||||
{isDeployed}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if useDrawer}
|
||||
<Drawer size="700px" bind:this={drawer}>
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit email trigger ${initialPath}`
|
||||
: `Email trigger ${initialPath}`
|
||||
: 'New email trigger'}
|
||||
on:close={() => drawer?.closeDrawer()}
|
||||
>
|
||||
{#snippet actions()}
|
||||
{@render saveButton()}
|
||||
{/snippet}
|
||||
{@render config()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
<Section label={!customLabel ? 'Email trigger' : ''} headerClass="grow min-w-0 h-[30px]">
|
||||
{#snippet header()}
|
||||
{#if customLabel}
|
||||
{@render customLabel()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
{@render saveButton()}
|
||||
{/snippet}
|
||||
{#if description}
|
||||
{@render description()}
|
||||
{/if}
|
||||
{@render config()}
|
||||
</Section>
|
||||
{/if}
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import EmailTriggerEditorInner from './EmailTriggerEditorInner.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { enterpriseLicense, userStore } from '$lib/stores'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { onMount, type Snippet } from 'svelte'
|
||||
import { getEmailDomain } from './utils'
|
||||
import type { Trigger } from '../utils'
|
||||
|
||||
let emailTriggerEditor = $state<EmailTriggerEditorInner | null>(null)
|
||||
|
||||
interface Props {
|
||||
selectedTrigger: Trigger
|
||||
isFlow: boolean
|
||||
path: string
|
||||
defaultValues?: Record<string, any>
|
||||
isEditor?: boolean
|
||||
customLabel: Snippet
|
||||
onEmailDomain: (domain: string) => void
|
||||
}
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
defaultValues = undefined,
|
||||
isEditor = false,
|
||||
customLabel,
|
||||
onEmailDomain,
|
||||
...restProps
|
||||
}: Props = $props()
|
||||
|
||||
async function openEmailTriggerEditor(isFlow: boolean, isDraft: boolean) {
|
||||
if (isDraft) {
|
||||
emailTriggerEditor?.openNew(isFlow, path, defaultValues)
|
||||
} else {
|
||||
emailTriggerEditor?.openEdit(selectedTrigger.path ?? '', isFlow, defaultValues)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (emailTriggerEditor) {
|
||||
openEmailTriggerEditor(isFlow, selectedTrigger.isDraft ?? false)
|
||||
}
|
||||
})
|
||||
|
||||
let emailDomain: string | null = $state(null)
|
||||
|
||||
getEmailDomain().then((domain) => {
|
||||
emailDomain = domain
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (emailDomain) {
|
||||
onEmailDomain(emailDomain)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<EmailTriggerEditorInner
|
||||
useDrawer={false}
|
||||
bind:this={emailTriggerEditor}
|
||||
hideTarget
|
||||
{isEditor}
|
||||
{customLabel}
|
||||
trigger={selectedTrigger}
|
||||
allowDraft
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet description()}
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<Description link="https://www.windmill.dev/docs/advanced/email_triggers">
|
||||
Email triggers execute scripts and flows when emails are sent to specific addresses. Each
|
||||
trigger can be configured with a specific local part.
|
||||
</Description>
|
||||
|
||||
{#if !$userStore?.is_admin && !$userStore?.is_super_admin && selectedTrigger.isDraft}
|
||||
<Alert title="Only workspace admins can create email triggers" type="info" size="xs" />
|
||||
{/if}
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="Community Edition limitations" type="warning" size="xs">
|
||||
Email triggers on Windmill Community Edition are limited to 100 emails per day.
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</EmailTriggerEditorInner>
|
||||
@@ -0,0 +1,68 @@
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { type NewEmailTrigger, EmailTriggerService, SettingService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export function getEmailAddress(
|
||||
local_part: string | undefined,
|
||||
workspaced_local_part: boolean,
|
||||
workspace_id: string,
|
||||
emailDomain: string
|
||||
) {
|
||||
return `${isCloudHosted() || workspaced_local_part ? workspace_id + '-' : ''}${local_part ?? ''}@${emailDomain}`
|
||||
}
|
||||
|
||||
export async function saveEmailTriggerFromCfg(
|
||||
initialPath: string,
|
||||
routeCfg: 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
|
||||
}
|
||||
try {
|
||||
if (edit) {
|
||||
await EmailTriggerService.updateEmailTrigger({
|
||||
workspace: workspace,
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
...requestBody,
|
||||
local_part: isAdmin || !edit ? routeCfg.local_part : undefined
|
||||
}
|
||||
})
|
||||
sendUserToast(`Route ${routeCfg.path} updated`)
|
||||
} else {
|
||||
await EmailTriggerService.createEmailTrigger({
|
||||
workspace: workspace,
|
||||
requestBody: requestBody
|
||||
})
|
||||
sendUserToast(`Route ${routeCfg.path} created`)
|
||||
}
|
||||
if (!get(usedTriggerKinds).includes('email')) {
|
||||
usedTriggerKinds.update((t) => [...t, 'email'])
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEmailDomain(): Promise<string> {
|
||||
return (
|
||||
((await SettingService.getGlobal({
|
||||
key: 'email_domain'
|
||||
})) as any) ?? 'mail.test.com'
|
||||
)
|
||||
}
|
||||
@@ -166,7 +166,7 @@
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={workspaced_route}
|
||||
disabled={!can_write}
|
||||
disabled={!can_write || !userCanEditConfig}
|
||||
on:change={() => {
|
||||
workspaced_route = !workspaced_route
|
||||
dirtyRoutePath = true
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
type TriggersCount,
|
||||
type HttpTrigger,
|
||||
HttpTriggerService,
|
||||
GcpTriggerService
|
||||
GcpTriggerService,
|
||||
type EmailTrigger,
|
||||
EmailTriggerService
|
||||
} from '$lib/gen'
|
||||
import { getLightConfig, sortTriggers, updateTriggersCount, type Trigger } from './utils'
|
||||
import type { Writable } from 'svelte/store'
|
||||
@@ -430,6 +432,32 @@ export class Triggers {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchEmailTriggers(
|
||||
triggersCountStore: Writable<TriggersCount | undefined>,
|
||||
workspaceId: string | undefined,
|
||||
path: string,
|
||||
isFlow: boolean,
|
||||
user: UserExt | undefined = undefined
|
||||
): Promise<void> {
|
||||
if (!workspaceId) return
|
||||
try {
|
||||
const emailTriggers: EmailTrigger[] = await EmailTriggerService.listEmailTriggers({
|
||||
workspace: workspaceId,
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
const emailCount = this.updateTriggers(emailTriggers, 'email', user)
|
||||
triggersCountStore.update((triggersCount) => {
|
||||
return {
|
||||
...(triggersCount ?? {}),
|
||||
email_count: emailCount
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch email triggers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTriggers(
|
||||
triggersCountStore: Writable<TriggersCount | undefined>,
|
||||
workspaceId: string | undefined,
|
||||
@@ -450,7 +478,8 @@ export class Triggers {
|
||||
this.fetchNatsTriggers(triggersCountStore, workspaceId, path, isFlow, user),
|
||||
this.fetchMqttTriggers(triggersCountStore, workspaceId, path, isFlow, user),
|
||||
this.fetchSqsTriggers(triggersCountStore, workspaceId, path, isFlow, user),
|
||||
this.fetchGcpTriggers(triggersCountStore, workspaceId, path, isFlow, user)
|
||||
this.fetchGcpTriggers(triggersCountStore, workspaceId, path, isFlow, user),
|
||||
this.fetchEmailTriggers(triggersCountStore, workspaceId, path, isFlow, user)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { saveMqttTriggerFromCfg } from './mqtt/utils'
|
||||
import { saveGcpTriggerFromCfg } from './gcp/utils'
|
||||
import type { Triggers } from './triggers.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { saveEmailTriggerFromCfg } from './email/utils'
|
||||
|
||||
export const CLOUD_DISABLED_TRIGGER_TYPES = [
|
||||
'nats',
|
||||
@@ -38,6 +39,7 @@ export const CLOUD_DISABLED_TRIGGER_TYPES = [
|
||||
|
||||
export type TriggerType =
|
||||
| 'webhook'
|
||||
| 'default_email'
|
||||
| 'email'
|
||||
| 'schedule'
|
||||
| 'http'
|
||||
@@ -48,6 +50,7 @@ export type TriggerType =
|
||||
| 'mqtt'
|
||||
| 'sqs'
|
||||
| 'gcp'
|
||||
| 'email'
|
||||
| 'poll'
|
||||
| 'cli'
|
||||
|
||||
@@ -68,6 +71,7 @@ export type Trigger = {
|
||||
export const triggerIconMap = {
|
||||
webhook: Webhook,
|
||||
email: Mail,
|
||||
default_email: Mail,
|
||||
schedule: Calendar,
|
||||
http: Route,
|
||||
websocket: Unplug,
|
||||
@@ -92,6 +96,7 @@ export function triggerTypeToCaptureKind(triggerType: TriggerType): CaptureTrigg
|
||||
const capturableTriggerTypes: TriggerType[] = [
|
||||
'webhook',
|
||||
'email',
|
||||
'default_email',
|
||||
'http',
|
||||
'websocket',
|
||||
'postgres',
|
||||
@@ -120,7 +125,7 @@ export function updateTriggersCount(
|
||||
// Map trigger types to their corresponding count property names
|
||||
const countPropertyMap: Record<TriggerType, string | undefined> = {
|
||||
webhook: undefined,
|
||||
email: undefined,
|
||||
default_email: undefined,
|
||||
schedule: 'schedule_count',
|
||||
http: 'http_routes_count',
|
||||
websocket: 'websocket_count',
|
||||
@@ -130,6 +135,7 @@ export function updateTriggersCount(
|
||||
mqtt: 'mqtt_count',
|
||||
sqs: 'sqs_count',
|
||||
gcp: 'gcp_count',
|
||||
email: 'email_count',
|
||||
poll: undefined,
|
||||
cli: undefined
|
||||
}
|
||||
@@ -178,6 +184,8 @@ export function triggerKindToTriggerType(kind: TriggerKind): TriggerType | undef
|
||||
return 'webhook'
|
||||
case 'emails':
|
||||
return 'email'
|
||||
case 'default_emails':
|
||||
return 'default_email'
|
||||
case 'schedules':
|
||||
return 'schedule'
|
||||
case 'routes':
|
||||
@@ -225,7 +233,7 @@ export async function deployTriggers(
|
||||
// Map of trigger types to their save functions
|
||||
const triggerSaveFunctions: Record<TriggerType, Function | undefined> = {
|
||||
webhook: undefined,
|
||||
email: undefined,
|
||||
default_email: undefined,
|
||||
schedule: (trigger: Trigger) => {
|
||||
if (trigger.isPrimary && initialPath) {
|
||||
trigger.draftConfig = {
|
||||
@@ -301,6 +309,15 @@ export async function deployTriggers(
|
||||
workspaceId,
|
||||
usedTriggerKinds
|
||||
),
|
||||
email: (trigger: Trigger) =>
|
||||
saveEmailTriggerFromCfg(
|
||||
trigger.path ?? trigger.draftConfig?.path ?? '',
|
||||
trigger.draftConfig ?? {},
|
||||
!trigger.isDraft,
|
||||
workspaceId,
|
||||
isAdmin,
|
||||
usedTriggerKinds
|
||||
),
|
||||
poll: undefined,
|
||||
cli: undefined
|
||||
}
|
||||
@@ -398,6 +415,8 @@ export function getLightConfig(
|
||||
return { queue_url: trigger.queue_url }
|
||||
} else if (triggerType === 'gcp') {
|
||||
return { gcp_resource_path: trigger.gcp_resource_path, topic: trigger.topic }
|
||||
} else if (triggerType === 'email') {
|
||||
return { local_part: trigger.local_part }
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
@@ -409,7 +428,7 @@ export function getTriggerLabel(trigger: Trigger): string {
|
||||
|
||||
if (type === 'webhook') {
|
||||
return 'Webhook'
|
||||
} else if (type === 'email') {
|
||||
} else if (type === 'default_email') {
|
||||
return 'Email'
|
||||
} else if (type === 'cli') {
|
||||
return 'CLI'
|
||||
@@ -430,6 +449,8 @@ export function getTriggerLabel(trigger: Trigger): string {
|
||||
return `${config?.gcp_resource_path} - ${config?.topic}`
|
||||
} else if (type === 'websocket' && config?.url) {
|
||||
return `${config?.url}`
|
||||
} else if (type === 'email' && config?.local_part) {
|
||||
return `${config?.local_part}`
|
||||
} else if (isDraft && draftConfig?.path) {
|
||||
return `${draftConfig?.path}`
|
||||
} else if (isDraft) {
|
||||
@@ -443,7 +464,7 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] {
|
||||
const triggerTypeOrder = [
|
||||
'webhook',
|
||||
'cli',
|
||||
'email',
|
||||
'default_email',
|
||||
'poll',
|
||||
'schedule',
|
||||
'http',
|
||||
@@ -453,7 +474,8 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] {
|
||||
'nats',
|
||||
'mqtt',
|
||||
'sqs',
|
||||
'gcp'
|
||||
'gcp',
|
||||
'email'
|
||||
]
|
||||
|
||||
return triggers.sort((a, b) => {
|
||||
|
||||
@@ -201,7 +201,8 @@
|
||||
nats_used,
|
||||
sqs_used,
|
||||
mqtt_used,
|
||||
gcp_used
|
||||
gcp_used,
|
||||
email_used
|
||||
} = await WorkspaceService.getUsedTriggers({
|
||||
workspace: $workspaceStore ?? ''
|
||||
})
|
||||
@@ -229,6 +230,9 @@
|
||||
if (gcp_used) {
|
||||
usedKinds.push('gcp')
|
||||
}
|
||||
if (email_used) {
|
||||
usedKinds.push('email')
|
||||
}
|
||||
$usedTriggerKinds = usedKinds
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'Email triggers' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import {
|
||||
EmailTriggerService,
|
||||
WorkspaceService,
|
||||
type EmailTrigger,
|
||||
type WorkspaceDeployUISettings
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
canWrite,
|
||||
copyToClipboard,
|
||||
displayDate,
|
||||
getLocalSetting,
|
||||
storeLocalSetting,
|
||||
removeTriggerKindIfUnused,
|
||||
sendUserToast
|
||||
} from '$lib/utils'
|
||||
import { base } from '$app/paths'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import Dropdown from '$lib/components/DropdownV2.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import {
|
||||
userStore,
|
||||
workspaceStore,
|
||||
userWorkspaces,
|
||||
enterpriseLicense,
|
||||
usedTriggerKinds
|
||||
} from '$lib/stores'
|
||||
import { Mail, Code, Eye, Pen, Plus, Share, Trash, FileUp, ClipboardCopy } from 'lucide-svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
|
||||
import ListFilters from '$lib/components/home/ListFilters.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
import EmailTriggerEditor from '$lib/components/triggers/email/EmailTriggerEditor.svelte'
|
||||
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
import { ALL_DEPLOYABLE, isDeployable } from '$lib/utils_deployable'
|
||||
import { getEmailAddress, getEmailDomain } from '$lib/components/triggers/email/utils'
|
||||
|
||||
type TriggerW = EmailTrigger & { canWrite: boolean }
|
||||
|
||||
let triggers: TriggerW[] = $state([])
|
||||
let shareModal: ShareModal | undefined = $state()
|
||||
let loading = $state(true)
|
||||
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
|
||||
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
|
||||
let emailDomain: string | null = $state(null)
|
||||
|
||||
async function getDeployUiSettings() {
|
||||
if (!$enterpriseLicense) {
|
||||
deployUiSettings = ALL_DEPLOYABLE
|
||||
return
|
||||
}
|
||||
let settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
deployUiSettings = settings.deploy_ui ?? ALL_DEPLOYABLE
|
||||
}
|
||||
getDeployUiSettings()
|
||||
async function loadTriggers(): Promise<void> {
|
||||
triggers = (await EmailTriggerService.listEmailTriggers({ workspace: $workspaceStore! })).map(
|
||||
(x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
}
|
||||
)
|
||||
$usedTriggerKinds = removeTriggerKindIfUnused(triggers.length, 'emails', $usedTriggerKinds)
|
||||
emailDomain = await getEmailDomain()
|
||||
loading = false
|
||||
}
|
||||
|
||||
run(() => {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadTriggers()
|
||||
}
|
||||
})
|
||||
let emailTriggerEditor: EmailTriggerEditor | undefined = $state()
|
||||
let filteredItems: (TriggerW & { marked?: any })[] | undefined = $state([])
|
||||
let items: typeof filteredItems | undefined = $state([])
|
||||
let preFilteredItems: typeof filteredItems | undefined = $state([])
|
||||
let filter = $state('')
|
||||
let ownerFilter: string | undefined = $state(undefined)
|
||||
let nbDisplayed = $state(15)
|
||||
|
||||
const TRIGGER_PATH_KIND_FILTER_SETTING = 'filter_path_of'
|
||||
const FILTER_USER_FOLDER_SETTING_NAME = 'user_and_folders_only'
|
||||
let selectedFilterKind = $state(
|
||||
(getLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING) as 'trigger' | 'script_flow') ?? 'trigger'
|
||||
)
|
||||
let filterUserFolders = $state(getLocalSetting(FILTER_USER_FOLDER_SETTING_NAME) == 'true')
|
||||
|
||||
run(() => {
|
||||
storeLocalSetting(TRIGGER_PATH_KIND_FILTER_SETTING, selectedFilterKind)
|
||||
})
|
||||
run(() => {
|
||||
storeLocalSetting(FILTER_USER_FOLDER_SETTING_NAME, filterUserFolders ? 'true' : undefined)
|
||||
})
|
||||
|
||||
function filterItemsPathsBaseOnUserFilters(
|
||||
item: TriggerW,
|
||||
selectedFilterKind: 'trigger' | 'script_flow',
|
||||
filterUserFolders: boolean
|
||||
) {
|
||||
if ($workspaceStore == 'admins') return true
|
||||
if (filterUserFolders) {
|
||||
if (selectedFilterKind === 'trigger') {
|
||||
return (
|
||||
!item.path.startsWith('u/') || item.path.startsWith('u/' + $userStore?.username + '/')
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
!item.script_path.startsWith('u/') ||
|
||||
item.script_path.startsWith('u/' + $userStore?.username + '/')
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
run(() => {
|
||||
preFilteredItems =
|
||||
ownerFilter != undefined
|
||||
? selectedFilterKind === 'trigger'
|
||||
? triggers?.filter(
|
||||
(x) =>
|
||||
x.path.startsWith(ownerFilter + '/') &&
|
||||
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
|
||||
)
|
||||
: triggers?.filter(
|
||||
(x) =>
|
||||
x.script_path.startsWith(ownerFilter + '/') &&
|
||||
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
|
||||
)
|
||||
: triggers?.filter((x) =>
|
||||
filterItemsPathsBaseOnUserFilters(x, selectedFilterKind, filterUserFolders)
|
||||
)
|
||||
})
|
||||
|
||||
run(() => {
|
||||
if ($workspaceStore) {
|
||||
ownerFilter = undefined
|
||||
}
|
||||
})
|
||||
|
||||
let owners = $derived(
|
||||
selectedFilterKind === 'trigger'
|
||||
? Array.from(
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
: Array.from(
|
||||
new Set(
|
||||
filteredItems
|
||||
?.filter((x) => x.script_path)
|
||||
.map((x) => x.script_path.split('/').slice(0, 2).join('/')) ?? []
|
||||
)
|
||||
).sort()
|
||||
)
|
||||
|
||||
run(() => {
|
||||
items = filter !== '' ? filteredItems : preFilteredItems
|
||||
})
|
||||
|
||||
function updateQueryFilters(selectedFilterKind, filterUserFolders) {
|
||||
setQuery(
|
||||
new URL(window.location.href),
|
||||
TRIGGER_PATH_KIND_FILTER_SETTING,
|
||||
selectedFilterKind
|
||||
).then(() => {
|
||||
setQuery(
|
||||
new URL(window.location.href),
|
||||
FILTER_USER_FOLDER_SETTING_NAME,
|
||||
String(filterUserFolders)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function loadQueryFilters() {
|
||||
let url = new URL(window.location.href)
|
||||
let queryFilterKind = url.searchParams.get(TRIGGER_PATH_KIND_FILTER_SETTING)
|
||||
let queryFilterUserFolders = url.searchParams.get(FILTER_USER_FOLDER_SETTING_NAME)
|
||||
if (queryFilterKind) {
|
||||
selectedFilterKind = queryFilterKind as 'trigger' | 'script_flow'
|
||||
}
|
||||
if (queryFilterUserFolders) {
|
||||
filterUserFolders = queryFilterUserFolders == 'true'
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadQueryFilters()
|
||||
})
|
||||
|
||||
run(() => {
|
||||
updateQueryFilters(selectedFilterKind, filterUserFolders)
|
||||
})
|
||||
</script>
|
||||
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
<EmailTriggerEditor onUpdate={loadTriggers} bind:this={emailTriggerEditor} />
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={preFilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'}
|
||||
/>
|
||||
|
||||
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.triggers}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
|
||||
<p class="font-bold">Unauthorized</p>
|
||||
<p>Page not available for operators</p>
|
||||
</div>
|
||||
{:else}
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
title="Custom email triggers"
|
||||
tooltip="Every script and flow already has a canonical email trigger attached to it, this is to create additional parametrizable ones."
|
||||
documentationLink="https://www.windmill.dev/docs/advanced/email_triggers"
|
||||
>
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Button
|
||||
size="md"
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => emailTriggerEditor?.openNew(false)}
|
||||
>
|
||||
New email trigger
|
||||
</Button>
|
||||
{/if}
|
||||
</PageHeader>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="w-full pb-4 pt-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search email triggers"
|
||||
bind:value={filter}
|
||||
class="search-item"
|
||||
/>
|
||||
<div class="flex flex-row items-center gap-2 mt-6">
|
||||
<div class="text-sm shrink-0"> Filter by path of </div>
|
||||
<ToggleButtonGroup bind:selected={selectedFilterKind}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton small value="trigger" label="Email" icon={Mail} {item} />
|
||||
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<ListFilters syncQuery bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
|
||||
<div class="flex flex-row items-center justify-end gap-4">
|
||||
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
|
||||
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
|
||||
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={filterUserFolders}
|
||||
options={{ right: `Only u/${$userStore.username} and f/*` }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[6], 0.4]} />
|
||||
{/each}
|
||||
{:else if !triggers?.length}
|
||||
<div class="text-center text-sm text-tertiary 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)}
|
||||
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
|
||||
{@const emailAddress = getEmailAddress(
|
||||
local_part,
|
||||
workspaced_local_part ?? false,
|
||||
workspace_id,
|
||||
emailDomain ?? ''
|
||||
)}
|
||||
|
||||
<div
|
||||
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md flex flex-col"
|
||||
>
|
||||
<div class="w-full flex gap-5 items-center">
|
||||
<RowIcon kind={is_flow ? 'flow' : 'script'} />
|
||||
|
||||
<a
|
||||
href="#{path}"
|
||||
onclick={() => emailTriggerEditor?.openEdit(path, is_flow)}
|
||||
class="min-w-0 grow hover:underline decoration-gray-400"
|
||||
>
|
||||
<div class="text-primary flex-wrap text-left text-md font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
<span class="text-xs">
|
||||
{@html marked}
|
||||
</span>
|
||||
{:else}
|
||||
{emailAddress}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-secondary text-xs truncate text-left font-light">
|
||||
{path}
|
||||
</div>
|
||||
<div class="text-secondary text-xs truncate text-left font-light">
|
||||
runnable: {script_path}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="hidden lg:flex flex-row gap-1 items-center">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<Button
|
||||
on:click={() => copyToClipboard(emailAddress)}
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: ClipboardCopy }}
|
||||
>
|
||||
Copy email address
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => emailTriggerEditor?.openEdit(path, is_flow)}
|
||||
size="xs"
|
||||
startIcon={canWrite
|
||||
? { icon: Pen }
|
||||
: {
|
||||
icon: Eye
|
||||
}}
|
||||
color="dark"
|
||||
>
|
||||
{canWrite ? 'Edit' : 'View'}
|
||||
</Button>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
|
||||
icon: Eye,
|
||||
action: () => {
|
||||
goto(href)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
type: 'delete',
|
||||
icon: Trash,
|
||||
disabled:
|
||||
!canWrite || !($userStore?.is_admin || $userStore?.is_super_admin),
|
||||
action: async () => {
|
||||
try {
|
||||
await EmailTriggerService.deleteEmailTrigger({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
sendUserToast(`Successfully deleted Email trigger: ${path}`)
|
||||
loadTriggers()
|
||||
} catch (error) {
|
||||
sendUserToast(error.body || error.message, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Edit' : 'View',
|
||||
icon: canWrite ? Pen : Eye,
|
||||
action: () => {
|
||||
emailTriggerEditor?.openEdit(path, is_flow)
|
||||
}
|
||||
},
|
||||
...(isDeployable('trigger', path, deployUiSettings)
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => {
|
||||
deploymentDrawer?.openDrawer(path, 'trigger', {
|
||||
triggers: {
|
||||
kind: 'routes'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
displayName: 'Audit logs',
|
||||
icon: Eye,
|
||||
href: `${base}/audit_logs?resource=${path}`
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: Share,
|
||||
action: () => {
|
||||
shareModal?.openDrawer(path, 'email_trigger')
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex justify-between items-baseline">
|
||||
<div
|
||||
class="flex flex-wrap text-[0.7em] text-tertiary gap-1 items-center justify-end truncate pr-2"
|
||||
>
|
||||
<div class="truncate">edited by {edited_by}</div>
|
||||
<div class="truncate">at {displayDate(edited_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<NoItemFound />
|
||||
{/if}
|
||||
</div>
|
||||
{#if items && items?.length > 15 && nbDisplayed < items.length}
|
||||
<span class="text-xs"
|
||||
>{nbDisplayed} items out of {items.length}
|
||||
<button class="ml-4" onclick={() => (nbDisplayed += 30)}>load 30 more</button></span
|
||||
>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
{/if}
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModal}
|
||||
on:change={() => {
|
||||
loadTriggers()
|
||||
}}
|
||||
/>
|
||||
@@ -83,7 +83,7 @@
|
||||
const triggersState = $state(
|
||||
new Triggers([
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
{ type: 'default_email', path: '', isDraft: false },
|
||||
{ type: 'cli', path: '', isDraft: false }
|
||||
])
|
||||
)
|
||||
@@ -504,7 +504,7 @@
|
||||
{#if flow?.archived}
|
||||
<Alert type="error" title="Archived">This flow was archived</Alert>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="mb-1">
|
||||
{#if !emptyString(flow?.description)}
|
||||
<GfmMarkdown md={defaultIfEmptyString(flow?.description, 'No description')} />
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
const triggersState = $state(
|
||||
new Triggers([
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
{ type: 'default_email', path: '', isDraft: false },
|
||||
{ type: 'cli', path: '', isDraft: false }
|
||||
])
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user