feat(editor): show composed webhook URL + Copy button on Webhook nodes

When the selected node is http.webhook, the config panel now shows the full
POST URL (${origin}/wh/${workflowId}/${path}) with a one-click Copy button
and an active-workflow reminder. Removes the guesswork of assembling the
URL by hand from the design spec.
This commit is contained in:
Claude
2026-08-28 19:52:38 +00:00
parent 8c2d630af5
commit c705f58531
2 changed files with 58 additions and 1 deletions

View File

@@ -1,3 +1,5 @@
import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { useNodeTypes, useConnections } from "@/api/hooks";
import { SchemaForm } from "@/forms/SchemaForm";
import type { Node } from "@xyflow/react";
@@ -7,9 +9,11 @@ interface Props {
selected: Node<VcNodeData> | null;
onChange: (patch: Partial<VcNodeData>) => void;
onDelete: () => void;
workflowId: string;
workflowActive: boolean;
}
export function NodeConfigPanel({ selected, onChange, onDelete }: Props) {
export function NodeConfigPanel({ selected, onChange, onDelete, workflowId, workflowActive }: Props) {
const nts = useNodeTypes();
const cs = useConnections();
if (!selected) {
@@ -56,6 +60,13 @@ export function NodeConfigPanel({ selected, onChange, onDelete }: Props) {
onChange={(v) => onChange({ config: v })}
/>
)}
{selected.data.typeId === "http.webhook" && (
<WebhookUrlHint
workflowId={workflowId}
workflowActive={workflowActive}
path={typeof selected.data.config.path === "string" ? selected.data.config.path : ""}
/>
)}
<button
onClick={onDelete}
className="mt-4 text-sm text-red-400 hover:text-red-300"
@@ -65,3 +76,47 @@ export function NodeConfigPanel({ selected, onChange, onDelete }: Props) {
</aside>
);
}
function WebhookUrlHint({
workflowId, workflowActive, path,
}: { workflowId: string; workflowActive: boolean; path: string }) {
const [copied, setCopied] = useState(false);
const cleanPath = path.replace(/^\/+/, "");
const url = `${window.location.origin}/wh/${workflowId}/${cleanPath}`;
return (
<div className="bg-slate-950/60 border border-slate-800 rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs uppercase text-slate-500">Webhook URL</span>
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// Clipboard blocked (insecure origin, no permission) — leave state alone.
}
}}
className="text-slate-400 hover:text-slate-200 flex items-center gap-1 text-xs"
aria-label="Copy webhook URL"
>
{copied ? <><Check size={12} /> Copied</> : <><Copy size={12} /> Copy</>}
</button>
</div>
<code className="block break-all text-xs text-slate-200 font-mono">
POST {url}
</code>
{!path && (
<p className="text-xs text-amber-400">Set a URL path above to complete the endpoint.</p>
)}
{!workflowActive && (
<p className="text-xs text-slate-500">
The webhook only responds when the workflow is <strong>active</strong> — activate it
from the toolbar to receive requests.
</p>
)}
</div>
);
}

View File

@@ -158,6 +158,8 @@ export default function EditorPage() {
selected={selected}
onChange={patchSelected}
onDelete={deleteSelected}
workflowId={id ?? ""}
workflowActive={wf.data.active}
/>
</div>
</div>