// 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(),
};
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()}`;
renderNetworks();
} catch (e) {
console.error(e);
if (statusEl) statusEl.textContent = 'Fout: ' + (e?.message || e);
}
}
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 owner = meta.networkOwnerName || meta.networkOwnerId || '?';
return `- ${esc(name)} deelt netwerkstack met ${esc(owner)}
`;
}).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 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 byNetwork = usage.byNetwork;
const names = Object.keys(byNetwork).sort((a, b) => a.localeCompare(b));
for (const netName of names) {
const slot = byNetwork[netName] || {};
const containers = slot.containers || [];
const isOpen = state.expanded.has(netName);
const arrow = isOpen ? '▾' : '▸';
const tr = document.createElement('tr');
tr.innerHTML = `
|
${esc(netName)} |
${containers.length} |
`;
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);
}
}
// 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();
})();