// webui/html/assets/js/tabs/networks.js
(function () {
// Gebruik gedeelde helpers uit index.html (staan op window in non-module scripts)
const apiGet = window.apiGet || (async (path) => {
const r = await fetch('/api' + path, { headers: { 'Accept': 'application/json' } });
if (!r.ok) {
const t = await r.text().catch(() => '');
throw new Error(`HTTP ${r.status} ${path}: ${t}`);
}
return r.json();
});
const esc = window.esc || ((s) => String(s)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''')
);
function cssId(s) {
return String(s).replaceAll(/[^a-zA-Z0-9_-]/g, '_');
}
async function fetchNetworksUsage() {
return apiGet('/networks/usage');
}
async function fetchNetworksList() {
// returns { networks: [...] }
return apiGet('/networks');
}
async function fetchNetworkInspect(name) {
return apiGet('/networks/' + encodeURIComponent(name));
}
const state = {
expanded: new Set(),
usage: null,
list: null,
inspectCache: new Map(),
filters: {
q: '',
connectedOnly: false,
hideDefaults: true,
sharedOnly: false,
sort: 'name_asc',
},
};
function toggleNetworkRow(name) {
if (state.expanded.has(name)) state.expanded.delete(name);
else state.expanded.add(name);
renderNetworks();
}
async function refresh() {
const statusEl = document.getElementById('networksStatus');
if (statusEl) statusEl.textContent = 'Bezig met laden...';
try {
const [usage, list] = await Promise.all([
fetchNetworksUsage(),
fetchNetworksList(),
]);
state.usage = usage;
state.list = list;
if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
renderNetworksSummary();
renderNetworks();
} catch (e) {
console.error(e);
if (statusEl) statusEl.textContent = 'Fout: ' + (e?.message || e);
}
}
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 = `
`;
return;
}
const s = computeNetworkStats();
host.innerHTML = `
${s.totalNetworks}
Netwerken
${s.usedNetworks} in gebruik
${s.connectedContainers}
Verbonden containers
${s.unusedNetworks}
Ongebruikt
${s.sharedNetns}
Shared netns
`;
}
function renderNetworkUsersListHTML(netName, containers) {
if (!containers.length) {
return `Geen containers gevonden op dit netwerk.
`;
}
const rows = containers.map(c => {
const cname = c.name || '?';
const pod = c.pod || '';
const shared = c.networkMode && String(c.networkMode).startsWith('container:');
const via = c.networkOwnerName ? `via ${c.networkOwnerName}` : (c.networkOwnerId ? `via ${c.networkOwnerId}` : '');
const badges = [];
if (pod) badges.push(`pod: ${esc(pod)}`);
if (shared) badges.push(`shared netns`);
if (via) badges.push(`${esc(via)}`);
return `
${esc(cname)}
${badges.length ? `${badges.join('')}
` : ``}
`;
}).join('');
return `
Containers op ${esc(netName)}:
`;
}
function renderNetworksRelationsHTML(usage) {
const byContainer = usage.byContainer || {};
const byMeta = usage.byContainerMeta || {};
const multi = Object.entries(byContainer)
.filter(([_, nets]) => Array.isArray(nets) && nets.length > 1)
.sort((a, b) => a[0].localeCompare(b[0]));
const shared = Object.entries(byMeta)
.filter(([_, meta]) => meta && meta.networkMode && String(meta.networkMode).startsWith('container:'))
.sort((a, b) => a[0].localeCompare(b[0]));
let html = '';
html += `Containers met meerdere netwerken
`;
if (!multi.length) {
html += `Geen.
`;
} else {
html += `` + multi.map(([name, nets]) =>
`- ${esc(name)} → ${esc(nets.join(', '))}
`
).join('') + `
`;
}
html += `Shared network namespace
`;
if (!shared.length) {
html += `Geen.
`;
} else {
html += `` + shared.map(([name, meta]) => {
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 ? ` (mode: ${esc(mode)})` : '';
return `- ${esc(name)} deelt netwerkstack met ${esc(owner)}${extra}
`;
}).join('') + `
`;
}
return html;
}
async function onInspectNetwork(name) {
const box = document.getElementById('inspectBox-' + cssId(name));
const status = document.getElementById('inspectStatus-' + cssId(name));
if (status) status.textContent = 'Inspect laden...';
try {
let data = state.inspectCache.get(name);
if (!data) {
data = await fetchNetworkInspect(name);
state.inspectCache.set(name, data);
}
if (box) {
box.style.display = '';
box.textContent = JSON.stringify(data, null, 2);
}
if (status) status.textContent = '';
} catch (e) {
console.error(e);
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(`default`);
if (netMeta?.internal === true) badges.push(`internal`);
if (netMeta?.dnsEnabled === true) badges.push(`dns`);
if (netMeta?.ipv6Enabled === true) badges.push(`ipv6`);
// 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(`${esc(d)}`);
}
}
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');
const rel = document.getElementById('networksRelations');
if (!tbody || !rel) return;
tbody.innerHTML = '';
rel.innerHTML = '';
const usage = state.usage;
if (!usage || !usage.byNetwork) {
tbody.innerHTML = `| Geen data. Klik op Vernieuwen. |
`;
rel.innerHTML = `Geen data.
`;
return;
}
const vmAll = buildNetworksViewModel();
const vm = applyFiltersAndSort(vmAll);
for (const row of vm) {
const netName = row.name;
const containers = row.containers;
const meta = row.meta;
const isOpen = state.expanded.has(netName);
const arrow = isOpen ? '▾' : '▸';
const driver = meta?.driver || meta?.Driver || '';
const subnets = fmtSubnets(meta);
const subnetsHtml = subnets.length
? `${subnets.slice(0, 2).map(s => `${esc(s)}`).join('')}${subnets.length > 2 ? `+${subnets.length - 2}` : ''}
`
: (
(meta?.driver === 'mode')
? `network mode`
: `—`
);
// 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 = `
|
${esc(netName)}
${isDefaultNetworkName(netName) ? `default` : ``}
|
${driver
? (String(driver) === 'mode' ? `MODE` : esc(driver))
: `—`}
|
${subnetsHtml} |
${containers.length}
|
${isShared ? `shared` : ``}
${flagsHTML(netName, meta)}
|
`;
tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName));
tbody.appendChild(tr);
if (isOpen) {
const tr2 = document.createElement('tr');
tr2.innerHTML = `
|
${renderNetworkUsersListHTML(netName, containers)}
|
`;
tbody.appendChild(tr2);
const btn = tr2.querySelector('button[data-inspect]');
btn.addEventListener('click', async () => {
await onInspectNetwork(netName);
});
}
}
rel.innerHTML = renderNetworksRelationsHTML(usage);
}
function bindUiOnce() {
const btn = document.getElementById('networksRefreshBtn');
if (btn && !btn.dataset.bound) {
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
window.mvpNetworks = {
refresh,
state,
bindUiOnce,
};
// Bind when script loads (DOM is already mostly there because script is at end of body)
bindUiOnce();
})();