diff --git a/control/app.py b/control/app.py index cdf0d16..e8d35c6 100644 --- a/control/app.py +++ b/control/app.py @@ -921,6 +921,60 @@ def networks_usage(): return [str(x) for x in nn if x] return None + + def _extract_networks_from_inspect_obj(insp: dict) -> list[str]: + """ + Probeert netwerk-namen uit een container inspect te halen. + Ondersteunt varianten/casing die per Podman/driver kunnen verschillen. + """ + if not isinstance(insp, dict): + return [] + + candidates = [] + + # 1) meest voorkomend + ns = insp.get("NetworkSettings") + if isinstance(ns, dict): + candidates.append(ns.get("Networks")) + candidates.append(ns.get("networks")) + + # 2) sommige outputs hebben Networks top-level + candidates.append(insp.get("Networks")) + candidates.append(insp.get("networks")) + + # 3) extra varianten + n2 = insp.get("networkSettings") + if isinstance(n2, dict): + candidates.append(n2.get("Networks")) + candidates.append(n2.get("networks")) + + n3 = insp.get("Network") + if isinstance(n3, dict): + candidates.append(n3.get("Networks")) + candidates.append(n3.get("networks")) + + cfg = insp.get("Config") + if isinstance(cfg, dict): + candidates.append(cfg.get("Networks")) + candidates.append(cfg.get("networks")) + + # Normaliseer candidates naar lijst[str] + out: list[str] = [] + for val in candidates: + if isinstance(val, dict): + out.extend([str(k) for k in val.keys() if k]) + elif isinstance(val, list): + for x in val: + if isinstance(x, str) and x: + out.append(x) + elif isinstance(x, dict): + # Best-effort: soms bevat list entries met Name + nm = x.get("Name") or x.get("name") + if isinstance(nm, str) and nm: + out.append(nm) + + # uniq + stable sort + return sorted(set([n for n in out if isinstance(n, str) and n])) def _extract_networks_from_inspect(cid: str) -> tuple[list[str], dict]: """ @@ -933,12 +987,10 @@ def networks_usage(): insp = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/containers/{cid}/json") extra: dict = {} - # 1) normale inspect: NetworkSettings.Networks - ns = insp.get("NetworkSettings") if isinstance(insp, dict) else None - if isinstance(ns, dict): - nets = ns.get("Networks") - if isinstance(nets, dict): - return (list(nets.keys()), extra) + # 1) normale inspect: probeer meerdere paden + nets0 = _extract_networks_from_inspect_obj(insp) + if nets0: + return (nets0, extra) # 2) container network namespace mode: HostConfig.NetworkMode = "container:" hc = insp.get("HostConfig") if isinstance(insp, dict) else None @@ -952,15 +1004,7 @@ def networks_usage(): owner = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/containers/{owner_id}/json") # 1) netwerken van owner vinden (meerdere varianten) - owner_ns = owner.get("NetworkSettings") or {} - owner_nets = owner_ns.get("Networks") - - # fallback: sommige outputs hebben dit anders of leeg - if not isinstance(owner_nets, dict) or not owner_nets: - owner_nets = ( - (owner.get("networkSettings") or {}).get("Networks") or - (owner.get("Network") or {}).get("Networks") - ) + owner_nets_list = _extract_networks_from_inspect_obj(owner) # 2) owner naam vinden (meerdere varianten) owner_name = None @@ -988,14 +1032,43 @@ def networks_usage(): extra["networkOwnerName"] = str(owner_name).lstrip("/") # 3) netwerken returnen (als we ze gevonden hebben) - if isinstance(owner_nets, dict) and owner_nets: - return (list(owner_nets.keys()), extra) + if owner_nets_list: + return (owner_nets_list, extra) - # als owner_nets niet bruikbaar is: return leeg maar mét ownerName + # Extra fallback: probeer inspect via ownerName (soms werkt naam beter dan id) + try: + owner_name_for_lookup = extra.get("networkOwnerName") + if owner_name_for_lookup and owner_name_for_lookup != owner_id: + owner2 = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/containers/{owner_name_for_lookup}/json") + owner2_nets = _extract_networks_from_inspect_obj(owner2) + if owner2_nets: + return (owner2_nets, extra) + except Exception: + pass + + # Owner fallback: sommige infra containers gebruiken pasta/host/none + try: + ohc = owner.get("HostConfig") if isinstance(owner, dict) else None + if isinstance(ohc, dict): + onm = ohc.get("NetworkMode") + if isinstance(onm, str) and onm in ("pasta", "host", "none"): + # owner gebruikt geen Podman-netwerk; behandel als pseudo-netwerk + return ([onm], extra) + except Exception: + pass + return ([], extra) + + # 3) Special networking modes: pasta/host/none + # In deze modes bestaat vaak geen NetworkSettings.Networks map. + if isinstance(hc, dict): + nm2 = hc.get("NetworkMode") + if isinstance(nm2, str) and nm2 in ("pasta", "host", "none"): + extra["networkMode"] = nm2 + return ([nm2], extra) return ([], extra) - + # 2) Loop containers: verzamel netwerken for c in containers: if not isinstance(c, dict): @@ -1032,6 +1105,169 @@ def networks_usage(): pods = sorted({c.get("pod") for c in slot["containers"] if isinstance(c.get("pod"), str) and c.get("pod")}) slot["pods"] = [{"name": p} for p in pods] + # --- FALLBACK: derive owner networks via network-inspect (works for pod infra/shared netns) --- + # We look at shared netns containers (networkMode=container:...) and map their owner-id to networks + owner_ids: set[str] = set() + owner_names: dict[str, str] = {} # ownerId -> ownerName + + for cname, meta in by_container_meta.items(): + try: + mode = str((meta or {}).get("networkMode") or "") + except Exception: + mode = "" + if not mode.startswith("container:"): + continue + + owner_id = (meta or {}).get("networkOwnerId") + owner_name = (meta or {}).get("networkOwnerName") + if isinstance(owner_id, str) and owner_id: + owner_ids.add(owner_id) + if isinstance(owner_name, str) and owner_name: + owner_names[owner_id] = owner_name + + def _collect_container_ids_from_network_inspect(net_inspect) -> set[str]: + """ + Key-agnostic: scan alle strings in network inspect en verzamel hex IDs (12..64 chars). + Dit is robuust tegen schema-verschillen tussen netavark/cni/podman versies. + """ + ids: set[str] = set() + + def looks_like_hex_id(s: str) -> bool: + if not isinstance(s, str): + return False + s = s.strip() + if len(s) < 12 or len(s) > 64: + return False + # alleen hex chars + for ch in s: + if ch not in "0123456789abcdef": + return False + return True + + def walk(obj): + if obj is None: + return + if isinstance(obj, str): + # soms staat id als "container:" + if obj.startswith("container:"): + cand = obj.split("container:", 1)[1] + if looks_like_hex_id(cand): + ids.add(cand) + elif looks_like_hex_id(obj): + ids.add(obj) + return + if isinstance(obj, dict): + for k, v in obj.items(): + # keys kunnen ook ids zijn + if isinstance(k, str) and looks_like_hex_id(k): + ids.add(k) + walk(v) + return + if isinstance(obj, list): + for it in obj: + walk(it) + return + + walk(net_inspect) + return ids + + owner_networks_by_id: dict[str, set[str]] = {oid: set() for oid in owner_ids} + + # List networks + try: + nets_list = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/networks/json") + except Exception: + nets_list = [] + + net_names: list[str] = [] + if isinstance(nets_list, list): + for n in nets_list: + if isinstance(n, dict): + nm = n.get("name") or n.get("Name") + if isinstance(nm, str) and nm: + net_names.append(nm) + + # Inspect each network and see if it contains any owner_id + for net_name in sorted(set(net_names)): + try: + net_inspect = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/networks/{net_name}/json") + except Exception: + continue + + attached_ids = _collect_container_ids_from_network_inspect(net_inspect) + if not attached_ids: + continue + + for oid in owner_ids: + short = oid[:12] + for aid in attached_ids: + if not isinstance(aid, str) or not aid: + continue + # match exact / short / prefix + if aid == oid or aid == short or oid.startswith(aid) or aid.startswith(short): + owner_networks_by_id.setdefault(oid, set()).add(net_name) + break + + # Apply: if shared container or owner container has empty by_container[], fill it with owner's networks + for cname, meta in by_container_meta.items(): + try: + mode = str((meta or {}).get("networkMode") or "") + except Exception: + mode = "" + if not mode.startswith("container:"): + continue + + owner_id = (meta or {}).get("networkOwnerId") + if not (isinstance(owner_id, str) and owner_id): + continue + + owner_nets = sorted(owner_networks_by_id.get(owner_id, set())) + if not owner_nets: + continue + + # 1) fill owner-name entry (if known) + owner_name = (meta or {}).get("networkOwnerName") or owner_names.get(owner_id) + if isinstance(owner_name, str) and owner_name and not by_container.get(owner_name): + by_container[owner_name] = owner_nets + + # 2) fill shared container entry + if not by_container.get(cname): + by_container[cname] = owner_nets + + # --- FINALIZE: derive by_container from by_network (robust for pods/shared netns) --- + by_container_derived: dict[str, list[str]] = {} + + for net_name, info in (by_network or {}).items(): + containers2 = (info or {}).get("containers") or [] + for c2 in containers2: + if not isinstance(c2, dict): + continue + cname2 = c2.get("name") or c2.get("Name") + if not cname2: + continue + by_container_derived.setdefault(cname2, []).append(net_name) + + # dedupe + stable sort + for k, v in by_container_derived.items(): + by_container_derived[k] = sorted(set(v)) + + # merge: vul lege items in by_container, maar breek niks + for k, v in by_container_derived.items(): + if not by_container.get(k): + by_container[k] = v + + # --- shared netns: shared containers erven owner-netwerken (als owner bekend is) --- + for cname, meta in by_container_meta.items(): + try: + mode = str((meta or {}).get("networkMode") or "") + except Exception: + mode = "" + if not mode.startswith("container:"): + continue + owner = (meta or {}).get("networkOwnerName") or (meta or {}).get("networkOwnerId") + if owner and by_container.get(owner) and not by_container.get(cname): + by_container[cname] = by_container[owner] + return {"byNetwork": by_network, "byContainer": by_container, "byContainerMeta": by_container_meta} diff --git a/webui/html/assets/css/app.css b/webui/html/assets/css/app.css index 1d368d4..a26e7ad 100644 --- a/webui/html/assets/css/app.css +++ b/webui/html/assets/css/app.css @@ -144,14 +144,17 @@ th,td{ } th{color: var(--muted); font-weight:600} tr:hover td{background: rgba(96,165,250,.06)} -.badge{ - display:inline-flex; - align-items:center; - border:1px solid rgba(36,52,95,.9); - padding:4px 8px; - border-radius:999px; - font-size:12px; +.badge { + display: inline-flex; + align-items: center; + vertical-align: middle; + line-height: 1; + padding: 4px 8px; + border: 1px solid rgba(36, 52, 95, .9); + border-radius: 999px; + font-size: 12px; color: var(--muted); + white-space: nowrap; /* Tip: dit voorkomt dat je badge tekst afbreekt */ } pre.code{ padding:10px; @@ -189,6 +192,38 @@ pre.code{ .split{grid-template-columns: 1fr 1fr} } +/* Stats / summary cards */ +.statGrid{ + display:grid; + grid-template-columns: 1fr; + gap:10px; +} +@media (min-width: 980px){ + .statGrid{grid-template-columns: repeat(4, 1fr)} +} +.statCard{ + background: rgba(8,12,25,.45); + border:1px solid rgba(36,52,95,.9); + border-radius: 14px; + padding:10px 12px; +} +.statValue{ + font-weight:800; + font-size: 20px; + line-height: 1.1; + letter-spacing: .2px; +} +.statLabel{ + color: var(--muted); + font-size: 12px; + margin-top: 4px; +} +.statHint{ + color: var(--muted); + font-size: 12px; + margin-top: 6px; +} + /* Modal */ .modalBack{ position: fixed; inset:0; @@ -278,6 +313,34 @@ pre{ .menuItem.warn{ border-color: rgba(251,191,36,.35); } .menuItem.bad{ border-color: rgba(251,113,133,.35); } +/* css voor herziene Netwerken pagina */ +/* Toolbar controls */ +.toolbar .input, +.toolbar .select{ + background: rgba(8,12,25,.45); + border:1px solid rgba(36,52,95,.9); + color: var(--fg); + border-radius: 12px; + padding:8px 10px; + outline: none; +} +.toolbar .input::placeholder{ color: rgba(200,210,255,.45); } + +.chip{ + display:inline-flex; + align-items:center; + gap:8px; + padding:7px 10px; + border-radius: 999px; + border:1px solid rgba(36,52,95,.9); + background: rgba(8,12,25,.35); + color: var(--fg); + cursor:pointer; + user-select:none; + font-size: 13px; +} +.chip input{ accent-color: var(--accent); } + /* ========================= Layout: Sidebar + Main ========================= */ @@ -403,12 +466,6 @@ pre{ text-align: left; } -.badge { - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; -} - .badge-green { background: #2ecc71; color: white; diff --git a/webui/html/assets/js/tabs/networks.js b/webui/html/assets/js/tabs/networks.js index 6a9dae3..833f3d5 100644 --- a/webui/html/assets/js/tabs/networks.js +++ b/webui/html/assets/js/tabs/networks.js @@ -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 = ` +
-
Netwerken
+
-
Verbonden containers
+
-
Ongebruikt
+
-
Shared netns
+ `; + 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.
`; @@ -123,11 +190,19 @@ html += `
Geen.
`; } else { html += ``; - } + 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; } @@ -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(`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'); @@ -163,26 +392,62 @@ const usage = state.usage; if (!usage || !usage.byNetwork) { - tbody.innerHTML = `Geen data. Klik op Vernieuwen.`; + 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)); + 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 + ? `
    ${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)} - ${containers.length} + +
    + ${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); @@ -190,7 +455,7 @@ const tr2 = document.createElement('tr'); tr2.innerHTML = ` - +
    ${renderNetworkUsersListHTML(netName, containers)}
    @@ -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 diff --git a/webui/html/index.html b/webui/html/index.html index 33efb52..aeefbc3 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -122,14 +122,44 @@
    +
    +
    +
    + + + + + + + + + +
    +
    - - - + + + + + +
    Netwerk# containersNaamDriverSubnetsContainersFlags