feat(gui): netwerk tab netwerk layout grafisch

This commit is contained in:
kodi
2026-02-22 18:24:57 +01:00
parent e4214858ac
commit 18ee367e1d
4 changed files with 360 additions and 3 deletions
+38
View File
@@ -0,0 +1,38 @@
import os
# Bestanden of mappen die we NIET willen zien
EXCLUDE_DIRS = {'.git', 'node_modules', '__pycache__', 'venv', '.next', 'dist', 'build'}
EXCLUDE_FILES = {'collect_code.py', 'project_context.txt', 'package-lock.json', '.DS_Store'}
# Welke bestandstypes we wel willen verzamelen
INCLUDE_EXTENSIONS = {'.js', '.jsx', '.ts', '.tsx', '.py', '.html', '.css', '.json'}
def collect_code():
output_file = "project_context.txt"
with open(output_file, "w", encoding="utf-8") as f:
for root, dirs, files in os.walk("."):
# Filter uitgesloten mappen
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
for file in files:
if file in EXCLUDE_FILES:
continue
ext = os.path.splitext(file)[1]
if ext in INCLUDE_EXTENSIONS:
full_path = os.path.join(root, file)
f.write(f"\n{'='*50}\n")
f.write(f"FILE: {full_path}\n")
f.write(f"{'='*50}\n\n")
try:
with open(full_path, "r", encoding="utf-8") as code_file:
f.write(code_file.read())
except Exception as e:
f.write(f"Fout bij lezen bestand: {e}")
f.write("\n")
print(f"Klaar! Alle code staat in {output_file}")
if __name__ == "__main__":
collect_code()
+84
View File
@@ -488,3 +488,87 @@ pre{
margin-left: 4px;
opacity: .75;
}
/* ===== Netwerken: Tabel/Kaart toggle + kaart placeholders (STAP 3A-1) ===== */
.segToggle{
display:inline-flex;
border:1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.04);
border-radius: 12px;
overflow:hidden;
}
.segToggle .seg{
appearance:none;
border:0;
background: transparent;
color: inherit;
padding: 8px 10px;
font-size: 13px;
line-height: 1;
cursor:pointer;
opacity: .85;
}
.segToggle .seg:hover{ opacity: 1; }
.segToggle .seg.active{
background: rgba(255,255,255,0.08);
opacity: 1;
}
.mapHost{
border: 1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.18);
border-radius: 14px;
min-height: 420px;
overflow: hidden;
}
.mapLegend{
margin-top: 10px;
padding: 10px 12px;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.18);
border-radius: 14px;
max-width: 360px;
}
.mapLegend .legendTitle{
font-weight: 700;
margin-bottom: 8px;
}
.mapLegend .legendRow{
display:flex;
align-items:center;
gap: 10px;
margin: 6px 0;
}
.mapLegend .legendSwatch{
width: 14px;
height: 14px;
border-radius: 6px;
display:inline-block;
border: 1px solid rgba(255,255,255,0.14);
}
.mapLegend .legendSwatch.link{
width: 18px;
height: 2px;
border-radius: 2px;
border: 0;
background: rgba(255,255,255,0.22);
}
.mapLegend .legendSwatch.shared{
width: 18px;
height: 2px;
border-radius: 0;
border: 0;
background: rgba(255,255,255,0.22);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,0.22), rgba(255,255,255,0.22) 6px, transparent 6px, transparent 10px);
}
.mapLegend .legendSwatch.net{ background: rgba(80,160,255,0.35); }
.mapLegend .legendSwatch.ctr{ background: rgba(150,230,150,0.30); }
+205 -1
View File
@@ -40,6 +40,7 @@
usage: null,
list: null,
inspectCache: new Map(),
view: 'table', // 'table' | 'map'
filters: {
q: '',
connectedOnly: false,
@@ -48,6 +49,39 @@
sort: 'name_asc',
},
};
function setNetworksView(view) {
const v = (view === 'map') ? 'map' : 'table';
state.view = v;
const table = document.getElementById('networksTable');
const relCard = document.getElementById('networksRelationsCard');
const mapWrap = document.getElementById('networksMapWrap');
if (table) table.style.display = (v === 'table') ? '' : 'none';
if (relCard) relCard.style.display = (v === 'table') ? '' : 'none';
if (mapWrap) mapWrap.style.display = (v === 'map') ? '' : 'none';
// Toggle active state in segmented control
const toggle = document.getElementById('networksViewToggle');
if (toggle) {
const btns = toggle.querySelectorAll('button[data-view]');
btns.forEach(b => {
const bv = b.getAttribute('data-view');
if (bv === v) b.classList.add('active');
else b.classList.remove('active');
});
}
if (v === 'map') {
try {
const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
} catch (e) {
console.error('[networks] buildGlobalGraphModel failed', e);
}
}
}
function toggleNetworkRow(name) {
if (state.expanded.has(name)) state.expanded.delete(name);
@@ -266,7 +300,7 @@
if (d !== 'mode') {
badges.push(`<span class="badge">${esc(d)}</span>`);
}
}
}
return badges.join(' ');
}
@@ -382,6 +416,117 @@
return out;
}
function _ellipsize(s, n) {
s = String(s ?? '');
if (s.length <= n) return s;
return s.slice(0, Math.max(0, n - 1)) + '…';
}
function buildGlobalGraphModel() {
// We nemen exact dezelfde selectie als de tabel.
// 1) Bouw dezelfde rows als tabel
const vm = buildNetworksViewModel();
const rows = applyFiltersAndSort(vm);
const usage = state.usage || {};
const byNetwork = usage.byNetwork || {};
const byContainerMeta = usage.byContainerMeta || {};
const nodes = [];
const links = [];
const nodeIndex = new Map(); // key -> nodeId
const idByName = new Map(); // "mvp-webui" -> "<id>"
const nameById = new Map(); // "<id>" -> "mvp-webui"
function addNode(key, node) {
if (nodeIndex.has(key)) return nodeIndex.get(key);
node.id = key; // stabiel ID
nodes.push(node);
nodeIndex.set(key, key);
return key;
}
// Network nodes: alleen netwerken die in de huidige tabel-selectie zitten
for (const r of rows) {
const nkey = `net:${r.name}`;
addNode(nkey, {
type: 'network',
key: r.name,
label: _ellipsize(r.name, 22),
meta: r.meta || {},
usageCount: Number(r.containerCount || 0),
flags: {
internal: !!r.meta?.internal,
dnsEnabled: !!r.meta?.dnsEnabled,
ipv6Enabled: !!r.meta?.ipv6Enabled,
isDefault: !!r.meta?.isDefault,
driver: r.meta?.driver || ''
}
});
// Container links + container nodes vanuit usage.byNetwork
const netUsage = byNetwork[r.name];
const ctrs = (netUsage && Array.isArray(netUsage.containers)) ? netUsage.containers : [];
for (const c of ctrs) {
const ckey = `ctr:${c.id}`;
addNode(ckey, {
type: 'container',
key: c.id,
label: _ellipsize(c.name || c.id.slice(0, 12), 22),
name: c.name || '',
pod: c.pod || '',
networkMode: c.networkMode || '',
});
if (c.name) idByName.set(String(c.name), String(c.id));
nameById.set(String(c.id), String(c.name || ''));
links.push({
type: 'attach',
source: nkey,
target: ckey
});
}
}
// Optionele shared-netns links (stippellijn) op basis van byContainerMeta.networkMode=container:<id>
// We maken alleen links als beide kanten in het huidige model zitten (licht + consistent met filter)
for (const [ctrId, cm] of Object.entries(byContainerMeta)) {
const rawKey = String(ctrId);
const mode = String(cm?.networkMode || '');
if (!mode.startsWith('container:')) continue;
const ownerId = mode.split(':', 2)[1] || '';
if (!ownerId) continue;
// byContainerMeta kan keys hebben als NAME of ID → normaliseer naar ID als we kunnen
const childId = idByName.get(rawKey) || rawKey;
const ownerKey = `ctr:${ownerId}`;
const childKey = `ctr:${childId}`;
if (nodeIndex.has(ownerKey) && nodeIndex.has(childKey)) {
links.push({
type: 'shared',
source: ownerKey,
target: childKey,
ownerName: cm?.networkOwnerName || '',
ownerId: cm?.networkOwnerId || ''
});
}
}
// Kleine meta voor debug / later
const meta = {
view: 'global',
networksSelected: rows.length,
nodes: nodes.length,
links: links.length
};
return { nodes, links, meta };
}
function renderNetworks() {
const tbody = document.getElementById('networksTbody');
const rel = document.getElementById('networksRelations');
@@ -485,6 +630,52 @@
btn.addEventListener('click', refresh);
}
// View toggle (Tabel/Kaart)
const viewToggle = document.getElementById('networksViewToggle');
if (viewToggle && !viewToggle.dataset.bound) {
viewToggle.dataset.bound = '1';
viewToggle.addEventListener('click', (ev) => {
const t = ev.target;
if (!(t instanceof HTMLElement)) return;
const b = t.closest('button[data-view]');
if (!b) return;
setNetworksView(b.getAttribute('data-view'));
});
}
// Map toolbar placeholders (no-op for now)
const legendBtn = document.getElementById('networksMapLegendBtn');
const legend = document.getElementById('networksMapLegend');
if (legendBtn && !legendBtn.dataset.bound) {
legendBtn.dataset.bound = '1';
legendBtn.addEventListener('click', () => {
if (!legend) return;
legend.style.display = (legend.style.display === 'none' || !legend.style.display) ? '' : 'none';
});
}
const resetBtn = document.getElementById('networksMapResetBtn');
if (resetBtn && !resetBtn.dataset.bound) {
resetBtn.dataset.bound = '1';
resetBtn.addEventListener('click', () => {
// In 3C koppelen we dit aan zoom reset + simulation reset
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = 'Reset view (placeholder)';
setTimeout(() => { if (s) s.textContent = 'Kaartweergave (placeholder)'; }, 900);
});
}
const layoutBtn = document.getElementById('networksMapLayoutBtn');
if (layoutBtn && !layoutBtn.dataset.bound) {
layoutBtn.dataset.bound = '1';
layoutBtn.addEventListener('click', () => {
// In 3C koppelen we dit aan simulation.restart()
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = 'Auto-layout (placeholder)';
setTimeout(() => { if (s) s.textContent = 'Kaartweergave (placeholder)'; }, 900);
});
}
const search = document.getElementById('networksSearch');
const fConnected = document.getElementById('networksFilterConnected');
const fHideDefaults = document.getElementById('networksFilterHideDefaults');
@@ -493,6 +684,11 @@
function rerender() {
renderNetworks();
if (state.view === 'map') {
const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`;
}
}
if (search && !search.dataset.bound) {
@@ -537,6 +733,7 @@
}
renderNetworksSummary();
setNetworksView(state.view);
}
// Expose minimal API
@@ -544,6 +741,13 @@
refresh,
state,
bindUiOnce,
debugGraph: () => {
const model = buildGlobalGraphModel();
console.log('[networks] graph meta', model.meta);
console.table(model.nodes.slice(0, 12));
console.table(model.links.slice(0, 12));
return model;
},
};
// Bind when script loads (DOM is already mostly there because script is at end of body)
+32 -1
View File
@@ -128,6 +128,11 @@
<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;" />
<div class="segToggle" id="networksViewToggle" title="Weergave">
<button class="seg active" type="button" data-view="table">Tabel</button>
<button class="seg" type="button" data-view="map">Kaart</button>
</div>
<label class="chip">
<input id="networksFilterConnected" type="checkbox" />
Alleen verbonden
@@ -149,6 +154,32 @@
<option value="driver_asc">Sorteer: Driver (A→Z)</option>
</select>
</div>
<!-- Kaartweergave (STAP 3A-1: alleen UI/placeholder, geen D3) -->
<div id="networksMapWrap" style="display:none; margin:10px 14px 0 14px;">
<div class="toolbar" style="margin:0 0 10px 0;">
<div class="row gap" style="flex-wrap:wrap; align-items:center;">
<!-- Zoekveld is hetzelfde als tabel (networksSearch) -->
<button class="btn small" type="button" id="networksMapResetBtn">Reset view</button>
<button class="btn small" type="button" id="networksMapLayoutBtn">Auto-layout</button>
<button class="btn small ghost" type="button" id="networksMapLegendBtn">Legenda</button>
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span>
</div>
</div>
<div id="networksMapHost" class="mapHost">
<div class="muted" style="padding:12px;">
Kaartweergave is actief. (STAP 3A-1: alleen layout/controls. D3 rendering komt in 3C.)
</div>
</div>
<div id="networksMapLegend" class="mapLegend" style="display:none;">
<div class="legendTitle">Legenda</div>
<div class="legendRow"><span class="legendSwatch net"></span> Netwerk</div>
<div class="legendRow"><span class="legendSwatch ctr"></span> Container</div>
<div class="legendRow"><span class="legendSwatch link"></span> Verbinding</div>
<div class="legendRow"><span class="legendSwatch shared"></span> Shared netns (stippellijn)</div>
</div>
</div>
</div>
<table class="table" id="networksTable">
@@ -166,7 +197,7 @@
</table>
</div>
<div class="card" style="margin-top:12px;">
<div class="card" id="networksRelationsCard" style="margin-top:12px;">
<div class="cardHeader">
<h2>Relaties</h2>
</div>