From 6bf30db62c5127b6ebdf1b82da4081e942873f48 Mon Sep 17 00:00:00 2001 From: kodi Date: Wed, 4 Mar 2026 07:48:58 +0100 Subject: [PATCH] feat (ui): Light/Dark Theme added Complete --- webui/html/assets/css/app.css | 100 +++++++++++++++++++++++- webui/html/assets/js/tabs/containers.js | 3 + webui/html/assets/js/tabs/files.js | 30 ++++++- webui/html/assets/js/tabs/images.js | 36 +++++++-- webui/html/assets/js/tabs/networks.js | 44 ++++++++++- webui/html/index.html | 30 +++++-- 6 files changed, 224 insertions(+), 19 deletions(-) diff --git a/webui/html/assets/css/app.css b/webui/html/assets/css/app.css index 5bb5718..5daae73 100644 --- a/webui/html/assets/css/app.css +++ b/webui/html/assets/css/app.css @@ -56,6 +56,18 @@ --badge-yellow-text: #111111; --table-zebra: rgba(96,165,250,.03); --sticky-head-bg: rgba(17,26,46,.96); + --state-info-bg: rgba(96,165,250,.08); + --state-info-border: rgba(96,165,250,.35); + --state-empty-bg: rgba(251,191,36,.08); + --state-empty-border: rgba(251,191,36,.35); + --state-error-bg: rgba(251,113,133,.08); + --state-error-border: rgba(251,113,133,.35); + --map-tooltip-bg: rgba(11,18,32,.95); + --map-tooltip-border: rgba(36,52,95,.9); + --fs-body: 14px; + --fs-small: 12px; + --fs-title: 16px; + --fs-kpi: 22px; --radius: 14px; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; @@ -115,11 +127,25 @@ --badge-yellow-text: #fffbeb; --table-zebra: rgba(15,23,42,.03); --sticky-head-bg: rgba(255,255,255,.97); + --state-info-bg: rgba(37,99,235,.08); + --state-info-border: rgba(37,99,235,.3); + --state-empty-bg: rgba(180,83,9,.09); + --state-empty-border: rgba(180,83,9,.35); + --state-error-bg: rgba(225,29,72,.08); + --state-error-border: rgba(225,29,72,.35); + --map-tooltip-bg: rgba(255,255,255,.98); + --map-tooltip-border: rgba(148,163,184,.45); + --fs-body: 14px; + --fs-small: 12px; + --fs-title: 16px; + --fs-kpi: 22px; } *{box-sizing:border-box} body{ margin:0; font-family: var(--sans); + font-size: var(--fs-body); + line-height: 1.45; background: radial-gradient(1200px 600px at 20% 0%, var(--bg-grad-start) 0%, var(--bg) 55%); color: var(--text); } @@ -166,6 +192,10 @@ header{ cursor:pointer; user-select:none; font-size:14px; + transition: transform .12s ease, border-color .16s ease, background .16s ease; +} +.tab:hover{ + transform: translateY(-1px); } .tab.active{ background: var(--tab-active-bg); @@ -200,6 +230,7 @@ header{ .cardTitle{ font-weight:700; display:flex; gap:10px; align-items:center; + font-size: var(--fs-title); } .cardBody{padding:14px} .dashboardKpiGrid{ @@ -225,6 +256,7 @@ header{ font-size:13px; } .btn:hover{background: var(--btn2)} +.btn:active{transform: translateY(1px)} .btn.small{padding:7px 9px; border-radius: 10px} .btn.ghost{background: transparent} .btn.ok{border-color: rgba(45,212,191,.6)} @@ -280,7 +312,7 @@ tr:hover td{background: var(--hover-bg)} padding: 4px 8px; border: 1px solid var(--card-border); border-radius: 999px; - font-size: 12px; + font-size: var(--fs-small); color: var(--muted); white-space: nowrap; /* Tip: dit voorkomt dat je badge tekst afbreekt */ } @@ -338,18 +370,18 @@ pre.code{ } .statValue{ font-weight:800; - font-size: 20px; + font-size: var(--fs-kpi); line-height: 1.1; letter-spacing: .2px; } .statLabel{ color: var(--muted); - font-size: 12px; + font-size: var(--fs-small); margin-top: 4px; } .statHint{ color: var(--muted); - font-size: 12px; + font-size: var(--fs-small); margin-top: 6px; } @@ -543,11 +575,25 @@ pre{ .navLabel { white-space: nowrap; } +.navCount{ + margin-left: auto; + min-width: 26px; + text-align: center; + border: 1px solid var(--card-border); + border-radius: 999px; + font-size: 11px; + color: var(--muted); + padding: 2px 7px; + background: var(--input-bg); +} /* Collapsed: alleen icon zichtbaar */ .sidebar.collapsed .navLabel { display: none; } +.sidebar.collapsed .navCount { + display: none; +} .sidebar.collapsed .tab { justify-content: center; padding: 10px 10px; @@ -666,6 +712,35 @@ pre{ padding:7px 9px; font-size: 12px; } +.stateBox{ + border-radius: 12px; + padding: 10px 12px; + border: 1px solid var(--state-info-border); + background: var(--state-info-bg); +} +.stateBox.empty{ + border-color: var(--state-empty-border); + background: var(--state-empty-bg); +} +.stateBox.error{ + border-color: var(--state-error-border); + background: var(--state-error-bg); +} +.stateTitle{ + font-weight: 700; + margin-bottom: 4px; +} +.stateText{ + color: var(--muted); + font-size: var(--fs-small); +} +.viewAnim{ + animation: viewFade .18s ease; +} +@keyframes viewFade{ + from{opacity:.0; transform: translateY(3px)} + to{opacity:1; transform: translateY(0)} +} .data-table { width: 100%; border-collapse: collapse; @@ -734,6 +809,23 @@ pre{ border-radius: 14px; min-height: 420px; overflow: hidden; + position: relative; +} +.mapTooltip{ + position: absolute; + left: 0; + top: 0; + pointer-events: none; + max-width: 260px; + background: var(--map-tooltip-bg); + color: var(--text); + border: 1px solid var(--map-tooltip-border); + border-radius: 10px; + padding: 7px 9px; + font-size: 12px; + line-height: 1.35; + box-shadow: var(--shadow); + z-index: 3; } .mapLegend{ diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js index 81f7852..fa6c999 100644 --- a/webui/html/assets/js/tabs/containers.js +++ b/webui/html/assets/js/tabs/containers.js @@ -219,6 +219,9 @@ async function fetchContainers() { const list = Array.isArray(containers) ? containers : (containers?.containers || []); document.getElementById('countContainers').textContent = list.length; + if (typeof window.updateNavCount === 'function') { + window.updateNavCount('countNavContainers', list.length); + } const podsList = Array.isArray(pods) ? pods : []; const podStatus = {}; diff --git a/webui/html/assets/js/tabs/files.js b/webui/html/assets/js/tabs/files.js index 8e8cec2..95298d0 100644 --- a/webui/html/assets/js/tabs/files.js +++ b/webui/html/assets/js/tabs/files.js @@ -159,7 +159,19 @@ async function filesRefresh() { const treeEl = document.getElementById('filesTree'); treeEl.textContent = 'Laden...'; - const data = await api('/files/tree', 'GET'); + let data; + try { + data = await api('/files/tree', 'GET'); + } catch (e) { + if (typeof window.updateNavCount === 'function') { + window.updateNavCount('countNavFiles', 0); + } + treeEl.innerHTML = (typeof window.renderStateBox === 'function') + ? window.renderStateBox('error', 'Files laden mislukt', e.message || String(e)) + : 'Files laden mislukt.'; + filesUpdateEditorStatus(); + return; + } // Filter alleen systemd subtree const scoped = (data || []).filter(folder => { @@ -168,10 +180,24 @@ async function filesRefresh() { }); if (!scoped.length) { - treeEl.textContent = 'Geen bestanden gevonden onder systemd.'; + if (typeof window.updateNavCount === 'function') { + window.updateNavCount('countNavFiles', 0); + } + treeEl.innerHTML = (typeof window.renderStateBox === 'function') + ? window.renderStateBox('empty', 'Geen bestanden', 'Er zijn geen bestanden gevonden onder systemd.') + : 'Geen bestanden gevonden onder systemd.'; + filesUpdateEditorStatus(); return; } + let totalFiles = 0; + for (const folder of scoped) { + totalFiles += Array.isArray(folder?.files) ? folder.files.length : 0; + } + if (typeof window.updateNavCount === 'function') { + window.updateNavCount('countNavFiles', totalFiles); + } + // Bouw een geneste folder-tree uit de "platte" API response. const folderByPath = new Map(); for (const f of scoped) { diff --git a/webui/html/assets/js/tabs/images.js b/webui/html/assets/js/tabs/images.js index 69e633c..36a2b1b 100644 --- a/webui/html/assets/js/tabs/images.js +++ b/webui/html/assets/js/tabs/images.js @@ -2,18 +2,44 @@ let imagesData = []; let imagesSort = { field: null, dir: null }; async function loadImages() { - const res = await fetch("/api/images"); - const images = await res.json(); + const tbody = document.getElementById("images-tbody"); + try { + const res = await fetch("/api/images"); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const images = await res.json(); - imagesData = images; - updateSortIndicators(); - applyImageSorting(); + imagesData = Array.isArray(images) ? images : []; + if (typeof window.updateNavCount === "function") { + window.updateNavCount("countNavImages", imagesData.length); + } + updateSortIndicators(); + applyImageSorting(); + } catch (e) { + imagesData = []; + if (typeof window.updateNavCount === "function") { + window.updateNavCount("countNavImages", 0); + } + if (tbody) { + const box = (typeof window.renderStateBox === "function") + ? window.renderStateBox("error", "Images laden mislukt", e.message || String(e)) + : "Images laden mislukt."; + tbody.innerHTML = `${box}`; + } + } } function renderImages(images) { const tbody = document.getElementById("images-tbody"); tbody.innerHTML = ""; + if (!images.length) { + const box = (typeof window.renderStateBox === "function") + ? window.renderStateBox("empty", "Geen images", "Er zijn momenteel geen images gevonden.") + : "Geen images gevonden."; + tbody.innerHTML = `${box}`; + return; + } + images.forEach(img => { const tr = document.createElement("tr"); diff --git a/webui/html/assets/js/tabs/networks.js b/webui/html/assets/js/tabs/networks.js index bb43634..b78d8c0 100644 --- a/webui/html/assets/js/tabs/networks.js +++ b/webui/html/assets/js/tabs/networks.js @@ -96,11 +96,18 @@ ]); state.usage = usage; state.list = list; + if (typeof window.updateNavCount === 'function') { + const n = Array.isArray(list?.networks) ? list.networks.length : 0; + window.updateNavCount('countNavNetworks', n); + } if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`; renderNetworksSummary(); renderNetworks(); } catch (e) { console.error(e); + if (typeof window.updateNavCount === 'function') { + window.updateNavCount('countNavNetworks', 0); + } if (statusEl) statusEl.textContent = 'Fout: ' + (e?.message || e); } } @@ -620,6 +627,10 @@ // leeg host (placeholder weg) host.innerHTML = ''; + const tooltip = document.createElement('div'); + tooltip.className = 'mapTooltip'; + tooltip.style.display = 'none'; + host.appendChild(tooltip); const w = Math.max(600, host.clientWidth || 600); const h = Math.max(420, host.clientHeight || 420); @@ -694,11 +705,27 @@ d3.select(this).classed('graphActive', false); } }); + + const typeLabel = d.type === 'network' ? 'Netwerk' : 'Container'; + const extra = d.type === 'network' + ? `Driver: ${d?.meta?.driver || 'onbekend'}` + : (d?.pod ? `Pod: ${d.pod}` : 'Pod: -'); + tooltip.innerHTML = `${typeLabel}
${d.label || d.key}
${extra}`; + tooltip.style.display = 'block'; + }); + + node.on('mousemove', (ev) => { + const rect = host.getBoundingClientRect(); + const x = (ev.clientX - rect.left) + 14; + const y = (ev.clientY - rect.top) + 14; + tooltip.style.left = `${x}px`; + tooltip.style.top = `${y}px`; }); node.on('mouseleave', () => { node.classed('graphDim', false); link.classed('graphDim', false).classed('graphActive', false); + tooltip.style.display = 'none'; }); node.on('click', (ev, d) => { @@ -822,13 +849,24 @@ const usage = state.usage; if (!usage || !usage.byNetwork) { - tbody.innerHTML = `Geen data. Klik op Vernieuwen.`; - rel.innerHTML = `
Geen data.
`; + const box = (typeof window.renderStateBox === 'function') + ? window.renderStateBox('empty', 'Geen netwerkdata', 'Klik op Vernieuwen om netwerkdata op te halen.') + : 'Geen data.'; + tbody.innerHTML = `${box}`; + rel.innerHTML = box; return; } const vmAll = buildNetworksViewModel(); const vm = applyFiltersAndSort(vmAll); + if (!vm.length) { + const box = (typeof window.renderStateBox === 'function') + ? window.renderStateBox('empty', 'Geen resultaten', 'Pas filters aan of schakel opties uit om netwerken te tonen.') + : 'Geen resultaten.'; + tbody.innerHTML = `${box}`; + rel.innerHTML = box; + return; + } for (const row of vm) { const netName = row.name; @@ -1137,4 +1175,4 @@ // Bind when script loads (DOM is already mostly there because script is at end of body) bindUiOnce(); -})(); \ No newline at end of file +})(); diff --git a/webui/html/index.html b/webui/html/index.html index d3e2791..69c3225 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -48,16 +48,16 @@ 🏠Dashboard
- 📦Containers + 📦Containers-
- 🌐Netwerk + 🌐Netwerk-
- 📦Images + 📦Images-
- 📁Files + 📁Files-
@@ -486,6 +486,22 @@ return `${esc(s || 'unknown')}`; } + function updateNavCount(id, value) { + const el = document.getElementById(id); + if (!el) return; + el.textContent = Number.isFinite(Number(value)) ? String(value) : '-'; + } + + function renderStateBox(type, title, message) { + const t = type === 'error' ? 'error' : (type === 'empty' ? 'empty' : 'info'); + return ` +
+
${esc(title || 'Status')}
+
${esc(message || '')}
+
+ `; + } + // ---- Modal ---- function showModal(title, content) { document.getElementById('modalTitle').textContent = title; @@ -522,7 +538,10 @@ document.getElementById('tab-' + tab).classList.add('active'); document.querySelectorAll('[id^="view-"]').forEach(v => v.style.display='none'); - document.getElementById('view-' + tab).style.display = ''; + const view = document.getElementById('view-' + tab); + view.style.display = ''; + view.classList.remove('viewAnim'); + requestAnimationFrame(() => view.classList.add('viewAnim')); if (tab === 'files') { filesRefresh(); } @@ -610,6 +629,7 @@ const list = Array.isArray(containers) ? containers : (containers?.containers || []); const cCount = list.length; document.getElementById('countContainers').textContent = cCount; + updateNavCount('countNavContainers', cCount); } setApiState(true, 'API: OK'); setLastRefreshNow();