mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
email triggers UI
This commit is contained in:
@@ -63,4 +63,8 @@ CREATE POLICY see_extra_perms_groups_delete ON email_trigger FOR DELETE TO wind
|
||||
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));
|
||||
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';
|
||||
@@ -10814,6 +10814,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 deleted
|
||||
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
|
||||
@@ -11899,6 +12064,7 @@ paths:
|
||||
mqtt_trigger,
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
]
|
||||
responses:
|
||||
"200":
|
||||
@@ -11943,6 +12109,7 @@ paths:
|
||||
mqtt_trigger,
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
@@ -11998,6 +12165,7 @@ paths:
|
||||
mqtt_trigger,
|
||||
gcp_trigger,
|
||||
sqs_trigger,
|
||||
email_trigger,
|
||||
]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
@@ -16031,6 +16199,8 @@ components:
|
||||
type: number
|
||||
email_count:
|
||||
type: number
|
||||
runnable_email_count:
|
||||
type: number
|
||||
websocket_count:
|
||||
type: number
|
||||
postgres_count:
|
||||
@@ -16986,6 +17156,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:
|
||||
@@ -18041,7 +18280,7 @@ components:
|
||||
CaptureTriggerKind:
|
||||
type: string
|
||||
enum:
|
||||
[webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt, gcp]
|
||||
[webhook, http, websocket, kafka, runnable_email, nats, postgres, sqs, mqtt, gcp, email]
|
||||
|
||||
Capture:
|
||||
type: object
|
||||
|
||||
@@ -146,6 +146,12 @@ struct HttpTriggerConfig {
|
||||
wrap_body: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
#[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
|
||||
|
||||
@@ -736,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
|
||||
}
|
||||
|
||||
@@ -751,6 +761,7 @@ pub struct TriggersCount {
|
||||
http_routes_count: i64,
|
||||
webhook_count: i64,
|
||||
email_count: i64,
|
||||
runnable_email_count: i64,
|
||||
websocket_count: i64,
|
||||
kafka_count: i64,
|
||||
nats_count: i64,
|
||||
@@ -871,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 {
|
||||
@@ -889,7 +912,7 @@ pub async fn get_triggers_count_internal(
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let email_count = (if is_flow {
|
||||
let runnable_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,
|
||||
@@ -910,6 +933,7 @@ pub async fn get_triggers_count_internal(
|
||||
schedule_count,
|
||||
http_routes_count,
|
||||
webhook_count,
|
||||
runnable_email_count,
|
||||
email_count,
|
||||
websocket_count,
|
||||
kafka_count,
|
||||
|
||||
@@ -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,
|
||||
RunnableEmail,
|
||||
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::RunnableEmail => "runnable_email".to_string(),
|
||||
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::RunnableEmail => "runnable_email",
|
||||
TriggerKind::Nats => "nats",
|
||||
TriggerKind::Mqtt => "mqtt",
|
||||
TriggerKind::Sqs => "sqs",
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
new Triggers(
|
||||
[
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'email', path: '', isDraft: false },
|
||||
{ type: 'runnable_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: 'runnable_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
-1
@@ -40,7 +40,7 @@
|
||||
|
||||
{#if captureInfo}
|
||||
<CaptureSection
|
||||
captureType="email"
|
||||
captureType="runnable_email"
|
||||
disabled={false}
|
||||
{captureInfo}
|
||||
{captureLoading}
|
||||
+1
-1
@@ -123,7 +123,7 @@
|
||||
{/key}
|
||||
{/key}
|
||||
|
||||
<Alert title="Email trigger" size="xs">
|
||||
<Alert title="Runnable email" 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.
|
||||
+13
-16
@@ -4,13 +4,13 @@
|
||||
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'
|
||||
import RunnableEmailConfigSection from './RunnableEmailConfigSection.svelte'
|
||||
import { getEmailDomain } from '../triggers/email/utils'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
@@ -26,15 +26,11 @@
|
||||
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()
|
||||
getEmailDomain().then((domain) => {
|
||||
emailDomain = domain
|
||||
loading = false
|
||||
})
|
||||
|
||||
$: emailDomain && dispatch('email-domain', emailDomain)
|
||||
</script>
|
||||
@@ -52,19 +48,20 @@
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<Section label="Email trigger" class="flex flex-col gap-4">
|
||||
<Section label="Runnable email" 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.
|
||||
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}
|
||||
<EmailTriggerConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
|
||||
<RunnableEmailConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" type="warning">
|
||||
<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
|
||||
@@ -75,7 +72,7 @@
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert title="Community Edition limitations" type="warning" size="xs">
|
||||
Email triggers on Windmill Community Edition are limited to 100 emails per day.
|
||||
Runnable emails on Windmill Community Edition are limited to 100 emails per day.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Calendar, Mail, Webhook, Unplug, Database, Terminal } from 'lucide-svelte'
|
||||
import { Calendar, Mail, Webhook, Unplug, Database, Terminal, MailWarning } from 'lucide-svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { type Component, type ComponentType } from 'svelte'
|
||||
import { Route } from 'lucide-svelte'
|
||||
@@ -56,6 +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' },
|
||||
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',
|
||||
'runnable_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'
|
||||
| 'runnable_emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
@@ -61,6 +62,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg
|
||||
return 'webhooks'
|
||||
case 'email':
|
||||
return 'emails'
|
||||
case 'runnable_email':
|
||||
return 'runnable_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 RunnableEmailCapture from '../details/RunnableEmailCapture.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 === 'runnable_email'}
|
||||
<RunnableEmailCapture
|
||||
{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}
|
||||
|
||||
@@ -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 ?? {}), runnable_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?.runnable_email_count}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
>
|
||||
{`${webhookToken} token${webhookToken > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
{:else if trigger.type === 'email' && emailToken}
|
||||
{:else if trigger.type === 'runnable_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 !['runnable_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 RunnableEmailPanel from '../details/RunnableEmailPanel.svelte'
|
||||
import SchedulePanel from '$lib/components/SchedulePanel.svelte'
|
||||
import PostgresTriggersPanel from './postgres/PostgresTriggersPanel.svelte'
|
||||
import KafkaTriggerPanel from './kafka/KafkaTriggersPanel.svelte'
|
||||
@@ -71,8 +72,8 @@
|
||||
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
|
||||
{newItem}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'email'}
|
||||
<EmailTriggerPanel
|
||||
{:else if selectedTrigger.type === 'runnable_email'}
|
||||
<RunnableEmailPanel
|
||||
token=""
|
||||
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
|
||||
path={initialPath || fakeInitialPath}
|
||||
@@ -153,6 +154,15 @@
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'email'}
|
||||
<EmailTriggerPanel
|
||||
{isFlow}
|
||||
path={initialPath || fakeInitialPath}
|
||||
{selectedTrigger}
|
||||
defaultValues={selectedTrigger.draftConfig ?? selectedTrigger.captureConfig ?? undefined}
|
||||
{customLabel}
|
||||
{...props}
|
||||
/>
|
||||
{:else if selectedTrigger.type === 'poll'}
|
||||
<ScheduledPollPanel />
|
||||
{:else if selectedTrigger.type === 'cli'}
|
||||
|
||||
@@ -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 || !/^\w[a-z0-9._]*\w$/.test(localPart)) {
|
||||
addressError =
|
||||
'Local part not valid, only accepts lowercase alphanumeric characters, dots and underscores'
|
||||
} 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}
|
||||
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,383 @@
|
||||
<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=62' : '/scripts/add?hub=hub%2F19669'}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<EmailTriggerEditorConfigSection
|
||||
initialTriggerPath={initialPath}
|
||||
bind:local_part
|
||||
bind:isValid
|
||||
bind:workspaced_local_part
|
||||
{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,56 @@
|
||||
<script lang="ts">
|
||||
import EmailTriggerEditorInner from './EmailTriggerEditorInner.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let emailTriggerEditor = $state<EmailTriggerEditorInner | null>(null)
|
||||
let {
|
||||
selectedTrigger,
|
||||
isFlow,
|
||||
path,
|
||||
defaultValues = undefined,
|
||||
isEditor = false,
|
||||
customLabel = undefined,
|
||||
...restProps
|
||||
} = $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)
|
||||
}
|
||||
})
|
||||
</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}
|
||||
</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'
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Webhook, Mail, Calendar, Route, Unplug, Database, Terminal } from 'lucide-svelte'
|
||||
import {
|
||||
Webhook,
|
||||
Mail,
|
||||
Calendar,
|
||||
Route,
|
||||
Unplug,
|
||||
Database,
|
||||
Terminal,
|
||||
MailWarning
|
||||
} 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'
|
||||
@@ -25,6 +34,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 +48,7 @@ export const CLOUD_DISABLED_TRIGGER_TYPES = [
|
||||
|
||||
export type TriggerType =
|
||||
| 'webhook'
|
||||
| 'runnable_email'
|
||||
| 'email'
|
||||
| 'schedule'
|
||||
| 'http'
|
||||
@@ -48,6 +59,7 @@ export type TriggerType =
|
||||
| 'mqtt'
|
||||
| 'sqs'
|
||||
| 'gcp'
|
||||
| 'email'
|
||||
| 'poll'
|
||||
| 'cli'
|
||||
|
||||
@@ -68,6 +80,7 @@ export type Trigger = {
|
||||
export const triggerIconMap = {
|
||||
webhook: Webhook,
|
||||
email: Mail,
|
||||
runnable_email: MailWarning,
|
||||
schedule: Calendar,
|
||||
http: Route,
|
||||
websocket: Unplug,
|
||||
@@ -92,6 +105,7 @@ export function triggerTypeToCaptureKind(triggerType: TriggerType): CaptureTrigg
|
||||
const capturableTriggerTypes: TriggerType[] = [
|
||||
'webhook',
|
||||
'email',
|
||||
'runnable_email',
|
||||
'http',
|
||||
'websocket',
|
||||
'postgres',
|
||||
@@ -120,7 +134,7 @@ export function updateTriggersCount(
|
||||
// Map trigger types to their corresponding count property names
|
||||
const countPropertyMap: Record<TriggerType, string | undefined> = {
|
||||
webhook: undefined,
|
||||
email: undefined,
|
||||
runnable_email: undefined,
|
||||
schedule: 'schedule_count',
|
||||
http: 'http_routes_count',
|
||||
websocket: 'websocket_count',
|
||||
@@ -130,6 +144,7 @@ export function updateTriggersCount(
|
||||
mqtt: 'mqtt_count',
|
||||
sqs: 'sqs_count',
|
||||
gcp: 'gcp_count',
|
||||
email: 'email_count',
|
||||
poll: undefined,
|
||||
cli: undefined
|
||||
}
|
||||
@@ -178,6 +193,8 @@ export function triggerKindToTriggerType(kind: TriggerKind): TriggerType | undef
|
||||
return 'webhook'
|
||||
case 'emails':
|
||||
return 'email'
|
||||
case 'runnable_emails':
|
||||
return 'runnable_email'
|
||||
case 'schedules':
|
||||
return 'schedule'
|
||||
case 'routes':
|
||||
@@ -225,7 +242,7 @@ export async function deployTriggers(
|
||||
// Map of trigger types to their save functions
|
||||
const triggerSaveFunctions: Record<TriggerType, Function | undefined> = {
|
||||
webhook: undefined,
|
||||
email: undefined,
|
||||
runnable_email: undefined,
|
||||
schedule: (trigger: Trigger) => {
|
||||
if (trigger.isPrimary && initialPath) {
|
||||
trigger.draftConfig = {
|
||||
@@ -301,6 +318,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 +424,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,8 +437,8 @@ export function getTriggerLabel(trigger: Trigger): string {
|
||||
|
||||
if (type === 'webhook') {
|
||||
return 'Webhook'
|
||||
} else if (type === 'email') {
|
||||
return 'Email'
|
||||
} else if (type === 'runnable_email') {
|
||||
return 'Runnable email'
|
||||
} else if (type === 'cli') {
|
||||
return 'CLI'
|
||||
} else if (type === 'http' && !emptyString(config?.route_path)) {
|
||||
@@ -430,6 +458,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 +473,7 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] {
|
||||
const triggerTypeOrder = [
|
||||
'webhook',
|
||||
'cli',
|
||||
'email',
|
||||
'runnable_email',
|
||||
'poll',
|
||||
'schedule',
|
||||
'http',
|
||||
@@ -453,7 +483,8 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] {
|
||||
'nats',
|
||||
'mqtt',
|
||||
'sqs',
|
||||
'gcp'
|
||||
'gcp',
|
||||
'email'
|
||||
]
|
||||
|
||||
return triggers.sort((a, b) => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'Email triggers' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
<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 { isCloudHosted } from '$lib/cloud'
|
||||
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)
|
||||
|
||||
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)
|
||||
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()
|
||||
})
|
||||
|
||||
let emailDomain: string | null = $state(null)
|
||||
getEmailDomain().then((domain) => {
|
||||
emailDomain = domain
|
||||
})
|
||||
|
||||
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}`}
|
||||
|
||||
<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}
|
||||
{isCloudHosted() || workspaced_local_part
|
||||
? workspace_id + '/' + local_part
|
||||
: local_part}
|
||||
{/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(
|
||||
getEmailAddress(
|
||||
local_part,
|
||||
workspaced_local_part ?? false,
|
||||
workspace_id,
|
||||
emailDomain ?? ''
|
||||
)
|
||||
)}
|
||||
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: 'runnable_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: 'runnable_email', path: '', isDraft: false },
|
||||
{ type: 'cli', path: '', isDraft: false }
|
||||
])
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user