-
-
+
+
+
-
+
- | Tyyppi |
+ Tyyppi |
Verkko / IP |
- VLAN |
Nimi / Kuvaus |
+ VLAN |
Sijainti |
Tila |
Asiakas |
@@ -291,11 +292,38 @@
-
Ei IPAM-merkintöjä vielä.
+
Ei verkkoja tai IP-osoitteita vielä.
- 0 merkintää
+ 0 verkkoa
+
+
+
+
+
+
VLANit
+
+
+
+
+
+
+ | VLAN |
+ Verkko |
+ Nimi / Kuvaus |
+ Sijainti |
+ Tila |
+ Asiakas |
+ Toiminnot |
+
+
+
+
+
+
+ 0 VLANia
+
diff --git a/script.js b/script.js
index 5b1dd75..98c3852 100644
--- a/script.js
+++ b/script.js
@@ -2957,6 +2957,102 @@ document.getElementById('btn-cancel-site')?.addEventListener('click', () => {
// ==================== IPAM ====================
let ipamData = [];
+let ipamExpandedIds = new Set();
+let ipamDrillStack = []; // [{id, label}] breadcrumb
+
+// --- IP-laskenta-apufunktiot ---
+function ipToInt(ip) {
+ return ip.split('.').reduce((acc, oct) => (acc << 8) + parseInt(oct), 0) >>> 0;
+}
+function prefixToMask(prefix) {
+ return prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
+}
+function parseNetwork(verkko) {
+ if (!verkko) return null;
+ const v = verkko.trim();
+ if (v.includes('/')) {
+ const [ip, pfx] = v.split('/');
+ const prefix = parseInt(pfx);
+ if (isNaN(prefix) || prefix < 0 || prefix > 32) return null;
+ const parts = ip.split('.');
+ if (parts.length !== 4) return null;
+ return { net: ipToInt(ip), prefix };
+ } else {
+ const parts = v.split('.');
+ if (parts.length !== 4) return null;
+ return { net: ipToInt(v), prefix: 32 };
+ }
+}
+function isSubnetOf(childNet, childPrefix, parentNet, parentPrefix) {
+ if (childPrefix <= parentPrefix) return false;
+ const mask = prefixToMask(parentPrefix);
+ return (childNet & mask) === (parentNet & mask);
+}
+
+// --- Puurakenne ---
+function buildIpamTree(entries) {
+ // Parsitaan verkko-osoitteet ja suodatetaan validit
+ const items = entries.map(e => {
+ const parsed = parseNetwork(e.verkko);
+ return parsed ? { entry: e, net: parsed.net, prefix: parsed.prefix, children: [] } : null;
+ }).filter(Boolean);
+ // Järjestetään: pienin prefix (isoin verkko) ensin, sitten IP-osoitteen mukaan
+ items.sort((a, b) => a.prefix - b.prefix || a.net - b.net);
+ const roots = [];
+ for (const item of items) {
+ let placed = false;
+ // Etsi lähin parent (suurin prefix joka sisältää tämän)
+ const findParent = (nodes) => {
+ for (let i = nodes.length - 1; i >= 0; i--) {
+ const node = nodes[i];
+ if (isSubnetOf(item.net, item.prefix, node.net, node.prefix)) {
+ // Tarkista ensin onko jokin lapsi tarkempi parent
+ if (!findParent(node.children)) {
+ node.children.push(item);
+ }
+ return true;
+ }
+ }
+ return false;
+ };
+ if (!findParent(roots)) {
+ roots.push(item);
+ }
+ }
+ return roots;
+}
+
+function flattenTree(nodes, depth, drillId) {
+ // Jos drill-down aktiivinen, etsi drill-node ja renderöi vain sen lapset
+ if (drillId) {
+ const findNode = (list) => {
+ for (const n of list) {
+ if (n.entry.id === drillId) return n;
+ const found = findNode(n.children);
+ if (found) return found;
+ }
+ return null;
+ };
+ const drillNode = findNode(nodes);
+ if (drillNode) {
+ nodes = drillNode.children;
+ depth = 0;
+ }
+ }
+ const rows = [];
+ const render = (list, d) => {
+ for (const node of list) {
+ const hasChildren = node.children.length > 0;
+ const expanded = ipamExpandedIds.has(node.entry.id);
+ rows.push({ entry: node.entry, depth: d, hasChildren, expanded });
+ if (hasChildren && expanded) {
+ render(node.children, d + 1);
+ }
+ }
+ };
+ render(nodes, depth);
+ return rows;
+}
async function loadIpam() {
try {
@@ -2967,9 +3063,11 @@ async function loadIpam() {
function renderIpam() {
const query = (document.getElementById('ipam-search-input')?.value || '').toLowerCase().trim();
- let filtered = ipamData;
+
+ // --- Verkot + IP:t (hierarkkinen puu) ---
+ let networkEntries = ipamData.filter(e => e.tyyppi === 'subnet' || e.tyyppi === 'ip');
if (query) {
- filtered = ipamData.filter(e =>
+ networkEntries = networkEntries.filter(e =>
(e.tyyppi || '').toLowerCase().includes(query) ||
(e.verkko || '').toLowerCase().includes(query) ||
(e.nimi || '').toLowerCase().includes(query) ||
@@ -2979,22 +3077,107 @@ function renderIpam() {
String(e.vlan_id || '').includes(query)
);
}
+
+ const tree = buildIpamTree(networkEntries);
+
+ // Breadcrumb
+ const bcEl = document.getElementById('ipam-breadcrumb');
+ if (bcEl) {
+ const drillId = ipamDrillStack.length > 0 ? ipamDrillStack[ipamDrillStack.length - 1].id : null;
+ if (ipamDrillStack.length === 0) {
+ bcEl.style.display = 'none';
+ } else {
+ bcEl.style.display = '';
+ bcEl.innerHTML = `Kaikki verkot` +
+ ipamDrillStack.map((s, i) =>
+ ` › ${esc(s.label)}`
+ ).join('');
+ }
+ // Flatten tree siten että drill-down huomioidaan
+ var drillTarget = drillId;
+ } else {
+ var drillTarget = null;
+ }
+
+ const rows = flattenTree(tree, 0, drillTarget);
+
const tbody = document.getElementById('ipam-tbody');
const noIpam = document.getElementById('no-ipam');
- if (filtered.length === 0) {
+ const tilaClass = { vapaa: 'ipam-tila-vapaa', varattu: 'ipam-tila-varattu', reserved: 'ipam-tila-reserved' };
+ const tilaLabel = { vapaa: 'Vapaa', varattu: 'Varattu', reserved: 'Reserved' };
+
+ if (rows.length === 0 && !query) {
tbody.innerHTML = '';
if (noIpam) noIpam.style.display = 'block';
} else {
if (noIpam) noIpam.style.display = 'none';
- const tyyppiLabel = { subnet: 'Subnet', vlan: 'VLAN', ip: 'IP' };
- const tilaClass = { vapaa: 'ipam-tila-vapaa', varattu: 'ipam-tila-varattu', reserved: 'ipam-tila-reserved' };
- const tilaLabel = { vapaa: 'Vapaa', varattu: 'Varattu', reserved: 'Reserved' };
- tbody.innerHTML = filtered.map(e => `
- | ${tyyppiLabel[e.tyyppi] || e.tyyppi} |
+ tbody.innerHTML = rows.map(r => {
+ const e = r.entry;
+ const indent = r.depth * 1.5;
+ const toggleIcon = r.hasChildren
+ ? `${r.expanded ? '▼' : '▶'} `
+ : ' ';
+ const typeTag = e.tyyppi === 'subnet'
+ ? 'Subnet'
+ : 'IP';
+ const drillBtn = (e.tyyppi === 'subnet' && r.hasChildren)
+ ? ` →`
+ : '';
+ return `
+ |
+ ${toggleIcon}${typeTag}
+ |
+ ${esc(e.verkko || '-')}${drillBtn} |
+ ${esc(e.nimi || '-')} |
+ ${e.vlan_id ? '' + e.vlan_id + '' : '-'} |
+ ${e.site_name ? esc(e.site_name) : '—'} |
+ ${tilaLabel[e.tila] || e.tila} |
+ ${esc(e.asiakas || '-')} |
+
+
+
+ |
+
`;
+ }).join('');
+ }
+
+ const netCount = networkEntries.length;
+ document.getElementById('ipam-count').textContent = netCount + ' verkkoa/IP:tä' + (query ? ` (${ipamData.filter(e => e.tyyppi !== 'vlan').length} yhteensä)` : '');
+
+ // --- VLANit ---
+ renderIpamVlans(query);
+}
+
+function renderIpamVlans(query) {
+ let vlans = ipamData.filter(e => e.tyyppi === 'vlan');
+ if (query) {
+ vlans = vlans.filter(e =>
+ String(e.vlan_id || '').includes(query) ||
+ (e.nimi || '').toLowerCase().includes(query) ||
+ (e.verkko || '').toLowerCase().includes(query) ||
+ (e.site_name || '').toLowerCase().includes(query) ||
+ (e.asiakas || '').toLowerCase().includes(query)
+ );
+ }
+ vlans.sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0));
+
+ const tbody = document.getElementById('ipam-vlan-tbody');
+ const section = document.getElementById('ipam-vlan-section');
+ if (!tbody) return;
+
+ const tilaClass = { vapaa: 'ipam-tila-vapaa', varattu: 'ipam-tila-varattu', reserved: 'ipam-tila-reserved' };
+ const tilaLabel = { vapaa: 'Vapaa', varattu: 'Varattu', reserved: 'Reserved' };
+
+ if (vlans.length === 0 && !query) {
+ tbody.innerHTML = '';
+ if (section) section.style.display = 'none';
+ } else {
+ if (section) section.style.display = '';
+ tbody.innerHTML = vlans.map(e => `
+ | ${e.vlan_id || '-'} |
${esc(e.verkko || '-')} |
- ${e.vlan_id ? '' + e.vlan_id + '' : '-'} |
${esc(e.nimi || '-')} |
- ${e.site_name ? esc(e.site_name) : '-'} |
+ ${e.site_name ? esc(e.site_name) : '—'} |
${tilaLabel[e.tila] || e.tila} |
${esc(e.asiakas || '-')} |
@@ -3003,7 +3186,28 @@ function renderIpam() {
|
`).join('');
}
- document.getElementById('ipam-count').textContent = filtered.length + ' merkintää' + (query ? ` (${ipamData.length} yhteensä)` : '');
+ document.getElementById('ipam-vlan-count').textContent = vlans.length + ' VLANia';
+}
+
+// --- Toggle & Drill ---
+function ipamToggle(id) {
+ if (ipamExpandedIds.has(id)) ipamExpandedIds.delete(id);
+ else ipamExpandedIds.add(id);
+ renderIpam();
+}
+function ipamDrillInto(id, label) {
+ ipamDrillStack.push({ id, label });
+ ipamExpandedIds.clear(); // reset expand-tila uudessa näkymässä
+ renderIpam();
+}
+function ipamDrillTo(index) {
+ if (index < 0) {
+ ipamDrillStack = [];
+ } else {
+ ipamDrillStack = ipamDrillStack.slice(0, index + 1);
+ }
+ ipamExpandedIds.clear();
+ renderIpam();
}
async function loadIpamSitesDropdown() {
@@ -3043,8 +3247,18 @@ async function deleteIpam(id, name) {
document.getElementById('btn-add-ipam')?.addEventListener('click', async () => {
document.getElementById('ipam-form-id').value = '';
document.getElementById('ipam-form').reset();
+ document.getElementById('ipam-form-tyyppi').value = 'subnet';
await loadIpamSitesDropdown();
- document.getElementById('ipam-modal-title').textContent = 'Lisää IPAM-merkintä';
+ document.getElementById('ipam-modal-title').textContent = 'Lisää verkko / IP';
+ document.getElementById('ipam-modal').style.display = 'flex';
+});
+
+document.getElementById('btn-add-vlan')?.addEventListener('click', async () => {
+ document.getElementById('ipam-form-id').value = '';
+ document.getElementById('ipam-form').reset();
+ document.getElementById('ipam-form-tyyppi').value = 'vlan';
+ await loadIpamSitesDropdown();
+ document.getElementById('ipam-modal-title').textContent = 'Lisää VLAN';
document.getElementById('ipam-modal').style.display = 'flex';
});
diff --git a/style.css b/style.css
index e5b8abd..9e7f6fa 100644
--- a/style.css
+++ b/style.css
@@ -1073,6 +1073,23 @@ span.empty {
.ipam-tila-varattu { background:#fef3cd; color:#856404; }
.ipam-tila-reserved { background:#e2e3e5; color:#495057; }
+/* IPAM puunäkymä */
+.ipam-tree-table tbody tr { cursor: pointer; }
+.ipam-tree-table tbody tr:hover { background: #f0f4ff; }
+.ipam-toggle { cursor:pointer; display:inline-block; width:1rem; text-align:center; font-size:0.7rem; color:#555; user-select:none; }
+.ipam-toggle:hover { color:#0f3460; }
+.ipam-toggle-placeholder { display:inline-block; width:1rem; }
+.ipam-type-subnet { display:inline-block; padding:1px 8px; border-radius:4px; font-size:0.75rem; font-weight:700; background:#e0e7ff; color:#3730a3; }
+.ipam-type-ip { display:inline-block; padding:1px 8px; border-radius:4px; font-size:0.75rem; font-weight:700; background:#f0fdf4; color:#166534; }
+.ipam-network { font-size:0.85rem; background:#f8f9fa; padding:1px 6px; border-radius:3px; }
+.ipam-drill { cursor:pointer; font-size:0.8rem; color:#6366f1; margin-left:0.4rem; }
+.ipam-drill:hover { color:#4338ca; text-decoration:underline; }
+.ipam-breadcrumb { padding:0.5rem 0; font-size:0.85rem; color:#555; }
+.ipam-bc-link { cursor:pointer; color:#6366f1; }
+.ipam-bc-link:hover { text-decoration:underline; }
+.ipam-bc-current { color:#333; font-weight:600; cursor:default; }
+.ipam-bc-current:hover { text-decoration:none; }
+
/* Role badge */
.role-badge {
display: inline-block;