mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-20 16:03:21 +00:00
all
This commit is contained in:
@@ -111,15 +111,15 @@
|
||||
// `scriptPath` is the related script — the drawer locks its
|
||||
// script-picker to it so the trigger can't be reassigned off the
|
||||
// pipeline from this entry point.
|
||||
onEditTrigger?: (
|
||||
kind: NativeTriggerKind,
|
||||
triggerPath: string,
|
||||
scriptPath: string
|
||||
) => void
|
||||
onEditTrigger?: (kind: NativeTriggerKind, triggerPath: string, scriptPath: string) => void
|
||||
// Click handler for the kebab → Delete entry on an attached trigger.
|
||||
// The page is expected to confirm + call the matching delete API
|
||||
// and refetch the graph.
|
||||
onDeleteTrigger?: (kind: NativeTriggerKind, triggerPath: string) => void
|
||||
// Click handler for a webhook node — opens the webhook drawer (URLs +
|
||||
// webhook-specific token creation) for the given script. Webhooks have
|
||||
// no trigger row, so they don't use the create/edit/delete flows.
|
||||
onOpenWebhook?: (scriptPath: string) => void
|
||||
}
|
||||
let {
|
||||
graph,
|
||||
@@ -136,7 +136,8 @@
|
||||
runStates,
|
||||
onCreateMissingTrigger,
|
||||
onEditTrigger,
|
||||
onDeleteTrigger
|
||||
onDeleteTrigger,
|
||||
onOpenWebhook
|
||||
}: Props = $props()
|
||||
|
||||
const ADD_NODE_ID = '__add__'
|
||||
@@ -149,12 +150,7 @@
|
||||
// the DAG. They force sugiyama to put + at layer 0 (top) and center
|
||||
// it horizontally over the roots — same mechanism the flow editor
|
||||
// uses for its Trigger node. Filtered out of rendered edges.
|
||||
kind:
|
||||
| 'lineage-write'
|
||||
| 'lineage-read'
|
||||
| 'trigger-asset'
|
||||
| 'trigger-native'
|
||||
| 'add-anchor'
|
||||
kind: 'lineage-write' | 'lineage-read' | 'trigger-asset' | 'trigger-native' | 'add-anchor'
|
||||
unsaved?: boolean
|
||||
// Edge from a missing-trigger placeholder — styled red dashed to
|
||||
// signal "this script declared `// on kafka` but no trigger row
|
||||
@@ -434,7 +430,8 @@
|
||||
: false,
|
||||
onCreateMissingTrigger,
|
||||
onEditTrigger,
|
||||
onDeleteTrigger
|
||||
onDeleteTrigger,
|
||||
onOpenWebhook
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,9 +135,15 @@
|
||||
runnable_unsaved?: boolean
|
||||
// Page-supplied dispatcher that opens the matching native
|
||||
// trigger drawer with `script_path` pre-filled. When absent
|
||||
// (e.g. webhook, schedule, or a kind without an editor) the
|
||||
// placeholder is non-clickable.
|
||||
// (e.g. schedule, or a kind without an editor) the placeholder is
|
||||
// non-clickable. Webhook is handled separately via `onOpenWebhook`.
|
||||
onCreateMissingTrigger?: (kind: NativeTriggerKind, scriptPath: string) => void
|
||||
// Page-supplied dispatcher that opens the webhook drawer (URLs +
|
||||
// webhook-specific token creation) for `scriptPath`. Webhooks have
|
||||
// no trigger row, so they never use the create/edit/delete flows —
|
||||
// the node is always clickable to view its endpoint instead of
|
||||
// rendering a dead "missing" placeholder.
|
||||
onOpenWebhook?: (scriptPath: string) => void
|
||||
// Page-supplied dispatcher to open the matching native trigger
|
||||
// drawer in edit mode for an attached (non-missing) trigger.
|
||||
// `triggerPath` is the trigger row's path (e.g. the mqtt_trigger
|
||||
@@ -145,11 +151,7 @@
|
||||
// drawer locks its script-picker to this so the user can't
|
||||
// reassign the trigger off the pipeline. Absent for kinds
|
||||
// without an editor.
|
||||
onEditTrigger?: (
|
||||
kind: NativeTriggerKind,
|
||||
triggerPath: string,
|
||||
scriptPath: string
|
||||
) => void
|
||||
onEditTrigger?: (kind: NativeTriggerKind, triggerPath: string, scriptPath: string) => void
|
||||
// Page-supplied dispatcher to delete an attached (non-missing)
|
||||
// trigger. Confirmation is the caller's responsibility — the
|
||||
// node just exposes the entry point on the kebab menu.
|
||||
@@ -162,20 +164,25 @@
|
||||
let menuOpen = $state(false)
|
||||
|
||||
let style = $derived(TRIGGER_NODE_STYLE[data.kind])
|
||||
let Icon = $derived(data.missing ? AlertTriangle : style.icon)
|
||||
// Webhooks are never genuinely "missing" — every deployed runnable has an
|
||||
// implicit endpoint. Suppress the broken/red treatment for them so they
|
||||
// render as a normal clickable source node.
|
||||
let isWebhook = $derived(data.kind === 'webhook')
|
||||
let displayMissing = $derived(data.missing && !isWebhook)
|
||||
let Icon = $derived(displayMissing ? AlertTriangle : style.icon)
|
||||
let missingTitle = $derived(
|
||||
data.missing
|
||||
displayMissing
|
||||
? `Missing ${style.label} trigger: ${data.runnable_path ?? ''} declares \`// on ${style.label}\` but no ${style.label} trigger targets it. Click to create one, or remove the annotation.`
|
||||
: undefined
|
||||
)
|
||||
// Webhook has no dedicated drawer (it's an implicit endpoint), so the
|
||||
// placeholder is not clickable for that kind. Schedule + the other
|
||||
// native kinds all have dedicated editors.
|
||||
// Webhook gets a drawer (URLs + webhook-specific token creation) rather
|
||||
// than a create/edit flow, so it's clickable whenever the page supplies
|
||||
// the handler.
|
||||
let canOpenWebhook = $derived(isWebhook && !!data.runnable_path && !!data.onOpenWebhook)
|
||||
// Schedule + the other native kinds all have dedicated editors. Webhook is
|
||||
// excluded — it routes through `canOpenWebhook` instead.
|
||||
let canCreate = $derived(
|
||||
data.missing &&
|
||||
data.kind !== 'webhook' &&
|
||||
!!data.runnable_path &&
|
||||
!!data.onCreateMissingTrigger
|
||||
data.missing && data.kind !== 'webhook' && !!data.runnable_path && !!data.onCreateMissingTrigger
|
||||
)
|
||||
// Attached native trigger → clickable to open its drawer in edit mode.
|
||||
let canEdit = $derived(
|
||||
@@ -190,10 +197,7 @@
|
||||
// non-missing ref + a backing editor (i.e. excludes webhook). Schedule
|
||||
// has its own delete endpoint, same shape as the other natives.
|
||||
let canDelete = $derived(
|
||||
!data.missing &&
|
||||
data.kind !== 'webhook' &&
|
||||
!!data.ref &&
|
||||
!!data.onDeleteTrigger
|
||||
!data.missing && data.kind !== 'webhook' && !!data.ref && !!data.onDeleteTrigger
|
||||
)
|
||||
|
||||
let menuItems: Item[] = $derived(
|
||||
@@ -221,14 +225,15 @@
|
||||
if (!canEdit || !data.ref || !data.runnable_path || !data.onEditTrigger) return
|
||||
data.onEditTrigger(data.kind as NativeTriggerKind, data.ref, data.runnable_path)
|
||||
}
|
||||
|
||||
function handleWebhookClick() {
|
||||
if (!canOpenWebhook || !data.runnable_path || !data.onOpenWebhook) return
|
||||
data.onOpenWebhook(data.runnable_path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="relative"
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
>
|
||||
<div class="relative" onmouseenter={() => (hover = true)} onmouseleave={() => (hover = false)}>
|
||||
{#if canCreate}
|
||||
<!-- Same affordance as the asset's downstream + button: render the
|
||||
whole node as a button so the cursor + hover + click is obvious
|
||||
@@ -280,14 +285,39 @@
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{:else if canOpenWebhook}
|
||||
<!-- Webhook endpoint: implicit (no trigger row), so the node opens a
|
||||
drawer with the URLs + webhook-specific token creation instead of
|
||||
an editor. Styled like an attached trigger, never the red
|
||||
"missing" state. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleWebhookClick}
|
||||
class={twMerge(
|
||||
'flex items-center rounded-md drop-shadow-sm overflow-hidden outline outline-1 w-full text-left',
|
||||
style.bg,
|
||||
data.unsaved ? `opacity-80 ${style.borderUnsaved}` : style.border,
|
||||
'hover:brightness-95 dark:hover:brightness-110 transition-[filter]'
|
||||
)}
|
||||
style="width: {NODE.width}px; min-height: {NODE.height}px;"
|
||||
title={`Webhook endpoint for ${data.runnable_path ?? ''} — click to view URLs and create a token`}
|
||||
>
|
||||
<Icon size={14} class={`shrink-0 ml-2 mr-2 ${style.iconText}`} />
|
||||
<div class="flex flex-col min-w-0 flex-1 pr-2 py-0.5 leading-tight">
|
||||
<span class="text-3xs uppercase tracking-wide truncate text-tertiary">
|
||||
{style.label}
|
||||
</span>
|
||||
<span class="text-2xs font-mono truncate text-emphasis"> URLs & token </span>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex items-center rounded-md drop-shadow-sm overflow-hidden outline outline-1',
|
||||
data.missing
|
||||
displayMissing
|
||||
? 'bg-red-50 dark:bg-red-900/30 outline-dashed outline-red-400 dark:outline-red-500'
|
||||
: style.bg,
|
||||
data.missing ? '' : data.unsaved ? `opacity-80 ${style.borderUnsaved}` : style.border
|
||||
displayMissing ? '' : data.unsaved ? `opacity-80 ${style.borderUnsaved}` : style.border
|
||||
)}
|
||||
style="width: {NODE.width}px; min-height: {NODE.height}px;"
|
||||
title={missingTitle ??
|
||||
@@ -295,24 +325,24 @@
|
||||
>
|
||||
<Icon
|
||||
size={14}
|
||||
class={`shrink-0 ml-2 mr-2 ${data.missing ? 'text-red-600 dark:text-red-400' : style.iconText}`}
|
||||
class={`shrink-0 ml-2 mr-2 ${displayMissing ? 'text-red-600 dark:text-red-400' : style.iconText}`}
|
||||
/>
|
||||
<div class="flex flex-col min-w-0 flex-1 pr-2 py-0.5 leading-tight">
|
||||
<span
|
||||
class={twMerge(
|
||||
'text-3xs uppercase tracking-wide truncate',
|
||||
data.missing ? 'text-red-700 dark:text-red-400' : 'text-tertiary'
|
||||
displayMissing ? 'text-red-700 dark:text-red-400' : 'text-tertiary'
|
||||
)}
|
||||
>
|
||||
{style.label}{data.missing ? ' · missing' : data.unsaved ? ' · unsaved' : ''}
|
||||
{style.label}{displayMissing ? ' · missing' : data.unsaved ? ' · unsaved' : ''}
|
||||
</span>
|
||||
<span
|
||||
class={twMerge(
|
||||
'text-2xs font-mono truncate',
|
||||
data.missing ? 'text-red-700 dark:text-red-400' : 'text-emphasis'
|
||||
displayMissing ? 'text-red-700 dark:text-red-400' : 'text-emphasis'
|
||||
)}
|
||||
>
|
||||
{data.missing ? 'no trigger row' : data.ref}
|
||||
{displayMissing ? 'no trigger row' : data.ref}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { setContext, tick } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import WebhooksPanel from './WebhooksPanel.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
import type { TriggersCount } from '$lib/gen'
|
||||
|
||||
// WebhooksPanel → TriggerTokens reads the 'TriggerContext' to publish the
|
||||
// webhook token count. The pipeline canvas isn't a trigger editor, so no
|
||||
// ancestor provides it — supply a standalone context here. Nothing
|
||||
// consumes the count in this view, but the store must exist.
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
triggersCount: writable<TriggersCount | undefined>(undefined),
|
||||
simplifiedPoll: writable(false),
|
||||
showCaptureHint: writable(undefined),
|
||||
triggersState: new Triggers()
|
||||
})
|
||||
|
||||
let open = $state(false)
|
||||
let drawer = $state<Drawer | undefined>(undefined)
|
||||
let scriptPath = $state('')
|
||||
let isFlow = $state(false)
|
||||
let runnableVersion = $state<string | undefined>(undefined)
|
||||
let args = $state<Record<string, any>>({})
|
||||
|
||||
// Webhooks have no trigger row to create — every deployed runnable gets an
|
||||
// implicit endpoint. The drawer just surfaces the URLs + the
|
||||
// webhook-specific token creation flow, so there's no edit/new split.
|
||||
export async function openDrawer(
|
||||
path: string,
|
||||
is_flow = false,
|
||||
version?: string,
|
||||
runnableArgs: Record<string, any> = {}
|
||||
) {
|
||||
scriptPath = path
|
||||
isFlow = is_flow
|
||||
runnableVersion = version
|
||||
args = runnableArgs
|
||||
open = true
|
||||
await tick()
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
let scopes = $derived(
|
||||
isFlow ? [`jobs:run:flows:${scriptPath}`] : [`jobs:run:scripts:${scriptPath}`]
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<Drawer size="700px" bind:this={drawer} on:close={() => (open = false)}>
|
||||
<DrawerContent title={`Webhooks for ${scriptPath}`} on:close={() => drawer?.closeDrawer()}>
|
||||
<WebhooksPanel
|
||||
{isFlow}
|
||||
path={scriptPath}
|
||||
{runnableVersion}
|
||||
token=""
|
||||
{args}
|
||||
{scopes}
|
||||
newItem={false}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/if}
|
||||
@@ -77,6 +77,7 @@
|
||||
import GcpTriggerEditor from '$lib/components/triggers/gcp/GcpTriggerEditor.svelte'
|
||||
import EmailTriggerEditor from '$lib/components/triggers/email/EmailTriggerEditor.svelte'
|
||||
import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte'
|
||||
import WebhookEditor from '$lib/components/triggers/webhook/WebhookEditor.svelte'
|
||||
|
||||
// Variables and resources are declarative config, not pipeline assets —
|
||||
// they're hub-shaped (referenced by most runnables) and would swamp the
|
||||
@@ -798,7 +799,8 @@
|
||||
// Non-fatal: surface a warning so the user
|
||||
// knows to clean up manually. The fresh deploy
|
||||
// at the new path still landed.
|
||||
const msg = (archiveErr as any)?.body ?? (archiveErr as any)?.message ?? String(archiveErr)
|
||||
const msg =
|
||||
(archiveErr as any)?.body ?? (archiveErr as any)?.message ?? String(archiveErr)
|
||||
sendUserToast(
|
||||
`Renamed to "${path}" but couldn't archive the old "${prev}": ${msg}`,
|
||||
true
|
||||
@@ -1051,6 +1053,22 @@
|
||||
let gcpEditor: GcpTriggerEditor | undefined = $state()
|
||||
let emailEditor: EmailTriggerEditor | undefined = $state()
|
||||
let scheduleEditor: ScheduleEditor | undefined = $state()
|
||||
let webhookEditor: WebhookEditor | undefined = $state()
|
||||
|
||||
// Webhooks have no trigger row to create — clicking the node opens a
|
||||
// drawer with the endpoint URLs + the webhook-specific token creation
|
||||
// flow. Drafts have no deployed endpoint yet, so nudge the user to save
|
||||
// first (mirrors openMissingTriggerDrawer).
|
||||
function openWebhookDrawer(scriptPath: string) {
|
||||
if (drafts.has(scriptPath)) {
|
||||
sendUserToast(
|
||||
`Save the script "${scriptPath}" first — webhooks only trigger the deployed version.`,
|
||||
true
|
||||
)
|
||||
return
|
||||
}
|
||||
webhookEditor?.openDrawer(scriptPath, false)
|
||||
}
|
||||
|
||||
function openMissingTriggerDrawer(kind: NativeTriggerKind, scriptPath: string) {
|
||||
// A native trigger row stores `script_path` as a hard reference —
|
||||
@@ -1095,9 +1113,7 @@
|
||||
// Delete-trigger confirmation state. Kebab → Delete opens the standard
|
||||
// ConfirmationModal; the actual delete is dispatched from onConfirmed
|
||||
// so the dialog stays consistent with the rest of the app.
|
||||
let triggerDeleteTarget = $state<
|
||||
{ kind: NativeTriggerKind; path: string } | undefined
|
||||
>(undefined)
|
||||
let triggerDeleteTarget = $state<{ kind: NativeTriggerKind; path: string } | undefined>(undefined)
|
||||
let triggerDeleteLoading = $state(false)
|
||||
let triggerDeleteOpen = $derived(triggerDeleteTarget != undefined)
|
||||
|
||||
@@ -1152,11 +1168,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openEditTriggerDrawer(
|
||||
kind: NativeTriggerKind,
|
||||
triggerPath: string,
|
||||
scriptPath: string
|
||||
) {
|
||||
function openEditTriggerDrawer(kind: NativeTriggerKind, triggerPath: string, scriptPath: string) {
|
||||
switch (kind) {
|
||||
case 'schedule':
|
||||
return scheduleEditor?.openEdit(triggerPath, false, scriptPath)
|
||||
@@ -1461,6 +1473,7 @@
|
||||
onCreateMissingTrigger={openMissingTriggerDrawer}
|
||||
onEditTrigger={openEditTriggerDrawer}
|
||||
onDeleteTrigger={deleteAttachedTrigger}
|
||||
onOpenWebhook={openWebhookDrawer}
|
||||
onselect={(s) => {
|
||||
// Clicking a draft runnable node re-opens it in the pane;
|
||||
// clicking anything else selects it normally and detaches
|
||||
@@ -1754,9 +1767,8 @@
|
||||
{#if triggerDeleteTarget}
|
||||
<p>
|
||||
Delete <code class="font-mono">{triggerDeleteTarget.path}</code>? The
|
||||
<code class="font-mono">// on {triggerDeleteTarget.kind}</code> annotation on the script
|
||||
stays — the trigger will read as missing on the canvas until you recreate it or remove
|
||||
the annotation.
|
||||
<code class="font-mono">// on {triggerDeleteTarget.kind}</code> annotation on the script stays
|
||||
— the trigger will read as missing on the canvas until you recreate it or remove the annotation.
|
||||
</p>
|
||||
{/if}
|
||||
</ConfirmationModal>
|
||||
@@ -1773,6 +1785,7 @@
|
||||
<GcpTriggerEditor bind:this={gcpEditor} onUpdate={() => graphRes.refetch()} />
|
||||
<EmailTriggerEditor bind:this={emailEditor} onUpdate={() => graphRes.refetch()} />
|
||||
<ScheduleEditor bind:this={scheduleEditor} onUpdate={() => graphRes.refetch()} />
|
||||
<WebhookEditor bind:this={webhookEditor} />
|
||||
|
||||
{#if leaveModalOpen}
|
||||
<!-- Three-button leave guard. Built inline rather than reusing
|
||||
|
||||
Reference in New Issue
Block a user