From cffb5e94a2f2c82554d5e0a50f457591f8d680e5 Mon Sep 17 00:00:00 2001 From: kodi Date: Sun, 22 Feb 2026 07:48:39 +0100 Subject: [PATCH] refract(ui): WebUI: netwerklogica uit index.html gehaald naar networks.js --- webui/html/assets/js/tabs/networks.js | 233 ++++++++++++++++++++++++++ webui/html/index.html | 215 +----------------------- 2 files changed, 236 insertions(+), 212 deletions(-) create mode 100644 webui/html/assets/js/tabs/networks.js diff --git a/webui/html/assets/js/tabs/networks.js b/webui/html/assets/js/tabs/networks.js new file mode 100644 index 0000000..6a9dae3 --- /dev/null +++ b/webui/html/assets/js/tabs/networks.js @@ -0,0 +1,233 @@ +// 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 += ``; + } + + html += `

    Shared network namespace

    `; + if (!shared.length) { + html += `
    Geen.
    `; + } else { + html += ``; + } + + 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(); +})(); \ No newline at end of file diff --git a/webui/html/index.html b/webui/html/index.html index 92e4f93..33efb52 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -367,19 +367,6 @@ return r.json(); } - async function fetchNetworksUsage() { - return apiGet('/networks/usage'); - } - - async function fetchNetworksList() { - // returns { networks: [...] } - return apiGet('/networks'); - } - - async function fetchNetworkInspect(name) { - return apiGet('/networks/' + encodeURIComponent(name)); - } - function badgeFromStatus(s) { const t = (s || '').toLowerCase(); if (t.includes('running') || t === 'running' || t === 'active') return `${esc(s)}`; @@ -428,7 +415,8 @@ filesRefresh(); } if (tab === 'networks') { - networksRefresh(); + window.mvpNetworks?.bindUiOnce?.(); + window.mvpNetworks?.refresh?.(); } if (tab === "images") { loadImages(); @@ -910,201 +898,6 @@ showModal('daemon-reload fout', e.stack || e.message); } } - // ========================= - // Files tab (systemd subtree) - // ========================= - - let networksState = { - expanded: new Set(), // welke networks staan open - usage: null, - list: null, - inspectCache: new Map(), // name -> inspect json - }; - - function toggleNetworkRow(name) { - if (networksState.expanded.has(name)) networksState.expanded.delete(name); - else networksState.expanded.add(name); - renderNetworks(); // re-render - } - - async function networksRefresh() { - const statusEl = document.getElementById('networksStatus'); - statusEl.textContent = 'Bezig met laden...'; - - try { - const [usage, list] = await Promise.all([ - fetchNetworksUsage(), - fetchNetworksList(), - ]); - networksState.usage = usage; - networksState.list = list; - statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`; - renderNetworks(); - } catch (e) { - console.error(e); - statusEl.textContent = 'Fout: ' + (e?.message || e); - } - } - - function renderNetworks() { - const tbody = document.getElementById('networksTbody'); - const rel = document.getElementById('networksRelations'); - tbody.innerHTML = ''; - rel.innerHTML = ''; - - const usage = networksState.usage; - if (!usage || !usage.byNetwork) { - tbody.innerHTML = `Geen data. Klik op Vernieuwen.`; - rel.innerHTML = `
    Geen data.
    `; - return; - } - - const byNetwork = usage.byNetwork; - - // Sorteer op naam - const names = Object.keys(byNetwork).sort((a,b) => a.localeCompare(b)); - - // Build rows - for (const netName of names) { - const slot = byNetwork[netName] || {}; - const containers = slot.containers || []; - const isOpen = networksState.expanded.has(netName); - - const arrow = isOpen ? '▾' : '▸'; - - // hoofd-rij - const tr = document.createElement('tr'); - tr.innerHTML = ` - - ${esc(netName)} - ${containers.length} - `; - tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName)); - tbody.appendChild(tr); - - // detail-rij (uitklap) - if (isOpen) { - const tr2 = document.createElement('tr'); - tr2.innerHTML = ` - - -
    - ${renderNetworkUsersListHTML(netName, containers)} -
    - - -
    - -
    - - `; - tbody.appendChild(tr2); - - // inspect button handler - const btn = tr2.querySelector('button[data-inspect]'); - btn.addEventListener('click', async () => { - await onInspectNetwork(netName); - }); - } - } - - // Relaties sectie - rel.innerHTML = renderNetworksRelationsHTML(usage); - } - - function renderNetworkUsersListHTML(netName, containers) { - if (!containers.length) { - return `
    Geen containers gevonden op dit netwerk.
    `; - } - - // lijstje - 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 || {}; - - // A) containers met meerdere netwerken - const multi = Object.entries(byContainer) - .filter(([_, nets]) => Array.isArray(nets) && nets.length > 1) - .sort((a,b) => a[0].localeCompare(b[0])); - - // B) shared netns containers - 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 += ``; - } - - html += `

    Shared network namespace

    `; - if (!shared.length) { - html += `
    Geen.
    `; - } else { - html += ``; - } - - return html; - } - - // helper: maak safe id voor DOM - function cssId(s) { - return String(s).replaceAll(/[^a-zA-Z0-9_-]/g, '_'); - } - - async function onInspectNetwork(name) { - const key = name; - const box = document.getElementById('inspectBox-' + cssId(name)); - const status = document.getElementById('inspectStatus-' + cssId(name)); - - status.textContent = 'Inspect laden...'; - - try { - let data = networksState.inspectCache.get(key); - if (!data) { - data = await fetchNetworkInspect(name); - networksState.inspectCache.set(key, data); - } - box.style.display = ''; - box.textContent = JSON.stringify(data, null, 2); - status.textContent = ''; - } catch (e) { - console.error(e); - status.textContent = 'Fout: ' + (e?.message || e); - } - } // ========================= // Files tab (systemd subtree) @@ -1335,9 +1128,6 @@ const t = document.getElementById('sidebarToggle'); if (t) t.onclick = toggleSidebar; - // Networks refresh button: 1x listener (niet in setTab!) - document.getElementById('networksRefreshBtn')?.addEventListener('click', networksRefresh); - // Files editor: CodeMirror init (alleen als textarea bestaat) const taFiles = document.getElementById('filesEditor'); if (taFiles && window.CodeMirror) { @@ -1357,6 +1147,7 @@ setInterval(() => { pingApi(); }, 20000); })(); +