This commit is contained in:
HugoCasa
2025-09-09 18:13:42 +02:00
parent 2d8e6d768e
commit 62458c9aae
54 changed files with 521 additions and 263 deletions
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow_version\n (workspace_id, path, value, schema, created_by)\n\n SELECT workspace_id, path, value, schema, created_by\n FROM flow_version WHERE path = $1 AND workspace_id = $2\n\n RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "0a026af1b84c3724cadcd162422f0402b908050b06cbc8d8987469217f59dbcf"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND NOT (versions @> ARRAY[$1::bigint])",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "15174732d4b52510d1ea73ce5d6192b7373de62395626a18ad3672b604e23788"
}
@@ -38,7 +38,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -22,7 +22,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -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"
}
@@ -30,7 +30,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -66,7 +67,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -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"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -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"
}
@@ -25,7 +25,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -33,7 +33,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -30,7 +30,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -1,25 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n flow \n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n value = $8,\n schema = $9::text::json,\n edited_by = $10,\n edited_at = now()\n WHERE \n path = $11 AND workspace_id = $12",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Varchar",
"Bool",
"Bool",
"Text",
"Jsonb",
"Text",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "9ee36b224e931f24b4d48b22de0095fa1a373c2f9fdda11ce30fec6dda362d3d"
}
@@ -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"
}
@@ -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"
}
@@ -22,7 +22,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
@@ -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"
}
@@ -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"
}
@@ -22,7 +22,8 @@
"postgres",
"sqs",
"mqtt",
"gcp"
"gcp",
"default_email"
]
}
}
+1 -1
View File
@@ -1 +1 @@
b72d44c2a8a2ad3a1e36c938863a9b02891ac4a2
0f744552e1f0a20194b5042b5a4ed36a0dc6809f
@@ -65,6 +65,4 @@ USING (exists(
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 'runnable_email';
UPDATE TABLE capture SET trigger_kind = 'runnable_email' WHERE trigger_kind = 'email';
UPDATE TABLE capture_config SET trigger_kind = 'runnable_email' WHERE trigger_kind = 'email';
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';
+5 -2
View File
@@ -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
@@ -16199,7 +16202,7 @@ components:
type: number
email_count:
type: number
runnable_email_count:
default_email_count:
type: number
websocket_count:
type: number
@@ -18280,7 +18283,7 @@ components:
CaptureTriggerKind:
type: string
enum:
[webhook, http, websocket, kafka, runnable_email, nats, postgres, sqs, mqtt, gcp, email]
[webhook, http, websocket, kafka, default_email, nats, postgres, sqs, mqtt, gcp, email]
Capture:
type: object
+1 -1
View File
@@ -146,7 +146,7 @@ struct HttpTriggerConfig {
wrap_body: Option<bool>,
}
#[cfg(feature = "http_trigger")]
#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))]
#[derive(Serialize, Deserialize)]
struct EmailTriggerConfig {
local_part: String,
+3 -3
View File
@@ -761,7 +761,7 @@ pub struct TriggersCount {
http_routes_count: i64,
webhook_count: i64,
email_count: i64,
runnable_email_count: i64,
default_email_count: i64,
websocket_count: i64,
kafka_count: i64,
nats_count: i64,
@@ -912,7 +912,7 @@ pub async fn get_triggers_count_internal(
.await?
.unwrap_or(0);
let runnable_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,
@@ -933,7 +933,7 @@ pub async fn get_triggers_count_internal(
schedule_count,
http_routes_count,
webhook_count,
runnable_email_count,
default_email_count,
email_count,
websocket_count,
kafka_count,
+3 -3
View File
@@ -12,7 +12,7 @@ pub enum TriggerKind {
Http,
Websocket,
Kafka,
RunnableEmail,
DefaultEmail,
Email,
Nats,
Mqtt,
@@ -29,7 +29,7 @@ impl TriggerKind {
TriggerKind::Websocket => "websocket".to_string(),
TriggerKind::Kafka => "kafka".to_string(),
TriggerKind::Email => "email".to_string(),
TriggerKind::RunnableEmail => "runnable_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(),
@@ -47,7 +47,7 @@ impl fmt::Display for TriggerKind {
TriggerKind::Websocket => "websocket",
TriggerKind::Kafka => "kafka",
TriggerKind::Email => "email",
TriggerKind::RunnableEmail => "runnable_email",
TriggerKind::DefaultEmail => "default_email",
TriggerKind::Nats => "nats",
TriggerKind::Mqtt => "mqtt",
TriggerKind::Sqs => "sqs",
@@ -615,7 +615,7 @@
new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'runnable_email', path: '', isDraft: false },
{ type: 'default_email', path: '', isDraft: false },
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
],
selectedTriggerIndexFromUrl,
@@ -251,7 +251,7 @@
new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'runnable_email', path: '', isDraft: false },
{ type: 'default_email', path: '', isDraft: false },
...(script.draft_triggers ?? [])
],
undefined,
@@ -1,81 +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 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 RunnableEmailConfigSection from './RunnableEmailConfigSection.svelte'
import { getEmailDomain } from '../triggers/email/utils'
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
getEmailDomain().then((domain) => {
emailDomain = domain
loading = false
})
$: 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="Runnable email" class="flex flex-col gap-4">
<Description link="https://www.windmill.dev/docs/advanced/email_triggers">
Runnable email 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}
<RunnableEmailConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
{:else}
<div>
<Alert title="Runnable emails 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">
Runnable emails on Windmill Community Edition are limited to 100 emails per day.
</Alert>
{/if}
<TriggerTokens bind:this={triggerTokens} {isFlow} {path} labelPrefix="email" />
{/if}
</Section>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Calendar, Mail, Webhook, Unplug, Database, Terminal, MailWarning } from 'lucide-svelte'
import { Calendar, Mail, Webhook, Unplug, Database, Terminal } from 'lucide-svelte'
import { Loader2 } from 'lucide-svelte'
import { type Component, type ComponentType } from 'svelte'
import { Route } from 'lucide-svelte'
@@ -56,7 +56,7 @@
websocket: { icon: Unplug, countKey: 'websocket_count' },
postgres: { icon: Database, countKey: 'postgres_count' },
kafka: { icon: KafkaIcon, countKey: 'kafka_count', disabled: !$enterpriseLicense },
runnable_email: { icon: MailWarning, countKey: 'runnable_email_count' },
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 },
@@ -73,7 +73,7 @@
'websocket',
'postgres',
'kafka',
'runnable_email',
'default_email',
'nats',
'mqtt',
'sqs',
+3 -3
View File
@@ -44,7 +44,7 @@ export function setScheduledPollSchedule(
export type TriggerKind =
| 'webhooks'
| 'emails'
| 'runnable_emails'
| 'default_emails'
| 'schedules'
| 'cli'
| 'routes'
@@ -62,8 +62,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg
return 'webhooks'
case 'email':
return 'emails'
case 'runnable_email':
return 'runnable_emails'
case 'default_email':
return 'default_emails'
case 'http':
return 'routes'
case 'websocket':
@@ -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 RunnableEmailCapture from '../details/RunnableEmailCapture.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'
@@ -239,8 +239,8 @@
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'runnable_email'}
<RunnableEmailCapture
{:else if captureType === 'default_email'}
<DefaultEmailCapture
{path}
{isFlow}
emailDomain={data?.emailDomain}
@@ -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 ?? {}), runnable_email_count: tokens?.length }
$triggersCount = { ...($triggersCount ?? {}), default_email_count: tokens?.length }
} else {
$triggersCount = { ...($triggersCount ?? {}), webhook_count: tokens?.length }
}
@@ -295,7 +295,7 @@
onDeleteDraft={deleteTrigger}
onReset={handleResetDraft}
webhookToken={$triggersCount?.webhook_count}
emailToken={$triggersCount?.runnable_email_count}
emailToken={$triggersCount?.default_email_count}
/>
</div>
{:else}
@@ -369,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 === 'runnable_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 !['runnable_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}
@@ -2,7 +2,7 @@
import RoutesPanel from './http/RoutesPanel.svelte'
import WebhooksPanel from './webhook/WebhooksPanel.svelte'
import EmailTriggerPanel from './email/EmailTriggerPanel.svelte'
import RunnableEmailPanel from '../details/RunnableEmailPanel.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'
@@ -36,6 +36,7 @@
onUpdate?: (path: string) => void
onDelete?: () => void
onReset?: () => void
onEmailDomain: (domain: string) => void
}
let {
@@ -49,6 +50,7 @@
args,
newItem,
schema,
onEmailDomain,
...props
}: Props = $props()
</script>
@@ -72,14 +74,14 @@
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
{newItem}
/>
{:else if selectedTrigger.type === 'runnable_email'}
<RunnableEmailPanel
{: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
@@ -161,6 +163,7 @@
{selectedTrigger}
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
{customLabel}
{onEmailDomain}
{...props}
/>
{:else if selectedTrigger.type === 'poll'}
@@ -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="runnable_email"
captureType="default_email"
disabled={false}
{captureInfo}
{captureLoading}
@@ -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>
@@ -123,7 +116,7 @@
{/key}
{/key}
<Alert title="Runnable email" size="xs">
<Alert title="Email trigger" size="xs">
To trigger the job by email, send an email to the address above. The job will receive two
arguments: `raw_email` containing the raw email as string, and `parsed_email` containing the
parsed email as an object.
@@ -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>
@@ -27,7 +27,7 @@
captureLoading = false
}: Props = $props()
let captureEmail = $derived(`capture+${$workspaceStore}+${local_part}@${emailDomain}`)
let captureEmail = $derived(`capture+${$workspaceStore}-${local_part}@${emailDomain}`)
</script>
{#if captureInfo}
@@ -46,9 +46,9 @@
clearTimeout(validateTimeout)
}
validateTimeout = setTimeout(async () => {
if (!localPart || !/^\w[a-z0-9._]*\w$/.test(localPart)) {
if (!localPart || !/^[a-z0-9._]{1,64}$/.test(localPart)) {
addressError =
'Local part not valid, only accepts lowercase alphanumeric characters, dots and underscores'
'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 {
@@ -145,7 +145,7 @@
<Toggle
size="sm"
checked={workspaced_local_part}
disabled={!can_write}
disabled={!can_write || !userCanEditConfig}
on:change={() => {
workspaced_local_part = !workspaced_local_part
dirtyLocalPart = true
@@ -287,7 +287,7 @@
btnClasses="ml-4 mt-2"
color="dark"
size="xs"
href={itemKind === 'flow' ? '/flows/add?hub=62' : '/scripts/add?hub=hub%2F19669'}
href={itemKind === 'flow' ? '/flows/add?hub=72' : '/scripts/add?hub=hub%2F19813'}
target="_blank">Create from template</Button
>
{/if}
@@ -301,6 +301,7 @@
bind:local_part
bind:isValid
bind:workspaced_local_part
bind:dirtyLocalPart
{can_write}
showTestingBadge={isEditor}
isDraftOnly={trigger ? trigger.isDraft : false}
@@ -1,26 +1,39 @@
<script lang="ts">
import EmailTriggerEditorInner from './EmailTriggerEditorInner.svelte'
import Description from '$lib/components/Description.svelte'
import { userStore } from '$lib/stores'
import { enterpriseLicense, userStore } from '$lib/stores'
import { Alert } from '$lib/components/common'
import { onMount } from 'svelte'
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 = undefined,
customLabel,
onEmailDomain,
...restProps
} = $props()
}: Props = $props()
async function openEmailTriggerEditor(isFlow: boolean, isDraft: boolean) {
if (isDraft) {
emailTriggerEditor?.openNew(isFlow, path, defaultValues)
} else {
emailTriggerEditor?.openEdit(selectedTrigger.path, isFlow, defaultValues)
emailTriggerEditor?.openEdit(selectedTrigger.path ?? '', isFlow, defaultValues)
}
}
@@ -29,6 +42,18 @@
openEmailTriggerEditor(isFlow, selectedTrigger.isDraft ?? false)
}
})
let emailDomain: string | null = $state(null)
getEmailDomain().then((domain) => {
emailDomain = domain
})
$effect(() => {
if (emailDomain) {
onEmailDomain(emailDomain)
}
})
</script>
<EmailTriggerEditorInner
@@ -51,6 +76,12 @@
{#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>
@@ -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
+11 -20
View File
@@ -1,13 +1,4 @@
import {
Webhook,
Mail,
Calendar,
Route,
Unplug,
Database,
Terminal,
MailWarning
} from 'lucide-svelte'
import { Webhook, Mail, Calendar, Route, Unplug, Database, Terminal } from 'lucide-svelte'
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
import NatsIcon from '$lib/components/icons/NatsIcon.svelte'
import MqttIcon from '$lib/components/icons/MqttIcon.svelte'
@@ -48,7 +39,7 @@ export const CLOUD_DISABLED_TRIGGER_TYPES = [
export type TriggerType =
| 'webhook'
| 'runnable_email'
| 'default_email'
| 'email'
| 'schedule'
| 'http'
@@ -80,7 +71,7 @@ export type Trigger = {
export const triggerIconMap = {
webhook: Webhook,
email: Mail,
runnable_email: MailWarning,
default_email: Mail,
schedule: Calendar,
http: Route,
websocket: Unplug,
@@ -105,7 +96,7 @@ export function triggerTypeToCaptureKind(triggerType: TriggerType): CaptureTrigg
const capturableTriggerTypes: TriggerType[] = [
'webhook',
'email',
'runnable_email',
'default_email',
'http',
'websocket',
'postgres',
@@ -134,7 +125,7 @@ export function updateTriggersCount(
// Map trigger types to their corresponding count property names
const countPropertyMap: Record<TriggerType, string | undefined> = {
webhook: undefined,
runnable_email: undefined,
default_email: undefined,
schedule: 'schedule_count',
http: 'http_routes_count',
websocket: 'websocket_count',
@@ -193,8 +184,8 @@ export function triggerKindToTriggerType(kind: TriggerKind): TriggerType | undef
return 'webhook'
case 'emails':
return 'email'
case 'runnable_emails':
return 'runnable_email'
case 'default_emails':
return 'default_email'
case 'schedules':
return 'schedule'
case 'routes':
@@ -242,7 +233,7 @@ export async function deployTriggers(
// Map of trigger types to their save functions
const triggerSaveFunctions: Record<TriggerType, Function | undefined> = {
webhook: undefined,
runnable_email: undefined,
default_email: undefined,
schedule: (trigger: Trigger) => {
if (trigger.isPrimary && initialPath) {
trigger.draftConfig = {
@@ -437,8 +428,8 @@ export function getTriggerLabel(trigger: Trigger): string {
if (type === 'webhook') {
return 'Webhook'
} else if (type === 'runnable_email') {
return 'Runnable email'
} else if (type === 'default_email') {
return 'Email'
} else if (type === 'cli') {
return 'CLI'
} else if (type === 'http' && !emptyString(config?.route_path)) {
@@ -473,7 +464,7 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] {
const triggerTypeOrder = [
'webhook',
'cli',
'runnable_email',
'default_email',
'poll',
'schedule',
'http',
@@ -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
}
@@ -44,7 +44,6 @@
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 { isCloudHosted } from '$lib/cloud'
import { getEmailAddress, getEmailDomain } from '$lib/components/triggers/email/utils'
type TriggerW = EmailTrigger & { canWrite: boolean }
@@ -54,6 +53,7 @@
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) {
@@ -71,6 +71,7 @@
}
)
$usedTriggerKinds = removeTriggerKindIfUnused(triggers.length, 'emails', $usedTriggerKinds)
emailDomain = await getEmailDomain()
loading = false
}
@@ -196,11 +197,6 @@
loadQueryFilters()
})
let emailDomain: string | null = $state(null)
getEmailDomain().then((domain) => {
emailDomain = domain
})
run(() => {
updateQueryFilters(selectedFilterKind, filterUserFolders)
})
@@ -279,6 +275,12 @@
<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
@@ -298,9 +300,7 @@
{@html marked}
</span>
{:else}
{isCloudHosted() || workspaced_local_part
? workspace_id + '/' + local_part
: local_part}
{emailAddress}
{/if}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
@@ -317,15 +317,7 @@
<div class="flex gap-2 items-center justify-end">
<Button
on:click={() =>
copyToClipboard(
getEmailAddress(
local_part,
workspaced_local_part ?? false,
workspace_id,
emailDomain ?? ''
)
)}
on:click={() => copyToClipboard(emailAddress)}
color="dark"
size="xs"
startIcon={{ icon: ClipboardCopy }}
@@ -83,7 +83,7 @@
const triggersState = $state(
new Triggers([
{ type: 'webhook', path: '', isDraft: false },
{ type: 'runnable_email', path: '', isDraft: false },
{ type: 'default_email', path: '', isDraft: false },
{ type: 'cli', path: '', isDraft: false }
])
)
@@ -106,7 +106,7 @@
const triggersState = $state(
new Triggers([
{ type: 'webhook', path: '', isDraft: false },
{ type: 'runnable_email', path: '', isDraft: false },
{ type: 'default_email', path: '', isDraft: false },
{ type: 'cli', path: '', isDraft: false }
])
)