feat: add soap wizard foundation

This commit is contained in:
a.tolmachev
2026-04-07 17:09:01 +03:00
parent 78dc5ebc37
commit d8981f8a75
7 changed files with 475 additions and 9 deletions
+5 -5
View File
@@ -2,18 +2,18 @@
## Current
### `feat/staging-note-block-helper`
### `feat/soap-wizard-foundation`
Status: completed
DoD:
- authenticated staging notes block can be generated through a helper command
- authenticated pass docs point to the helper
- README and scripts docs expose the helper
- wizard step 1 exposes SOAP as a selectable protocol
- wizard step 3 supports WSDL/XSD upload and backend-driven SOAP service inspection
- SOAP service, port and operation selection is serialized into operation target payload
## Next
- `feat/live-authenticated-staging-entry`
- `feat/websocket-wizard-foundation`
## Backlog
+23 -3
View File
@@ -12,7 +12,7 @@
<!-- Protocol selection cards -->
<div class="protocol-grid">
<div class="protocol-card rest selected" role="radio" aria-checked="true" tabindex="0">
<div class="protocol-card rest selected" data-protocol="rest" role="radio" aria-checked="true" tabindex="0">
<div class="protocol-card-check">
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
</div>
@@ -34,7 +34,7 @@
</div>
</div>
<div class="protocol-card graphql" role="radio" aria-checked="false" tabindex="0">
<div class="protocol-card graphql" data-protocol="graphql" role="radio" aria-checked="false" tabindex="0">
<div class="protocol-card-check">
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
</div>
@@ -57,7 +57,7 @@
</div>
</div>
<div class="protocol-card grpc" role="radio" aria-checked="false" tabindex="0">
<div class="protocol-card grpc" data-protocol="grpc" role="radio" aria-checked="false" tabindex="0">
<div class="protocol-card-check">
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
</div>
@@ -76,6 +76,26 @@
</div>
</div>
<div class="protocol-card soap" data-protocol="soap" role="radio" aria-checked="false" tabindex="0">
<div class="protocol-card-check">
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
</div>
<div class="protocol-icon-wrap">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5h16v14H4z"/>
<path d="M8 5V3h8v2"/>
<path d="M7 10h10M7 14h6"/>
</svg>
</div>
<div class="protocol-card-name" data-i18n="wizard.step1.soap_name">SOAP</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.soap_tagline">Enterprise WSDL/XSD service contracts normalized into MCP tools with structured XML mapping.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">WSDL</span>
<span class="protocol-tag">XML</span>
<span class="protocol-tag" data-i18n="wizard.step1.unary">Unary</span>
</div>
</div>
</div><!-- /protocol-grid -->
+101
View File
@@ -0,0 +1,101 @@
<div id="step-panel-3-soap" class="step-pane">
<div class="step-panel-header">
<div class="step-panel-eyebrow">
<div class="step-panel-eyebrow-line"></div>
<span data-i18n-html="wizard.step_of">Step 3 of 5</span>
</div>
<h1 class="step-panel-title" data-i18n="wizard.step3.soap.title">WSDL and SOAP binding</h1>
<p class="step-panel-subtitle" data-i18n="wizard.step3.soap.subtitle">Upload the WSDL/XSD bundle, inspect services and choose the exact service, port and operation that Crank should expose as an MCP tool.</p>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M3.5 2.5h6l3 3v8a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1z"/>
<path d="M9.5 2.5v3h3"/>
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step3.soap.wsdl_title">Contract files</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.soap.wsdl_subtitle">WSDL is required. Supporting XSD files are optional and can be uploaded before discovery.</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-row">
<input id="wizard-soap-wsdl-input" type="file" accept=".wsdl,.xml,text/xml,application/wsdl+xml" style="display:none">
<button id="wizard-select-soap-wsdl" class="btn-ghost-sm" type="button" data-i18n="wizard.step3.soap.choose_wsdl">Choose WSDL</button>
<span id="wizard-soap-wsdl-name" style="color: var(--text-muted); font-size: 12px;" data-i18n="wizard.step3.soap.no_wsdl">No WSDL selected</span>
</div>
<div class="form-row">
<input id="wizard-soap-xsd-input" type="file" accept=".xsd,.xml,text/xml,application/xml" style="display:none">
<button id="wizard-select-soap-xsd" class="btn-ghost-sm" type="button" data-i18n="wizard.step3.soap.choose_xsd">Choose XSD</button>
<span id="wizard-soap-xsd-name" style="color: var(--text-muted); font-size: 12px;" data-i18n="wizard.step3.soap.no_xsd">No XSD selected</span>
</div>
<div class="form-row">
<button id="wizard-upload-soap-contracts" class="btn-primary-sm" type="button" data-i18n="wizard.step3.soap.upload_discover">Upload and inspect services</button>
</div>
<div class="form-hint" id="wizard-soap-services-summary"></div>
<div id="wizard-soap-services-list" style="display:grid; gap: 10px;"></div>
</div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M2.5 5.5h11M5.5 2.5v3M10.5 2.5v3M4 8.5h8v5H4z"/>
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.step3.soap.binding_title">Selected binding</div>
<div class="config-card-subtitle" data-i18n="wizard.step3.soap.binding_subtitle">The chosen service, port and operation become the target binding for this operation draft.</div>
</div>
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
</div>
<div class="config-card-body">
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.service_name">Service</label>
<input id="soap-service-name" class="form-input input-mono" type="text" placeholder="LeadService">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.port_name">Port</label>
<input id="soap-port-name" class="form-input input-mono" type="text" placeholder="LeadPort">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.operation_name">Operation</label>
<input id="soap-operation-name" class="form-input input-mono" type="text" placeholder="CreateLead">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.version">SOAP version</label>
<select id="soap-version" class="form-select">
<option value="soap_11">SOAP 1.1</option>
<option value="soap_12">SOAP 1.2</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.action">SOAPAction</label>
<input id="soap-action" class="form-input input-mono" type="text" placeholder="urn:createLead">
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.binding_style">Binding style</label>
<select id="soap-binding-style" class="form-select">
<option value="document_literal" data-i18n="wizard.step3.soap.binding.document_literal">Document literal</option>
<option value="rpc_literal" data-i18n="wizard.step3.soap.binding.rpc_literal">RPC literal</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step3.soap.endpoint_override">Endpoint override</label>
<input id="soap-endpoint-override" class="form-input input-mono" type="text" placeholder="https://soap.example.com/lead">
<div class="form-hint" data-i18n="wizard.step3.soap.endpoint_override_hint">Defaults to the upstream base URL from step 2. Override only when the binding address differs from the selected upstream.</div>
</div>
</div>
</div>
</div>
+20
View File
@@ -257,12 +257,32 @@
fileName
);
},
uploadWsdlFile: function(workspaceId, operationId, content, fileName) {
return postBytes(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/descriptors/wsdl',
content,
fileName
);
},
uploadXsdFile: function(workspaceId, operationId, content, fileName) {
return postBytes(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/descriptors/xsd',
content,
fileName
);
},
listGrpcServices: function(workspaceId, operationId, version) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/grpc/services'
+ query({ version: version })
);
},
listSoapServices: function(workspaceId, operationId, version) {
return get(
'/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/soap/services'
+ query({ version: version })
);
},
generateDraft: function(workspaceId, operationId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/operations/' + encodeURIComponent(operationId) + '/drafts/generate', payload || {});
},
+74
View File
@@ -605,6 +605,7 @@ var TRANSLATIONS = {
'wizard.step3.rest.label': 'Request config',
'wizard.step3.graphql.label': 'GQL operation',
'wizard.step3.grpc.label': 'RPC method',
'wizard.step3.soap.label': 'WSDL binding',
'wizard.step3.rest.title': 'HTTP method & format',
'wizard.step3.rest.subtitle': 'Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.',
'wizard.step3.rest.method_title': 'HTTP method',
@@ -666,6 +667,27 @@ var TRANSLATIONS = {
'wizard.step3.grpc.route_label': 'gRPC route',
'wizard.step3.grpc.request_label': 'Request',
'wizard.step3.grpc.response_label': 'Response',
'wizard.step3.soap.title': 'WSDL and SOAP binding',
'wizard.step3.soap.subtitle': 'Upload the WSDL/XSD bundle, inspect services and choose the exact service, port and operation that Crank should expose as an MCP tool.',
'wizard.step3.soap.wsdl_title': 'Contract files',
'wizard.step3.soap.wsdl_subtitle': 'WSDL is required. Supporting XSD files are optional and can be uploaded before discovery.',
'wizard.step3.soap.choose_wsdl': 'Choose WSDL',
'wizard.step3.soap.choose_xsd': 'Choose XSD',
'wizard.step3.soap.no_wsdl': 'No WSDL selected',
'wizard.step3.soap.no_xsd': 'No XSD selected',
'wizard.step3.soap.upload_discover': 'Upload and inspect services',
'wizard.step3.soap.binding_title': 'Selected binding',
'wizard.step3.soap.binding_subtitle': 'The chosen service, port and operation become the target binding for this operation draft.',
'wizard.step3.soap.service_name': 'Service',
'wizard.step3.soap.port_name': 'Port',
'wizard.step3.soap.operation_name': 'Operation',
'wizard.step3.soap.version': 'SOAP version',
'wizard.step3.soap.action': 'SOAPAction',
'wizard.step3.soap.binding_style': 'Binding style',
'wizard.step3.soap.binding.document_literal': 'Document literal',
'wizard.step3.soap.binding.rpc_literal': 'RPC literal',
'wizard.step3.soap.endpoint_override': 'Endpoint override',
'wizard.step3.soap.endpoint_override_hint': 'Defaults to the upstream base URL from step 2. Override only when the binding address differs from the selected upstream.',
'wizard.step4.title': 'Tool config',
'wizard.step4.subtitle': 'Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.',
'wizard.step4.identity_title': 'Tool identity',
@@ -766,6 +788,21 @@ var TRANSLATIONS = {
'wizard.busy.import_yaml': 'Importing YAML…',
'wizard.busy.publish': 'Publishing operation…',
'wizard.busy.descriptor': 'Uploading descriptor set…',
'wizard.busy.wsdl': 'Inspecting WSDL…',
'wizard.step3.soap.wsdl_selected': 'WSDL selected',
'wizard.step3.soap.wsdl_selected_body': 'The WSDL file is staged and will be uploaded with the current draft.',
'wizard.step3.soap.xsd_selected': 'XSD selected',
'wizard.step3.soap.xsd_selected_body': 'The XSD file is staged and will be uploaded with the current draft.',
'wizard.step3.soap.wsdl_uploaded': 'WSDL uploaded',
'wizard.step3.soap.xsd_uploaded': 'XSD uploaded',
'wizard.step3.soap.services_discovered': '{services} service(s), {ports} port(s) and {operations} operation(s) discovered from the uploaded contract.',
'wizard.step3.soap.services_none': 'No SOAP services discovered from the uploaded WSDL yet.',
'wizard.step3.soap.applied': 'SOAP binding applied',
'wizard.step3.soap.applied_body': 'The selected service, port and operation were applied to the current draft.',
'wizard.step3.soap.no_wsdl_error': 'WSDL is required',
'wizard.step3.soap.no_wsdl_error_body': 'Choose a WSDL file or reuse an already uploaded draft contract before inspection.',
'wizard.step3.soap.discovered': 'SOAP services discovered',
'wizard.step3.soap.discovered_body': 'The uploaded WSDL was inspected through the backend and the available bindings are ready to apply.',
'wizard.sample.input_saved': 'Input sample saved',
'wizard.sample.input_saved_body': 'The input sample is now stored against the current draft version.',
'wizard.sample.output_saved': 'Output sample saved',
@@ -1532,6 +1569,7 @@ var TRANSLATIONS = {
'wizard.step3.rest.label': 'Конфиг запроса',
'wizard.step3.graphql.label': 'GQL операция',
'wizard.step3.grpc.label': 'RPC метод',
'wizard.step3.soap.label': 'WSDL binding',
'wizard.step3.rest.title': 'HTTP метод и формат',
'wizard.step3.rest.subtitle': 'Выберите HTTP-метод, который отправляет эта операция, и настройте content negotiation headers. Crank сериализует аргументы MCP-инструмента в нужный формат request body.',
'wizard.step3.rest.method_title': 'HTTP метод',
@@ -1593,6 +1631,27 @@ var TRANSLATIONS = {
'wizard.step3.grpc.route_label': 'gRPC route',
'wizard.step3.grpc.request_label': 'Запрос',
'wizard.step3.grpc.response_label': 'Ответ',
'wizard.step3.soap.title': 'WSDL и SOAP binding',
'wizard.step3.soap.subtitle': 'Загрузите WSDL/XSD bundle, получите список сервисов и выберите точный service, port и operation, который Crank должен опубликовать как MCP tool.',
'wizard.step3.soap.wsdl_title': 'Файлы контракта',
'wizard.step3.soap.wsdl_subtitle': 'WSDL обязателен. Supporting XSD files можно загрузить дополнительно до discovery.',
'wizard.step3.soap.choose_wsdl': 'Выбрать WSDL',
'wizard.step3.soap.choose_xsd': 'Выбрать XSD',
'wizard.step3.soap.no_wsdl': 'WSDL не выбран',
'wizard.step3.soap.no_xsd': 'XSD не выбран',
'wizard.step3.soap.upload_discover': 'Загрузить и получить сервисы',
'wizard.step3.soap.binding_title': 'Выбранный binding',
'wizard.step3.soap.binding_subtitle': 'Выбранные service, port и operation станут target binding для текущего draft.',
'wizard.step3.soap.service_name': 'Service',
'wizard.step3.soap.port_name': 'Port',
'wizard.step3.soap.operation_name': 'Operation',
'wizard.step3.soap.version': 'SOAP версия',
'wizard.step3.soap.action': 'SOAPAction',
'wizard.step3.soap.binding_style': 'Binding style',
'wizard.step3.soap.binding.document_literal': 'Document literal',
'wizard.step3.soap.binding.rpc_literal': 'RPC literal',
'wizard.step3.soap.endpoint_override': 'Endpoint override',
'wizard.step3.soap.endpoint_override_hint': 'По умолчанию берется upstream base URL из шага 2. Переопределяйте только если binding address отличается от выбранного upstream.',
'wizard.step4.title': 'Конфиг инструмента',
'wizard.step4.subtitle': 'Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.',
'wizard.step4.identity_title': 'Идентичность инструмента',
@@ -1693,6 +1752,21 @@ var TRANSLATIONS = {
'wizard.busy.import_yaml': 'Импорт YAML…',
'wizard.busy.publish': 'Публикация операции…',
'wizard.busy.descriptor': 'Загрузка descriptor set…',
'wizard.busy.wsdl': 'Inspection WSDL…',
'wizard.step3.soap.wsdl_selected': 'WSDL выбран',
'wizard.step3.soap.wsdl_selected_body': 'WSDL staged и будет загружен вместе с текущим draft.',
'wizard.step3.soap.xsd_selected': 'XSD выбран',
'wizard.step3.soap.xsd_selected_body': 'XSD staged и будет загружен вместе с текущим draft.',
'wizard.step3.soap.wsdl_uploaded': 'WSDL загружен',
'wizard.step3.soap.xsd_uploaded': 'XSD загружен',
'wizard.step3.soap.services_discovered': 'Из загруженного контракта найдено {services} service(s), {ports} port(s) и {operations} operation(s).',
'wizard.step3.soap.services_none': 'Из загруженного WSDL пока не найдено ни одного SOAP service.',
'wizard.step3.soap.applied': 'SOAP binding применен',
'wizard.step3.soap.applied_body': 'Выбранные service, port и operation применены к текущему draft.',
'wizard.step3.soap.no_wsdl_error': 'Нужен WSDL',
'wizard.step3.soap.no_wsdl_error_body': 'Выберите WSDL или используйте уже загруженный контракт текущего draft перед inspection.',
'wizard.step3.soap.discovered': 'SOAP services получены',
'wizard.step3.soap.discovered_body': 'Загруженный WSDL был inspected через backend, и доступные bindings готовы к применению.',
'wizard.sample.input_saved': 'Входной sample сохранен',
'wizard.sample.input_saved_body': 'Входной sample теперь сохранен в текущей версии черновика.',
'wizard.sample.output_saved': 'Выходной sample сохранен',
+243 -1
View File
@@ -8,8 +8,11 @@ var wizardCurrentOperation = null;
var wizardCurrentVersion = null;
var wizardProtoUpload = null;
var wizardDescriptorSetUpload = null;
var wizardSoapWsdlUpload = null;
var wizardSoapXsdUpload = null;
var wizardTestResponsePreview = null;
var grpcDescriptorServices = [];
var soapServiceCatalog = [];
var wizardProtocolCapabilities = null;
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
@@ -91,6 +94,7 @@ function loadStep(n, callback) {
function step3PanelId() {
if (wizardProtocol === 'graphql') return 'step-panel-3-graphql';
if (wizardProtocol === 'grpc') return 'step-panel-3-grpc';
if (wizardProtocol === 'soap') return 'step-panel-3-soap';
return 'step-panel-3-rest';
}
@@ -99,6 +103,7 @@ var STEP3_LABELS = {
rest: tKey('wizard.step3.rest.label'),
graphql: tKey('wizard.step3.graphql.label'),
grpc: tKey('wizard.step3.grpc.label'),
soap: tKey('wizard.step3.soap.label'),
};
var CHECK_SVG = '<svg width="10" height="10"><use href="' + (window.APP_BASE||'') + 'icons/general/check.svg#icon"/></svg>';
@@ -212,6 +217,8 @@ function bindProtocolCards() {
? 'graphql'
: card.classList.contains('grpc')
? 'grpc'
: card.classList.contains('soap')
? 'soap'
: 'rest');
wizardProtocol = proto;
@@ -1586,6 +1593,28 @@ function buildTarget() {
};
}
if (wizardProtocol === 'soap') {
var endpointOverride = textValue('soap-endpoint-override') || upstream.url;
var existingWsdlRef = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'soap'
? wizardCurrentVersion.target.wsdl_ref
: 'sample_wsdl_pending';
return {
kind: 'soap',
wsdl_ref: existingWsdlRef,
service_name: textValue('soap-service-name') || 'Service',
port_name: textValue('soap-port-name') || 'Port',
operation_name: textValue('soap-operation-name') || 'Operation',
endpoint_override: endpointOverride || null,
soap_version: textValue('soap-version') || 'soap_11',
soap_action: textValue('soap-action') || null,
binding_style: textValue('soap-binding-style') || 'document_literal',
headers: [],
fault_contract: null,
metadata: { input_part_names: [], output_part_names: [], namespaces: [] },
};
}
var route = textValue('grpc-manual-route') || (document.getElementById('grpc-path-value') ? document.getElementById('grpc-path-value').textContent.trim() : '');
var descriptorRef = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc'
? wizardCurrentVersion.target.descriptor_ref
@@ -1963,7 +1992,15 @@ function selectProtocol(protocol) {
wizardProtocol = protocol || 'rest';
document.querySelectorAll('.protocol-card').forEach(function(card) {
var proto = card.dataset.protocol
|| (card.classList.contains('rest') ? 'rest' : card.classList.contains('graphql') ? 'graphql' : 'grpc');
|| (card.classList.contains('rest')
? 'rest'
: card.classList.contains('graphql')
? 'graphql'
: card.classList.contains('grpc')
? 'grpc'
: card.classList.contains('soap')
? 'soap'
: 'rest');
var selected = proto === wizardProtocol;
card.classList.toggle('selected', selected);
card.setAttribute('aria-checked', selected ? 'true' : 'false');
@@ -2091,6 +2128,19 @@ function prefillWizardFromEdit(detail, versionDocument) {
setValue('grpc-manual-res-type', '');
grpcManualRouteInput(route);
grpcManualTypeInput();
} else if (target.kind === 'soap') {
prefillUpstream(
target.endpoint_override || '',
{},
versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null
);
setValue('soap-service-name', target.service_name || '');
setValue('soap-port-name', target.port_name || '');
setValue('soap-operation-name', target.operation_name || '');
setValue('soap-endpoint-override', target.endpoint_override || '');
setValue('soap-action', target.soap_action || '');
setValue('soap-version', target.soap_version || 'soap_11');
setValue('soap-binding-style', target.binding_style || 'document_literal');
}
setValue('tool-name', detail.name);
@@ -2123,6 +2173,15 @@ function bindWizardLiveActions() {
if (input) input.click();
});
bindLiveAction('wizard-upload-descriptor-set', tKey('wizard.busy.descriptor'), uploadDescriptorSetAndDiscover);
bindClick('wizard-select-soap-wsdl', function() {
var input = document.getElementById('wizard-soap-wsdl-input');
if (input) input.click();
});
bindClick('wizard-select-soap-xsd', function() {
var input = document.getElementById('wizard-soap-xsd-input');
if (input) input.click();
});
bindLiveAction('wizard-upload-soap-contracts', tKey('wizard.busy.wsdl'), uploadSoapArtifactsAndInspect);
bindClick('wizard-import-yaml-file-trigger', function() {
var input = document.getElementById('wizard-import-yaml-file');
if (input) input.click();
@@ -2130,6 +2189,10 @@ function bindWizardLiveActions() {
var descriptorInput = document.getElementById('wizard-descriptor-set-input');
if (descriptorInput) descriptorInput.addEventListener('change', handleDescriptorSetSelection);
var soapWsdlInput = document.getElementById('wizard-soap-wsdl-input');
if (soapWsdlInput) soapWsdlInput.addEventListener('change', handleSoapWsdlSelection);
var soapXsdInput = document.getElementById('wizard-soap-xsd-input');
if (soapXsdInput) soapXsdInput.addEventListener('change', handleSoapXsdSelection);
var yamlInput = document.getElementById('wizard-import-yaml-file');
if (yamlInput) yamlInput.addEventListener('change', handleImportYamlFileSelection);
@@ -2290,6 +2353,44 @@ async function uploadPendingGrpcArtifacts(payload) {
);
}
async function uploadPendingSoapArtifacts(payload) {
if (wizardProtocol !== 'soap' || !wizardEditId) return;
var wsdlResponse = null;
if (wizardSoapWsdlUpload) {
wsdlResponse = await window.CrankApi.uploadWsdlFile(
wizardWorkspaceId,
wizardEditId,
wizardSoapWsdlUpload.bytes,
wizardSoapWsdlUpload.fileName
);
wizardSoapWsdlUpload = null;
var wsdlName = document.getElementById('wizard-soap-wsdl-name');
if (wsdlName) wsdlName.textContent = tKey('wizard.step3.soap.wsdl_uploaded');
}
if (wizardSoapXsdUpload) {
await window.CrankApi.uploadXsdFile(
wizardWorkspaceId,
wizardEditId,
wizardSoapXsdUpload.bytes,
wizardSoapXsdUpload.fileName
);
wizardSoapXsdUpload = null;
var xsdName = document.getElementById('wizard-soap-xsd-name');
if (xsdName) xsdName.textContent = tKey('wizard.step3.soap.xsd_uploaded');
}
if (!wsdlResponse) return;
payload.target.wsdl_ref = wsdlResponse.descriptor_id;
await window.CrankApi.updateOperation(
wizardWorkspaceId,
wizardEditId,
updateOperationPayload(payload)
);
}
async function persistCurrentDraft(stayOnPage) {
if (!wizardWorkspaceId) {
throw new Error(tKey('wizard.error.no_workspace'));
@@ -2322,6 +2423,7 @@ async function persistCurrentDraft(stayOnPage) {
}
await uploadPendingGrpcArtifacts(payload);
await uploadPendingSoapArtifacts(payload);
await refreshCurrentOperationState();
if (stayOnPage) {
@@ -2487,6 +2589,38 @@ function handleDescriptorSetSelection(event) {
reader.readAsArrayBuffer(file);
}
function handleSoapWsdlSelection(event) {
var file = event.target.files && event.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(loadEvent) {
wizardSoapWsdlUpload = {
fileName: file.name,
bytes: new Uint8Array(loadEvent.target.result),
};
var fileNameEl = document.getElementById('wizard-soap-wsdl-name');
if (fileNameEl) fileNameEl.textContent = file.name;
showWizardLiveStatus(tKey('wizard.step3.soap.wsdl_selected'), tKey('wizard.step3.soap.wsdl_selected_body'));
};
reader.readAsArrayBuffer(file);
}
function handleSoapXsdSelection(event) {
var file = event.target.files && event.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(loadEvent) {
wizardSoapXsdUpload = {
fileName: file.name,
bytes: new Uint8Array(loadEvent.target.result),
};
var fileNameEl = document.getElementById('wizard-soap-xsd-name');
if (fileNameEl) fileNameEl.textContent = file.name;
showWizardLiveStatus(tKey('wizard.step3.soap.xsd_selected'), tKey('wizard.step3.soap.xsd_selected_body'));
};
reader.readAsArrayBuffer(file);
}
function renderDescriptorServiceList(services) {
grpcDescriptorServices = services || [];
var summaryEl = document.getElementById('wizard-descriptor-services-summary');
@@ -2574,3 +2708,111 @@ async function uploadDescriptorSetAndDiscover() {
renderDescriptorServiceList(services.services || []);
showWizardLiveStatus(tKey('wizard.descriptor.discovered'), tKey('wizard.descriptor.discovered_body'));
}
function renderSoapServiceCatalog(services) {
soapServiceCatalog = services || [];
var summaryEl = document.getElementById('wizard-soap-services-summary');
var listEl = document.getElementById('wizard-soap-services-list');
if (!summaryEl || !listEl) return;
var portCount = 0;
var operationCount = 0;
soapServiceCatalog.forEach(function(service) {
portCount += (service.ports || []).length;
(service.ports || []).forEach(function(port) {
operationCount += (port.operations || []).length;
});
});
summaryEl.textContent = soapServiceCatalog.length
? tfKey('wizard.step3.soap.services_discovered', {
services: soapServiceCatalog.length,
ports: portCount,
operations: operationCount
})
: tKey('wizard.step3.soap.services_none');
listEl.innerHTML = '';
soapServiceCatalog.forEach(function(service, serviceIndex) {
var group = document.createElement('div');
group.className = 'config-card';
group.style.marginBottom = '0';
var body = document.createElement('div');
body.className = 'config-card-body';
body.style.padding = '16px';
body.style.gap = '10px';
var title = document.createElement('div');
title.className = 'config-card-title';
title.textContent = service.service_name;
body.appendChild(title);
(service.ports || []).forEach(function(port, portIndex) {
var portLabel = document.createElement('div');
portLabel.className = 'form-hint';
portLabel.textContent = port.port_name + ' · ' + port.soap_version;
body.appendChild(portLabel);
(port.operations || []).forEach(function(operation, operationIndex) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'btn-ghost-sm';
button.style.width = '100%';
button.style.justifyContent = 'space-between';
button.textContent = operation.operation_name;
button.addEventListener('click', function() {
applyDiscoveredSoapOperation(serviceIndex, portIndex, operationIndex);
});
body.appendChild(button);
});
});
group.appendChild(body);
listEl.appendChild(group);
});
}
function applyDiscoveredSoapOperation(serviceIndex, portIndex, operationIndex) {
var service = soapServiceCatalog[serviceIndex];
var port = service && service.ports ? service.ports[portIndex] : null;
var operation = port && port.operations ? port.operations[operationIndex] : null;
if (!service || !port || !operation) return;
setValue('soap-service-name', service.service_name);
setValue('soap-port-name', port.port_name);
setValue('soap-operation-name', operation.operation_name);
setValue('soap-version', port.soap_version || 'soap_11');
setValue('soap-action', operation.soap_action || '');
setValue('soap-binding-style', operation.binding_style || 'document_literal');
if (!textValue('soap-endpoint-override') && port.endpoint) {
setValue('soap-endpoint-override', port.endpoint);
}
showWizardLiveStatus(tKey('wizard.step3.soap.applied'), tKey('wizard.step3.soap.applied_body'));
}
async function uploadSoapArtifactsAndInspect() {
var hasSavedWsdl = !!(
wizardCurrentVersion
&& wizardCurrentVersion.target
&& wizardCurrentVersion.target.kind === 'soap'
&& wizardCurrentVersion.target.wsdl_ref
&& wizardCurrentVersion.target.wsdl_ref !== 'sample_wsdl_pending'
);
if (!wizardSoapWsdlUpload && !hasSavedWsdl) {
showWizardLiveStatus(tKey('wizard.step3.soap.no_wsdl_error'), tKey('wizard.step3.soap.no_wsdl_error_body'), true);
return;
}
await persistCurrentDraft(true);
var services = await window.CrankApi.listSoapServices(
wizardWorkspaceId,
wizardEditId,
currentDraftVersion()
);
renderSoapServiceCatalog(services.services || []);
showWizardLiveStatus(tKey('wizard.step3.soap.discovered'), tKey('wizard.step3.soap.discovered_body'));
}
+9
View File
@@ -118,6 +118,15 @@ Foundation routes:
- `POST /operations/{operation_id}/descriptors/xsd`
- `GET /operations/{operation_id}/soap/services`
Current UI foundation:
- protocol selection card on wizard step 1;
- dedicated SOAP step 3 panel;
- WSDL upload;
- optional XSD upload;
- backend-driven inspection of `service -> port -> operation`;
- applying selected binding metadata into the operation draft target.
## 8. Поведение runtime
При выполнении SOAP operation runtime должен: