diff --git a/ex4.html b/ex4.html
index 2897609..cae0e42 100644
--- a/ex4.html
+++ b/ex4.html
@@ -176,7 +176,10 @@
}},
{ 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' }}
+ { selector: 'edge.sg-attach', style: { 'line-style': 'dotted','line-color': '#f59e0b','target-arrow-color': '#f59e0b' }},
+ { selector: 'edge.sg-sg', style: { 'line-color': '#f59e0b','target-arrow-color': '#f59e0b','source-arrow-color': '#f59e0b' }},
+ { selector: 'edge.sg-sg[tarrow > 0]', style: { 'target-arrow-shape': 'triangle' }},
+ { selector: 'edge.sg-sg[sarrow > 0]', style: { 'source-arrow-shape': 'triangle' }}
]
});
@@ -219,6 +222,32 @@
const label = `${sg.name || sg.id}\ningress: ${(rin[0]||'—')}${rin.length>1?'…':''}`;
elements.push({ data: { id: sg.id, type: 'sg', label, rules: { in: rin, out: rout } } });
}
+ // SG<->SG edges based on rules referencing other SGs
+ function parseSgRef(rule){
+ // match: "proto port from sg-xxxx"
+ const m = (rule||'').match(/^(\S+)\s+([^\s]+)\s+from\s+(sg-[0-9a-zA-Z]+)/);
+ if(!m) return null; return { proto:m[1], port:m[2], sg:m[3] };
+ }
+ const pairMap = new Map(); // key: A|B -> {inOn:{}, outFrom:{}}
+ function keyAB(a,b){ return aT
+ const sarrow = t2sList.length ? 1 : 0; // arrow at source if flow T->S
+ const label = [summarize(s2tList), summarize(t2sList)].filter(Boolean).join(' | ');
+ elements.push({ data: { id:`sg:${s}|${t}`, source:s, target:t, label, tarrow, sarrow }, classes:'sg-sg' });
+ }
// EC2
for (const i of ec2s) {
const label = `${i.name || i.id}\n${i.id}\n${i.type || ''}\n${i.privateIp ? ('Private IP: ' + i.privateIp) : ''}`;
@@ -431,7 +460,7 @@
document.getElementById('toggle-sg').addEventListener('change',(e)=>{
const show=e.target.checked;
- cy.batch(()=>{ cy.nodes('[type = "sg"]').style('display', show?'element':'none'); cy.edges('.sg-attach').style('display', show?'element':'none'); });
+ cy.batch(()=>{ cy.nodes('[type = "sg"]').style('display', show?'element':'none'); cy.edges('.sg-attach, .sg-sg').style('display', show?'element':'none'); });
});
document.getElementById('toggle-routes').addEventListener('change',(e)=>{
const show=e.target.checked;
@@ -470,6 +499,12 @@
});
cy.fit(undefined,24);
}
+ async function tryLoadStartupLayout(){
+ try{
+ const resp = await fetch('layout.json', { cache: 'no-cache' });
+ if(resp && resp.ok){ const data = await resp.json(); applyLayoutData(data); }
+ }catch(e){ /* ignore if missing */ }
+ }
document.getElementById('btn-save').addEventListener('click',()=> downloadJSON(snapshotLayout(),'vpc-layout.json'));
document.getElementById('btn-load').addEventListener('click',()=> document.getElementById('load-file').click());
document.getElementById('load-file').addEventListener('change',(e)=>{
@@ -494,6 +529,8 @@
const LKEY='vpc-layout-autosave';
function saveLocal(){ try{ localStorage.setItem(LKEY, JSON.stringify(snapshotLayout())); }catch{} }
function restoreLocal(){ try{ const raw=localStorage.getItem(LKEY); if(raw) applyLayoutData(JSON.parse(raw)); }catch{} }
+ // Load file-based layout first (if present), then restore any local override
+ await tryLoadStartupLayout();
restoreLocal();
cy.on('position','node', saveLocal);
cy.on('mouseup', ()=> { if(labelMode) saveLocal(); });