From 4b0545ab23e6dfb43a3c019f10b23fb23dc65b51 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sat, 2 May 2026 19:42:59 +0000 Subject: [PATCH] ui: extract wizard grpc module --- apps/ui/js/wizard-grpc.js | 394 +++++++++++++++++++++++++++++++++++ apps/ui/js/wizard.js | 426 -------------------------------------- apps/ui/scripts/build.js | 3 +- 3 files changed, 396 insertions(+), 427 deletions(-) create mode 100644 apps/ui/js/wizard-grpc.js diff --git a/apps/ui/js/wizard-grpc.js b/apps/ui/js/wizard-grpc.js new file mode 100644 index 0000000..0d58c72 --- /dev/null +++ b/apps/ui/js/wizard-grpc.js @@ -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()) || '—'; +} diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js index ac2e8b4..22ceede 100644 --- a/apps/ui/js/wizard.js +++ b/apps/ui/js/wizard.js @@ -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() : ''; diff --git a/apps/ui/scripts/build.js b/apps/ui/scripts/build.js index b159993..08f3426 100644 --- a/apps/ui/scripts/build.js +++ b/apps/ui/scripts/build.js @@ -102,8 +102,9 @@ const BUNDLES = { 'js/wizard-shell.js', 'js/streaming-form.js', 'js/stream-test-run.js', - 'js/wizard.js', + 'js/wizard-grpc.js', 'js/wizard-upstreams.js', + 'js/wizard.js', 'js/nav.js', ], },