@gadmin2n/schematics 0.0.85 → 0.0.87
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/dist/lib/application/application.factory.js +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +38 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/show.tsx +10 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +98 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +1 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +1 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +5 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +110 -65
- package/package.json +1 -1
|
@@ -74,11 +74,6 @@ function generateProject(options, path) {
|
|
|
74
74
|
}
|
|
75
75
|
function generateServer(options, path) {
|
|
76
76
|
const framework = options['server-framework'];
|
|
77
|
-
if (framework === 'iris') {
|
|
78
|
-
return (0, schematics_1.apply)((0, schematics_1.url)((0, core_1.join)('./files/customize/server', framework)), [
|
|
79
|
-
(0, schematics_1.move)((0, core_1.join)(path, 'server')),
|
|
80
|
-
]);
|
|
81
|
-
}
|
|
82
77
|
return (0, schematics_1.apply)((0, schematics_1.url)((0, core_1.join)('./files/customize/server', framework)), [
|
|
83
78
|
(0, schematics_1.template)(Object.assign(Object.assign({}, core_1.strings), options)),
|
|
84
79
|
(0, schematics_1.move)((0, core_1.join)(path, 'server')),
|
|
@@ -102,16 +102,20 @@ function RunDot({
|
|
|
102
102
|
count,
|
|
103
103
|
color,
|
|
104
104
|
label,
|
|
105
|
+
onClick,
|
|
105
106
|
}: {
|
|
106
107
|
count: number;
|
|
107
108
|
color: string;
|
|
108
109
|
label: string;
|
|
110
|
+
onClick?: () => void;
|
|
109
111
|
}) {
|
|
110
112
|
const size = 28;
|
|
111
113
|
const hasCount = count > 0;
|
|
114
|
+
const clickable = hasCount && !!onClick;
|
|
112
115
|
return (
|
|
113
116
|
<Tooltip title={`${label}: ${count}`}>
|
|
114
117
|
<span
|
|
118
|
+
onClick={clickable ? onClick : undefined}
|
|
115
119
|
style={{
|
|
116
120
|
display: 'inline-flex',
|
|
117
121
|
alignItems: 'center',
|
|
@@ -124,7 +128,7 @@ function RunDot({
|
|
|
124
128
|
fontSize: 11,
|
|
125
129
|
fontWeight: 600,
|
|
126
130
|
color: hasCount ? color : '#d9d9d9',
|
|
127
|
-
cursor: 'default',
|
|
131
|
+
cursor: clickable ? 'pointer' : 'default',
|
|
128
132
|
lineHeight: 1,
|
|
129
133
|
}}
|
|
130
134
|
>
|
|
@@ -339,14 +343,38 @@ export default function AgendaJobsPage() {
|
|
|
339
343
|
title: 'Runs',
|
|
340
344
|
key: 'runs',
|
|
341
345
|
width: 170,
|
|
342
|
-
render: (_: any, record: JobItem) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
346
|
+
render: (_: any, record: JobItem) => {
|
|
347
|
+
const go = (status: string) =>
|
|
348
|
+
navigate(`show/${encodeURIComponent(record.name)}?status=${status}`);
|
|
349
|
+
return (
|
|
350
|
+
<Space size={6}>
|
|
351
|
+
<RunDot
|
|
352
|
+
count={record.runs.queued}
|
|
353
|
+
color="#faad14"
|
|
354
|
+
label="Queued"
|
|
355
|
+
onClick={() => go('queued')}
|
|
356
|
+
/>
|
|
357
|
+
<RunDot
|
|
358
|
+
count={record.runs.success}
|
|
359
|
+
color="#006d32"
|
|
360
|
+
label="Success"
|
|
361
|
+
onClick={() => go('success')}
|
|
362
|
+
/>
|
|
363
|
+
<RunDot
|
|
364
|
+
count={record.runs.running}
|
|
365
|
+
color="#52c41a"
|
|
366
|
+
label="Running"
|
|
367
|
+
onClick={() => go('running')}
|
|
368
|
+
/>
|
|
369
|
+
<RunDot
|
|
370
|
+
count={record.runs.failed}
|
|
371
|
+
color="#ff4d4f"
|
|
372
|
+
label="Failed"
|
|
373
|
+
onClick={() => go('failed')}
|
|
374
|
+
/>
|
|
375
|
+
</Space>
|
|
376
|
+
);
|
|
377
|
+
},
|
|
350
378
|
},
|
|
351
379
|
{
|
|
352
380
|
title: 'Schedule',
|
|
@@ -451,7 +479,7 @@ export default function AgendaJobsPage() {
|
|
|
451
479
|
icon={<DatabaseOutlined />}
|
|
452
480
|
onClick={() => navigate('/admin/data-mngt/agenda-job')}
|
|
453
481
|
>
|
|
454
|
-
|
|
482
|
+
任务原始表
|
|
455
483
|
</Button>
|
|
456
484
|
</Tooltip>
|
|
457
485
|
<Button
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
DeleteOutlined,
|
|
23
23
|
PlayCircleOutlined,
|
|
24
24
|
} from '@ant-design/icons';
|
|
25
|
-
import { useNavigate, useParams } from 'react-router-dom';
|
|
25
|
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
26
26
|
import { customRequest } from 'helpers/http';
|
|
27
27
|
import { useECharts } from 'hooks/useECharts';
|
|
28
28
|
import dayjs from 'dayjs';
|
|
@@ -216,6 +216,8 @@ export default function AgendaJobShow() {
|
|
|
216
216
|
const { jobName: rawJobName } = useParams<{ jobName: string }>();
|
|
217
217
|
const jobName = decodeURIComponent(rawJobName || '');
|
|
218
218
|
const navigate = useNavigate();
|
|
219
|
+
const [searchParams] = useSearchParams();
|
|
220
|
+
const initialStatus = searchParams.get('status') || undefined;
|
|
219
221
|
|
|
220
222
|
const [detail, setDetail] = useState<JobDetail | null>(null);
|
|
221
223
|
const [loading, setLoading] = useState(true);
|
|
@@ -231,7 +233,9 @@ export default function AgendaJobShow() {
|
|
|
231
233
|
// Chart filters
|
|
232
234
|
const [filterBefore, setFilterBefore] = useState<dayjs.Dayjs | null>(dayjs());
|
|
233
235
|
const [filterLimit, setFilterLimit] = useState(25);
|
|
234
|
-
const [filterStatus, setFilterStatus] = useState<string | undefined>(
|
|
236
|
+
const [filterStatus, setFilterStatus] = useState<string | undefined>(
|
|
237
|
+
initialStatus,
|
|
238
|
+
);
|
|
235
239
|
|
|
236
240
|
// Fetch job detail
|
|
237
241
|
const fetchDetail = useCallback(async () => {
|
|
@@ -327,9 +331,10 @@ export default function AgendaJobShow() {
|
|
|
327
331
|
if (!detail) {
|
|
328
332
|
return (
|
|
329
333
|
<div style={{ padding: 24 }}>
|
|
330
|
-
<Button
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
<Button
|
|
335
|
+
icon={<ArrowLeftOutlined />}
|
|
336
|
+
onClick={() => navigate(-1)}
|
|
337
|
+
></Button>
|
|
333
338
|
<div style={{ marginTop: 24, textAlign: 'center', color: '#999' }}>
|
|
334
339
|
Job 不存在
|
|
335
340
|
</div>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { Alert, Input, message } from 'antd';
|
|
9
|
+
import type { WorkflowDSL } from '../types';
|
|
10
|
+
|
|
11
|
+
const { TextArea } = Input;
|
|
12
|
+
|
|
13
|
+
const EMPTY_TEMPLATE = '{\n "nodes": [],\n "edges": []\n}';
|
|
14
|
+
|
|
15
|
+
interface DslViewProps {
|
|
16
|
+
dsl: WorkflowDSL | null;
|
|
17
|
+
readonly?: boolean;
|
|
18
|
+
onApply?: (newDsl: WorkflowDSL) => void;
|
|
19
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DslViewHandle {
|
|
23
|
+
apply: () => void;
|
|
24
|
+
copy: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stringifyDsl(dsl: WorkflowDSL | null): string {
|
|
28
|
+
if (!dsl) return EMPTY_TEMPLATE;
|
|
29
|
+
return JSON.stringify(dsl, null, 2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 形状级校验:只检查必需字段,深层字段(position/config 等)由画布或保存时暴露 */
|
|
33
|
+
function validateDsl(
|
|
34
|
+
parsed: any,
|
|
35
|
+
): { ok: true; dsl: WorkflowDSL } | { ok: false; error: string } {
|
|
36
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
37
|
+
return { ok: false, error: '根必须是对象 { nodes, edges }' };
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(parsed.nodes)) {
|
|
40
|
+
return { ok: false, error: '字段 nodes 必须是数组' };
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(parsed.edges)) {
|
|
43
|
+
return { ok: false, error: '字段 edges 必须是数组' };
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < parsed.nodes.length; i++) {
|
|
46
|
+
const n = parsed.nodes[i];
|
|
47
|
+
if (!n || typeof n !== 'object') {
|
|
48
|
+
return { ok: false, error: `节点 [${i}] 必须是对象` };
|
|
49
|
+
}
|
|
50
|
+
if (typeof n.id !== 'string' || !n.id) {
|
|
51
|
+
return { ok: false, error: `节点 [${i}] 缺少必需字段:id` };
|
|
52
|
+
}
|
|
53
|
+
if (typeof n.type !== 'string' || !n.type) {
|
|
54
|
+
return { ok: false, error: `节点 [${i}] 缺少必需字段:type` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (let i = 0; i < parsed.edges.length; i++) {
|
|
58
|
+
const e = parsed.edges[i];
|
|
59
|
+
if (!e || typeof e !== 'object') {
|
|
60
|
+
return { ok: false, error: `连线 [${i}] 必须是对象` };
|
|
61
|
+
}
|
|
62
|
+
if (typeof e.id !== 'string' || !e.id) {
|
|
63
|
+
return { ok: false, error: `连线 [${i}] 缺少必需字段:id` };
|
|
64
|
+
}
|
|
65
|
+
if (typeof e.source !== 'string' || !e.source) {
|
|
66
|
+
return { ok: false, error: `连线 [${i}] 缺少必需字段:source` };
|
|
67
|
+
}
|
|
68
|
+
if (typeof e.target !== 'string' || !e.target) {
|
|
69
|
+
return { ok: false, error: `连线 [${i}] 缺少必需字段:target` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, dsl: parsed as WorkflowDSL };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function copyToClipboard(text: string): Promise<boolean> {
|
|
76
|
+
try {
|
|
77
|
+
if (navigator.clipboard?.writeText) {
|
|
78
|
+
await navigator.clipboard.writeText(text);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// fallthrough
|
|
83
|
+
}
|
|
84
|
+
// Fallback for non-secure context
|
|
85
|
+
try {
|
|
86
|
+
const ta = document.createElement('textarea');
|
|
87
|
+
ta.value = text;
|
|
88
|
+
ta.style.position = 'fixed';
|
|
89
|
+
ta.style.opacity = '0';
|
|
90
|
+
document.body.appendChild(ta);
|
|
91
|
+
ta.select();
|
|
92
|
+
const ok = document.execCommand('copy');
|
|
93
|
+
document.body.removeChild(ta);
|
|
94
|
+
return ok;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const DslView = forwardRef<DslViewHandle, DslViewProps>(function DslView(
|
|
101
|
+
{ dsl, readonly = false, onApply, onDirtyChange },
|
|
102
|
+
ref,
|
|
103
|
+
) {
|
|
104
|
+
const initial = useMemo(() => stringifyDsl(dsl), [dsl]);
|
|
105
|
+
const [text, setText] = useState(initial);
|
|
106
|
+
const [error, setError] = useState<string | null>(null);
|
|
107
|
+
|
|
108
|
+
// dsl prop 变化时重置 text(应用、版本切换、AI 生成等场景)
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
setText(initial);
|
|
111
|
+
setError(null);
|
|
112
|
+
onDirtyChange?.(false);
|
|
113
|
+
// onDirtyChange 故意不放进依赖数组:调用方传不稳定函数会导致死循环
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [initial]);
|
|
116
|
+
|
|
117
|
+
function handleTextChange(next: string) {
|
|
118
|
+
setText(next);
|
|
119
|
+
if (error) setError(null);
|
|
120
|
+
if (!readonly) {
|
|
121
|
+
const dirty = next !== initial;
|
|
122
|
+
onDirtyChange?.(dirty);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleApply() {
|
|
127
|
+
let parsed: any;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(text);
|
|
130
|
+
} catch (e: any) {
|
|
131
|
+
setError(`JSON 解析失败:${e?.message ?? String(e)}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const result = validateDsl(parsed);
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
setError(result.error);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const normalized = JSON.stringify(result.dsl, null, 2);
|
|
140
|
+
setText(normalized);
|
|
141
|
+
setError(null);
|
|
142
|
+
onApply?.(result.dsl);
|
|
143
|
+
onDirtyChange?.(false);
|
|
144
|
+
message.success('已应用到画布');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleCopy() {
|
|
148
|
+
const ok = await copyToClipboard(text);
|
|
149
|
+
if (ok) {
|
|
150
|
+
message.success('已复制到剪贴板');
|
|
151
|
+
} else {
|
|
152
|
+
message.error('复制失败,请手动选择文本复制');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
useImperativeHandle(
|
|
157
|
+
ref,
|
|
158
|
+
() => ({
|
|
159
|
+
apply: handleApply,
|
|
160
|
+
copy: handleCopy,
|
|
161
|
+
}),
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
[text, onApply, onDirtyChange],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
style={{
|
|
169
|
+
flex: 1,
|
|
170
|
+
display: 'flex',
|
|
171
|
+
flexDirection: 'column',
|
|
172
|
+
minHeight: 0,
|
|
173
|
+
background: '#fff',
|
|
174
|
+
padding: 16,
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{error && (
|
|
178
|
+
<Alert
|
|
179
|
+
type="error"
|
|
180
|
+
message={error}
|
|
181
|
+
showIcon
|
|
182
|
+
closable
|
|
183
|
+
onClose={() => setError(null)}
|
|
184
|
+
style={{ marginBottom: 8, flexShrink: 0 }}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
<TextArea
|
|
189
|
+
value={text}
|
|
190
|
+
onChange={(e) => handleTextChange(e.target.value)}
|
|
191
|
+
readOnly={readonly}
|
|
192
|
+
spellCheck={false}
|
|
193
|
+
autoSize={false}
|
|
194
|
+
style={{
|
|
195
|
+
flex: 1,
|
|
196
|
+
fontFamily:
|
|
197
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
198
|
+
fontSize: 13,
|
|
199
|
+
lineHeight: 1.5,
|
|
200
|
+
resize: 'none',
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
});
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Input,
|
|
12
12
|
message,
|
|
13
13
|
Modal,
|
|
14
|
+
Segmented,
|
|
14
15
|
Select,
|
|
15
16
|
Space,
|
|
16
17
|
Spin,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from 'antd';
|
|
20
21
|
import {
|
|
21
22
|
ArrowLeftOutlined,
|
|
23
|
+
CheckOutlined,
|
|
22
24
|
EditOutlined,
|
|
23
25
|
RobotOutlined,
|
|
24
26
|
RocketOutlined,
|
|
@@ -28,6 +30,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
28
30
|
import { customRequest } from 'helpers/http';
|
|
29
31
|
import { FlowRenderer } from './components/FlowRenderer';
|
|
30
32
|
import { NodePropertyPanel } from './components/NodePropertyPanel';
|
|
33
|
+
import { DslView, type DslViewHandle } from './components/DslView';
|
|
31
34
|
import { useWorkflowAgent } from './hooks/useWorkflowAgent';
|
|
32
35
|
import type {
|
|
33
36
|
Workflow,
|
|
@@ -59,6 +62,9 @@ export default function WorkflowEditorPage() {
|
|
|
59
62
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
60
63
|
|
|
61
64
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
65
|
+
const [viewMode, setViewMode] = useState<'canvas' | 'dsl'>('canvas');
|
|
66
|
+
const [dslDirty, setDslDirty] = useState(false);
|
|
67
|
+
const dslViewRef = useRef<DslViewHandle>(null);
|
|
62
68
|
|
|
63
69
|
// Title editing state
|
|
64
70
|
const [title, setTitle] = useState(isNewMode ? 'Untitled Workflow' : '');
|
|
@@ -334,10 +340,12 @@ export default function WorkflowEditorPage() {
|
|
|
334
340
|
async function handleSelectVersion(version: number) {
|
|
335
341
|
if (!id) return;
|
|
336
342
|
|
|
337
|
-
if (hasUnsavedChanges) {
|
|
343
|
+
if (hasUnsavedChanges || (viewMode === 'dsl' && dslDirty)) {
|
|
338
344
|
Modal.confirm({
|
|
339
|
-
title: '未保存的修改',
|
|
340
|
-
content:
|
|
345
|
+
title: hasUnsavedChanges ? '未保存的修改' : 'DSL 有未应用的编辑',
|
|
346
|
+
content: hasUnsavedChanges
|
|
347
|
+
? '当前有未保存的修改,切换版本将丢失这些更改。确定要继续吗?'
|
|
348
|
+
: 'DSL 视图中有未应用到画布的编辑,切换版本将丢弃这些内容。确定要继续吗?',
|
|
341
349
|
okText: '确定切换',
|
|
342
350
|
cancelText: '取消',
|
|
343
351
|
onOk: () => doLoadVersion(version),
|
|
@@ -369,6 +377,30 @@ export default function WorkflowEditorPage() {
|
|
|
369
377
|
setSelectedNodeId(nodeId || null);
|
|
370
378
|
}
|
|
371
379
|
|
|
380
|
+
function handleViewModeChange(next: 'canvas' | 'dsl') {
|
|
381
|
+
if (next === 'canvas' && dslDirty) {
|
|
382
|
+
Modal.confirm({
|
|
383
|
+
title: '未应用的修改',
|
|
384
|
+
content:
|
|
385
|
+
'DSL 视图中有未应用到画布的修改,切换到 Canvas 将丢弃这些修改。确定要继续吗?',
|
|
386
|
+
okText: '确定切换',
|
|
387
|
+
cancelText: '取消',
|
|
388
|
+
onOk: () => {
|
|
389
|
+
setDslDirty(false);
|
|
390
|
+
setViewMode('canvas');
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
setViewMode(next);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function handleDslApply(newDsl: WorkflowDSL) {
|
|
399
|
+
setDsl(newDsl);
|
|
400
|
+
setHasUnsavedChanges(true);
|
|
401
|
+
setSelectedNodeId(null);
|
|
402
|
+
}
|
|
403
|
+
|
|
372
404
|
if (loading) {
|
|
373
405
|
return (
|
|
374
406
|
<div
|
|
@@ -408,9 +440,7 @@ export default function WorkflowEditorPage() {
|
|
|
408
440
|
type="text"
|
|
409
441
|
icon={<ArrowLeftOutlined />}
|
|
410
442
|
onClick={() => navigate('/admin/workflow')}
|
|
411
|
-
>
|
|
412
|
-
Back
|
|
413
|
-
</Button>
|
|
443
|
+
></Button>
|
|
414
444
|
{editingTitle ? (
|
|
415
445
|
<Input
|
|
416
446
|
ref={titleInputRef}
|
|
@@ -448,7 +478,7 @@ export default function WorkflowEditorPage() {
|
|
|
448
478
|
}))}
|
|
449
479
|
/>
|
|
450
480
|
)}
|
|
451
|
-
{isDev && (
|
|
481
|
+
{isDev && viewMode === 'canvas' && (
|
|
452
482
|
<Button
|
|
453
483
|
type="primary"
|
|
454
484
|
icon={<RobotOutlined />}
|
|
@@ -470,30 +500,73 @@ export default function WorkflowEditorPage() {
|
|
|
470
500
|
</div>
|
|
471
501
|
</Card>
|
|
472
502
|
|
|
503
|
+
{/* View toggle row + DSL actions */}
|
|
504
|
+
<div
|
|
505
|
+
style={{
|
|
506
|
+
marginBottom: 12,
|
|
507
|
+
flexShrink: 0,
|
|
508
|
+
display: 'flex',
|
|
509
|
+
alignItems: 'center',
|
|
510
|
+
justifyContent: 'space-between',
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
513
|
+
<Segmented
|
|
514
|
+
value={viewMode}
|
|
515
|
+
onChange={(v) => handleViewModeChange(v as 'canvas' | 'dsl')}
|
|
516
|
+
options={[
|
|
517
|
+
{ label: 'Canvas', value: 'canvas' },
|
|
518
|
+
{ label: 'DSL', value: 'dsl' },
|
|
519
|
+
]}
|
|
520
|
+
/>
|
|
521
|
+
{viewMode === 'dsl' && (
|
|
522
|
+
<Space>
|
|
523
|
+
{dslDirty && <Tag color="orange">未应用</Tag>}
|
|
524
|
+
<Button
|
|
525
|
+
type="primary"
|
|
526
|
+
icon={<CheckOutlined />}
|
|
527
|
+
onClick={() => dslViewRef.current?.apply()}
|
|
528
|
+
>
|
|
529
|
+
应用到画布
|
|
530
|
+
</Button>
|
|
531
|
+
</Space>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
|
|
473
535
|
{/* Main area: Flow + Property Panel */}
|
|
474
536
|
<div
|
|
475
537
|
style={{ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0 }}
|
|
476
538
|
>
|
|
477
|
-
|
|
478
|
-
|
|
539
|
+
{viewMode === 'canvas' ? (
|
|
540
|
+
<>
|
|
541
|
+
<div style={{ flex: 1, position: 'relative' }}>
|
|
542
|
+
<FlowRenderer
|
|
543
|
+
dsl={dsl}
|
|
544
|
+
nodeTypeMap={nodeTypeMap}
|
|
545
|
+
selectedNodeId={selectedNodeId}
|
|
546
|
+
onNodeClick={handleNodeClick}
|
|
547
|
+
onDslChange={handleDslChange}
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
<NodePropertyPanel
|
|
551
|
+
node={selectedNode}
|
|
552
|
+
nodeTypes={nodeTypes}
|
|
553
|
+
currentWorkflowId={id}
|
|
554
|
+
onSave={handleNodePropertySave}
|
|
555
|
+
onLabelChange={handleNodeLabelChange}
|
|
556
|
+
onAiEdit={handleNodeAiEdit}
|
|
557
|
+
onDelete={handleNodeDelete}
|
|
558
|
+
onAbort={abortAgent}
|
|
559
|
+
aiLoading={agentLoading}
|
|
560
|
+
/>
|
|
561
|
+
</>
|
|
562
|
+
) : (
|
|
563
|
+
<DslView
|
|
564
|
+
ref={dslViewRef}
|
|
479
565
|
dsl={dsl}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
onNodeClick={handleNodeClick}
|
|
483
|
-
onDslChange={handleDslChange}
|
|
566
|
+
onApply={handleDslApply}
|
|
567
|
+
onDirtyChange={setDslDirty}
|
|
484
568
|
/>
|
|
485
|
-
|
|
486
|
-
<NodePropertyPanel
|
|
487
|
-
node={selectedNode}
|
|
488
|
-
nodeTypes={nodeTypes}
|
|
489
|
-
currentWorkflowId={id}
|
|
490
|
-
onSave={handleNodePropertySave}
|
|
491
|
-
onLabelChange={handleNodeLabelChange}
|
|
492
|
-
onAiEdit={handleNodeAiEdit}
|
|
493
|
-
onDelete={handleNodeDelete}
|
|
494
|
-
onAbort={abortAgent}
|
|
495
|
-
aiLoading={agentLoading}
|
|
496
|
-
/>
|
|
569
|
+
)}
|
|
497
570
|
</div>
|
|
498
571
|
|
|
499
572
|
{/* Name modal for untitled workflows */}
|
|
@@ -234,9 +234,7 @@ export default function WorkflowInstanceDetailPage() {
|
|
|
234
234
|
type="text"
|
|
235
235
|
icon={<ArrowLeftOutlined />}
|
|
236
236
|
onClick={() => navigate(-1)}
|
|
237
|
-
>
|
|
238
|
-
Back
|
|
239
|
-
</Button>
|
|
237
|
+
></Button>
|
|
240
238
|
<Title level={4} style={{ margin: 0 }}>
|
|
241
239
|
{data.workflow.name} — Instance #{data.id}
|
|
242
240
|
</Title>
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx
CHANGED
|
@@ -197,9 +197,7 @@ export default function WorkflowInstancesPage() {
|
|
|
197
197
|
type="text"
|
|
198
198
|
icon={<ArrowLeftOutlined />}
|
|
199
199
|
onClick={() => navigate('/admin/workflow')}
|
|
200
|
-
>
|
|
201
|
-
Back
|
|
202
|
-
</Button>
|
|
200
|
+
></Button>
|
|
203
201
|
<Title level={4} style={{ margin: 0 }}>
|
|
204
202
|
{workflowName || 'Workflow'} — Execution History
|
|
205
203
|
</Title>
|
|
@@ -93,9 +93,11 @@ export function NodeInstanceForm({
|
|
|
93
93
|
}}
|
|
94
94
|
>
|
|
95
95
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
96
|
-
<Button
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
<Button
|
|
97
|
+
type="text"
|
|
98
|
+
icon={<ArrowLeftOutlined />}
|
|
99
|
+
onClick={onBack}
|
|
100
|
+
></Button>
|
|
99
101
|
<Title level={4} style={{ margin: 0 }}>
|
|
100
102
|
{title}
|
|
101
103
|
</Title>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import {
|
|
3
9
|
Button,
|
|
4
10
|
Card,
|
|
@@ -6,6 +12,7 @@ import {
|
|
|
6
12
|
Input,
|
|
7
13
|
message,
|
|
8
14
|
Modal,
|
|
15
|
+
Segmented,
|
|
9
16
|
Select,
|
|
10
17
|
Space,
|
|
11
18
|
Spin,
|
|
@@ -14,6 +21,7 @@ import {
|
|
|
14
21
|
} from 'antd';
|
|
15
22
|
import {
|
|
16
23
|
ArrowLeftOutlined,
|
|
24
|
+
CopyOutlined,
|
|
17
25
|
DiffOutlined,
|
|
18
26
|
EditOutlined,
|
|
19
27
|
HistoryOutlined,
|
|
@@ -24,6 +32,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
24
32
|
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
|
|
25
33
|
import { customRequest } from 'helpers/http';
|
|
26
34
|
import { FlowRenderer } from './components/FlowRenderer';
|
|
35
|
+
import { DslView, type DslViewHandle } from './components/DslView';
|
|
27
36
|
import { RunWorkflowModal } from './components/RunWorkflowModal';
|
|
28
37
|
import type {
|
|
29
38
|
Workflow,
|
|
@@ -52,6 +61,8 @@ export default function WorkflowShowPage() {
|
|
|
52
61
|
|
|
53
62
|
const [dsl, setDsl] = useState<WorkflowDSL | null>(null);
|
|
54
63
|
const [currentVersion, setCurrentVersion] = useState<number | null>(null);
|
|
64
|
+
const [viewMode, setViewMode] = useState<'canvas' | 'dsl'>('canvas');
|
|
65
|
+
const dslViewRef = useRef<DslViewHandle>(null);
|
|
55
66
|
|
|
56
67
|
// Publish modal state
|
|
57
68
|
const [publishModalOpen, setPublishModalOpen] = useState(false);
|
|
@@ -260,9 +271,7 @@ ${diffRightDsl}
|
|
|
260
271
|
type="text"
|
|
261
272
|
icon={<ArrowLeftOutlined />}
|
|
262
273
|
onClick={() => navigate('/admin/workflow')}
|
|
263
|
-
>
|
|
264
|
-
Back
|
|
265
|
-
</Button>
|
|
274
|
+
></Button>
|
|
266
275
|
<Title level={4} style={{ margin: 0 }}>
|
|
267
276
|
{workflow?.name || 'Workflow'}
|
|
268
277
|
</Title>
|
|
@@ -324,69 +333,105 @@ ${diffRightDsl}
|
|
|
324
333
|
</div>
|
|
325
334
|
</Card>
|
|
326
335
|
|
|
336
|
+
{/* View toggle row + DSL actions */}
|
|
337
|
+
<div
|
|
338
|
+
style={{
|
|
339
|
+
marginBottom: 12,
|
|
340
|
+
flexShrink: 0,
|
|
341
|
+
display: 'flex',
|
|
342
|
+
alignItems: 'center',
|
|
343
|
+
justifyContent: 'space-between',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<Segmented
|
|
347
|
+
value={viewMode}
|
|
348
|
+
onChange={(v) => setViewMode(v as 'canvas' | 'dsl')}
|
|
349
|
+
options={[
|
|
350
|
+
{ label: 'Canvas', value: 'canvas' },
|
|
351
|
+
{ label: 'DSL', value: 'dsl' },
|
|
352
|
+
]}
|
|
353
|
+
/>
|
|
354
|
+
{viewMode === 'dsl' && (
|
|
355
|
+
<Button
|
|
356
|
+
icon={<CopyOutlined />}
|
|
357
|
+
onClick={() => dslViewRef.current?.copy()}
|
|
358
|
+
>
|
|
359
|
+
Copy
|
|
360
|
+
</Button>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
|
|
327
364
|
{/* Canvas + Node Detail Panel */}
|
|
328
365
|
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{selectedNode.instanceRef
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<Descriptions
|
|
381
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
366
|
+
{viewMode === 'canvas' ? (
|
|
367
|
+
<>
|
|
368
|
+
<div style={{ flex: 1, position: 'relative' }}>
|
|
369
|
+
<FlowRenderer
|
|
370
|
+
dsl={dsl}
|
|
371
|
+
nodeTypeMap={nodeTypeMap}
|
|
372
|
+
selectedNodeId={selectedNodeId}
|
|
373
|
+
readonly
|
|
374
|
+
onNodeClick={(nodeId) => setSelectedNodeId(nodeId || null)}
|
|
375
|
+
/>
|
|
376
|
+
</div>
|
|
377
|
+
{selectedNode && (
|
|
378
|
+
<div
|
|
379
|
+
style={{
|
|
380
|
+
width: 320,
|
|
381
|
+
flexShrink: 0,
|
|
382
|
+
borderLeft: '1px solid #f0f0f0',
|
|
383
|
+
background: '#fff',
|
|
384
|
+
overflowY: 'auto',
|
|
385
|
+
padding: 16,
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<Title level={5} style={{ marginTop: 0 }}>
|
|
389
|
+
{selectedNode.label}
|
|
390
|
+
</Title>
|
|
391
|
+
<Descriptions column={1} size="small" bordered>
|
|
392
|
+
<Descriptions.Item label="ID">
|
|
393
|
+
<Typography.Text copyable style={{ fontSize: 12 }}>
|
|
394
|
+
{selectedNode.id}
|
|
395
|
+
</Typography.Text>
|
|
396
|
+
</Descriptions.Item>
|
|
397
|
+
<Descriptions.Item label="类型">
|
|
398
|
+
{selectedNode.type}
|
|
399
|
+
</Descriptions.Item>
|
|
400
|
+
<Descriptions.Item label="分类">
|
|
401
|
+
<Tag>
|
|
402
|
+
{nodeTypeMap[selectedNode.type]?.category || 'ACTION'}
|
|
403
|
+
</Tag>
|
|
404
|
+
</Descriptions.Item>
|
|
405
|
+
{selectedNode.instanceRef && (
|
|
406
|
+
<Descriptions.Item label="节点实例">
|
|
407
|
+
{selectedNode.instanceRef.instanceName}
|
|
408
|
+
</Descriptions.Item>
|
|
409
|
+
)}
|
|
410
|
+
</Descriptions>
|
|
411
|
+
{selectedNode.config &&
|
|
412
|
+
Object.keys(selectedNode.config).length > 0 && (
|
|
413
|
+
<>
|
|
414
|
+
<Title level={5} style={{ marginTop: 16 }}>
|
|
415
|
+
配置
|
|
416
|
+
</Title>
|
|
417
|
+
<Descriptions column={1} size="small" bordered>
|
|
418
|
+
{Object.entries(selectedNode.config).map(
|
|
419
|
+
([key, value]) => (
|
|
420
|
+
<Descriptions.Item key={key} label={key}>
|
|
421
|
+
{typeof value === 'object'
|
|
422
|
+
? JSON.stringify(value, null, 2)
|
|
423
|
+
: String(value ?? '')}
|
|
424
|
+
</Descriptions.Item>
|
|
425
|
+
),
|
|
426
|
+
)}
|
|
427
|
+
</Descriptions>
|
|
428
|
+
</>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</>
|
|
433
|
+
) : (
|
|
434
|
+
<DslView ref={dslViewRef} dsl={dsl} readonly />
|
|
390
435
|
)}
|
|
391
436
|
</div>
|
|
392
437
|
|