From 37b9ab4fbe5a38a06b27e864e86e8a45318ee19b Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sat, 2 May 2026 19:57:27 +0000 Subject: [PATCH] ui: extract wizard model helpers --- apps/ui/js/wizard-model.js | 528 +++++++++++++++++++++++++++++++++++ apps/ui/js/wizard.js | 544 ------------------------------------- apps/ui/scripts/build.js | 1 + 3 files changed, 529 insertions(+), 544 deletions(-) create mode 100644 apps/ui/js/wizard-model.js diff --git a/apps/ui/js/wizard-model.js b/apps/ui/js/wizard-model.js new file mode 100644 index 0000000..fd124ba --- /dev/null +++ b/apps/ui/js/wizard-model.js @@ -0,0 +1,528 @@ +function inferSchemaKind(typeName) { + if (typeName === 'object') return 'object'; + if (typeName === 'array') return 'array'; + if (typeName === 'string') return 'string'; + if (typeName === 'integer') return 'integer'; + if (typeName === 'number') return 'number'; + if (typeName === 'boolean') return 'boolean'; + if (typeName === 'null') return 'null'; + return 'string'; +} + +function convertJsonSchemaToCrankSchema(node, requiredNames) { + var schemaNode = node || {}; + var typeName = Array.isArray(schemaNode.type) + ? schemaNode.type.find(function(value) { return value !== 'null'; }) || 'string' + : schemaNode.type || (schemaNode.properties ? 'object' : 'string'); + var nullable = Array.isArray(schemaNode.type) && schemaNode.type.indexOf('null') >= 0; + var kind = schemaNode.enum ? 'enum' : inferSchemaKind(typeName); + var schema = { + type: kind, + description: schemaNode.description || null, + required: false, + nullable: nullable, + default_value: Object.prototype.hasOwnProperty.call(schemaNode, 'default') ? schemaNode.default : null, + fields: {}, + items: null, + enum_values: schemaNode.enum || [], + variants: [], + }; + + if (kind === 'object') { + var requiredSet = new Set(schemaNode.required || requiredNames || []); + Object.keys(schemaNode.properties || {}).forEach(function(fieldName) { + var field = convertJsonSchemaToCrankSchema(schemaNode.properties[fieldName], []); + field.required = requiredSet.has(fieldName); + schema.fields[fieldName] = field; + }); + } + + if (kind === 'array' && schemaNode.items) { + schema.items = convertJsonSchemaToCrankSchema(schemaNode.items, []); + } + + return schema; +} + +function mappingRootForProtocol(protocol) { + if (protocol === 'graphql') return '$.request.variables'; + if (protocol === 'grpc') return '$.request.message'; + if (protocol === 'soap') return '$.request.body'; + if (protocol === 'websocket') return '$.request.body'; + return '$.request.body'; +} + +function normalizeInputSource(path) { + if (!path) return '$.input'; + if (path === '$') return '$.input'; + if (path.indexOf('$.input') === 0) return path; + return '$.input.' + path.replace(/^\$\./, ''); +} + +function normalizeOutputSource(path) { + if (!path) return '$.response.body'; + if (path.indexOf('$.') === 0) return path; + return '$.' + path.replace(/^\$\./, ''); +} + +function buildMappingSet(rawValue, mode) { + var value = rawValue || {}; + var root = mappingRootForProtocol(wizardProtocol); + var rules = []; + + Object.keys(value).forEach(function(target) { + var source = value[target]; + if (mode === 'input') { + rules.push({ + source: normalizeInputSource(source), + target: root + '.' + target, + }); + return; + } + + rules.push({ + source: normalizeOutputSource(source), + target: '$.output.' + target, + }); + }); + + return { rules: rules }; +} + +function parseExecutionConfig(text) { + var value = parseStructuredText(text); + var retry = value.retry || {}; + var headers = value.headers && typeof value.headers === 'object' ? value.headers : {}; + var selectedUpstream = currentUpstream(); + if (!selectedUpstream && wizardMode !== 'edit') { + throw new Error(tKey('wizard.error.save_upstream_first')); + } + var authProfileRef = selectedUpstream && selectedUpstream.authProfileId + ? selectedUpstream.authProfileId + : (value.auth && value.auth.profile ? value.auth.profile : null); + var config = { + timeout_ms: Number(value.timeout_ms || 10000), + retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null, + auth_profile_ref: authProfileRef, + headers: headers, + protocol_options: null, + }; + + if (wizardProtocol === 'grpc') { + var useTls = false; + if (value.protocol_options && value.protocol_options.grpc) { + useTls = !!value.protocol_options.grpc.use_tls; + } else if (value.tls) { + useTls = !!value.tls.verify; + } + config.protocol_options = { grpc: { use_tls: useTls } }; + } else if (wizardProtocol === 'websocket') { + var websocketProtocolOptions = value.protocol_options && value.protocol_options.websocket + ? value.protocol_options.websocket + : {}; + var heartbeatInterval = websocketProtocolOptions.heartbeat_interval_ms != null + ? Number(websocketProtocolOptions.heartbeat_interval_ms) + : numericValueOrNull('websocket-heartbeat-interval-ms'); + var reconnectAttempts = websocketProtocolOptions.reconnect_max_attempts != null + ? Number(websocketProtocolOptions.reconnect_max_attempts) + : numericValueOrNull('websocket-reconnect-max-attempts'); + var reconnectBackoff = websocketProtocolOptions.reconnect_backoff_ms != null + ? Number(websocketProtocolOptions.reconnect_backoff_ms) + : numericValueOrNull('websocket-reconnect-backoff-ms'); + var runtimeOptions = {}; + if (Number.isFinite(heartbeatInterval) && heartbeatInterval > 0) { + runtimeOptions.heartbeat_interval_ms = heartbeatInterval; + } + if (Number.isFinite(reconnectAttempts) && reconnectAttempts >= 0) { + runtimeOptions.reconnect_max_attempts = reconnectAttempts; + } + if (Number.isFinite(reconnectBackoff) && reconnectBackoff >= 0) { + runtimeOptions.reconnect_backoff_ms = reconnectBackoff; + } + if (Object.keys(runtimeOptions).length) { + config.protocol_options = { websocket: runtimeOptions }; + } + } + + config.streaming = collectStreamingConfig(); + + return config; +} + +function currentUpstream() { + var selected = upstreams.find(function(item) { return item.id === selectedUpstreamId; }); + if (selected) return selected; + + var customName = textValue('new-upstream-name'); + var customUrl = textValue('new-upstream-url'); + if (!customUrl && wizardProtocol === 'soap') { + customUrl = textValue('soap-endpoint-override'); + } + if (!customUrl) return null; + + var authMode = textValue('new-upstream-auth-mode') || 'none'; + var authProfileId = authMode === 'existing' ? textValue('new-upstream-auth-profile') : null; + var authProfile = authProfileById(authProfileId); + return { + id: 'custom', + name: customName || 'custom-upstream', + url: customUrl, + staticHeaders: textValue('new-upstream-static-headers') || '{\n}', + authMode: authMode, + authProfileId: authProfileId || null, + authProfileName: authProfile ? authProfile.name : '', + authKind: authProfile ? authProfile.kind : null, + }; +} + +function joinUrl(baseUrl, path) { + if (!baseUrl) return path || ''; + if (!path) return baseUrl; + return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, ''); +} + +function selectedGraphqlOperationType() { + var active = document.querySelector('.gql-type-card.active'); + return active ? active.dataset.gqlType : 'query'; +} + +function graphqlTopLevelField(queryText) { + var match = queryText.replace(/#[^\n]*/g, '').match(/\{\s*([A-Za-z_][A-Za-z0-9_]*)/); + return match ? match[1] : null; +} + +function graphqlOperationName(queryText) { + var match = queryText.match(/\b(query|mutation)\s+([A-Za-z_][A-Za-z0-9_]*)/); + return match ? match[2] : textValue('tool-name'); +} + +function parseGrpcRoute(route) { + var normalized = route.replace(/^\/+/, ''); + var parts = normalized.split('/'); + if (parts.length !== 2) { + return { + package: '', + service: '', + method: '', + }; + } + + var servicePart = parts[0]; + var method = parts[1]; + var serviceSegments = servicePart.split('.'); + var service = serviceSegments.pop() || ''; + var packageName = serviceSegments.join('.'); + + return { + package: packageName, + service: service, + method: method, + }; +} + +function parseHeaderMap(text) { + if (!text || !text.trim()) return {}; + var value = parseStructuredText(text); + return value && typeof value === 'object' ? value : {}; +} + +function numericValueOrNull(id) { + var value = textValue(id); + if (!value) return null; + var parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + +function stringValueOrNull(id) { + var value = textValue(id); + return value ? value : null; +} + +function textareaLines(id) { + var value = textValue(id); + if (!value) return []; + return value + .split(/\r?\n/) + .map(function(line) { return line.trim(); }) + .filter(Boolean); +} + +function buildTarget() { + var upstream = currentUpstream(); + if (!upstream || !upstream.url) { + throw new Error(tKey('wizard.error.select_upstream')); + } + + var pathTemplate = textValue('endpoint-path') || '/'; + + if (wizardProtocol === 'rest') { + var activeMethod = document.querySelector('.method-card.active'); + return { + kind: 'rest', + base_url: upstream.url, + method: activeMethod ? activeMethod.dataset.method : 'POST', + path_template: pathTemplate, + static_headers: parseHeaderMap(upstream.staticHeaders), + }; + } + + if (wizardProtocol === 'graphql') { + var queryTemplate = textValue('gql-query-editor'); + var topField = graphqlTopLevelField(queryTemplate); + return { + kind: 'graphql', + endpoint: joinUrl(upstream.url, pathTemplate), + operation_type: selectedGraphqlOperationType(), + operation_name: graphqlOperationName(queryTemplate), + query_template: queryTemplate, + response_path: topField ? '$.response.body.data.' + topField : '$.response.body.data', + }; + } + + 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: [] }, + }; + } + + if (wizardProtocol === 'websocket') { + return { + kind: 'websocket', + url: joinUrl(upstream.url, textValue('websocket-path')), + subprotocols: textareaLines('websocket-subprotocols'), + subscribe_message_template: stringValueOrNull('websocket-subscribe-template') + ? parseStructuredText(textValue('websocket-subscribe-template')) + : null, + unsubscribe_message_template: stringValueOrNull('websocket-unsubscribe-template') + ? parseStructuredText(textValue('websocket-unsubscribe-template')) + : null, + static_headers: parseHeaderMap(upstream.staticHeaders), + }; + } + + 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 + : 'desc_pending'; + var descriptorSetB64 = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc' + ? (wizardCurrentVersion.target.descriptor_set_b64 || '') + : ''; + var parsedRoute = parseGrpcRoute(route); + + return { + kind: 'grpc', + server_addr: upstream.url, + package: parsedRoute.package, + service: parsedRoute.service, + method: parsedRoute.method, + descriptor_ref: descriptorRef, + descriptor_set_b64: descriptorSetB64, + }; +} + +function crankSchemaToJsonSchema(schema) { + var result = {}; + var typeName = schema.type === 'enum' ? 'string' : schema.type; + result.type = schema.nullable ? [typeName, 'null'] : typeName; + if (schema.description) result.description = schema.description; + if (schema.default_value !== null && schema.default_value !== undefined) result.default = schema.default_value; + if (schema.enum_values && schema.enum_values.length) result.enum = schema.enum_values.slice(); + if (schema.type === 'object') { + result.type = schema.nullable ? ['object', 'null'] : 'object'; + result.properties = {}; + var required = []; + Object.keys(schema.fields || {}).forEach(function(fieldName) { + var field = schema.fields[fieldName]; + result.properties[fieldName] = crankSchemaToJsonSchema(field); + if (field.required) required.push(fieldName); + }); + if (required.length) result.required = required; + result.additionalProperties = false; + } + if (schema.type === 'array' && schema.items) { + result.items = crankSchemaToJsonSchema(schema.items); + } + return result; +} + +function mappingSetToEditorValue(mappingSet, mode, protocol) { + var rules = mappingSet && mappingSet.rules ? mappingSet.rules : []; + var object = {}; + rules.forEach(function(rule) { + if (mode === 'input') { + var key = rule.target.replace(mappingRootForProtocol(protocol) + '.', ''); + object[key] = rule.source.replace('$.mcp.', '$.input.'); + return; + } + var outputKey = rule.target.replace('$.output.', ''); + object[outputKey] = rule.source; + }); + return window.jsyaml ? window.jsyaml.dump(object, { lineWidth: -1 }) : JSON.stringify(object, null, 2); +} + +function executionConfigToEditorValue(config) { + var value = { + timeout_ms: config.timeout_ms, + }; + if (config.retry_policy) { + value.retry = { max_attempts: config.retry_policy.max_attempts }; + } + if (config.auth_profile_ref) { + value.auth = { profile: config.auth_profile_ref }; + } + if (config.headers && Object.keys(config.headers).length) { + value.headers = config.headers; + } + if (config.protocol_options && config.protocol_options.grpc) { + value.tls = { verify: !!config.protocol_options.grpc.use_tls }; + } + if (config.protocol_options && config.protocol_options.websocket) { + value.protocol_options = value.protocol_options || {}; + value.protocol_options.websocket = {}; + if (config.protocol_options.websocket.heartbeat_interval_ms != null) { + value.protocol_options.websocket.heartbeat_interval_ms = config.protocol_options.websocket.heartbeat_interval_ms; + } + if (config.protocol_options.websocket.reconnect_max_attempts != null) { + value.protocol_options.websocket.reconnect_max_attempts = config.protocol_options.websocket.reconnect_max_attempts; + } + if (config.protocol_options.websocket.reconnect_backoff_ms != null) { + value.protocol_options.websocket.reconnect_backoff_ms = config.protocol_options.websocket.reconnect_backoff_ms; + } + if (!Object.keys(value.protocol_options.websocket).length) { + delete value.protocol_options.websocket; + } + if (!Object.keys(value.protocol_options).length) { + delete value.protocol_options; + } + } + return window.jsyaml ? window.jsyaml.dump(value, { lineWidth: -1 }) : JSON.stringify(value, null, 2); +} + +function prefillUpstream(baseUrl, headers, authProfileRef) { + var known = upstreams.find(function(item) { + return item.url === baseUrl && (item.authProfileId || null) === (authProfileRef || null); + }); + if (known) { + pickUpstream(known.id); + return; + } + + selectedUpstreamId = null; + var trigger = document.getElementById('upstream-new-trigger'); + var form = document.getElementById('upstream-new-form'); + if (trigger) trigger.classList.add('active'); + if (form) form.hidden = false; + setValue('new-upstream-name', 'imported-upstream'); + setValue('new-upstream-url', baseUrl); + setValue('new-upstream-static-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}'); + setValue('new-upstream-auth-mode', authProfileRef ? 'existing' : 'none'); + setValue('new-upstream-auth-profile', authProfileRef || ''); + updateUpstreamAuthUi(); + + var valueEl = document.getElementById('upstream-combobox-value'); + if (valueEl) { + valueEl.innerHTML = ''; + valueEl.appendChild(buildUpstreamTriggerContent('imported-upstream', baseUrl)); + } +} + +function prefillWizardFromEdit(detail, versionDocument) { + if (!detail || !versionDocument) return; + + setEditModePresentation(); + selectProtocol(detail.protocol); + + var target = versionDocument.target || {}; + if (target.kind === 'rest') { + prefillUpstream(target.base_url, target.static_headers, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null); + setValue('endpoint-path', target.path_template); + document.querySelectorAll('.method-card').forEach(function(card) { + card.classList.toggle('active', card.dataset.method === target.method); + }); + } else if (target.kind === 'graphql') { + var endpoint = target.endpoint || ''; + var parsedEndpoint = new URL(endpoint, window.location.origin); + prefillUpstream(parsedEndpoint.origin, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null); + setValue('endpoint-path', parsedEndpoint.pathname + parsedEndpoint.search); + setValue('gql-query-editor', target.query_template); + document.querySelectorAll('.gql-type-card').forEach(function(card) { + card.classList.toggle('active', card.dataset.gqlType === target.operation_type); + }); + } else if (target.kind === 'grpc') { + prefillUpstream(target.server_addr, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null); + var route = '/' + (target.package ? target.package + '.' : '') + target.service + '/' + target.method; + setValue('grpc-manual-route', route); + setValue('grpc-manual-req-type', ''); + 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'); + } else if (target.kind === 'websocket') { + prefillUpstream( + target.url, + target.static_headers || {}, + versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null + ); + setValue('websocket-path', ''); + setValue('websocket-subprotocols', (target.subprotocols || []).join('\n')); + setValue( + 'websocket-subscribe-template', + target.subscribe_message_template ? JSON.stringify(target.subscribe_message_template, null, 2) : '' + ); + setValue( + 'websocket-unsubscribe-template', + target.unsubscribe_message_template ? JSON.stringify(target.unsubscribe_message_template, null, 2) : '' + ); + var websocketOptions = versionDocument.execution_config + && versionDocument.execution_config.protocol_options + && versionDocument.execution_config.protocol_options.websocket + ? versionDocument.execution_config.protocol_options.websocket + : null; + setValue('websocket-heartbeat-interval-ms', websocketOptions && websocketOptions.heartbeat_interval_ms ? websocketOptions.heartbeat_interval_ms : ''); + setValue('websocket-reconnect-max-attempts', websocketOptions && websocketOptions.reconnect_max_attempts != null ? websocketOptions.reconnect_max_attempts : ''); + setValue('websocket-reconnect-backoff-ms', websocketOptions && websocketOptions.reconnect_backoff_ms ? websocketOptions.reconnect_backoff_ms : ''); + } + + setValue('tool-name', detail.name); + setValue('tool-display-name', detail.display_name); + setValue('tool-title', versionDocument.tool_description ? versionDocument.tool_description.title : detail.display_name); + setValue('tool-description', versionDocument.tool_description ? versionDocument.tool_description.description : ''); + setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(versionDocument.input_schema), null, 2)); + setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(versionDocument.output_schema), null, 2)); + setValue('tool-input-mapping', mappingSetToEditorValue(versionDocument.input_mapping, 'input', detail.protocol)); + setValue('tool-output-mapping', mappingSetToEditorValue(versionDocument.output_mapping, 'output', detail.protocol)); + setValue('tool-exec-config', executionConfigToEditorValue(versionDocument.execution_config)); + applyStreamingConfig(versionDocument.execution_config ? versionDocument.execution_config.streaming : null); + + var toolNameInput = document.getElementById('tool-name'); + if (toolNameInput) toolNameInput.disabled = true; + updateWizardProtocolVisibility(); +} diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js index 8ce62d1..d557a16 100644 --- a/apps/ui/js/wizard.js +++ b/apps/ui/js/wizard.js @@ -262,359 +262,6 @@ function parseStructuredText(text) { } } -function inferSchemaKind(typeName) { - if (typeName === 'object') return 'object'; - if (typeName === 'array') return 'array'; - if (typeName === 'string') return 'string'; - if (typeName === 'integer') return 'integer'; - if (typeName === 'number') return 'number'; - if (typeName === 'boolean') return 'boolean'; - if (typeName === 'null') return 'null'; - return 'string'; -} - -function convertJsonSchemaToCrankSchema(node, requiredNames) { - var schemaNode = node || {}; - var typeName = Array.isArray(schemaNode.type) - ? schemaNode.type.find(function(value) { return value !== 'null'; }) || 'string' - : schemaNode.type || (schemaNode.properties ? 'object' : 'string'); - var nullable = Array.isArray(schemaNode.type) && schemaNode.type.indexOf('null') >= 0; - var kind = schemaNode.enum ? 'enum' : inferSchemaKind(typeName); - var schema = { - type: kind, - description: schemaNode.description || null, - required: false, - nullable: nullable, - default_value: Object.prototype.hasOwnProperty.call(schemaNode, 'default') ? schemaNode.default : null, - fields: {}, - items: null, - enum_values: schemaNode.enum || [], - variants: [], - }; - - if (kind === 'object') { - var requiredSet = new Set(schemaNode.required || requiredNames || []); - Object.keys(schemaNode.properties || {}).forEach(function(fieldName) { - var field = convertJsonSchemaToCrankSchema(schemaNode.properties[fieldName], []); - field.required = requiredSet.has(fieldName); - schema.fields[fieldName] = field; - }); - } - - if (kind === 'array' && schemaNode.items) { - schema.items = convertJsonSchemaToCrankSchema(schemaNode.items, []); - } - - return schema; -} - -function mappingRootForProtocol(protocol) { - if (protocol === 'graphql') return '$.request.variables'; - if (protocol === 'grpc') return '$.request.grpc'; - var activeMethod = document.querySelector('.method-card.active'); - var method = activeMethod ? activeMethod.dataset.method : 'POST'; - return method === 'GET' || method === 'DELETE' ? '$.request.query' : '$.request.body'; -} - -function normalizeInputSource(path) { - if (typeof path !== 'string') return '$.mcp'; - if (path.indexOf('$.input.') === 0) return '$.mcp.' + path.slice('$.input.'.length); - if (path === '$.input') return '$.mcp'; - return path; -} - -function normalizeOutputSource(path) { - if (typeof path !== 'string') return '$.response.data'; - return path; -} - -function buildMappingSet(rawValue, mode) { - if (rawValue && Array.isArray(rawValue.rules)) { - return { rules: rawValue.rules }; - } - - var rules = []; - if (!rawValue || typeof rawValue !== 'object') { - return { rules: rules }; - } - - var root = mappingRootForProtocol(wizardProtocol); - - Object.keys(rawValue).forEach(function(key) { - var source = rawValue[key]; - if (typeof source !== 'string') return; - if (mode === 'input') { - rules.push({ - source: normalizeInputSource(source), - target: root + '.' + key, - required: false, - }); - return; - } - - rules.push({ - source: normalizeOutputSource(source), - target: '$.output.' + key, - required: false, - }); - }); - - return { rules: rules }; -} - -function parseExecutionConfig(text) { - if (window.CrankStreamingForm && typeof window.CrankStreamingForm.validateStreamingConfig === 'function') { - var validation = window.CrankStreamingForm.validateStreamingConfig([currentProtocolCapabilities()]); - if (!validation.valid) { - throw new Error(validation.errors[0]); - } - } - var value = parseStructuredText(text); - var retry = value.retry || value.retry_policy || null; - var headers = value.headers && typeof value.headers === 'object' ? value.headers : {}; - var selectedUpstream = currentUpstream(); - if (selectedUpstream && selectedUpstream.authMode === 'create') { - throw new Error(tKey('wizard.error.save_upstream_first')); - } - var authProfileRef = selectedUpstream && selectedUpstream.authProfileId - ? selectedUpstream.authProfileId - : (value.auth && value.auth.profile ? value.auth.profile : null); - var config = { - timeout_ms: Number(value.timeout_ms || 10000), - retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null, - auth_profile_ref: authProfileRef, - headers: headers, - protocol_options: null, - }; - - if (wizardProtocol === 'grpc') { - var useTls = false; - if (value.protocol_options && value.protocol_options.grpc) { - useTls = !!value.protocol_options.grpc.use_tls; - } else if (value.tls) { - useTls = !!value.tls.verify; - } - config.protocol_options = { grpc: { use_tls: useTls } }; - } else if (wizardProtocol === 'websocket') { - var websocketProtocolOptions = value.protocol_options && value.protocol_options.websocket - ? value.protocol_options.websocket - : {}; - var heartbeatInterval = websocketProtocolOptions.heartbeat_interval_ms != null - ? Number(websocketProtocolOptions.heartbeat_interval_ms) - : numericValueOrNull('websocket-heartbeat-interval-ms'); - var reconnectAttempts = websocketProtocolOptions.reconnect_max_attempts != null - ? Number(websocketProtocolOptions.reconnect_max_attempts) - : numericValueOrNull('websocket-reconnect-max-attempts'); - var reconnectBackoff = websocketProtocolOptions.reconnect_backoff_ms != null - ? Number(websocketProtocolOptions.reconnect_backoff_ms) - : numericValueOrNull('websocket-reconnect-backoff-ms'); - var runtimeOptions = {}; - if (Number.isFinite(heartbeatInterval) && heartbeatInterval > 0) { - runtimeOptions.heartbeat_interval_ms = heartbeatInterval; - } - if (Number.isFinite(reconnectAttempts) && reconnectAttempts >= 0) { - runtimeOptions.reconnect_max_attempts = reconnectAttempts; - } - if (Number.isFinite(reconnectBackoff) && reconnectBackoff >= 0) { - runtimeOptions.reconnect_backoff_ms = reconnectBackoff; - } - if (Object.keys(runtimeOptions).length) { - config.protocol_options = { websocket: runtimeOptions }; - } - } - - config.streaming = collectStreamingConfig(); - - return config; -} - -function currentUpstream() { - var selected = upstreams.find(function(item) { return item.id === selectedUpstreamId; }); - if (selected) return selected; - - var customName = textValue('new-upstream-name'); - var customUrl = textValue('new-upstream-url'); - if (!customUrl && wizardProtocol === 'soap') { - customUrl = textValue('soap-endpoint-override'); - } - if (!customUrl) return null; - - var authMode = textValue('new-upstream-auth-mode') || 'none'; - var authProfileId = authMode === 'existing' ? textValue('new-upstream-auth-profile') : null; - var authProfile = authProfileById(authProfileId); - return { - id: 'custom', - name: customName || 'custom-upstream', - url: customUrl, - staticHeaders: textValue('new-upstream-static-headers') || '{\n}', - authMode: authMode, - authProfileId: authProfileId || null, - authProfileName: authProfile ? authProfile.name : '', - authKind: authProfile ? authProfile.kind : null, - }; -} - -function joinUrl(baseUrl, path) { - if (!baseUrl) return path || ''; - if (!path) return baseUrl; - return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, ''); -} - -function selectedGraphqlOperationType() { - var active = document.querySelector('.gql-type-card.active'); - return active ? active.dataset.gqlType : 'query'; -} - -function graphqlTopLevelField(queryText) { - var match = queryText.replace(/#[^\n]*/g, '').match(/\{\s*([A-Za-z_][A-Za-z0-9_]*)/); - return match ? match[1] : null; -} - -function graphqlOperationName(queryText) { - var match = queryText.match(/\b(query|mutation)\s+([A-Za-z_][A-Za-z0-9_]*)/); - return match ? match[2] : textValue('tool-name'); -} - -function buildTarget() { - var upstream = currentUpstream(); - if (!upstream || !upstream.url) { - throw new Error(tKey('wizard.error.select_upstream')); - } - - var pathTemplate = textValue('endpoint-path') || '/'; - - if (wizardProtocol === 'rest') { - var activeMethod = document.querySelector('.method-card.active'); - return { - kind: 'rest', - base_url: upstream.url, - method: activeMethod ? activeMethod.dataset.method : 'POST', - path_template: pathTemplate, - static_headers: parseHeaderMap(upstream.staticHeaders), - }; - } - - if (wizardProtocol === 'graphql') { - var queryTemplate = textValue('gql-query-editor'); - var topField = graphqlTopLevelField(queryTemplate); - return { - kind: 'graphql', - endpoint: joinUrl(upstream.url, pathTemplate), - operation_type: selectedGraphqlOperationType(), - operation_name: graphqlOperationName(queryTemplate), - query_template: queryTemplate, - response_path: topField ? '$.response.body.data.' + topField : '$.response.body.data', - }; - } - - 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: [] }, - }; - } - - if (wizardProtocol === 'websocket') { - return { - kind: 'websocket', - url: joinUrl(upstream.url, textValue('websocket-path')), - subprotocols: textareaLines('websocket-subprotocols'), - subscribe_message_template: stringValueOrNull('websocket-subscribe-template') - ? parseStructuredText(textValue('websocket-subscribe-template')) - : null, - unsubscribe_message_template: stringValueOrNull('websocket-unsubscribe-template') - ? parseStructuredText(textValue('websocket-unsubscribe-template')) - : null, - static_headers: parseHeaderMap(upstream.staticHeaders), - }; - } - - 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 - : 'desc_pending'; - var descriptorSetB64 = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc' - ? (wizardCurrentVersion.target.descriptor_set_b64 || '') - : ''; - var parsedRoute = parseGrpcRoute(route); - - return { - kind: 'grpc', - server_addr: upstream.url, - package: parsedRoute.package, - service: parsedRoute.service, - method: parsedRoute.method, - descriptor_ref: descriptorRef, - descriptor_set_b64: descriptorSetB64, - }; -} - -function parseGrpcRoute(route) { - var normalized = route.replace(/^\/+/, ''); - var parts = normalized.split('/'); - if (parts.length !== 2) { - return { - package: '', - service: '', - method: '', - }; - } - - var servicePart = parts[0]; - var method = parts[1]; - var serviceSegments = servicePart.split('.'); - var service = serviceSegments.pop() || ''; - var packageName = serviceSegments.join('.'); - - return { - package: packageName, - service: service, - method: method, - }; -} - -function parseHeaderMap(text) { - if (!text || !text.trim()) return {}; - var value = parseStructuredText(text); - return value && typeof value === 'object' ? value : {}; -} - -function numericValueOrNull(id) { - var value = textValue(id); - if (!value) return null; - var parsed = Number(value); - return Number.isFinite(parsed) && parsed > 0 ? parsed : null; -} - -function stringValueOrNull(id) { - var value = textValue(id); - return value ? value : null; -} - -function textareaLines(id) { - var value = textValue(id); - if (!value) return []; - return value - .split(/\r?\n/) - .map(function(line) { return line.trim(); }) - .filter(Boolean); -} - function buildToolDescription() { return { title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'), @@ -706,197 +353,6 @@ function selectProtocol(protocol) { updateWizardProtocolVisibility(); } -function prefillUpstream(baseUrl, headers, authProfileRef) { - var known = upstreams.find(function(item) { - return item.url === baseUrl && (item.authProfileId || null) === (authProfileRef || null); - }); - if (known) { - pickUpstream(known.id); - return; - } - - selectedUpstreamId = null; - var trigger = document.getElementById('upstream-new-trigger'); - var form = document.getElementById('upstream-new-form'); - if (trigger) trigger.classList.add('active'); - if (form) form.hidden = false; - setValue('new-upstream-name', 'imported-upstream'); - setValue('new-upstream-url', baseUrl); - setValue('new-upstream-static-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}'); - setValue('new-upstream-auth-mode', authProfileRef ? 'existing' : 'none'); - setValue('new-upstream-auth-profile', authProfileRef || ''); - updateUpstreamAuthUi(); - - var valueEl = document.getElementById('upstream-combobox-value'); - if (valueEl) { - valueEl.innerHTML = ''; - valueEl.appendChild(buildUpstreamTriggerContent('imported-upstream', baseUrl)); - } -} - -function crankSchemaToJsonSchema(schema) { - var result = {}; - var typeName = schema.type === 'enum' ? 'string' : schema.type; - result.type = schema.nullable ? [typeName, 'null'] : typeName; - if (schema.description) result.description = schema.description; - if (schema.default_value !== null && schema.default_value !== undefined) result.default = schema.default_value; - if (schema.enum_values && schema.enum_values.length) result.enum = schema.enum_values.slice(); - if (schema.type === 'object') { - result.type = schema.nullable ? ['object', 'null'] : 'object'; - result.properties = {}; - var required = []; - Object.keys(schema.fields || {}).forEach(function(fieldName) { - var field = schema.fields[fieldName]; - result.properties[fieldName] = crankSchemaToJsonSchema(field); - if (field.required) required.push(fieldName); - }); - if (required.length) result.required = required; - result.additionalProperties = false; - } - if (schema.type === 'array' && schema.items) { - result.items = crankSchemaToJsonSchema(schema.items); - } - return result; -} - -function mappingSetToEditorValue(mappingSet, mode, protocol) { - var rules = mappingSet && mappingSet.rules ? mappingSet.rules : []; - var object = {}; - rules.forEach(function(rule) { - if (mode === 'input') { - var key = rule.target.replace(mappingRootForProtocol(protocol) + '.', ''); - object[key] = rule.source.replace('$.mcp.', '$.input.'); - return; - } - var outputKey = rule.target.replace('$.output.', ''); - object[outputKey] = rule.source; - }); - return window.jsyaml ? window.jsyaml.dump(object, { lineWidth: -1 }) : JSON.stringify(object, null, 2); -} - -function executionConfigToEditorValue(config) { - var value = { - timeout_ms: config.timeout_ms, - }; - if (config.retry_policy) { - value.retry = { max_attempts: config.retry_policy.max_attempts }; - } - if (config.auth_profile_ref) { - value.auth = { profile: config.auth_profile_ref }; - } - if (config.headers && Object.keys(config.headers).length) { - value.headers = config.headers; - } - if (config.protocol_options && config.protocol_options.grpc) { - value.tls = { verify: !!config.protocol_options.grpc.use_tls }; - } - if (config.protocol_options && config.protocol_options.websocket) { - value.protocol_options = value.protocol_options || {}; - value.protocol_options.websocket = {}; - if (config.protocol_options.websocket.heartbeat_interval_ms != null) { - value.protocol_options.websocket.heartbeat_interval_ms = config.protocol_options.websocket.heartbeat_interval_ms; - } - if (config.protocol_options.websocket.reconnect_max_attempts != null) { - value.protocol_options.websocket.reconnect_max_attempts = config.protocol_options.websocket.reconnect_max_attempts; - } - if (config.protocol_options.websocket.reconnect_backoff_ms != null) { - value.protocol_options.websocket.reconnect_backoff_ms = config.protocol_options.websocket.reconnect_backoff_ms; - } - if (!Object.keys(value.protocol_options.websocket).length) { - delete value.protocol_options.websocket; - } - if (!Object.keys(value.protocol_options).length) { - delete value.protocol_options; - } - } - return window.jsyaml ? window.jsyaml.dump(value, { lineWidth: -1 }) : JSON.stringify(value, null, 2); -} - -function prefillWizardFromEdit(detail, versionDocument) { - if (!detail || !versionDocument) return; - - setEditModePresentation(); - selectProtocol(detail.protocol); - - var target = versionDocument.target || {}; - if (target.kind === 'rest') { - prefillUpstream(target.base_url, target.static_headers, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null); - setValue('endpoint-path', target.path_template); - document.querySelectorAll('.method-card').forEach(function(card) { - card.classList.toggle('active', card.dataset.method === target.method); - }); - } else if (target.kind === 'graphql') { - var endpoint = target.endpoint || ''; - var parsedEndpoint = new URL(endpoint, window.location.origin); - prefillUpstream(parsedEndpoint.origin, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null); - setValue('endpoint-path', parsedEndpoint.pathname + parsedEndpoint.search); - setValue('gql-query-editor', target.query_template); - document.querySelectorAll('.gql-type-card').forEach(function(card) { - card.classList.toggle('active', card.dataset.gqlType === target.operation_type); - }); - } else if (target.kind === 'grpc') { - prefillUpstream(target.server_addr, {}, versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null); - var route = '/' + (target.package ? target.package + '.' : '') + target.service + '/' + target.method; - setValue('grpc-manual-route', route); - setValue('grpc-manual-req-type', ''); - 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'); - } else if (target.kind === 'websocket') { - prefillUpstream( - target.url, - target.static_headers || {}, - versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null - ); - setValue('websocket-path', ''); - setValue('websocket-subprotocols', (target.subprotocols || []).join('\n')); - setValue( - 'websocket-subscribe-template', - target.subscribe_message_template ? JSON.stringify(target.subscribe_message_template, null, 2) : '' - ); - setValue( - 'websocket-unsubscribe-template', - target.unsubscribe_message_template ? JSON.stringify(target.unsubscribe_message_template, null, 2) : '' - ); - var websocketOptions = versionDocument.execution_config - && versionDocument.execution_config.protocol_options - && versionDocument.execution_config.protocol_options.websocket - ? versionDocument.execution_config.protocol_options.websocket - : null; - setValue('websocket-heartbeat-interval-ms', websocketOptions && websocketOptions.heartbeat_interval_ms ? websocketOptions.heartbeat_interval_ms : ''); - setValue('websocket-reconnect-max-attempts', websocketOptions && websocketOptions.reconnect_max_attempts != null ? websocketOptions.reconnect_max_attempts : ''); - setValue('websocket-reconnect-backoff-ms', websocketOptions && websocketOptions.reconnect_backoff_ms ? websocketOptions.reconnect_backoff_ms : ''); - } - - setValue('tool-name', detail.name); - setValue('tool-display-name', detail.display_name); - setValue('tool-title', versionDocument.tool_description ? versionDocument.tool_description.title : detail.display_name); - setValue('tool-description', versionDocument.tool_description ? versionDocument.tool_description.description : ''); - setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(versionDocument.input_schema), null, 2)); - setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(versionDocument.output_schema), null, 2)); - setValue('tool-input-mapping', mappingSetToEditorValue(versionDocument.input_mapping, 'input', detail.protocol)); - setValue('tool-output-mapping', mappingSetToEditorValue(versionDocument.output_mapping, 'output', detail.protocol)); - setValue('tool-exec-config', executionConfigToEditorValue(versionDocument.execution_config)); - applyStreamingConfig(versionDocument.execution_config ? versionDocument.execution_config.streaming : null); - - var toolNameInput = document.getElementById('tool-name'); - if (toolNameInput) toolNameInput.disabled = true; - updateWizardProtocolVisibility(); -} - function bindWizardLiveActions() { bindLiveAction('wizard-upload-input-sample', tKey('wizard.busy.input_sample'), uploadInputSampleFromWizard); bindLiveAction('wizard-upload-output-sample', tKey('wizard.busy.output_sample'), uploadOutputSampleFromWizard); diff --git a/apps/ui/scripts/build.js b/apps/ui/scripts/build.js index 96b1bad..277ba34 100644 --- a/apps/ui/scripts/build.js +++ b/apps/ui/scripts/build.js @@ -105,6 +105,7 @@ const BUNDLES = { 'js/wizard-grpc.js', 'js/wizard-artifacts.js', 'js/wizard-upstreams.js', + 'js/wizard-model.js', 'js/wizard.js', 'js/nav.js', ],