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{} }