feat (ui): netwerk map functionaliteit verder uitgebreid en polish

This commit is contained in:
kodi
2026-02-24 12:37:17 +01:00
parent 289d222707
commit ec13059437
4 changed files with 100 additions and 13 deletions
+9 -1
View File
@@ -22,6 +22,14 @@ DirectoryIndex index.html
ErrorLog /proc/self/fd/2 ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 combined CustomLog /proc/self/fd/1 combined
#ProxyPreserveHost On
#ProxyPass "/api/" "http://127.0.0.1:8000/api/"
#ProxyPassReverse "/api/" "http://127.0.0.1:8000/api/"
# allow long-running upstream requests (image builds)
Timeout 600
ProxyTimeout 600
ProxyPreserveHost On ProxyPreserveHost On
ProxyPass "/api/" "http://127.0.0.1:8000/api/" ProxyPass "/api/" "http://127.0.0.1:8000/api/" connectiontimeout=5 timeout=600 retry=0
ProxyPassReverse "/api/" "http://127.0.0.1:8000/api/" ProxyPassReverse "/api/" "http://127.0.0.1:8000/api/"
+9 -1
View File
@@ -165,7 +165,15 @@ async function buildImage() {
}) })
}); });
const data = await res.json(); const ct = (res.headers.get("content-type") || "").toLowerCase();
let data;
if (ct.includes("application/json")) {
data = await res.json();
} else {
const text = await res.text();
data = { ok: res.ok, status: res.status, non_json: true, body: text.slice(0, 4000) };
}
if (!res.ok) { if (!res.ok) {
outputBox.value += "\nERROR:\n" + JSON.stringify(data, null, 2); outputBox.value += "\nERROR:\n" + JSON.stringify(data, null, 2);
+73 -7
View File
@@ -48,6 +48,7 @@
connectedOnly: false, connectedOnly: false,
hideDefaults: true, hideDefaults: true,
sharedOnly: false, sharedOnly: false,
showModes: true,
sort: 'name_asc', sort: 'name_asc',
}, },
}; };
@@ -126,6 +127,18 @@
return { totalNetworks, usedNetworks, unusedNetworks, connectedContainers, sharedNetns }; return { totalNetworks, usedNetworks, unusedNetworks, connectedContainers, sharedNetns };
} }
function buildMapStatus(label, nodesCount, linksCount) {
const f = state.filters || {};
const flags = [];
if (f.connectedOnly) flags.push('connected');
if (f.hideDefaults) flags.push('hide-defaults');
if (f.sharedOnly) flags.push('shared');
if (f.showModes === false) flags.push('modes-off');
const flagTxt = flags.length ? ` | ${flags.join(', ')}` : '';
return `${label} | ${nodesCount} nodes, ${linksCount} links${flagTxt}`;
}
function renderNetworksSummary() { function renderNetworksSummary() {
const host = document.getElementById('networksSummary'); const host = document.getElementById('networksSummary');
if (!host) return; if (!host) return;
@@ -345,6 +358,7 @@
const connectedOnly = !!state.filters.connectedOnly; const connectedOnly = !!state.filters.connectedOnly;
const hideDefaults = !!state.filters.hideDefaults; const hideDefaults = !!state.filters.hideDefaults;
const sharedOnly = !!state.filters.sharedOnly; const sharedOnly = !!state.filters.sharedOnly;
const showModes = state.filters.showModes !== false;
const byMeta = state.usage?.byContainerMeta || {}; const byMeta = state.usage?.byContainerMeta || {};
@@ -353,6 +367,9 @@
if (hideDefaults) { if (hideDefaults) {
out = out.filter(n => !isDefaultNetworkName(n.name)); out = out.filter(n => !isDefaultNetworkName(n.name));
} }
if (!showModes) {
out = out.filter(n => String(n.meta?.driver || '') !== 'mode');
}
if (connectedOnly) { if (connectedOnly) {
out = out.filter(n => n.containerCount > 0); out = out.filter(n => n.containerCount > 0);
@@ -696,8 +713,8 @@
// simulation // simulation
const sim = d3.forceSimulation(model.nodes) const sim = d3.forceSimulation(model.nodes)
.force('link', d3.forceLink(model.links).id(d => d.id).distance(45)) .force('link', d3.forceLink(model.links).id(d => d.id).distance(80))
.force('charge', d3.forceManyBody().strength(-40)) .force('charge', d3.forceManyBody().strength(-30))
.force('center', d3.forceCenter(w / 2, h / 2).strength(0.15)) .force('center', d3.forceCenter(w / 2, h / 2).strength(0.15))
.force('collide', d3.forceCollide().radius(d => d.type === 'network' ? 18 : 16)) .force('collide', d3.forceCollide().radius(d => d.type === 'network' ? 18 : 16))
.on('tick', () => { .on('tick', () => {
@@ -731,7 +748,7 @@
showDetailPanel(networkName); showDetailPanel(networkName);
const s = document.getElementById('networksMapStatus'); const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Detail: ${networkName}`; if (s) s.textContent = buildMapStatus(`Detail: ${networkName}`, model.nodes.length, model.links.length);
} }
function showDetailPanel(networkName) { function showDetailPanel(networkName) {
@@ -792,7 +809,7 @@
hideDetailPanel(); hideDetailPanel();
const s = document.getElementById('networksMapStatus'); const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`; if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length);
} }
function renderNetworks() { function renderNetworks() {
@@ -927,6 +944,10 @@
resetBtn.dataset.bound = '1'; resetBtn.dataset.bound = '1';
resetBtn.addEventListener('click', () => { resetBtn.addEventListener('click', () => {
if (!graphCtx) return; if (!graphCtx) return;
// wis highlight/dim
graphCtx.g.selectAll('.graphNode').classed('graphDim', false);
graphCtx.g.selectAll('.graphLink').classed('graphDim', false).classed('graphActive', false);
// reset zoom // reset zoom
graphCtx.svg graphCtx.svg
@@ -990,7 +1011,13 @@
}); });
const s = document.getElementById('networksMapStatus'); const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Container: ${id.slice(0, 12)}`; if (s) {
const label = (state.mapMode === 'detail' && state.selectedNetwork)
? `Detail: ${state.selectedNetwork}`
: 'Global';
s.textContent = buildMapStatus(label, graphCtx.model.nodes.length, graphCtx.model.links.length) + ` | container: ${id.slice(0, 12)}`;
}
}); });
} }
@@ -999,13 +1026,24 @@
const fHideDefaults = document.getElementById('networksFilterHideDefaults'); const fHideDefaults = document.getElementById('networksFilterHideDefaults');
const fShared = document.getElementById('networksFilterShared'); const fShared = document.getElementById('networksFilterShared');
const sort = document.getElementById('networksSort'); const sort = document.getElementById('networksSort');
const mapShowModes = document.getElementById('networksMapShowModes');
const mapConnectedOnly = document.getElementById('networksMapConnectedOnly');
function rerender() { function rerender() {
renderNetworks(); renderNetworks();
if (state.view === 'map') { if (state.view === 'map') {
// als we in detail zitten → detail opnieuw renderen
if (state.mapMode === 'detail' && state.selectedNetwork) {
openNetworkDetail(state.selectedNetwork);
return;
}
// anders global map
const model = buildGlobalGraphModel(); const model = buildGlobalGraphModel();
const s = document.getElementById('networksMapStatus'); const s = document.getElementById('networksMapStatus');
if (s) s.textContent = `Kaart: ${model.meta.nodes} nodes, ${model.meta.links} links`; if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length);
renderGraph(model); renderGraph(model);
} }
} }
@@ -1051,8 +1089,36 @@
}); });
} }
if (mapConnectedOnly && !mapConnectedOnly.dataset.bound) {
mapConnectedOnly.dataset.bound = '1';
mapConnectedOnly.checked = !!state.filters.connectedOnly;
mapConnectedOnly.addEventListener('change', () => {
state.filters.connectedOnly = !!mapConnectedOnly.checked;
// sync ook de tabel checkbox (zodat alles hetzelfde blijft)
const tableChk = document.getElementById('networksFilterConnected');
if (tableChk) tableChk.checked = !!state.filters.connectedOnly;
rerender();
});
}
if (mapShowModes && !mapShowModes.dataset.bound) {
mapShowModes.dataset.bound = '1';
mapShowModes.checked = state.filters.showModes !== false;
mapShowModes.addEventListener('change', () => {
state.filters.showModes = !!mapShowModes.checked;
rerender();
});
}
renderNetworksSummary(); renderNetworksSummary();
setNetworksView(state.view); setNetworksView(state.view);
// sync kaart-checkbox bij init
if (mapConnectedOnly) {
mapConnectedOnly.checked = !!state.filters.connectedOnly;
}
} }
// Expose minimal API // Expose minimal API
@@ -1066,7 +1132,7 @@
console.table(model.nodes.slice(0, 12)); console.table(model.nodes.slice(0, 12));
console.table(model.links.slice(0, 12)); console.table(model.links.slice(0, 12));
return model; return model;
}, },
}; };
// Bind when script loads (DOM is already mostly there because script is at end of body) // Bind when script loads (DOM is already mostly there because script is at end of body)
+9 -4
View File
@@ -162,14 +162,19 @@
<button class="btn small" type="button" id="networksMapResetBtn">Reset view</button> <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" type="button" id="networksMapLayoutBtn">Auto-layout</button>
<button class="btn small ghost" type="button" id="networksMapLegendBtn">Legenda</button> <button class="btn small ghost" type="button" id="networksMapLegendBtn">Legenda</button>
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span> <label class="chk" style="display:flex; align-items:center; gap:8px; margin-left:10px;">
<input type="checkbox" id="networksMapShowModes" checked>
<span class="muted">Toon modes</span>
</label>
<label class="chk" style="display:flex; align-items:center; gap:8px; margin-left:10px;">
<input type="checkbox" id="networksMapConnectedOnly">
<span class="muted">Alleen verbonden</span>
</label>
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span>
</div> </div>
</div> </div>
<div id="networksMapHost" class="mapHost"> <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>
<div id="networksDetailPanel" class="mapDetail" style="display:none;"> <div id="networksDetailPanel" class="mapDetail" style="display:none;">
<div class="mapDetailHeader"> <div class="mapDetailHeader">