feat (ui): Light/Dark Theme added 02
This commit is contained in:
@@ -54,6 +54,8 @@
|
|||||||
--badge-green-text: #ffffff;
|
--badge-green-text: #ffffff;
|
||||||
--badge-yellow-bg: #f1c40f;
|
--badge-yellow-bg: #f1c40f;
|
||||||
--badge-yellow-text: #111111;
|
--badge-yellow-text: #111111;
|
||||||
|
--table-zebra: rgba(96,165,250,.03);
|
||||||
|
--sticky-head-bg: rgba(17,26,46,.96);
|
||||||
--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";
|
||||||
@@ -111,6 +113,8 @@
|
|||||||
--badge-green-text: #f8fafc;
|
--badge-green-text: #f8fafc;
|
||||||
--badge-yellow-bg: #b45309;
|
--badge-yellow-bg: #b45309;
|
||||||
--badge-yellow-text: #fffbeb;
|
--badge-yellow-text: #fffbeb;
|
||||||
|
--table-zebra: rgba(15,23,42,.03);
|
||||||
|
--sticky-head-bg: rgba(255,255,255,.97);
|
||||||
}
|
}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
body{
|
body{
|
||||||
@@ -129,6 +133,10 @@ header{
|
|||||||
.topbar{
|
.topbar{
|
||||||
display:flex; gap:12px; align-items:center; justify-content:space-between;
|
display:flex; gap:12px; align-items:center; justify-content:space-between;
|
||||||
}
|
}
|
||||||
|
.headerMeta{
|
||||||
|
margin-left: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.brand{
|
.brand{
|
||||||
display:flex; gap:12px; align-items:center;
|
display:flex; gap:12px; align-items:center;
|
||||||
font-weight:700; letter-spacing:.2px;
|
font-weight:700; letter-spacing:.2px;
|
||||||
@@ -194,6 +202,19 @@ header{
|
|||||||
display:flex; gap:10px; align-items:center;
|
display:flex; gap:10px; align-items:center;
|
||||||
}
|
}
|
||||||
.cardBody{padding:14px}
|
.cardBody{padding:14px}
|
||||||
|
.dashboardKpiGrid{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
gap:10px;
|
||||||
|
}
|
||||||
|
@media (min-width: 980px){
|
||||||
|
.dashboardKpiGrid{grid-template-columns: repeat(4, 1fr)}
|
||||||
|
}
|
||||||
|
.actionBar{
|
||||||
|
display:flex;
|
||||||
|
gap:8px;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
.btn{
|
.btn{
|
||||||
border:1px solid var(--card-border);
|
border:1px solid var(--card-border);
|
||||||
background: var(--btn);
|
background: var(--btn);
|
||||||
@@ -236,6 +257,12 @@ table{
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
thead th{
|
||||||
|
position: sticky;
|
||||||
|
top: 72px;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--sticky-head-bg);
|
||||||
|
}
|
||||||
th,td{
|
th,td{
|
||||||
padding:10px 8px;
|
padding:10px 8px;
|
||||||
border-bottom:1px solid var(--soft-line);
|
border-bottom:1px solid var(--soft-line);
|
||||||
@@ -243,6 +270,7 @@ th,td{
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
th{color: var(--muted); font-weight:600}
|
th{color: var(--muted); font-weight:600}
|
||||||
|
tbody tr:nth-child(even) td{background: var(--table-zebra)}
|
||||||
tr:hover td{background: var(--hover-bg)}
|
tr:hover td{background: var(--hover-bg)}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -264,6 +292,7 @@ pre.code{
|
|||||||
.badge.ok{border-color: rgba(45,212,191,.6); color: var(--ok)}
|
.badge.ok{border-color: rgba(45,212,191,.6); color: var(--ok)}
|
||||||
.badge.bad{border-color: rgba(251,113,133,.6); color: var(--bad)}
|
.badge.bad{border-color: rgba(251,113,133,.6); color: var(--bad)}
|
||||||
.badge.warn{border-color: rgba(251,191,36,.6); color: var(--warn)}
|
.badge.warn{border-color: rgba(251,191,36,.6); color: var(--warn)}
|
||||||
|
.badge.info{border-color: rgba(96,165,250,.45); color: var(--accent)}
|
||||||
.mono{font-family: var(--mono)}
|
.mono{font-family: var(--mono)}
|
||||||
.muted{color:var(--muted)}
|
.muted{color:var(--muted)}
|
||||||
.num{
|
.num{
|
||||||
@@ -603,6 +632,40 @@ pre{
|
|||||||
.file-folder-files > div:hover{
|
.file-folder-files > div:hover{
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
}
|
}
|
||||||
|
.file-entry{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap:10px;
|
||||||
|
padding:4px 0;
|
||||||
|
border-bottom:1px dashed var(--soft-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.file-entry-name{
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
.file-entry-state{
|
||||||
|
min-width: 24px;
|
||||||
|
text-align:right;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.file-entry.active{
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
.file-entry.active .file-entry-name{
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.file-entry.dirty .file-entry-state{
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
.filesEditorStatus{
|
||||||
|
margin-top:8px;
|
||||||
|
border:1px solid var(--soft-line);
|
||||||
|
border-radius:10px;
|
||||||
|
padding:7px 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
let cmEditor = null;
|
let cmEditor = null;
|
||||||
|
let filesDirty = false;
|
||||||
|
let filesSuppressDirtyEvent = false;
|
||||||
|
let filesTextareaBound = false;
|
||||||
|
|
||||||
function filesCurrentTheme() {
|
function filesCurrentTheme() {
|
||||||
const t = document.documentElement.getAttribute('data-theme');
|
const t = document.documentElement.getAttribute('data-theme');
|
||||||
@@ -34,6 +37,72 @@ const FILES_ROOT = 'systemd'; // API-root binnen WORKLOADS_DIR
|
|||||||
let filesCurrentUiPath = ''; // zonder "systemd/"
|
let filesCurrentUiPath = ''; // zonder "systemd/"
|
||||||
let filesCurrentApiPath = ''; // met "systemd/"
|
let filesCurrentApiPath = ''; // met "systemd/"
|
||||||
|
|
||||||
|
function filesModeLabel(uiPath) {
|
||||||
|
const mode = cmModeForPath(uiPath);
|
||||||
|
if (mode === 'yaml') return 'YAML';
|
||||||
|
if (mode === 'application/json') return 'JSON';
|
||||||
|
if (mode === 'javascript') return 'JavaScript';
|
||||||
|
return 'Text';
|
||||||
|
}
|
||||||
|
|
||||||
|
function filesCursorLabel() {
|
||||||
|
if (cmEditor) {
|
||||||
|
const c = cmEditor.getCursor();
|
||||||
|
return `Ln ${c.line + 1}, Kol ${c.ch + 1}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function filesUpdateEditorStatus() {
|
||||||
|
const el = document.getElementById('filesEditorStatus');
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
if (!filesCurrentUiPath) {
|
||||||
|
el.textContent = 'Geen bestand geselecteerd';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirtyTxt = filesDirty ? 'Niet opgeslagen' : 'Opgeslagen';
|
||||||
|
const parts = [
|
||||||
|
dirtyTxt,
|
||||||
|
filesModeLabel(filesCurrentUiPath),
|
||||||
|
filesCurrentUiPath,
|
||||||
|
];
|
||||||
|
const cursor = filesCursorLabel();
|
||||||
|
if (cursor) parts.push(cursor);
|
||||||
|
el.textContent = parts.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filesUpdateTreeSelection() {
|
||||||
|
const treeEl = document.getElementById('filesTree');
|
||||||
|
if (!treeEl) return;
|
||||||
|
|
||||||
|
treeEl.querySelectorAll('.file-entry').forEach(row => {
|
||||||
|
row.classList.remove('active', 'dirty');
|
||||||
|
const state = row.querySelector('.file-entry-state');
|
||||||
|
if (state) state.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filesCurrentUiPath) return;
|
||||||
|
|
||||||
|
const key = encodeURIComponent(filesCurrentUiPath);
|
||||||
|
const row = treeEl.querySelector(`.file-entry[data-file="${CSS.escape(key)}"]`);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
row.classList.add('active');
|
||||||
|
if (filesDirty) {
|
||||||
|
row.classList.add('dirty');
|
||||||
|
const state = row.querySelector('.file-entry-state');
|
||||||
|
if (state) state.textContent = '●';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filesSetDirty(v) {
|
||||||
|
filesDirty = !!v && !!filesCurrentUiPath;
|
||||||
|
filesUpdateEditorStatus();
|
||||||
|
filesUpdateTreeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
function cmModeForPath(uiPath) {
|
function cmModeForPath(uiPath) {
|
||||||
const p = (uiPath || '').toLowerCase();
|
const p = (uiPath || '').toLowerCase();
|
||||||
if (p.endsWith('.yaml') || p.endsWith('.yml') || p.endsWith('.kube') || p.endsWith('.container')) return 'yaml';
|
if (p.endsWith('.yaml') || p.endsWith('.yml') || p.endsWith('.kube') || p.endsWith('.container')) return 'yaml';
|
||||||
@@ -58,6 +127,7 @@ function filesSetCurrent(uiPath) {
|
|||||||
filesCurrentUiPath = (uiPath || '').trim().replace(/^\/+/, '');
|
filesCurrentUiPath = (uiPath || '').trim().replace(/^\/+/, '');
|
||||||
filesCurrentApiPath = filesToApiPath(filesCurrentUiPath);
|
filesCurrentApiPath = filesToApiPath(filesCurrentUiPath);
|
||||||
document.getElementById('filesCurrent').textContent = filesCurrentUiPath || '-';
|
document.getElementById('filesCurrent').textContent = filesCurrentUiPath || '-';
|
||||||
|
filesSetDirty(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function filesRefresh() {
|
async function filesRefresh() {
|
||||||
@@ -72,6 +142,17 @@ async function filesRefresh() {
|
|||||||
theme: filesCodeMirrorTheme()
|
theme: filesCodeMirrorTheme()
|
||||||
});
|
});
|
||||||
cmEditor.setSize('100%', 360);
|
cmEditor.setSize('100%', 360);
|
||||||
|
cmEditor.on('change', () => {
|
||||||
|
if (filesSuppressDirtyEvent || !filesCurrentUiPath) return;
|
||||||
|
filesSetDirty(true);
|
||||||
|
});
|
||||||
|
cmEditor.on('cursorActivity', filesUpdateEditorStatus);
|
||||||
|
} else if (taFiles && !filesTextareaBound) {
|
||||||
|
filesTextareaBound = true;
|
||||||
|
taFiles.addEventListener('input', () => {
|
||||||
|
if (!filesCurrentUiPath) return;
|
||||||
|
filesSetDirty(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,9 +260,11 @@ async function filesRefresh() {
|
|||||||
|
|
||||||
for (const f of sortedFiles) {
|
for (const f of sortedFiles) {
|
||||||
const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f;
|
const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f;
|
||||||
|
const fileKey = encodeURIComponent(fullUi);
|
||||||
out.push(`
|
out.push(`
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:4px 0 4px ${indent + 18}px;border-bottom:1px dashed rgba(36,52,95,.35)">
|
<div class="file-entry" data-file="${fileKey}" style="padding-left:${indent + 18}px;">
|
||||||
<span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
|
<span class="mono file-entry-name" onclick="filesOpen(decodeURIComponent('${fileKey}'))">📄 ${esc(f)}</span>
|
||||||
|
<span class="file-entry-state"></span>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -216,9 +299,11 @@ async function filesRefresh() {
|
|||||||
<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">
|
<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">
|
||||||
${(rootFolder.files || []).slice().sort((a,b)=>a.localeCompare(b)).map(f => {
|
${(rootFolder.files || []).slice().sort((a,b)=>a.localeCompare(b)).map(f => {
|
||||||
const fullUi = f;
|
const fullUi = f;
|
||||||
|
const fileKey = encodeURIComponent(fullUi);
|
||||||
return `
|
return `
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:4px 0 4px 18px;border-bottom:1px dashed rgba(36,52,95,.35)">
|
<div class="file-entry" data-file="${fileKey}" style="padding-left:18px;">
|
||||||
<span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
|
<span class="mono file-entry-name" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${fileKey}'))">📄 ${esc(f)}</span>
|
||||||
|
<span class="file-entry-state"></span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
@@ -243,6 +328,8 @@ async function filesRefresh() {
|
|||||||
const filesBlock = treeEl.querySelector(`[data-folder-files="${CSS.escape(folderKey)}"]`);
|
const filesBlock = treeEl.querySelector(`[data-folder-files="${CSS.escape(folderKey)}"]`);
|
||||||
if (filesBlock) filesBlock.style.display = isNowCollapsed ? 'none' : '';
|
if (filesBlock) filesBlock.style.display = isNowCollapsed ? 'none' : '';
|
||||||
};
|
};
|
||||||
|
filesUpdateTreeSelection();
|
||||||
|
filesUpdateEditorStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function filesOpen(uiPath) {
|
async function filesOpen(uiPath) {
|
||||||
@@ -253,11 +340,16 @@ async function filesOpen(uiPath) {
|
|||||||
const text = res.content || '';
|
const text = res.content || '';
|
||||||
if (cmEditor) {
|
if (cmEditor) {
|
||||||
cmEditor.setOption('mode', cmModeForPath(uiPath));
|
cmEditor.setOption('mode', cmModeForPath(uiPath));
|
||||||
|
filesSuppressDirtyEvent = true;
|
||||||
cmEditor.setValue(text);
|
cmEditor.setValue(text);
|
||||||
|
filesSuppressDirtyEvent = false;
|
||||||
cmEditor.refresh();
|
cmEditor.refresh();
|
||||||
|
cmEditor.setCursor({ line: 0, ch: 0 });
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('filesEditor').value = text;
|
document.getElementById('filesEditor').value = text;
|
||||||
}
|
}
|
||||||
|
filesSetDirty(false);
|
||||||
|
filesUpdateEditorStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function filesSave() {
|
async function filesSave() {
|
||||||
@@ -275,6 +367,7 @@ async function filesSave() {
|
|||||||
{ content }
|
{ content }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
filesSetDirty(false);
|
||||||
showModal('Opgeslagen', JSON.stringify(res, null, 2));
|
showModal('Opgeslagen', JSON.stringify(res, null, 2));
|
||||||
await filesRefresh();
|
await filesRefresh();
|
||||||
}
|
}
|
||||||
@@ -290,7 +383,14 @@ async function filesDelete() {
|
|||||||
|
|
||||||
// reset current
|
// reset current
|
||||||
filesSetCurrent('');
|
filesSetCurrent('');
|
||||||
|
if (cmEditor) {
|
||||||
|
filesSuppressDirtyEvent = true;
|
||||||
|
cmEditor.setValue('');
|
||||||
|
filesSuppressDirtyEvent = false;
|
||||||
|
cmEditor.refresh();
|
||||||
|
} else {
|
||||||
document.getElementById('filesEditor').value = '';
|
document.getElementById('filesEditor').value = '';
|
||||||
|
}
|
||||||
await filesRefresh();
|
await filesRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ function renderImages(images) {
|
|||||||
const fullId = img.Id;
|
const fullId = img.Id;
|
||||||
|
|
||||||
const status = containers > 0
|
const status = containers > 0
|
||||||
? `<span class="badge badge-green">In use</span>`
|
? `<span class="badge ok">In use</span>`
|
||||||
: `<span class="badge badge-yellow">Unused</span>`;
|
: `<span class="badge warn">Unused</span>`;
|
||||||
|
|
||||||
const disabled = containers > 0 ? "disabled" : "";
|
const disabled = containers > 0 ? "disabled" : "";
|
||||||
|
|
||||||
@@ -39,9 +39,9 @@ function renderImages(images) {
|
|||||||
</td>
|
</td>
|
||||||
<td>${repoTag}</td>
|
<td>${repoTag}</td>
|
||||||
<td>${shortId}</td>
|
<td>${shortId}</td>
|
||||||
<td>${sizeMB} MB</td>
|
<td class="num">${sizeMB} MB</td>
|
||||||
<td>${created}</td>
|
<td class="muted">${created}</td>
|
||||||
<td>${containers}</td>
|
<td class="num">${containers}</td>
|
||||||
<td>${status}</td>
|
<td>${status}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn small bad" onclick="removeSingleImage('${fullId}')" ${disabled}>
|
<button class="btn small bad" onclick="removeSingleImage('${fullId}')" ${disabled}>
|
||||||
|
|||||||
+54
-9
@@ -28,9 +28,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button class="btn ghost" onclick="pingApi()">Ping</button>
|
<button class="btn ghost" onclick="pingApi()">◉ Ping</button>
|
||||||
<button class="btn" onclick="refreshActive()">Ververs</button>
|
<button class="btn" onclick="refreshActive()">↻ Ververs</button>
|
||||||
<button class="btn ghost" id="themeToggleBtn" title="Schakel light/dark mode">Theme</button>
|
<button class="btn ghost" id="themeToggleBtn" title="Schakel light/dark mode">◐ Theme</button>
|
||||||
|
<span class="statusline headerMeta" id="lastRefreshHeader">Laatste refresh: -</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,18 +66,32 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div id="view-dashboard" class="grid">
|
<div id="view-dashboard" class="grid">
|
||||||
<div class="card half">
|
<div class="card">
|
||||||
<div class="cardHeader">
|
<div class="cardHeader">
|
||||||
<div class="cardTitle">Snel acties</div>
|
<div class="cardTitle">Platform overzicht</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button class="btn ok" onclick="daemonReload()">daemon-reload</button>
|
<button class="btn ok" onclick="daemonReload()">daemon-reload</button>
|
||||||
<button class="btn" onclick="refreshActive()">Ververs alles</button>
|
<button class="btn" onclick="refreshActive()">Ververs alles</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cardBody">
|
<div class="cardBody">
|
||||||
<div class="flex">
|
<div class="dashboardKpiGrid">
|
||||||
<span class="pill"><span class="b" id="countPods">-</span> pods</span>
|
<div class="statCard">
|
||||||
<span class="pill"><span class="b" id="countContainers">-</span> containers</span>
|
<div class="statValue" id="countPods">-</div>
|
||||||
|
<div class="statLabel">Pods</div>
|
||||||
|
</div>
|
||||||
|
<div class="statCard">
|
||||||
|
<div class="statValue" id="countContainers">-</div>
|
||||||
|
<div class="statLabel">Containers</div>
|
||||||
|
</div>
|
||||||
|
<div class="statCard">
|
||||||
|
<div class="statValue" id="dashboardApiState">-</div>
|
||||||
|
<div class="statLabel">API status</div>
|
||||||
|
</div>
|
||||||
|
<div class="statCard">
|
||||||
|
<div class="statValue" id="dashboardLastRefresh">-</div>
|
||||||
|
<div class="statLabel">Laatste refresh</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
Deze UI gebruikt jouw API endpoints onder <span class="mono">/api</span> (same origin).
|
Deze UI gebruikt jouw API endpoints onder <span class="mono">/api</span> (same origin).
|
||||||
@@ -84,6 +99,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card half">
|
||||||
|
<div class="cardHeader">
|
||||||
|
<div class="cardTitle">Snel acties</div>
|
||||||
|
</div>
|
||||||
|
<div class="cardBody">
|
||||||
|
<div class="actionBar">
|
||||||
|
<button class="btn" onclick="setTab('containers')">Ga naar containers</button>
|
||||||
|
<button class="btn" onclick="setTab('networks')">Ga naar netwerken</button>
|
||||||
|
<button class="btn" onclick="setTab('images')">Ga naar images</button>
|
||||||
|
<button class="btn" onclick="setTab('files')">Ga naar files</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">Gebruik de zijbalk voor detailbeheer; deze acties geven snelle toegang tot de hoofdsecties.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="view-containers" class="grid" style="display:none">
|
<div id="view-containers" class="grid" style="display:none">
|
||||||
<div class="card card--menu-overflow" style="grid-column: 1 / -1;">
|
<div class="card card--menu-overflow" style="grid-column: 1 / -1;">
|
||||||
@@ -304,6 +333,7 @@
|
|||||||
Huidig bestand: <span class="mono" id="filesCurrent">-</span>
|
Huidig bestand: <span class="mono" id="filesCurrent">-</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="filesEditor" class="textarea mono" spellcheck="false" placeholder="Selecteer links een bestand..."></textarea>
|
<textarea id="filesEditor" class="textarea mono" spellcheck="false" placeholder="Selecteer links een bestand..."></textarea>
|
||||||
|
<div id="filesEditorStatus" class="filesEditorStatus muted">Geen bestand geselecteerd</div>
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
Na wijzigen van <span class="mono">*.container</span> moet je meestal <span class="mono">daemon-reload</span> doen (via de dashboard-knop).
|
Na wijzigen van <span class="mono">*.container</span> moet je meestal <span class="mono">daemon-reload</span> doen (via de dashboard-knop).
|
||||||
</div>
|
</div>
|
||||||
@@ -453,7 +483,7 @@
|
|||||||
const t = (s || '').toLowerCase();
|
const t = (s || '').toLowerCase();
|
||||||
if (t.includes('running') || t === 'running' || t === 'active') return `<span class="badge ok">${esc(s)}</span>`;
|
if (t.includes('running') || t === 'running' || t === 'active') return `<span class="badge ok">${esc(s)}</span>`;
|
||||||
if (t.includes('exited') || t.includes('dead') || t.includes('stopped') || t === 'inactive') return `<span class="badge bad">${esc(s)}</span>`;
|
if (t.includes('exited') || t.includes('dead') || t.includes('stopped') || t === 'inactive') return `<span class="badge bad">${esc(s)}</span>`;
|
||||||
return `<span class="badge warn">${esc(s || 'unknown')}</span>`;
|
return `<span class="badge">${esc(s || 'unknown')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Modal ----
|
// ---- Modal ----
|
||||||
@@ -550,6 +580,20 @@
|
|||||||
dot.style.background = ok ? 'var(--ok)' : 'var(--bad)';
|
dot.style.background = ok ? 'var(--ok)' : 'var(--bad)';
|
||||||
dot.style.boxShadow = ok ? '0 0 0 6px rgba(45,212,191,.15)' : '0 0 0 6px rgba(251,113,133,.15)';
|
dot.style.boxShadow = ok ? '0 0 0 6px rgba(45,212,191,.15)' : '0 0 0 6px rgba(251,113,133,.15)';
|
||||||
document.getElementById('statusLine').textContent = msg;
|
document.getElementById('statusLine').textContent = msg;
|
||||||
|
const apiStat = document.getElementById('dashboardApiState');
|
||||||
|
if (apiStat) apiStat.textContent = ok ? 'OK' : 'Fout';
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentClockText() {
|
||||||
|
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLastRefreshNow() {
|
||||||
|
const now = currentClockText();
|
||||||
|
const hdr = document.getElementById('lastRefreshHeader');
|
||||||
|
if (hdr) hdr.textContent = 'Laatste refresh: ' + now;
|
||||||
|
const dash = document.getElementById('dashboardLastRefresh');
|
||||||
|
if (dash) dash.textContent = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Dashboard refresh ----
|
// ---- Dashboard refresh ----
|
||||||
@@ -568,6 +612,7 @@
|
|||||||
document.getElementById('countContainers').textContent = cCount;
|
document.getElementById('countContainers').textContent = cCount;
|
||||||
}
|
}
|
||||||
setApiState(true, 'API: OK');
|
setApiState(true, 'API: OK');
|
||||||
|
setLastRefreshNow();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setApiState(false, 'API: fout (' + e.message + ')');
|
setApiState(false, 'API: fout (' + e.message + ')');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user