feat (ui): Light/Dark Theme added Complete

This commit is contained in:
kodi
2026-03-04 07:48:58 +01:00
parent ebefd2d80c
commit 6bf30db62c
6 changed files with 224 additions and 19 deletions
+96 -4
View File
@@ -56,6 +56,18 @@
--badge-yellow-text: #111111; --badge-yellow-text: #111111;
--table-zebra: rgba(96,165,250,.03); --table-zebra: rgba(96,165,250,.03);
--sticky-head-bg: rgba(17,26,46,.96); --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; --radius: 14px;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --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"; --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; --badge-yellow-text: #fffbeb;
--table-zebra: rgba(15,23,42,.03); --table-zebra: rgba(15,23,42,.03);
--sticky-head-bg: rgba(255,255,255,.97); --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} *{box-sizing:border-box}
body{ body{
margin:0; margin:0;
font-family: var(--sans); 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%); background: radial-gradient(1200px 600px at 20% 0%, var(--bg-grad-start) 0%, var(--bg) 55%);
color: var(--text); color: var(--text);
} }
@@ -166,6 +192,10 @@ header{
cursor:pointer; cursor:pointer;
user-select:none; user-select:none;
font-size:14px; font-size:14px;
transition: transform .12s ease, border-color .16s ease, background .16s ease;
}
.tab:hover{
transform: translateY(-1px);
} }
.tab.active{ .tab.active{
background: var(--tab-active-bg); background: var(--tab-active-bg);
@@ -200,6 +230,7 @@ header{
.cardTitle{ .cardTitle{
font-weight:700; font-weight:700;
display:flex; gap:10px; align-items:center; display:flex; gap:10px; align-items:center;
font-size: var(--fs-title);
} }
.cardBody{padding:14px} .cardBody{padding:14px}
.dashboardKpiGrid{ .dashboardKpiGrid{
@@ -225,6 +256,7 @@ header{
font-size:13px; font-size:13px;
} }
.btn:hover{background: var(--btn2)} .btn:hover{background: var(--btn2)}
.btn:active{transform: translateY(1px)}
.btn.small{padding:7px 9px; border-radius: 10px} .btn.small{padding:7px 9px; border-radius: 10px}
.btn.ghost{background: transparent} .btn.ghost{background: transparent}
.btn.ok{border-color: rgba(45,212,191,.6)} .btn.ok{border-color: rgba(45,212,191,.6)}
@@ -280,7 +312,7 @@ tr:hover td{background: var(--hover-bg)}
padding: 4px 8px; padding: 4px 8px;
border: 1px solid var(--card-border); border: 1px solid var(--card-border);
border-radius: 999px; border-radius: 999px;
font-size: 12px; font-size: var(--fs-small);
color: var(--muted); color: var(--muted);
white-space: nowrap; /* Tip: dit voorkomt dat je badge tekst afbreekt */ white-space: nowrap; /* Tip: dit voorkomt dat je badge tekst afbreekt */
} }
@@ -338,18 +370,18 @@ pre.code{
} }
.statValue{ .statValue{
font-weight:800; font-weight:800;
font-size: 20px; font-size: var(--fs-kpi);
line-height: 1.1; line-height: 1.1;
letter-spacing: .2px; letter-spacing: .2px;
} }
.statLabel{ .statLabel{
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: var(--fs-small);
margin-top: 4px; margin-top: 4px;
} }
.statHint{ .statHint{
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: var(--fs-small);
margin-top: 6px; margin-top: 6px;
} }
@@ -543,11 +575,25 @@ pre{
.navLabel { .navLabel {
white-space: nowrap; 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 */ /* Collapsed: alleen icon zichtbaar */
.sidebar.collapsed .navLabel { .sidebar.collapsed .navLabel {
display: none; display: none;
} }
.sidebar.collapsed .navCount {
display: none;
}
.sidebar.collapsed .tab { .sidebar.collapsed .tab {
justify-content: center; justify-content: center;
padding: 10px 10px; padding: 10px 10px;
@@ -666,6 +712,35 @@ pre{
padding:7px 9px; padding:7px 9px;
font-size: 12px; 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 { .data-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -734,6 +809,23 @@ pre{
border-radius: 14px; border-radius: 14px;
min-height: 420px; min-height: 420px;
overflow: hidden; 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{ .mapLegend{
+3
View File
@@ -219,6 +219,9 @@ async function fetchContainers() {
const list = Array.isArray(containers) ? containers : (containers?.containers || []); const list = Array.isArray(containers) ? containers : (containers?.containers || []);
document.getElementById('countContainers').textContent = list.length; document.getElementById('countContainers').textContent = list.length;
if (typeof window.updateNavCount === 'function') {
window.updateNavCount('countNavContainers', list.length);
}
const podsList = Array.isArray(pods) ? pods : []; const podsList = Array.isArray(pods) ? pods : [];
const podStatus = {}; const podStatus = {};
+28 -2
View File
@@ -159,7 +159,19 @@ async function filesRefresh() {
const treeEl = document.getElementById('filesTree'); const treeEl = document.getElementById('filesTree');
treeEl.textContent = 'Laden...'; 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 // Filter alleen systemd subtree
const scoped = (data || []).filter(folder => { const scoped = (data || []).filter(folder => {
@@ -168,10 +180,24 @@ async function filesRefresh() {
}); });
if (!scoped.length) { 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; 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. // Bouw een geneste folder-tree uit de "platte" API response.
const folderByPath = new Map(); const folderByPath = new Map();
for (const f of scoped) { for (const f of scoped) {
+31 -5
View File
@@ -2,18 +2,44 @@ let imagesData = [];
let imagesSort = { field: null, dir: null }; let imagesSort = { field: null, dir: null };
async function loadImages() { async function loadImages() {
const res = await fetch("/api/images"); const tbody = document.getElementById("images-tbody");
const images = await res.json(); try {
const res = await fetch("/api/images");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const images = await res.json();
imagesData = images; imagesData = Array.isArray(images) ? images : [];
updateSortIndicators(); if (typeof window.updateNavCount === "function") {
applyImageSorting(); 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 = `<tr><td colspan="8">${box}</td></tr>`;
}
}
} }
function renderImages(images) { function renderImages(images) {
const tbody = document.getElementById("images-tbody"); const tbody = document.getElementById("images-tbody");
tbody.innerHTML = ""; 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 = `<tr><td colspan="8">${box}</td></tr>`;
return;
}
images.forEach(img => { images.forEach(img => {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
+40 -2
View File
@@ -96,11 +96,18 @@
]); ]);
state.usage = usage; state.usage = usage;
state.list = list; 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()}`; if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
renderNetworksSummary(); renderNetworksSummary();
renderNetworks(); renderNetworks();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (typeof window.updateNavCount === 'function') {
window.updateNavCount('countNavNetworks', 0);
}
if (statusEl) statusEl.textContent = 'Fout: ' + (e?.message || e); if (statusEl) statusEl.textContent = 'Fout: ' + (e?.message || e);
} }
} }
@@ -620,6 +627,10 @@
// leeg host (placeholder weg) // leeg host (placeholder weg)
host.innerHTML = ''; 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 w = Math.max(600, host.clientWidth || 600);
const h = Math.max(420, host.clientHeight || 420); const h = Math.max(420, host.clientHeight || 420);
@@ -694,11 +705,27 @@
d3.select(this).classed('graphActive', false); 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 = `<strong>${typeLabel}</strong><br>${d.label || d.key}<br>${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.on('mouseleave', () => {
node.classed('graphDim', false); node.classed('graphDim', false);
link.classed('graphDim', false).classed('graphActive', false); link.classed('graphDim', false).classed('graphActive', false);
tooltip.style.display = 'none';
}); });
node.on('click', (ev, d) => { node.on('click', (ev, d) => {
@@ -822,13 +849,24 @@
const usage = state.usage; const usage = state.usage;
if (!usage || !usage.byNetwork) { if (!usage || !usage.byNetwork) {
tbody.innerHTML = `<tr><td colspan="6" class="muted">Geen data. Klik op Vernieuwen.</td></tr>`; const box = (typeof window.renderStateBox === 'function')
rel.innerHTML = `<div class="muted">Geen data.</div>`; ? window.renderStateBox('empty', 'Geen netwerkdata', 'Klik op Vernieuwen om netwerkdata op te halen.')
: 'Geen data.';
tbody.innerHTML = `<tr><td colspan="6">${box}</td></tr>`;
rel.innerHTML = box;
return; return;
} }
const vmAll = buildNetworksViewModel(); const vmAll = buildNetworksViewModel();
const vm = applyFiltersAndSort(vmAll); 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 = `<tr><td colspan="6">${box}</td></tr>`;
rel.innerHTML = box;
return;
}
for (const row of vm) { for (const row of vm) {
const netName = row.name; const netName = row.name;
+25 -5
View File
@@ -48,16 +48,16 @@
<span class="navIcon">🏠</span><span class="navLabel">Dashboard</span> <span class="navIcon">🏠</span><span class="navLabel">Dashboard</span>
</div> </div>
<div class="tab" id="tab-containers" onclick="setTab('containers')" title="Containers"> <div class="tab" id="tab-containers" onclick="setTab('containers')" title="Containers">
<span class="navIcon">📦</span><span class="navLabel">Containers</span> <span class="navIcon">📦</span><span class="navLabel">Containers</span><span class="navCount" id="countNavContainers">-</span>
</div> </div>
<div class="tab" id="tab-networks" onclick="setTab('networks')" title="Netwerk"> <div class="tab" id="tab-networks" onclick="setTab('networks')" title="Netwerk">
<span class="navIcon">🌐</span><span class="navLabel">Netwerk</span> <span class="navIcon">🌐</span><span class="navLabel">Netwerk</span><span class="navCount" id="countNavNetworks">-</span>
</div> </div>
<div class="tab" id="tab-images" onclick="setTab('images')" title="Images"> <div class="tab" id="tab-images" onclick="setTab('images')" title="Images">
<span class="navIcon">📦</span><span class="navLabel">Images</span> <span class="navIcon">📦</span><span class="navLabel">Images</span><span class="navCount" id="countNavImages">-</span>
</div> </div>
<div class="tab" id="tab-files" onclick="setTab('files')" title="Files"> <div class="tab" id="tab-files" onclick="setTab('files')" title="Files">
<span class="navIcon">📁</span><span class="navLabel">Files</span> <span class="navIcon">📁</span><span class="navLabel">Files</span><span class="navCount" id="countNavFiles">-</span>
</div> </div>
</div> </div>
</aside> </aside>
@@ -486,6 +486,22 @@
return `<span class="badge">${esc(s || 'unknown')}</span>`; return `<span class="badge">${esc(s || 'unknown')}</span>`;
} }
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 `
<div class="stateBox ${t}">
<div class="stateTitle">${esc(title || 'Status')}</div>
<div class="stateText">${esc(message || '')}</div>
</div>
`;
}
// ---- Modal ---- // ---- Modal ----
function showModal(title, content) { function showModal(title, content) {
document.getElementById('modalTitle').textContent = title; document.getElementById('modalTitle').textContent = title;
@@ -522,7 +538,10 @@
document.getElementById('tab-' + tab).classList.add('active'); document.getElementById('tab-' + tab).classList.add('active');
document.querySelectorAll('[id^="view-"]').forEach(v => v.style.display='none'); 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') { if (tab === 'files') {
filesRefresh(); filesRefresh();
} }
@@ -610,6 +629,7 @@
const list = Array.isArray(containers) ? containers : (containers?.containers || []); const list = Array.isArray(containers) ? containers : (containers?.containers || []);
const cCount = list.length; const cCount = list.length;
document.getElementById('countContainers').textContent = cCount; document.getElementById('countContainers').textContent = cCount;
updateNavCount('countNavContainers', cCount);
} }
setApiState(true, 'API: OK'); setApiState(true, 'API: OK');
setLastRefreshNow(); setLastRefreshNow();