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
+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(' ');
}
@@ -381,6 +415,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');
@@ -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)