@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to the Data Hub Plugin are documented here.
4
4
 
5
+ ## [0.1.2] - 2026-02-24
6
+
7
+ ### Fixed
8
+ - Fixed dashboard extension path: Copy `dashboard/` into `dist/` during build so the relative path `../dashboard/index.tsx` resolves correctly from both source (`src/`) and compiled (`dist/src/`) locations
9
+
10
+ ## [0.1.1] - 2026-02-24
11
+
12
+ ### Fixed
13
+ - Fixed package.json entry points: Updated `main` and `types` paths from `dist/index.js` to `dist/src/index.js` to match actual build output structure
14
+
5
15
  ## [0.1.0] - 2026-02-24
6
16
 
7
17
  Initial production release of the Data Hub Plugin for Vendure.
@@ -0,0 +1,589 @@
1
+ import * as React from 'react';
2
+ import { Button, Input, Switch, Label } from '@vendure/dashboard';
3
+ import { PlusCircle, Trash2 } from 'lucide-react';
4
+ import { ConnectionAuthType } from '../../../shared/types';
5
+ import {
6
+ HTTP_CONNECTION_DEFAULTS,
7
+ PLACEHOLDERS,
8
+ CONNECTION_TYPE,
9
+ } from '../../constants';
10
+ import { useOptionValues, useConnectionSchemas } from '../../hooks/api/use-config-options';
11
+ import type { ConnectionSchemaField } from '../../hooks/api/use-config-options';
12
+ import { validateUrl, validatePort, validateHostname } from '../../utils';
13
+ import { FieldError } from './ValidationFeedback';
14
+ import type { UIConnectionType, HttpConnectionConfig, DataHubSecret } from '../../types';
15
+
16
+ const DEFAULT_HTTP_CONFIG: HttpConnectionConfig = {
17
+ baseUrl: '',
18
+ timeout: HTTP_CONNECTION_DEFAULTS.TIMEOUT_MS,
19
+ headers: {},
20
+ auth: { type: ConnectionAuthType.NONE },
21
+ };
22
+
23
+ type SecretOption = Pick<DataHubSecret, 'code' | 'provider'>;
24
+
25
+ export interface ConnectionConfigEditorProps {
26
+ type: UIConnectionType;
27
+ config: Record<string, unknown>;
28
+ onChange: (config: Record<string, unknown>) => void;
29
+ disabled?: boolean;
30
+ secretOptions?: SecretOption[];
31
+ }
32
+
33
+ interface ConfigFieldDef {
34
+ key: string;
35
+ label: string;
36
+ type: 'string' | 'number' | 'boolean' | 'password' | 'secret';
37
+ placeholder?: string;
38
+ required?: boolean;
39
+ description?: string;
40
+ }
41
+
42
+ /**
43
+ * Fallback connection schemas used when the backend `connectionSchemas` query
44
+ * has not yet loaded or is unavailable.
45
+ */
46
+
47
+ export function ConnectionConfigEditor({ type, config, onChange, disabled, secretOptions = [] }: ConnectionConfigEditorProps) {
48
+ const resolvedType = (typeof type === 'string' && type.length > 0 ? type : CONNECTION_TYPE.HTTP) as UIConnectionType;
49
+ const { schemas: backendSchemas } = useConnectionSchemas();
50
+
51
+ // HTTP-like types use the dedicated HTTP editor with auth/headers support.
52
+ // Determined by backend `httpLike` metadata on the connection schema.
53
+ const isHttpLike = backendSchemas.some(s => s.type === resolvedType && s.httpLike === true);
54
+
55
+ if (isHttpLike) {
56
+ return (
57
+ <HttpConnectionFields
58
+ config={config as Record<string, unknown>}
59
+ onChange={onChange}
60
+ disabled={disabled}
61
+ secretOptions={secretOptions}
62
+ />
63
+ );
64
+ }
65
+
66
+ const schema = resolveSchema(resolvedType, backendSchemas);
67
+ if (!schema || schema.length === 0) {
68
+ return <div className="text-center py-4 text-muted-foreground">No configuration options available for this type.</div>;
69
+ }
70
+
71
+ const updateField = (key: string, value: unknown) => {
72
+ const next = { ...config };
73
+ if (value === undefined || value === '' || value === null) {
74
+ delete next[key];
75
+ } else {
76
+ next[key] = value;
77
+ }
78
+ onChange(next);
79
+ };
80
+
81
+ return (
82
+ <div className="space-y-4">
83
+ {schema.map(field => (
84
+ <div key={field.key} className="space-y-1">
85
+ <div className="flex items-center gap-1">
86
+ <Label className="text-sm font-medium">
87
+ {field.label}
88
+ {field.required && <span className="text-destructive ml-0.5">*</span>}
89
+ </Label>
90
+ </div>
91
+ <ConfigField
92
+ field={field}
93
+ value={config[field.key]}
94
+ onChange={value => updateField(field.key, value)}
95
+ disabled={disabled}
96
+ secretOptions={secretOptions}
97
+ />
98
+ {field.description && <p className="text-xs text-muted-foreground">{field.description}</p>}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function HttpConnectionFields({
106
+ config,
107
+ onChange,
108
+ disabled,
109
+ secretOptions,
110
+ }: {
111
+ config: Record<string, unknown>;
112
+ onChange: (cfg: Record<string, unknown>) => void;
113
+ disabled?: boolean;
114
+ secretOptions?: SecretOption[];
115
+ }) {
116
+ const authOptions = useAuthOptions();
117
+ const normalized = React.useMemo(() => normalizeHttpConfig(config), [config]);
118
+ const [urlTouched, setUrlTouched] = React.useState(false);
119
+
120
+ const urlError = React.useMemo(() => {
121
+ if (!normalized.baseUrl || normalized.baseUrl.trim() === '') {
122
+ return null;
123
+ }
124
+ const error = validateUrl(normalized.baseUrl, 'Base URL');
125
+ return error?.message ?? null;
126
+ }, [normalized.baseUrl]);
127
+
128
+ const updateConfig = (patch: Partial<HttpConnectionConfig>) => {
129
+ onChange({ ...normalized, ...patch });
130
+ };
131
+
132
+ // Derive headerRows from props. Use a stable key map to preserve row IDs across renders
133
+ // while still allowing the UI to reflect prop changes.
134
+ const headerRowsKeyRef = React.useRef<Map<string, string>>(new Map());
135
+ const headerRows = React.useMemo(() => {
136
+ const headers = normalized.headers;
137
+ if (!headers) {
138
+ headerRowsKeyRef.current.clear();
139
+ return [];
140
+ }
141
+ const newKeyMap = new Map<string, string>();
142
+ const rows = Object.entries(headers).map(([name, value]) => {
143
+ // Reuse existing ID if we had this header name before, otherwise create new
144
+ const existingId = headerRowsKeyRef.current.get(name);
145
+ const id = existingId ?? createRowId();
146
+ newKeyMap.set(name, id);
147
+ return { id, name, value };
148
+ });
149
+ headerRowsKeyRef.current = newKeyMap;
150
+ return rows;
151
+ }, [normalized.headers]);
152
+
153
+ const commitHeaders = (rows: HeaderRow[]) => {
154
+ const cleaned = rows.filter(row => row.name.trim() && row.value.trim());
155
+ const next = cleaned.length ? Object.fromEntries(cleaned.map(row => [row.name.trim(), row.value])) : undefined;
156
+ updateConfig({ headers: next });
157
+ };
158
+
159
+ const auth = normalized.auth ?? { type: ConnectionAuthType.NONE };
160
+
161
+ const handleAuthTypeChange = (next: ConnectionAuthType) => {
162
+ if (next === ConnectionAuthType.NONE) {
163
+ updateConfig({ auth: { type: ConnectionAuthType.NONE } });
164
+ return;
165
+ }
166
+ updateConfig({ auth: { type: next } });
167
+ };
168
+
169
+ const updateAuthField = (key: string, value?: string) => {
170
+ const nextAuth: Record<string, unknown> = { ...(auth ?? { type: ConnectionAuthType.NONE }) };
171
+ if (value === undefined || value === '') {
172
+ delete nextAuth[key];
173
+ } else {
174
+ nextAuth[key] = value;
175
+ }
176
+ updateConfig({ auth: nextAuth as HttpConnectionConfig['auth'] });
177
+ };
178
+
179
+ return (
180
+ <div className="space-y-6">
181
+ <div className="space-y-2">
182
+ <Label className="text-sm font-medium">Base URL</Label>
183
+ <Input
184
+ placeholder={HTTP_CONNECTION_DEFAULTS.BASE_URL_PLACEHOLDER}
185
+ value={normalized.baseUrl}
186
+ onChange={e => updateConfig({ baseUrl: e.target.value })}
187
+ onBlur={() => setUrlTouched(true)}
188
+ disabled={disabled}
189
+ className={urlError && urlTouched ? 'border-destructive focus-visible:ring-destructive' : ''}
190
+ />
191
+ <FieldError error={urlError} touched={urlTouched} />
192
+ {!urlError && (
193
+ <p className="text-xs text-muted-foreground">Relative endpoints will be resolved against this URL.</p>
194
+ )}
195
+ </div>
196
+
197
+ <div className="space-y-2">
198
+ <Label className="text-sm font-medium">Timeout (ms)</Label>
199
+ <Input
200
+ type="number"
201
+ min={0}
202
+ value={normalized.timeout ?? ''}
203
+ onChange={e => updateConfig({ timeout: e.target.value ? Number(e.target.value) : undefined })}
204
+ disabled={disabled}
205
+ />
206
+ </div>
207
+
208
+ <div className="space-y-3" role="group" aria-labelledby="default-headers-label">
209
+ <div className="flex items-center justify-between">
210
+ <div>
211
+ <Label id="default-headers-label" className="text-sm font-medium">Default Headers</Label>
212
+ <p className="text-xs text-muted-foreground">Applied to every request.</p>
213
+ </div>
214
+ <Button
215
+ type="button"
216
+ variant="outline"
217
+ size="sm"
218
+ onClick={() => commitHeaders([...headerRows, createHeaderRow()])}
219
+ disabled={disabled}
220
+ aria-label="Add new HTTP header"
221
+ >
222
+ <PlusCircle className="w-4 h-4 mr-2" />
223
+ Add header
224
+ </Button>
225
+ </div>
226
+ {headerRows.length === 0 && <p className="text-sm text-muted-foreground">No headers configured.</p>}
227
+ {headerRows.map(row => (
228
+ <div key={row.id} className="grid grid-cols-[1fr,1fr,auto] gap-3">
229
+ <Input
230
+ placeholder={PLACEHOLDERS.HEADER_NAME}
231
+ value={row.name}
232
+ onChange={e => {
233
+ const next = headerRows.map(r => (r.id === row.id ? { ...r, name: e.target.value } : r));
234
+ commitHeaders(next);
235
+ }}
236
+ disabled={disabled}
237
+ />
238
+ <Input
239
+ placeholder={PLACEHOLDERS.HEADER_VALUE}
240
+ value={row.value}
241
+ onChange={e => {
242
+ const next = headerRows.map(r => (r.id === row.id ? { ...r, value: e.target.value } : r));
243
+ commitHeaders(next);
244
+ }}
245
+ disabled={disabled}
246
+ />
247
+ <Button
248
+ type="button"
249
+ variant="ghost"
250
+ size="icon"
251
+ onClick={() => commitHeaders(headerRows.filter(r => r.id !== row.id))}
252
+ disabled={disabled}
253
+ aria-label="Remove header"
254
+ >
255
+ <Trash2 className="w-4 h-4" />
256
+ </Button>
257
+ </div>
258
+ ))}
259
+ </div>
260
+
261
+ <div className="space-y-3" role="group" aria-labelledby="authentication-label">
262
+ <Label id="authentication-label" className="text-sm font-medium">Authentication</Label>
263
+ <div className="flex flex-wrap gap-2" role="radiogroup" aria-label="Select authentication method">
264
+ {authOptions.map(option => (
265
+ <Button
266
+ key={option.value}
267
+ type="button"
268
+ variant={auth.type === option.value ? 'default' : 'outline'}
269
+ onClick={() => handleAuthTypeChange(option.value)}
270
+ disabled={disabled}
271
+ aria-pressed={auth.type === option.value}
272
+ >
273
+ {option.label}
274
+ </Button>
275
+ ))}
276
+ </div>
277
+
278
+ {auth.type === ConnectionAuthType.BEARER && (
279
+ <div className="space-y-2">
280
+ <Label className="text-sm font-medium">Secret Code</Label>
281
+ <SecretReferenceInput
282
+ value={auth.secretCode ?? ''}
283
+ onChange={value => updateAuthField('secretCode', value)}
284
+ placeholder={PLACEHOLDERS.BEARER_TOKEN}
285
+ disabled={disabled}
286
+ options={secretOptions ?? []}
287
+ />
288
+ <p className="text-xs text-muted-foreground">Token will be sent as a Bearer Authorization header.</p>
289
+ </div>
290
+ )}
291
+
292
+ {auth.type === ConnectionAuthType.API_KEY && (
293
+ <div className="space-y-4">
294
+ <div className="space-y-2">
295
+ <Label className="text-sm font-medium">Header Name</Label>
296
+ <Input
297
+ placeholder={PLACEHOLDERS.API_KEY_HEADER}
298
+ value={auth.headerName ?? ''}
299
+ onChange={e => updateAuthField('headerName', e.target.value)}
300
+ disabled={disabled}
301
+ />
302
+ </div>
303
+ <div className="space-y-2">
304
+ <Label className="text-sm font-medium">Secret Code</Label>
305
+ <SecretReferenceInput
306
+ value={auth.secretCode ?? ''}
307
+ onChange={value => updateAuthField('secretCode', value)}
308
+ placeholder={PLACEHOLDERS.API_KEY_SECRET}
309
+ disabled={disabled}
310
+ options={secretOptions ?? []}
311
+ />
312
+ </div>
313
+ </div>
314
+ )}
315
+
316
+ {auth.type === ConnectionAuthType.BASIC && (
317
+ <div className="space-y-4">
318
+ <div className="space-y-2">
319
+ <Label className="text-sm font-medium">Username</Label>
320
+ <Input
321
+ placeholder={PLACEHOLDERS.SERVICE_USER}
322
+ value={auth.username ?? ''}
323
+ onChange={e => updateAuthField('username', e.target.value)}
324
+ disabled={disabled}
325
+ />
326
+ </div>
327
+ <div className="space-y-2">
328
+ <Label className="text-sm font-medium">Password Secret Code</Label>
329
+ <SecretReferenceInput
330
+ value={auth.secretCode ?? ''}
331
+ onChange={value => updateAuthField('secretCode', value)}
332
+ placeholder={PLACEHOLDERS.PASSWORD_SECRET}
333
+ disabled={disabled}
334
+ options={secretOptions ?? []}
335
+ />
336
+ </div>
337
+ </div>
338
+ )}
339
+ </div>
340
+ </div>
341
+ );
342
+ }
343
+
344
+ interface HeaderRow {
345
+ id: string;
346
+ name: string;
347
+ value: string;
348
+ }
349
+
350
+ function useAuthOptions(): Array<{ value: ConnectionAuthType; label: string }> {
351
+ const { options: backendOptions } = useOptionValues('authTypes');
352
+ return React.useMemo(() => {
353
+ return backendOptions.map(opt => ({
354
+ value: opt.value as ConnectionAuthType,
355
+ label: opt.label,
356
+ }));
357
+ }, [backendOptions]);
358
+ }
359
+
360
+ function createHeaderRow(): HeaderRow {
361
+ return { id: createRowId(), name: '', value: '' };
362
+ }
363
+
364
+ function createRowId(): string {
365
+ return (crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2, 10)).slice(0, 8);
366
+ }
367
+
368
+ function normalizeHttpConfig(config: Record<string, unknown>): HttpConnectionConfig {
369
+ const next: HttpConnectionConfig = { ...DEFAULT_HTTP_CONFIG };
370
+ if (typeof config.baseUrl === 'string') {
371
+ next.baseUrl = config.baseUrl;
372
+ }
373
+ if (typeof config.timeout === 'number') {
374
+ next.timeout = config.timeout;
375
+ }
376
+ if (config.headers && typeof config.headers === 'object') {
377
+ const headers: Record<string, string> = {};
378
+ for (const [key, value] of Object.entries(config.headers as Record<string, unknown>)) {
379
+ if (typeof value === 'string') {
380
+ headers[key] = value;
381
+ }
382
+ }
383
+ next.headers = headers;
384
+ }
385
+ if (config.auth && typeof config.auth === 'object') {
386
+ const auth = config.auth as Record<string, unknown>;
387
+ const type = (auth.type as ConnectionAuthType) ?? ConnectionAuthType.NONE;
388
+ next.auth = { type };
389
+ if (typeof auth.headerName === 'string') next.auth.headerName = auth.headerName;
390
+ if (typeof auth.secretCode === 'string') next.auth.secretCode = auth.secretCode;
391
+ if (typeof auth.username === 'string') next.auth.username = auth.username;
392
+ if (typeof auth.usernameSecretCode === 'string') next.auth.usernameSecretCode = auth.usernameSecretCode;
393
+ }
394
+ return next;
395
+ }
396
+
397
+ /**
398
+ * Resolves the field schema for a given connection type from backend data.
399
+ */
400
+ function resolveSchema(
401
+ type: string,
402
+ backendSchemas: ReadonlyArray<{ type: string; fields: ConnectionSchemaField[] }>,
403
+ ): ConfigFieldDef[] {
404
+ const backendEntry = backendSchemas.find(s => s.type === type);
405
+ if (!backendEntry || backendEntry.fields.length === 0) return [];
406
+ return backendEntry.fields.map(f => ({
407
+ key: f.key,
408
+ label: f.label,
409
+ type: mapBackendFieldType(f.type),
410
+ placeholder: f.placeholder ?? undefined,
411
+ required: f.required ?? undefined,
412
+ description: f.description ?? undefined,
413
+ }));
414
+ }
415
+
416
+ /** Maps backend field type strings to the ConfigFieldDef type union. */
417
+ function mapBackendFieldType(backendType: string): ConfigFieldDef['type'] {
418
+ switch (backendType) {
419
+ case 'text': return 'string';
420
+ case 'number': return 'number';
421
+ case 'password': return 'password';
422
+ case 'boolean': return 'boolean';
423
+ case 'secret': return 'secret';
424
+ default: return 'string';
425
+ }
426
+ }
427
+
428
+ interface ConfigFieldProps {
429
+ field: ConfigFieldDef;
430
+ value: unknown;
431
+ onChange: (value: unknown) => void;
432
+ disabled?: boolean;
433
+ secretOptions?: SecretOption[];
434
+ }
435
+
436
+ function ConfigField({ field, value, onChange, disabled, secretOptions }: ConfigFieldProps) {
437
+ const [touched, setTouched] = React.useState(false);
438
+
439
+ const portError = React.useMemo(() => {
440
+ if (field.key === 'port' && value !== undefined && value !== null && value !== '') {
441
+ const error = validatePort(value as string | number, field.label);
442
+ return error?.message ?? null;
443
+ }
444
+ return null;
445
+ }, [field.key, field.label, value]);
446
+
447
+ switch (field.type) {
448
+ case 'secret':
449
+ return (
450
+ <SecretReferenceInput
451
+ value={value != null ? String(value) : ''}
452
+ onChange={next => onChange(next)}
453
+ placeholder={field.placeholder}
454
+ disabled={disabled}
455
+ options={secretOptions ?? []}
456
+ />
457
+ );
458
+ case 'boolean':
459
+ return (
460
+ <div className="flex items-center gap-2">
461
+ <Switch checked={Boolean(value)} onCheckedChange={onChange} disabled={disabled} />
462
+ <span className="text-sm text-muted-foreground">{value ? 'Enabled' : 'Disabled'}</span>
463
+ </div>
464
+ );
465
+ case 'number':
466
+ return (
467
+ <div>
468
+ <Input
469
+ type="number"
470
+ value={value != null ? String(value) : ''}
471
+ onChange={e => onChange(e.target.value ? Number(e.target.value) : undefined)}
472
+ onBlur={() => setTouched(true)}
473
+ placeholder={field.placeholder}
474
+ disabled={disabled}
475
+ className={portError && touched ? 'border-destructive focus-visible:ring-destructive' : ''}
476
+ />
477
+ {field.key === 'port' && <FieldError error={portError} touched={touched} />}
478
+ </div>
479
+ );
480
+ case 'password':
481
+ return (
482
+ <Input
483
+ type="password"
484
+ value={String(value ?? '')}
485
+ onChange={e => onChange(e.target.value || undefined)}
486
+ placeholder={field.placeholder}
487
+ disabled={disabled}
488
+ />
489
+ );
490
+ default:
491
+ return (
492
+ <Input
493
+ type="text"
494
+ value={String(value ?? '')}
495
+ onChange={e => onChange(e.target.value || undefined)}
496
+ placeholder={field.placeholder}
497
+ disabled={disabled}
498
+ />
499
+ );
500
+ }
501
+ }
502
+
503
+ interface SecretReferenceInputProps {
504
+ value: string;
505
+ onChange: (value?: string) => void;
506
+ placeholder?: string;
507
+ disabled?: boolean;
508
+ options: SecretOption[];
509
+ }
510
+
511
+ function SecretReferenceInput({ value, onChange, placeholder, disabled, options }: SecretReferenceInputProps) {
512
+ const listId = React.useId();
513
+ const handleChange = (next: string) => {
514
+ onChange(next ? next : undefined);
515
+ };
516
+ return (
517
+ <div className="space-y-1">
518
+ <Input
519
+ type="text"
520
+ value={value}
521
+ onChange={e => handleChange(e.target.value)}
522
+ placeholder={placeholder}
523
+ disabled={disabled}
524
+ list={options.length > 0 ? listId : undefined}
525
+ />
526
+ {options.length > 0 && (
527
+ <>
528
+ <datalist id={listId}>
529
+ {options.map(option => (
530
+ <option key={option.code} value={option.code}>
531
+ {option.provider ?? 'INLINE'}
532
+ </option>
533
+ ))}
534
+ </datalist>
535
+ <p className="text-xs text-muted-foreground">
536
+ Choose an existing secret or type a new reference code.
537
+ </p>
538
+ </>
539
+ )}
540
+ </div>
541
+ );
542
+ }
543
+
544
+ /**
545
+ * Hook that returns connection type options for dropdowns.
546
+ * Uses backend-provided connection schemas.
547
+ */
548
+ export function useConnectionTypeOptions(): ReadonlyArray<{ value: string; label: string }> {
549
+ const { schemas } = useConnectionSchemas();
550
+ return React.useMemo(() => {
551
+ return schemas.map(s => ({ value: s.type, label: s.label }));
552
+ }, [schemas]);
553
+ }
554
+
555
+ export function createDefaultConnectionConfig(type: UIConnectionType): Record<string, unknown> {
556
+ if (type === CONNECTION_TYPE.HTTP) {
557
+ return { ...DEFAULT_HTTP_CONFIG };
558
+ }
559
+ return {};
560
+ }
561
+
562
+ export function normalizeConnectionConfig(
563
+ type: UIConnectionType,
564
+ config: Record<string, unknown> | string | null | undefined,
565
+ ): Record<string, unknown> {
566
+ if (config == null) {
567
+ return createDefaultConnectionConfig(type);
568
+ }
569
+ let obj: Record<string, unknown> | null = null;
570
+ if (typeof config === 'string') {
571
+ try {
572
+ const parsed = JSON.parse(config);
573
+ if (parsed && typeof parsed === 'object') {
574
+ obj = parsed as Record<string, unknown>;
575
+ }
576
+ } catch {
577
+ obj = null;
578
+ }
579
+ } else {
580
+ obj = config as Record<string, unknown>;
581
+ }
582
+ if (!obj) {
583
+ return createDefaultConnectionConfig(type);
584
+ }
585
+ if (type === CONNECTION_TYPE.HTTP) {
586
+ return { ...normalizeHttpConfig(obj) };
587
+ }
588
+ return obj;
589
+ }
@@ -0,0 +1,90 @@
1
+ import { useCallback, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Input,
5
+ Label,
6
+ } from '@vendure/dashboard';
7
+ import { Plus, X } from 'lucide-react';
8
+
9
+ interface HeadersEditorProps {
10
+ headers: Record<string, string>;
11
+ onChange: (headers: Record<string, string>) => void;
12
+ label?: string;
13
+ placeholder?: string;
14
+ }
15
+
16
+ export function HeadersEditor({ headers, onChange, label = 'Custom Headers', placeholder = 'Header value' }: HeadersEditorProps) {
17
+ const [newKey, setNewKey] = useState('');
18
+ const [newValue, setNewValue] = useState('');
19
+ const entries = Object.entries(headers);
20
+
21
+ const addHeader = useCallback(() => {
22
+ const key = newKey.trim();
23
+ if (!key) return;
24
+ onChange({ ...headers, [key]: newValue });
25
+ setNewKey('');
26
+ setNewValue('');
27
+ }, [newKey, newValue, headers, onChange]);
28
+
29
+ const removeHeader = useCallback((key: string) => {
30
+ const next = { ...headers };
31
+ delete next[key];
32
+ onChange(next);
33
+ }, [headers, onChange]);
34
+
35
+ const updateHeaderValue = useCallback((key: string, value: string) => {
36
+ onChange({ ...headers, [key]: value });
37
+ }, [headers, onChange]);
38
+
39
+ return (
40
+ <div className="space-y-3">
41
+ <Label>{label}</Label>
42
+ {entries.map(([key, value]) => (
43
+ <div key={key} className="flex items-center gap-2">
44
+ <Input value={key} readOnly className="flex-1 bg-muted" />
45
+ <Input
46
+ value={value}
47
+ onChange={e => updateHeaderValue(key, e.target.value)}
48
+ className="flex-1"
49
+ placeholder={placeholder}
50
+ />
51
+ <Button
52
+ variant="ghost"
53
+ size="icon"
54
+ onClick={() => removeHeader(key)}
55
+ aria-label={`Remove ${key} header`}
56
+ >
57
+ <X className="w-4 h-4" />
58
+ </Button>
59
+ </div>
60
+ ))}
61
+ <div className="flex items-center gap-2">
62
+ <Input
63
+ value={newKey}
64
+ onChange={e => setNewKey(e.target.value)}
65
+ placeholder="Header name"
66
+ className="flex-1"
67
+ />
68
+ <Input
69
+ value={newValue}
70
+ onChange={e => setNewValue(e.target.value)}
71
+ placeholder={placeholder}
72
+ className="flex-1"
73
+ onKeyDown={e => { if (e.key === 'Enter') addHeader(); }}
74
+ />
75
+ <Button
76
+ variant="outline"
77
+ size="icon"
78
+ onClick={addHeader}
79
+ disabled={!newKey.trim()}
80
+ aria-label="Add header"
81
+ >
82
+ <Plus className="w-4 h-4" />
83
+ </Button>
84
+ </div>
85
+ {entries.length === 0 && (
86
+ <p className="text-xs text-muted-foreground">No custom headers. Add headers using the fields above.</p>
87
+ )}
88
+ </div>
89
+ );
90
+ }