Files
crank/apps/ui/js/wizard.js
T
2026-04-06 13:23:45 +03:00

2229 lines
84 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var currentStep = 1;
var TOTAL_STEPS = 5;
var wizardProtocol = 'rest'; // 'rest' | 'graphql' | 'grpc' | 'websocket'
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'],
},
};
}
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 = '<svg width="10" height="10"><use href="' + (window.APP_BASE||'') + 'icons/general/check.svg#icon"/></svg>';
var ARROW_SVG = '<svg width="13" height="13"><use href="' + (window.APP_BASE||'') + 'icons/general/arrow-right.svg#icon"/></svg>';
var BACK_SVG = '<svg width="13" height="13"><use href="' + (window.APP_BASE||'') + 'icons/general/arrow-left.svg#icon"/></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]);
bindProtocolCards();
bindWizardLiveActions();
bindStreamingConfigControls();
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: '<strong>POST selected — body encoding required.</strong> 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: '<strong>Выбран POST — требуется кодирование тела запроса.</strong> На шаге 4 вы зададите JSON-схему для body payload. Crank автоматически сериализует аргументы MCP-инструмента в выбранный формат.' } },
PUT: { icon: 'info', text: { en: '<strong>PUT selected — full-resource replacement.</strong> The request body must contain a complete resource representation. Define the body schema in step 4.', ru: '<strong>Выбран PUT — полная замена ресурса.</strong> Request body должен содержать полное представление ресурса. Задайте body schema на шаге 4.' } },
PATCH: { icon: 'info', text: { en: '<strong>PATCH selected — partial update.</strong> Only include the fields you want to modify in the body schema defined in step 4.', ru: '<strong>Выбран PATCH — частичное обновление.</strong> В 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 =
'<svg width="15" height="15" style="flex-shrink:0;color:var(--accent)"><use href="' + (window.APP_BASE||'') + 'icons/general/info-circle.svg#icon"/></svg>' +
'<span>' + text + '</span>';
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 3) ── */
var upstreams = [
{ id: 'acme-api', name: 'acme-api', url: 'https://api.acme.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.ACME_API_TOKEN}\"\n}' },
{ id: 'stripe', name: 'stripe-payments', url: 'https://api.stripe.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.STRIPE_API_KEY}\"\n}' },
{ id: 'internal-crm', name: 'internal-crm', url: 'https://crm.internal.acme.io', authHeaders: '{\n}' },
{ id: 'sendgrid', name: 'sendgrid-mail', url: 'https://api.sendgrid.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.SENDGRID_API_KEY}\"\n}' },
{ id: 'twilio', name: 'twilio-sms', url: 'https://api.twilio.com', authHeaders: '{\n \"Authorization\": \"Bearer ${secrets.TWILIO_API_KEY}\"\n}' },
];
var selectedUpstreamId = null;
var dropdownOpen = false;
var editingUpstreamId = null;
function inferUpstreamAuth(headersText) {
var parsed = parseStructuredText(headersText || '{}');
var keys = parsed && typeof parsed === 'object' ? Object.keys(parsed) : [];
if (!keys.length) {
return { kind: 'none', label: tKey('wizard.step2.auth_none') };
}
var hasAuthorization = keys.some(function(key) {
return String(key).toLowerCase() === 'authorization';
});
if (hasAuthorization) {
return { kind: 'bearer', label: tKey('wizard.step2.auth_bearer') };
}
return { kind: 'apikey', label: tKey('wizard.step2.auth_apikey') };
}
function upstreamMeta(upstream) {
var auth = inferUpstreamAuth(upstream && upstream.authHeaders);
return {
kind: auth.kind,
label: auth.label,
};
}
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 = '<div class="upstream-dropdown-empty">' + tfKey('agents.drawer.ops_no_match', { query: escapeHtml(filter) }) + '</div>';
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 =
'<div class="upstream-trigger-name">' + escapeHtml(u.name) + '</div>' +
'<div class="upstream-trigger-url">' + escapeHtml(u.url) + '</div>';
}
}
// 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 = '<span class="upstream-combobox-placeholder">' + tKey('wizard.step2.upstream_placeholder') + '</span>';
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-auth-headers', '{\n}');
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-auth-headers', upstream.authHeaders || '{\n}');
}
}
function saveNewUpstream(e) {
e.stopPropagation();
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 authHeadersEl = document.getElementById('new-upstream-auth-headers');
var authHeaders = authHeadersEl ? authHeadersEl.value : '{\n}';
var nextRecord = {
id: editingUpstreamId || ('custom-' + Date.now()),
name: name,
url: url,
authHeaders: authHeaders
};
if (editingUpstreamId) {
upstreams = upstreams.map(function(item) {
return item.id === editingUpstreamId ? nextRecord : item;
});
} else {
upstreams.push(nextRecord);
}
// Reset form
if (nameEl) nameEl.value = '';
if (urlEl) urlEl.value = '';
if (authHeadersEl) authHeadersEl.value = '{\n}';
var form = document.getElementById('upstream-new-form');
if (form) form.style.display = 'none';
editingUpstreamId = null;
// Select the new upstream
pickUpstream(nextRecord.id);
}
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 escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// 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') + '<br><span style="color:var(--text-muted);font-size:11px;">' + tKey('wizard.grpc.fields_unresolved_body') + '</span>';
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 config = {
timeout_ms: Number(value.timeout_ms || 10000),
retry_policy: retry && retry.max_attempts ? { max_attempts: Number(retry.max_attempts) } : null,
auth_profile_ref: value.auth && value.auth.profile ? value.auth.profile : null,
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 authHeaders = textValue('new-upstream-auth-headers');
return {
id: 'custom',
name: customName || 'custom-upstream',
url: customUrl,
authHeaders: authHeaders,
};
}
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.authHeaders),
};
}
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) {
var known = upstreams.find(function(item) { return item.url === baseUrl; });
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-auth-headers', headers && Object.keys(headers).length ? JSON.stringify(headers, null, 2) : '{\n}');
var valueEl = document.getElementById('upstream-combobox-value');
if (valueEl) {
valueEl.innerHTML =
'<div class="upstream-trigger-name">imported-upstream</div>' +
'<div class="upstream-trigger-url">' + escapeHtml(baseUrl) + '</div>';
}
}
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);
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, {});
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, {});
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'));
}