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:
+254
-18
@@ -922,6 +922,60 @@ def networks_usage():
|
|||||||
|
|
||||||
return None
|
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]:
|
def _extract_networks_from_inspect(cid: str) -> tuple[list[str], dict]:
|
||||||
"""
|
"""
|
||||||
Returns: (networks, extra_info)
|
Returns: (networks, extra_info)
|
||||||
@@ -933,12 +987,10 @@ def networks_usage():
|
|||||||
insp = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/containers/{cid}/json")
|
insp = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/containers/{cid}/json")
|
||||||
extra: dict = {}
|
extra: dict = {}
|
||||||
|
|
||||||
# 1) normale inspect: NetworkSettings.Networks
|
# 1) normale inspect: probeer meerdere paden
|
||||||
ns = insp.get("NetworkSettings") if isinstance(insp, dict) else None
|
nets0 = _extract_networks_from_inspect_obj(insp)
|
||||||
if isinstance(ns, dict):
|
if nets0:
|
||||||
nets = ns.get("Networks")
|
return (nets0, extra)
|
||||||
if isinstance(nets, dict):
|
|
||||||
return (list(nets.keys()), extra)
|
|
||||||
|
|
||||||
# 2) container network namespace mode: HostConfig.NetworkMode = "container:<id>"
|
# 2) container network namespace mode: HostConfig.NetworkMode = "container:<id>"
|
||||||
hc = insp.get("HostConfig") if isinstance(insp, dict) else None
|
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")
|
owner = _podman_get_json_checked(f"{PODMAN_API_BASE}/libpod/containers/{owner_id}/json")
|
||||||
|
|
||||||
# 1) netwerken van owner vinden (meerdere varianten)
|
# 1) netwerken van owner vinden (meerdere varianten)
|
||||||
owner_ns = owner.get("NetworkSettings") or {}
|
owner_nets_list = _extract_networks_from_inspect_obj(owner)
|
||||||
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2) owner naam vinden (meerdere varianten)
|
# 2) owner naam vinden (meerdere varianten)
|
||||||
owner_name = None
|
owner_name = None
|
||||||
@@ -988,12 +1032,41 @@ def networks_usage():
|
|||||||
extra["networkOwnerName"] = str(owner_name).lstrip("/")
|
extra["networkOwnerName"] = str(owner_name).lstrip("/")
|
||||||
|
|
||||||
# 3) netwerken returnen (als we ze gevonden hebben)
|
# 3) netwerken returnen (als we ze gevonden hebben)
|
||||||
if isinstance(owner_nets, dict) and owner_nets:
|
if owner_nets_list:
|
||||||
return (list(owner_nets.keys()), extra)
|
return (owner_nets_list, extra)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# als owner_nets niet bruikbaar is: return leeg maar mét ownerName
|
|
||||||
return ([], extra)
|
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)
|
return ([], extra)
|
||||||
|
|
||||||
# 2) Loop containers: verzamel netwerken
|
# 2) Loop containers: verzamel netwerken
|
||||||
@@ -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")})
|
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]
|
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:<id>"
|
||||||
|
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}
|
return {"byNetwork": by_network, "byContainer": by_container, "byContainerMeta": by_container_meta}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -144,14 +144,17 @@ th,td{
|
|||||||
}
|
}
|
||||||
th{color: var(--muted); font-weight:600}
|
th{color: var(--muted); font-weight:600}
|
||||||
tr:hover td{background: rgba(96,165,250,.06)}
|
tr:hover td{background: rgba(96,165,250,.06)}
|
||||||
.badge{
|
.badge {
|
||||||
display:inline-flex;
|
display: inline-flex;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
border:1px solid rgba(36,52,95,.9);
|
vertical-align: middle;
|
||||||
padding:4px 8px;
|
line-height: 1;
|
||||||
border-radius:999px;
|
padding: 4px 8px;
|
||||||
font-size:12px;
|
border: 1px solid rgba(36, 52, 95, .9);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
white-space: nowrap; /* Tip: dit voorkomt dat je badge tekst afbreekt */
|
||||||
}
|
}
|
||||||
pre.code{
|
pre.code{
|
||||||
padding:10px;
|
padding:10px;
|
||||||
@@ -189,6 +192,38 @@ pre.code{
|
|||||||
.split{grid-template-columns: 1fr 1fr}
|
.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 */
|
/* Modal */
|
||||||
.modalBack{
|
.modalBack{
|
||||||
position: fixed; inset:0;
|
position: fixed; inset:0;
|
||||||
@@ -278,6 +313,34 @@ pre{
|
|||||||
.menuItem.warn{ border-color: rgba(251,191,36,.35); }
|
.menuItem.warn{ border-color: rgba(251,191,36,.35); }
|
||||||
.menuItem.bad{ border-color: rgba(251,113,133,.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
|
Layout: Sidebar + Main
|
||||||
========================= */
|
========================= */
|
||||||
@@ -403,12 +466,6 @@ pre{
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-green {
|
.badge-green {
|
||||||
background: #2ecc71;
|
background: #2ecc71;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -40,6 +40,13 @@
|
|||||||
usage: null,
|
usage: null,
|
||||||
list: null,
|
list: null,
|
||||||
inspectCache: new Map(),
|
inspectCache: new Map(),
|
||||||
|
filters: {
|
||||||
|
q: '',
|
||||||
|
connectedOnly: false,
|
||||||
|
hideDefaults: true,
|
||||||
|
sharedOnly: false,
|
||||||
|
sort: 'name_asc',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggleNetworkRow(name) {
|
function toggleNetworkRow(name) {
|
||||||
@@ -60,6 +67,7 @@
|
|||||||
state.usage = usage;
|
state.usage = usage;
|
||||||
state.list = list;
|
state.list = list;
|
||||||
if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
|
if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
|
||||||
|
renderNetworksSummary();
|
||||||
renderNetworks();
|
renderNetworks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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) {
|
function renderNetworkUsersListHTML(netName, containers) {
|
||||||
if (!containers.length) {
|
if (!containers.length) {
|
||||||
return `<div class="muted">Geen containers gevonden op dit netwerk.</div>`;
|
return `<div class="muted">Geen containers gevonden op dit netwerk.</div>`;
|
||||||
@@ -123,11 +190,19 @@
|
|||||||
html += `<div class="muted">Geen.</div>`;
|
html += `<div class="muted">Geen.</div>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<ul style="margin:0; padding-left:18px;">` + shared.map(([name, meta]) => {
|
html += `<ul style="margin:0; padding-left:18px;">` + shared.map(([name, meta]) => {
|
||||||
const owner = meta.networkOwnerName || meta.networkOwnerId || '?';
|
const ownerName = meta.networkOwnerName || '';
|
||||||
return `<li><b>${esc(name)}</b> deelt netwerkstack met <b>${esc(owner)}</b></li>`;
|
const ownerId = meta.networkOwnerId || '';
|
||||||
}).join('') + `</ul>`;
|
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;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +227,160 @@
|
|||||||
if (status) status.textContent = 'Fout: ' + (e?.message || 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(`<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() {
|
function renderNetworks() {
|
||||||
const tbody = document.getElementById('networksTbody');
|
const tbody = document.getElementById('networksTbody');
|
||||||
@@ -163,26 +392,62 @@
|
|||||||
|
|
||||||
const usage = state.usage;
|
const usage = state.usage;
|
||||||
if (!usage || !usage.byNetwork) {
|
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>`;
|
rel.innerHTML = `<div class="muted">Geen data.</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const byNetwork = usage.byNetwork;
|
const vmAll = buildNetworksViewModel();
|
||||||
const names = Object.keys(byNetwork).sort((a, b) => a.localeCompare(b));
|
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 isOpen = state.expanded.has(netName);
|
||||||
const arrow = isOpen ? '▾' : '▸';
|
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');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><button class="btn small ghost" title="Open/dicht">${arrow}</button></td>
|
<td><button class="btn small ghost" title="Open/dicht">${arrow}</button></td>
|
||||||
<td>${esc(netName)}</td>
|
<td>
|
||||||
<td>${containers.length}</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));
|
tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName));
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
@@ -190,7 +455,7 @@
|
|||||||
const tr2 = document.createElement('tr');
|
const tr2 = document.createElement('tr');
|
||||||
tr2.innerHTML = `
|
tr2.innerHTML = `
|
||||||
<td></td>
|
<td></td>
|
||||||
<td colspan="2">
|
<td colspan="5">
|
||||||
<div style="padding:8px 0;">
|
<div style="padding:8px 0;">
|
||||||
${renderNetworkUsersListHTML(netName, containers)}
|
${renderNetworkUsersListHTML(netName, containers)}
|
||||||
<div class="row gap" style="margin-top:8px;">
|
<div class="row gap" style="margin-top:8px;">
|
||||||
@@ -211,7 +476,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
rel.innerHTML = renderNetworksRelationsHTML(usage);
|
rel.innerHTML = renderNetworksRelationsHTML(usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindUiOnce() {
|
function bindUiOnce() {
|
||||||
const btn = document.getElementById('networksRefreshBtn');
|
const btn = document.getElementById('networksRefreshBtn');
|
||||||
@@ -219,6 +484,59 @@
|
|||||||
btn.dataset.bound = '1';
|
btn.dataset.bound = '1';
|
||||||
btn.addEventListener('click', refresh);
|
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
|
// Expose minimal API
|
||||||
|
|||||||
+33
-3
@@ -122,14 +122,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="networksSummary" class="statGrid" style="margin:12px 14px 0 14px;"></div>
|
||||||
<div id="networksStatus" class="muted" style="margin:8px 0;"></div>
|
<div id="networksStatus" class="muted" style="margin:8px 0;"></div>
|
||||||
|
<div id="networksToolbar" class="toolbar" style="margin:10px 14px 0 14px;">
|
||||||
|
<div class="row gap" style="flex-wrap:wrap; align-items:center;">
|
||||||
|
<input id="networksSearch" class="input" type="search" placeholder="Zoek netwerk, subnet of driver…" style="min-width:260px; flex:1;" />
|
||||||
|
|
||||||
|
<label class="chip">
|
||||||
|
<input id="networksFilterConnected" type="checkbox" />
|
||||||
|
Alleen verbonden
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="chip">
|
||||||
|
<input id="networksFilterHideDefaults" type="checkbox" checked />
|
||||||
|
Verberg standaard
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="chip">
|
||||||
|
<input id="networksFilterShared" type="checkbox" />
|
||||||
|
Shared netns
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select id="networksSort" class="select">
|
||||||
|
<option value="name_asc">Sorteer: Naam (A→Z)</option>
|
||||||
|
<option value="containers_desc">Sorteer: Containers (hoog→laag)</option>
|
||||||
|
<option value="driver_asc">Sorteer: Driver (A→Z)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table" id="networksTable">
|
<table class="table" id="networksTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:40px;"></th>
|
<th style="width:42px;"></th>
|
||||||
<th>Netwerk</th>
|
<th>Naam</th>
|
||||||
<th># containers</th>
|
<th style="width:120px;">Driver</th>
|
||||||
|
<th>Subnets</th>
|
||||||
|
<th style="width:140px; text-align:right;">Containers</th>
|
||||||
|
<th style="width:220px;">Flags</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="networksTbody"></tbody>
|
<tbody id="networksTbody"></tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user