ui: extract wizard grpc module
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
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()) || '—';
|
||||
}
|
||||
@@ -239,432 +239,6 @@ document.addEventListener('click', function() {
|
||||
|
||||
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').hidden = source !== 'proto';
|
||||
document.getElementById('grpc-src-reflection').hidden = source !== 'reflection';
|
||||
document.getElementById('grpc-src-manual').hidden = source !== 'manual';
|
||||
// hide shared detail when switching sources
|
||||
var detail = document.getElementById('grpc-method-detail');
|
||||
if (detail) detail.hidden = true;
|
||||
selectedRpcMethod = null;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
gRPC — Proto file parser & UI
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
/* 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.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',
|
||||
});
|
||||
}
|
||||
|
||||
/* 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.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);
|
||||
});
|
||||
}
|
||||
|
||||
/* 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.hidden = true;
|
||||
if (info) info.hidden = false;
|
||||
if (nameEl) nameEl.textContent = name;
|
||||
if (sizeEl) sizeEl.textContent = (size / 1024).toFixed(1) + ' KB';
|
||||
// hide paste area
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
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.hidden = true;
|
||||
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.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()) || '—';
|
||||
}
|
||||
|
||||
|
||||
function textValue(id) {
|
||||
var element = document.getElementById(id);
|
||||
return element ? element.value.trim() : '';
|
||||
|
||||
Reference in New Issue
Block a user