mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
fix(frontend): improve UI for email triggers (#4243)
* fix(frontend): improve UI for email triggers * fix: nits
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, Tab, TabContent, Button } from '$lib/components/common'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { CalendarCheck2, Clipboard, Terminal, Webhook } from 'lucide-svelte'
|
||||
import { CalendarCheck2, Clipboard, MailIcon, Terminal, Webhook } from 'lucide-svelte'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { yaml } from 'svelte-highlight/languages'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
@@ -9,7 +9,7 @@
|
||||
import YAML from 'yaml'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
|
||||
export let triggerSelected: 'webhooks' | 'schedule' | 'cli' = 'webhooks'
|
||||
export let triggerSelected: 'webhooks' | 'email' | 'schedule' | 'cli' = 'webhooks'
|
||||
export let flow_json: any | undefined = undefined
|
||||
export let hasStepDetails: boolean = false
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="email">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<MailIcon size={14} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Terminal size={14} />
|
||||
@@ -78,6 +84,8 @@
|
||||
<div class="h-full overflow-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
<slot name="webhooks" />
|
||||
{:else if triggerSelected === 'email'}
|
||||
<slot name="email" />
|
||||
{:else if triggerSelected === 'schedule'}
|
||||
<slot name="schedule" />
|
||||
{:else if triggerSelected === 'cli'}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
{hasStepDetails}
|
||||
>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="email" name="email" />
|
||||
<slot slot="schedule" name="schedule" />
|
||||
<slot slot="cli" name="cli" />
|
||||
<slot slot="details" name="details" />
|
||||
@@ -57,6 +58,7 @@
|
||||
<TabContent value="detail" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageDetailPanel bind:triggerSelected bind:selected {isOperator} {hasStepDetails}>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="email" name="email" />
|
||||
<slot slot="schedule" name="schedule" />
|
||||
<slot slot="cli" name="cli" />
|
||||
<slot slot="details" name="details" />
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON } from '$lib/consts'
|
||||
import UserSettings from '../UserSettings.svelte'
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { base32 } from 'rfc4648'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
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
|
||||
async function getEmailDomain() {
|
||||
emailDomain =
|
||||
((await SettingService.getGlobal({
|
||||
key: 'email_domain'
|
||||
})) as any) ?? null
|
||||
}
|
||||
getEmailDomain()
|
||||
|
||||
let requestType: 'hash' | 'path' = 'path'
|
||||
|
||||
function emailAddress() {
|
||||
const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.')
|
||||
const plainPrefix = `${$workspaceStore}+${
|
||||
(requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
|
||||
}+${token}`
|
||||
const encodedPrefix = base32
|
||||
.stringify(new TextEncoder().encode(plainPrefix), {
|
||||
pad: false
|
||||
})
|
||||
.toLowerCase()
|
||||
return `${pathOrHash}+${encodedPrefix}@${emailDomain}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<UserSettings
|
||||
bind:this={userSettings}
|
||||
on:tokenCreated={(e) => {
|
||||
token = e.detail
|
||||
}}
|
||||
newTokenLabel={`${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col w-full gap-4">
|
||||
{#if emailDomain}
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="my-2">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<input
|
||||
bind:value={token}
|
||||
placeholder="paste your token here once created to alter examples below"
|
||||
class="!text-xs"
|
||||
/>
|
||||
<Button size="xs" color="light" variant="border" on:click={userSettings.openDrawer}>
|
||||
Create an Email-specific Token
|
||||
<Tooltip light>
|
||||
The token will have a scope such that it can only be used to trigger this script. It
|
||||
is safe to share as it cannot be used to impersonate you.
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
{#if token === 'TOKEN_TO_CREATE'}
|
||||
<div class="flex flex-row gap-1 text-xs text-red-500 items-center mt-1">
|
||||
<AlertTriangle size="12" />
|
||||
Create/input a valid token before copying the email address below
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isFlow}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Call method</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={requestType}>
|
||||
<ToggleButton label="By path" value="path" />
|
||||
<ToggleButton label="By hash" value="hash" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#key requestType}
|
||||
{#key token}
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel title="Email address" content={emailAddress()} />
|
||||
</div>
|
||||
{/key}
|
||||
{/key}
|
||||
<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.
|
||||
</Alert>
|
||||
</div>
|
||||
{: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}
|
||||
</div>
|
||||
@@ -18,11 +18,7 @@
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import { copyToClipboard, generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { base } from '$lib/base'
|
||||
import { base32 } from 'rfc4648'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
export let token: string
|
||||
@@ -46,15 +42,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let emailDomain: string | null = null
|
||||
async function getEmailDomain() {
|
||||
emailDomain =
|
||||
((await SettingService.getGlobal({
|
||||
key: 'email_domain'
|
||||
})) as any) ?? null
|
||||
}
|
||||
getEmailDomain()
|
||||
|
||||
$: webhooks = isFlow ? computeFlowWebhooks(path) : computeScriptWebhooks(hash, path)
|
||||
|
||||
function computeScriptWebhooks(hash: string | undefined, path: string) {
|
||||
@@ -97,10 +84,6 @@
|
||||
requestType = 'hash'
|
||||
}
|
||||
|
||||
$: if (webhookType === 'sync' && selectedTab === 'email') {
|
||||
webhookType = 'async'
|
||||
}
|
||||
|
||||
$: url =
|
||||
webhooks[webhookType][requestType] +
|
||||
(tokenType === 'query'
|
||||
@@ -127,19 +110,6 @@
|
||||
return headers
|
||||
}
|
||||
|
||||
function emailAddress() {
|
||||
const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.')
|
||||
const plainPrefix = `${$workspaceStore}+${
|
||||
(requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
|
||||
}+${token}`
|
||||
const encodedPrefix = base32
|
||||
.stringify(new TextEncoder().encode(plainPrefix), {
|
||||
pad: false
|
||||
})
|
||||
.toLowerCase()
|
||||
return `${pathOrHash}+${encodedPrefix}@${emailDomain}`
|
||||
}
|
||||
|
||||
function fetchCode() {
|
||||
if (webhookType === 'sync') {
|
||||
return `
|
||||
@@ -293,7 +263,6 @@ done`
|
||||
label="Sync"
|
||||
value="sync"
|
||||
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
|
||||
disabled={selectedTab === 'email'}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
@@ -324,15 +293,13 @@ done`
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if selectedTab !== 'email'}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={tokenType}>
|
||||
<ToggleButton label="Token in Headers" value="headers" />
|
||||
<ToggleButton label="Token in Query" value="query" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={tokenType}>
|
||||
<ToggleButton label="Token in Headers" value="headers" />
|
||||
<ToggleButton label="Token in Query" value="query" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
@@ -342,7 +309,6 @@ done`
|
||||
<Tab value="curl" size="xs">Curl</Tab>
|
||||
{/if}
|
||||
<Tab value="fetch" size="xs">Fetch</Tab>
|
||||
<Tab value="email" size="xs">Email</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#key token}
|
||||
@@ -401,39 +367,6 @@ done`
|
||||
{/key}{/key}{/key}{/key}
|
||||
{/key}
|
||||
</TabContent>
|
||||
<TabContent value="email">
|
||||
{#if emailDomain}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#key args}
|
||||
{#key requestType}
|
||||
{#key webhookType}
|
||||
{#key tokenType}
|
||||
{#key token}
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel title="Email address" content={emailAddress()} />
|
||||
</div>
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
<Alert title="Email triggers" 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.
|
||||
</Alert>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" kind="danger">
|
||||
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}
|
||||
</TabContent>
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
import { loadFlowSchedule, type Schedule } from '$lib/components/flows/scheduleUtils'
|
||||
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
|
||||
import FlowHistory from '$lib/components/flows/FlowHistory.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
|
||||
let flow: Flow | undefined
|
||||
let can_write = false
|
||||
@@ -472,6 +473,14 @@
|
||||
{args}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="email">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<RunPageSchedules isFlow={true} path={flow.path ?? ''} {can_write} />
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
import PersistentScriptDrawer from '$lib/components/PersistentScriptDrawer.svelte'
|
||||
import { loadScriptSchedule, type ScriptSchedule } from '$lib/scripts'
|
||||
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
|
||||
let script: Script | undefined
|
||||
let topHash: string | undefined
|
||||
@@ -668,6 +669,14 @@
|
||||
{args}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="email">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<RunPageSchedules isFlow={false} path={script.path ?? ''} {can_write} />
|
||||
</svelte:fragment>
|
||||
|
||||
Reference in New Issue
Block a user