Files
crank/apps/ui/js/wizard-grpc.js
T
2026-05-02 19:42:59 +00:00

395 lines
15 KiB
JavaScript

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').hidden = source !== 'proto';
document.getElementById('grpc-src-reflection').hidden = source !== 'reflection';
document.getElementById('grpc-src-manual').hidden = source !== 'manual';
var detail = document.getElementById('grpc-method-detail');
if (detail) detail.hidden = true;
selectedRpcMethod = null;
}
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;
}
function stripProtoComments(text) {
text = text.replace(/\/\*[\s\S]*?\*\//g, '');
text = text.replace(/\/\/[^\n]*/g, '');
return text;
}
function parseProto(text) {
text = stripProtoComments(text);
var pkg = '';
var pkgMatch = text.match(/\bpackage\s+([\w.]+)\s*;/);
if (pkgMatch) pkg = pkgMatch[1];
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) {
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 = [];
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 += 1;
} else {
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 };
}
function renderServiceResults(parsed, ids) {
protoParsed = parsed;
selectedRpcMethod = null;
var notice = document.getElementById(ids.notice);
var countEl = document.getElementById(ids.count);
if (notice) {
notice.hidden = parsed.streamingCount <= 0;
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) {
var servicesLabel = window.tPlural('wizard.grpc.services_label', parsed.services.length, {
count: parsed.services.length,
});
var methodsLabel = window.tPlural('wizard.grpc.methods_label', totalMethods, {
count: totalMethods,
});
summaryEl.textContent = tfKey('wizard.grpc.services_found', {
services_label: servicesLabel,
methods_label: methodsLabel,
});
}
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).hidden = false;
document.getElementById('grpc-method-detail').hidden = true;
}
function renderParsedProto(parsed) {
var notice = document.getElementById('proto-streaming-text');
if (notice && parsed && parsed.streamingCount) {
notice.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, {
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 = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, {
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',
});
}
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.hidden = false;
renderFieldList('grpc-req-fields', reqType);
renderFieldList('grpc-res-fields', resType);
document.getElementById('grpc-method-detail').hidden = false;
}
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.textContent = '';
emptyEl.appendChild(document.createTextNode(tKey('wizard.grpc.fields_unresolved')));
emptyEl.appendChild(document.createElement('br'));
var hint = document.createElement('span');
hint.style.color = 'var(--text-muted)';
hint.style.fontSize = '11px';
hint.textContent = tKey('wizard.grpc.fields_unresolved_body');
emptyEl.appendChild(hint);
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);
});
}
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.hidden = true;
if (info) info.hidden = false;
if (nameEl) nameEl.textContent = name;
if (sizeEl) sizeEl.textContent = (size / 1024).toFixed(1) + ' KB';
document.getElementById('proto-paste-area').hidden = true;
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.hidden = false;
if (info) info.hidden = true;
if (parsedView) parsedView.hidden = true;
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.hidden;
area.hidden = open;
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.hidden = true;
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').hidden = true;
var dropzone = document.getElementById('proto-dropzone');
if (dropzone) dropzone.hidden = true;
var info = document.getElementById('proto-file-info');
var nameEl = document.getElementById('proto-file-name');
var sizeEl = document.getElementById('proto-file-size');
if (info) info.hidden = false;
if (nameEl) nameEl.textContent = 'pasted.proto';
if (sizeEl) sizeEl.textContent = (ta.value.length / 1024).toFixed(1) + ' KB';
renderParsedProto(parsed);
}
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')
);
}
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.hidden = true;
return;
}
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;
if (sigGrid) sigGrid.hidden = true;
grpcManualTypeInput();
if (detail) detail.hidden = false;
}
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()) || '—';
}