@arronqzy/react-blueprint 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +5 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/assets/index-CneNgzWb.css +1 -0
- package/dist/assets/index-DsVhHV0-.js +226 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/index.ts +70 -0
- package/package.json +43 -0
- package/postcss.config.cjs +6 -0
- package/src/BlueprintCanvasContext.tsx +61 -0
- package/src/BlueprintContextMenu.tsx +108 -0
- package/src/BlueprintNodeConfigSidebar.tsx +358 -0
- package/src/blueprint.css +321 -0
- package/src/blueprintNodeTypes.ts +20 -0
- package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
- package/src/components/BlueprintLibrarySelect.tsx +68 -0
- package/src/components/BlueprintMetaDialog.tsx +96 -0
- package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
- package/src/components/BlueprintPanelToolbar.tsx +664 -0
- package/src/components/ClockNodeConfigPanel.tsx +139 -0
- package/src/components/FetchNodeConfigPanel.tsx +716 -0
- package/src/components/FetchUrlAutocomplete.tsx +190 -0
- package/src/components/JsonNodeConfigPanel.tsx +79 -0
- package/src/components/LogicNodeConfigPanel.tsx +80 -0
- package/src/components/ViewElementMultiSelect.tsx +140 -0
- package/src/createBlueprintEdgeTypes.tsx +10 -0
- package/src/dev-seed.ts +34 -0
- package/src/dev.css +38 -0
- package/src/edges/BlueprintSmoothEdge.tsx +35 -0
- package/src/fetch-config-task-store.ts +197 -0
- package/src/flowCoordinates.ts +19 -0
- package/src/flowDefaults.ts +9 -0
- package/src/graph/blueprint-graph.ts +261 -0
- package/src/graph/document.ts +384 -0
- package/src/graph/index.ts +7 -0
- package/src/graph/node-summary.ts +88 -0
- package/src/graph/node-types.ts +9 -0
- package/src/graph/react-flow-adapter.ts +127 -0
- package/src/graph/sync-edges.ts +69 -0
- package/src/graph/sync-nodes.ts +104 -0
- package/src/hooks/useBlueprintDebugSession.ts +515 -0
- package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
- package/src/hooks/useBlueprintFlowState.ts +486 -0
- package/src/hooks/useBlueprintFlowViewport.ts +54 -0
- package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
- package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
- package/src/index.tsx +271 -0
- package/src/library/blueprint-io.ts +108 -0
- package/src/library/blueprint-library-db.ts +112 -0
- package/src/library/execution-log-db.ts +171 -0
- package/src/library/execution-log-settings.ts +50 -0
- package/src/library/swagger-docs.ts +56 -0
- package/src/library/types.ts +35 -0
- package/src/main.tsx +43 -0
- package/src/nodes/AndFlowNode.tsx +59 -0
- package/src/nodes/BlueprintFlowNode.tsx +26 -0
- package/src/nodes/BlueprintNodeCard.tsx +169 -0
- package/src/nodes/BlueprintNodeShell.tsx +45 -0
- package/src/nodes/ClockFlowNode.tsx +67 -0
- package/src/nodes/FetchFlowNode.tsx +26 -0
- package/src/nodes/JsonFlowNode.tsx +26 -0
- package/src/nodes/LifecycleFlowNode.tsx +44 -0
- package/src/nodes/LogicFlowNode.tsx +26 -0
- package/src/runtime/document-to-runnable-graph.ts +48 -0
- package/src/runtime/execution-overlay.ts +169 -0
- package/src/types.ts +1 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +6 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +16 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { useCallback, useState, type ChangeEvent } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Input,
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
cn,
|
|
11
|
+
} from "@arronqzy/ui";
|
|
12
|
+
import type {
|
|
13
|
+
FetchHttpMethod,
|
|
14
|
+
FetchRequestConfig,
|
|
15
|
+
FetchResponseType,
|
|
16
|
+
} from "@arronqzy/blueprint-dsl";
|
|
17
|
+
import {
|
|
18
|
+
FETCH_CACHES,
|
|
19
|
+
FETCH_CREDENTIALS,
|
|
20
|
+
FETCH_HTTP_METHODS,
|
|
21
|
+
FETCH_MODES,
|
|
22
|
+
FETCH_REDIRECTS,
|
|
23
|
+
FETCH_RESPONSE_TYPES,
|
|
24
|
+
} from "@arronqzy/blueprint-dsl";
|
|
25
|
+
|
|
26
|
+
import type { BlueprintGraphNode } from "../graph/document";
|
|
27
|
+
import { resolveNodeFetchConfig } from "../graph/document";
|
|
28
|
+
import {
|
|
29
|
+
cancelSwaggerLoadTask,
|
|
30
|
+
cancelFetchDebugTask,
|
|
31
|
+
startFetchDebugTask,
|
|
32
|
+
startSwaggerLoadTask,
|
|
33
|
+
useFetchDebugTask,
|
|
34
|
+
useSwaggerLoadTask,
|
|
35
|
+
} from "../fetch-config-task-store";
|
|
36
|
+
import { FetchUrlAutocomplete } from "./FetchUrlAutocomplete";
|
|
37
|
+
|
|
38
|
+
function SendIcon({ className }: { className?: string }) {
|
|
39
|
+
return (
|
|
40
|
+
<svg
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
className={className}
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth="2"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
>
|
|
48
|
+
<path d="M22 2 11 13" />
|
|
49
|
+
<path d="M22 2 15 22l-4-9-9-4z" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function StopIcon({ className }: { className?: string }) {
|
|
55
|
+
return (
|
|
56
|
+
<svg
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
className={className}
|
|
59
|
+
fill="currentColor"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
>
|
|
62
|
+
<rect x="6" y="6" width="12" height="12" rx="1" />
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ChevronIcon({ className, expanded }: { className?: string; expanded: boolean }) {
|
|
68
|
+
return (
|
|
69
|
+
<svg
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
className={cn("transition-transform", expanded && "rotate-180", className)}
|
|
72
|
+
fill="none"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
strokeWidth="2"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
>
|
|
77
|
+
<path d="m6 9 6 6 6-6" />
|
|
78
|
+
</svg>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function AsyncSendButton({
|
|
83
|
+
loading,
|
|
84
|
+
disabled,
|
|
85
|
+
onSend,
|
|
86
|
+
onAbort,
|
|
87
|
+
sendTitle,
|
|
88
|
+
abortTitle,
|
|
89
|
+
sendAriaLabel,
|
|
90
|
+
abortAriaLabel,
|
|
91
|
+
}: {
|
|
92
|
+
loading: boolean;
|
|
93
|
+
disabled?: boolean;
|
|
94
|
+
onSend: () => void;
|
|
95
|
+
onAbort: () => void;
|
|
96
|
+
sendTitle: string;
|
|
97
|
+
abortTitle: string;
|
|
98
|
+
sendAriaLabel: string;
|
|
99
|
+
abortAriaLabel: string;
|
|
100
|
+
}) {
|
|
101
|
+
if (loading) {
|
|
102
|
+
return (
|
|
103
|
+
<Button
|
|
104
|
+
type="button"
|
|
105
|
+
variant="outline"
|
|
106
|
+
size="icon"
|
|
107
|
+
className="h-8 w-8 shrink-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
108
|
+
title={abortTitle}
|
|
109
|
+
aria-label={abortAriaLabel}
|
|
110
|
+
onClick={onAbort}
|
|
111
|
+
>
|
|
112
|
+
<StopIcon className="h-3.5 w-3.5" />
|
|
113
|
+
</Button>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Button
|
|
119
|
+
type="button"
|
|
120
|
+
variant="outline"
|
|
121
|
+
size="icon"
|
|
122
|
+
className="h-8 w-8 shrink-0"
|
|
123
|
+
title={sendTitle}
|
|
124
|
+
aria-label={sendAriaLabel}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
onClick={onSend}
|
|
127
|
+
>
|
|
128
|
+
<SendIcon className="h-4 w-4" />
|
|
129
|
+
</Button>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatFetchDebugData(data: unknown): string {
|
|
134
|
+
if (typeof data === "string") return data;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.stringify(data, null, 2);
|
|
137
|
+
} catch {
|
|
138
|
+
return String(data);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function FetchDebugResponsePanel({
|
|
143
|
+
task,
|
|
144
|
+
}: {
|
|
145
|
+
task: ReturnType<typeof useFetchDebugTask>;
|
|
146
|
+
}) {
|
|
147
|
+
const [expanded, setExpanded] = useState(false);
|
|
148
|
+
|
|
149
|
+
if (task.status === "idle" || task.status === "loading") return null;
|
|
150
|
+
|
|
151
|
+
if (task.status === "error") {
|
|
152
|
+
return (
|
|
153
|
+
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-2">
|
|
154
|
+
<p className="text-[11px] text-destructive">{task.error ?? "请求失败"}</p>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = task.result;
|
|
160
|
+
if (!result) return null;
|
|
161
|
+
|
|
162
|
+
const bodyText = formatFetchDebugData(result.data);
|
|
163
|
+
const preview =
|
|
164
|
+
bodyText.length > 120 ? `${bodyText.slice(0, 120).trimEnd()}…` : bodyText;
|
|
165
|
+
const ok = result.status >= 200 && result.status < 300;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="rounded-md border border-border/70 bg-background/80">
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="flex w-full items-center gap-2 px-2 py-1.5 text-left"
|
|
172
|
+
onClick={() => setExpanded((value) => !value)}
|
|
173
|
+
>
|
|
174
|
+
<ChevronIcon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" expanded={expanded} />
|
|
175
|
+
<span className="text-[11px] font-medium text-foreground">调试响应</span>
|
|
176
|
+
<span
|
|
177
|
+
className={cn(
|
|
178
|
+
"rounded px-1.5 py-0.5 font-mono text-[10px]",
|
|
179
|
+
ok
|
|
180
|
+
? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300"
|
|
181
|
+
: "bg-destructive/15 text-destructive"
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
{result.status} {result.statusText}
|
|
185
|
+
</span>
|
|
186
|
+
{!expanded ? (
|
|
187
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
188
|
+
{preview}
|
|
189
|
+
</span>
|
|
190
|
+
) : null}
|
|
191
|
+
</button>
|
|
192
|
+
{expanded ? (
|
|
193
|
+
<div className="border-t border-border/60 px-2 py-2">
|
|
194
|
+
<div className="mb-1 truncate font-mono text-[10px] text-muted-foreground">
|
|
195
|
+
{result.url}
|
|
196
|
+
</div>
|
|
197
|
+
<pre className="max-h-56 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/30 p-2 font-mono text-[10px] leading-relaxed text-foreground">
|
|
198
|
+
{bodyText}
|
|
199
|
+
</pre>
|
|
200
|
+
</div>
|
|
201
|
+
) : null}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function LoaderIcon({ className }: { className?: string }) {
|
|
207
|
+
return (
|
|
208
|
+
<svg
|
|
209
|
+
viewBox="0 0 24 24"
|
|
210
|
+
className={cn("animate-spin", className)}
|
|
211
|
+
fill="none"
|
|
212
|
+
stroke="currentColor"
|
|
213
|
+
strokeWidth="2"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
>
|
|
216
|
+
<path d="M12 2v4" />
|
|
217
|
+
<path d="m16.2 7.8 2.9-2.9" />
|
|
218
|
+
<path d="M18 12h4" />
|
|
219
|
+
<path d="m16.2 16.2 2.9 2.9" />
|
|
220
|
+
<path d="M12 18v4" />
|
|
221
|
+
<path d="m4.9 19.1 2.9-2.9" />
|
|
222
|
+
<path d="M2 12h4" />
|
|
223
|
+
<path d="m4.9 4.9 2.9 2.9" />
|
|
224
|
+
</svg>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type FetchNodeConfigPanelProps = {
|
|
229
|
+
node: BlueprintGraphNode;
|
|
230
|
+
onUpdateNode: (
|
|
231
|
+
nodeId: string,
|
|
232
|
+
patch: Partial<Pick<BlueprintGraphNode, "fetchConfig" | "configSource">>
|
|
233
|
+
) => void;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
function patchFetchConfig(
|
|
237
|
+
node: BlueprintGraphNode,
|
|
238
|
+
patch: Partial<FetchRequestConfig>
|
|
239
|
+
) {
|
|
240
|
+
return {
|
|
241
|
+
fetchConfig: { ...resolveNodeFetchConfig(node), ...patch },
|
|
242
|
+
configSource: "fetch" as const,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function FetchNodeConfigPanel({
|
|
247
|
+
node,
|
|
248
|
+
onUpdateNode,
|
|
249
|
+
}: FetchNodeConfigPanelProps) {
|
|
250
|
+
const fetchConfig = resolveNodeFetchConfig(node);
|
|
251
|
+
const endpoints = fetchConfig.swaggerEndpoints ?? [];
|
|
252
|
+
const hasSwaggerEndpoints = endpoints.length > 0;
|
|
253
|
+
const urlInputMode =
|
|
254
|
+
fetchConfig.urlInputMode ?? (hasSwaggerEndpoints ? "swagger" : "manual");
|
|
255
|
+
const swaggerTask = useSwaggerLoadTask(node.id);
|
|
256
|
+
const fetchDebugTask = useFetchDebugTask(node.id);
|
|
257
|
+
const loadingSwagger = swaggerTask.status === "loading";
|
|
258
|
+
const loadingFetchDebug = fetchDebugTask.status === "loading";
|
|
259
|
+
const swaggerTaskError =
|
|
260
|
+
swaggerTask.status === "error" ? swaggerTask.error : null;
|
|
261
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
262
|
+
const [fetchValidationError, setFetchValidationError] = useState<string | null>(null);
|
|
263
|
+
const swaggerError = validationError ?? swaggerTaskError;
|
|
264
|
+
|
|
265
|
+
const setUrlInputMode = useCallback(
|
|
266
|
+
(mode: "swagger" | "manual") => {
|
|
267
|
+
onUpdateNode(node.id, patchFetchConfig(node, { urlInputMode: mode }));
|
|
268
|
+
},
|
|
269
|
+
[node, onUpdateNode]
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const handleLoadSwagger = useCallback(() => {
|
|
273
|
+
const docsUrl = fetchConfig.swaggerDocsUrl?.trim();
|
|
274
|
+
if (!docsUrl) {
|
|
275
|
+
setValidationError("请先填写 Swagger 文档 URL");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
setValidationError(null);
|
|
280
|
+
startSwaggerLoadTask({
|
|
281
|
+
nodeId: node.id,
|
|
282
|
+
docsUrl,
|
|
283
|
+
onSuccess: (parsed, url) => {
|
|
284
|
+
onUpdateNode(
|
|
285
|
+
node.id,
|
|
286
|
+
patchFetchConfig(node, {
|
|
287
|
+
swaggerDocsUrl: url,
|
|
288
|
+
apiBaseUrl: parsed.apiBaseUrl,
|
|
289
|
+
swaggerEndpoints: parsed.endpoints,
|
|
290
|
+
urlInputMode: "swagger",
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}, [fetchConfig.swaggerDocsUrl, node, onUpdateNode]);
|
|
296
|
+
|
|
297
|
+
const handleAbortSwagger = useCallback(() => {
|
|
298
|
+
cancelSwaggerLoadTask(node.id);
|
|
299
|
+
setValidationError(null);
|
|
300
|
+
}, [node.id]);
|
|
301
|
+
|
|
302
|
+
const handleSendFetchDebug = useCallback(() => {
|
|
303
|
+
const url = fetchConfig.url?.trim();
|
|
304
|
+
if (!url) {
|
|
305
|
+
setFetchValidationError("请先填写请求 URL");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setFetchValidationError(null);
|
|
310
|
+
startFetchDebugTask({
|
|
311
|
+
nodeId: node.id,
|
|
312
|
+
config: fetchConfig,
|
|
313
|
+
});
|
|
314
|
+
}, [fetchConfig, node.id]);
|
|
315
|
+
|
|
316
|
+
const handleAbortFetchDebug = useCallback(() => {
|
|
317
|
+
cancelFetchDebugTask(node.id);
|
|
318
|
+
setFetchValidationError(null);
|
|
319
|
+
}, [node.id]);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
|
|
323
|
+
<div className="font-medium text-foreground">数据源获取 (Fetch)</div>
|
|
324
|
+
<p className="text-[11px] text-muted-foreground">
|
|
325
|
+
收到<strong>真信号</strong>后发起 HTTP 请求;可导入 Swagger 文档后从接口列表联想选择
|
|
326
|
+
URL。
|
|
327
|
+
</p>
|
|
328
|
+
|
|
329
|
+
<label className="block space-y-1">
|
|
330
|
+
<span className="text-muted-foreground">Swagger 文档 URL(可选)</span>
|
|
331
|
+
<div className="flex gap-1.5">
|
|
332
|
+
<Input
|
|
333
|
+
value={fetchConfig.swaggerDocsUrl ?? ""}
|
|
334
|
+
disabled={loadingSwagger}
|
|
335
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
336
|
+
onUpdateNode(
|
|
337
|
+
node.id,
|
|
338
|
+
patchFetchConfig(node, { swaggerDocsUrl: e.target.value })
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
placeholder="https://example.com/v3/api-docs"
|
|
342
|
+
className="h-8 flex-1 font-mono text-[11px]"
|
|
343
|
+
/>
|
|
344
|
+
{loadingSwagger ? (
|
|
345
|
+
<Button
|
|
346
|
+
type="button"
|
|
347
|
+
variant="outline"
|
|
348
|
+
size="icon"
|
|
349
|
+
className="h-8 w-8 shrink-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
350
|
+
title="中止解析"
|
|
351
|
+
aria-label="中止 Swagger 解析"
|
|
352
|
+
onClick={handleAbortSwagger}
|
|
353
|
+
>
|
|
354
|
+
<StopIcon className="h-3.5 w-3.5" />
|
|
355
|
+
</Button>
|
|
356
|
+
) : (
|
|
357
|
+
<Button
|
|
358
|
+
type="button"
|
|
359
|
+
variant="outline"
|
|
360
|
+
size="icon"
|
|
361
|
+
className="h-8 w-8 shrink-0"
|
|
362
|
+
title="解析 Swagger 文档"
|
|
363
|
+
onClick={() => handleLoadSwagger()}
|
|
364
|
+
>
|
|
365
|
+
<SendIcon className="h-4 w-4" />
|
|
366
|
+
</Button>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
{loadingSwagger ? (
|
|
370
|
+
<p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
371
|
+
<LoaderIcon className="h-3.5 w-3.5 shrink-0" />
|
|
372
|
+
正在解析 Swagger 文档,点击右侧按钮可中止…
|
|
373
|
+
</p>
|
|
374
|
+
) : null}
|
|
375
|
+
{swaggerError ? (
|
|
376
|
+
<p className="text-[11px] text-destructive">{swaggerError}</p>
|
|
377
|
+
) : null}
|
|
378
|
+
{endpoints.length > 0 ? (
|
|
379
|
+
<p className="text-[11px] text-muted-foreground">
|
|
380
|
+
已解析 {endpoints.length} 个接口
|
|
381
|
+
</p>
|
|
382
|
+
) : null}
|
|
383
|
+
</label>
|
|
384
|
+
|
|
385
|
+
<label className="block space-y-1">
|
|
386
|
+
<div className="flex items-center justify-between gap-2">
|
|
387
|
+
<span className="text-muted-foreground">请求 URL</span>
|
|
388
|
+
{hasSwaggerEndpoints ? (
|
|
389
|
+
<div className="flex rounded-md border border-border p-0.5">
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
disabled={loadingSwagger}
|
|
393
|
+
className={cn(
|
|
394
|
+
"rounded px-2 py-0.5 text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
395
|
+
urlInputMode === "swagger"
|
|
396
|
+
? "bg-primary text-primary-foreground"
|
|
397
|
+
: "text-muted-foreground hover:text-foreground"
|
|
398
|
+
)}
|
|
399
|
+
onClick={() => setUrlInputMode("swagger")}
|
|
400
|
+
>
|
|
401
|
+
接口联想
|
|
402
|
+
</button>
|
|
403
|
+
<button
|
|
404
|
+
type="button"
|
|
405
|
+
disabled={loadingSwagger}
|
|
406
|
+
className={cn(
|
|
407
|
+
"rounded px-2 py-0.5 text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
408
|
+
urlInputMode === "manual"
|
|
409
|
+
? "bg-primary text-primary-foreground"
|
|
410
|
+
: "text-muted-foreground hover:text-foreground"
|
|
411
|
+
)}
|
|
412
|
+
onClick={() => setUrlInputMode("manual")}
|
|
413
|
+
>
|
|
414
|
+
手动输入
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
) : null}
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
{urlInputMode === "swagger" && hasSwaggerEndpoints ? (
|
|
421
|
+
<div
|
|
422
|
+
className={cn(
|
|
423
|
+
"space-y-2",
|
|
424
|
+
loadingSwagger && "pointer-events-none opacity-60"
|
|
425
|
+
)}
|
|
426
|
+
>
|
|
427
|
+
<label className="block space-y-1">
|
|
428
|
+
<span className="text-muted-foreground">API 主机 / Base URL</span>
|
|
429
|
+
<Input
|
|
430
|
+
value={fetchConfig.apiBaseUrl ?? ""}
|
|
431
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
432
|
+
onUpdateNode(
|
|
433
|
+
node.id,
|
|
434
|
+
patchFetchConfig(node, { apiBaseUrl: e.target.value })
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
placeholder="https://api.example.com/v1"
|
|
438
|
+
className="h-8 font-mono text-[11px]"
|
|
439
|
+
/>
|
|
440
|
+
</label>
|
|
441
|
+
<label className="block space-y-1">
|
|
442
|
+
<span className="text-muted-foreground">选择接口</span>
|
|
443
|
+
<div className="flex gap-1.5">
|
|
444
|
+
<div className="min-w-0 flex-1">
|
|
445
|
+
<FetchUrlAutocomplete
|
|
446
|
+
value={fetchConfig.url}
|
|
447
|
+
apiBaseUrl={fetchConfig.apiBaseUrl ?? ""}
|
|
448
|
+
endpoints={endpoints}
|
|
449
|
+
selectOnly
|
|
450
|
+
placeholder="点击选择或搜索接口"
|
|
451
|
+
onChange={(url) =>
|
|
452
|
+
onUpdateNode(node.id, patchFetchConfig(node, { url }))
|
|
453
|
+
}
|
|
454
|
+
onSelectEndpoint={(endpoint) =>
|
|
455
|
+
onUpdateNode(
|
|
456
|
+
node.id,
|
|
457
|
+
patchFetchConfig(node, {
|
|
458
|
+
url: endpoint.path,
|
|
459
|
+
method: endpoint.method,
|
|
460
|
+
})
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
<AsyncSendButton
|
|
466
|
+
loading={loadingFetchDebug}
|
|
467
|
+
disabled={loadingSwagger}
|
|
468
|
+
onSend={handleSendFetchDebug}
|
|
469
|
+
onAbort={handleAbortFetchDebug}
|
|
470
|
+
sendTitle="发送调试请求"
|
|
471
|
+
abortTitle="中止请求"
|
|
472
|
+
sendAriaLabel="发送调试请求"
|
|
473
|
+
abortAriaLabel="中止调试请求"
|
|
474
|
+
/>
|
|
475
|
+
</div>
|
|
476
|
+
</label>
|
|
477
|
+
</div>
|
|
478
|
+
) : (
|
|
479
|
+
<div className="flex gap-1.5">
|
|
480
|
+
<Input
|
|
481
|
+
value={fetchConfig.url}
|
|
482
|
+
disabled={loadingFetchDebug}
|
|
483
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
484
|
+
onUpdateNode(node.id, patchFetchConfig(node, { url: e.target.value }))
|
|
485
|
+
}
|
|
486
|
+
placeholder="https://api.example.com/data"
|
|
487
|
+
className="h-8 flex-1 font-mono text-[11px]"
|
|
488
|
+
/>
|
|
489
|
+
<AsyncSendButton
|
|
490
|
+
loading={loadingFetchDebug}
|
|
491
|
+
disabled={loadingSwagger}
|
|
492
|
+
onSend={handleSendFetchDebug}
|
|
493
|
+
onAbort={handleAbortFetchDebug}
|
|
494
|
+
sendTitle="发送调试请求"
|
|
495
|
+
abortTitle="中止请求"
|
|
496
|
+
sendAriaLabel="发送调试请求"
|
|
497
|
+
abortAriaLabel="中止调试请求"
|
|
498
|
+
/>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
{loadingFetchDebug ? (
|
|
502
|
+
<p className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
503
|
+
<LoaderIcon className="h-3.5 w-3.5 shrink-0" />
|
|
504
|
+
正在发送调试请求,点击右侧按钮可中止…
|
|
505
|
+
</p>
|
|
506
|
+
) : null}
|
|
507
|
+
{fetchValidationError ? (
|
|
508
|
+
<p className="text-[11px] text-destructive">{fetchValidationError}</p>
|
|
509
|
+
) : null}
|
|
510
|
+
<FetchDebugResponsePanel task={fetchDebugTask} />
|
|
511
|
+
</label>
|
|
512
|
+
|
|
513
|
+
<label className="block space-y-1">
|
|
514
|
+
<span className="text-muted-foreground">请求方法</span>
|
|
515
|
+
<Select
|
|
516
|
+
value={fetchConfig.method ?? "GET"}
|
|
517
|
+
onValueChange={(value: string) =>
|
|
518
|
+
onUpdateNode(
|
|
519
|
+
node.id,
|
|
520
|
+
patchFetchConfig(node, { method: value as FetchHttpMethod })
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
>
|
|
524
|
+
<SelectTrigger className="h-8">
|
|
525
|
+
<SelectValue />
|
|
526
|
+
</SelectTrigger>
|
|
527
|
+
<SelectContent>
|
|
528
|
+
{FETCH_HTTP_METHODS.map((method) => (
|
|
529
|
+
<SelectItem key={method} value={method}>
|
|
530
|
+
{method}
|
|
531
|
+
</SelectItem>
|
|
532
|
+
))}
|
|
533
|
+
</SelectContent>
|
|
534
|
+
</Select>
|
|
535
|
+
</label>
|
|
536
|
+
|
|
537
|
+
<label className="block space-y-1">
|
|
538
|
+
<span className="text-muted-foreground">请求头 (JSON)</span>
|
|
539
|
+
<textarea
|
|
540
|
+
defaultValue={JSON.stringify(fetchConfig.headers ?? {}, null, 2)}
|
|
541
|
+
key={`${node.id}-headers-${JSON.stringify(fetchConfig.headers)}`}
|
|
542
|
+
onBlur={(e) => {
|
|
543
|
+
try {
|
|
544
|
+
const headers = JSON.parse(e.target.value || "{}") as Record<
|
|
545
|
+
string,
|
|
546
|
+
string
|
|
547
|
+
>;
|
|
548
|
+
onUpdateNode(node.id, patchFetchConfig(node, { headers }));
|
|
549
|
+
} catch {
|
|
550
|
+
/* 保留上次有效值 */
|
|
551
|
+
}
|
|
552
|
+
}}
|
|
553
|
+
rows={4}
|
|
554
|
+
className="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
|
|
555
|
+
placeholder='{"Content-Type":"application/json"}'
|
|
556
|
+
/>
|
|
557
|
+
</label>
|
|
558
|
+
|
|
559
|
+
<label className="block space-y-1">
|
|
560
|
+
<span className="text-muted-foreground">请求体</span>
|
|
561
|
+
<textarea
|
|
562
|
+
value={fetchConfig.body ?? ""}
|
|
563
|
+
onChange={(e) =>
|
|
564
|
+
onUpdateNode(node.id, patchFetchConfig(node, { body: e.target.value }))
|
|
565
|
+
}
|
|
566
|
+
rows={4}
|
|
567
|
+
className="w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary"
|
|
568
|
+
placeholder='{"key":"value"}'
|
|
569
|
+
/>
|
|
570
|
+
</label>
|
|
571
|
+
|
|
572
|
+
<div className="grid grid-cols-2 gap-2">
|
|
573
|
+
<label className="block space-y-1">
|
|
574
|
+
<span className="text-muted-foreground">Credentials</span>
|
|
575
|
+
<Select
|
|
576
|
+
value={fetchConfig.credentials ?? "same-origin"}
|
|
577
|
+
onValueChange={(value: string) =>
|
|
578
|
+
onUpdateNode(
|
|
579
|
+
node.id,
|
|
580
|
+
patchFetchConfig(node, {
|
|
581
|
+
credentials: value as RequestCredentials,
|
|
582
|
+
})
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
>
|
|
586
|
+
<SelectTrigger className="h-8">
|
|
587
|
+
<SelectValue />
|
|
588
|
+
</SelectTrigger>
|
|
589
|
+
<SelectContent>
|
|
590
|
+
{FETCH_CREDENTIALS.map((item) => (
|
|
591
|
+
<SelectItem key={item} value={item}>
|
|
592
|
+
{item}
|
|
593
|
+
</SelectItem>
|
|
594
|
+
))}
|
|
595
|
+
</SelectContent>
|
|
596
|
+
</Select>
|
|
597
|
+
</label>
|
|
598
|
+
<label className="block space-y-1">
|
|
599
|
+
<span className="text-muted-foreground">Mode</span>
|
|
600
|
+
<Select
|
|
601
|
+
value={fetchConfig.mode ?? "cors"}
|
|
602
|
+
onValueChange={(value: string) =>
|
|
603
|
+
onUpdateNode(
|
|
604
|
+
node.id,
|
|
605
|
+
patchFetchConfig(node, { mode: value as RequestMode })
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
>
|
|
609
|
+
<SelectTrigger className="h-8">
|
|
610
|
+
<SelectValue />
|
|
611
|
+
</SelectTrigger>
|
|
612
|
+
<SelectContent>
|
|
613
|
+
{FETCH_MODES.map((item) => (
|
|
614
|
+
<SelectItem key={item} value={item}>
|
|
615
|
+
{item}
|
|
616
|
+
</SelectItem>
|
|
617
|
+
))}
|
|
618
|
+
</SelectContent>
|
|
619
|
+
</Select>
|
|
620
|
+
</label>
|
|
621
|
+
<label className="block space-y-1">
|
|
622
|
+
<span className="text-muted-foreground">Cache</span>
|
|
623
|
+
<Select
|
|
624
|
+
value={fetchConfig.cache ?? "default"}
|
|
625
|
+
onValueChange={(value: string) =>
|
|
626
|
+
onUpdateNode(
|
|
627
|
+
node.id,
|
|
628
|
+
patchFetchConfig(node, { cache: value as RequestCache })
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
>
|
|
632
|
+
<SelectTrigger className="h-8">
|
|
633
|
+
<SelectValue />
|
|
634
|
+
</SelectTrigger>
|
|
635
|
+
<SelectContent>
|
|
636
|
+
{FETCH_CACHES.map((item) => (
|
|
637
|
+
<SelectItem key={item} value={item}>
|
|
638
|
+
{item}
|
|
639
|
+
</SelectItem>
|
|
640
|
+
))}
|
|
641
|
+
</SelectContent>
|
|
642
|
+
</Select>
|
|
643
|
+
</label>
|
|
644
|
+
<label className="block space-y-1">
|
|
645
|
+
<span className="text-muted-foreground">Redirect</span>
|
|
646
|
+
<Select
|
|
647
|
+
value={fetchConfig.redirect ?? "follow"}
|
|
648
|
+
onValueChange={(value: string) =>
|
|
649
|
+
onUpdateNode(
|
|
650
|
+
node.id,
|
|
651
|
+
patchFetchConfig(node, { redirect: value as RequestRedirect })
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
>
|
|
655
|
+
<SelectTrigger className="h-8">
|
|
656
|
+
<SelectValue />
|
|
657
|
+
</SelectTrigger>
|
|
658
|
+
<SelectContent>
|
|
659
|
+
{FETCH_REDIRECTS.map((item) => (
|
|
660
|
+
<SelectItem key={item} value={item}>
|
|
661
|
+
{item}
|
|
662
|
+
</SelectItem>
|
|
663
|
+
))}
|
|
664
|
+
</SelectContent>
|
|
665
|
+
</Select>
|
|
666
|
+
</label>
|
|
667
|
+
</div>
|
|
668
|
+
|
|
669
|
+
<div className="grid grid-cols-2 gap-2">
|
|
670
|
+
<label className="block space-y-1">
|
|
671
|
+
<span className="text-muted-foreground">响应解析</span>
|
|
672
|
+
<Select
|
|
673
|
+
value={fetchConfig.responseType ?? "json"}
|
|
674
|
+
onValueChange={(value: string) =>
|
|
675
|
+
onUpdateNode(
|
|
676
|
+
node.id,
|
|
677
|
+
patchFetchConfig(node, {
|
|
678
|
+
responseType: value as FetchResponseType,
|
|
679
|
+
})
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
>
|
|
683
|
+
<SelectTrigger className="h-8">
|
|
684
|
+
<SelectValue />
|
|
685
|
+
</SelectTrigger>
|
|
686
|
+
<SelectContent>
|
|
687
|
+
{FETCH_RESPONSE_TYPES.map((item) => (
|
|
688
|
+
<SelectItem key={item} value={item}>
|
|
689
|
+
{item}
|
|
690
|
+
</SelectItem>
|
|
691
|
+
))}
|
|
692
|
+
</SelectContent>
|
|
693
|
+
</Select>
|
|
694
|
+
</label>
|
|
695
|
+
<label className="block space-y-1">
|
|
696
|
+
<span className="text-muted-foreground">超时 (ms)</span>
|
|
697
|
+
<Input
|
|
698
|
+
type="number"
|
|
699
|
+
min={0}
|
|
700
|
+
step={1000}
|
|
701
|
+
value={fetchConfig.timeoutMs ?? 30000}
|
|
702
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
703
|
+
onUpdateNode(
|
|
704
|
+
node.id,
|
|
705
|
+
patchFetchConfig(node, {
|
|
706
|
+
timeoutMs: Number(e.target.value) || 0,
|
|
707
|
+
})
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
className="h-8"
|
|
711
|
+
/>
|
|
712
|
+
</label>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
);
|
|
716
|
+
}
|