diff --git a/ex4.html b/ex4.html index a38b00c..2897609 100644 --- a/ex4.html +++ b/ex4.html @@ -14,7 +14,7 @@ - @@ -106,45 +110,71 @@ 'background-color': '#e5e7eb', 'border-width': 1, 'border-color': '#9ca3af', - 'label': 'data(label)', 'text-wrap': 'wrap', 'text-max-width': 180, 'font-size': 12, 'padding': '10px', 'text-valign': 'top', 'text-halign': 'left', - 'text-margin-x': 'data(lx)', - 'text-margin-y': 'data(ly)', 'text-opacity': 0 /* hide built-in labels; we render HTML labels */ }}, + { selector: 'node[lx]', style: { 'text-margin-x': 'data(lx)' }}, + { selector: 'node[ly]', style: { 'text-margin-y': 'data(ly)' }}, + { selector: 'node[label]', style: { 'label': 'data(label)' }}, { selector: 'node[type = "vpc"]', style: { 'background-color': '#f9fafb','border-color': '#111827','border-width': 2,'padding': '30px','font-weight': 700 }}, - { selector: 'node[type = "subnet"]', style: { 'background-color': '#f3f4f6','border-color': '#9ca3af' }}, - { selector: 'node[type = "alb"], node[type = "nlb"]', style: { 'background-color': '#fee2e2','border-color': '#ef4444','border-width': 2 }}, + { selector: 'node[type = "subnet"]', style: { + 'background-color': '#f3f4f6', + 'border-color': '#9ca3af', + 'border-radius': 4 + }}, + { selector: 'node[type = "subnet-sizer"]', style: { + 'shape': 'rectangle', + 'background-opacity': 0, + 'border-width': 0, + 'width': 1, + 'height': 1, + 'events': 'no', + 'opacity': 0 + }}, + { selector: 'node[type = "alb"], node[type = "nlb"]', style: { + 'background-color': '#fee2e2', + 'border-color': '#ef4444', + 'border-width': 2, + 'width': 260, + 'height': 100, + 'padding': '12px' + }}, { selector: 'node[type = "ec2"]', style: { 'background-color': '#dbeafe', 'border-color': '#3b82f6', - 'width': 'label', - 'height': 'label', + 'width': 260, + 'height': 100, 'padding': '12px', - 'text-opacity': 1, /* show built-in text for EC2 */ - 'text-wrap': 'wrap', - 'text-max-width': 240, - 'text-halign': 'center', /* place label inside node */ - 'text-valign': 'center', - 'text-justification': 'left', /* left-align lines within block */ - 'text-margin-x': 0, - 'text-margin-y': 0 + 'text-opacity': 0 /* use HTML labels for EC2 */ + }}, + { selector: 'node[type = "rds"]', style: { + 'background-color': '#dcfce7', + 'border-color': '#10b981', + 'width': 260, + 'height': 100, + 'padding': '12px' + }}, + { selector: 'node[type = "sg"]', style: { + 'background-color': '#fff7ed', + 'border-color': '#f59e0b', + 'width': 260, + 'height': 90, + 'padding': '12px' }}, - { selector: 'node[type = "rds"]', style: { 'background-color': '#dcfce7','border-color': '#10b981' }}, - { selector: 'node[type = "sg"]', style: { 'background-color': '#fff7ed','border-color': '#f59e0b' }}, { selector: 'node[type = "igw"], node[type = "nat"]', style: { 'background-color': '#ffe4e6','border-color': '#fb7185' }}, { selector: 'edge', style: { 'width': 2,'line-color': '#9ca3af','target-arrow-shape': 'triangle','target-arrow-color': '#9ca3af', - 'curve-style': 'bezier','label': 'data(label)','font-size': 11,'text-rotation': 'autorotate', + 'curve-style': 'bezier','font-size': 11,'text-rotation': 'autorotate', 'text-background-color': '#ffffff','text-background-opacity': 0.7,'text-border-opacity': 0,'text-margin-y': -6 }}, + { selector: 'edge[label]', style: { 'label': 'data(label)' }}, { selector: 'edge.route', style: { 'line-style': 'dashed','line-color': '#94a3b8','target-arrow-color': '#94a3b8' }}, { selector: 'edge.sg-attach', style: { 'line-style': 'dotted','line-color': '#f59e0b','target-arrow-color': '#f59e0b' }} ] @@ -191,7 +221,7 @@ } // EC2 for (const i of ec2s) { - const label = `${i.name || i.id}\n${i.id}\n${i.type || ''}\n${i.privateIp || ''}`; + const label = `${i.name || i.id}\n${i.id}\n${i.type || ''}\n${i.privateIp ? ('Private IP: ' + i.privateIp) : ''}`; elements.push({ data: { id: i.id, type: 'ec2', label, name: i.name, instanceId: i.id, privateIp: i.privateIp, publicIp: i.publicIp, state: i.state, parent: i.subnet, sgs: i.sgs || [] } }); @@ -243,20 +273,47 @@ function statusDot(state){ const cls=(state==='running')?'run':(state==='stopped'?'stop':'unk'); const txt=state||'unknown'; return `${escapeHtml(txt)}`; } function sgRuleCount(d){ const rin=(d.rules&&d.rules.in)?d.rules.in.length:0; const rout=(d.rules&&d.rules.out)?d.rules.out.length:0; return rin+rout; } - // Attach HTML labels (exclude EC2; they use built-in labels sized to node) + // Attach HTML labels for SG/ALB/NLB/RDS/EC2 centered inside nodes and Subnet at top-left if (cy.nodeHtmlLabel) { cy.nodeHtmlLabel([ { - query:'node[type != "vpc"][type != "ec2"]', halign:'left', valign:'top', halignBox:'left', valignBox:'top', + query:'node[type = "sg"], node[type = "alb"], node[type = "nlb"], node[type = "rds"], node[type = "ec2"]', + halign:'center', valign:'center', halignBox:'center', valignBox:'center', tpl: function(d){ const type=d.type; const name=d.name||(d.label||'').split('\n')[0]||d.id; const pub=d.publicIp||d.publicIpAddress||d.public_ip||d.dns||''; - const detailA=(type==='ec2')?(d.privateIp||''):(type==='rds'?`${d.engine||''}:${d.port||''}`:(type==='alb'||type==='nlb')?(d.scheme||''):(d.cidr||'')); - const detailB=pub?`Public: ${escapeHtml(pub)}`:''; - const extra=(type==='ec2')?statusDot(d.state):(type==='sg')?`${sgRuleCount(d)} rules`:''; + const priv=d.privateIp||d.private_ip||''; + const detailA=(type==='rds')?`${d.engine||''}:${d.port||''}`:(type==='alb'||type==='nlb')?(d.scheme||''):(type==='sg')?`${sgRuleCount(d)} rules`:(type==='ec2')? (d.state||'') : (d.cidr||''); + const lines = []; + if (type==='ec2'){ + if (d.state) lines.push(`
${statusDot(d.state)}
`); + if (d.instanceId) lines.push(`
${escapeHtml(d.instanceId)}
`); + if (priv) lines.push(`
Private IP: ${escapeHtml(priv)}
`); + if (pub) lines.push(`
Public: ${escapeHtml(pub)}
`); + } else { + if (detailA) lines.push(`
${escapeHtml(detailA)}
`); + if (priv) lines.push(`
Private IP: ${escapeHtml(priv)}
`); + if (pub) lines.push(`
Public: ${escapeHtml(pub)}
`); + } const badge=vpnBadge(name); - const ml=(d.lx||0), mt=(d.ly||0); - return `
${iconSvg(type)}
${escapeHtml(name)}${badge}
${escapeHtml(detailA)}
${detailB?`
${escapeHtml(detailB)}
`:''}${extra?`
${extra}
`:''}
`; + return `
+
${escapeHtml(name)}${badge}
+ ${lines.join('')} +
`; + } + }, + { + query:'node[type = "subnet"]', + halign:'left', valign:'top', halignBox:'left', valignBox:'top', + tpl: function(d){ + const name=(d.name||(d.label||'').split('\n')[0]||d.id); + const cidr=d.cidr||''; + const az=d.az||''; + const subline=[cidr, az].filter(Boolean).join(' \u2022 '); + return `
+
${escapeHtml(name)}
+ ${subline?`
${escapeHtml(subline)}
`:''} +
`; } }, { @@ -265,6 +322,83 @@ } ]); } + + // Wrap subnet tabs and reset outer transform so plugin can position parent + function prepareSubnetTabs(){ + const tabs = Array.from(document.querySelectorAll('.subnet-tab')); + tabs.forEach(el => { + if (!el.querySelector('.subnet-tab-inner')){ + const inner=document.createElement('div'); inner.className='subnet-tab-inner'; + while (el.firstChild) inner.appendChild(el.firstChild); + el.appendChild(inner); + } + el.style.transform = 'translate(0px,-1px)'; + }); + } + + // Position subnet tabs at left + labelWidth + 10% of node width and match colors + function positionSubnetTabs(){ + const containerRect = cy.container().getBoundingClientRect(); + const tabs = Array.from(document.querySelectorAll('.subnet-tab')); + if (!tabs.length) return; + const subnets = cy.nodes('[type = "subnet"]'); + if (subnets.empty()) return; + for (const el of tabs) { + const r = el.getBoundingClientRect(); + const px = r.left + 20, py = r.top + 2; // near top-left of tab + const hit = subnets.filter(n=>{ + const bb = n.renderedBoundingBox({ includeNodes:true, includeLabels:false, includeEdges:false }); + const left=containerRect.left+bb.x1, top=containerRect.top+bb.y1, right=containerRect.left+bb.x2, bottom=containerRect.top+bb.y2; + return px>=left && px<=right && py>=top && py<=bottom; + })[0]; + if (!hit) continue; + const nw = hit.renderedWidth(); + const lw = r.width; + const offx = Math.round(lw + nw * 0.10); + el.style.transform = `translate(${offx}px,-1px)`; + // Match node colors + const bg = hit.style('background-color') || '#f3f4f6'; + const br = hit.style('border-color') || '#9ca3af'; + el.style.backgroundColor = bg; + el.style.borderColor = br; + } + } + // Ensure empty subnets are at least larger than their tab label + function ensureEmptySubnetSizers(){ + const subnets = cy.nodes('[type = "subnet"]'); + subnets.forEach(sn => { + const nonSizerKids = sn.children().filter('[type != "subnet-sizer"]'); + const sid = 'sizer:'+sn.id(); + const had = !cy.getElementById(sid).empty(); + if (nonSizerKids.length > 0) { + if (had) cy.remove(cy.getElementById(sid)); + } else { + if (!had) cy.add({ group:'nodes', data:{ id:sid, type:'subnet-sizer', parent: sn.id() } }); + } + }); + } + function updateEmptySubnetSizerSizes(){ + const tabs = Array.from(document.querySelectorAll('.subnet-tab')); + if (!tabs.length) return; + tabs.forEach(el => { + const id = el.getAttribute('data-node-id'); + const sn = id ? cy.getElementById(id) : null; + if (!sn || sn.empty()) return; + const sid = 'sizer:'+sn.id(); + const sizer = cy.getElementById(sid); + if (sizer.empty()) return; + const r = el.getBoundingClientRect(); + const minW = Math.ceil(r.width) + 24; + const minH = Math.ceil(r.height) + 24; + sizer.style({ width: minW, height: Math.max(60, minH) }); + sizer.position(sn.position()); + }); + } + + // Initial and reactive updates + requestAnimationFrame(()=>{ prepareSubnetTabs(); positionSubnetTabs(); ensureEmptySubnetSizers(); updateEmptySubnetSizerSizes(); }); + cy.on('layoutstop zoom', () => requestAnimationFrame(()=>{ prepareSubnetTabs(); positionSubnetTabs(); ensureEmptySubnetSizers(); updateEmptySubnetSizerSizes(); })); + window.addEventListener('resize', () => requestAnimationFrame(()=>{ prepareSubnetTabs(); positionSubnetTabs(); updateEmptySubnetSizerSizes(); })); // --- UI, details, toggles, label edit, save/load, size, search, SG sort --- @@ -327,7 +461,7 @@ cy.on('mouseup',()=>{ if(!labelMode) return; labelTarget=null; cy.panningEnabled(prevPan); }); // Save / Load layout (positions + label offsets) - function snapshotLayout(){ const nodes={}; cy.nodes().forEach(n=>{ nodes[n.id()]={ position:n.position(), lx:n.data('lx')||0, ly:n.data('ly')||0 }; }); return { nodes }; } + function snapshotLayout(){ const nodes={}; cy.nodes().forEach(n=>{ if(n.data('type')==='subnet-sizer') return; nodes[n.id()]={ position:n.position(), lx:n.data('lx')||0, ly:n.data('ly')||0 }; }); return { nodes }; } function downloadJSON(obj,filename){ const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=filename; a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); } function applyLayoutData(data){ cy.batch(()=>{ @@ -345,10 +479,17 @@ // Size slider adjusts typography + padding const sizeSlider=document.getElementById('size-slider'); - function applySizeScale(v){ document.documentElement.style.setProperty('--name-size',(16*v)+'px'); document.documentElement.style.setProperty('--detail-size',(12*v)+'px'); cy.style().selector('node').style('padding',(10*v)+'px').update(); } + function applySizeScale(v){ + document.documentElement.style.setProperty('--name-size',(16*v)+'px'); + document.documentElement.style.setProperty('--detail-size',(12*v)+'px'); + cy.style().selector('node').style('padding',(10*v)+'px').update(); + requestAnimationFrame(()=>{ prepareSubnetTabs(); positionSubnetTabs(); ensureEmptySubnetSizers(); updateEmptySubnetSizerSizes(); }); + } sizeSlider.addEventListener('input',()=> applySizeScale(parseFloat(sizeSlider.value))); applySizeScale(parseFloat(sizeSlider.value)); + // No additional resize hooks required for subnet tabs + // Autosave positions/offsets so layouts survive re-scrapes const LKEY='vpc-layout-autosave'; function saveLocal(){ try{ localStorage.setItem(LKEY, JSON.stringify(snapshotLayout())); }catch{} }