@gadmin2n/schematics 0.0.149 → 0.0.151

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.
Files changed (81) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +125 -36
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +42 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +2 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +0 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +3 -1
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-dev-env.js +218 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +78 -12
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +37 -67
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -17
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +46 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +60 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +3 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +9 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +55 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +491 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.dto.ts +213 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.module.ts +28 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/resource-transfer.service.ts +137 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/role-transfer.service.ts +248 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-token.service.ts +65 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/user-transfer.service.ts +146 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +55 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +2 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +55 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +2 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.spec.ts +46 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.ts +3 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +55 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +2 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +1 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +96 -14
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +41 -41
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.spec.ts +36 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.ts +90 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +4 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +1 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +115 -63
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.spec.ts +166 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.ts +217 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +33 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +57 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +196 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.ts +125 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +9 -3
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +148 -4
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +12 -1
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +14 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +75 -45
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +25 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +115 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +257 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +40 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +15 -13
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +40 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +15 -13
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -1
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +89 -65
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +51 -127
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +53 -109
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +45 -41
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +51 -72
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +49 -40
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +16 -5
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +28 -10
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +60 -1
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +94 -12
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +13 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +3 -3
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +8 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +167 -94
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +95 -95
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.test.tsx +81 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.tsx +126 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +48 -52
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +2 -2
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -3
  79. package/package.json +1 -1
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/ac.config.ts +0 -24
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +0 -495
@@ -8,13 +8,13 @@ import {
8
8
  ShowButton,
9
9
  useTable,
10
10
  } from '@refinedev/antd';
11
- import {
12
- getDefaultFilter,
13
- type HttpError,
14
- useGetIdentity,
15
- } from '@refinedev/core';
11
+ import { getDefaultFilter, type HttpError } from '@refinedev/core';
16
12
 
17
- import { PlusOutlined, CodeOutlined } from '@ant-design/icons';
13
+ import {
14
+ PlusOutlined,
15
+ ExportOutlined,
16
+ ImportOutlined,
17
+ } from '@ant-design/icons';
18
18
  import { Button, Card, Input, Select, Space, Table, Tag, Tooltip } from 'antd';
19
19
 
20
20
  import {
@@ -30,11 +30,10 @@ import { CreateModal } from './components/create-modal';
30
30
  import { EditModal } from './components/edit-modal';
31
31
  import { ShowDrawer } from './components/show-drawer';
32
32
  import { LIST_CONTENT_PROPS } from '@/constants/layout';
33
- import { SqlModal, generateUserSql } from '../../components/SqlModal';
33
+ import { TransferExportModal } from '@/components/TransferExportModal';
34
+ import { TransferImportModal } from '@/components/TransferImportModal';
34
35
 
35
36
  export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
36
- const { data: identity } = useGetIdentity<any>();
37
-
38
37
  // Fetch roles from backend using custom hook
39
38
  const { roleOptions, loading: rolesLoading } = useRoles();
40
39
 
@@ -59,7 +58,11 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
59
58
  });
60
59
  };
61
60
 
62
- const { tableProps, filters } = useTable<User, HttpError, Partial<User>>({
61
+ const {
62
+ tableProps,
63
+ filters,
64
+ tableQuery: tableQueryResult,
65
+ } = useTable<User, HttpError, Partial<User>>({
63
66
  resource: 'user',
64
67
  pagination: {
65
68
  pageSize: 20,
@@ -105,20 +108,8 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
105
108
  id: null,
106
109
  });
107
110
 
108
- const [sqlModal, setSqlModal] = useState<{
109
- open: boolean;
110
- title: string;
111
- sql: string;
112
- }>({ open: false, title: '', sql: '' });
113
-
114
- const handleShowSql = (record: any) => {
115
- const operator = {
116
- authorId: identity?.userid,
117
- authorName: identity?.username,
118
- };
119
- const sql = generateUserSql(record, operator);
120
- setSqlModal({ open: true, title: `SQL — User: ${record.userid}`, sql });
121
- };
111
+ const [exportEndpoint, setExportEndpoint] = useState<string | null>(null);
112
+ const [importOpen, setImportOpen] = useState(false);
122
113
 
123
114
  // 处理modal关闭
124
115
  const handleModalClose = () => {
@@ -132,13 +123,21 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
132
123
  contentProps={LIST_CONTENT_PROPS}
133
124
  headerButtons={() => {
134
125
  return (
135
- <Button
136
- type="primary"
137
- icon={<PlusOutlined />}
138
- onClick={() => setShowModal({ type: 'create', id: null })}
139
- >
140
- 新增访问主体
141
- </Button>
126
+ <Space>
127
+ <Button
128
+ icon={<ImportOutlined />}
129
+ onClick={() => setImportOpen(true)}
130
+ >
131
+ 导入
132
+ </Button>
133
+ <Button
134
+ type="primary"
135
+ icon={<PlusOutlined />}
136
+ onClick={() => setShowModal({ type: 'create', id: null })}
137
+ >
138
+ 新增访问主体
139
+ </Button>
140
+ </Space>
142
141
  );
143
142
  }}
144
143
  >
@@ -313,12 +312,16 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
313
312
  setShowModal({ type: 'edit', id: record.id })
314
313
  }
315
314
  />
316
- <Tooltip title="Generate SQL">
315
+ <Tooltip title="导出">
317
316
  <Button
318
317
  size="small"
319
- icon={<CodeOutlined />}
318
+ icon={<ExportOutlined />}
320
319
  style={{ backgroundColor: 'transparent' }}
321
- onClick={() => handleShowSql(record)}
320
+ onClick={() =>
321
+ setExportEndpoint(
322
+ `user/${encodeURIComponent(record.userid)}/export-token`,
323
+ )
324
+ }
322
325
  />
323
326
  </Tooltip>
324
327
  <DeleteButton
@@ -350,12 +353,18 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
350
353
  <CreateModal onClose={handleModalClose} />
351
354
  )}
352
355
 
353
- {/* SQL Modal */}
354
- <SqlModal
355
- open={sqlModal.open}
356
- onClose={() => setSqlModal({ open: false, title: '', sql: '' })}
357
- title={sqlModal.title}
358
- sql={sqlModal.sql}
356
+ <TransferExportModal
357
+ open={!!exportEndpoint}
358
+ exportTokenEndpoint={exportEndpoint}
359
+ entity="user"
360
+ onClose={() => setExportEndpoint(null)}
361
+ />
362
+ <TransferImportModal
363
+ open={importOpen}
364
+ onClose={() => setImportOpen(false)}
365
+ onSuccess={() => tableQueryResult?.refetch()}
366
+ previewEndpoint="user/import-preview"
367
+ confirmEndpoint="user/import-confirm"
359
368
  />
360
369
 
361
370
  {children}
@@ -129,11 +129,18 @@ describe('NodePropertyPanel instance references', () => {
129
129
  configSchema: {
130
130
  type: 'object',
131
131
  properties: {
132
- allowWrite: { type: 'boolean' },
133
- sql: { type: 'string' },
132
+ functionRef: {
133
+ type: 'object',
134
+ properties: {
135
+ schema: { type: 'string' },
136
+ name: { type: 'string' },
137
+ identityArguments: { type: 'string' },
138
+ },
139
+ },
140
+ argumentBindings: { type: 'array' },
134
141
  timeoutSeconds: { type: 'integer' },
135
142
  },
136
- required: ['allowWrite', 'sql'],
143
+ required: ['functionRef', 'argumentBindings'],
137
144
  },
138
145
  uiSchema: { widget: 'db-execute-config' },
139
146
  } as unknown as WorkflowNodeType;
@@ -157,8 +164,12 @@ describe('NodePropertyPanel instance references', () => {
157
164
  label: 'Database Operation',
158
165
  position: { x: 0, y: 0 },
159
166
  config: {
160
- allowWrite: false,
161
- sql: 'SELECT 1',
167
+ functionRef: {
168
+ schema: 'workflow_api',
169
+ name: 'refresh_summary',
170
+ identityArguments: '',
171
+ },
172
+ argumentBindings: [],
162
173
  timeoutSeconds: 3,
163
174
  },
164
175
  })
@@ -9,28 +9,46 @@ const node: WorkflowNode = {
9
9
  type: 'db_execute',
10
10
  label: '查询待处理订单',
11
11
  position: { x: 0, y: 0 },
12
- config: { allowWrite: false, sql: 'SELECT * FROM orders' },
12
+ config: {
13
+ functionRef: {
14
+ schema: 'workflow_api',
15
+ name: 'find_pending_orders',
16
+ identityArguments: '',
17
+ },
18
+ argumentBindings: [],
19
+ },
13
20
  };
14
21
 
15
22
  const nodeType = {
16
23
  id: '1',
17
24
  type: 'db_execute',
18
- label: 'Database Operation',
19
- description: 'Read records from PostgreSQL',
25
+ label: 'Database Function',
26
+ description: 'Call a PostgreSQL Function',
20
27
  category: 'ACTION',
21
28
  configSchema: {
22
29
  type: 'object',
23
30
  properties: {
24
- allowWrite: { type: 'boolean', title: 'Allow writes' },
25
- sql: { type: 'string', title: 'SQL' },
31
+ functionRef: {
32
+ type: 'object',
33
+ title: 'Function',
34
+ properties: {
35
+ schema: { type: 'string', title: 'Schema' },
36
+ name: { type: 'string', title: 'Name' },
37
+ identityArguments: { type: 'string', title: 'Arguments' },
38
+ },
39
+ },
40
+ argumentBindings: {
41
+ type: 'array',
42
+ title: 'Argument bindings',
43
+ items: { type: 'object' },
44
+ },
26
45
  },
27
46
  },
28
47
  uiSchema: {},
29
48
  outputSchema: {
30
49
  type: 'object',
31
50
  properties: {
32
- rows: { type: 'array' },
33
- rowCount: { type: 'number' },
51
+ result: {},
34
52
  },
35
53
  },
36
54
  outputs: [],
@@ -62,9 +80,9 @@ describe('ReadonlyNodePanel', () => {
62
80
  screen.getByRole('heading', { name: 'Node configuration' }),
63
81
  ).toBeInTheDocument();
64
82
  expect(screen.getByDisplayValue('查询待处理订单')).toBeDisabled();
65
- expect(screen.getByDisplayValue('SELECT * FROM orders')).toBeDisabled();
83
+ expect(screen.getByDisplayValue('find_pending_orders')).toBeDisabled();
66
84
  expect(screen.getByText('No node template is used')).toBeInTheDocument();
67
- expect(screen.getByText('input.rows')).toBeInTheDocument();
85
+ expect(screen.getByText('input.result')).toBeInTheDocument();
68
86
  expect(screen.queryByText(/"position"/)).not.toBeInTheDocument();
69
87
  expect(screen.getByText('Database Operation')).toBeInTheDocument();
70
88
  expect(screen.queryByText('Perform an action')).not.toBeInTheDocument();
@@ -82,7 +100,7 @@ describe('ReadonlyNodePanel', () => {
82
100
  fireEvent.click(screen.getByText('Advanced configuration'));
83
101
 
84
102
  expect(screen.getByText(/"position"/)).toBeInTheDocument();
85
- expect(screen.getByText(/SELECT \* FROM orders/)).toBeInTheDocument();
103
+ expect(screen.getByText(/find_pending_orders/)).toBeInTheDocument();
86
104
  expect(
87
105
  screen.getByRole('button', { name: 'Copy JSON' }),
88
106
  ).toBeInTheDocument();
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getStepStatus, shouldShowNodeError } from './instance-detail';
2
+ import {
3
+ getDisplayNodeExecutions,
4
+ getErrorSummary,
5
+ getStepStatus,
6
+ getWorkflowFailureSummary,
7
+ shouldShowNodeError,
8
+ } from './instance-detail';
3
9
 
4
10
  describe('Workflow instance cancellation display', () => {
5
11
  it('does not present a cancelled node as an error', () => {
@@ -23,4 +29,57 @@ describe('Workflow instance cancellation display', () => {
23
29
  ).toBe(true);
24
30
  expect(getStepStatus('FAILED')).toBe('error');
25
31
  });
32
+
33
+ it('shows the root cause instead of the generic Activity wrapper', () => {
34
+ const error = {
35
+ message: 'database connection refused',
36
+ errorType: 'DatabaseConnectionError',
37
+ stack: 'DatabaseConnectionError: database connection refused',
38
+ causes: [
39
+ { name: 'ActivityFailure', message: 'Activity task failed' },
40
+ {
41
+ name: 'ApplicationFailure',
42
+ message: 'database connection refused',
43
+ },
44
+ ],
45
+ };
46
+
47
+ expect(getErrorSummary(error)).toBe('database connection refused');
48
+ expect(
49
+ getWorkflowFailureSummary({ error, failedNodeId: 'database-node' }),
50
+ ).toEqual({
51
+ message: 'database connection refused',
52
+ failedNodeId: 'database-node',
53
+ });
54
+ });
55
+
56
+ it('shows a completed trigger for legacy instances without a trigger execution record', () => {
57
+ const executions = getDisplayNodeExecutions({
58
+ context: { workflow: { requestId: 42 } },
59
+ startedAt: '2026-09-02T10:00:00.000Z',
60
+ nodeExecutions: [],
61
+ dsl: {
62
+ nodes: [
63
+ {
64
+ id: 'event-start',
65
+ type: 'event_trigger',
66
+ label: '业务事件触发',
67
+ config: {},
68
+ position: { x: 0, y: 0 },
69
+ },
70
+ ],
71
+ edges: [],
72
+ },
73
+ });
74
+
75
+ expect(executions).toEqual([
76
+ expect.objectContaining({
77
+ nodeId: 'event-start',
78
+ nodeType: 'event_trigger',
79
+ status: 'COMPLETED',
80
+ input: { requestId: 42 },
81
+ output: { requestId: 42 },
82
+ }),
83
+ ]);
84
+ });
26
85
  });
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
2
  import {
3
+ Alert,
3
4
  Button,
4
5
  Card,
5
6
  Descriptions,
@@ -93,6 +94,38 @@ interface InstanceDetail {
93
94
  dsl?: WorkflowDSL;
94
95
  }
95
96
 
97
+ export function getDisplayNodeExecutions(
98
+ instance: Pick<
99
+ InstanceDetail,
100
+ 'context' | 'dsl' | 'nodeExecutions' | 'startedAt'
101
+ >,
102
+ ): NodeExecution[] {
103
+ if (!instance.dsl || !instance.startedAt) return instance.nodeExecutions;
104
+
105
+ const recordedNodeIds = new Set(
106
+ instance.nodeExecutions.map((execution) => execution.nodeId),
107
+ );
108
+ const workflowInput = instance.context?.workflow ?? null;
109
+ const legacyTriggerExecutions = instance.dsl.nodes
110
+ .filter(
111
+ (node) => node.type.endsWith('_trigger') && !recordedNodeIds.has(node.id),
112
+ )
113
+ .map<NodeExecution>((node) => ({
114
+ id: `legacy-trigger-${node.id}`,
115
+ nodeId: node.id,
116
+ nodeType: node.type,
117
+ nodeLabel: node.label ?? null,
118
+ status: 'COMPLETED',
119
+ input: workflowInput,
120
+ output: workflowInput,
121
+ error: null,
122
+ startedAt: instance.startedAt,
123
+ finishedAt: instance.startedAt,
124
+ }));
125
+
126
+ return [...legacyTriggerExecutions, ...instance.nodeExecutions];
127
+ }
128
+
96
129
  function formatDuration(start: string | null, end: string | null): string {
97
130
  if (!start) return '—';
98
131
  const endTime = end ? new Date(end).getTime() : Date.now();
@@ -128,6 +161,35 @@ export function shouldShowNodeError(
128
161
  return execution.status === 'FAILED' && Boolean(execution.error);
129
162
  }
130
163
 
164
+ export function getErrorSummary(error: unknown): string {
165
+ if (typeof error === 'string') return error;
166
+ if (error && typeof error === 'object') {
167
+ const message = (error as Record<string, unknown>).message;
168
+ if (typeof message === 'string' && message.trim()) return message;
169
+ try {
170
+ return JSON.stringify(error);
171
+ } catch {
172
+ return String(error);
173
+ }
174
+ }
175
+ return String(error ?? 'Unknown error');
176
+ }
177
+
178
+ export function getWorkflowFailureSummary(
179
+ result: unknown,
180
+ ): { message: string; failedNodeId?: string } | null {
181
+ if (!result || typeof result !== 'object' || Array.isArray(result))
182
+ return null;
183
+ const record = result as Record<string, unknown>;
184
+ if (record.error === undefined || record.error === null) return null;
185
+ const failedNodeId =
186
+ typeof record.failedNodeId === 'string' ? record.failedNodeId : undefined;
187
+ return {
188
+ message: getErrorSummary(record.error),
189
+ ...(failedNodeId && { failedNodeId }),
190
+ };
191
+ }
192
+
131
193
  export default function WorkflowInstanceDetailPage() {
132
194
  const { t } = useTranslation('workflow');
133
195
  const { instanceId } = useParams<{ instanceId: string }>();
@@ -251,9 +313,13 @@ export default function WorkflowInstanceDetailPage() {
251
313
 
252
314
  if (!data) return null;
253
315
 
316
+ const displayNodeExecutions = getDisplayNodeExecutions(data);
317
+ const instanceHasFinished = ['COMPLETED', 'FAILED', 'CANCELLED'].includes(
318
+ data.status,
319
+ );
254
320
  const statusCfg = STATUS_TAG[data.status] || STATUS_TAG.PENDING;
255
321
  const selectedExecution = selectedNodeId
256
- ? data.nodeExecutions.find((ne) => ne.nodeId === selectedNodeId)
322
+ ? displayNodeExecutions.find((ne) => ne.nodeId === selectedNodeId)
257
323
  : null;
258
324
  const selectedDslNode =
259
325
  selectedNodeId && data.dsl
@@ -268,6 +334,8 @@ export default function WorkflowInstanceDetailPage() {
268
334
  (attempt) => attempt.retryFromNodeId === selectedNodeId,
269
335
  )
270
336
  : undefined;
337
+ const workflowFailure =
338
+ data.status === 'FAILED' ? getWorkflowFailureSummary(data.result) : null;
271
339
 
272
340
  return (
273
341
  <div className="workflow-page-stack">
@@ -420,6 +488,19 @@ export default function WorkflowInstanceDetailPage() {
420
488
  </Descriptions.Item>
421
489
  )}
422
490
  </Descriptions>
491
+ {workflowFailure && (
492
+ <Alert
493
+ type="error"
494
+ showIcon
495
+ style={{ marginTop: 16 }}
496
+ message={workflowFailure.message}
497
+ description={
498
+ workflowFailure.failedNodeId
499
+ ? `${t('instanceDetail.failedNode')}: ${workflowFailure.failedNodeId}`
500
+ : undefined
501
+ }
502
+ />
503
+ )}
423
504
  {data.result !== null && data.result !== undefined && (
424
505
  <div style={{ marginTop: 16 }}>
425
506
  <Text strong style={{ display: 'block', marginBottom: 8 }}>
@@ -474,14 +555,14 @@ export default function WorkflowInstanceDetailPage() {
474
555
  // Timeline View
475
556
  <div style={{ display: 'flex', gap: 16 }}>
476
557
  <div style={{ flex: 1 }}>
477
- {data.nodeExecutions.length === 0 ? (
558
+ {displayNodeExecutions.length === 0 ? (
478
559
  <Text type="secondary">No node executions recorded yet.</Text>
479
560
  ) : (
480
561
  <Steps
481
562
  direction="vertical"
482
563
  size="small"
483
564
  current={-1}
484
- items={data.nodeExecutions.map((ne) => {
565
+ items={displayNodeExecutions.map((ne) => {
485
566
  const isApprovalPending =
486
567
  ne.nodeType === 'approval' &&
487
568
  ne.status === 'RUNNING' &&
@@ -590,10 +671,7 @@ export default function WorkflowInstanceDetailPage() {
590
671
  </span>
591
672
  {shouldShowNodeError(ne) && (
592
673
  <div style={{ color: '#ff4d4f', marginTop: 4 }}>
593
- Error:{' '}
594
- {typeof ne.error === 'object'
595
- ? JSON.stringify(ne.error)
596
- : String(ne.error)}
674
+ Error: {getErrorSummary(ne.error)}
597
675
  </div>
598
676
  )}
599
677
  </div>
@@ -717,7 +795,7 @@ export default function WorkflowInstanceDetailPage() {
717
795
  borderRadius: 4,
718
796
  fontSize: 11,
719
797
  color: '#ff4d4f',
720
- maxHeight: 150,
798
+ maxHeight: 320,
721
799
  overflow: 'auto',
722
800
  }}
723
801
  >
@@ -739,7 +817,7 @@ export default function WorkflowInstanceDetailPage() {
739
817
  <div style={{ flex: 1, height: 500, minHeight: 500 }}>
740
818
  <EnhancedFlowRenderer
741
819
  dsl={data.dsl}
742
- nodeExecutions={data.nodeExecutions}
820
+ nodeExecutions={displayNodeExecutions}
743
821
  showExecutionStatus={true}
744
822
  selectedNodeId={selectedNodeId}
745
823
  readonly={true}
@@ -875,7 +953,7 @@ export default function WorkflowInstanceDetailPage() {
875
953
  borderRadius: 4,
876
954
  fontSize: 11,
877
955
  color: '#ff4d4f',
878
- maxHeight: 150,
956
+ maxHeight: 320,
879
957
  overflow: 'auto',
880
958
  }}
881
959
  >
@@ -974,7 +1052,9 @@ export default function WorkflowInstanceDetailPage() {
974
1052
  >
975
1053
  {selectedNodeWasReused
976
1054
  ? t('instanceDetail.reusedResult')
977
- : 'Not Executed'}
1055
+ : instanceHasFinished
1056
+ ? 'Not Executed / Skipped'
1057
+ : 'Not Executed'}
978
1058
  </Tag>
979
1059
  </div>
980
1060
  <Descriptions column={1} size="small">
@@ -985,7 +1065,9 @@ export default function WorkflowInstanceDetailPage() {
985
1065
  <Text type="secondary">
986
1066
  {selectedNodeWasReused
987
1067
  ? t('instanceDetail.reusedResultDescription')
988
- : 'Waiting for execution'}
1068
+ : instanceHasFinished
1069
+ ? 'Not executed in this run'
1070
+ : 'Waiting for execution'}
989
1071
  </Text>
990
1072
  </Descriptions.Item>
991
1073
  </Descriptions>
@@ -98,6 +98,19 @@
98
98
  background: var(--ant-color-fill-quaternary);
99
99
  }
100
100
 
101
+ .node-instance-show__raw-configuration {
102
+ max-width: 960px;
103
+ margin: 0;
104
+ padding: 16px;
105
+ overflow: auto;
106
+ border: 1px solid var(--ant-color-border-secondary);
107
+ border-radius: 10px;
108
+ background: var(--ant-color-fill-quaternary);
109
+ font-family: var(--ant-font-family-code);
110
+ white-space: pre-wrap;
111
+ overflow-wrap: anywhere;
112
+ }
113
+
101
114
  .node-instance-show__empty {
102
115
  margin: 0;
103
116
  padding: 28px 0;
@@ -5,7 +5,7 @@ import { CodeConfigEditor } from '../widgets/CodeConfigEditor';
5
5
  import { CronTriggerConfigEditor } from '../widgets/CronTriggerConfigEditor';
6
6
  import { DbExecuteConfigEditor } from '../widgets/DbExecuteConfigEditor';
7
7
  import { DelayConfigEditor } from '../widgets/DelayConfigEditor';
8
- import { EventTriggerConfigEditor } from '../widgets/EventTriggerConfigEditor';
8
+ import { EventTriggerLifecycleConfigEditor } from '../widgets/EventTriggerLifecycleConfigEditor';
9
9
  import { ExternalRequestConfigEditor } from '../widgets/ExternalRequestConfigEditor';
10
10
  import { ExternalWaitConfigEditor } from '../widgets/ExternalWaitConfigEditor';
11
11
  import { ForEachConfigEditor } from '../widgets/ForEachConfigEditor';
@@ -14,9 +14,9 @@ import { HumanApprovalConfigEditor } from '../widgets/HumanApprovalConfigEditor'
14
14
  import { IfElseConfigEditor } from '../widgets/IfElseConfigEditor';
15
15
  import { ManualTriggerConfigEditor } from '../widgets/ManualTriggerConfigEditor';
16
16
  import { ParallelConfigEditor } from '../widgets/ParallelConfigEditor';
17
+ import { SubjectPicker } from '../widgets/SubjectPicker';
17
18
  import { SubWorkflowConfigEditor } from '../widgets/SubWorkflowConfigEditor';
18
19
  import { SwitchConfigEditor } from '../widgets/SwitchConfigEditor';
19
- import { SubjectPicker } from '../widgets/SubjectPicker';
20
20
  import {
21
21
  widgetRegistry,
22
22
  type WidgetComponent,
@@ -130,7 +130,7 @@ const DEFAULT_WIDGETS = {
130
130
  'parallel-config': ParallelConfigEditor,
131
131
  'sub-workflow-config': SubWorkflowConfigEditor,
132
132
  'db-execute-config': DbExecuteConfigEditor,
133
- 'event-trigger-config': EventTriggerConfigEditor,
133
+ 'event-trigger-config': EventTriggerLifecycleConfigEditor,
134
134
  'if-else-config': IfElseConfigEditor,
135
135
  'manual-trigger-config': ManualTriggerConfigEditor,
136
136
  'switch-config': SwitchConfigEditor,
@@ -308,7 +308,14 @@ export default function NodeInstanceShowPage() {
308
308
  />
309
309
  </div>
310
310
  </Form>
311
- ) : null}
311
+ ) : (
312
+ <pre
313
+ className="node-instance-show__raw-configuration"
314
+ aria-label={t('nodeInstance.rawConfiguration')}
315
+ >
316
+ {JSON.stringify(record.config, null, 2)}
317
+ </pre>
318
+ )}
312
319
  </Card>
313
320
 
314
321
  <Card>