@oronts/vendure-data-hub-plugin 0.1.1 → 0.1.2

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 (235) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/dashboard/components/common/ConnectionConfigEditor.tsx +589 -0
  3. package/dist/dashboard/components/common/HeadersEditor.tsx +90 -0
  4. package/dist/dashboard/components/common/ValidationFeedback.tsx +17 -0
  5. package/dist/dashboard/components/common/index.ts +10 -0
  6. package/dist/dashboard/components/pipelines/PipelineEditor.tsx +504 -0
  7. package/dist/dashboard/components/pipelines/PipelineExport.tsx +63 -0
  8. package/dist/dashboard/components/pipelines/PipelineImport.tsx +87 -0
  9. package/dist/dashboard/components/pipelines/ReactFlowPipelineEditor.tsx +539 -0
  10. package/dist/dashboard/components/pipelines/shared/NodePropertiesPanel.tsx +146 -0
  11. package/dist/dashboard/components/pipelines/shared/PipelineNode.tsx +155 -0
  12. package/dist/dashboard/components/pipelines/shared/PipelineSettingsPanel.tsx +392 -0
  13. package/dist/dashboard/components/pipelines/shared/StepListItem.tsx +144 -0
  14. package/dist/dashboard/components/pipelines/shared/index.ts +33 -0
  15. package/dist/dashboard/components/pipelines/shared/visual-node-config.ts +169 -0
  16. package/dist/dashboard/components/shared/LoadMoreButton.tsx +18 -0
  17. package/dist/dashboard/components/shared/entity-selector/EntitySelector.tsx +59 -0
  18. package/dist/dashboard/components/shared/entity-selector/index.ts +1 -0
  19. package/dist/dashboard/components/shared/error-boundary/ErrorBoundary.tsx +90 -0
  20. package/dist/dashboard/components/shared/error-boundary/index.ts +1 -0
  21. package/dist/dashboard/components/shared/feedback/EmptyState.tsx +36 -0
  22. package/dist/dashboard/components/shared/feedback/ErrorState.tsx +69 -0
  23. package/dist/dashboard/components/shared/feedback/LoadingState.tsx +104 -0
  24. package/dist/dashboard/components/shared/feedback/ValidationErrorDisplay.tsx +29 -0
  25. package/dist/dashboard/components/shared/feedback/index.ts +4 -0
  26. package/dist/dashboard/components/shared/file-dropzone/FileDropzone.tsx +167 -0
  27. package/dist/dashboard/components/shared/file-dropzone/index.ts +1 -0
  28. package/dist/dashboard/components/shared/filter-conditions-editor/FilterConditionsEditor.tsx +226 -0
  29. package/dist/dashboard/components/shared/filter-conditions-editor/index.ts +1 -0
  30. package/dist/dashboard/components/shared/index.ts +45 -0
  31. package/dist/dashboard/components/shared/schema-form/SchemaFormRenderer.tsx +248 -0
  32. package/dist/dashboard/components/shared/schema-form/fields/BooleanField.tsx +26 -0
  33. package/dist/dashboard/components/shared/schema-form/fields/FieldWrapper.tsx +28 -0
  34. package/dist/dashboard/components/shared/schema-form/fields/FileUploadField.tsx +171 -0
  35. package/dist/dashboard/components/shared/schema-form/fields/JsonField.tsx +132 -0
  36. package/dist/dashboard/components/shared/schema-form/fields/NumberField.tsx +33 -0
  37. package/dist/dashboard/components/shared/schema-form/fields/SelectField.tsx +70 -0
  38. package/dist/dashboard/components/shared/schema-form/fields/StringField.tsx +36 -0
  39. package/dist/dashboard/components/shared/schema-form/fields/TextareaField.tsx +31 -0
  40. package/dist/dashboard/components/shared/schema-form/fields/index.ts +23 -0
  41. package/dist/dashboard/components/shared/schema-form/index.ts +2 -0
  42. package/dist/dashboard/components/shared/schema-form/utils.ts +3 -0
  43. package/dist/dashboard/components/shared/selectable-card/SelectableCard.tsx +65 -0
  44. package/dist/dashboard/components/shared/selectable-card/index.ts +1 -0
  45. package/dist/dashboard/components/shared/stat-card/StatCard.tsx +121 -0
  46. package/dist/dashboard/components/shared/stat-card/index.ts +1 -0
  47. package/dist/dashboard/components/shared/step-config/AdapterRequiredWarning.tsx +25 -0
  48. package/dist/dashboard/components/shared/step-config/AdapterSelector.tsx +109 -0
  49. package/dist/dashboard/components/shared/step-config/AdvancedEditors.tsx +634 -0
  50. package/dist/dashboard/components/shared/step-config/EnrichConfigComponent.tsx +295 -0
  51. package/dist/dashboard/components/shared/step-config/ExtractTestResults.tsx +143 -0
  52. package/dist/dashboard/components/shared/step-config/GateConfigComponent.tsx +127 -0
  53. package/dist/dashboard/components/shared/step-config/LoadTestResults.tsx +104 -0
  54. package/dist/dashboard/components/shared/step-config/OperatorCard.tsx +266 -0
  55. package/dist/dashboard/components/shared/step-config/OperatorCheatSheetButton.tsx +54 -0
  56. package/dist/dashboard/components/shared/step-config/OperatorFieldInput.tsx +209 -0
  57. package/dist/dashboard/components/shared/step-config/RetrySettingsComponent.tsx +111 -0
  58. package/dist/dashboard/components/shared/step-config/RouteConfigComponent.tsx +125 -0
  59. package/dist/dashboard/components/shared/step-config/StepConfigPanel.tsx +564 -0
  60. package/dist/dashboard/components/shared/step-config/StepTester.tsx +165 -0
  61. package/dist/dashboard/components/shared/step-config/TestResultContainer.tsx +57 -0
  62. package/dist/dashboard/components/shared/step-config/TransformTestResults.tsx +130 -0
  63. package/dist/dashboard/components/shared/step-config/ValidateConfigComponent.tsx +334 -0
  64. package/dist/dashboard/components/shared/step-config/index.ts +29 -0
  65. package/dist/dashboard/components/shared/step-config/step-test-handlers.ts +297 -0
  66. package/dist/dashboard/components/shared/trigger-config/TriggerForm.tsx +478 -0
  67. package/dist/dashboard/components/shared/trigger-config/index.ts +1 -0
  68. package/dist/dashboard/components/shared/triggers-panel/TriggersPanel.tsx +281 -0
  69. package/dist/dashboard/components/shared/triggers-panel/index.ts +1 -0
  70. package/dist/dashboard/components/shared/wizard/ConfigurationNameCard.tsx +59 -0
  71. package/dist/dashboard/components/shared/wizard/SummaryCard.tsx +53 -0
  72. package/dist/dashboard/components/shared/wizard/WizardFooter.tsx +47 -0
  73. package/dist/dashboard/components/shared/wizard/WizardProgressBar.tsx +78 -0
  74. package/dist/dashboard/components/shared/wizard/index.ts +4 -0
  75. package/dist/dashboard/components/shared/wizard-trigger/TriggerSchemaFields.tsx +128 -0
  76. package/dist/dashboard/components/shared/wizard-trigger/TriggerSelector.tsx +33 -0
  77. package/dist/dashboard/components/shared/wizard-trigger/index.ts +2 -0
  78. package/dist/dashboard/components/templates/TemplateGallery.tsx +210 -0
  79. package/dist/dashboard/components/templates/TemplatePreview.tsx +214 -0
  80. package/dist/dashboard/components/templates/index.ts +4 -0
  81. package/dist/dashboard/components/wizards/export-wizard/DestinationStep.tsx +207 -0
  82. package/dist/dashboard/components/wizards/export-wizard/ExportWizard.tsx +221 -0
  83. package/dist/dashboard/components/wizards/export-wizard/FieldsStep.tsx +159 -0
  84. package/dist/dashboard/components/wizards/export-wizard/FormatStep.tsx +246 -0
  85. package/dist/dashboard/components/wizards/export-wizard/ReviewStep.tsx +231 -0
  86. package/dist/dashboard/components/wizards/export-wizard/SourceStep.tsx +154 -0
  87. package/dist/dashboard/components/wizards/export-wizard/TriggerStep.tsx +234 -0
  88. package/dist/dashboard/components/wizards/export-wizard/constants.ts +73 -0
  89. package/dist/dashboard/components/wizards/export-wizard/index.ts +2 -0
  90. package/dist/dashboard/components/wizards/export-wizard/types.ts +39 -0
  91. package/dist/dashboard/components/wizards/import-wizard/ImportWizard.tsx +350 -0
  92. package/dist/dashboard/components/wizards/import-wizard/MappingStep.tsx +286 -0
  93. package/dist/dashboard/components/wizards/import-wizard/PreviewStep.tsx +79 -0
  94. package/dist/dashboard/components/wizards/import-wizard/ReviewStep.tsx +266 -0
  95. package/dist/dashboard/components/wizards/import-wizard/SourceStep.tsx +537 -0
  96. package/dist/dashboard/components/wizards/import-wizard/StrategyStep.tsx +328 -0
  97. package/dist/dashboard/components/wizards/import-wizard/TargetStep.tsx +76 -0
  98. package/dist/dashboard/components/wizards/import-wizard/TemplateStep.tsx +116 -0
  99. package/dist/dashboard/components/wizards/import-wizard/TransformStep.tsx +666 -0
  100. package/dist/dashboard/components/wizards/import-wizard/TriggerStep.tsx +51 -0
  101. package/dist/dashboard/components/wizards/import-wizard/constants.ts +104 -0
  102. package/dist/dashboard/components/wizards/import-wizard/index.ts +3 -0
  103. package/dist/dashboard/components/wizards/import-wizard/types.ts +35 -0
  104. package/dist/dashboard/components/wizards/index.ts +7 -0
  105. package/dist/dashboard/components/wizards/shared/WizardStepContainer.tsx +27 -0
  106. package/dist/dashboard/components/wizards/shared/constants.ts +16 -0
  107. package/dist/dashboard/components/wizards/shared/index.ts +10 -0
  108. package/dist/dashboard/constants/colors.ts +25 -0
  109. package/dist/dashboard/constants/connection-defaults.ts +7 -0
  110. package/dist/dashboard/constants/connection-types.ts +1 -0
  111. package/dist/dashboard/constants/defaults.ts +18 -0
  112. package/dist/dashboard/constants/editor.ts +69 -0
  113. package/dist/dashboard/constants/enum-maps.ts +18 -0
  114. package/dist/dashboard/constants/fallbacks.ts +44 -0
  115. package/dist/dashboard/constants/file-format-registry.ts +206 -0
  116. package/dist/dashboard/constants/index.ts +24 -0
  117. package/dist/dashboard/constants/navigation.ts +29 -0
  118. package/dist/dashboard/constants/permissions.ts +41 -0
  119. package/dist/dashboard/constants/placeholders.ts +77 -0
  120. package/dist/dashboard/constants/routes.ts +12 -0
  121. package/dist/dashboard/constants/run-status.ts +1 -0
  122. package/dist/dashboard/constants/sentinel-values.ts +12 -0
  123. package/dist/dashboard/constants/step-configs.ts +9 -0
  124. package/dist/dashboard/constants/step-mappings.ts +170 -0
  125. package/dist/dashboard/constants/steps.ts +37 -0
  126. package/dist/dashboard/constants/toast-messages.ts +149 -0
  127. package/dist/dashboard/constants/triggers.ts +5 -0
  128. package/dist/dashboard/constants/ui-config.ts +139 -0
  129. package/dist/dashboard/constants/ui-dimensions.ts +145 -0
  130. package/dist/dashboard/constants/ui-states.ts +28 -0
  131. package/dist/dashboard/constants/ui-types.ts +85 -0
  132. package/dist/dashboard/constants/validation-patterns.ts +26 -0
  133. package/dist/dashboard/gql/gql.ts +370 -0
  134. package/dist/dashboard/gql/graphql.ts +10378 -0
  135. package/dist/dashboard/gql/index.ts +1 -0
  136. package/dist/dashboard/hooks/api/index.ts +115 -0
  137. package/dist/dashboard/hooks/api/mutation-helpers.ts +34 -0
  138. package/dist/dashboard/hooks/api/use-adapters.ts +92 -0
  139. package/dist/dashboard/hooks/api/use-config-options.ts +513 -0
  140. package/dist/dashboard/hooks/api/use-connections.ts +84 -0
  141. package/dist/dashboard/hooks/api/use-entity-field-schemas.ts +99 -0
  142. package/dist/dashboard/hooks/api/use-entity-loaders.ts +45 -0
  143. package/dist/dashboard/hooks/api/use-hooks.ts +68 -0
  144. package/dist/dashboard/hooks/api/use-logs.ts +102 -0
  145. package/dist/dashboard/hooks/api/use-pipeline-runs.ts +221 -0
  146. package/dist/dashboard/hooks/api/use-pipelines.ts +279 -0
  147. package/dist/dashboard/hooks/api/use-queues.ts +141 -0
  148. package/dist/dashboard/hooks/api/use-secrets.ts +75 -0
  149. package/dist/dashboard/hooks/api/use-settings.ts +55 -0
  150. package/dist/dashboard/hooks/api/use-step-tester.ts +79 -0
  151. package/dist/dashboard/hooks/index.ts +13 -0
  152. package/dist/dashboard/hooks/use-adapter-catalog.ts +253 -0
  153. package/dist/dashboard/hooks/use-export-templates.ts +80 -0
  154. package/dist/dashboard/hooks/use-import-templates.ts +139 -0
  155. package/dist/dashboard/hooks/use-load-more.ts +29 -0
  156. package/dist/dashboard/hooks/use-stable-keys.ts +54 -0
  157. package/dist/dashboard/hooks/use-trigger-types.ts +100 -0
  158. package/dist/dashboard/hooks/use-wizard-navigation.ts +128 -0
  159. package/dist/dashboard/index.tsx +55 -0
  160. package/dist/dashboard/routes/adapters/AdapterCard.tsx +102 -0
  161. package/dist/dashboard/routes/adapters/AdapterConstants.tsx +20 -0
  162. package/dist/dashboard/routes/adapters/AdapterDetail.tsx +208 -0
  163. package/dist/dashboard/routes/adapters/AdapterTypeSection.tsx +105 -0
  164. package/dist/dashboard/routes/adapters/AdaptersPage.tsx +276 -0
  165. package/dist/dashboard/routes/adapters/AdaptersTable.tsx +107 -0
  166. package/dist/dashboard/routes/adapters/index.ts +1 -0
  167. package/dist/dashboard/routes/connections/ConnectionDetail.tsx +218 -0
  168. package/dist/dashboard/routes/connections/ConnectionsList.tsx +34 -0
  169. package/dist/dashboard/routes/connections/index.ts +2 -0
  170. package/dist/dashboard/routes/hooks/Hooks.tsx +425 -0
  171. package/dist/dashboard/routes/hooks/hook-stages.ts +52 -0
  172. package/dist/dashboard/routes/hooks/index.ts +1 -0
  173. package/dist/dashboard/routes/index.ts +8 -0
  174. package/dist/dashboard/routes/logs/Logs.tsx +93 -0
  175. package/dist/dashboard/routes/logs/components/LogDetailDrawer.tsx +118 -0
  176. package/dist/dashboard/routes/logs/components/LogExplorerTab.tsx +367 -0
  177. package/dist/dashboard/routes/logs/components/LogLevelBadge.tsx +34 -0
  178. package/dist/dashboard/routes/logs/components/LogTableRow.tsx +70 -0
  179. package/dist/dashboard/routes/logs/components/LogsOverviewTab.tsx +178 -0
  180. package/dist/dashboard/routes/logs/components/RealtimeLogTab.tsx +122 -0
  181. package/dist/dashboard/routes/logs/index.ts +1 -0
  182. package/dist/dashboard/routes/pipelines/ErrorAuditList.tsx +39 -0
  183. package/dist/dashboard/routes/pipelines/ExportWizardPage.tsx +96 -0
  184. package/dist/dashboard/routes/pipelines/ImportWizardPage.tsx +104 -0
  185. package/dist/dashboard/routes/pipelines/PipelineDetail.tsx +211 -0
  186. package/dist/dashboard/routes/pipelines/PipelineRunsBlock.tsx +377 -0
  187. package/dist/dashboard/routes/pipelines/PipelinesList.tsx +87 -0
  188. package/dist/dashboard/routes/pipelines/RetryPatchHelper.tsx +51 -0
  189. package/dist/dashboard/routes/pipelines/RunDetailsPanel.tsx +238 -0
  190. package/dist/dashboard/routes/pipelines/RunErrorsList.tsx +116 -0
  191. package/dist/dashboard/routes/pipelines/StepCounters.tsx +24 -0
  192. package/dist/dashboard/routes/pipelines/StepSummaryTable.tsx +36 -0
  193. package/dist/dashboard/routes/pipelines/components/DryRunDialog.tsx +341 -0
  194. package/dist/dashboard/routes/pipelines/components/PipelineActionButtons.tsx +201 -0
  195. package/dist/dashboard/routes/pipelines/components/PipelineEditorToggle.tsx +116 -0
  196. package/dist/dashboard/routes/pipelines/components/PipelineFormFields.tsx +156 -0
  197. package/dist/dashboard/routes/pipelines/components/PipelineWebhookInfo.tsx +111 -0
  198. package/dist/dashboard/routes/pipelines/components/ReviewActionsPanel.tsx +342 -0
  199. package/dist/dashboard/routes/pipelines/components/ValidationPanel.tsx +121 -0
  200. package/dist/dashboard/routes/pipelines/components/VersionHistoryDialog.tsx +131 -0
  201. package/dist/dashboard/routes/pipelines/components/index.ts +25 -0
  202. package/dist/dashboard/routes/pipelines/hooks/index.ts +1 -0
  203. package/dist/dashboard/routes/pipelines/hooks/use-pipeline-validation.ts +114 -0
  204. package/dist/dashboard/routes/pipelines/index.ts +4 -0
  205. package/dist/dashboard/routes/pipelines/utils/index.ts +1 -0
  206. package/dist/dashboard/routes/pipelines/utils/pipeline-conversion.ts +261 -0
  207. package/dist/dashboard/routes/queues/ConsumersTable.tsx +134 -0
  208. package/dist/dashboard/routes/queues/DeadLettersTable.tsx +118 -0
  209. package/dist/dashboard/routes/queues/FailedRunsTable.tsx +74 -0
  210. package/dist/dashboard/routes/queues/QueuesPage.tsx +290 -0
  211. package/dist/dashboard/routes/queues/index.ts +1 -0
  212. package/dist/dashboard/routes/queues/types.ts +22 -0
  213. package/dist/dashboard/routes/secrets/SecretDetail.tsx +278 -0
  214. package/dist/dashboard/routes/secrets/SecretsList.tsx +34 -0
  215. package/dist/dashboard/routes/secrets/index.ts +2 -0
  216. package/dist/dashboard/routes/settings/Settings.tsx +343 -0
  217. package/dist/dashboard/routes/settings/index.ts +1 -0
  218. package/dist/dashboard/types/index.ts +89 -0
  219. package/dist/dashboard/types/pipeline.ts +51 -0
  220. package/dist/dashboard/types/ui-types.ts +400 -0
  221. package/dist/dashboard/types/wizard.ts +235 -0
  222. package/dist/dashboard/utils/adapter-grouping.ts +43 -0
  223. package/dist/dashboard/utils/column-analysis.ts +11 -0
  224. package/dist/dashboard/utils/field-preparation.ts +31 -0
  225. package/dist/dashboard/utils/form-validation.ts +373 -0
  226. package/dist/dashboard/utils/formatters.ts +92 -0
  227. package/dist/dashboard/utils/icon-resolver.ts +35 -0
  228. package/dist/dashboard/utils/index.ts +60 -0
  229. package/dist/dashboard/utils/query-key-factory.ts +54 -0
  230. package/dist/dashboard/utils/step-helpers.ts +32 -0
  231. package/dist/dashboard/utils/string-helpers.ts +4 -0
  232. package/dist/dashboard/utils/template-helpers.ts +26 -0
  233. package/dist/dashboard/utils/trigger-sync.ts +138 -0
  234. package/dist/dashboard/utils/wizard-to-pipeline.ts +569 -0
  235. package/package.json +2 -2
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+ import { AlertCircle } from 'lucide-react';
3
+ import type { FieldErrorProps } from '../../types';
4
+
5
+ export function FieldError({ error, touched = true, showImmediately = false, className = '' }: FieldErrorProps) {
6
+ if (!error || (!touched && !showImmediately)) return null;
7
+
8
+ return (
9
+ <div
10
+ className={`flex items-center gap-1.5 mt-1.5 text-sm text-destructive animate-in fade-in slide-in-from-top-1 duration-200 ${className}`}
11
+ role="alert"
12
+ >
13
+ <AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
14
+ <span>{error}</span>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,10 @@
1
+ export { HeadersEditor } from './HeadersEditor';
2
+
3
+ export { FieldError } from './ValidationFeedback';
4
+
5
+ export { ConnectionConfigEditor, useConnectionTypeOptions, createDefaultConnectionConfig, normalizeConnectionConfig } from './ConnectionConfigEditor';
6
+ export type { ConnectionConfigEditorProps } from './ConnectionConfigEditor';
7
+
8
+ export type {
9
+ FieldErrorProps,
10
+ } from '../../types';
@@ -0,0 +1,504 @@
1
+ import * as React from 'react';
2
+ import { useCallback, useMemo, useState, memo } from 'react';
3
+ import { Button } from '@vendure/dashboard';
4
+ import {
5
+ Play,
6
+ Settings,
7
+ Clock,
8
+ AlertTriangle,
9
+ Zap,
10
+ Bell,
11
+ } from 'lucide-react';
12
+ import { TriggersPanel } from '../shared/triggers-panel';
13
+ import { StepConfigPanel } from '../shared/step-config';
14
+ import { PipelineSettingsPanel, StepListItem } from './shared';
15
+ import {
16
+ MOVE_DIRECTION,
17
+ STEP_TYPE,
18
+ getStepTypeIcon,
19
+ PIPELINE_EDITOR_PANEL,
20
+ DEFAULT_STEP_CONFIGS,
21
+ } from '../../constants';
22
+ import { useStepConfigs, useTriggerTypes } from '../../hooks';
23
+ import type { MoveDirection, PipelineEditorPanel } from '../../constants';
24
+ import { useAdapterCatalog } from '../../hooks';
25
+ import type {
26
+ StepType,
27
+ PipelineStepDefinition,
28
+ PipelineTrigger,
29
+ PipelineEditorProps,
30
+ JsonObject,
31
+ } from '../../types';
32
+ import { getCombinedTriggers, updateDefinitionWithTriggers } from '../../utils';
33
+
34
+ const ADDABLE_STEP_TYPES = Object.keys(DEFAULT_STEP_CONFIGS).filter(
35
+ t => t !== STEP_TYPE.ROUTE && t !== STEP_TYPE.GATE
36
+ ) as StepType[];
37
+
38
+ export function PipelineEditor({ definition, onChange, issues = [] }: PipelineEditorProps) {
39
+ const [selectedStepIndex, setSelectedStepIndex] = useState<number | null>(null);
40
+ const [activePanel, setActivePanel] = useState<PipelineEditorPanel>(PIPELINE_EDITOR_PANEL.STEPS);
41
+
42
+ const { adapters, connectionCodes, secretOptions } = useAdapterCatalog();
43
+ const { configList: triggerTypeList, isLoading: isTriggerTypesLoading } = useTriggerTypes();
44
+
45
+ const steps = definition.steps ?? [];
46
+ const selectedStep = selectedStepIndex !== null ? steps[selectedStepIndex] : null;
47
+
48
+ const addStep = useCallback((type: StepType) => {
49
+ const stepCfg = DEFAULT_STEP_CONFIGS[type];
50
+ const defaultConfig: JsonObject = stepCfg?.defaultConfig
51
+ ? { ...stepCfg.defaultConfig }
52
+ : {};
53
+
54
+ const newStep: PipelineStepDefinition = {
55
+ key: `${type.toLowerCase()}-${Date.now()}`,
56
+ type,
57
+ config: defaultConfig,
58
+ };
59
+
60
+ const existingEdges = definition.edges ?? [];
61
+ const currentSteps = definition.steps ?? [];
62
+
63
+ let newEdges = existingEdges;
64
+
65
+ if (type === STEP_TYPE.TRIGGER) {
66
+ // TRIGGER steps connect TO the first non-trigger step (parallel entry points)
67
+ const firstExecutionStep = currentSteps.find(s => s.type !== STEP_TYPE.TRIGGER);
68
+ if (firstExecutionStep) {
69
+ newEdges = [
70
+ ...existingEdges,
71
+ { from: newStep.key, to: firstExecutionStep.key },
72
+ ];
73
+ }
74
+ } else if (existingEdges.length > 0 && currentSteps.length > 0) {
75
+ // Non-trigger steps chain to the last step
76
+ const lastStep = currentSteps[currentSteps.length - 1];
77
+ if (lastStep) {
78
+ newEdges = [
79
+ ...existingEdges,
80
+ { from: lastStep.key, to: newStep.key },
81
+ ];
82
+ }
83
+ }
84
+
85
+ onChange({
86
+ ...definition,
87
+ steps: [...currentSteps, newStep],
88
+ edges: newEdges,
89
+ });
90
+ setSelectedStepIndex(currentSteps.length);
91
+ }, [definition, onChange]);
92
+
93
+ const updateStep = useCallback((index: number, updatedStep: PipelineStepDefinition) => {
94
+ const currentSteps = definition.steps ?? [];
95
+ const newSteps = [...currentSteps];
96
+ newSteps[index] = updatedStep;
97
+ onChange({ ...definition, steps: newSteps });
98
+ }, [definition, onChange]);
99
+
100
+ const removeStep = useCallback((index: number) => {
101
+ const currentSteps = definition.steps ?? [];
102
+ const stepToRemove = currentSteps[index];
103
+ const stepKey = stepToRemove?.key;
104
+ const newSteps = currentSteps.filter((_, i) => i !== index);
105
+
106
+ const existingEdges = definition.edges ?? [];
107
+ const incomingEdges = existingEdges.filter(edge => edge.to === stepKey);
108
+ const outgoingEdges = existingEdges.filter(edge => edge.from === stepKey);
109
+
110
+ let newEdges = existingEdges.filter(
111
+ edge => edge.from !== stepKey && edge.to !== stepKey
112
+ );
113
+
114
+ if (incomingEdges.length > 0 && outgoingEdges.length > 0) {
115
+ const reconnectionEdges: Array<{ from: string; to: string; branch?: string }> = [];
116
+ for (const inEdge of incomingEdges) {
117
+ for (const outEdge of outgoingEdges) {
118
+ const edgeExists = newEdges.some(
119
+ e => e.from === inEdge.from && e.to === outEdge.to
120
+ );
121
+ if (!edgeExists) {
122
+ reconnectionEdges.push({
123
+ from: inEdge.from,
124
+ to: outEdge.to,
125
+ ...(inEdge.branch ? { branch: inEdge.branch } : {}),
126
+ });
127
+ }
128
+ }
129
+ }
130
+ if (reconnectionEdges.length > 0) {
131
+ newEdges = [...newEdges, ...reconnectionEdges];
132
+ }
133
+ }
134
+
135
+ onChange({ ...definition, steps: newSteps, edges: newEdges });
136
+ setSelectedStepIndex(null);
137
+ }, [definition, onChange]);
138
+
139
+ const moveStep = useCallback((index: number, direction: MoveDirection) => {
140
+ const currentSteps = definition.steps ?? [];
141
+ if (direction === MOVE_DIRECTION.UP && index === 0) return;
142
+ if (direction === MOVE_DIRECTION.DOWN && index === currentSteps.length - 1) return;
143
+
144
+ const newSteps = [...currentSteps];
145
+ const targetIndex = direction === MOVE_DIRECTION.UP ? index - 1 : index + 1;
146
+ [newSteps[index], newSteps[targetIndex]] = [newSteps[targetIndex], newSteps[index]];
147
+ onChange({ ...definition, steps: newSteps });
148
+ setSelectedStepIndex(targetIndex);
149
+ }, [definition, onChange]);
150
+
151
+ // Sync triggers between steps and triggers array
152
+ // Both visual trigger nodes (in steps) and Triggers tab edit the same data
153
+ const updateTriggers = useCallback((triggers: PipelineTrigger[]) => {
154
+ onChange(updateDefinitionWithTriggers(definition, triggers));
155
+ }, [definition, onChange]);
156
+
157
+ // Get combined triggers from both steps and triggers array
158
+ const combinedTriggers = useMemo(() => getCombinedTriggers(definition), [definition]);
159
+
160
+ // Panel switch handlers
161
+ const handleSwitchToSteps = useCallback(() => setActivePanel(PIPELINE_EDITOR_PANEL.STEPS), []);
162
+ const handleSwitchToTriggers = useCallback(() => setActivePanel(PIPELINE_EDITOR_PANEL.TRIGGERS), []);
163
+ const handleSwitchToSettings = useCallback(() => setActivePanel(PIPELINE_EDITOR_PANEL.SETTINGS), []);
164
+
165
+ // Settings change handler
166
+ const handleContextChange = useCallback((context: JsonObject) => {
167
+ onChange({ ...definition, context });
168
+ }, [definition, onChange]);
169
+
170
+ // Step selection handler factory - memoized per step index
171
+ const handleStepClick = useCallback((index: number) => {
172
+ setSelectedStepIndex(index);
173
+ }, []);
174
+
175
+ // Step action handler factories
176
+ const handleMoveStepUp = useCallback((index: number) => {
177
+ moveStep(index, MOVE_DIRECTION.UP);
178
+ }, [moveStep]);
179
+
180
+ const handleMoveStepDown = useCallback((index: number) => {
181
+ moveStep(index, MOVE_DIRECTION.DOWN);
182
+ }, [moveStep]);
183
+
184
+ const handleRemoveStep = useCallback((index: number) => {
185
+ removeStep(index);
186
+ }, [removeStep]);
187
+
188
+ // Selected step update handler
189
+ const handleSelectedStepChange = useCallback((updated: { key: string; type: string; config: JsonObject; adapterCode?: string }) => {
190
+ if (selectedStepIndex === null || !selectedStep) return;
191
+ updateStep(selectedStepIndex, {
192
+ ...selectedStep,
193
+ key: updated.key,
194
+ type: updated.type as StepType,
195
+ config: { ...updated.config, adapterCode: updated.adapterCode },
196
+ });
197
+ }, [selectedStepIndex, selectedStep, updateStep]);
198
+
199
+ return (
200
+ <div className="flex h-full border rounded-lg overflow-hidden bg-background">
201
+ <div className="w-80 border-r flex flex-col">
202
+ <div className="border-b">
203
+ <div className="flex" role="tablist" aria-label="Pipeline editor panels">
204
+ <button
205
+ type="button"
206
+ role="tab"
207
+ aria-selected={activePanel === PIPELINE_EDITOR_PANEL.STEPS}
208
+ aria-controls="panel-steps"
209
+ className={`flex-1 px-3 py-2 text-xs font-medium transition-colors ${
210
+ activePanel === PIPELINE_EDITOR_PANEL.STEPS
211
+ ? 'bg-primary/10 text-primary border-b-2 border-primary'
212
+ : 'text-muted-foreground hover:bg-muted'
213
+ }`}
214
+ onClick={handleSwitchToSteps}
215
+ data-testid="datahub-editor-tab-steps"
216
+ >
217
+ <Play className="h-3 w-3 inline mr-1" />
218
+ Steps
219
+ </button>
220
+ <button
221
+ type="button"
222
+ role="tab"
223
+ aria-selected={activePanel === PIPELINE_EDITOR_PANEL.TRIGGERS}
224
+ aria-controls="panel-triggers"
225
+ className={`flex-1 px-3 py-2 text-xs font-medium transition-colors ${
226
+ activePanel === PIPELINE_EDITOR_PANEL.TRIGGERS
227
+ ? 'bg-primary/10 text-primary border-b-2 border-primary'
228
+ : 'text-muted-foreground hover:bg-muted'
229
+ }`}
230
+ onClick={handleSwitchToTriggers}
231
+ data-testid="datahub-editor-tab-triggers"
232
+ >
233
+ <Bell className="h-3 w-3 inline mr-1" />
234
+ Triggers
235
+ </button>
236
+ <button
237
+ type="button"
238
+ role="tab"
239
+ aria-selected={activePanel === PIPELINE_EDITOR_PANEL.SETTINGS}
240
+ aria-controls="panel-settings"
241
+ className={`flex-1 px-3 py-2 text-xs font-medium transition-colors ${
242
+ activePanel === PIPELINE_EDITOR_PANEL.SETTINGS
243
+ ? 'bg-primary/10 text-primary border-b-2 border-primary'
244
+ : 'text-muted-foreground hover:bg-muted'
245
+ }`}
246
+ onClick={handleSwitchToSettings}
247
+ data-testid="datahub-editor-tab-settings"
248
+ >
249
+ <Settings className="h-3 w-3 inline mr-1" />
250
+ Settings
251
+ </button>
252
+ </div>
253
+ </div>
254
+
255
+ {activePanel === PIPELINE_EDITOR_PANEL.STEPS && (
256
+ <>
257
+ <div className="p-3 border-b bg-muted/50">
258
+ <h3 className="font-semibold text-sm">Pipeline Steps</h3>
259
+ <p className="text-xs text-muted-foreground">Click to configure each step</p>
260
+ </div>
261
+ <div className="flex-1 overflow-auto p-2 space-y-1">
262
+ {steps.map((step, index) => (
263
+ <StepListItemWrapper
264
+ key={step.key}
265
+ step={step}
266
+ index={index}
267
+ edges={definition.edges ?? []}
268
+ selectedStepIndex={selectedStepIndex}
269
+ stepsLength={steps.length}
270
+ issues={issues}
271
+ onStepClick={handleStepClick}
272
+ onMoveUp={handleMoveStepUp}
273
+ onMoveDown={handleMoveStepDown}
274
+ onRemove={handleRemoveStep}
275
+ />
276
+ ))}
277
+ {steps.length === 0 && (
278
+ <div className="p-4 text-center text-muted-foreground text-sm">
279
+ No steps yet. Add a step to get started.
280
+ </div>
281
+ )}
282
+ </div>
283
+ <div className="p-3 border-t bg-muted/50">
284
+ <p className="text-xs text-muted-foreground mb-2">Add Step:</p>
285
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-1" data-testid="datahub-editor-add-step-buttons">
286
+ {ADDABLE_STEP_TYPES.map((type) => (
287
+ <AddStepButton
288
+ key={type}
289
+ type={type}
290
+ onAddStep={addStep}
291
+ />
292
+ ))}
293
+ </div>
294
+ </div>
295
+ </>
296
+ )}
297
+
298
+ {activePanel === PIPELINE_EDITOR_PANEL.TRIGGERS && (
299
+ <TriggersPanel
300
+ triggers={combinedTriggers}
301
+ onChange={updateTriggers}
302
+ variant="compact"
303
+ />
304
+ )}
305
+
306
+ {activePanel === PIPELINE_EDITOR_PANEL.SETTINGS && (
307
+ <PipelineSettingsPanel
308
+ context={definition.context ?? {}}
309
+ onChange={handleContextChange}
310
+ />
311
+ )}
312
+ </div>
313
+
314
+ <div className="flex-1 overflow-auto">
315
+ {activePanel === PIPELINE_EDITOR_PANEL.STEPS && selectedStep ? (
316
+ <div className="p-4">
317
+ <StepConfigPanel
318
+ data={{
319
+ key: selectedStep.key,
320
+ type: selectedStep.type,
321
+ config: selectedStep.config ?? {},
322
+ adapterCode: selectedStep.config?.adapterCode as string | undefined,
323
+ }}
324
+ onChange={handleSelectedStepChange}
325
+ catalog={adapters}
326
+ connectionCodes={connectionCodes}
327
+ secretOptions={secretOptions}
328
+ variant="inline"
329
+ showHeader={true}
330
+ showDeleteButton={false}
331
+ showKeyInput={true}
332
+ showCheatSheet={true}
333
+ showStepTester={true}
334
+ showAdvancedEditors={true}
335
+ />
336
+ </div>
337
+ ) : activePanel === PIPELINE_EDITOR_PANEL.STEPS ? (
338
+ <div className="flex items-center justify-center h-full text-muted-foreground">
339
+ <div className="text-center">
340
+ <p className="text-sm">Select a step to configure</p>
341
+ <p className="text-xs mt-1">or add a new step from the left panel</p>
342
+ </div>
343
+ </div>
344
+ ) : activePanel === PIPELINE_EDITOR_PANEL.TRIGGERS ? (
345
+ <div className="p-6">
346
+ <h3 className="font-semibold mb-4">Trigger Configuration</h3>
347
+ <p className="text-sm text-muted-foreground">
348
+ Configure how and when this pipeline runs. You can have multiple triggers -
349
+ for example, run on a schedule AND allow manual triggering.
350
+ </p>
351
+ <div className="mt-4 p-4 bg-muted/50 rounded-lg">
352
+ <h4 className="text-sm font-medium mb-2">Trigger Types:</h4>
353
+ {isTriggerTypesLoading || triggerTypeList.length === 0 ? (
354
+ <ul className="text-sm text-muted-foreground space-y-1">
355
+ <li><strong>Manual</strong> - Run from dashboard or API</li>
356
+ <li><strong>Schedule</strong> - Run on cron schedule</li>
357
+ <li><strong>Webhook</strong> - Run when webhook is called</li>
358
+ <li><strong>Event</strong> - Run when Vendure event fires</li>
359
+ <li><strong>File Watch</strong> - Run when files appear</li>
360
+ </ul>
361
+ ) : (
362
+ <ul className="text-sm text-muted-foreground space-y-1">
363
+ {triggerTypeList.map(trigger => (
364
+ <li key={trigger.type}>
365
+ <strong>{trigger.label}</strong> - {trigger.description}
366
+ </li>
367
+ ))}
368
+ </ul>
369
+ )}
370
+ </div>
371
+ </div>
372
+ ) : (
373
+ <div className="p-6">
374
+ <h3 className="font-semibold mb-4">Pipeline Settings</h3>
375
+ <p className="text-sm text-muted-foreground">
376
+ Configure execution behavior including error handling, checkpointing for
377
+ resumable runs, and throughput controls.
378
+ </p>
379
+ <div className="mt-4 space-y-4">
380
+ <div className="p-4 bg-muted/50 rounded-lg">
381
+ <h4 className="text-sm font-medium mb-2 flex items-center gap-2">
382
+ <AlertTriangle className="h-4 w-4 text-amber-500" />
383
+ Error Handling
384
+ </h4>
385
+ <p className="text-xs text-muted-foreground">
386
+ Configure retries, backoff, and dead letter queue for failed records.
387
+ </p>
388
+ </div>
389
+ <div className="p-4 bg-muted/50 rounded-lg">
390
+ <h4 className="text-sm font-medium mb-2 flex items-center gap-2">
391
+ <Clock className="h-4 w-4 text-blue-500" />
392
+ Checkpointing
393
+ </h4>
394
+ <p className="text-xs text-muted-foreground">
395
+ Enable resumable execution with periodic checkpoints.
396
+ </p>
397
+ </div>
398
+ <div className="p-4 bg-muted/50 rounded-lg">
399
+ <h4 className="text-sm font-medium mb-2 flex items-center gap-2">
400
+ <Zap className="h-4 w-4 text-green-500" />
401
+ Throughput
402
+ </h4>
403
+ <p className="text-xs text-muted-foreground">
404
+ Control batch size, concurrency, and rate limiting.
405
+ </p>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ )}
410
+ </div>
411
+ </div>
412
+ );
413
+ }
414
+
415
+ interface StepListItemWrapperProps {
416
+ step: PipelineStepDefinition;
417
+ index: number;
418
+ edges: Array<{ from: string; to: string }>;
419
+ selectedStepIndex: number | null;
420
+ stepsLength: number;
421
+ issues: Array<{ stepKey?: string }>;
422
+ onStepClick: (index: number) => void;
423
+ onMoveUp: (index: number) => void;
424
+ onMoveDown: (index: number) => void;
425
+ onRemove: (index: number) => void;
426
+ }
427
+
428
+ const StepListItemWrapper = memo(function StepListItemWrapper({
429
+ step,
430
+ index,
431
+ edges,
432
+ selectedStepIndex,
433
+ stepsLength,
434
+ issues,
435
+ onStepClick,
436
+ onMoveUp,
437
+ onMoveDown,
438
+ onRemove,
439
+ }: StepListItemWrapperProps) {
440
+ const connectionCount = edges.filter(
441
+ e => e.from === step.key || e.to === step.key
442
+ ).length;
443
+
444
+ const handleClick = useCallback(() => {
445
+ onStepClick(index);
446
+ }, [index, onStepClick]);
447
+
448
+ const handleMoveUp = useCallback(() => {
449
+ onMoveUp(index);
450
+ }, [index, onMoveUp]);
451
+
452
+ const handleMoveDown = useCallback(() => {
453
+ onMoveDown(index);
454
+ }, [index, onMoveDown]);
455
+
456
+ const handleRemove = useCallback(() => {
457
+ onRemove(index);
458
+ }, [index, onRemove]);
459
+
460
+ return (
461
+ <StepListItem
462
+ step={step}
463
+ index={index}
464
+ isSelected={selectedStepIndex === index}
465
+ onClick={handleClick}
466
+ onMoveUp={handleMoveUp}
467
+ onMoveDown={handleMoveDown}
468
+ onRemove={handleRemove}
469
+ isFirst={index === 0}
470
+ isLast={index === stepsLength - 1}
471
+ issueCount={issues.filter(i => i.stepKey === step.key).length}
472
+ connectionCount={connectionCount}
473
+ />
474
+ );
475
+ });
476
+
477
+ interface AddStepButtonProps {
478
+ type: StepType;
479
+ onAddStep: (type: StepType) => void;
480
+ }
481
+
482
+ const AddStepButton = memo(function AddStepButton({ type, onAddStep }: AddStepButtonProps) {
483
+ const { getStepConfig } = useStepConfigs();
484
+ const config = getStepConfig(type);
485
+ const Icon = getStepTypeIcon(type) ?? Play;
486
+
487
+ const handleClick = useCallback(() => {
488
+ onAddStep(type);
489
+ }, [type, onAddStep]);
490
+
491
+ return (
492
+ <Button
493
+ variant="outline"
494
+ size="sm"
495
+ className="h-8 text-xs"
496
+ onClick={handleClick}
497
+ title={config?.description}
498
+ data-testid={`datahub-editor-add-step-${type.toLowerCase()}`}
499
+ >
500
+ <Icon className="h-3 w-3 mr-1" />
501
+ {config?.label ?? type}
502
+ </Button>
503
+ );
504
+ });
@@ -0,0 +1,63 @@
1
+ import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Textarea } from '@vendure/dashboard';
2
+ import * as React from 'react';
3
+ import { DIALOG_DIMENSIONS, TEXTAREA_HEIGHTS } from '../../constants';
4
+
5
+ interface Props {
6
+ definition: unknown;
7
+ }
8
+
9
+ export function PipelineExportDialog({ definition }: Readonly<Props>) {
10
+ const [open, setOpen] = React.useState(false);
11
+ const code = React.useMemo(() => toPipelineTs(definition), [definition]);
12
+
13
+ async function copyToClipboard() {
14
+ try {
15
+ await navigator.clipboard.writeText(code);
16
+ } catch {
17
+ // Clipboard API failed - silently ignore (user can still use download)
18
+ }
19
+ }
20
+
21
+ function downloadFile() {
22
+ const blob = new Blob([code], { type: 'text/plain' });
23
+ const url = URL.createObjectURL(blob);
24
+ const downloadLink = document.createElement('a');
25
+ downloadLink.href = url;
26
+ downloadLink.download = 'pipeline.ts';
27
+ downloadLink.click();
28
+ URL.revokeObjectURL(url);
29
+ }
30
+
31
+ return (
32
+ <>
33
+ <Button variant="outline" onClick={() => setOpen(true)}>
34
+ Export to code
35
+ </Button>
36
+ <Dialog open={open} onOpenChange={setOpen}>
37
+ <DialogContent className={`${DIALOG_DIMENSIONS.MAX_WIDTH_2XL} ${DIALOG_DIMENSIONS.MAX_HEIGHT_80VH} flex flex-col`}>
38
+ <DialogHeader className="flex-none">
39
+ <DialogTitle>Export pipeline</DialogTitle>
40
+ <DialogDescription>Copy or download TypeScript DSL</DialogDescription>
41
+ </DialogHeader>
42
+ <div className="flex flex-col gap-3 flex-1 min-h-0">
43
+ <Textarea value={code} readOnly className={`font-mono text-xs flex-1 ${TEXTAREA_HEIGHTS.CODE_EXPORT_MIN} ${TEXTAREA_HEIGHTS.CODE_EXPORT_MAX} resize-none`} />
44
+ <div className="flex gap-2 flex-none">
45
+ <Button onClick={copyToClipboard}>Copy</Button>
46
+ <Button variant="secondary" onClick={downloadFile}>
47
+ Download
48
+ </Button>
49
+ </div>
50
+ </div>
51
+ </DialogContent>
52
+ </Dialog>
53
+ </>
54
+ );
55
+ }
56
+
57
+ function toPipelineTs(definition: unknown): string {
58
+ const json = JSON.stringify(definition, null, 2);
59
+ return `import { definePipeline } from '@vendure/data-hub';
60
+
61
+ export default definePipeline(${json} as const);
62
+ `;
63
+ }
@@ -0,0 +1,87 @@
1
+ import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Textarea } from '@vendure/dashboard';
2
+ import * as React from 'react';
3
+ import { getErrorMessage } from '../../../shared';
4
+ import { useValidatePipelineDefinition } from '../../hooks';
5
+ import type { PipelineDefinition } from '../../types';
6
+
7
+ interface Props {
8
+ onImport: (definition: PipelineDefinition) => void;
9
+ }
10
+
11
+ export function PipelineImportDialog({ onImport }: Readonly<Props>) {
12
+ const [open, setOpen] = React.useState(false);
13
+ const [text, setText] = React.useState('');
14
+ const [errors, setErrors] = React.useState<string[]>([]);
15
+ const [parsed, setParsed] = React.useState<PipelineDefinition | null>(null);
16
+
17
+ const validateMutation = useValidatePipelineDefinition();
18
+
19
+ async function handleValidate() {
20
+ setErrors([]);
21
+ setParsed(null);
22
+ try {
23
+ const def = JSON.parse(text);
24
+ const result = await validateMutation.mutateAsync(def);
25
+ if (result?.isValid) {
26
+ setParsed(def);
27
+ } else {
28
+ setErrors(result?.errors ?? ['Invalid definition']);
29
+ }
30
+ } catch (e) {
31
+ setErrors([getErrorMessage(e)]);
32
+ }
33
+ }
34
+
35
+ function handleImport() {
36
+ if (parsed) {
37
+ onImport(parsed);
38
+ setOpen(false);
39
+ setText('');
40
+ setErrors([]);
41
+ setParsed(null);
42
+ }
43
+ }
44
+
45
+ return (
46
+ <>
47
+ <Button variant="outline" onClick={() => setOpen(true)}>
48
+ Import JSON
49
+ </Button>
50
+ <Dialog open={open} onOpenChange={setOpen}>
51
+ <DialogContent className="max-w-3xl">
52
+ <DialogHeader>
53
+ <DialogTitle>Import pipeline from JSON</DialogTitle>
54
+ <DialogDescription>Paste a PipelineDefinition JSON and validate before importing.</DialogDescription>
55
+ </DialogHeader>
56
+ <div className="space-y-3">
57
+ <Textarea
58
+ value={text}
59
+ onChange={(e) => setText(e.target.value)}
60
+ placeholder='{"version":1,"steps":[]}'
61
+ className="font-mono min-h-[260px]"
62
+ />
63
+ {errors.length > 0 && (
64
+ <div className="border border-destructive/40 rounded-md p-3">
65
+ <div className="text-sm font-medium text-destructive mb-1">Validation errors</div>
66
+ <ul className="list-disc pl-5 text-sm">
67
+ {/* Index as key acceptable - error messages are static after validation */}
68
+ {errors.map((e, errorIndex) => (
69
+ <li key={`error-${errorIndex}`}>{e}</li>
70
+ ))}
71
+ </ul>
72
+ </div>
73
+ )}
74
+ <div className="flex gap-2">
75
+ <Button variant="outline" onClick={handleValidate} disabled={validateMutation.isPending}>
76
+ {validateMutation.isPending ? 'Validating…' : 'Validate'}
77
+ </Button>
78
+ <Button onClick={handleImport} disabled={!parsed}>
79
+ Import
80
+ </Button>
81
+ </div>
82
+ </div>
83
+ </DialogContent>
84
+ </Dialog>
85
+ </>
86
+ );
87
+ }