@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,427 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
Input,
|
|
12
|
+
Label,
|
|
13
|
+
Tooltip,
|
|
14
|
+
TooltipContent,
|
|
15
|
+
TooltipProvider,
|
|
16
|
+
TooltipTrigger,
|
|
17
|
+
cn,
|
|
18
|
+
} from "@arronqzy/ui";
|
|
19
|
+
import {
|
|
20
|
+
PAGE_LIFECYCLE_LABELS,
|
|
21
|
+
type ExecutionTraceEntry,
|
|
22
|
+
type PageLifecyclePhase,
|
|
23
|
+
} from "@arronqzy/blueprint-dsl";
|
|
24
|
+
|
|
25
|
+
import type { ExecutionLogSettings } from "../library/execution-log-settings";
|
|
26
|
+
|
|
27
|
+
function IconClear({ className }: { className?: string }) {
|
|
28
|
+
return (
|
|
29
|
+
<svg
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
className={className}
|
|
32
|
+
fill="none"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
strokeWidth="2"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
>
|
|
37
|
+
<path d="M3 6h18" />
|
|
38
|
+
<path d="M8 6V4h8v2" />
|
|
39
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
|
40
|
+
<path d="M10 11v6M14 11v6" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function IconClearDb({ className }: { className?: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<svg
|
|
48
|
+
viewBox="0 0 24 24"
|
|
49
|
+
className={className}
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
strokeWidth="2"
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
>
|
|
55
|
+
<ellipse cx="12" cy="6" rx="7" ry="2.5" />
|
|
56
|
+
<path d="M5 6v12c0 1.5 3.1 2.5 7 2.5s7-1 2.5-7-2.5V6" />
|
|
57
|
+
<path d="M5 12c0 1.5 3.1 2.5 7 2.5s7-1 7-2.5" />
|
|
58
|
+
<path d="m16 16 4 4M20 16l-4 4" />
|
|
59
|
+
</svg>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ArrowDownIcon({ className }: { className?: string }) {
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
className={className}
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
strokeWidth="2"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
>
|
|
73
|
+
<path d="M12 5v14" />
|
|
74
|
+
<path d="m19 12-7 7-7-7" />
|
|
75
|
+
</svg>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatJson(value: unknown) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(value, null, 2);
|
|
82
|
+
} catch {
|
|
83
|
+
return String(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function LogTooltip({
|
|
88
|
+
content,
|
|
89
|
+
children,
|
|
90
|
+
}: {
|
|
91
|
+
content: string;
|
|
92
|
+
children: ReactElement;
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<Tooltip>
|
|
96
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
97
|
+
<TooltipContent side="top" className="max-w-[260px] text-[11px]">
|
|
98
|
+
{content}
|
|
99
|
+
</TooltipContent>
|
|
100
|
+
</Tooltip>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function LogTooltipButton({
|
|
105
|
+
content,
|
|
106
|
+
disabled,
|
|
107
|
+
className,
|
|
108
|
+
...props
|
|
109
|
+
}: React.ComponentProps<typeof Button> & { content: string }) {
|
|
110
|
+
const button = (
|
|
111
|
+
<Button disabled={disabled} className={className} {...props} />
|
|
112
|
+
);
|
|
113
|
+
return (
|
|
114
|
+
<LogTooltip content={content}>
|
|
115
|
+
{disabled ? <span className="inline-flex w-full">{button}</span> : button}
|
|
116
|
+
</LogTooltip>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const SCROLL_BOTTOM_THRESHOLD = 48;
|
|
121
|
+
|
|
122
|
+
function isScrollNearBottom(element: HTMLElement) {
|
|
123
|
+
return (
|
|
124
|
+
element.scrollHeight - element.scrollTop - element.clientHeight <=
|
|
125
|
+
SCROLL_BOTTOM_THRESHOLD
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type BlueprintExecutionLogPanelProps = {
|
|
130
|
+
entries: ExecutionTraceEntry[];
|
|
131
|
+
settings: ExecutionLogSettings;
|
|
132
|
+
onUpdateSettings: (patch: Partial<ExecutionLogSettings>) => void;
|
|
133
|
+
onSave: () => void;
|
|
134
|
+
onExport: () => void;
|
|
135
|
+
onClear: () => void;
|
|
136
|
+
onClearAllSaved?: () => void | Promise<void>;
|
|
137
|
+
onApplyRetention: () => void;
|
|
138
|
+
hasSavedRuns?: boolean;
|
|
139
|
+
lifecyclePhase?: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export function BlueprintExecutionLogPanel({
|
|
143
|
+
entries,
|
|
144
|
+
settings,
|
|
145
|
+
onUpdateSettings,
|
|
146
|
+
onSave,
|
|
147
|
+
onExport,
|
|
148
|
+
onClear,
|
|
149
|
+
onClearAllSaved,
|
|
150
|
+
onApplyRetention,
|
|
151
|
+
hasSavedRuns = false,
|
|
152
|
+
lifecyclePhase,
|
|
153
|
+
}: BlueprintExecutionLogPanelProps) {
|
|
154
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
155
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
156
|
+
const prevEntryCountRef = useRef(entries.length);
|
|
157
|
+
const stickToBottomRef = useRef(true);
|
|
158
|
+
|
|
159
|
+
const [hasNewBelow, setHasNewBelow] = useState(false);
|
|
160
|
+
|
|
161
|
+
const phaseLabel = useMemo(() => {
|
|
162
|
+
if (!lifecyclePhase) return null;
|
|
163
|
+
return PAGE_LIFECYCLE_LABELS[lifecyclePhase as PageLifecyclePhase] ?? lifecyclePhase;
|
|
164
|
+
}, [lifecyclePhase]);
|
|
165
|
+
|
|
166
|
+
const latestEntryPreview = useMemo(() => {
|
|
167
|
+
const latest = entries[entries.length - 1];
|
|
168
|
+
if (!latest) return "";
|
|
169
|
+
return latest.nodeLabel ?? latest.nodeId;
|
|
170
|
+
}, [entries]);
|
|
171
|
+
|
|
172
|
+
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
|
173
|
+
bottomRef.current?.scrollIntoView({ behavior, block: "end" });
|
|
174
|
+
stickToBottomRef.current = true;
|
|
175
|
+
setHasNewBelow(false);
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const handleScroll = useCallback(() => {
|
|
179
|
+
const el = scrollRef.current;
|
|
180
|
+
if (!el) return;
|
|
181
|
+
const nearBottom = isScrollNearBottom(el);
|
|
182
|
+
stickToBottomRef.current = nearBottom;
|
|
183
|
+
if (nearBottom) {
|
|
184
|
+
setHasNewBelow(false);
|
|
185
|
+
}
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const prevCount = prevEntryCountRef.current;
|
|
190
|
+
prevEntryCountRef.current = entries.length;
|
|
191
|
+
|
|
192
|
+
if (entries.length === 0) {
|
|
193
|
+
setHasNewBelow(false);
|
|
194
|
+
stickToBottomRef.current = true;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (entries.length <= prevCount) return;
|
|
199
|
+
|
|
200
|
+
if (stickToBottomRef.current) {
|
|
201
|
+
scrollToBottom(prevCount === 0 ? "auto" : "smooth");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setHasNewBelow(true);
|
|
206
|
+
}, [entries, scrollToBottom]);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<TooltipProvider delayDuration={200}>
|
|
210
|
+
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-background text-foreground">
|
|
211
|
+
<div className="shrink-0 border-b border-border px-3 py-2">
|
|
212
|
+
<div className="flex items-start justify-between gap-2">
|
|
213
|
+
<div className="min-w-0">
|
|
214
|
+
<div className="text-xs font-semibold">蓝图任务输出日志</div>
|
|
215
|
+
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
|
216
|
+
{phaseLabel ? `模拟场景:${phaseLabel}` : "选择生命周期节点并开始调试"}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
220
|
+
{onClearAllSaved ? (
|
|
221
|
+
<LogTooltipButton
|
|
222
|
+
type="button"
|
|
223
|
+
variant="outline"
|
|
224
|
+
size="icon"
|
|
225
|
+
className="h-7 w-7 shrink-0"
|
|
226
|
+
content="立即清空 IndexedDB 中所有已保存的蓝图执行日志"
|
|
227
|
+
aria-label="清空 IndexedDB 日志"
|
|
228
|
+
disabled={!hasSavedRuns}
|
|
229
|
+
onClick={() => void onClearAllSaved()}
|
|
230
|
+
>
|
|
231
|
+
<IconClearDb className="h-3.5 w-3.5" />
|
|
232
|
+
</LogTooltipButton>
|
|
233
|
+
) : null}
|
|
234
|
+
<LogTooltipButton
|
|
235
|
+
type="button"
|
|
236
|
+
variant="outline"
|
|
237
|
+
size="icon"
|
|
238
|
+
className="h-7 w-7 shrink-0"
|
|
239
|
+
content="清空当前日志,并重置画布执行高亮"
|
|
240
|
+
aria-label="清空日志"
|
|
241
|
+
disabled={entries.length === 0}
|
|
242
|
+
onClick={onClear}
|
|
243
|
+
>
|
|
244
|
+
<IconClear className="h-3.5 w-3.5" />
|
|
245
|
+
</LogTooltipButton>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div className="relative min-h-0 flex-1">
|
|
251
|
+
<div
|
|
252
|
+
ref={scrollRef}
|
|
253
|
+
className="h-full overflow-auto p-3"
|
|
254
|
+
onScroll={handleScroll}
|
|
255
|
+
>
|
|
256
|
+
{entries.length === 0 ? (
|
|
257
|
+
<p className="text-[11px] text-muted-foreground">
|
|
258
|
+
暂无执行记录。使用工具栏「走完全流程」或「下一步」开始模拟。
|
|
259
|
+
</p>
|
|
260
|
+
) : (
|
|
261
|
+
<div className="space-y-0">
|
|
262
|
+
{entries.map((entry, index) => (
|
|
263
|
+
<div key={entry.id}>
|
|
264
|
+
<div className="rounded-md border border-border/70 bg-muted/20 p-2.5">
|
|
265
|
+
<div className="flex items-start justify-between gap-2">
|
|
266
|
+
<div>
|
|
267
|
+
<div className="text-xs font-medium text-foreground">
|
|
268
|
+
{entry.nodeLabel ?? entry.nodeId}
|
|
269
|
+
</div>
|
|
270
|
+
<div className="mt-0.5 font-mono text-[10px] text-muted-foreground">
|
|
271
|
+
{entry.nodeType} · {entry.nodeId}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
<time className="shrink-0 text-[10px] text-muted-foreground">
|
|
275
|
+
{entry.isoTime}
|
|
276
|
+
</time>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="mt-2 grid gap-2">
|
|
280
|
+
<div>
|
|
281
|
+
<div className="mb-1 text-[10px] font-medium text-muted-foreground">
|
|
282
|
+
输入
|
|
283
|
+
</div>
|
|
284
|
+
<pre className="max-h-32 overflow-auto rounded border border-border/60 bg-background p-2 font-mono text-[10px] leading-relaxed text-foreground">
|
|
285
|
+
{formatJson(entry.inputs)}
|
|
286
|
+
</pre>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<div className="mb-1 text-[10px] font-medium text-muted-foreground">
|
|
290
|
+
输出
|
|
291
|
+
</div>
|
|
292
|
+
<pre
|
|
293
|
+
className={cn(
|
|
294
|
+
"max-h-32 overflow-auto rounded border p-2 font-mono text-[10px] leading-relaxed",
|
|
295
|
+
entry.error
|
|
296
|
+
? "border-destructive/40 bg-destructive/5 text-destructive"
|
|
297
|
+
: "border-border/60 bg-background text-foreground"
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{entry.error ?? formatJson(entry.outputs)}
|
|
301
|
+
</pre>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
{index < entries.length - 1 ? (
|
|
306
|
+
<div className="flex justify-center py-1 text-muted-foreground">
|
|
307
|
+
<ArrowDownIcon className="h-4 w-4" />
|
|
308
|
+
</div>
|
|
309
|
+
) : null}
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
<div ref={bottomRef} className="h-px shrink-0" aria-hidden="true" />
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{hasNewBelow ? (
|
|
318
|
+
<div className="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center px-3">
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
className="pointer-events-auto inline-flex max-w-full items-center gap-1.5 rounded-full border border-primary/30 bg-primary px-3 py-1.5 text-[11px] font-medium text-primary-foreground shadow-md transition hover:bg-primary/90"
|
|
322
|
+
onClick={() => scrollToBottom("smooth")}
|
|
323
|
+
>
|
|
324
|
+
<ArrowDownIcon className="h-3.5 w-3.5 shrink-0" />
|
|
325
|
+
<span className="truncate">有新日志</span>
|
|
326
|
+
{latestEntryPreview ? (
|
|
327
|
+
<span className="truncate opacity-90">· {latestEntryPreview}</span>
|
|
328
|
+
) : null}
|
|
329
|
+
</button>
|
|
330
|
+
</div>
|
|
331
|
+
) : null}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div className="shrink-0 space-y-2 border-t border-border p-3 text-xs">
|
|
335
|
+
<div className="grid grid-cols-2 gap-2">
|
|
336
|
+
<LogTooltipButton
|
|
337
|
+
type="button"
|
|
338
|
+
variant="outline"
|
|
339
|
+
size="sm"
|
|
340
|
+
className="w-full"
|
|
341
|
+
content="将当前调试日志保存到 IndexedDB"
|
|
342
|
+
disabled={entries.length === 0}
|
|
343
|
+
onClick={onSave}
|
|
344
|
+
>
|
|
345
|
+
保存日志
|
|
346
|
+
</LogTooltipButton>
|
|
347
|
+
<LogTooltipButton
|
|
348
|
+
type="button"
|
|
349
|
+
variant="outline"
|
|
350
|
+
size="sm"
|
|
351
|
+
className="w-full"
|
|
352
|
+
content="将当前调试日志导出为 JSON 文件"
|
|
353
|
+
disabled={entries.length === 0}
|
|
354
|
+
onClick={onExport}
|
|
355
|
+
>
|
|
356
|
+
导出 JSON
|
|
357
|
+
</LogTooltipButton>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<LogTooltip content="调试运行完成后,自动将日志写入 IndexedDB">
|
|
361
|
+
<label className="flex cursor-default items-center gap-2">
|
|
362
|
+
<input
|
|
363
|
+
type="checkbox"
|
|
364
|
+
checked={settings.autoSave}
|
|
365
|
+
onChange={(e) => onUpdateSettings({ autoSave: e.target.checked })}
|
|
366
|
+
className="h-3.5 w-3.5 rounded border border-input"
|
|
367
|
+
/>
|
|
368
|
+
<span className="text-[11px] text-muted-foreground">
|
|
369
|
+
运行完成后自动保存到 IndexedDB
|
|
370
|
+
</span>
|
|
371
|
+
</label>
|
|
372
|
+
</LogTooltip>
|
|
373
|
+
|
|
374
|
+
<div className="grid grid-cols-2 gap-2">
|
|
375
|
+
<LogTooltip content="IndexedDB 中最多保留的日志条数,超出后自动删除最旧记录">
|
|
376
|
+
<div className="space-y-1">
|
|
377
|
+
<Label className="text-[11px] text-muted-foreground">最多保存条数</Label>
|
|
378
|
+
<Input
|
|
379
|
+
type="number"
|
|
380
|
+
min={1}
|
|
381
|
+
step={1}
|
|
382
|
+
value={settings.maxSavedRuns}
|
|
383
|
+
onChange={(e) =>
|
|
384
|
+
onUpdateSettings({
|
|
385
|
+
maxSavedRuns: Math.max(1, Number(e.target.value) || 1),
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
className="h-8"
|
|
389
|
+
aria-label="IndexedDB 最多保存日志条数"
|
|
390
|
+
/>
|
|
391
|
+
</div>
|
|
392
|
+
</LogTooltip>
|
|
393
|
+
<LogTooltip content="超过保留天数的已保存日志将在清理时被删除">
|
|
394
|
+
<div className="space-y-1">
|
|
395
|
+
<Label className="text-[11px] text-muted-foreground">保留天数</Label>
|
|
396
|
+
<Input
|
|
397
|
+
type="number"
|
|
398
|
+
min={1}
|
|
399
|
+
step={1}
|
|
400
|
+
value={settings.retentionDays}
|
|
401
|
+
onChange={(e) =>
|
|
402
|
+
onUpdateSettings({
|
|
403
|
+
retentionDays: Math.max(1, Number(e.target.value) || 1),
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
className="h-8"
|
|
407
|
+
aria-label="日志保留天数"
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
</LogTooltip>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<LogTooltipButton
|
|
414
|
+
type="button"
|
|
415
|
+
variant="secondary"
|
|
416
|
+
size="sm"
|
|
417
|
+
className="w-full"
|
|
418
|
+
content="按保留天数与条数上限,清理 IndexedDB 中的日志记录"
|
|
419
|
+
onClick={onApplyRetention}
|
|
420
|
+
>
|
|
421
|
+
清理过期与超额
|
|
422
|
+
</LogTooltipButton>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</TooltipProvider>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Select,
|
|
3
|
+
SelectContent,
|
|
4
|
+
SelectGroup,
|
|
5
|
+
SelectItem,
|
|
6
|
+
SelectLabel,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@arronqzy/ui";
|
|
10
|
+
|
|
11
|
+
import type { BlueprintLibraryListItem } from "../library/types";
|
|
12
|
+
|
|
13
|
+
export type BlueprintLibrarySelectProps = {
|
|
14
|
+
items: BlueprintLibraryListItem[];
|
|
15
|
+
value: string | null;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
onSelect: (id: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function groupItems(items: BlueprintLibraryListItem[]) {
|
|
21
|
+
const saved = items.filter((item) => item.source === "saved");
|
|
22
|
+
const imported = items.filter((item) => item.source === "imported");
|
|
23
|
+
return { saved, imported };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function BlueprintLibrarySelect({
|
|
27
|
+
items,
|
|
28
|
+
value,
|
|
29
|
+
placeholder = "蓝图库",
|
|
30
|
+
onSelect,
|
|
31
|
+
}: BlueprintLibrarySelectProps) {
|
|
32
|
+
const { saved, imported } = groupItems(items);
|
|
33
|
+
const hasItems = items.length > 0;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Select
|
|
37
|
+
value={value ?? undefined}
|
|
38
|
+
onValueChange={onSelect}
|
|
39
|
+
disabled={!hasItems}
|
|
40
|
+
>
|
|
41
|
+
<SelectTrigger className="h-7 w-[168px] text-xs">
|
|
42
|
+
<SelectValue placeholder={hasItems ? placeholder : "蓝图库为空"} />
|
|
43
|
+
</SelectTrigger>
|
|
44
|
+
<SelectContent className="z-[10100]">
|
|
45
|
+
{saved.length > 0 ? (
|
|
46
|
+
<SelectGroup>
|
|
47
|
+
<SelectLabel>已保存</SelectLabel>
|
|
48
|
+
{saved.map((item) => (
|
|
49
|
+
<SelectItem key={item.id} value={item.id} className="text-xs">
|
|
50
|
+
{item.name}
|
|
51
|
+
</SelectItem>
|
|
52
|
+
))}
|
|
53
|
+
</SelectGroup>
|
|
54
|
+
) : null}
|
|
55
|
+
{imported.length > 0 ? (
|
|
56
|
+
<SelectGroup>
|
|
57
|
+
<SelectLabel>已导入</SelectLabel>
|
|
58
|
+
{imported.map((item) => (
|
|
59
|
+
<SelectItem key={item.id} value={item.id} className="text-xs">
|
|
60
|
+
{item.name}
|
|
61
|
+
</SelectItem>
|
|
62
|
+
))}
|
|
63
|
+
</SelectGroup>
|
|
64
|
+
) : null}
|
|
65
|
+
</SelectContent>
|
|
66
|
+
</Select>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
Input,
|
|
11
|
+
Label,
|
|
12
|
+
Textarea,
|
|
13
|
+
} from "@arronqzy/ui";
|
|
14
|
+
|
|
15
|
+
import type { BlueprintMetaDraft } from "../library/types";
|
|
16
|
+
|
|
17
|
+
export type BlueprintMetaDialogProps = {
|
|
18
|
+
open: boolean;
|
|
19
|
+
mode: "export" | "save";
|
|
20
|
+
initialMeta: BlueprintMetaDraft;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
onConfirm: (meta: BlueprintMetaDraft) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function BlueprintMetaDialog({
|
|
26
|
+
open,
|
|
27
|
+
mode,
|
|
28
|
+
initialMeta,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
onConfirm,
|
|
31
|
+
}: BlueprintMetaDialogProps) {
|
|
32
|
+
const [name, setName] = useState(initialMeta.name);
|
|
33
|
+
const [remark, setRemark] = useState(initialMeta.remark);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!open) return;
|
|
37
|
+
setName(initialMeta.name);
|
|
38
|
+
setRemark(initialMeta.remark);
|
|
39
|
+
}, [initialMeta.name, initialMeta.remark, open]);
|
|
40
|
+
|
|
41
|
+
const title = mode === "export" ? "导出蓝图" : "保存蓝图";
|
|
42
|
+
const description =
|
|
43
|
+
mode === "export"
|
|
44
|
+
? "填写蓝图名称与备注,将当前蓝图导出为 JSON 文件。"
|
|
45
|
+
: "填写蓝图名称与备注,将当前蓝图保存到本地蓝图库。";
|
|
46
|
+
const confirmLabel = mode === "export" ? "导出" : "保存";
|
|
47
|
+
|
|
48
|
+
const handleConfirm = () => {
|
|
49
|
+
onConfirm({
|
|
50
|
+
name: name.trim() || "未命名蓝图",
|
|
51
|
+
remark: remark.trim(),
|
|
52
|
+
});
|
|
53
|
+
onOpenChange(false);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
58
|
+
<DialogContent className="sm:max-w-md">
|
|
59
|
+
<DialogHeader>
|
|
60
|
+
<DialogTitle>{title}</DialogTitle>
|
|
61
|
+
<DialogDescription>{description}</DialogDescription>
|
|
62
|
+
</DialogHeader>
|
|
63
|
+
<div className="space-y-3 py-1">
|
|
64
|
+
<div className="space-y-1.5">
|
|
65
|
+
<Label htmlFor="blueprint-meta-name">蓝图名称</Label>
|
|
66
|
+
<Input
|
|
67
|
+
id="blueprint-meta-name"
|
|
68
|
+
value={name}
|
|
69
|
+
onChange={(e) => setName(e.target.value)}
|
|
70
|
+
placeholder="例如:首页初始化流程"
|
|
71
|
+
autoFocus
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="space-y-1.5">
|
|
75
|
+
<Label htmlFor="blueprint-meta-remark">蓝图备注</Label>
|
|
76
|
+
<Textarea
|
|
77
|
+
id="blueprint-meta-remark"
|
|
78
|
+
value={remark}
|
|
79
|
+
onChange={(e) => setRemark(e.target.value)}
|
|
80
|
+
placeholder="可选:描述蓝图用途、触发条件等"
|
|
81
|
+
rows={3}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<DialogFooter>
|
|
86
|
+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
87
|
+
取消
|
|
88
|
+
</Button>
|
|
89
|
+
<Button type="button" onClick={handleConfirm}>
|
|
90
|
+
{confirmLabel}
|
|
91
|
+
</Button>
|
|
92
|
+
</DialogFooter>
|
|
93
|
+
</DialogContent>
|
|
94
|
+
</Dialog>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from "@arronqzy/ui";
|
|
10
|
+
|
|
11
|
+
export type BlueprintNodeSwitchTaskDialogProps = {
|
|
12
|
+
open: boolean;
|
|
13
|
+
fromNodeId: string;
|
|
14
|
+
toNodeId: string | null;
|
|
15
|
+
onOpenChange: (open: boolean) => void;
|
|
16
|
+
onKeepTaskAndSwitch: () => void;
|
|
17
|
+
onCancelTaskAndSwitch: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function BlueprintNodeSwitchTaskDialog({
|
|
21
|
+
open,
|
|
22
|
+
fromNodeId,
|
|
23
|
+
toNodeId,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
onKeepTaskAndSwitch,
|
|
26
|
+
onCancelTaskAndSwitch,
|
|
27
|
+
}: BlueprintNodeSwitchTaskDialogProps) {
|
|
28
|
+
const targetLabel =
|
|
29
|
+
toNodeId === null ? "取消选中" : `节点 ${toNodeId}`;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
33
|
+
<DialogContent className="sm:max-w-md">
|
|
34
|
+
<DialogHeader>
|
|
35
|
+
<DialogTitle>节点正在执行任务</DialogTitle>
|
|
36
|
+
<DialogDescription>
|
|
37
|
+
节点 <span className="font-mono text-foreground">{fromNodeId}</span>{" "}
|
|
38
|
+
正在执行任务(Swagger 解析或请求调试)。是否切换到 {targetLabel}?
|
|
39
|
+
</DialogDescription>
|
|
40
|
+
</DialogHeader>
|
|
41
|
+
<p className="text-xs text-muted-foreground">
|
|
42
|
+
选择「保留并切换」将在后台继续执行,回到该节点时仍能看到进度;选择「取消并切换」会中止当前任务。
|
|
43
|
+
</p>
|
|
44
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
45
|
+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
46
|
+
留在此节点
|
|
47
|
+
</Button>
|
|
48
|
+
<Button type="button" variant="secondary" onClick={onCancelTaskAndSwitch}>
|
|
49
|
+
取消并切换
|
|
50
|
+
</Button>
|
|
51
|
+
<Button type="button" onClick={onKeepTaskAndSwitch}>
|
|
52
|
+
保留并切换
|
|
53
|
+
</Button>
|
|
54
|
+
</DialogFooter>
|
|
55
|
+
</DialogContent>
|
|
56
|
+
</Dialog>
|
|
57
|
+
);
|
|
58
|
+
}
|