Netwerken UI refactor: shared netns badge verplaatst naar Flags kolom
- Containers kolom toont nu uitsluitend het numerieke aantal containers - Shared network namespace wordt bepaald via expliciete isShared check - 'shared' badge verplaatst van Containers kolom naar Flags kolom - Eerdere uitlijnings-experimenten en CSS overrides opgeschoond - Duidelijke scheiding aangebracht tussen metriek (aantal) en status (shared netns) Resultaat: semantisch correctere tabel, stabielere layout en betere leesbaarheid.
This commit is contained in:
@@ -40,6 +40,13 @@
|
||||
usage: null,
|
||||
list: null,
|
||||
inspectCache: new Map(),
|
||||
filters: {
|
||||
q: '',
|
||||
connectedOnly: false,
|
||||
hideDefaults: true,
|
||||
sharedOnly: false,
|
||||
sort: 'name_asc',
|
||||
},
|
||||
};
|
||||
|
||||
function toggleNetworkRow(name) {
|
||||
@@ -60,6 +67,7 @@
|
||||
state.usage = usage;
|
||||
state.list = list;
|
||||
if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
|
||||
renderNetworksSummary();
|
||||
renderNetworks();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -67,6 +75,65 @@
|
||||
}
|
||||
}
|
||||
|
||||
function computeNetworkStats() {
|
||||
const totalNetworks = Array.isArray(state.list?.networks) ? state.list.networks.length : 0;
|
||||
const byNetwork = state.usage?.byNetwork || {};
|
||||
const usedNetworks = Object.keys(byNetwork).length;
|
||||
|
||||
let connectedContainers = 0;
|
||||
for (const v of Object.values(byNetwork)) {
|
||||
const cs = v?.containers;
|
||||
if (Array.isArray(cs)) connectedContainers += cs.length;
|
||||
}
|
||||
|
||||
const unusedNetworks = Math.max(0, totalNetworks - usedNetworks);
|
||||
|
||||
const byMeta = state.usage?.byContainerMeta || {};
|
||||
let sharedNetns = 0;
|
||||
for (const meta of Object.values(byMeta)) {
|
||||
if (meta?.networkMode && String(meta.networkMode).startsWith('container:')) sharedNetns += 1;
|
||||
}
|
||||
|
||||
return { totalNetworks, usedNetworks, unusedNetworks, connectedContainers, sharedNetns };
|
||||
}
|
||||
|
||||
function renderNetworksSummary() {
|
||||
const host = document.getElementById('networksSummary');
|
||||
if (!host) return;
|
||||
|
||||
// beginstatus
|
||||
if (!state.list && !state.usage) {
|
||||
host.innerHTML = `
|
||||
<div class="statCard"><div class="statValue">-</div><div class="statLabel">Netwerken</div></div>
|
||||
<div class="statCard"><div class="statValue">-</div><div class="statLabel">Verbonden containers</div></div>
|
||||
<div class="statCard"><div class="statValue">-</div><div class="statLabel">Ongebruikt</div></div>
|
||||
<div class="statCard"><div class="statValue">-</div><div class="statLabel">Shared netns</div></div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const s = computeNetworkStats();
|
||||
host.innerHTML = `
|
||||
<div class="statCard">
|
||||
<div class="statValue">${s.totalNetworks}</div>
|
||||
<div class="statLabel">Netwerken</div>
|
||||
<div class="statHint">${s.usedNetworks} in gebruik</div>
|
||||
</div>
|
||||
<div class="statCard">
|
||||
<div class="statValue">${s.connectedContainers}</div>
|
||||
<div class="statLabel">Verbonden containers</div>
|
||||
</div>
|
||||
<div class="statCard">
|
||||
<div class="statValue">${s.unusedNetworks}</div>
|
||||
<div class="statLabel">Ongebruikt</div>
|
||||
</div>
|
||||
<div class="statCard">
|
||||
<div class="statValue">${s.sharedNetns}</div>
|
||||
<div class="statLabel">Shared netns</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderNetworkUsersListHTML(netName, containers) {
|
||||
if (!containers.length) {
|
||||
return `<div class="muted">Geen containers gevonden op dit netwerk.</div>`;
|
||||
@@ -123,11 +190,19 @@
|
||||
html += `<div class="muted">Geen.</div>`;
|
||||
} else {
|
||||
html += `<ul style="margin:0; padding-left:18px;">` + shared.map(([name, meta]) => {
|
||||
const owner = meta.networkOwnerName || meta.networkOwnerId || '?';
|
||||
return `<li><b>${esc(name)}</b> deelt netwerkstack met <b>${esc(owner)}</b></li>`;
|
||||
}).join('') + `</ul>`;
|
||||
}
|
||||
const ownerName = meta.networkOwnerName || '';
|
||||
const ownerId = meta.networkOwnerId || '';
|
||||
const owner = ownerName || ownerId || '?';
|
||||
|
||||
// Probeer netwerken van de owner te tonen (bv. ["pasta"] of ["none"])
|
||||
const ownerNets = ownerName && Array.isArray(byContainer[ownerName]) ? byContainer[ownerName] : [];
|
||||
const mode = ownerNets.find(n => n === 'pasta' || n === 'none' || n === 'host') || '';
|
||||
|
||||
const extra = mode ? ` <span class="muted">(mode: ${esc(mode)})</span>` : '';
|
||||
|
||||
return `<li><b>${esc(name)}</b> deelt netwerkstack met <b>${esc(owner)}</b>${extra}</li>`;
|
||||
}).join('') + `</ul>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -152,6 +227,160 @@
|
||||
if (status) status.textContent = 'Fout: ' + (e?.message || e);
|
||||
}
|
||||
}
|
||||
function isDefaultNetworkName(name) {
|
||||
return name === 'podman' || name === 'podman-default-kube-network';
|
||||
}
|
||||
|
||||
function fmtSubnets(net) {
|
||||
// We proberen verschillende mogelijke shapes:
|
||||
// - net.subnets: ["10.88.0.0/16"]
|
||||
// - net.subnets: [{subnet:"10.88.0.0/16", gateway:"10.88.0.1"}]
|
||||
// - net.ipam / net.ipamConfig: unknown => tonen we niets i.p.v. crash
|
||||
const s = net?.subnets;
|
||||
if (!s) return [];
|
||||
if (Array.isArray(s)) {
|
||||
return s.map(x => {
|
||||
if (typeof x === 'string') return x;
|
||||
if (x && typeof x === 'object') {
|
||||
const subnet = x.subnet || x.Subnet || '';
|
||||
const gw = x.gateway || x.Gateway || '';
|
||||
return gw ? `${subnet} (gw ${gw})` : subnet;
|
||||
}
|
||||
return String(x);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
return [String(s)];
|
||||
}
|
||||
|
||||
function flagsHTML(netName, netMeta) {
|
||||
const badges = [];
|
||||
|
||||
if (isDefaultNetworkName(netName)) badges.push(`<span class="badge">default</span>`);
|
||||
if (netMeta?.internal === true) badges.push(`<span class="badge">internal</span>`);
|
||||
if (netMeta?.dnsEnabled === true) badges.push(`<span class="badge">dns</span>`);
|
||||
if (netMeta?.ipv6Enabled === true) badges.push(`<span class="badge">ipv6</span>`);
|
||||
|
||||
// driver badge (maar niet voor mode, die tonen we in driver kolom)
|
||||
if (netMeta?.driver) {
|
||||
const d = String(netMeta.driver);
|
||||
if (d !== 'mode') {
|
||||
badges.push(`<span class="badge">${esc(d)}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
return badges.join(' ');
|
||||
}
|
||||
|
||||
function buildNetworksViewModel() {
|
||||
const list = Array.isArray(state.list?.networks) ? state.list.networks : [];
|
||||
const usageByNetwork = state.usage?.byNetwork || {};
|
||||
|
||||
// 1) Start met de lijst uit /api/networks (leidend voor metadata/unused)
|
||||
const items = list
|
||||
.map(n => {
|
||||
const name = n?.name || n?.Name || '';
|
||||
const usage = usageByNetwork[name] || null;
|
||||
const containers = Array.isArray(usage?.containers) ? usage.containers : [];
|
||||
return {
|
||||
name,
|
||||
meta: n || {},
|
||||
containers,
|
||||
containerCount: containers.length,
|
||||
};
|
||||
})
|
||||
.filter(x => x.name); // sanity
|
||||
|
||||
// 2) Voeg "usage-only" netwerken toe (zoals pasta/host/none) die niet in /api/networks staan
|
||||
const seen = new Set(items.map(x => x.name));
|
||||
for (const netName of Object.keys(usageByNetwork)) {
|
||||
if (seen.has(netName)) continue;
|
||||
|
||||
const usage = usageByNetwork[netName] || null;
|
||||
const containers = Array.isArray(usage?.containers) ? usage.containers : [];
|
||||
|
||||
// meta leeg, maar we geven driver=mode zodat het netjes in de UI komt
|
||||
items.push({
|
||||
name: netName,
|
||||
meta: { driver: 'mode' },
|
||||
containers,
|
||||
containerCount: containers.length,
|
||||
});
|
||||
seen.add(netName);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function applyFiltersAndSort(vm) {
|
||||
const q = (state.filters.q || '').trim().toLowerCase();
|
||||
const connectedOnly = !!state.filters.connectedOnly;
|
||||
const hideDefaults = !!state.filters.hideDefaults;
|
||||
const sharedOnly = !!state.filters.sharedOnly;
|
||||
|
||||
const byMeta = state.usage?.byContainerMeta || {};
|
||||
|
||||
let out = vm.slice();
|
||||
|
||||
if (hideDefaults) {
|
||||
out = out.filter(n => !isDefaultNetworkName(n.name));
|
||||
}
|
||||
|
||||
if (connectedOnly) {
|
||||
out = out.filter(n => n.containerCount > 0);
|
||||
}
|
||||
|
||||
if (sharedOnly) {
|
||||
const byMeta = state.usage?.byContainerMeta || {};
|
||||
const byContainer = state.usage?.byContainer || {};
|
||||
|
||||
// alle containers met networkMode=container:...
|
||||
const sharedEntries = Object.entries(byMeta)
|
||||
.filter(([_, meta]) => String(meta?.networkMode ?? "").startsWith("container:"));
|
||||
|
||||
// welke netwerken zijn "relevant" voor shared netns?
|
||||
const sharedNetworks = new Set();
|
||||
|
||||
// helper: voeg netwerken van containerKey toe via byContainer
|
||||
function addNetworksFor(containerKey) {
|
||||
const nets = byContainer[containerKey];
|
||||
if (Array.isArray(nets)) {
|
||||
for (const n of nets) sharedNetworks.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, meta] of sharedEntries) {
|
||||
addNetworksFor(name); // netwerken van de shared container zelf
|
||||
|
||||
// netwerken van de owner/infra (heel belangrijk in jouw geval)
|
||||
const ownerName = meta?.networkOwnerName;
|
||||
if (ownerName) addNetworksFor(ownerName);
|
||||
}
|
||||
|
||||
out = out.filter(n => sharedNetworks.has(n.name));
|
||||
}
|
||||
|
||||
if (q) {
|
||||
out = out.filter(n => {
|
||||
const nameHit = n.name.toLowerCase().includes(q);
|
||||
const driver = (n.meta?.driver || '').toLowerCase();
|
||||
const driverHit = driver.includes(q);
|
||||
const subnets = fmtSubnets(n.meta).join(' ').toLowerCase();
|
||||
const subnetHit = subnets.includes(q);
|
||||
return nameHit || driverHit || subnetHit;
|
||||
});
|
||||
}
|
||||
|
||||
const sort = state.filters.sort || 'name_asc';
|
||||
if (sort === 'containers_desc') {
|
||||
out.sort((a, b) => (b.containerCount - a.containerCount) || a.name.localeCompare(b.name));
|
||||
} else if (sort === 'driver_asc') {
|
||||
out.sort((a, b) => String(a.meta?.driver || '').localeCompare(String(b.meta?.driver || '')) || a.name.localeCompare(b.name));
|
||||
} else {
|
||||
out.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderNetworks() {
|
||||
const tbody = document.getElementById('networksTbody');
|
||||
@@ -163,26 +392,62 @@
|
||||
|
||||
const usage = state.usage;
|
||||
if (!usage || !usage.byNetwork) {
|
||||
tbody.innerHTML = `<tr><td colspan="3" class="muted">Geen data. Klik op Vernieuwen.</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="6" class="muted">Geen data. Klik op Vernieuwen.</td></tr>`;
|
||||
rel.innerHTML = `<div class="muted">Geen data.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const byNetwork = usage.byNetwork;
|
||||
const names = Object.keys(byNetwork).sort((a, b) => a.localeCompare(b));
|
||||
const vmAll = buildNetworksViewModel();
|
||||
const vm = applyFiltersAndSort(vmAll);
|
||||
|
||||
for (const row of vm) {
|
||||
const netName = row.name;
|
||||
const containers = row.containers;
|
||||
const meta = row.meta;
|
||||
|
||||
for (const netName of names) {
|
||||
const slot = byNetwork[netName] || {};
|
||||
const containers = slot.containers || [];
|
||||
const isOpen = state.expanded.has(netName);
|
||||
const arrow = isOpen ? '▾' : '▸';
|
||||
|
||||
const driver = meta?.driver || meta?.Driver || '';
|
||||
const subnets = fmtSubnets(meta);
|
||||
const subnetsHtml = subnets.length
|
||||
? `<div style="display:flex; flex-direction:column; gap:2px;">${subnets.slice(0, 2).map(s => `<span class="muted">${esc(s)}</span>`).join('')}${subnets.length > 2 ? `<span class="muted">+${subnets.length - 2}</span>` : ''}</div>`
|
||||
: (
|
||||
(meta?.driver === 'mode')
|
||||
? `<span class="muted">network mode</span>`
|
||||
: `<span class="muted">—</span>`
|
||||
);
|
||||
|
||||
// 1. Controleer of dit een shared netwerk is
|
||||
const isShared = containers.some(c => typeof c?.networkMode === 'string' && c.networkMode.startsWith('container:'));
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td><button class="btn small ghost" title="Open/dicht">${arrow}</button></td>
|
||||
<td>${esc(netName)}</td>
|
||||
<td>${containers.length}</td>
|
||||
<td>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<span>${esc(netName)}</span>
|
||||
${isDefaultNetworkName(netName) ? `<span class="badge">default</span>` : ``}
|
||||
</div>
|
||||
</td>
|
||||
<td>${driver
|
||||
? (String(driver) === 'mode' ? `<span class="badge">MODE</span>` : esc(driver))
|
||||
: `<span class="muted">—</span>`}
|
||||
</td>
|
||||
<td>${subnetsHtml}</td>
|
||||
|
||||
<td style="text-align:right;">
|
||||
<span>${containers.length}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="row gap" style="flex-wrap:wrap;">
|
||||
${isShared ? `<span class="badge">shared</span>` : ``}
|
||||
${flagsHTML(netName, meta)}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName));
|
||||
tbody.appendChild(tr);
|
||||
|
||||
@@ -190,7 +455,7 @@
|
||||
const tr2 = document.createElement('tr');
|
||||
tr2.innerHTML = `
|
||||
<td></td>
|
||||
<td colspan="2">
|
||||
<td colspan="5">
|
||||
<div style="padding:8px 0;">
|
||||
${renderNetworkUsersListHTML(netName, containers)}
|
||||
<div class="row gap" style="margin-top:8px;">
|
||||
@@ -211,7 +476,7 @@
|
||||
}
|
||||
|
||||
rel.innerHTML = renderNetworksRelationsHTML(usage);
|
||||
}
|
||||
}
|
||||
|
||||
function bindUiOnce() {
|
||||
const btn = document.getElementById('networksRefreshBtn');
|
||||
@@ -219,6 +484,59 @@
|
||||
btn.dataset.bound = '1';
|
||||
btn.addEventListener('click', refresh);
|
||||
}
|
||||
|
||||
const search = document.getElementById('networksSearch');
|
||||
const fConnected = document.getElementById('networksFilterConnected');
|
||||
const fHideDefaults = document.getElementById('networksFilterHideDefaults');
|
||||
const fShared = document.getElementById('networksFilterShared');
|
||||
const sort = document.getElementById('networksSort');
|
||||
|
||||
function rerender() {
|
||||
renderNetworks();
|
||||
}
|
||||
|
||||
if (search && !search.dataset.bound) {
|
||||
search.dataset.bound = '1';
|
||||
search.addEventListener('input', () => {
|
||||
state.filters.q = search.value || '';
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
|
||||
if (fConnected && !fConnected.dataset.bound) {
|
||||
fConnected.dataset.bound = '1';
|
||||
fConnected.addEventListener('change', () => {
|
||||
state.filters.connectedOnly = !!fConnected.checked;
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
|
||||
if (fHideDefaults && !fHideDefaults.dataset.bound) {
|
||||
fHideDefaults.dataset.bound = '1';
|
||||
state.filters.hideDefaults = !!fHideDefaults.checked;
|
||||
fHideDefaults.addEventListener('change', () => {
|
||||
state.filters.hideDefaults = !!fHideDefaults.checked;
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
|
||||
if (fShared && !fShared.dataset.bound) {
|
||||
fShared.dataset.bound = '1';
|
||||
fShared.addEventListener('change', () => {
|
||||
state.filters.sharedOnly = !!fShared.checked;
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
|
||||
if (sort && !sort.dataset.bound) {
|
||||
sort.dataset.bound = '1';
|
||||
sort.addEventListener('change', () => {
|
||||
state.filters.sort = sort.value || 'name_asc';
|
||||
rerender();
|
||||
});
|
||||
}
|
||||
|
||||
renderNetworksSummary();
|
||||
}
|
||||
|
||||
// Expose minimal API
|
||||
|
||||
Reference in New Issue
Block a user