@gadmin2n/schematics 0.0.151 → 0.0.153

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 (42) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +1 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +2 -1
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +23 -44
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +25 -4
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +74 -64
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +0 -7
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +17 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +58 -3
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +66 -6
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +61 -149
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +1 -1
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +1 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +1 -1
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +2 -2
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +38 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +18 -7
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +1 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +131 -242
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +7 -5
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -36
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -4
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +29 -7
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +29 -7
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +20 -17
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +38 -35
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +6 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +24 -22
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +0 -11
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +24 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +20 -18
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +7 -0
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +28 -79
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +5 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +0 -1
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +227 -97
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +62 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/settings/index.tsx +73 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +21 -46
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +2 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -0
  42. package/package.json +1 -1
@@ -40,6 +40,11 @@ const STATUS_TAG: Record<
40
40
  > = {
41
41
  PENDING: { color: 'gold', label: 'Pending', icon: <ClockCircleOutlined /> },
42
42
  RUNNING: { color: 'blue', label: 'Running', icon: <LoadingOutlined /> },
43
+ WAITING_RETRY: {
44
+ color: 'orange',
45
+ label: 'Waiting to retry',
46
+ icon: <ClockCircleOutlined />,
47
+ },
43
48
  COMPLETED: {
44
49
  color: 'green',
45
50
  label: 'Completed',
@@ -65,7 +70,6 @@ interface NodeExecution {
65
70
  interface RetryAttempt {
66
71
  id: string;
67
72
  status: string;
68
- retryFromNodeId: string | null;
69
73
  createdAt: string;
70
74
  }
71
75
 
@@ -88,9 +92,7 @@ interface InstanceDetail {
88
92
  approvableNodeIds: string[];
89
93
  retryableNodeIds: string[];
90
94
  retryAttempts: RetryAttempt[];
91
- reusedNodeIds: string[];
92
95
  retryOfInstanceId: string | null;
93
- retryFromNodeId: string | null;
94
96
  dsl?: WorkflowDSL;
95
97
  }
96
98
 
@@ -221,7 +223,11 @@ export default function WorkflowInstanceDetailPage() {
221
223
 
222
224
  // Auto-refresh for running instances
223
225
  useEffect(() => {
224
- if (data?.status === 'RUNNING' || data?.status === 'PENDING') {
226
+ if (
227
+ data?.status === 'RUNNING' ||
228
+ data?.status === 'PENDING' ||
229
+ data?.status === 'WAITING_RETRY'
230
+ ) {
225
231
  const interval = setInterval(fetchData, 3000);
226
232
  return () => clearInterval(interval);
227
233
  }
@@ -263,16 +269,16 @@ export default function WorkflowInstanceDetailPage() {
263
269
  }
264
270
  };
265
271
 
266
- const handleRetryFromNode = async (nodeId: string) => {
272
+ const handleRetryFailedNode = async (nodeId: string) => {
267
273
  setActionLoading(true);
268
274
  try {
269
- const res = await customRequest<{ instanceId: string }>(
270
- `workflow/instance/${instanceId}/retry-from-node`,
275
+ await customRequest(
276
+ `workflow/instance/${instanceId}/retry-failed-node`,
271
277
  'POST',
272
278
  { nodeId },
273
279
  );
274
280
  message.success(t('instanceDetail.retryFromNodeStarted'));
275
- navigate(`/admin/workflow/instance/show/${res.instanceId}`);
281
+ await fetchData();
276
282
  } catch (e: any) {
277
283
  message.error(e?.message || t('instanceDetail.retryFromNodeFailed'));
278
284
  } finally {
@@ -325,17 +331,10 @@ export default function WorkflowInstanceDetailPage() {
325
331
  selectedNodeId && data.dsl
326
332
  ? data.dsl.nodes.find((n) => n.id === selectedNodeId)
327
333
  : null;
328
- const selectedNodeWasReused = selectedNodeId
329
- ? data.reusedNodeIds.includes(selectedNodeId)
330
- : false;
331
334
  const latestRetryAttempt = data.retryAttempts[0];
332
- const selectedRetryAttempt = selectedNodeId
333
- ? data.retryAttempts.find(
334
- (attempt) => attempt.retryFromNodeId === selectedNodeId,
335
- )
336
- : undefined;
337
- const workflowFailure =
338
- data.status === 'FAILED' ? getWorkflowFailureSummary(data.result) : null;
335
+ const workflowFailure = ['FAILED', 'WAITING_RETRY'].includes(data.status)
336
+ ? getWorkflowFailureSummary(data.result)
337
+ : null;
339
338
 
340
339
  return (
341
340
  <div className="workflow-page-stack">
@@ -383,7 +382,7 @@ export default function WorkflowInstanceDetailPage() {
383
382
  {t('instanceDetail.openTemporal')}
384
383
  </Button>
385
384
  </Tooltip>
386
- {(data.status === 'RUNNING' || data.status === 'PENDING') && (
385
+ {['RUNNING', 'PENDING', 'WAITING_RETRY'].includes(data.status) && (
387
386
  <Popconfirm
388
387
  title="Cancel this workflow execution?"
389
388
  onConfirm={handleCancel}
@@ -480,17 +479,10 @@ export default function WorkflowInstanceDetailPage() {
480
479
  </Button>
481
480
  </Descriptions.Item>
482
481
  )}
483
- {data.reusedNodeIds.length > 0 && (
484
- <Descriptions.Item label={t('instanceDetail.reusedResults')}>
485
- {t('instanceDetail.reusedResultsCount', {
486
- count: data.reusedNodeIds.length,
487
- })}
488
- </Descriptions.Item>
489
- )}
490
482
  </Descriptions>
491
483
  {workflowFailure && (
492
484
  <Alert
493
- type="error"
485
+ type={data.status === 'WAITING_RETRY' ? 'warning' : 'error'}
494
486
  showIcon
495
487
  style={{ marginTop: 16 }}
496
488
  message={workflowFailure.message}
@@ -571,9 +563,6 @@ export default function WorkflowInstanceDetailPage() {
571
563
  const canRetryFromNode =
572
564
  ne.status === 'FAILED' &&
573
565
  data.retryableNodeIds.includes(ne.nodeId);
574
- const retryAttempt = data.retryAttempts.find(
575
- (attempt) => attempt.retryFromNodeId === ne.nodeId,
576
- );
577
566
  return {
578
567
  title: (
579
568
  <Space
@@ -620,7 +609,7 @@ export default function WorkflowInstanceDetailPage() {
620
609
  description={t(
621
610
  'instanceDetail.retryFromNodeDescription',
622
611
  )}
623
- onConfirm={() => handleRetryFromNode(ne.nodeId)}
612
+ onConfirm={() => handleRetryFailedNode(ne.nodeId)}
624
613
  okText={t('instanceDetail.retryFromNode')}
625
614
  cancelText={t('common.cancel')}
626
615
  >
@@ -635,23 +624,6 @@ export default function WorkflowInstanceDetailPage() {
635
624
  </Button>
636
625
  </Popconfirm>
637
626
  )}
638
- {retryAttempt && (
639
- <Button
640
- size="small"
641
- type="link"
642
- icon={<LinkOutlined />}
643
- onClick={(event) => {
644
- event.stopPropagation();
645
- navigate(
646
- `/admin/workflow/instance/show/${retryAttempt.id}`,
647
- );
648
- }}
649
- >
650
- {t('instanceDetail.viewRetryAttempt', {
651
- id: retryAttempt.id,
652
- })}
653
- </Button>
654
- )}
655
627
  </Space>
656
628
  ),
657
629
  description: (
@@ -821,7 +793,6 @@ export default function WorkflowInstanceDetailPage() {
821
793
  showExecutionStatus={true}
822
794
  selectedNodeId={selectedNodeId}
823
795
  readonly={true}
824
- reusedNodeIds={data.reusedNodeIds}
825
796
  onNodeClick={(nodeId) => setSelectedNodeId(nodeId)}
826
797
  />
827
798
  </div>
@@ -982,7 +953,7 @@ export default function WorkflowInstanceDetailPage() {
982
953
  'instanceDetail.retryFromNodeDescription',
983
954
  )}
984
955
  onConfirm={() =>
985
- handleRetryFromNode(selectedExecution.nodeId)
956
+ handleRetryFailedNode(selectedExecution.nodeId)
986
957
  }
987
958
  okText={t('instanceDetail.retryFromNode')}
988
959
  cancelText={t('common.cancel')}
@@ -997,22 +968,6 @@ export default function WorkflowInstanceDetailPage() {
997
968
  </Popconfirm>
998
969
  )}
999
970
 
1000
- {selectedExecution.status === 'FAILED' &&
1001
- selectedRetryAttempt && (
1002
- <Button
1003
- icon={<LinkOutlined />}
1004
- onClick={() =>
1005
- navigate(
1006
- `/admin/workflow/instance/show/${selectedRetryAttempt.id}`,
1007
- )
1008
- }
1009
- >
1010
- {t('instanceDetail.viewRetryAttempt', {
1011
- id: selectedRetryAttempt.id,
1012
- })}
1013
- </Button>
1014
- )}
1015
-
1016
971
  {/* Approval actions */}
1017
972
  {selectedExecution.nodeType === 'approval' &&
1018
973
  selectedExecution.status === 'RUNNING' &&
@@ -1047,14 +1002,10 @@ export default function WorkflowInstanceDetailPage() {
1047
1002
  <div>
1048
1003
  <div style={{ marginBottom: 16 }}>
1049
1004
  <Title level={5}>{selectedDslNode.label}</Title>
1050
- <Tag
1051
- color={selectedNodeWasReused ? 'green' : 'default'}
1052
- >
1053
- {selectedNodeWasReused
1054
- ? t('instanceDetail.reusedResult')
1055
- : instanceHasFinished
1056
- ? 'Not Executed / Skipped'
1057
- : 'Not Executed'}
1005
+ <Tag>
1006
+ {instanceHasFinished
1007
+ ? 'Not Executed / Skipped'
1008
+ : 'Not Executed'}
1058
1009
  </Tag>
1059
1010
  </div>
1060
1011
  <Descriptions column={1} size="small">
@@ -1063,11 +1014,9 @@ export default function WorkflowInstanceDetailPage() {
1063
1014
  </Descriptions.Item>
1064
1015
  <Descriptions.Item label="Status">
1065
1016
  <Text type="secondary">
1066
- {selectedNodeWasReused
1067
- ? t('instanceDetail.reusedResultDescription')
1068
- : instanceHasFinished
1069
- ? 'Not executed in this run'
1070
- : 'Waiting for execution'}
1017
+ {instanceHasFinished
1018
+ ? 'Not executed in this run'
1019
+ : 'Waiting for execution'}
1071
1020
  </Text>
1072
1021
  </Descriptions.Item>
1073
1022
  </Descriptions>
@@ -13,6 +13,7 @@ const PAGE_SIZE = 20;
13
13
  const STATUS_TAG: Record<string, { color: string; label: string }> = {
14
14
  PENDING: { color: 'gold', label: 'Pending' },
15
15
  RUNNING: { color: 'blue', label: 'Running' },
16
+ WAITING_RETRY: { color: 'orange', label: 'Waiting to retry' },
16
17
  COMPLETED: { color: 'green', label: 'Completed' },
17
18
  FAILED: { color: 'red', label: 'Failed' },
18
19
  CANCELLED: { color: 'default', label: 'Cancelled' },
@@ -233,6 +234,10 @@ export default function WorkflowInstancesPage() {
233
234
  options={[
234
235
  { value: 'PENDING', label: t('instances.statuses.pending') },
235
236
  { value: 'RUNNING', label: t('instances.statuses.running') },
237
+ {
238
+ value: 'WAITING_RETRY',
239
+ label: t('instances.statuses.waiting_retry'),
240
+ },
236
241
  { value: 'COMPLETED', label: t('instances.statuses.completed') },
237
242
  { value: 'FAILED', label: t('instances.statuses.failed') },
238
243
  { value: 'CANCELLED', label: t('instances.statuses.cancelled') },
@@ -181,7 +181,6 @@ export function NodeInstanceForm({
181
181
  ]}
182
182
  >
183
183
  <Select
184
- size="large"
185
184
  placeholder={t('nodeInstance.selectNodeType')}
186
185
  disabled={disableNodeType}
187
186
  showSearch
@@ -1,10 +1,22 @@
1
- import { Alert, Button, InputNumber, Select, Space, Typography } from 'antd';
1
+ import {
2
+ Alert,
3
+ Button,
4
+ InputNumber,
5
+ Segmented,
6
+ Select,
7
+ Space,
8
+ Switch,
9
+ Tag,
10
+ Typography,
11
+ } from 'antd';
2
12
  import { customRequest } from 'helpers/http';
3
13
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
14
  import { useTranslation } from 'react-i18next';
5
15
  import { ParameterBindingEditor } from '../../components/ParameterBindingEditor';
16
+ import { WorkflowCodeEditor } from '../../components/WorkflowCodeEditor';
6
17
  import type { ParameterBinding } from '../../types/parameter-binding';
7
18
  import type { WidgetProps } from '../schema-form/widget-registry';
19
+ import { ParameterBindingListEditor } from './ParameterBindingListEditor';
8
20
 
9
21
  export interface DatabaseFunctionArgument {
10
22
  position: number;
@@ -26,11 +38,24 @@ type DatabaseFunctionRef = Pick<
26
38
  >;
27
39
 
28
40
  type DbExecuteConfig = {
41
+ mode?: 'FUNCTION' | 'SQL';
29
42
  functionRef?: DatabaseFunctionRef;
30
43
  argumentBindings?: ParameterBinding[];
44
+ allowWrite?: boolean;
45
+ sql?: string;
46
+ parameters?: ParameterBinding[];
31
47
  timeoutSeconds?: number | null;
32
48
  };
33
49
 
50
+ function executionMode(config: DbExecuteConfig): 'FUNCTION' | 'SQL' {
51
+ if (config.mode === 'FUNCTION' || config.mode === 'SQL') return config.mode;
52
+ return config.sql !== undefined ||
53
+ config.allowWrite !== undefined ||
54
+ config.parameters !== undefined
55
+ ? 'SQL'
56
+ : 'FUNCTION';
57
+ }
58
+
34
59
  function functionKey(value: DatabaseFunctionRef): string {
35
60
  return `${value.schema}.${value.name}(${value.identityArguments})`;
36
61
  }
@@ -50,10 +75,12 @@ export function DbExecuteConfigEditor({
50
75
  }: WidgetProps<DbExecuteConfig | undefined>) {
51
76
  const { t } = useTranslation('workflow');
52
77
  const config = value ?? {};
78
+ const mode = executionMode(config);
79
+ const allowWrite = config.allowWrite === true;
53
80
  const [functions, setFunctions] = useState<DatabaseFunctionOption[]>([]);
54
81
  const [catalogStatus, setCatalogStatus] = useState<
55
- 'loading' | 'ready' | 'error'
56
- >('loading');
82
+ 'idle' | 'loading' | 'ready' | 'error'
83
+ >('idle');
57
84
 
58
85
  const loadFunctions = useCallback(async () => {
59
86
  setCatalogStatus('loading');
@@ -70,8 +97,8 @@ export function DbExecuteConfigEditor({
70
97
  }, []);
71
98
 
72
99
  useEffect(() => {
73
- void loadFunctions();
74
- }, [loadFunctions]);
100
+ if (mode === 'FUNCTION') void loadFunctions();
101
+ }, [loadFunctions, mode]);
75
102
 
76
103
  const selected = useMemo(
77
104
  () =>
@@ -89,109 +116,212 @@ export function DbExecuteConfigEditor({
89
116
  return (
90
117
  <Space direction="vertical" size={14} style={{ width: '100%' }}>
91
118
  <Space direction="vertical" size={6} style={{ width: '100%' }}>
92
- <Typography.Text>{t('nodeEditors.database.function')}</Typography.Text>
93
- <Typography.Text type="secondary">
94
- {t('nodeEditors.database.functionHint')}
95
- </Typography.Text>
96
- {catalogStatus === 'error' && (
97
- <Typography.Text type="danger">
98
- {t('nodeEditors.database.loadFailed')}{' '}
99
- <Button type="link" size="small" onClick={loadFunctions}>
100
- {t('common.retry')}
101
- </Button>
102
- </Typography.Text>
103
- )}
104
- <Select
105
- aria-label={t('nodeEditors.database.function')}
106
- value={selectedKey}
107
- disabled={disabled || catalogStatus !== 'ready'}
108
- loading={catalogStatus === 'loading'}
109
- placeholder={t('nodeEditors.database.selectFunction')}
110
- showSearch
111
- optionFilterProp="label"
112
- options={functions.map((item) => ({
113
- value: functionKey(item),
114
- label: functionKey(item),
115
- }))}
116
- onChange={(key) => {
117
- const databaseFunction = functions.find(
118
- (item) => functionKey(item) === key,
119
- );
120
- if (!databaseFunction) return;
121
- const keepExistingBindings = selectedKey === key;
119
+ <Typography.Text>{t('nodeEditors.database.mode')}</Typography.Text>
120
+ <Segmented
121
+ block
122
+ aria-label={t('nodeEditors.database.mode')}
123
+ value={mode}
124
+ disabled={disabled}
125
+ options={[
126
+ {
127
+ label: t('nodeEditors.database.functionMode'),
128
+ value: 'FUNCTION',
129
+ },
130
+ { label: t('nodeEditors.database.sqlMode'), value: 'SQL' },
131
+ ]}
132
+ onChange={(nextMode) =>
122
133
  onChange({
123
- functionRef: {
124
- schema: databaseFunction.schema,
125
- name: databaseFunction.name,
126
- identityArguments: databaseFunction.identityArguments,
127
- },
128
- argumentBindings: databaseFunction.arguments.map(
129
- (argument, index) =>
130
- (keepExistingBindings
131
- ? config.argumentBindings?.[index]
132
- : undefined) ?? defaultBinding(argument),
133
- ),
134
+ ...config,
135
+ mode: nextMode as 'FUNCTION' | 'SQL',
134
136
  timeoutSeconds: config.timeoutSeconds ?? 30,
135
- });
136
- }}
137
- style={{ width: '100%' }}
137
+ })
138
+ }
138
139
  />
139
140
  </Space>
140
141
 
141
- {catalogStatus === 'ready' && config.functionRef && !selected && (
142
- <Alert
143
- type="warning"
144
- showIcon
145
- message={t('nodeEditors.database.functionMissing')}
146
- />
147
- )}
148
-
149
- {selected && (
142
+ {mode === 'FUNCTION' ? (
150
143
  <>
151
- <Typography.Text type="secondary">
152
- {t('nodeEditors.database.returnType', {
153
- type: selected.returnType,
154
- })}
155
- </Typography.Text>
156
- {selected.arguments.length === 0 ? (
144
+ <Space direction="vertical" size={6} style={{ width: '100%' }}>
145
+ <Typography.Text>
146
+ {t('nodeEditors.database.function')}
147
+ </Typography.Text>
157
148
  <Typography.Text type="secondary">
158
- {t('nodeEditors.database.noArguments')}
149
+ {t('nodeEditors.database.functionHint')}
159
150
  </Typography.Text>
160
- ) : (
161
- selected.arguments.map((argument, index) => (
162
- <Space
163
- key={`${argument.position}:${argument.name}`}
164
- direction="vertical"
165
- size={6}
166
- style={{ width: '100%' }}
167
- >
168
- <Typography.Text>
169
- {argument.name}{' '}
170
- <Typography.Text type="secondary">
171
- ({argument.type})
172
- </Typography.Text>
151
+ {catalogStatus === 'error' && (
152
+ <Typography.Text type="danger">
153
+ {t('nodeEditors.database.loadFailed')}{' '}
154
+ <Button type="link" size="small" onClick={loadFunctions}>
155
+ {t('common.retry')}
156
+ </Button>
157
+ </Typography.Text>
158
+ )}
159
+ <Select
160
+ aria-label={t('nodeEditors.database.function')}
161
+ value={selectedKey}
162
+ disabled={disabled || catalogStatus !== 'ready'}
163
+ loading={catalogStatus === 'loading'}
164
+ placeholder={t('nodeEditors.database.selectFunction')}
165
+ showSearch
166
+ optionFilterProp="label"
167
+ options={functions.map((item) => ({
168
+ value: functionKey(item),
169
+ label: functionKey(item),
170
+ }))}
171
+ onChange={(key) => {
172
+ const databaseFunction = functions.find(
173
+ (item) => functionKey(item) === key,
174
+ );
175
+ if (!databaseFunction) return;
176
+ const keepExistingBindings = selectedKey === key;
177
+ onChange({
178
+ ...config,
179
+ mode: 'FUNCTION',
180
+ functionRef: {
181
+ schema: databaseFunction.schema,
182
+ name: databaseFunction.name,
183
+ identityArguments: databaseFunction.identityArguments,
184
+ },
185
+ argumentBindings: databaseFunction.arguments.map(
186
+ (argument, index) =>
187
+ (keepExistingBindings
188
+ ? config.argumentBindings?.[index]
189
+ : undefined) ?? defaultBinding(argument),
190
+ ),
191
+ timeoutSeconds: config.timeoutSeconds ?? 30,
192
+ });
193
+ }}
194
+ style={{ width: '100%' }}
195
+ />
196
+ </Space>
197
+
198
+ {catalogStatus === 'ready' && config.functionRef && !selected && (
199
+ <Alert
200
+ type="warning"
201
+ showIcon
202
+ message={t('nodeEditors.database.functionMissing')}
203
+ />
204
+ )}
205
+
206
+ {selected && (
207
+ <>
208
+ <Typography.Text type="secondary">
209
+ {t('nodeEditors.database.returnType', {
210
+ type: selected.returnType,
211
+ })}
212
+ </Typography.Text>
213
+ {selected.arguments.length === 0 ? (
214
+ <Typography.Text type="secondary">
215
+ {t('nodeEditors.database.noArguments')}
173
216
  </Typography.Text>
174
- <ParameterBindingEditor
175
- name={argument.name}
176
- defaultPath={defaultArgumentPath(argument.name)}
177
- value={
178
- config.argumentBindings?.[index] ?? defaultBinding(argument)
179
- }
180
- disabled={disabled}
181
- onChange={(binding) => {
182
- const argumentBindings = selected.arguments.map(
183
- (item, bindingIndex) =>
184
- config.argumentBindings?.[bindingIndex] ??
185
- defaultBinding(item),
186
- );
187
- argumentBindings[index] = binding;
188
- onChange({ ...config, argumentBindings });
189
- }}
190
- />
191
- </Space>
192
- ))
217
+ ) : (
218
+ selected.arguments.map((argument, index) => (
219
+ <Space
220
+ key={`${argument.position}:${argument.name}`}
221
+ direction="vertical"
222
+ size={6}
223
+ style={{ width: '100%' }}
224
+ >
225
+ <Typography.Text>
226
+ {argument.name}{' '}
227
+ <Typography.Text type="secondary">
228
+ ({argument.type})
229
+ </Typography.Text>
230
+ </Typography.Text>
231
+ <ParameterBindingEditor
232
+ name={argument.name}
233
+ defaultPath={defaultArgumentPath(argument.name)}
234
+ value={
235
+ config.argumentBindings?.[index] ??
236
+ defaultBinding(argument)
237
+ }
238
+ disabled={disabled}
239
+ onChange={(binding) => {
240
+ const argumentBindings = selected.arguments.map(
241
+ (item, bindingIndex) =>
242
+ config.argumentBindings?.[bindingIndex] ??
243
+ defaultBinding(item),
244
+ );
245
+ argumentBindings[index] = binding;
246
+ onChange({ ...config, argumentBindings });
247
+ }}
248
+ />
249
+ </Space>
250
+ ))
251
+ )}
252
+ </>
193
253
  )}
194
254
  </>
255
+ ) : (
256
+ <>
257
+ <div className="db-execute-config__permission">
258
+ <div>
259
+ <Typography.Text strong>
260
+ {t('nodeEditors.database.allowWrite')}
261
+ </Typography.Text>
262
+ <Typography.Text
263
+ type="secondary"
264
+ className="db-execute-config__hint"
265
+ >
266
+ {t(
267
+ allowWrite
268
+ ? 'nodeEditors.database.readWriteDescription'
269
+ : 'nodeEditors.database.readOnlyDescription',
270
+ )}
271
+ </Typography.Text>
272
+ </div>
273
+ <Switch
274
+ aria-label={t('nodeEditors.database.allowWrite')}
275
+ checked={allowWrite}
276
+ disabled={disabled}
277
+ onChange={(nextAllowWrite) =>
278
+ onChange({ ...config, allowWrite: nextAllowWrite })
279
+ }
280
+ />
281
+ </div>
282
+
283
+ <Space direction="vertical" size={6} style={{ width: '100%' }}>
284
+ <div className="db-execute-config__sql-head">
285
+ <Typography.Text strong>
286
+ {t('nodeEditors.database.sql')}
287
+ </Typography.Text>
288
+ <Tag bordered={false} color={allowWrite ? 'orange' : 'green'}>
289
+ {t(
290
+ allowWrite
291
+ ? 'nodeEditors.database.writeEnabled'
292
+ : 'nodeEditors.database.readOnly',
293
+ )}
294
+ </Tag>
295
+ </div>
296
+ <WorkflowCodeEditor
297
+ language="sql"
298
+ value={config.sql ?? ''}
299
+ readOnly={disabled}
300
+ height={280}
301
+ wordWrap="off"
302
+ ariaLabel={t('nodeEditors.database.sql')}
303
+ onChange={(sql) => onChange({ ...config, sql })}
304
+ />
305
+ </Space>
306
+
307
+ <Space direction="vertical" size={6} style={{ width: '100%' }}>
308
+ <Typography.Text>
309
+ {t('nodeEditors.database.parameters')}
310
+ </Typography.Text>
311
+ <Typography.Text
312
+ type="secondary"
313
+ className="db-execute-config__hint"
314
+ >
315
+ {t('nodeEditors.database.parametersHint')}
316
+ </Typography.Text>
317
+ <ParameterBindingListEditor
318
+ value={config.parameters}
319
+ disabled={disabled}
320
+ addLabel={t('nodeEditors.database.addParameter')}
321
+ onChange={(parameters) => onChange({ ...config, parameters })}
322
+ />
323
+ </Space>
324
+ </>
195
325
  )}
196
326
 
197
327
  <Space direction="vertical" size={6} style={{ width: '100%' }}>