@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.
- package/CHANGELOG.md +10 -0
- package/dist/dashboard/components/common/ConnectionConfigEditor.tsx +589 -0
- package/dist/dashboard/components/common/HeadersEditor.tsx +90 -0
- package/dist/dashboard/components/common/ValidationFeedback.tsx +17 -0
- package/dist/dashboard/components/common/index.ts +10 -0
- package/dist/dashboard/components/pipelines/PipelineEditor.tsx +504 -0
- package/dist/dashboard/components/pipelines/PipelineExport.tsx +63 -0
- package/dist/dashboard/components/pipelines/PipelineImport.tsx +87 -0
- package/dist/dashboard/components/pipelines/ReactFlowPipelineEditor.tsx +539 -0
- package/dist/dashboard/components/pipelines/shared/NodePropertiesPanel.tsx +146 -0
- package/dist/dashboard/components/pipelines/shared/PipelineNode.tsx +155 -0
- package/dist/dashboard/components/pipelines/shared/PipelineSettingsPanel.tsx +392 -0
- package/dist/dashboard/components/pipelines/shared/StepListItem.tsx +144 -0
- package/dist/dashboard/components/pipelines/shared/index.ts +33 -0
- package/dist/dashboard/components/pipelines/shared/visual-node-config.ts +169 -0
- package/dist/dashboard/components/shared/LoadMoreButton.tsx +18 -0
- package/dist/dashboard/components/shared/entity-selector/EntitySelector.tsx +59 -0
- package/dist/dashboard/components/shared/entity-selector/index.ts +1 -0
- package/dist/dashboard/components/shared/error-boundary/ErrorBoundary.tsx +90 -0
- package/dist/dashboard/components/shared/error-boundary/index.ts +1 -0
- package/dist/dashboard/components/shared/feedback/EmptyState.tsx +36 -0
- package/dist/dashboard/components/shared/feedback/ErrorState.tsx +69 -0
- package/dist/dashboard/components/shared/feedback/LoadingState.tsx +104 -0
- package/dist/dashboard/components/shared/feedback/ValidationErrorDisplay.tsx +29 -0
- package/dist/dashboard/components/shared/feedback/index.ts +4 -0
- package/dist/dashboard/components/shared/file-dropzone/FileDropzone.tsx +167 -0
- package/dist/dashboard/components/shared/file-dropzone/index.ts +1 -0
- package/dist/dashboard/components/shared/filter-conditions-editor/FilterConditionsEditor.tsx +226 -0
- package/dist/dashboard/components/shared/filter-conditions-editor/index.ts +1 -0
- package/dist/dashboard/components/shared/index.ts +45 -0
- package/dist/dashboard/components/shared/schema-form/SchemaFormRenderer.tsx +248 -0
- package/dist/dashboard/components/shared/schema-form/fields/BooleanField.tsx +26 -0
- package/dist/dashboard/components/shared/schema-form/fields/FieldWrapper.tsx +28 -0
- package/dist/dashboard/components/shared/schema-form/fields/FileUploadField.tsx +171 -0
- package/dist/dashboard/components/shared/schema-form/fields/JsonField.tsx +132 -0
- package/dist/dashboard/components/shared/schema-form/fields/NumberField.tsx +33 -0
- package/dist/dashboard/components/shared/schema-form/fields/SelectField.tsx +70 -0
- package/dist/dashboard/components/shared/schema-form/fields/StringField.tsx +36 -0
- package/dist/dashboard/components/shared/schema-form/fields/TextareaField.tsx +31 -0
- package/dist/dashboard/components/shared/schema-form/fields/index.ts +23 -0
- package/dist/dashboard/components/shared/schema-form/index.ts +2 -0
- package/dist/dashboard/components/shared/schema-form/utils.ts +3 -0
- package/dist/dashboard/components/shared/selectable-card/SelectableCard.tsx +65 -0
- package/dist/dashboard/components/shared/selectable-card/index.ts +1 -0
- package/dist/dashboard/components/shared/stat-card/StatCard.tsx +121 -0
- package/dist/dashboard/components/shared/stat-card/index.ts +1 -0
- package/dist/dashboard/components/shared/step-config/AdapterRequiredWarning.tsx +25 -0
- package/dist/dashboard/components/shared/step-config/AdapterSelector.tsx +109 -0
- package/dist/dashboard/components/shared/step-config/AdvancedEditors.tsx +634 -0
- package/dist/dashboard/components/shared/step-config/EnrichConfigComponent.tsx +295 -0
- package/dist/dashboard/components/shared/step-config/ExtractTestResults.tsx +143 -0
- package/dist/dashboard/components/shared/step-config/GateConfigComponent.tsx +127 -0
- package/dist/dashboard/components/shared/step-config/LoadTestResults.tsx +104 -0
- package/dist/dashboard/components/shared/step-config/OperatorCard.tsx +266 -0
- package/dist/dashboard/components/shared/step-config/OperatorCheatSheetButton.tsx +54 -0
- package/dist/dashboard/components/shared/step-config/OperatorFieldInput.tsx +209 -0
- package/dist/dashboard/components/shared/step-config/RetrySettingsComponent.tsx +111 -0
- package/dist/dashboard/components/shared/step-config/RouteConfigComponent.tsx +125 -0
- package/dist/dashboard/components/shared/step-config/StepConfigPanel.tsx +564 -0
- package/dist/dashboard/components/shared/step-config/StepTester.tsx +165 -0
- package/dist/dashboard/components/shared/step-config/TestResultContainer.tsx +57 -0
- package/dist/dashboard/components/shared/step-config/TransformTestResults.tsx +130 -0
- package/dist/dashboard/components/shared/step-config/ValidateConfigComponent.tsx +334 -0
- package/dist/dashboard/components/shared/step-config/index.ts +29 -0
- package/dist/dashboard/components/shared/step-config/step-test-handlers.ts +297 -0
- package/dist/dashboard/components/shared/trigger-config/TriggerForm.tsx +478 -0
- package/dist/dashboard/components/shared/trigger-config/index.ts +1 -0
- package/dist/dashboard/components/shared/triggers-panel/TriggersPanel.tsx +281 -0
- package/dist/dashboard/components/shared/triggers-panel/index.ts +1 -0
- package/dist/dashboard/components/shared/wizard/ConfigurationNameCard.tsx +59 -0
- package/dist/dashboard/components/shared/wizard/SummaryCard.tsx +53 -0
- package/dist/dashboard/components/shared/wizard/WizardFooter.tsx +47 -0
- package/dist/dashboard/components/shared/wizard/WizardProgressBar.tsx +78 -0
- package/dist/dashboard/components/shared/wizard/index.ts +4 -0
- package/dist/dashboard/components/shared/wizard-trigger/TriggerSchemaFields.tsx +128 -0
- package/dist/dashboard/components/shared/wizard-trigger/TriggerSelector.tsx +33 -0
- package/dist/dashboard/components/shared/wizard-trigger/index.ts +2 -0
- package/dist/dashboard/components/templates/TemplateGallery.tsx +210 -0
- package/dist/dashboard/components/templates/TemplatePreview.tsx +214 -0
- package/dist/dashboard/components/templates/index.ts +4 -0
- package/dist/dashboard/components/wizards/export-wizard/DestinationStep.tsx +207 -0
- package/dist/dashboard/components/wizards/export-wizard/ExportWizard.tsx +221 -0
- package/dist/dashboard/components/wizards/export-wizard/FieldsStep.tsx +159 -0
- package/dist/dashboard/components/wizards/export-wizard/FormatStep.tsx +246 -0
- package/dist/dashboard/components/wizards/export-wizard/ReviewStep.tsx +231 -0
- package/dist/dashboard/components/wizards/export-wizard/SourceStep.tsx +154 -0
- package/dist/dashboard/components/wizards/export-wizard/TriggerStep.tsx +234 -0
- package/dist/dashboard/components/wizards/export-wizard/constants.ts +73 -0
- package/dist/dashboard/components/wizards/export-wizard/index.ts +2 -0
- package/dist/dashboard/components/wizards/export-wizard/types.ts +39 -0
- package/dist/dashboard/components/wizards/import-wizard/ImportWizard.tsx +350 -0
- package/dist/dashboard/components/wizards/import-wizard/MappingStep.tsx +286 -0
- package/dist/dashboard/components/wizards/import-wizard/PreviewStep.tsx +79 -0
- package/dist/dashboard/components/wizards/import-wizard/ReviewStep.tsx +266 -0
- package/dist/dashboard/components/wizards/import-wizard/SourceStep.tsx +537 -0
- package/dist/dashboard/components/wizards/import-wizard/StrategyStep.tsx +328 -0
- package/dist/dashboard/components/wizards/import-wizard/TargetStep.tsx +76 -0
- package/dist/dashboard/components/wizards/import-wizard/TemplateStep.tsx +116 -0
- package/dist/dashboard/components/wizards/import-wizard/TransformStep.tsx +666 -0
- package/dist/dashboard/components/wizards/import-wizard/TriggerStep.tsx +51 -0
- package/dist/dashboard/components/wizards/import-wizard/constants.ts +104 -0
- package/dist/dashboard/components/wizards/import-wizard/index.ts +3 -0
- package/dist/dashboard/components/wizards/import-wizard/types.ts +35 -0
- package/dist/dashboard/components/wizards/index.ts +7 -0
- package/dist/dashboard/components/wizards/shared/WizardStepContainer.tsx +27 -0
- package/dist/dashboard/components/wizards/shared/constants.ts +16 -0
- package/dist/dashboard/components/wizards/shared/index.ts +10 -0
- package/dist/dashboard/constants/colors.ts +25 -0
- package/dist/dashboard/constants/connection-defaults.ts +7 -0
- package/dist/dashboard/constants/connection-types.ts +1 -0
- package/dist/dashboard/constants/defaults.ts +18 -0
- package/dist/dashboard/constants/editor.ts +69 -0
- package/dist/dashboard/constants/enum-maps.ts +18 -0
- package/dist/dashboard/constants/fallbacks.ts +44 -0
- package/dist/dashboard/constants/file-format-registry.ts +206 -0
- package/dist/dashboard/constants/index.ts +24 -0
- package/dist/dashboard/constants/navigation.ts +29 -0
- package/dist/dashboard/constants/permissions.ts +41 -0
- package/dist/dashboard/constants/placeholders.ts +77 -0
- package/dist/dashboard/constants/routes.ts +12 -0
- package/dist/dashboard/constants/run-status.ts +1 -0
- package/dist/dashboard/constants/sentinel-values.ts +12 -0
- package/dist/dashboard/constants/step-configs.ts +9 -0
- package/dist/dashboard/constants/step-mappings.ts +170 -0
- package/dist/dashboard/constants/steps.ts +37 -0
- package/dist/dashboard/constants/toast-messages.ts +149 -0
- package/dist/dashboard/constants/triggers.ts +5 -0
- package/dist/dashboard/constants/ui-config.ts +139 -0
- package/dist/dashboard/constants/ui-dimensions.ts +145 -0
- package/dist/dashboard/constants/ui-states.ts +28 -0
- package/dist/dashboard/constants/ui-types.ts +85 -0
- package/dist/dashboard/constants/validation-patterns.ts +26 -0
- package/dist/dashboard/gql/gql.ts +370 -0
- package/dist/dashboard/gql/graphql.ts +10378 -0
- package/dist/dashboard/gql/index.ts +1 -0
- package/dist/dashboard/hooks/api/index.ts +115 -0
- package/dist/dashboard/hooks/api/mutation-helpers.ts +34 -0
- package/dist/dashboard/hooks/api/use-adapters.ts +92 -0
- package/dist/dashboard/hooks/api/use-config-options.ts +513 -0
- package/dist/dashboard/hooks/api/use-connections.ts +84 -0
- package/dist/dashboard/hooks/api/use-entity-field-schemas.ts +99 -0
- package/dist/dashboard/hooks/api/use-entity-loaders.ts +45 -0
- package/dist/dashboard/hooks/api/use-hooks.ts +68 -0
- package/dist/dashboard/hooks/api/use-logs.ts +102 -0
- package/dist/dashboard/hooks/api/use-pipeline-runs.ts +221 -0
- package/dist/dashboard/hooks/api/use-pipelines.ts +279 -0
- package/dist/dashboard/hooks/api/use-queues.ts +141 -0
- package/dist/dashboard/hooks/api/use-secrets.ts +75 -0
- package/dist/dashboard/hooks/api/use-settings.ts +55 -0
- package/dist/dashboard/hooks/api/use-step-tester.ts +79 -0
- package/dist/dashboard/hooks/index.ts +13 -0
- package/dist/dashboard/hooks/use-adapter-catalog.ts +253 -0
- package/dist/dashboard/hooks/use-export-templates.ts +80 -0
- package/dist/dashboard/hooks/use-import-templates.ts +139 -0
- package/dist/dashboard/hooks/use-load-more.ts +29 -0
- package/dist/dashboard/hooks/use-stable-keys.ts +54 -0
- package/dist/dashboard/hooks/use-trigger-types.ts +100 -0
- package/dist/dashboard/hooks/use-wizard-navigation.ts +128 -0
- package/dist/dashboard/index.tsx +55 -0
- package/dist/dashboard/routes/adapters/AdapterCard.tsx +102 -0
- package/dist/dashboard/routes/adapters/AdapterConstants.tsx +20 -0
- package/dist/dashboard/routes/adapters/AdapterDetail.tsx +208 -0
- package/dist/dashboard/routes/adapters/AdapterTypeSection.tsx +105 -0
- package/dist/dashboard/routes/adapters/AdaptersPage.tsx +276 -0
- package/dist/dashboard/routes/adapters/AdaptersTable.tsx +107 -0
- package/dist/dashboard/routes/adapters/index.ts +1 -0
- package/dist/dashboard/routes/connections/ConnectionDetail.tsx +218 -0
- package/dist/dashboard/routes/connections/ConnectionsList.tsx +34 -0
- package/dist/dashboard/routes/connections/index.ts +2 -0
- package/dist/dashboard/routes/hooks/Hooks.tsx +425 -0
- package/dist/dashboard/routes/hooks/hook-stages.ts +52 -0
- package/dist/dashboard/routes/hooks/index.ts +1 -0
- package/dist/dashboard/routes/index.ts +8 -0
- package/dist/dashboard/routes/logs/Logs.tsx +93 -0
- package/dist/dashboard/routes/logs/components/LogDetailDrawer.tsx +118 -0
- package/dist/dashboard/routes/logs/components/LogExplorerTab.tsx +367 -0
- package/dist/dashboard/routes/logs/components/LogLevelBadge.tsx +34 -0
- package/dist/dashboard/routes/logs/components/LogTableRow.tsx +70 -0
- package/dist/dashboard/routes/logs/components/LogsOverviewTab.tsx +178 -0
- package/dist/dashboard/routes/logs/components/RealtimeLogTab.tsx +122 -0
- package/dist/dashboard/routes/logs/index.ts +1 -0
- package/dist/dashboard/routes/pipelines/ErrorAuditList.tsx +39 -0
- package/dist/dashboard/routes/pipelines/ExportWizardPage.tsx +96 -0
- package/dist/dashboard/routes/pipelines/ImportWizardPage.tsx +104 -0
- package/dist/dashboard/routes/pipelines/PipelineDetail.tsx +211 -0
- package/dist/dashboard/routes/pipelines/PipelineRunsBlock.tsx +377 -0
- package/dist/dashboard/routes/pipelines/PipelinesList.tsx +87 -0
- package/dist/dashboard/routes/pipelines/RetryPatchHelper.tsx +51 -0
- package/dist/dashboard/routes/pipelines/RunDetailsPanel.tsx +238 -0
- package/dist/dashboard/routes/pipelines/RunErrorsList.tsx +116 -0
- package/dist/dashboard/routes/pipelines/StepCounters.tsx +24 -0
- package/dist/dashboard/routes/pipelines/StepSummaryTable.tsx +36 -0
- package/dist/dashboard/routes/pipelines/components/DryRunDialog.tsx +341 -0
- package/dist/dashboard/routes/pipelines/components/PipelineActionButtons.tsx +201 -0
- package/dist/dashboard/routes/pipelines/components/PipelineEditorToggle.tsx +116 -0
- package/dist/dashboard/routes/pipelines/components/PipelineFormFields.tsx +156 -0
- package/dist/dashboard/routes/pipelines/components/PipelineWebhookInfo.tsx +111 -0
- package/dist/dashboard/routes/pipelines/components/ReviewActionsPanel.tsx +342 -0
- package/dist/dashboard/routes/pipelines/components/ValidationPanel.tsx +121 -0
- package/dist/dashboard/routes/pipelines/components/VersionHistoryDialog.tsx +131 -0
- package/dist/dashboard/routes/pipelines/components/index.ts +25 -0
- package/dist/dashboard/routes/pipelines/hooks/index.ts +1 -0
- package/dist/dashboard/routes/pipelines/hooks/use-pipeline-validation.ts +114 -0
- package/dist/dashboard/routes/pipelines/index.ts +4 -0
- package/dist/dashboard/routes/pipelines/utils/index.ts +1 -0
- package/dist/dashboard/routes/pipelines/utils/pipeline-conversion.ts +261 -0
- package/dist/dashboard/routes/queues/ConsumersTable.tsx +134 -0
- package/dist/dashboard/routes/queues/DeadLettersTable.tsx +118 -0
- package/dist/dashboard/routes/queues/FailedRunsTable.tsx +74 -0
- package/dist/dashboard/routes/queues/QueuesPage.tsx +290 -0
- package/dist/dashboard/routes/queues/index.ts +1 -0
- package/dist/dashboard/routes/queues/types.ts +22 -0
- package/dist/dashboard/routes/secrets/SecretDetail.tsx +278 -0
- package/dist/dashboard/routes/secrets/SecretsList.tsx +34 -0
- package/dist/dashboard/routes/secrets/index.ts +2 -0
- package/dist/dashboard/routes/settings/Settings.tsx +343 -0
- package/dist/dashboard/routes/settings/index.ts +1 -0
- package/dist/dashboard/types/index.ts +89 -0
- package/dist/dashboard/types/pipeline.ts +51 -0
- package/dist/dashboard/types/ui-types.ts +400 -0
- package/dist/dashboard/types/wizard.ts +235 -0
- package/dist/dashboard/utils/adapter-grouping.ts +43 -0
- package/dist/dashboard/utils/column-analysis.ts +11 -0
- package/dist/dashboard/utils/field-preparation.ts +31 -0
- package/dist/dashboard/utils/form-validation.ts +373 -0
- package/dist/dashboard/utils/formatters.ts +92 -0
- package/dist/dashboard/utils/icon-resolver.ts +35 -0
- package/dist/dashboard/utils/index.ts +60 -0
- package/dist/dashboard/utils/query-key-factory.ts +54 -0
- package/dist/dashboard/utils/step-helpers.ts +32 -0
- package/dist/dashboard/utils/string-helpers.ts +4 -0
- package/dist/dashboard/utils/template-helpers.ts +26 -0
- package/dist/dashboard/utils/trigger-sync.ts +138 -0
- package/dist/dashboard/utils/wizard-to-pipeline.ts +569 -0
- 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
|
+
}
|