var currentStep = 1; var TOTAL_STEPS = 5; var wizardProtocol = 'rest'; // 'rest' | 'graphql' | 'grpc' | 'websocket' | 'soap' var wizardMode = 'create'; // 'create' | 'edit' var wizardEditId = null; var wizardWorkspaceId = null; var wizardCurrentOperation = null; var wizardCurrentVersion = null; var wizardProtoUpload = null; var wizardDescriptorSetUpload = null; var wizardTestResponsePreview = null; var grpcDescriptorServices = []; var wizardProtocolCapabilities = null; function tKey(key) { return typeof t === 'function' ? t(key) : key; } function tfKey(key, vars) { return typeof tf === 'function' ? tf(key, vars) : key; } function defaultProtocolCapabilities() { return { rest: { supports_execution_modes: ['unary', 'window', 'session', 'async_job'], supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'], }, graphql: { supports_execution_modes: ['unary'], supports_transport_behaviors: ['request_response'], }, grpc: { supports_execution_modes: ['unary', 'window', 'session', 'async_job'], supports_transport_behaviors: ['request_response', 'server_stream'], }, websocket: { supports_execution_modes: ['window', 'session', 'async_job'], supports_transport_behaviors: ['server_stream', 'stateful_session', 'deferred_result'], }, soap: { supports_execution_modes: ['unary', 'async_job'], supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'], }, }; } function currentProtocolCapabilities() { var capabilities = wizardProtocolCapabilities || defaultProtocolCapabilities(); return capabilities[wizardProtocol] || capabilities.rest; } async function loadProtocolCapabilities() { if (!wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') { wizardProtocolCapabilities = defaultProtocolCapabilities(); return wizardProtocolCapabilities; } try { var response = await window.CrankApi.getProtocolCapabilities(wizardWorkspaceId); var mapped = {}; (response.items || []).forEach(function(item) { mapped[item.protocol] = item; }); wizardProtocolCapabilities = mapped; } catch (_error) { wizardProtocolCapabilities = defaultProtocolCapabilities(); } return wizardProtocolCapabilities; } /* ── Dynamic step loading ── */ function _stepFile(n) { return (n === 3) ? 'step3-' + wizardProtocol + '.html' : 'step' + n + '.html'; } function loadStep(n, callback) { var panelId = (n === 3) ? step3PanelId() : 'step-panel-' + n; if (document.getElementById(panelId)) { if (callback) callback(); return; } fetch(_stepFile(n)) .then(function(r) { return r.text(); }) .then(function(html) { var c = document.getElementById('step-panel-container'); if (c) c.insertAdjacentHTML('beforeend', html); if (typeof applyLang === 'function') applyLang(); if (callback) callback(); }) .catch(function() { if (callback) callback(); }); } /* Step 3 panel id by protocol */ function step3PanelId() { if (wizardProtocol === 'graphql') return 'step-panel-3-graphql'; if (wizardProtocol === 'grpc') return 'step-panel-3-grpc'; return 'step-panel-3-rest'; } /* Sidebar step-3 label by protocol */ var STEP3_LABELS = { rest: tKey('wizard.step3.rest.label'), graphql: tKey('wizard.step3.graphql.label'), grpc: tKey('wizard.step3.grpc.label'), }; var CHECK_SVG = ''; var ARROW_SVG = ''; var BACK_SVG = ''; function goToStep(n) { if (n < 1 || n > TOTAL_STEPS) return; loadStep(n, function() { _doGoToStep(n); }); } function _doGoToStep(n) { document.querySelectorAll('.step-number[data-step]').forEach(function(element) { var step = Number(element.getAttribute('data-step')) || 0; element.textContent = tfKey('wizard.step_short', { step: step }); }); // 1. hide all panes, show target (step 4 is protocol-specific) document.querySelectorAll('.step-pane').forEach(function(p) { p.style.display = 'none'; }); var panelId = (n === 3) ? step3PanelId() : 'step-panel-' + n; var pane = document.getElementById(panelId); if (pane) pane.style.display = ''; // 2. update sidebar step items document.querySelectorAll('.step-item').forEach(function(item, i) { var num = i + 1; item.classList.remove('active', 'done', 'pending'); var ind = item.querySelector('.step-indicator'); var statusEl = item.querySelector('.step-status-text'); if (num < n) { item.classList.add('done'); if (ind) ind.innerHTML = CHECK_SVG; if (statusEl) statusEl.textContent = tKey('wizard.status.completed'); } else if (num === n) { item.classList.add('active'); if (ind) ind.textContent = num; if (statusEl) statusEl.textContent = tKey('wizard.status.in_progress'); } else { item.classList.add('pending'); if (ind) ind.textContent = num; if (statusEl) statusEl.textContent = tKey('wizard.status.not_started'); } }); // 3. update progress bar var pct = Math.round((n / TOTAL_STEPS) * 100); var fill = document.querySelector('.progress-bar-fill'); if (fill) fill.style.width = pct + '%'; var pctEl = document.querySelector('.progress-pct'); if (pctEl) pctEl.textContent = pct + '%'; // 4. update step counter var counter = document.querySelector('[data-step-counter]'); if (counter) counter.innerHTML = tfKey('wizard.step_of', { step: n, total: TOTAL_STEPS }); // 5. back button var backBtn = document.querySelector('.btn-back'); if (backBtn) { if (n === 1) { backBtn.disabled = true; backBtn.style.opacity = '0.35'; backBtn.style.cursor = 'not-allowed'; } else { backBtn.disabled = false; backBtn.style.opacity = ''; backBtn.style.cursor = ''; } } // 6. continue button var contBtn = document.querySelector('.btn-continue'); if (contBtn) { if (n === TOTAL_STEPS) { var finalLabel = wizardMode === 'edit' ? tKey('wizard.button.save_changes') : tKey('wizard.button.create'); contBtn.innerHTML = finalLabel + ' ' + ARROW_SVG; contBtn.style.cssText = 'background:#3fb950;border-color:#2ea043;box-shadow:0 1px 4px rgba(63,185,80,0.4);'; } else { contBtn.innerHTML = tKey('wizard.button.continue') + ' ' + ARROW_SVG; contBtn.style.cssText = ''; } } currentStep = n; window.scrollTo({ top: 0, behavior: 'smooth' }); } function loadWizardPanels(steps) { return steps.reduce(function(chain, step) { return chain.then(function() { return new Promise(function(resolve) { loadStep(step, resolve); }); }); }, Promise.resolve()); } function bindProtocolCards() { document.querySelectorAll('.protocol-card').forEach(function(card) { card.addEventListener('click', function() { document.querySelectorAll('.protocol-card').forEach(function(item) { item.classList.remove('selected'); item.setAttribute('aria-checked', 'false'); }); card.classList.add('selected'); card.setAttribute('aria-checked', 'true'); var proto = card.dataset.protocol || (card.classList.contains('rest') ? 'rest' : card.classList.contains('graphql') ? 'graphql' : card.classList.contains('grpc') ? 'grpc' : 'rest'); wizardProtocol = proto; var s3name = document.getElementById('sidebar-step-3-name'); if (s3name) s3name.textContent = STEP3_LABELS[proto] || tKey('wizard.step3.rest.label'); if (proto === 'graphql') { var pathInput = document.getElementById('endpoint-path'); if (pathInput && pathInput.value === '/v1/leads') pathInput.value = '/graphql'; } }); card.addEventListener('keydown', function(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); card.click(); } }); }); } document.addEventListener('DOMContentLoaded', async function() { document.querySelector('.btn-continue').addEventListener('click', function() { if (currentStep < TOTAL_STEPS) { goToStep(currentStep + 1); } else { saveOperation(); } }); document.querySelector('.btn-back').addEventListener('click', function() { if (!this.disabled && currentStep > 1) goToStep(currentStep - 1); }); document.querySelectorAll('.step-item').forEach(function(item, i) { item.addEventListener('click', function() { goToStep(i + 1); }); }); var backToCatalog = document.getElementById('back-to-catalog'); if (backToCatalog) { backToCatalog.addEventListener('click', function() { window.location.href = (window.CrankRoutes && window.CrankRoutes.home) || '/'; }); } var closeBtn = document.querySelector('.progress-close'); if (closeBtn) { closeBtn.addEventListener('click', function() { window.location.href = (window.CrankRoutes && window.CrankRoutes.home) || '/'; }); } var saveDraftBtn = document.querySelector('.btn-save-draft'); if (saveDraftBtn) { saveDraftBtn.addEventListener('click', function() { saveOperation(true); }); } await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve()); var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; wizardWorkspaceId = workspace ? workspace.id : null; await loadProtocolCapabilities(); await loadWizardPanels([1, 2, 3, 4, 5]); await loadWizardAuthResources(); bindProtocolCards(); bindWizardLiveActions(); bindStreamingConfigControls(); updateUpstreamAuthUi(); var quickSecretModal = document.getElementById('quick-secret-modal'); var quickSecretClose = document.getElementById('quick-secret-close-btn'); var quickSecretCancel = document.getElementById('quick-secret-cancel-btn'); var quickSecretSubmit = document.getElementById('quick-secret-submit-btn'); if (quickSecretClose) quickSecretClose.addEventListener('click', closeQuickSecretModal); if (quickSecretCancel) quickSecretCancel.addEventListener('click', closeQuickSecretModal); if (quickSecretModal) { quickSecretModal.addEventListener('click', function(event) { if (event.target === quickSecretModal) closeQuickSecretModal(); }); } if (quickSecretSubmit) { quickSecretSubmit.addEventListener('click', async function() { var button = quickSecretSubmit; var original = button.textContent; button.disabled = true; button.textContent = tKey('wizard.step2.quick_secret_submit'); try { await submitQuickSecret(); } catch (error) { if (window.CrankUi) { window.CrankUi.error( error.message || tKey('wizard.toast.quick_secret_error_title'), tKey('wizard.toast.quick_secret_error_title') ); } } finally { button.disabled = false; button.textContent = original; } }); } var params = new URLSearchParams(window.location.search); if (params.get('mode') === 'edit' && params.get('operationId')) { wizardMode = 'edit'; wizardEditId = params.get('operationId'); document.title = 'Crank — ' + tKey('wizard.progress.edit'); await loadOperationForEdit(); } updateWizardProtocolVisibility(); _doGoToStep(1); }); /* ── HTTP method picker (Step 4 REST) ── */ var METHOD_CALLOUTS = { GET: null, DELETE: null, POST: { icon: 'info', text: { en: 'POST selected — body encoding required. In step 4 you will define the JSON schema for the body payload. Crank will automatically serialize MCP tool arguments into the format you specify.', ru: 'Выбран POST — требуется кодирование тела запроса. На шаге 4 вы зададите JSON-схему для body payload. Crank автоматически сериализует аргументы MCP-инструмента в выбранный формат.' } }, PUT: { icon: 'info', text: { en: 'PUT selected — full-resource replacement. The request body must contain a complete resource representation. Define the body schema in step 4.', ru: 'Выбран PUT — полная замена ресурса. Request body должен содержать полное представление ресурса. Задайте body schema на шаге 4.' } }, PATCH: { icon: 'info', text: { en: 'PATCH selected — partial update. Only include the fields you want to modify in the body schema defined in step 4.', ru: 'Выбран PATCH — частичное обновление. В body schema на шаге 4 включайте только те поля, которые хотите изменить.' } }, }; function selectMethod(btn) { document.querySelectorAll('.method-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); var method = btn.dataset.method; var callout = document.getElementById('method-callout-rest'); if (!callout) return; var info = METHOD_CALLOUTS[method]; if (info) { var lang = localStorage.getItem('crank_lang') || 'en'; var text = typeof info.text === 'string' ? info.text : (info.text[lang] || info.text.en); callout.innerHTML = '' + '' + text + ''; callout.style.display = 'flex'; } else { callout.style.display = 'none'; } } /* ── GraphQL operation type picker (Step 4 GraphQL) ── */ function selectGqlType(btn) { document.querySelectorAll('.gql-type-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); // Update query editor placeholder if it's still the default var editor = document.getElementById('gql-query-editor'); if (!editor) return; var type = btn.dataset.gqlType; var defaults = { query: 'query GetContact($id: ID!) {\n contact(id: $id) {\n id\n firstName\n lastName\n email\n company {\n id\n name\n }\n createdAt\n }\n}', mutation: 'mutation CreateContact(\n $firstName: String!\n $lastName: String!\n $email: String!\n $company: String\n) {\n createContact(input: {\n firstName: $firstName\n lastName: $lastName\n email: $email\n company: $company\n }) {\n id\n firstName\n lastName\n createdAt\n }\n}' }; if (defaults[type]) editor.value = defaults[type]; } /* ── Upstream selector (Step 2) ── */ var upstreams = [ { id: 'open-meteo', name: 'Open Meteo', url: 'https://api.open-meteo.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }, { id: 'countries-graphql', name: 'Countries GraphQL', url: 'https://countries.trevorblades.com', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }, { id: 'grpcb', name: 'grpcb.in', url: 'https://grpcb.in:443', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null } ]; var wizardSecrets = []; var wizardAuthProfiles = []; var selectedUpstreamId = null; var dropdownOpen = false; var editingUpstreamId = null; function authProfileById(authProfileId) { return wizardAuthProfiles.find(function(item) { return item.id === authProfileId; }) || null; } function syncUpstreamAuthMetadata(upstream) { if (!upstream) return upstream; if (!upstream.authProfileId) { upstream.authProfileName = ''; upstream.authKind = null; return upstream; } var profile = authProfileById(upstream.authProfileId); if (profile) { upstream.authProfileName = profile.name; upstream.authKind = profile.kind; } return upstream; } function inferUpstreamAuth(upstream) { syncUpstreamAuthMetadata(upstream); if (!upstream || !upstream.authProfileId || !upstream.authKind) { return { kind: 'none', label: tKey('wizard.step2.auth_none') }; } if (upstream.authKind === 'bearer') { return { kind: 'bearer', label: tKey('wizard.step2.auth_bearer') }; } if (upstream.authKind === 'basic') { return { kind: 'basic', label: tKey('wizard.step2.auth_basic') }; } return { kind: 'apikey', label: tKey('wizard.step2.auth_apikey') }; } function upstreamMeta(upstream) { var auth = inferUpstreamAuth(upstream); return { kind: auth.kind, label: auth.label, }; } async function loadWizardAuthResources() { if (!wizardWorkspaceId || !window.CrankApi) { wizardSecrets = []; wizardAuthProfiles = []; renderAuthProfileOptions(); return; } try { var results = await Promise.all([ window.CrankApi.listSecrets(wizardWorkspaceId), window.CrankApi.listAuthProfiles(wizardWorkspaceId), ]); wizardSecrets = (results[0] && results[0].items) || []; wizardAuthProfiles = (results[1] && results[1].items) || []; } catch (_error) { wizardSecrets = []; wizardAuthProfiles = []; } upstreams.forEach(syncUpstreamAuthMetadata); renderAuthProfileOptions(); } function renderAuthProfileOptions() { var profileSelect = document.getElementById('new-upstream-auth-profile'); var secretSelect = document.getElementById('new-auth-profile-secret-id'); var usernameSelect = document.getElementById('new-auth-profile-username-secret'); var passwordSelect = document.getElementById('new-auth-profile-password-secret'); var profileHint = document.getElementById('new-upstream-auth-profile-hint'); if (profileSelect) { var previousProfile = profileSelect.value; profileSelect.innerHTML = ''; if (!wizardAuthProfiles.length) { var emptyProfile = document.createElement('option'); emptyProfile.value = ''; emptyProfile.textContent = tKey('wizard.step2.auth_profile_empty'); profileSelect.appendChild(emptyProfile); if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_empty'); } else { var placeholder = document.createElement('option'); placeholder.value = ''; placeholder.textContent = tKey('wizard.step2.auth_profile'); profileSelect.appendChild(placeholder); wizardAuthProfiles.forEach(function(profile) { var option = document.createElement('option'); option.value = profile.id; option.textContent = profile.name + ' · ' + profile.kind; profileSelect.appendChild(option); }); if (previousProfile) { profileSelect.value = previousProfile; } if (profileHint) profileHint.textContent = tKey('wizard.step2.auth_profile_hint'); } } [secretSelect, usernameSelect, passwordSelect].forEach(function(select) { if (!select) return; var previous = select.value; select.innerHTML = ''; var placeholder = document.createElement('option'); placeholder.value = ''; placeholder.textContent = tKey('wizard.step2.secret_value'); select.appendChild(placeholder); wizardSecrets.forEach(function(secret) { var option = document.createElement('option'); option.value = secret.id; option.textContent = secret.name + ' · ' + secret.kind; select.appendChild(option); }); if (previous) { select.value = previous; } }); } function updateAuthProfileCreateUi() { var kind = textValue('new-auth-profile-kind') || 'bearer'; var headerGroup = document.getElementById('auth-profile-header-name-group'); var queryGroup = document.getElementById('auth-profile-query-param-group'); var basicRow = document.getElementById('auth-profile-basic-secret-row'); var singleSecretGroup = document.getElementById('auth-profile-single-secret-group'); if (headerGroup) { headerGroup.style.display = (kind === 'bearer' || kind === 'api_key_header') ? '' : 'none'; } if (queryGroup) { queryGroup.style.display = kind === 'api_key_query' ? '' : 'none'; } if (basicRow) { basicRow.style.display = kind === 'basic' ? '' : 'none'; } if (singleSecretGroup) { singleSecretGroup.style.display = kind === 'basic' ? 'none' : ''; } } function updateUpstreamAuthUi() { var mode = textValue('new-upstream-auth-mode') || 'none'; var existingGroup = document.getElementById('upstream-auth-existing-group'); var createGroup = document.getElementById('upstream-auth-create-group'); if (existingGroup) existingGroup.style.display = mode === 'existing' ? '' : 'none'; if (createGroup) createGroup.style.display = mode === 'create' ? '' : 'none'; updateAuthProfileCreateUi(); } function renderDropdownList(filter) { var list = document.getElementById('upstream-dropdown-list'); if (!list) return; var q = (filter || '').toLowerCase(); var filtered = upstreams.filter(function(u) { return !q || u.name.toLowerCase().includes(q) || u.url.toLowerCase().includes(q); }); if (filtered.length === 0) { list.innerHTML = '
' + tfKey('agents.drawer.ops_no_match', { query: escapeHtml(filter) }) + '
'; return; } var tmpl = document.getElementById('tmpl-upstream-item'); list.innerHTML = ''; filtered.forEach(function(u) { var sel = u.id === selectedUpstreamId; var meta = upstreamMeta(u); var node = tmpl.content.cloneNode(true); var item = node.querySelector('.upstream-dropdown-item'); if (sel) item.classList.add('selected'); item.addEventListener('click', function() { pickUpstream(u.id); }); node.querySelector('.upstream-dropdown-item-name').textContent = u.name; node.querySelector('.upstream-dropdown-item-url').textContent = u.url; var badge = node.querySelector('.upstream-auth-badge'); badge.textContent = meta.label; badge.className = 'upstream-auth-badge auth-' + meta.kind; list.appendChild(node); }); } function openUpstreamDropdown() { var combobox = document.getElementById('upstream-combobox'); var dropdown = document.getElementById('upstream-dropdown'); var search = document.getElementById('upstream-search'); if (!combobox || !dropdown) return; combobox.classList.add('open'); dropdown.style.display = ''; dropdownOpen = true; renderDropdownList(''); if (search) { search.value = ''; search.focus(); } // Close new-upstream form if open var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; } function closeUpstreamDropdown() { var combobox = document.getElementById('upstream-combobox'); var dropdown = document.getElementById('upstream-dropdown'); if (!combobox || !dropdown) return; combobox.classList.remove('open'); dropdown.style.display = 'none'; dropdownOpen = false; } function toggleUpstreamDropdown(e) { e.stopPropagation(); if (dropdownOpen) { closeUpstreamDropdown(); } else { openUpstreamDropdown(); } } function filterUpstreams(val) { renderDropdownList(val); } function pickUpstream(id) { selectedUpstreamId = id; editingUpstreamId = null; closeUpstreamDropdown(); var u = upstreams.find(function(x) { return x.id === id; }); if (!u) return; // Update combobox trigger value var val = document.getElementById('upstream-combobox-value'); if (val) { var trigTmpl = document.getElementById('tmpl-upstream-trigger-value'); if (trigTmpl) { val.innerHTML = ''; var trigNode = trigTmpl.content.cloneNode(true); trigNode.querySelector('.upstream-trigger-name').textContent = u.name; trigNode.querySelector('.upstream-trigger-url').textContent = u.url; val.appendChild(trigNode); } else { val.innerHTML = '
' + escapeHtml(u.name) + '
' + '
' + escapeHtml(u.url) + '
'; } } // Show preview strip var preview = document.getElementById('upstream-preview'); var pName = document.getElementById('upstream-preview-name'); var pUrl = document.getElementById('upstream-preview-url'); var pBadge = document.getElementById('upstream-preview-badge'); if (preview) { var meta = upstreamMeta(u); pName.textContent = u.name; pUrl.textContent = u.url; pBadge.textContent = meta.label; pBadge.className = 'upstream-auth-badge auth-' + meta.kind; preview.style.display = 'flex'; } // Hide new-upstream form and reset trigger var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; var trigger = document.getElementById('upstream-new-trigger'); if (trigger) trigger.classList.remove('active'); // Update reflection panel upstream info strip var reflectName = document.getElementById('grpc-reflect-upstream-name'); var reflectUrl = document.getElementById('grpc-reflect-upstream-url'); if (reflectName) reflectName.textContent = u.name; if (reflectUrl) reflectUrl.textContent = u.url; } function startNewUpstream() { closeUpstreamDropdown(); editingUpstreamId = null; var trigger = document.getElementById('upstream-new-trigger'); var form = document.getElementById('upstream-new-form'); var isOpen = form && form.style.display !== 'none'; if (isOpen) { // toggle off — restore previous state if (form) form.style.display = 'none'; if (trigger) trigger.classList.remove('active'); return; } // Clear combobox trigger & hide preview var val = document.getElementById('upstream-combobox-value'); if (val) val.innerHTML = '' + tKey('wizard.step2.upstream_placeholder') + ''; var preview = document.getElementById('upstream-preview'); if (preview) preview.style.display = 'none'; selectedUpstreamId = null; // Mark trigger active and show form if (trigger) trigger.classList.add('active'); if (form) { form.style.display = ''; setValue('new-upstream-name', ''); setValue('new-upstream-url', ''); setValue('new-upstream-static-headers', '{\n}'); setValue('new-upstream-auth-mode', 'none'); setValue('new-upstream-auth-profile', ''); setValue('new-auth-profile-name', ''); setValue('new-auth-profile-kind', 'bearer'); setValue('new-auth-profile-header-name', 'Authorization'); setValue('new-auth-profile-query-param', 'api_key'); setValue('new-auth-profile-secret-id', ''); setValue('new-auth-profile-username-secret', ''); setValue('new-auth-profile-password-secret', ''); updateUpstreamAuthUi(); var first = form.querySelector('input'); if (first) setTimeout(function() { first.focus(); }, 50); } } function beginEditSelectedUpstream(e) { if (e) e.stopPropagation(); var upstream = upstreams.find(function(item) { return item.id === selectedUpstreamId; }); if (!upstream) return; closeUpstreamDropdown(); editingUpstreamId = upstream.id; var trigger = document.getElementById('upstream-new-trigger'); var form = document.getElementById('upstream-new-form'); var preview = document.getElementById('upstream-preview'); if (trigger) trigger.classList.add('active'); if (preview) preview.style.display = 'none'; if (form) { form.style.display = ''; setValue('new-upstream-name', upstream.name); setValue('new-upstream-url', upstream.url); setValue('new-upstream-static-headers', upstream.staticHeaders || '{\n}'); setValue('new-upstream-auth-profile', upstream.authProfileId || ''); setValue('new-upstream-auth-mode', upstream.authProfileId ? 'existing' : 'none'); updateUpstreamAuthUi(); } } function buildAuthProfileConfig(kind) { if (kind === 'bearer') { var bearerHeader = textValue('new-auth-profile-header-name') || 'Authorization'; var bearerSecretId = textValue('new-auth-profile-secret-id'); if (!bearerHeader) throw new Error(tKey('wizard.error.header_name_required')); if (!bearerSecretId) throw new Error(tKey('wizard.error.secret_required')); return { bearer: { header_name: bearerHeader, secret_id: bearerSecretId, } }; } if (kind === 'basic') { var usernameSecretId = textValue('new-auth-profile-username-secret'); var passwordSecretId = textValue('new-auth-profile-password-secret'); if (!usernameSecretId || !passwordSecretId) throw new Error(tKey('wizard.error.basic_secrets_required')); return { basic: { username_secret_id: usernameSecretId, password_secret_id: passwordSecretId, } }; } if (kind === 'api_key_header') { var headerName = textValue('new-auth-profile-header-name'); var headerSecretId = textValue('new-auth-profile-secret-id'); if (!headerName) throw new Error(tKey('wizard.error.header_name_required')); if (!headerSecretId) throw new Error(tKey('wizard.error.secret_required')); return { api_key_header: { header_name: headerName, secret_id: headerSecretId, } }; } var queryParam = textValue('new-auth-profile-query-param'); var querySecretId = textValue('new-auth-profile-secret-id'); if (!queryParam) throw new Error(tKey('wizard.error.query_param_required')); if (!querySecretId) throw new Error(tKey('wizard.error.secret_required')); return { api_key_query: { param_name: queryParam, secret_id: querySecretId, } }; } async function createAuthProfileFromForm() { var name = textValue('new-auth-profile-name'); var kind = textValue('new-auth-profile-kind') || 'bearer'; if (!name) throw new Error(tKey('wizard.error.auth_profile_name')); var profile = await window.CrankApi.createAuthProfile(wizardWorkspaceId, { name: name, kind: kind, config: buildAuthProfileConfig(kind), }); wizardAuthProfiles.push(profile); renderAuthProfileOptions(); if (window.CrankUi) { window.CrankUi.success( tKey('wizard.toast.auth_profile_created_body'), tKey('wizard.toast.auth_profile_created_title') ); } return profile; } async function saveNewUpstream(e) { e.stopPropagation(); try { var nameEl = document.getElementById('new-upstream-name'); var urlEl = document.getElementById('new-upstream-url'); var name = nameEl ? nameEl.value.trim() : ''; var url = urlEl ? urlEl.value.trim() : ''; if (!name) { if (nameEl) nameEl.focus(); return; } if (!url) { if (urlEl) urlEl.focus(); return; } var staticHeadersEl = document.getElementById('new-upstream-static-headers'); var staticHeaders = staticHeadersEl ? staticHeadersEl.value : '{\n}'; parseHeaderMap(staticHeaders); var authMode = textValue('new-upstream-auth-mode') || 'none'; var authProfileId = null; var authProfileName = ''; var authKind = null; if (authMode === 'existing') { authProfileId = textValue('new-upstream-auth-profile'); if (!authProfileId) throw new Error(tKey('wizard.error.auth_profile_required')); var existingProfile = authProfileById(authProfileId); authProfileName = existingProfile ? existingProfile.name : ''; authKind = existingProfile ? existingProfile.kind : null; } else if (authMode === 'create') { var createdProfile = await createAuthProfileFromForm(); authProfileId = createdProfile.id; authProfileName = createdProfile.name; authKind = createdProfile.kind; setValue('new-upstream-auth-profile', createdProfile.id); setValue('new-upstream-auth-mode', 'existing'); } var nextRecord = { id: editingUpstreamId || ('custom-' + Date.now()), name: name, url: url, staticHeaders: staticHeaders, authProfileId: authProfileId, authProfileName: authProfileName, authKind: authKind, }; if (editingUpstreamId) { upstreams = upstreams.map(function(item) { return item.id === editingUpstreamId ? nextRecord : item; }); } else { upstreams.push(nextRecord); } if (nameEl) nameEl.value = ''; if (urlEl) urlEl.value = ''; if (staticHeadersEl) staticHeadersEl.value = '{\n}'; var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; editingUpstreamId = null; pickUpstream(nextRecord.id); } catch (error) { if (window.CrankUi) { window.CrankUi.error( error.message || tKey('wizard.toast.auth_profile_error_title'), tKey('wizard.toast.auth_profile_error_title') ); } } } function cancelNewUpstream(e) { e.stopPropagation(); var form = document.getElementById('upstream-new-form'); if (form) form.style.display = 'none'; var trigger = document.getElementById('upstream-new-trigger'); if (trigger) trigger.classList.remove('active'); editingUpstreamId = null; } function openQuickSecretModal(e) { if (e) e.preventDefault(); var modal = document.getElementById('quick-secret-modal'); if (!modal) return; setValue('quick-secret-name', ''); setValue('quick-secret-value', ''); setValue('quick-secret-kind', 'token'); modal.classList.add('open'); setTimeout(function() { var input = document.getElementById('quick-secret-name'); if (input) input.focus(); }, 30); } function closeQuickSecretModal() { var modal = document.getElementById('quick-secret-modal'); if (modal) modal.classList.remove('open'); } async function submitQuickSecret() { var name = textValue('quick-secret-name'); var value = textValue('quick-secret-value'); var kind = textValue('quick-secret-kind') || 'token'; if (!name) throw new Error(tKey('wizard.error.secret_name_required')); if (!value) throw new Error(tKey('wizard.error.secret_value_required')); var secret = await window.CrankApi.createSecret(wizardWorkspaceId, { name: name, kind: kind, value: value, }); wizardSecrets.push(secret); renderAuthProfileOptions(); var primarySelect = document.getElementById('new-auth-profile-secret-id'); var usernameSelect = document.getElementById('new-auth-profile-username-secret'); var passwordSelect = document.getElementById('new-auth-profile-password-secret'); if (primarySelect && primarySelect.offsetParent !== null) { primarySelect.value = secret.id; } else if (usernameSelect && !usernameSelect.value) { usernameSelect.value = secret.id; } else if (passwordSelect) { passwordSelect.value = secret.id; } closeQuickSecretModal(); if (window.CrankUi) { window.CrankUi.success( tKey('wizard.toast.quick_secret_created_body'), tKey('wizard.toast.quick_secret_created_title') ); } } function escapeHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // Close dropdown on outside click document.addEventListener('click', function() { if (dropdownOpen) closeUpstreamDropdown(); }); /* ══════════════════════════════════════════════ gRPC — source selector ══════════════════════════════════════════════ */ var grpcSource = 'proto'; // 'proto' | 'reflection' | 'manual' function selectGrpcSource(source) { grpcSource = source; document.querySelectorAll('.grpc-source-btn').forEach(function(btn) { btn.classList.toggle('active', btn.dataset.source === source); }); document.getElementById('grpc-src-proto').style.display = source === 'proto' ? '' : 'none'; document.getElementById('grpc-src-reflection').style.display = source === 'reflection' ? '' : 'none'; document.getElementById('grpc-src-manual').style.display = source === 'manual' ? '' : 'none'; // hide shared detail when switching sources var detail = document.getElementById('grpc-method-detail'); if (detail) detail.style.display = 'none'; selectedRpcMethod = null; } /* ══════════════════════════════════════════════ gRPC — Proto file parser & UI ══════════════════════════════════════════════ */ var protoParsed = null; // { package, services, messages, streamingCount } var selectedRpcMethod = null; // { service, method, request, response } /* Extract top-level blocks with proper brace matching */ function extractBlocks(text, keyword) { var blocks = []; var re = new RegExp(keyword + '\\s+(\\w+)\\s*\\{', 'g'); var match; while ((match = re.exec(text)) !== null) { var name = match[1]; var start = match.index + match[0].length; var depth = 1; var i = start; while (i < text.length && depth > 0) { if (text[i] === '{') depth++; else if (text[i] === '}') depth--; i++; } blocks.push({ name: name, body: text.slice(start, i - 1), start: match.index }); } return blocks; } /* Strip single-line and block comments */ function stripProtoComments(text) { // block comments text = text.replace(/\/\*[\s\S]*?\*\//g, ''); // line comments text = text.replace(/\/\/[^\n]*/g, ''); return text; } /* Parse a proto text → { package, services, messages, streamingCount } */ function parseProto(text) { text = stripProtoComments(text); var pkg = ''; var pkgMatch = text.match(/\bpackage\s+([\w.]+)\s*;/); if (pkgMatch) pkg = pkgMatch[1]; // messages: map name → [{ repeated, type, name }] var messages = {}; extractBlocks(text, 'message').forEach(function(blk) { var fields = []; var fieldRe = /\b(repeated\s+)?([\w.]+)\s+(\w+)\s*=\s*\d+/g; var fm; while ((fm = fieldRe.exec(blk.body)) !== null) { // skip reserved / map keywords if (['option','reserved','extensions','oneof'].indexOf(fm[2]) !== -1) continue; fields.push({ repeated: !!fm[1], type: fm[2], name: fm[3] }); } messages[blk.name] = fields; }); var streamingCount = 0; var services = []; extractBlocks(text, 'service').forEach(function(svc) { var methods = []; // rpc Name ( [stream] ReqType ) returns ( [stream] ResType ) { } var rpcRe = /\brpc\s+(\w+)\s*\(\s*(stream\s+)?([\w.]+)\s*\)\s*returns\s*\(\s*(stream\s+)?([\w.]+)\s*\)/g; var rm; while ((rm = rpcRe.exec(svc.body)) !== null) { var streamIn = !!rm[2]; var streamOut = !!rm[4]; if (streamIn || streamOut) { streamingCount++; } else { // Strip package prefix from type names for display var reqType = rm[3].split('.').pop(); var resType = rm[5].split('.').pop(); methods.push({ name: rm[1], request: reqType, response: resType }); } } if (methods.length > 0) { services.push({ name: svc.name, methods: methods }); } }); return { package: pkg, services: services, messages: messages, streamingCount: streamingCount }; } /* Generic renderer: fills a service/method list into given element IDs */ function renderServiceResults(parsed, ids) { protoParsed = parsed; selectedRpcMethod = null; var notice = document.getElementById(ids.notice); var countEl = document.getElementById(ids.count); if (notice) { notice.style.display = parsed.streamingCount > 0 ? '' : 'none'; if (countEl && parsed.streamingCount > 0) countEl.textContent = parsed.streamingCount; } var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0); var summaryEl = document.getElementById(ids.summary); if (summaryEl) { summaryEl.textContent = tfKey('wizard.grpc.services_found', { services: parsed.services.length, methods: totalMethods, }); } var listEl = document.getElementById(ids.list); if (!listEl) return; listEl.innerHTML = ''; if (parsed.services.length === 0) { var emptyDiv = document.createElement('div'); emptyDiv.className = 'proto-empty'; emptyDiv.textContent = tKey('wizard.grpc.no_methods'); listEl.appendChild(emptyDiv); } else { var svcTmpl = document.getElementById('tmpl-proto-service-group'); var methodTmpl = document.getElementById('tmpl-proto-method-card'); parsed.services.forEach(function(svc) { var svcNode = svcTmpl.content.cloneNode(true); svcNode.querySelector('.proto-service-name').textContent = svc.name; var grid = svcNode.querySelector('.proto-method-grid'); svc.methods.forEach(function(m) { var mNode = methodTmpl.content.cloneNode(true); var btn = mNode.querySelector('.proto-method-card'); btn.dataset.service = svc.name; btn.dataset.method = m.name; btn.dataset.req = m.request; btn.dataset.res = m.response; mNode.querySelector('.proto-method-name').textContent = m.name; mNode.querySelector('.proto-req-type').textContent = m.request; mNode.querySelector('.proto-res-type').textContent = m.response; grid.appendChild(mNode); }); listEl.appendChild(svcNode); }); } document.getElementById(ids.view).style.display = ''; document.getElementById('grpc-method-detail').style.display = 'none'; } function renderParsedProto(parsed) { var notice = document.getElementById('proto-streaming-text'); if (notice && parsed && parsed.streamingCount) { notice.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount }); } renderServiceResults(parsed, { notice: 'proto-streaming-notice', count: 'proto-streaming-count', summary: 'proto-services-summary', list: 'proto-services-list', view: 'proto-parsed-view', }); } function renderReflectionResult(parsed) { var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text'); if (reflectText && parsed && parsed.streamingCount) { reflectText.textContent = tfKey('wizard.step3.grpc.streaming_hidden', { count: parsed.streamingCount }); } renderServiceResults(parsed, { notice: 'reflect-streaming-notice', count: 'reflect-streaming-count', summary: 'reflect-services-summary', list: 'reflect-services-list', view: 'reflect-parsed-view', }); } /* Select a method card (shared between proto + reflection) */ function selectRpcMethod(btn) { document.querySelectorAll('.proto-method-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); var svcName = btn.dataset.service; var methodName = btn.dataset.method; var reqType = btn.dataset.req; var resType = btn.dataset.res; selectedRpcMethod = { service: svcName, method: methodName, request: reqType, response: resType }; var pkg = protoParsed && protoParsed.package ? protoParsed.package + '.' : ''; var grpcPath = '/' + pkg + svcName + '/' + methodName; var pathEl = document.getElementById('grpc-path-value'); if (pathEl) pathEl.textContent = grpcPath; var sub = document.getElementById('grpc-method-subtitle'); if (sub) sub.textContent = 'rpc ' + methodName + ' (' + reqType + ') returns (' + resType + ')'; var reqTypeEl = document.getElementById('grpc-req-type'); var resTypeEl = document.getElementById('grpc-res-type'); if (reqTypeEl) reqTypeEl.textContent = reqType; if (resTypeEl) resTypeEl.textContent = resType; var sigGrid = document.getElementById('grpc-detail-sig'); if (sigGrid) sigGrid.style.display = ''; renderFieldList('grpc-req-fields', reqType); renderFieldList('grpc-res-fields', resType); document.getElementById('grpc-method-detail').style.display = ''; } function renderFieldList(elId, typeName) { var el = document.getElementById(elId); if (!el) return; var fields = protoParsed && protoParsed.messages[typeName]; el.innerHTML = ''; if (!fields || fields.length === 0) { var emptyEl = document.createElement('div'); emptyEl.className = 'proto-field-empty'; emptyEl.innerHTML = tKey('wizard.grpc.fields_unresolved') + '
' + tKey('wizard.grpc.fields_unresolved_body') + ''; el.appendChild(emptyEl); return; } var fieldTmpl = document.getElementById('tmpl-proto-field-row'); fields.forEach(function(f) { var node = fieldTmpl.content.cloneNode(true); node.querySelector('.proto-field-name').textContent = f.name; node.querySelector('.proto-field-type').textContent = (f.repeated ? 'repeated ' : '') + f.type; el.appendChild(node); }); } /* File handling */ function handleProtoFile(event) { var file = event.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { wizardProtoUpload = { fileName: file.name, content: e.target.result }; showProtoFileInfo(file.name, file.size); var parsed = parseProto(e.target.result); renderParsedProto(parsed); }; reader.readAsText(file); } function handleProtoDrop(event) { event.preventDefault(); var dropzone = document.getElementById('proto-dropzone'); if (dropzone) dropzone.classList.remove('drag-over'); var file = event.dataTransfer.files[0]; if (!file || !file.name.endsWith('.proto')) return; var reader = new FileReader(); reader.onload = function(e) { wizardProtoUpload = { fileName: file.name, content: e.target.result }; showProtoFileInfo(file.name, file.size); renderParsedProto(parseProto(e.target.result)); }; reader.readAsText(file); } function showProtoFileInfo(name, size) { var dropzone = document.getElementById('proto-dropzone'); var info = document.getElementById('proto-file-info'); var nameEl = document.getElementById('proto-file-name'); var sizeEl = document.getElementById('proto-file-size'); if (dropzone) dropzone.style.display = 'none'; if (info) info.style.display = ''; if (nameEl) nameEl.textContent = name; if (sizeEl) sizeEl.textContent = (size / 1024).toFixed(1) + ' KB'; // hide paste area document.getElementById('proto-paste-area').style.display = 'none'; document.getElementById('proto-paste-btn').textContent = 'Paste content'; } function resetProto() { var dropzone = document.getElementById('proto-dropzone'); var info = document.getElementById('proto-file-info'); var parsedView = document.getElementById('proto-parsed-view'); if (dropzone) dropzone.style.display = ''; if (info) info.style.display = 'none'; if (parsedView) parsedView.style.display = 'none'; var fileInput = document.getElementById('proto-file-input'); if (fileInput) fileInput.value = ''; protoParsed = null; selectedRpcMethod = null; wizardProtoUpload = null; } function toggleProtoPaste() { var area = document.getElementById('proto-paste-area'); var btn = document.getElementById('proto-paste-btn'); if (!area) return; var open = area.style.display !== 'none'; area.style.display = open ? 'none' : ''; if (btn) btn.textContent = open ? 'Paste content' : 'Cancel paste'; if (!open) { var ta = document.getElementById('proto-paste-input'); if (ta) setTimeout(function() { ta.focus(); }, 50); } } function cancelProtoPaste() { var area = document.getElementById('proto-paste-area'); var btn = document.getElementById('proto-paste-btn'); if (area) area.style.display = 'none'; if (btn) btn.textContent = 'Paste content'; } function parseProtoPasted() { var ta = document.getElementById('proto-paste-input'); if (!ta || !ta.value.trim()) return; var parsed = parseProto(ta.value); wizardProtoUpload = { fileName: 'pasted.proto', content: ta.value }; document.getElementById('proto-paste-area').style.display = 'none'; var dropzone = document.getElementById('proto-dropzone'); if (dropzone) dropzone.style.display = 'none'; var info = document.getElementById('proto-file-info'); var nameEl = document.getElementById('proto-file-name'); var sizeEl = document.getElementById('proto-file-size'); if (info) info.style.display = ''; if (nameEl) nameEl.textContent = 'pasted.proto'; if (sizeEl) sizeEl.textContent = (ta.value.length / 1024).toFixed(1) + ' KB'; renderParsedProto(parsed); } /* ══════════════════════════════════════════════ gRPC — Server reflection (simulated) ══════════════════════════════════════════════ */ var REFLECTION_MOCK = { package: 'acme.v1', streamingCount: 2, services: [ { name: 'ContactService', methods: [ { name: 'GetContact', request: 'GetContactRequest', response: 'Contact' }, { name: 'CreateContact', request: 'CreateContactRequest', response: 'Contact' }, { name: 'UpdateContact', request: 'UpdateContactRequest', response: 'Contact' }, { name: 'DeleteContact', request: 'DeleteContactRequest', response: 'DeleteContactResponse' }, ]}, { name: 'CompanyService', methods: [ { name: 'GetCompany', request: 'GetCompanyRequest', response: 'Company' }, { name: 'ListCompanies', request: 'ListCompaniesRequest', response: 'ListCompaniesResponse' }, ]}, ], messages: { 'GetContactRequest': [{ type: 'string', name: 'id' }], 'Contact': [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }, { type: 'string', name: 'created_at' }], 'CreateContactRequest': [{ type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }], 'UpdateContactRequest': [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }], 'DeleteContactRequest': [{ type: 'string', name: 'id' }], 'DeleteContactResponse': [{ type: 'bool', name: 'success' }], 'GetCompanyRequest': [{ type: 'string', name: 'id' }], 'Company': [{ type: 'string', name: 'id' }, { type: 'string', name: 'name' }, { type: 'string', name: 'domain' }, { type: 'int32', name: 'employee_count' }], 'ListCompaniesRequest': [{ type: 'int32', name: 'page' }, { type: 'int32', name: 'page_size' }, { type: 'string', name: 'query' }], 'ListCompaniesResponse': [{ type: 'Company', name: 'companies', repeated: true }, { type: 'int32', name: 'total' }], }, }; function startReflection() { goToStep(5); showWizardLiveStatus( tKey('wizard.grpc.discovery_title'), tKey('wizard.grpc.discovery_body') ); } /* ══════════════════════════════════════════════ gRPC — Manual entry ══════════════════════════════════════════════ */ function grpcManualRouteInput(val) { var detail = document.getElementById('grpc-method-detail'); var sigGrid = document.getElementById('grpc-detail-sig'); if (!val || !val.startsWith('/') || val.indexOf('/', 1) === -1) { if (detail) detail.style.display = 'none'; return; } // Parse service + method from route: /[pkg.]Service/Method var parts = val.replace(/^\//, '').split('/'); var servicePart = parts[0] || ''; var methodName = parts[1] || ''; var serviceName = servicePart.split('.').pop(); var pathEl = document.getElementById('grpc-path-value'); if (pathEl) pathEl.textContent = val; var sub = document.getElementById('grpc-method-subtitle'); if (sub) sub.textContent = serviceName + '/' + methodName; // hide sig grid in manual mode (no field data) if (sigGrid) sigGrid.style.display = 'none'; grpcManualTypeInput(); if (detail) detail.style.display = ''; } function grpcManualTypeInput() { var reqInput = document.getElementById('grpc-manual-req-type'); var resInput = document.getElementById('grpc-manual-res-type'); var reqTypeEl = document.getElementById('grpc-req-type'); var resTypeEl = document.getElementById('grpc-res-type'); if (reqTypeEl) reqTypeEl.textContent = (reqInput && reqInput.value.trim()) || '—'; if (resTypeEl) resTypeEl.textContent = (resInput && resInput.value.trim()) || '—'; } function textValue(id) { var element = document.getElementById(id); return element ? element.value.trim() : ''; } function setValue(id, value) { var element = document.getElementById(id); if (element) element.value = value || ''; } function parseStructuredText(text) { if (!text.trim()) return {}; try { return JSON.parse(text); } catch (_error) { if (!window.jsyaml) throw new Error(tKey('wizard.error.parser_unavailable')); return window.jsyaml.load(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 } }; } 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) 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', }; } 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 selectedStreamingMode() { var element = document.getElementById('streaming-mode'); return element ? element.value : 'unary'; } function transportOptionsForMode(mode) { var capabilities = currentProtocolCapabilities(); var behaviors = (capabilities.supports_transport_behaviors || []).slice(); if (mode === 'unary') return behaviors.filter(function(value) { return value === 'request_response'; }); if (mode === 'window') return behaviors.filter(function(value) { return value === 'request_response' || value === 'server_stream'; }); if (mode === 'session') return behaviors.filter(function(value) { return value === 'server_stream' || value === 'stateful_session'; }); if (mode === 'async_job') return behaviors.filter(function(value) { return value === 'deferred_result'; }); return behaviors; } function modeOptionsForProtocol() { var capabilities = currentProtocolCapabilities(); return (capabilities.supports_execution_modes || ['unary']).filter(function(mode) { return transportOptionsForMode(mode).length > 0 || mode === 'unary'; }); } function updateStreamingTransportOptions(preferredValue) { var element = document.getElementById('streaming-transport-behavior'); if (!element) return; var options = transportOptionsForMode(selectedStreamingMode()); element.innerHTML = ''; options.forEach(function(value) { var option = document.createElement('option'); option.value = value; option.textContent = tKey('wizard.streaming.transport.' + value); element.appendChild(option); }); if (preferredValue && options.indexOf(preferredValue) >= 0) { element.value = preferredValue; } else if (options.length) { element.value = options[0]; } } function updateStreamingModeOptions(preferredValue) { var element = document.getElementById('streaming-mode'); if (!element) return; var options = modeOptionsForProtocol(); element.innerHTML = ''; options.forEach(function(value) { var option = document.createElement('option'); option.value = value; option.textContent = tKey('wizard.streaming.mode.' + value); element.appendChild(option); }); if (preferredValue && options.indexOf(preferredValue) >= 0) { element.value = preferredValue; } else { element.value = options.indexOf('unary') >= 0 ? 'unary' : (options[0] || 'unary'); } updateStreamingTransportOptions(); } function ensureStreamingToolFamilyDefaults(mode) { var base = textValue('tool-name') || 'stream_tool'; if (mode === 'session') { if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start'); if (!textValue('streaming-poll-tool-name')) setValue('streaming-poll-tool-name', base + '_poll'); if (!textValue('streaming-stop-tool-name')) setValue('streaming-stop-tool-name', base + '_stop'); } if (mode === 'async_job') { if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start'); if (!textValue('streaming-status-tool-name')) setValue('streaming-status-tool-name', base + '_status'); if (!textValue('streaming-result-tool-name')) setValue('streaming-result-tool-name', base + '_result'); if (!textValue('streaming-cancel-tool-name')) setValue('streaming-cancel-tool-name', base + '_cancel'); } } function updateStreamingConfigVisibility() { var mode = selectedStreamingMode(); var card = document.getElementById('wizard-streaming-config-card'); var help = document.getElementById('streaming-mode-help'); var toolFamily = document.getElementById('streaming-tool-family-block'); var asyncRow = document.getElementById('streaming-async-tool-family-row'); if (!card) return; var isUnary = mode === 'unary'; var isWindow = mode === 'window'; var isSession = mode === 'session'; var isAsyncJob = mode === 'async_job'; [ 'streaming-window-duration-ms', 'streaming-poll-interval-ms', 'streaming-upstream-timeout-ms', 'streaming-idle-timeout-ms', 'streaming-session-lifetime-ms', 'streaming-max-items', 'streaming-max-bytes', 'streaming-max-field-length', 'streaming-sampling-rate', 'streaming-items-path', 'streaming-summary-path', 'streaming-done-path', 'streaming-cursor-path', 'streaming-status-path', 'streaming-redacted-paths', 'streaming-truncate-item-fields', 'streaming-drop-duplicates' ].forEach(function(id) { var node = document.getElementById(id); if (!node) return; var group = node.closest('.form-group') || node.closest('.checkbox-pill'); if (group) { group.style.display = isUnary ? 'none' : ''; } }); if (help) { help.textContent = isUnary ? tKey('wizard.streaming.help.unary') : (isSession ? tKey('wizard.streaming.help.session') : (isAsyncJob ? tKey('wizard.streaming.help.async_job') : tKey('wizard.streaming.help.window'))); } if (toolFamily) toolFamily.style.display = (isSession || isAsyncJob) ? '' : 'none'; if (asyncRow) asyncRow.style.display = isAsyncJob ? '' : 'none'; ensureStreamingToolFamilyDefaults(mode); } function bindStreamingConfigControls() { updateStreamingModeOptions(); updateStreamingConfigVisibility(); var mode = document.getElementById('streaming-mode'); if (mode) { mode.addEventListener('change', function() { updateStreamingTransportOptions(); updateStreamingConfigVisibility(); }); } } function collectStreamingConfig() { var mode = selectedStreamingMode(); if (mode === 'unary') return null; var config = { mode: mode, transport_behavior: stringValueOrNull('streaming-transport-behavior') || 'request_response', window_duration_ms: numericValueOrNull('streaming-window-duration-ms'), poll_interval_ms: numericValueOrNull('streaming-poll-interval-ms'), upstream_timeout_ms: numericValueOrNull('streaming-upstream-timeout-ms'), idle_timeout_ms: numericValueOrNull('streaming-idle-timeout-ms'), max_session_lifetime_ms: numericValueOrNull('streaming-session-lifetime-ms'), max_items: numericValueOrNull('streaming-max-items'), max_bytes: numericValueOrNull('streaming-max-bytes'), aggregation_mode: stringValueOrNull('streaming-aggregation-mode') || 'summary_plus_samples', summary_path: stringValueOrNull('streaming-summary-path'), items_path: stringValueOrNull('streaming-items-path'), cursor_path: stringValueOrNull('streaming-cursor-path'), status_path: stringValueOrNull('streaming-status-path'), done_path: stringValueOrNull('streaming-done-path'), redacted_paths: textareaLines('streaming-redacted-paths'), truncate_item_fields: !!document.getElementById('streaming-truncate-item-fields').checked, max_field_length: numericValueOrNull('streaming-max-field-length'), drop_duplicates: !!document.getElementById('streaming-drop-duplicates').checked, sampling_rate: (function() { var value = textValue('streaming-sampling-rate'); if (!value) return null; var parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; }()), tool_family: {}, }; if (mode === 'session') { config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name'); config.tool_family.poll_tool_name = stringValueOrNull('streaming-poll-tool-name'); config.tool_family.stop_tool_name = stringValueOrNull('streaming-stop-tool-name'); } else if (mode === 'async_job') { config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name'); config.tool_family.status_tool_name = stringValueOrNull('streaming-status-tool-name'); config.tool_family.result_tool_name = stringValueOrNull('streaming-result-tool-name'); config.tool_family.cancel_tool_name = stringValueOrNull('streaming-cancel-tool-name'); } Object.keys(config).forEach(function(key) { if (config[key] === null) delete config[key]; }); if (!config.redacted_paths.length) delete config.redacted_paths; return config; } function applyStreamingConfig(streaming) { updateStreamingModeOptions(streaming && streaming.mode ? streaming.mode : 'unary'); if (!streaming) { updateStreamingConfigVisibility(); return; } setValue('streaming-window-duration-ms', streaming.window_duration_ms || ''); setValue('streaming-poll-interval-ms', streaming.poll_interval_ms || ''); setValue('streaming-upstream-timeout-ms', streaming.upstream_timeout_ms || ''); setValue('streaming-idle-timeout-ms', streaming.idle_timeout_ms || ''); setValue('streaming-session-lifetime-ms', streaming.max_session_lifetime_ms || ''); setValue('streaming-max-items', streaming.max_items || ''); setValue('streaming-max-bytes', streaming.max_bytes || ''); setValue('streaming-max-field-length', streaming.max_field_length || ''); setValue('streaming-sampling-rate', streaming.sampling_rate || ''); setValue('streaming-items-path', streaming.items_path || ''); setValue('streaming-summary-path', streaming.summary_path || ''); setValue('streaming-done-path', streaming.done_path || ''); setValue('streaming-cursor-path', streaming.cursor_path || ''); setValue('streaming-status-path', streaming.status_path || ''); setValue('streaming-redacted-paths', (streaming.redacted_paths || []).join('\n')); document.getElementById('streaming-truncate-item-fields').checked = !!streaming.truncate_item_fields; document.getElementById('streaming-drop-duplicates').checked = !!streaming.drop_duplicates; updateStreamingTransportOptions(streaming.transport_behavior); setValue('streaming-start-tool-name', streaming.tool_family && streaming.tool_family.start_tool_name ? streaming.tool_family.start_tool_name : ''); setValue('streaming-poll-tool-name', streaming.tool_family && streaming.tool_family.poll_tool_name ? streaming.tool_family.poll_tool_name : ''); setValue('streaming-stop-tool-name', streaming.tool_family && streaming.tool_family.stop_tool_name ? streaming.tool_family.stop_tool_name : ''); setValue('streaming-status-tool-name', streaming.tool_family && streaming.tool_family.status_tool_name ? streaming.tool_family.status_tool_name : ''); setValue('streaming-result-tool-name', streaming.tool_family && streaming.tool_family.result_tool_name ? streaming.tool_family.result_tool_name : ''); setValue('streaming-cancel-tool-name', streaming.tool_family && streaming.tool_family.cancel_tool_name ? streaming.tool_family.cancel_tool_name : ''); var aggregation = document.getElementById('streaming-aggregation-mode'); if (aggregation && streaming.aggregation_mode) aggregation.value = streaming.aggregation_mode; updateStreamingConfigVisibility(); } function buildToolDescription() { return { title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'), description: textValue('tool-description'), tags: [], examples: [], }; } function collectWizardPayload() { var name = textValue('tool-name'); if (!name) throw new Error(tKey('wizard.error.tool_name')); var inputSchemaValue = parseStructuredText(textValue('tool-input-schema')); var outputSchemaValue = parseStructuredText(textValue('tool-output-schema')); var inputMappingValue = parseStructuredText(textValue('tool-input-mapping')); var outputMappingValue = parseStructuredText(textValue('tool-output-mapping')); return { name: name, display_name: textValue('tool-display-name') || name, category: 'general', protocol: wizardProtocol, target: buildTarget(), input_schema: convertJsonSchemaToCrankSchema(inputSchemaValue, []), output_schema: convertJsonSchemaToCrankSchema(outputSchemaValue, []), input_mapping: buildMappingSet(inputMappingValue, 'input'), output_mapping: buildMappingSet(outputMappingValue, 'output'), execution_config: parseExecutionConfig(textValue('tool-exec-config')), tool_description: buildToolDescription(), }; } async function saveOperation(stayOnPage) { try { await persistCurrentDraft(stayOnPage); } catch (error) { if (window.CrankUi) { window.CrankUi.error(error.message || tKey('wizard.error.save'), tKey('wizard.error.save_title')); } } } async function loadOperationForEdit() { if (!wizardWorkspaceId || !wizardEditId) return; var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId); wizardProtocol = detail.protocol || 'rest'; await loadWizardPanels([3]); var draftVersion = await window.CrankApi.getOperationVersion( wizardWorkspaceId, wizardEditId, detail.draft_version_ref.version ); wizardCurrentOperation = detail; wizardCurrentVersion = draftVersion; prefillWizardFromEdit(detail, draftVersion); } function setEditModePresentation() { var progressLabel = document.querySelector('.progress-label'); if (progressLabel) progressLabel.textContent = tKey('wizard.progress.edit'); var sidebarBrand = document.querySelector('.step-sidebar-brand'); if (sidebarBrand) sidebarBrand.innerHTML = tKey('wizard.sidebar.brand.edit'); } 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'); var selected = proto === wizardProtocol; card.classList.toggle('selected', selected); card.setAttribute('aria-checked', selected ? 'true' : 'false'); }); var s3name = document.getElementById('sidebar-step-3-name'); if (s3name) s3name.textContent = STEP3_LABELS[wizardProtocol] || tKey('wizard.step3.rest.label'); 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.style.display = ''; 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 = '
imported-upstream
' + '
' + escapeHtml(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 }; } 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(); } 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); bindLiveAction('wizard-generate-draft', tKey('wizard.busy.generate'), generateDraftFromWizard); bindLiveAction('wizard-run-test', tKey('wizard.busy.test'), runWizardTest); bindClick('wizard-copy-test-response', copyTestResponseToOutputSample); bindLiveAction('wizard-export-yaml', tKey('wizard.busy.export_yaml'), exportWizardYaml); bindLiveAction('wizard-import-yaml', tKey('wizard.busy.import_yaml'), importWizardYaml); bindLiveAction('wizard-publish-operation', tKey('wizard.busy.publish'), publishWizardOperation); bindClick('wizard-select-descriptor-set', function() { var input = document.getElementById('wizard-descriptor-set-input'); if (input) input.click(); }); bindLiveAction('wizard-upload-descriptor-set', tKey('wizard.busy.descriptor'), uploadDescriptorSetAndDiscover); bindClick('wizard-import-yaml-file-trigger', function() { var input = document.getElementById('wizard-import-yaml-file'); if (input) input.click(); }); var descriptorInput = document.getElementById('wizard-descriptor-set-input'); if (descriptorInput) descriptorInput.addEventListener('change', handleDescriptorSetSelection); var yamlInput = document.getElementById('wizard-import-yaml-file'); if (yamlInput) yamlInput.addEventListener('change', handleImportYamlFileSelection); } function bindClick(id, handler) { var element = document.getElementById(id); if (!element) return; element.addEventListener('click', function(event) { event.preventDefault(); handler(); }); } function bindLiveAction(id, busyLabel, handler) { var element = document.getElementById(id); if (!element) return; element.addEventListener('click', function(event) { event.preventDefault(); runWizardLiveAction(element, busyLabel, handler); }); } async function runWizardLiveAction(button, busyLabel, handler) { if (!button || button.dataset.busy === 'true') { return; } var originalLabel = button.textContent; button.dataset.busy = 'true'; button.disabled = true; button.classList.add('is-busy'); button.textContent = busyLabel; try { await handler(); } catch (error) { showWizardLiveStatus( tKey('wizard.live.failed_title'), error && error.message ? error.message : tKey('wizard.live.failed_body'), true ); if (window.CrankUi) { window.CrankUi.error( error && error.message ? error.message : tKey('wizard.live.failed_body'), tKey('wizard.live.failed_title') ); } } finally { button.dataset.busy = 'false'; button.disabled = false; button.classList.remove('is-busy'); button.textContent = originalLabel; } } function updateWizardProtocolVisibility() { var grpcTools = document.getElementById('wizard-grpc-live-tools'); if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none'; updateStreamingModeOptions(); updateStreamingConfigVisibility(); } function showWizardLiveStatus(title, text, isError) { var root = document.getElementById('wizard-live-status'); var titleEl = document.getElementById('wizard-live-status-title'); var textEl = document.getElementById('wizard-live-status-text'); if (!root || !titleEl || !textEl) return; titleEl.textContent = title; textEl.textContent = text; root.style.display = ''; root.classList.toggle('is-error', !!isError); root.classList.toggle('is-success', !isError); } function currentDraftVersion() { if (wizardCurrentOperation && wizardCurrentOperation.draft_version_ref) { return wizardCurrentOperation.draft_version_ref.version; } if (wizardCurrentOperation && wizardCurrentOperation.current_draft_version) { return wizardCurrentOperation.current_draft_version; } if (wizardCurrentVersion && wizardCurrentVersion.version) { return wizardCurrentVersion.version; } return 1; } function updateOperationPayload(payload) { return { display_name: payload.display_name, category: payload.category, target: payload.target, input_schema: payload.input_schema, output_schema: payload.output_schema, input_mapping: payload.input_mapping, output_mapping: payload.output_mapping, execution_config: payload.execution_config, tool_description: payload.tool_description, }; } async function refreshCurrentOperationState() { if (!wizardWorkspaceId || !wizardEditId) return null; var detail = await window.CrankApi.getOperation(wizardWorkspaceId, wizardEditId); var versionNumber = detail.draft_version_ref ? detail.draft_version_ref.version : detail.current_draft_version; var versionDocument = await window.CrankApi.getOperationVersion( wizardWorkspaceId, wizardEditId, versionNumber ); wizardCurrentOperation = detail; wizardCurrentVersion = versionDocument; updateWizardProtocolVisibility(); return { detail: detail, version: versionDocument, }; } async function uploadPendingGrpcArtifacts(payload) { if (wizardProtocol !== 'grpc' || !wizardEditId) return; var descriptorResponse = null; if (wizardProtoUpload) { descriptorResponse = await window.CrankApi.uploadProtoFile( wizardWorkspaceId, wizardEditId, new TextEncoder().encode(wizardProtoUpload.content), wizardProtoUpload.fileName ); wizardProtoUpload = null; } if (wizardDescriptorSetUpload) { descriptorResponse = await window.CrankApi.uploadDescriptorSet( wizardWorkspaceId, wizardEditId, wizardDescriptorSetUpload.bytes, wizardDescriptorSetUpload.fileName ); wizardDescriptorSetUpload = null; var descriptorName = document.getElementById('wizard-descriptor-set-name'); if (descriptorName) descriptorName.textContent = tKey('wizard.descriptor.uploaded'); } if (!descriptorResponse) return; payload.target.descriptor_ref = descriptorResponse.descriptor_id; payload.target.descriptor_set_b64 = ''; await window.CrankApi.updateOperation( wizardWorkspaceId, wizardEditId, updateOperationPayload(payload) ); } async function persistCurrentDraft(stayOnPage) { if (!wizardWorkspaceId) { throw new Error(tKey('wizard.error.no_workspace')); } var payload = collectWizardPayload(); var response; var createdNow = false; if (wizardMode === 'edit' && wizardEditId) { response = await window.CrankApi.updateOperation( wizardWorkspaceId, wizardEditId, updateOperationPayload(payload) ); } else { response = await window.CrankApi.createOperation(wizardWorkspaceId, payload); createdNow = true; wizardEditId = response.operation_id; wizardMode = 'edit'; history.replaceState( {}, '', window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(wizardEditId) ); setEditModePresentation(); _doGoToStep(currentStep); var toolNameInput = document.getElementById('tool-name'); if (toolNameInput) toolNameInput.disabled = true; } await uploadPendingGrpcArtifacts(payload); await refreshCurrentOperationState(); if (stayOnPage) { var saveDraftBtn = document.querySelector('.btn-save-draft'); if (saveDraftBtn) { var label = saveDraftBtn.textContent; saveDraftBtn.textContent = tKey('wizard.save.saved'); setTimeout(function() { saveDraftBtn.textContent = label; }, 1400); } } showWizardLiveStatus( createdNow ? tKey('wizard.save.created') : tKey('wizard.save.updated'), tKey('wizard.save.body') ); return response; } function safeStringify(value) { if (value === null || value === undefined) return ''; if (typeof value === 'string') return value; return JSON.stringify(value, null, 2); } function setTextareaValue(id, value) { var element = document.getElementById(id); if (!element) return; element.value = safeStringify(value); } async function uploadInputSampleFromWizard() { await persistCurrentDraft(true); await window.CrankApi.uploadInputSample( wizardWorkspaceId, wizardEditId, parseStructuredText(textValue('wizard-input-sample')) ); showWizardLiveStatus(tKey('wizard.sample.input_saved'), tKey('wizard.sample.input_saved_body')); } async function uploadOutputSampleFromWizard() { await persistCurrentDraft(true); await window.CrankApi.uploadOutputSample( wizardWorkspaceId, wizardEditId, parseStructuredText(textValue('wizard-output-sample')) ); showWizardLiveStatus(tKey('wizard.sample.output_saved'), tKey('wizard.sample.output_saved_body')); } async function generateDraftFromWizard() { await persistCurrentDraft(true); var generated = await window.CrankApi.generateDraft(wizardWorkspaceId, wizardEditId, {}); setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(generated.input_schema), null, 2)); setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(generated.output_schema), null, 2)); setValue('tool-input-mapping', mappingSetToEditorValue(generated.input_mapping, 'input', wizardProtocol)); setValue('tool-output-mapping', mappingSetToEditorValue(generated.output_mapping, 'output', wizardProtocol)); showWizardLiveStatus(tKey('wizard.sample.generated'), tKey('wizard.sample.generated_body')); } async function runWizardTest() { await persistCurrentDraft(true); var result = await window.CrankApi.runOperationTest(wizardWorkspaceId, wizardEditId, { version: currentDraftVersion(), input: parseStructuredText(textValue('wizard-test-input')), }); wizardTestResponsePreview = result.response_preview; setTextareaValue('wizard-test-request-preview', result.request_preview); setTextareaValue('wizard-test-response-preview', result.response_preview); setTextareaValue('wizard-test-errors', result.errors && result.errors.length ? result.errors : []); showWizardLiveStatus( result.ok ? tKey('wizard.test.completed') : tKey('wizard.test.failed'), result.ok ? tKey('wizard.test.completed_body') : tKey('wizard.test.failed_body'), !result.ok ); } function copyTestResponseToOutputSample() { if (!wizardTestResponsePreview) { showWizardLiveStatus(tKey('wizard.test.no_response'), tKey('wizard.test.no_response_body'), true); return; } setTextareaValue('wizard-output-sample', wizardTestResponsePreview); showWizardLiveStatus(tKey('wizard.test.response_copied'), tKey('wizard.test.response_copied_body')); } function downloadTextFile(fileName, content, mimeType) { var blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); } async function exportWizardYaml() { await persistCurrentDraft(true); var yaml = await window.CrankApi.exportOperation(wizardWorkspaceId, wizardEditId, { mode: 'portable', version: currentDraftVersion(), }); downloadTextFile((textValue('tool-name') || 'operation') + '.yaml', yaml, 'application/yaml'); showWizardLiveStatus(tKey('wizard.yaml.exported'), tKey('wizard.yaml.exported_body')); } async function importWizardYaml() { if (!wizardWorkspaceId) { throw new Error(tKey('wizard.error.no_workspace')); } var yamlDocument = textValue('wizard-import-yaml-text'); if (!yamlDocument) { showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true); return; } var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert'); showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body')); window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id); } async function publishWizardOperation() { await persistCurrentDraft(true); var published = await window.CrankApi.publishOperation( wizardWorkspaceId, wizardEditId, currentDraftVersion() ); await refreshCurrentOperationState(); showWizardLiveStatus( tKey('wizard.publish.done'), tfKey('wizard.publish.done_body', { version: published.published_version }) ); } function handleImportYamlFileSelection(event) { var file = event.target.files && event.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(loadEvent) { setValue('wizard-import-yaml-text', loadEvent.target.result || ''); showWizardLiveStatus(tKey('wizard.yaml.loaded'), tKey('wizard.yaml.loaded_body')); }; reader.readAsText(file); } function handleDescriptorSetSelection(event) { var file = event.target.files && event.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(loadEvent) { wizardDescriptorSetUpload = { fileName: file.name, bytes: new Uint8Array(loadEvent.target.result), }; var fileNameEl = document.getElementById('wizard-descriptor-set-name'); if (fileNameEl) fileNameEl.textContent = file.name; showWizardLiveStatus(tKey('wizard.descriptor.selected'), tKey('wizard.descriptor.selected_body')); }; reader.readAsArrayBuffer(file); } function renderDescriptorServiceList(services) { grpcDescriptorServices = services || []; var summaryEl = document.getElementById('wizard-descriptor-services-summary'); var listEl = document.getElementById('wizard-descriptor-services-list'); if (!summaryEl || !listEl) return; var methodCount = grpcDescriptorServices.reduce(function(total, service) { return total + ((service.methods || []).length); }, 0); summaryEl.textContent = grpcDescriptorServices.length ? tfKey('wizard.grpc.services_discovered', { services: grpcDescriptorServices.length, methods: methodCount }) : tKey('wizard.grpc.services_none'); listEl.innerHTML = ''; grpcDescriptorServices.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.package ? service.package + '.' : '') + service.service; body.appendChild(title); (service.methods || []).forEach(function(method, methodIndex) { var button = document.createElement('button'); button.type = 'button'; button.className = 'btn-ghost-sm'; button.style.width = '100%'; button.style.justifyContent = 'space-between'; button.textContent = method.name; button.addEventListener('click', function() { applyDiscoveredGrpcMethod(serviceIndex, methodIndex); }); body.appendChild(button); }); group.appendChild(body); listEl.appendChild(group); }); } function applyDiscoveredGrpcMethod(serviceIndex, methodIndex) { var service = grpcDescriptorServices[serviceIndex]; var method = service && service.methods ? service.methods[methodIndex] : null; if (!service || !method) return; var route = '/' + (service.package ? service.package + '.' : '') + service.service + '/' + method.name; selectGrpcSource('manual'); setValue('grpc-manual-route', route); setValue('grpc-manual-req-type', method.name + 'Request'); setValue('grpc-manual-res-type', method.name + 'Response'); grpcManualRouteInput(route); grpcManualTypeInput(); setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(method.input_schema), null, 2)); setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(method.output_schema), null, 2)); showWizardLiveStatus(tKey('wizard.descriptor.applied'), tKey('wizard.descriptor.applied_body')); } async function uploadDescriptorSetAndDiscover() { var hasSavedDescriptor = !!( wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc' && wizardCurrentVersion.target.descriptor_ref ); if (!wizardDescriptorSetUpload && !hasSavedDescriptor) { showWizardLiveStatus(tKey('wizard.descriptor.no_file'), tKey('wizard.descriptor.no_file_body'), true); return; } await persistCurrentDraft(true); var services = await window.CrankApi.listGrpcServices( wizardWorkspaceId, wizardEditId, currentDraftVersion() ); renderDescriptorServiceList(services.services || []); showWizardLiveStatus(tKey('wizard.descriptor.discovered'), tKey('wizard.descriptor.discovered_body')); }