@oronts/vendure-data-hub-plugin 0.1.0 → 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 +10 -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 +4 -4
@@ -0,0 +1,165 @@
1
+ import * as React from 'react';
2
+ import { useMutation } from '@tanstack/react-query';
3
+ import {
4
+ Button,
5
+ Card,
6
+ CardContent,
7
+ CardHeader,
8
+ CardTitle,
9
+ Input,
10
+ Label,
11
+ Textarea,
12
+ Badge,
13
+ } from '@vendure/dashboard';
14
+ import { PlayIcon, ChevronDown, ChevronUp } from 'lucide-react';
15
+ import { STEP_TYPE, ADAPTER_TYPES, DEFAULT_SAMPLE_DATA, STEP_TEST_DESCRIPTIONS, PLACEHOLDERS, UI_LIMITS } from '../../../constants';
16
+ import { createMutationErrorHandler } from '../../../hooks';
17
+ import { runStepTest, canTestStepType, type TestResult, type StepTestOptions } from './step-test-handlers';
18
+ import { ExtractTestResults } from './ExtractTestResults';
19
+ import { TransformTestResults, ValidateTestResults } from './TransformTestResults';
20
+ import { LoadTestResults, FeedTestResults, GenericTestResults } from './LoadTestResults';
21
+
22
+ interface StepTesterProps {
23
+ stepType: string;
24
+ adapterType: string;
25
+ config: Record<string, unknown>;
26
+ }
27
+
28
+ function getEffectiveStepType(stepType: string, adapterType: string): string {
29
+ const st = stepType?.toUpperCase() || '';
30
+ if (st === STEP_TYPE.EXTRACT || adapterType === ADAPTER_TYPES.EXTRACTOR) return STEP_TYPE.EXTRACT;
31
+ if (st === STEP_TYPE.TRANSFORM) return STEP_TYPE.TRANSFORM;
32
+ if (st === STEP_TYPE.VALIDATE) return STEP_TYPE.VALIDATE;
33
+ if (st === STEP_TYPE.LOAD || adapterType === ADAPTER_TYPES.LOADER) return STEP_TYPE.LOAD;
34
+ if (st === STEP_TYPE.FEED || adapterType === ADAPTER_TYPES.FEED) return STEP_TYPE.FEED;
35
+ if (st === STEP_TYPE.EXPORT || adapterType === ADAPTER_TYPES.EXPORTER) return STEP_TYPE.EXPORT;
36
+ if (st === STEP_TYPE.SINK || adapterType === ADAPTER_TYPES.SINK) return STEP_TYPE.SINK;
37
+ if (st === STEP_TYPE.TRIGGER) return STEP_TYPE.TRIGGER;
38
+ if (st === STEP_TYPE.ENRICH) return STEP_TYPE.ENRICH;
39
+ if (st === STEP_TYPE.ROUTE) return STEP_TYPE.ROUTE;
40
+ if (st === STEP_TYPE.GATE) return STEP_TYPE.GATE;
41
+ return st || 'UNKNOWN';
42
+ }
43
+
44
+ export function StepTester({ stepType, adapterType, config }: StepTesterProps) {
45
+ const [expanded, setExpanded] = React.useState(false);
46
+ const [result, setResult] = React.useState<TestResult | null>(null);
47
+ const [sampleInput, setSampleInput] = React.useState(DEFAULT_SAMPLE_DATA);
48
+ const [limit, setLimit] = React.useState(UI_LIMITS.PREVIEW_ROW_LIMIT);
49
+ const [resultView, setResultView] = React.useState<'table' | 'json'>('table');
50
+
51
+ const effectiveType = getEffectiveStepType(stepType, adapterType);
52
+ const canTest = canTestStepType(effectiveType);
53
+
54
+ const stepTestMutation = useMutation({
55
+ mutationFn: ({ type, options }: { type: string; options: StepTestOptions }) =>
56
+ runStepTest(type, options),
57
+ onSuccess: (data) => setResult(data),
58
+ onError: createMutationErrorHandler('test step'),
59
+ });
60
+ const loading = stepTestMutation.isPending;
61
+
62
+ // Use JSON serialization to detect actual config changes, not just reference changes
63
+ const configSignature = React.useMemo(() => JSON.stringify(config), [config]);
64
+
65
+ React.useEffect(() => {
66
+ setResult(null);
67
+ }, [configSignature, stepType, adapterType]);
68
+
69
+ // Store config in ref to use latest value without triggering callback recreation
70
+ const configRef = React.useRef(config);
71
+ React.useEffect(() => {
72
+ configRef.current = config;
73
+ }, [config]);
74
+
75
+ const runTest = React.useCallback(() => {
76
+ setResult(null);
77
+ stepTestMutation.mutate({
78
+ type: effectiveType,
79
+ options: { config: configRef.current, sampleInput, limit },
80
+ });
81
+ }, [effectiveType, sampleInput, limit, stepTestMutation.mutate]);
82
+
83
+ const renderInputSection = () => {
84
+ if (effectiveType === STEP_TYPE.EXTRACT || effectiveType === STEP_TYPE.FEED) {
85
+ return (
86
+ <div className="space-y-2">
87
+ <div className="flex items-center gap-2">
88
+ <Label htmlFor="step-tester-limit" className="text-xs">Record limit</Label>
89
+ <Input id="step-tester-limit" type="number" value={limit} onChange={e => setLimit(Math.max(1, parseInt(e.target.value) || UI_LIMITS.PREVIEW_ROW_LIMIT))} className="w-20 h-8" min={1} max={UI_LIMITS.MAX_PREVIEW_ROWS} />
90
+ </div>
91
+ <p className="text-xs text-muted-foreground">
92
+ {effectiveType === STEP_TYPE.EXTRACT ? STEP_TEST_DESCRIPTIONS.EXTRACT : 'Generates feed output using the configured feed adapter.'}
93
+ </p>
94
+ </div>
95
+ );
96
+ }
97
+ if ([STEP_TYPE.TRANSFORM, STEP_TYPE.VALIDATE, STEP_TYPE.LOAD].includes(effectiveType as typeof STEP_TYPE[keyof typeof STEP_TYPE])) {
98
+ const descriptions = {
99
+ [STEP_TYPE.TRANSFORM]: STEP_TEST_DESCRIPTIONS.TRANSFORM,
100
+ [STEP_TYPE.VALIDATE]: STEP_TEST_DESCRIPTIONS.VALIDATE,
101
+ [STEP_TYPE.LOAD]: STEP_TEST_DESCRIPTIONS.LOAD,
102
+ };
103
+ return (
104
+ <div className="space-y-2">
105
+ <Label className="text-xs">Sample Input Records (JSON Array)</Label>
106
+ <Textarea value={sampleInput} onChange={e => setSampleInput(e.target.value)} className="font-mono text-xs min-h-[100px]" placeholder={PLACEHOLDERS.SAMPLE_RECORDS} />
107
+ <p className="text-xs text-muted-foreground">{descriptions[effectiveType as keyof typeof descriptions]}</p>
108
+ </div>
109
+ );
110
+ }
111
+ return <p className="text-xs text-muted-foreground">This step type does not support direct testing.</p>;
112
+ };
113
+
114
+ const renderResults = () => {
115
+ if (!result) return null;
116
+ switch (effectiveType) {
117
+ case STEP_TYPE.EXTRACT:
118
+ return <ExtractTestResults result={result} resultView={resultView} onViewChange={setResultView} />;
119
+ case STEP_TYPE.TRANSFORM:
120
+ return <TransformTestResults result={result} resultView={resultView} onViewChange={setResultView} />;
121
+ case STEP_TYPE.VALIDATE:
122
+ return <ValidateTestResults result={result} resultView={resultView} onViewChange={setResultView} />;
123
+ case STEP_TYPE.LOAD:
124
+ return <LoadTestResults result={result} />;
125
+ case STEP_TYPE.FEED:
126
+ return <FeedTestResults result={result} />;
127
+ default:
128
+ return <GenericTestResults result={result} />;
129
+ }
130
+ };
131
+
132
+ return (
133
+ <Card className="mt-4" data-testid="datahub-steptester-tester">
134
+ <CardHeader className="py-2 px-3">
135
+ <div className="flex items-center justify-between">
136
+ <div className="flex items-center gap-2">
137
+ <CardTitle className="text-sm">Step Tester</CardTitle>
138
+ <Badge variant="outline" className="text-xs">{effectiveType}</Badge>
139
+ </div>
140
+ <Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)} className="h-7 px-2" aria-label="Toggle test panel">
141
+ {expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
142
+ </Button>
143
+ </div>
144
+ </CardHeader>
145
+ {expanded && (
146
+ <CardContent className="pt-0 pb-3 px-3 space-y-3">
147
+ {canTest ? (
148
+ <>
149
+ {renderInputSection()}
150
+ <Button onClick={runTest} disabled={loading} size="sm" className="gap-2" data-testid="datahub-steptester-run">
151
+ <PlayIcon className="h-3 w-3" />
152
+ {loading ? 'Running...' : 'Run Test'}
153
+ </Button>
154
+ {renderResults()}
155
+ </>
156
+ ) : (
157
+ <p className="text-sm text-muted-foreground">
158
+ {effectiveType === STEP_TYPE.TRIGGER ? 'Trigger steps define when pipelines run. Use the full pipeline dry run to test execution.' : `${effectiveType} steps cannot be tested individually. Use the full pipeline dry run.`}
159
+ </p>
160
+ )}
161
+ </CardContent>
162
+ )}
163
+ </Card>
164
+ );
165
+ }
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { StatusBadge, ViewToggle } from './ExtractTestResults';
3
+ import type { TestResult } from './step-test-handlers';
4
+
5
+ export interface TestResultContainerProps {
6
+ result: TestResult;
7
+ children: React.ReactNode;
8
+ showViewToggle?: boolean;
9
+ resultView?: 'table' | 'json';
10
+ onViewChange?: (view: 'table' | 'json') => void;
11
+ }
12
+
13
+ /**
14
+ * Shared container for test result displays.
15
+ * Consistent layout with status badge, message, and optional view toggle.
16
+ *
17
+ * Used by: ExtractTestResults, TransformTestResults, LoadTestResults,
18
+ * FeedTestResults, ValidateTestResults, GenericTestResults
19
+ */
20
+ export function TestResultContainer({
21
+ result,
22
+ children,
23
+ showViewToggle = false,
24
+ resultView = 'table',
25
+ onViewChange,
26
+ }: TestResultContainerProps) {
27
+ return (
28
+ <div className="space-y-3 pt-3 border-t">
29
+ <div className="flex items-center justify-between">
30
+ <div className="flex items-center gap-2">
31
+ <StatusBadge status={result.status} />
32
+ {result.message && (
33
+ <span className="text-sm text-muted-foreground">{result.message}</span>
34
+ )}
35
+ </div>
36
+ {showViewToggle && onViewChange && (
37
+ <ViewToggle resultView={resultView} onViewChange={onViewChange} />
38
+ )}
39
+ </div>
40
+ {children}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * JSON display component for test results
47
+ */
48
+ export function JsonDisplay({ data, maxHeight = '300px' }: { data: unknown; maxHeight?: string }) {
49
+ return (
50
+ <pre
51
+ className="text-xs bg-muted p-3 rounded overflow-auto"
52
+ style={{ maxHeight }}
53
+ >
54
+ {JSON.stringify(data, null, 2)}
55
+ </pre>
56
+ );
57
+ }
@@ -0,0 +1,130 @@
1
+ import * as React from 'react';
2
+ import { memo } from 'react';
3
+ import { RecordsTable } from './ExtractTestResults';
4
+ import { TestResultContainer, JsonDisplay } from './TestResultContainer';
5
+ import { UI_LIMITS, COMPONENT_HEIGHTS } from '../../../constants';
6
+ import type { TestResult } from './step-test-handlers';
7
+
8
+ interface TransformTestResultsProps {
9
+ result: TestResult;
10
+ resultView: 'table' | 'json';
11
+ onViewChange: (view: 'table' | 'json') => void;
12
+ }
13
+
14
+ /**
15
+ * Before/After diff display for transform operations
16
+ */
17
+ const BeforeAfterDiff = memo(function BeforeAfterDiff({
18
+ beforeAfter,
19
+ }: {
20
+ beforeAfter: Array<{ before: Record<string, unknown>; after: Record<string, unknown> }>;
21
+ }) {
22
+ return (
23
+ <div className="space-y-3">
24
+ {/* Index as key acceptable - static test result data, not reordered */}
25
+ {beforeAfter.slice(0, UI_LIMITS.DIFF_PREVIEW_RECORDS).map((pair, recordIndex) => (
26
+ <div key={`record-${recordIndex}`} className="border rounded overflow-hidden">
27
+ <div className="bg-muted/50 px-2 py-1 text-xs font-medium">
28
+ Record {recordIndex + 1}
29
+ </div>
30
+ <div className="grid grid-cols-2 divide-x">
31
+ <div className="p-2">
32
+ <div className="text-xs text-muted-foreground mb-1">Before</div>
33
+ <pre className={`text-[10px] bg-red-50 dark:bg-red-950/30 p-2 rounded overflow-auto ${COMPONENT_HEIGHTS.SCROLL_AREA_XXS}`}>
34
+ {JSON.stringify(pair.before, null, 2)}
35
+ </pre>
36
+ </div>
37
+ <div className="p-2">
38
+ <div className="text-xs text-muted-foreground mb-1">After</div>
39
+ <pre className={`text-[10px] bg-green-50 dark:bg-green-950/30 p-2 rounded overflow-auto ${COMPONENT_HEIGHTS.SCROLL_AREA_XXS}`}>
40
+ {JSON.stringify(pair.after, null, 2)}
41
+ </pre>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ ))}
46
+ {beforeAfter.length > UI_LIMITS.DIFF_PREVIEW_RECORDS && (
47
+ <div className="text-xs text-muted-foreground">
48
+ Showing {UI_LIMITS.DIFF_PREVIEW_RECORDS} of {beforeAfter.length} records
49
+ </div>
50
+ )}
51
+ </div>
52
+ );
53
+ });
54
+
55
+ /**
56
+ * Display component for TRANSFORM step test results
57
+ * Shows before/after comparison of transformed records
58
+ */
59
+ export const TransformTestResults = memo(function TransformTestResults({
60
+ result,
61
+ resultView,
62
+ onViewChange,
63
+ }: TransformTestResultsProps) {
64
+ if (!result.beforeAfter) {
65
+ // Fallback to records view if no beforeAfter data
66
+ if (result.records) {
67
+ return (
68
+ <TestResultContainer
69
+ result={result}
70
+ showViewToggle
71
+ resultView={resultView}
72
+ onViewChange={onViewChange}
73
+ >
74
+ {resultView === 'table' ? (
75
+ <RecordsTable records={result.records} />
76
+ ) : (
77
+ <JsonDisplay data={result.records} />
78
+ )}
79
+ </TestResultContainer>
80
+ );
81
+ }
82
+ return null;
83
+ }
84
+
85
+ return (
86
+ <TestResultContainer
87
+ result={result}
88
+ showViewToggle
89
+ resultView={resultView}
90
+ onViewChange={onViewChange}
91
+ >
92
+ {resultView === 'table' ? (
93
+ <BeforeAfterDiff beforeAfter={result.beforeAfter} />
94
+ ) : (
95
+ <JsonDisplay data={result.beforeAfter} />
96
+ )}
97
+ </TestResultContainer>
98
+ );
99
+ });
100
+
101
+ /**
102
+ * Display component for VALIDATE step test results
103
+ * Shows validation results with optional summary data
104
+ */
105
+ export const ValidateTestResults = memo(function ValidateTestResults({
106
+ result,
107
+ resultView,
108
+ onViewChange,
109
+ }: TransformTestResultsProps) {
110
+ return (
111
+ <TestResultContainer
112
+ result={result}
113
+ showViewToggle={!!result.records}
114
+ resultView={resultView}
115
+ onViewChange={onViewChange}
116
+ >
117
+ {result.records && (
118
+ resultView === 'table' ? (
119
+ <RecordsTable records={result.records} />
120
+ ) : (
121
+ <JsonDisplay data={result.records} />
122
+ )
123
+ )}
124
+
125
+ {result.data && (
126
+ <JsonDisplay data={result.data} maxHeight="200px" />
127
+ )}
128
+ </TestResultContainer>
129
+ );
130
+ });
@@ -0,0 +1,334 @@
1
+ import * as React from 'react';
2
+ import { useCallback, useMemo, memo } from 'react';
3
+ import {
4
+ Label,
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ Input,
11
+ Button,
12
+ } from '@vendure/dashboard';
13
+ import { Plus, Trash2 } from 'lucide-react';
14
+ import { PLACEHOLDERS } from '../../../constants';
15
+ import { useValidationRuleSchemas, useOptionValues, type TypedOptionValue, type ConnectionSchemaField } from '../../../hooks/api/use-config-options';
16
+ import { useStableIndexIds } from '../../../hooks/use-stable-keys';
17
+
18
+ interface ValidationRule {
19
+ id?: string;
20
+ type: string;
21
+ spec: {
22
+ field: string;
23
+ required?: boolean;
24
+ min?: number;
25
+ max?: number;
26
+ pattern?: string;
27
+ error?: string;
28
+ };
29
+ }
30
+
31
+ interface ValidationRuleWithId extends ValidationRule {
32
+ id: string;
33
+ }
34
+
35
+ const FALLBACK_RULE_TYPE = 'REQUIRED';
36
+
37
+ function detectRuleType(spec: ValidationRule['spec'], schemas: TypedOptionValue[]): string {
38
+ // Check which schema's defaultValues keys match the spec
39
+ for (const schema of schemas) {
40
+ if (!schema.defaultValues) continue;
41
+ const keys = Object.keys(schema.defaultValues);
42
+ if (keys.some(k => spec[k as keyof typeof spec] !== undefined)) {
43
+ return schema.value;
44
+ }
45
+ }
46
+ // Check if any field keys from schemas exist in spec
47
+ for (const schema of schemas) {
48
+ if (schema.fields.some(f => spec[f.key as keyof typeof spec] !== undefined)) {
49
+ return schema.value;
50
+ }
51
+ }
52
+ return schemas[0]?.value ?? FALLBACK_RULE_TYPE;
53
+ }
54
+
55
+ let validationRuleIdCounter = 0;
56
+ function generateValidationRuleId(): string {
57
+ return `validation-rule-${Date.now()}-${++validationRuleIdCounter}`;
58
+ }
59
+
60
+ export interface ValidateConfigComponentProps {
61
+ readonly config: Record<string, unknown>;
62
+ readonly onChange: (config: Record<string, unknown>) => void;
63
+ readonly showErrorHandling?: boolean;
64
+ readonly showValidationMode?: boolean;
65
+ readonly showRulesEditor?: boolean;
66
+ }
67
+
68
+ export function ValidateConfigComponent({
69
+ config,
70
+ onChange,
71
+ showErrorHandling = true,
72
+ showValidationMode = true,
73
+ showRulesEditor = true,
74
+ }: ValidateConfigComponentProps) {
75
+ const { schemas: ruleTypeSchemas } = useValidationRuleSchemas();
76
+ const ruleTypes = ruleTypeSchemas;
77
+ const { options: validationModes } = useOptionValues('validationModes');
78
+ const errorHandlingOptions = validationModes;
79
+ const errorHandlingMode = (config.errorHandlingMode as string) || 'FAIL_FAST';
80
+ const validationMode = (config.validationMode as string) || 'STRICT';
81
+ const rawRules = (config.rules as ValidationRule[]) || [];
82
+ const stableIds = useStableIndexIds(rawRules, 'validation-rule');
83
+ const rules = useMemo<ValidationRuleWithId[]>(() =>
84
+ rawRules.map((rule, index) => ({
85
+ ...rule,
86
+ id: rule.id || stableIds[index],
87
+ })),
88
+ [rawRules, stableIds]);
89
+
90
+ const updateField = useCallback((key: string, value: unknown) => {
91
+ onChange({ ...config, [key]: value });
92
+ }, [config, onChange]);
93
+
94
+ const addRule = useCallback(() => {
95
+ const defaultRuleType = ruleTypeSchemas[0]?.value ?? FALLBACK_RULE_TYPE;
96
+ const schema = ruleTypeSchemas.find(s => s.value === defaultRuleType);
97
+ const defaultSpec = schema?.defaultValues ?? { required: true };
98
+ const newRule: ValidationRuleWithId = {
99
+ id: generateValidationRuleId(),
100
+ type: 'business',
101
+ spec: { field: '', ...defaultSpec } as ValidationRule['spec'],
102
+ };
103
+ const newRules = [...rawRules, newRule];
104
+ onChange({ ...config, rules: newRules });
105
+ }, [config, rawRules, onChange, ruleTypeSchemas]);
106
+
107
+ const updateRule = useCallback((index: number, spec: ValidationRule['spec']) => {
108
+ const newRules = [...rawRules];
109
+ newRules[index] = { ...newRules[index], spec };
110
+ onChange({ ...config, rules: newRules });
111
+ }, [config, rawRules, onChange]);
112
+
113
+ const removeRule = useCallback((index: number) => {
114
+ const newRules = rawRules.filter((_, i) => i !== index);
115
+ onChange({ ...config, rules: newRules });
116
+ }, [config, rawRules, onChange]);
117
+
118
+ return (
119
+ <div className="space-y-4">
120
+ {showValidationMode && (
121
+ <div className="space-y-2">
122
+ <Label className="text-sm font-medium">Validation Mode</Label>
123
+ <Select
124
+ value={validationMode}
125
+ onValueChange={(v) => updateField('validationMode', v)}
126
+ >
127
+ <SelectTrigger className="w-full">
128
+ <SelectValue />
129
+ </SelectTrigger>
130
+ <SelectContent>
131
+ {validationModes.map((mode) => (
132
+ <SelectItem key={mode.value} value={mode.value}>
133
+ {mode.label}
134
+ </SelectItem>
135
+ ))}
136
+ </SelectContent>
137
+ </Select>
138
+ <p className="text-xs text-muted-foreground">
139
+ How to handle records that fail validation rules
140
+ </p>
141
+ </div>
142
+ )}
143
+
144
+ {showErrorHandling && (
145
+ <div className="space-y-2">
146
+ <Label className="text-sm font-medium">Error Handling</Label>
147
+ <Select
148
+ value={errorHandlingMode}
149
+ onValueChange={(v) => updateField('errorHandlingMode', v)}
150
+ >
151
+ <SelectTrigger className="w-full">
152
+ <SelectValue />
153
+ </SelectTrigger>
154
+ <SelectContent>
155
+ {errorHandlingOptions.map((mode) => (
156
+ <SelectItem key={mode.value} value={mode.value}>
157
+ {mode.label}
158
+ </SelectItem>
159
+ ))}
160
+ </SelectContent>
161
+ </Select>
162
+ <p className="text-xs text-muted-foreground">
163
+ When to stop processing: immediately on first error or after collecting all errors
164
+ </p>
165
+ </div>
166
+ )}
167
+
168
+ {showRulesEditor && (
169
+ <div className="space-y-3">
170
+ <div className="flex items-center justify-between">
171
+ <Label className="text-sm font-medium">Validation Rules</Label>
172
+ <Button
173
+ variant="outline"
174
+ size="sm"
175
+ onClick={addRule}
176
+ aria-label="Add validation rule"
177
+ data-testid="datahub-validate-add-rule-btn"
178
+ >
179
+ <Plus className="h-3 w-3 mr-1" />
180
+ Add Rule
181
+ </Button>
182
+ </div>
183
+
184
+ {rules.length === 0 && (
185
+ <p className="text-sm text-muted-foreground p-3 bg-muted/50 rounded-md">
186
+ No validation rules defined. Add rules to validate record fields.
187
+ </p>
188
+ )}
189
+
190
+ {rules.map((rule, index) => (
191
+ <ValidationRuleRow
192
+ key={rule.id}
193
+ rule={rule}
194
+ index={index}
195
+ ruleTypes={ruleTypes}
196
+ ruleTypeSchemas={ruleTypeSchemas}
197
+ updateRule={updateRule}
198
+ removeRule={removeRule}
199
+ />
200
+ ))}
201
+ </div>
202
+ )}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ interface ValidationRuleRowProps {
208
+ rule: ValidationRuleWithId;
209
+ index: number;
210
+ ruleTypes: TypedOptionValue[];
211
+ ruleTypeSchemas: TypedOptionValue[];
212
+ updateRule: (index: number, spec: ValidationRule['spec']) => void;
213
+ removeRule: (index: number) => void;
214
+ }
215
+
216
+ const ValidationRuleRow = memo(function ValidationRuleRow({
217
+ rule,
218
+ index,
219
+ ruleTypes,
220
+ ruleTypeSchemas,
221
+ updateRule,
222
+ removeRule,
223
+ }: ValidationRuleRowProps) {
224
+ const handleFieldChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
225
+ updateRule(index, { ...rule.spec, field: e.target.value });
226
+ }, [index, rule.spec, updateRule]);
227
+
228
+ const handleTypeChange = useCallback((v: string) => {
229
+ const schema = ruleTypeSchemas.find(s => s.value === v);
230
+ if (schema) {
231
+ const defaultSpec = schema.defaultValues ?? {};
232
+ updateRule(index, { field: rule.spec.field, ...defaultSpec } as ValidationRule['spec']);
233
+ }
234
+ }, [index, rule.spec.field, updateRule, ruleTypeSchemas]);
235
+
236
+ const handleErrorChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
237
+ updateRule(index, { ...rule.spec, error: e.target.value });
238
+ }, [index, rule.spec, updateRule]);
239
+
240
+ const handleRemove = useCallback(() => {
241
+ removeRule(index);
242
+ }, [index, removeRule]);
243
+
244
+ const currentType = detectRuleType(rule.spec, ruleTypeSchemas);
245
+ const currentRuleSchema = ruleTypeSchemas.find(s => s.value === currentType);
246
+
247
+ return (
248
+ <div className="flex items-start gap-2 p-3 border rounded-md bg-muted/30" data-testid={`datahub-validate-rule-row-${index}`}>
249
+ <div className="flex-1 space-y-2">
250
+ <div className="flex gap-2">
251
+ <Input
252
+ value={rule.spec.field || ''}
253
+ onChange={handleFieldChange}
254
+ placeholder={PLACEHOLDERS.FIELD_NAME}
255
+ className="flex-1"
256
+ />
257
+ <Select
258
+ value={currentType}
259
+ onValueChange={handleTypeChange}
260
+ >
261
+ <SelectTrigger className="w-32">
262
+ <SelectValue />
263
+ </SelectTrigger>
264
+ <SelectContent>
265
+ {ruleTypes.map((rt) => (
266
+ <SelectItem key={rt.value} value={rt.value}>
267
+ {rt.label}
268
+ </SelectItem>
269
+ ))}
270
+ </SelectContent>
271
+ </Select>
272
+ </div>
273
+ {currentRuleSchema && currentRuleSchema.fields.length > 0 && (
274
+ <div className="flex gap-2">
275
+ {currentRuleSchema.fields.map(field => (
276
+ <SchemaRuleField
277
+ key={field.key}
278
+ field={field}
279
+ spec={rule.spec}
280
+ index={index}
281
+ updateRule={updateRule}
282
+ />
283
+ ))}
284
+ </div>
285
+ )}
286
+ <Input
287
+ value={rule.spec.error || ''}
288
+ onChange={handleErrorChange}
289
+ placeholder={PLACEHOLDERS.ERROR_MESSAGE}
290
+ className="text-xs"
291
+ />
292
+ </div>
293
+ <Button
294
+ variant="ghost"
295
+ size="sm"
296
+ onClick={handleRemove}
297
+ className="text-destructive hover:text-destructive"
298
+ aria-label={`Delete validation rule for ${rule.spec.field || 'field'}`}
299
+ data-testid={`datahub-validate-rule-delete-${index}-btn`}
300
+ >
301
+ <Trash2 className="h-4 w-4" />
302
+ </Button>
303
+ </div>
304
+ );
305
+ });
306
+
307
+ function SchemaRuleField({
308
+ field,
309
+ spec,
310
+ index,
311
+ updateRule,
312
+ }: {
313
+ field: ConnectionSchemaField;
314
+ spec: ValidationRule['spec'];
315
+ index: number;
316
+ updateRule: (index: number, spec: ValidationRule['spec']) => void;
317
+ }) {
318
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
319
+ const value = field.type === 'number'
320
+ ? (isNaN(parseFloat(e.target.value)) ? undefined : parseFloat(e.target.value))
321
+ : e.target.value;
322
+ updateRule(index, { ...spec, [field.key]: value });
323
+ }, [index, spec, updateRule, field.key, field.type]);
324
+
325
+ return (
326
+ <Input
327
+ type={field.type === 'number' ? 'number' : 'text'}
328
+ value={spec[field.key as keyof typeof spec] ?? ''}
329
+ onChange={handleChange}
330
+ placeholder={field.placeholder ?? field.label}
331
+ className={field.type === 'number' ? 'w-24' : undefined}
332
+ />
333
+ );
334
+ }