more w/ styling
This commit is contained in:
parent
b7434fb205
commit
97b3486c72
199
ex4.html
199
ex4.html
|
|
@ -14,7 +14,7 @@
|
||||||
<script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
<script src="https://unpkg.com/cytoscape-dagre@2.5.0/cytoscape-dagre.js"></script>
|
||||||
<!-- HTML label plugin -->
|
<!-- HTML label plugin -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-node-html-label@1.2.2/dist/cytoscape-node-html-label.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/cytoscape-node-html-label@1.2.2/dist/cytoscape-node-html-label.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root { --bg:#0b1220; --panel:#0f172a; --muted:#334155; --text:#e5e7eb; --accent:#60a5fa;
|
:root { --bg:#0b1220; --panel:#0f172a; --muted:#334155; --text:#e5e7eb; --accent:#60a5fa;
|
||||||
--name-size:16px; --detail-size:12px; }
|
--name-size:16px; --detail-size:12px; }
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
|
|
@ -54,6 +54,10 @@
|
||||||
.dot{ width:8px; height:8px; border-radius:50%; display:inline-block; }
|
.dot{ width:8px; height:8px; border-radius:50%; display:inline-block; }
|
||||||
.dot.run{ background:#10b981; } .dot.stop{ background:#ef4444; } .dot.unk{ background:#9ca3af; }
|
.dot.run{ background:#10b981; } .dot.stop{ background:#ef4444; } .dot.unk{ background:#9ca3af; }
|
||||||
.vpc-big{ font-weight:800; font-size:clamp(18px,6vw,48px); color:#94a3b8; opacity:.35; text-shadow:0 1px 0 #fff; }
|
.vpc-big{ font-weight:800; font-size:clamp(18px,6vw,48px); color:#94a3b8; opacity:.35; text-shadow:0 1px 0 #fff; }
|
||||||
|
/* Subnet tab label */
|
||||||
|
.subnet-tab{ display:inline-block; background:#f3f4f6; color:#0f172a; border:1px solid #9ca3af; border-bottom:none; border-top-left-radius:4px; border-top-right-radius:4px; padding:6px 10px; box-shadow:0 1px 0 rgba(0,0,0,.05) inset; pointer-events:none; }
|
||||||
|
.subnet-tab .t-name{ font-weight:700; font-size:12px; line-height:1.1; }
|
||||||
|
.subnet-tab .t-sub{ font-size:11px; color:#475569; line-height:1.2; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -106,45 +110,71 @@
|
||||||
'background-color': '#e5e7eb',
|
'background-color': '#e5e7eb',
|
||||||
'border-width': 1,
|
'border-width': 1,
|
||||||
'border-color': '#9ca3af',
|
'border-color': '#9ca3af',
|
||||||
'label': 'data(label)',
|
|
||||||
'text-wrap': 'wrap',
|
'text-wrap': 'wrap',
|
||||||
'text-max-width': 180,
|
'text-max-width': 180,
|
||||||
'font-size': 12,
|
'font-size': 12,
|
||||||
'padding': '10px',
|
'padding': '10px',
|
||||||
'text-valign': 'top',
|
'text-valign': 'top',
|
||||||
'text-halign': 'left',
|
'text-halign': 'left',
|
||||||
'text-margin-x': 'data(lx)',
|
|
||||||
'text-margin-y': 'data(ly)',
|
|
||||||
'text-opacity': 0 /* hide built-in labels; we render HTML labels */
|
'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: {
|
{ selector: 'node[type = "vpc"]', style: {
|
||||||
'background-color': '#f9fafb','border-color': '#111827','border-width': 2,'padding': '30px','font-weight': 700
|
'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 = "subnet"]', style: {
|
||||||
{ selector: 'node[type = "alb"], node[type = "nlb"]', style: { 'background-color': '#fee2e2','border-color': '#ef4444','border-width': 2 }},
|
'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: {
|
{ selector: 'node[type = "ec2"]', style: {
|
||||||
'background-color': '#dbeafe',
|
'background-color': '#dbeafe',
|
||||||
'border-color': '#3b82f6',
|
'border-color': '#3b82f6',
|
||||||
'width': 'label',
|
'width': 260,
|
||||||
'height': 'label',
|
'height': 100,
|
||||||
'padding': '12px',
|
'padding': '12px',
|
||||||
'text-opacity': 1, /* show built-in text for EC2 */
|
'text-opacity': 0 /* use HTML labels for EC2 */
|
||||||
'text-wrap': 'wrap',
|
}},
|
||||||
'text-max-width': 240,
|
{ selector: 'node[type = "rds"]', style: {
|
||||||
'text-halign': 'center', /* place label inside node */
|
'background-color': '#dcfce7',
|
||||||
'text-valign': 'center',
|
'border-color': '#10b981',
|
||||||
'text-justification': 'left', /* left-align lines within block */
|
'width': 260,
|
||||||
'text-margin-x': 0,
|
'height': 100,
|
||||||
'text-margin-y': 0
|
'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: 'node[type = "igw"], node[type = "nat"]', style: { 'background-color': '#ffe4e6','border-color': '#fb7185' }},
|
||||||
{ selector: 'edge', style: {
|
{ selector: 'edge', style: {
|
||||||
'width': 2,'line-color': '#9ca3af','target-arrow-shape': 'triangle','target-arrow-color': '#9ca3af',
|
'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
|
'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.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' }}
|
||||||
]
|
]
|
||||||
|
|
@ -191,7 +221,7 @@
|
||||||
}
|
}
|
||||||
// EC2
|
// EC2
|
||||||
for (const i of ec2s) {
|
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,
|
elements.push({ data: { id: i.id, type: 'ec2', label,
|
||||||
name: i.name, instanceId: i.id, privateIp: i.privateIp,
|
name: i.name, instanceId: i.id, privateIp: i.privateIp,
|
||||||
publicIp: i.publicIp, state: i.state, parent: i.subnet, sgs: i.sgs || [] } });
|
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 `<span class="status"><i class="dot ${cls}"></i>${escapeHtml(txt)}</span>`; }
|
function statusDot(state){ const cls=(state==='running')?'run':(state==='stopped'?'stop':'unk'); const txt=state||'unknown'; return `<span class="status"><i class="dot ${cls}"></i>${escapeHtml(txt)}</span>`; }
|
||||||
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; }
|
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) {
|
if (cy.nodeHtmlLabel) {
|
||||||
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){
|
tpl: function(d){
|
||||||
const type=d.type; const name=d.name||(d.label||'').split('\n')[0]||d.id;
|
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 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 priv=d.privateIp||d.private_ip||'';
|
||||||
const detailB=pub?`Public: ${escapeHtml(pub)}`:'';
|
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 extra=(type==='ec2')?statusDot(d.state):(type==='sg')?`${sgRuleCount(d)} rules`:'';
|
const lines = [];
|
||||||
|
if (type==='ec2'){
|
||||||
|
if (d.state) lines.push(`<div class="n-sub">${statusDot(d.state)}</div>`);
|
||||||
|
if (d.instanceId) lines.push(`<div class="n-sub">${escapeHtml(d.instanceId)}</div>`);
|
||||||
|
if (priv) lines.push(`<div class="n-sub">Private IP: ${escapeHtml(priv)}</div>`);
|
||||||
|
if (pub) lines.push(`<div class="n-sub">Public: ${escapeHtml(pub)}</div>`);
|
||||||
|
} else {
|
||||||
|
if (detailA) lines.push(`<div class="n-sub">${escapeHtml(detailA)}</div>`);
|
||||||
|
if (priv) lines.push(`<div class="n-sub">Private IP: ${escapeHtml(priv)}</div>`);
|
||||||
|
if (pub) lines.push(`<div class="n-sub">Public: ${escapeHtml(pub)}</div>`);
|
||||||
|
}
|
||||||
const badge=vpnBadge(name);
|
const badge=vpnBadge(name);
|
||||||
const ml=(d.lx||0), mt=(d.ly||0);
|
return `<div class="ncard n-${type}" style="background:none;border:none;box-shadow:none;padding:0;margin:0;display:block;">
|
||||||
return `<div class="ncard n-${type}" style="margin-left:${ml}px; margin-top:${mt}px;"><div>${iconSvg(type)}</div><div><div class="n-title">${escapeHtml(name)}${badge}</div><div class="n-sub">${escapeHtml(detailA)}</div>${detailB?`<div class="n-sub">${escapeHtml(detailB)}</div>`:''}${extra?`<div class="n-sub">${extra}</div>`:''}</div></div>`;
|
<div class="n-title">${escapeHtml(name)}${badge}</div>
|
||||||
|
${lines.join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 `<div class=\"subnet-tab\" data-node-id=\"${d.id}\">
|
||||||
|
<div class=\"t-name\">${escapeHtml(name)}</div>
|
||||||
|
${subline?`<div class=\"t-sub\">${escapeHtml(subline)}</div>`:''}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -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 ---
|
// --- 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); });
|
cy.on('mouseup',()=>{ if(!labelMode) return; labelTarget=null; cy.panningEnabled(prevPan); });
|
||||||
|
|
||||||
// Save / Load layout (positions + label offsets)
|
// 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 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){
|
function applyLayoutData(data){
|
||||||
cy.batch(()=>{
|
cy.batch(()=>{
|
||||||
|
|
@ -345,10 +479,17 @@
|
||||||
|
|
||||||
// Size slider adjusts typography + padding
|
// Size slider adjusts typography + padding
|
||||||
const sizeSlider=document.getElementById('size-slider');
|
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)));
|
sizeSlider.addEventListener('input',()=> applySizeScale(parseFloat(sizeSlider.value)));
|
||||||
applySizeScale(parseFloat(sizeSlider.value));
|
applySizeScale(parseFloat(sizeSlider.value));
|
||||||
|
|
||||||
|
// No additional resize hooks required for subnet tabs
|
||||||
|
|
||||||
// Autosave positions/offsets so layouts survive re-scrapes
|
// Autosave positions/offsets so layouts survive re-scrapes
|
||||||
const LKEY='vpc-layout-autosave';
|
const LKEY='vpc-layout-autosave';
|
||||||
function saveLocal(){ try{ localStorage.setItem(LKEY, JSON.stringify(snapshotLayout())); }catch{} }
|
function saveLocal(){ try{ localStorage.setItem(LKEY, JSON.stringify(snapshotLayout())); }catch{} }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue