From d8da723be491c1bf52c01c0db3511662e3095937 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 28 May 2026 16:26:52 +0000 Subject: [PATCH] all --- .../assets/AssetGraph/AssetGraphCanvas.svelte | 23 ++--- .../assets/AssetGraph/TriggerNode.svelte | 94 ++++++++++++------- .../triggers/webhook/WebhookEditor.svelte | 66 +++++++++++++ .../(logged)/pipeline/[folder]/+page.svelte | 37 +++++--- 4 files changed, 163 insertions(+), 57 deletions(-) create mode 100644 frontend/src/lib/components/triggers/webhook/WebhookEditor.svelte diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index f5d3616975..e224633e74 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -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 } }) } diff --git a/frontend/src/lib/components/assets/AssetGraph/TriggerNode.svelte b/frontend/src/lib/components/assets/AssetGraph/TriggerNode.svelte index 40413ee984..6181d6eaef 100644 --- a/frontend/src/lib/components/assets/AssetGraph/TriggerNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/TriggerNode.svelte @@ -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) + } -
(hover = true)} - onmouseleave={() => (hover = false)} -> +
(hover = true)} onmouseleave={() => (hover = false)}> {#if canCreate} + {:else}
- {style.label}{data.missing ? ' · missing' : data.unsaved ? ' · unsaved' : ''} + {style.label}{displayMissing ? ' · missing' : data.unsaved ? ' · unsaved' : ''} - {data.missing ? 'no trigger row' : data.ref} + {displayMissing ? 'no trigger row' : data.ref}
diff --git a/frontend/src/lib/components/triggers/webhook/WebhookEditor.svelte b/frontend/src/lib/components/triggers/webhook/WebhookEditor.svelte new file mode 100644 index 0000000000..031da4aacf --- /dev/null +++ b/frontend/src/lib/components/triggers/webhook/WebhookEditor.svelte @@ -0,0 +1,66 @@ + + +{#if open} + (open = false)}> + drawer?.closeDrawer()}> + + + +{/if} diff --git a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte index d3de4e09fd..ce87c8d840 100644 --- a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte @@ -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}

Delete {triggerDeleteTarget.path}? The - // on {triggerDeleteTarget.kind} annotation on the script - stays — the trigger will read as missing on the canvas until you recreate it or remove - the annotation. + // on {triggerDeleteTarget.kind} annotation on the script stays + — the trigger will read as missing on the canvas until you recreate it or remove the annotation.

{/if} @@ -1773,6 +1785,7 @@ graphRes.refetch()} /> graphRes.refetch()} /> graphRes.refetch()} /> + {#if leaveModalOpen}