feat(gui): netwerk tab netwerk layout grafisch
This commit is contained in:
@@ -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()
|
||||||
@@ -488,3 +488,87 @@ pre{
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
opacity: .75;
|
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); }
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
usage: null,
|
usage: null,
|
||||||
list: null,
|
list: null,
|
||||||
inspectCache: new Map(),
|
inspectCache: new Map(),
|
||||||
|
view: 'table', // 'table' | 'map'
|
||||||
filters: {
|
filters: {
|
||||||
q: '',
|
q: '',
|
||||||
connectedOnly: false,
|
connectedOnly: false,
|
||||||
@@ -48,6 +49,39 @@
|
|||||||
sort: 'name_asc',
|
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) {
|
function toggleNetworkRow(name) {
|
||||||
if (state.expanded.has(name)) state.expanded.delete(name);
|
if (state.expanded.has(name)) state.expanded.delete(name);
|
||||||
@@ -382,6 +416,117 @@
|
|||||||
return out;
|
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() {
|
function renderNetworks() {
|
||||||
const tbody = document.getElementById('networksTbody');
|
const tbody = document.getElementById('networksTbody');
|
||||||
const rel = document.getElementById('networksRelations');
|
const rel = document.getElementById('networksRelations');
|
||||||
@@ -485,6 +630,52 @@
|
|||||||
btn.addEventListener('click', refresh);
|
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 search = document.getElementById('networksSearch');
|
||||||
const fConnected = document.getElementById('networksFilterConnected');
|
const fConnected = document.getElementById('networksFilterConnected');
|
||||||
const fHideDefaults = document.getElementById('networksFilterHideDefaults');
|
const fHideDefaults = document.getElementById('networksFilterHideDefaults');
|
||||||
@@ -493,6 +684,11 @@
|
|||||||
|
|
||||||
function rerender() {
|
function rerender() {
|
||||||
renderNetworks();
|
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) {
|
if (search && !search.dataset.bound) {
|
||||||
@@ -537,6 +733,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderNetworksSummary();
|
renderNetworksSummary();
|
||||||
|
setNetworksView(state.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose minimal API
|
// Expose minimal API
|
||||||
@@ -544,6 +741,13 @@
|
|||||||
refresh,
|
refresh,
|
||||||
state,
|
state,
|
||||||
bindUiOnce,
|
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)
|
// Bind when script loads (DOM is already mostly there because script is at end of body)
|
||||||
|
|||||||
+32
-1
@@ -128,6 +128,11 @@
|
|||||||
<div class="row gap" style="flex-wrap:wrap; align-items:center;">
|
<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;" />
|
<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">
|
<label class="chip">
|
||||||
<input id="networksFilterConnected" type="checkbox" />
|
<input id="networksFilterConnected" type="checkbox" />
|
||||||
Alleen verbonden
|
Alleen verbonden
|
||||||
@@ -149,6 +154,32 @@
|
|||||||
<option value="driver_asc">Sorteer: Driver (A→Z)</option>
|
<option value="driver_asc">Sorteer: Driver (A→Z)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<table class="table" id="networksTable">
|
<table class="table" id="networksTable">
|
||||||
@@ -166,7 +197,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="margin-top:12px;">
|
<div class="card" id="networksRelationsCard" style="margin-top:12px;">
|
||||||
<div class="cardHeader">
|
<div class="cardHeader">
|
||||||
<h2>Relaties</h2>
|
<h2>Relaties</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user