1817 lines
67 KiB
JavaScript
1817 lines
67 KiB
JavaScript
var currentStep = 1;
|
|
var TOTAL_STEPS = 5;
|
|
var wizardProtocol = 'rest'; // 'rest' | 'graphql' | 'grpc'
|
|
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 = [];
|
|
|
|
/* ── 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 (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: 'Request config',
|
|
graphql: 'GQL operation',
|
|
grpc: 'RPC method',
|
|
};
|
|
|
|
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) {
|
|
// 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 = 'Completed';
|
|
} else if (num === n) {
|
|
item.classList.add('active');
|
|
if (ind) ind.textContent = num;
|
|
if (statusEl) statusEl.textContent = 'In progress';
|
|
} else {
|
|
item.classList.add('pending');
|
|
if (ind) ind.textContent = num;
|
|
if (statusEl) statusEl.textContent = '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('.step-counter');
|
|
if (counter) counter.innerHTML = 'Step <strong>' + n + '</strong> of ' + 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' ? 'Save changes' : 'Create operation';
|
|
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 = '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] || 'Protocol config';
|
|
|
|
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.APP_BASE || '') + 'index.html';
|
|
});
|
|
}
|
|
|
|
var closeBtn = document.querySelector('.progress-close');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', function() {
|
|
window.location.href = (window.APP_BASE || '') + 'index.html';
|
|
});
|
|
}
|
|
|
|
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 loadWizardPanels([1, 2, 3, 4, 5]);
|
|
bindProtocolCards();
|
|
bindWizardLiveActions();
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
if (params.get('mode') === 'edit' && params.get('operationId')) {
|
|
wizardMode = 'edit';
|
|
wizardEditId = params.get('operationId');
|
|
document.title = 'Crank — Edit Operation';
|
|
await loadOperationForEdit();
|
|
}
|
|
|
|
updateWizardProtocolVisibility();
|
|
_doGoToStep(1);
|
|
});
|
|
|
|
|
|
/* ── HTTP method picker (Step 4 REST) ── */
|
|
|
|
var METHOD_CALLOUTS = {
|
|
GET: null,
|
|
DELETE: null,
|
|
POST: { icon: 'info', text: '<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.' },
|
|
PUT: { icon: 'info', text: '<strong>PUT selected — full-resource replacement.</strong> The request body must contain a complete resource representation. Define the body schema in step 4.' },
|
|
PATCH: { icon: 'info', text: '<strong>PATCH selected — partial update.</strong> Only include the fields you want to modify in the body schema defined in step 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) {
|
|
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>' + info.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', auth: 'bearer', authLabel: 'Bearer token' },
|
|
{ id: 'stripe', name: 'stripe-payments', url: 'https://api.stripe.com', auth: 'apikey', authLabel: 'API key' },
|
|
{ id: 'internal-crm', name: 'internal-crm', url: 'https://crm.internal.acme.io', auth: 'none', authLabel: 'No auth' },
|
|
{ id: 'sendgrid', name: 'sendgrid-mail', url: 'https://api.sendgrid.com', auth: 'apikey', authLabel: 'API key' },
|
|
{ id: 'twilio', name: 'twilio-sms', url: 'https://api.twilio.com', auth: 'bearer', authLabel: 'Bearer token' },
|
|
];
|
|
|
|
var selectedUpstreamId = null;
|
|
var dropdownOpen = false;
|
|
|
|
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">No upstreams match "' + escapeHtml(filter) + '"</div>';
|
|
return;
|
|
}
|
|
|
|
var tmpl = document.getElementById('tmpl-upstream-item');
|
|
list.innerHTML = '';
|
|
filtered.forEach(function(u) {
|
|
var sel = u.id === selectedUpstreamId;
|
|
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 = u.authLabel;
|
|
badge.className = 'upstream-auth-badge auth-' + u.auth;
|
|
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;
|
|
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) {
|
|
pName.textContent = u.name;
|
|
pUrl.textContent = u.url;
|
|
pBadge.textContent = u.authLabel;
|
|
pBadge.className = 'upstream-auth-badge auth-' + u.auth;
|
|
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();
|
|
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">Select an upstream…</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 = '';
|
|
var first = form.querySelector('input');
|
|
if (first) setTimeout(function() { first.focus(); }, 50);
|
|
}
|
|
}
|
|
|
|
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; }
|
|
|
|
// Add to list
|
|
var newId = 'custom-' + Date.now();
|
|
upstreams.push({ id: newId, name: name, url: url, auth: 'bearer', authLabel: 'Bearer token' });
|
|
|
|
// Reset form
|
|
if (nameEl) nameEl.value = '';
|
|
if (urlEl) urlEl.value = '';
|
|
var form = document.getElementById('upstream-new-form');
|
|
if (form) form.style.display = 'none';
|
|
|
|
// Select the new upstream
|
|
pickUpstream(newId);
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str).replace(/&/g,'&').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 =
|
|
parsed.services.length + ' service' + (parsed.services.length !== 1 ? 's' : '') +
|
|
' · ' + totalMethods + ' unary method' + (totalMethods !== 1 ? 's' : '') + ' found';
|
|
}
|
|
|
|
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 = 'No unary RPC methods found.';
|
|
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) {
|
|
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) {
|
|
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 = 'Fields not resolved.<br><span style="color:var(--text-muted);font-size:11px;">Import or external types are not expanded client-side.</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(
|
|
'gRPC discovery moved to live descriptor flow',
|
|
'Server reflection is not wired in this deployment. Save the draft, then use the descriptor-set tools in Step 5 to upload a real descriptor set and discover unary methods from the backend.'
|
|
);
|
|
}
|
|
|
|
|
|
/* ══════════════════════════════════════════════
|
|
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('YAML parser is not available');
|
|
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) {
|
|
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 } };
|
|
}
|
|
|
|
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('Select or register an upstream first');
|
|
}
|
|
|
|
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 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('Tool name is required');
|
|
|
|
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 || 'Failed to save operation', 'Save failed');
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = 'Edit operation';
|
|
var sidebarBrand = document.querySelector('.step-sidebar-brand');
|
|
if (sidebarBrand) sidebarBrand.innerHTML = 'Edit <span>operation</span>';
|
|
}
|
|
|
|
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] || 'Protocol config';
|
|
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));
|
|
|
|
var toolNameInput = document.getElementById('tool-name');
|
|
if (toolNameInput) toolNameInput.disabled = true;
|
|
updateWizardProtocolVisibility();
|
|
}
|
|
|
|
function bindWizardLiveActions() {
|
|
bindLiveAction('wizard-upload-input-sample', 'Saving input sample…', uploadInputSampleFromWizard);
|
|
bindLiveAction('wizard-upload-output-sample', 'Saving output sample…', uploadOutputSampleFromWizard);
|
|
bindLiveAction('wizard-generate-draft', 'Generating draft…', generateDraftFromWizard);
|
|
bindLiveAction('wizard-run-test', 'Running test…', runWizardTest);
|
|
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
|
|
bindLiveAction('wizard-export-yaml', 'Exporting YAML…', exportWizardYaml);
|
|
bindLiveAction('wizard-import-yaml', 'Importing YAML…', importWizardYaml);
|
|
bindLiveAction('wizard-publish-operation', 'Publishing operation…', publishWizardOperation);
|
|
bindClick('wizard-select-descriptor-set', function() {
|
|
var input = document.getElementById('wizard-descriptor-set-input');
|
|
if (input) input.click();
|
|
});
|
|
bindLiveAction('wizard-upload-descriptor-set', 'Uploading descriptor set…', 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(
|
|
'Live action failed',
|
|
error && error.message ? error.message : 'The backend request failed. Review the draft and try again.',
|
|
true
|
|
);
|
|
if (window.CrankUi) {
|
|
window.CrankUi.error(
|
|
error && error.message ? error.message : 'The backend request failed. Review the draft and try again.',
|
|
'Wizard action failed'
|
|
);
|
|
}
|
|
} 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';
|
|
}
|
|
|
|
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 = 'Descriptor set 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('No workspace selected');
|
|
}
|
|
|
|
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 = 'Saved ✓';
|
|
setTimeout(function() {
|
|
saveDraftBtn.textContent = label;
|
|
}, 1400);
|
|
}
|
|
}
|
|
|
|
showWizardLiveStatus(
|
|
createdNow ? 'Draft created' : 'Draft updated',
|
|
'The current draft is saved on the backend and ready for live actions in this wizard.'
|
|
);
|
|
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('Input sample saved', 'The input sample is now stored against the current draft version.');
|
|
}
|
|
|
|
async function uploadOutputSampleFromWizard() {
|
|
await persistCurrentDraft(true);
|
|
await window.CrankApi.uploadOutputSample(
|
|
wizardWorkspaceId,
|
|
wizardEditId,
|
|
parseStructuredText(textValue('wizard-output-sample'))
|
|
);
|
|
showWizardLiveStatus('Output sample saved', 'The output sample is now stored against the current draft version.');
|
|
}
|
|
|
|
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('Draft generated', 'Schemas and mappings were regenerated from the saved samples.');
|
|
}
|
|
|
|
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 ? 'Test run completed' : 'Test run returned errors',
|
|
result.ok
|
|
? 'The current draft executed successfully against the upstream.'
|
|
: 'The draft executed, but the backend returned validation or runtime errors.',
|
|
!result.ok
|
|
);
|
|
}
|
|
|
|
function copyTestResponseToOutputSample() {
|
|
if (!wizardTestResponsePreview) {
|
|
showWizardLiveStatus('No test response yet', 'Run a test first, then copy the response preview into the output sample editor.', true);
|
|
return;
|
|
}
|
|
setTextareaValue('wizard-output-sample', wizardTestResponsePreview);
|
|
showWizardLiveStatus('Response copied', 'The latest test response was copied into the output sample editor.');
|
|
}
|
|
|
|
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('YAML exported', 'The current draft YAML was downloaded from the backend.');
|
|
}
|
|
|
|
async function importWizardYaml() {
|
|
if (!wizardWorkspaceId) {
|
|
throw new Error('No workspace selected');
|
|
}
|
|
var yamlDocument = textValue('wizard-import-yaml-text');
|
|
if (!yamlDocument) {
|
|
showWizardLiveStatus('No YAML provided', 'Paste a YAML document or load it from a file before importing.', true);
|
|
return;
|
|
}
|
|
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
|
|
showWizardLiveStatus('YAML imported', 'The imported operation draft was stored. The wizard will reopen it now.');
|
|
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(
|
|
'Operation published',
|
|
'Version ' + published.published_version + ' is now published and can be bound into agents.'
|
|
);
|
|
}
|
|
|
|
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('YAML loaded', 'The YAML document was loaded into the import editor.');
|
|
};
|
|
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('Descriptor set selected', 'Upload the descriptor set to discover unary gRPC methods from the backend.');
|
|
};
|
|
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
|
|
? (grpcDescriptorServices.length + ' services · ' + methodCount + ' unary methods discovered')
|
|
: 'No unary methods discovered yet';
|
|
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('gRPC method applied', 'The route and schemas were filled from the uploaded descriptor set.');
|
|
}
|
|
|
|
async function uploadDescriptorSetAndDiscover() {
|
|
var hasSavedDescriptor = !!(
|
|
wizardCurrentVersion
|
|
&& wizardCurrentVersion.target
|
|
&& wizardCurrentVersion.target.kind === 'grpc'
|
|
&& wizardCurrentVersion.target.descriptor_ref
|
|
);
|
|
|
|
if (!wizardDescriptorSetUpload && !hasSavedDescriptor) {
|
|
showWizardLiveStatus('No descriptor set selected', 'Choose a descriptor-set file before uploading it, or save a draft that already has one attached.', true);
|
|
return;
|
|
}
|
|
|
|
await persistCurrentDraft(true);
|
|
|
|
var services = await window.CrankApi.listGrpcServices(
|
|
wizardWorkspaceId,
|
|
wizardEditId,
|
|
currentDraftVersion()
|
|
);
|
|
renderDescriptorServiceList(services.services || []);
|
|
showWizardLiveStatus('Descriptor services discovered', 'The backend parsed the descriptor set. Pick a method to apply its route and schemas.');
|
|
}
|