import { useCallback, useMemo, useState, type ChangeEvent } from "react"; import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, cn, } from "@arronqzy/ui"; import type { ExecutionTraceEntry, FetchHttpMethod, FetchRequestConfig, FetchResponseType, } from "@arronqzy/blueprint-dsl"; import { applyFetchConfigScope, draftFetchHeadersText, FETCH_CACHES, FETCH_CREDENTIALS, FETCH_HTTP_METHODS, FETCH_MODES, FETCH_NODE_TYPE, FETCH_REDIRECTS, FETCH_RESPONSE_TYPES, fetchConfigHasScopeTemplate, getFetchBodyValidationError, getFetchHeadersValidationError, latestTraceOutputsByNode, parseFetchHeadersJson, resolveFetchIncomingScope, resolveFetchRequestUrl, resolveFetchScopeAutocompleteRoot, } from "@arronqzy/blueprint-dsl"; import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document"; import { resolveNodeFetchConfig } from "../graph/document"; import { cancelSwaggerLoadTask, cancelFetchDebugTask, startFetchDebugTask, startSwaggerLoadTask, useFetchDebugTask, useSwaggerLoadTask, } from "../fetch-config-task-store"; import { useI18n } from "@arronqzy/i18n/react"; import { ConfigFieldLabel, ConfigHintIcon, ConfigSectionTitle } from "./ConfigHintIcon"; import { FetchUrlAutocomplete } from "./FetchUrlAutocomplete"; import { ScopeTemplateAutocompleteHost } from "./ScopeTemplateAutocompleteHost"; function SendIcon({ className }: { className?: string }) { return ( ); } function StopIcon({ className }: { className?: string }) { return ( ); } function ChevronIcon({ className, expanded }: { className?: string; expanded: boolean }) { return ( ); } function AsyncSendButton({ loading, disabled, onSend, onAbort, sendTitle, abortTitle, sendAriaLabel, abortAriaLabel, }: { loading: boolean; disabled?: boolean; onSend: () => void; onAbort: () => void; sendTitle: string; abortTitle: string; sendAriaLabel: string; abortAriaLabel: string; }) { if (loading) { return ( ); } return ( ); } function formatFetchDebugData(data: unknown): string { if (typeof data === "string") return data; try { return JSON.stringify(data, null, 2); } catch { return String(data); } } function FetchDebugResponsePanel({ task, }: { task: ReturnType; }) { const { t } = useI18n(); const [expanded, setExpanded] = useState(false); if (task.status === "idle" || task.status === "loading") return null; if (task.status === "error") { return (

{task.error ?? t("blueprint.config.requestFailed")}

); } const result = task.result; if (!result) return null; const bodyText = formatFetchDebugData(result.data); const preview = bodyText.length > 120 ? `${bodyText.slice(0, 120).trimEnd()}…` : bodyText; const ok = result.status >= 200 && result.status < 300; return (
{expanded ? (
{result.url}
            {bodyText}
          
) : null}
); } function LoaderIcon({ className }: { className?: string }) { return ( ); } export type FetchNodeConfigPanelProps = { node: BlueprintGraphNode; graphNodes?: BlueprintGraphNode[]; graphEdges?: BlueprintGraphEdge[]; traceEntries?: ExecutionTraceEntry[]; onUpdateNode: ( nodeId: string, patch: Partial> ) => void; }; function patchFetchConfig( node: BlueprintGraphNode, patch: Partial ) { return { role: "fetch" as const, nodeType: FETCH_NODE_TYPE, configSource: "fetch" as const, fetchConfig: { ...resolveNodeFetchConfig(node), ...patch }, }; } export function FetchNodeConfigPanel({ node, graphNodes = [], graphEdges = [], traceEntries = [], onUpdateNode, }: FetchNodeConfigPanelProps) { const { t } = useI18n(); const fetchConfig = resolveNodeFetchConfig(node); const endpoints = fetchConfig.swaggerEndpoints ?? []; const hasSwaggerEndpoints = endpoints.length > 0; const urlInputMode = fetchConfig.urlInputMode ?? (hasSwaggerEndpoints ? "swagger" : "manual"); const swaggerTask = useSwaggerLoadTask(node.id); const fetchDebugTask = useFetchDebugTask(node.id); const loadingSwagger = swaggerTask.status === "loading"; const loadingFetchDebug = fetchDebugTask.status === "loading"; const swaggerTaskError = swaggerTask.status === "error" ? swaggerTask.error : null; const [validationError, setValidationError] = useState(null); const [fetchValidationError, setFetchValidationError] = useState(null); const swaggerError = validationError ?? swaggerTaskError; const incomingScope = useMemo(() => { const outputs = latestTraceOutputsByNode(traceEntries); return resolveFetchIncomingScope({ fetchNodeId: node.id, nodes: graphNodes, edges: graphEdges, getOutput: (sourceId, port) => outputs[sourceId]?.[port], }); }, [graphEdges, graphNodes, node.id, traceEntries]); const usesScopeTemplate = fetchConfigHasScopeTemplate(fetchConfig); const resolvedFetchConfig = useMemo( () => applyFetchConfigScope(fetchConfig, incomingScope), [fetchConfig, incomingScope] ); const resolvedUrlPreview = useMemo(() => { if (!usesScopeTemplate) return ""; try { return resolveFetchRequestUrl(resolvedFetchConfig); } catch { return resolvedFetchConfig.url?.trim() ?? ""; } }, [resolvedFetchConfig, usesScopeTemplate]); const incomingScopeJson = useMemo(() => { if (incomingScope === undefined) return ""; try { return JSON.stringify(incomingScope, null, 2); } catch { return String(incomingScope); } }, [incomingScope]); const headersDraft = draftFetchHeadersText(fetchConfig); const resolvedHeadersPreview = useMemo(() => { if (!usesScopeTemplate) return ""; const headers = resolvedFetchConfig.headers; if (!headers || Object.keys(headers).length === 0) return ""; try { return JSON.stringify(headers, null, 2); } catch { return ""; } }, [resolvedFetchConfig.headers, usesScopeTemplate]); const resolvedBodyPreview = useMemo(() => { if (!usesScopeTemplate) return ""; return resolvedFetchConfig.body?.trim() ?? ""; }, [resolvedFetchConfig.body, usesScopeTemplate]); const headersJsonError = useMemo( () => getFetchHeadersValidationError(fetchConfig, incomingScope), [fetchConfig, incomingScope] ); const bodyJsonError = useMemo( () => getFetchBodyValidationError(fetchConfig.body, resolvedFetchConfig.body), [fetchConfig.body, resolvedFetchConfig.body] ); const incomingHasPendingValue = useMemo(() => { if (!incomingScope || typeof incomingScope !== "object") return false; const entries = "kind" in incomingScope ? [incomingScope] : Object.values(incomingScope as Record); return entries.some((entry) => entry && entry.kind === "pending"); }, [incomingScope]); const autocompleteScope = resolveFetchScopeAutocompleteRoot(incomingScope); const [formEl, setFormEl] = useState(null); const setUrlInputMode = useCallback( (mode: "swagger" | "manual") => { onUpdateNode(node.id, patchFetchConfig(node, { urlInputMode: mode })); }, [node, onUpdateNode] ); const handleLoadSwagger = useCallback(() => { const docsUrl = fetchConfig.swaggerDocsUrl?.trim(); if (!docsUrl) { setValidationError(t("blueprint.config.fillSwaggerUrlFirst")); return; } setValidationError(null); startSwaggerLoadTask({ nodeId: node.id, docsUrl, onSuccess: (parsed, url) => { onUpdateNode( node.id, patchFetchConfig(node, { swaggerDocsUrl: url, apiBaseUrl: parsed.apiBaseUrl, swaggerEndpoints: parsed.endpoints, urlInputMode: "swagger", }) ); }, }); }, [fetchConfig.swaggerDocsUrl, node, onUpdateNode]); const handleAbortSwagger = useCallback(() => { cancelSwaggerLoadTask(node.id); setValidationError(null); }, [node.id]); const handleSendFetchDebug = useCallback(() => { const resolved = applyFetchConfigScope(fetchConfig, incomingScope); const url = resolved.url?.trim(); if (!url) { setFetchValidationError( usesScopeTemplate && fetchConfig.url?.trim() ? t("blueprint.config.fetchScopeUnresolved") : t("blueprint.config.fillRequestUrlFirst") ); return; } const headersError = getFetchHeadersValidationError(fetchConfig, incomingScope); if (headersError) { setFetchValidationError( t("blueprint.config.fetchHeadersInvalidJson", { error: headersError }) ); return; } const bodyError = getFetchBodyValidationError(fetchConfig.body, resolved.body); if (bodyError) { setFetchValidationError( t("blueprint.config.fetchBodyInvalidJson", { error: bodyError }) ); return; } setFetchValidationError(null); startFetchDebugTask({ nodeId: node.id, config: resolved, }); }, [fetchConfig, incomingScope, node.id, t, usesScopeTemplate]); const handleAbortFetchDebug = useCallback(() => { cancelFetchDebugTask(node.id); setFetchValidationError(null); }, [node.id]); const handleHeadersChange = useCallback( (e: ChangeEvent) => { const headersJson = e.target.value; const parsed = parseFetchHeadersJson(headersJson); onUpdateNode( node.id, patchFetchConfig(node, { headersJson, ...(parsed ? { headers: parsed } : {}), }) ); }, [node, onUpdateNode] ); return (
{t("blueprint.config.fetchScopeTitle")}

{t("blueprint.config.fetchScopeHint")}

{t("blueprint.config.fetchScopeEmpty")}

{t("blueprint.config.fetchScopePending")}

{incomingScope === undefined ? null : ( <> {incomingHasPendingValue ? (

{t("blueprint.config.fetchScopePending")}

) : null}
              {incomingScopeJson}
            
)}