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;
|
||||
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,
|
||||
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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user