Initial commit - podman-mvp net na toevoegen cpu en mem kolommen
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Podman MVP | Dashboard V4.5</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/theme/material-darker.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/mode/yaml/yaml.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 250px;
|
||||
--portainer-blue: #00b3e6;
|
||||
--bg-dark: #2c3e50;
|
||||
--bg-light: #f4f6f9;
|
||||
--border-color: #dce1e5;
|
||||
}
|
||||
body { font-family: 'Segoe UI', sans-serif; margin: 0; display: flex; background: var(--bg-light); height: 100vh; overflow: hidden; }
|
||||
.sidebar { width: var(--sidebar-width); background: var(--bg-dark); color: white; display: flex; flex-direction: column; }
|
||||
.sidebar-header { padding: 15px 20px; background: #1a252f; font-weight: bold; display: flex; align-items: center; gap: 10px; }
|
||||
.sidebar-header img { width: 30px; }
|
||||
.nav-item { padding: 15px 20px; cursor: pointer; border-left: 4px solid transparent; transition: 0.2s; }
|
||||
.nav-item:hover { background: #34495e; }
|
||||
.nav-item.active { background: #34495e; border-left-color: var(--portainer-blue); color: var(--portainer-blue); }
|
||||
.main-content { flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
|
||||
.topbar { background: white; padding: 15px 30px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
||||
|
||||
/* Navigatie logica */
|
||||
.view-container { padding: 25px; display: none; }
|
||||
.view-container.active { display: block; }
|
||||
|
||||
.card { background: white; border-radius: 4px; border: 1px solid var(--border-color); margin-bottom: 20px; }
|
||||
.card-header { padding: 12px 20px; border-bottom: 1px solid var(--border-color); font-weight: bold; background: #fafafa; display: flex; justify-content: space-between; align-items: center; }
|
||||
.card-body { padding: 15px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { text-align: left; padding: 12px; border-bottom: 2px solid var(--border-color); font-size: 0.8rem; text-transform: uppercase; color: #7f8c8d; }
|
||||
td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 0.9rem; }
|
||||
.badge { padding: 4px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; }
|
||||
.badge-running { background: #d4edda; color: #155724; }
|
||||
.badge-stopped { background: #f8d7da; color: #721c24; }
|
||||
.btn { padding: 5px 10px; border-radius: 3px; border: 1px solid #ccc; cursor: pointer; font-size: 0.8rem; background: white; transition: 0.2s; }
|
||||
.btn:hover { background: #f8f9fa; border-color: #bbb; }
|
||||
.btn-primary { background: var(--portainer-blue); color: white; border: none; padding: 8px 15px; font-weight: bold; }
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; }
|
||||
.modal-content { background: white; width: 80%; max-height: 80%; border-radius: 8px; display: flex; flex-direction: column; position: relative; }
|
||||
.modal-header { padding: 15px 20px; border-bottom: 1px solid #ddd; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
|
||||
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
|
||||
.log-container { background: #1e1e1e; color: #d4d4d4; padding: 15px; font-family: 'Consolas', monospace; font-size: 0.85rem; border-radius: 4px; white-space: pre-wrap; }
|
||||
.inspect-container { background: #f8f9fa; padding: 15px; font-family: monospace; font-size: 0.85rem; border: 1px solid #ddd; border-radius: 4px; }
|
||||
|
||||
/* Studio Layout */
|
||||
.studio-layout { display: flex; gap: 20px; height: calc(100vh - 280px); }
|
||||
.file-tree { width: 280px; border-right: 1px solid #eee; overflow-y: auto; padding-right: 10px; }
|
||||
.editor-container { flex: 1; display: flex; flex-direction: column; }
|
||||
.CodeMirror { flex: 1; border: 1px solid #ddd; font-size: 13px; }
|
||||
.tree-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 8px; border-radius: 4px; margin-bottom: 2px; }
|
||||
.tree-item:hover { background: #f0f2f5; }
|
||||
.tree-folder { font-weight: bold; color: #2c3e50; background: #eaeff2; margin-top: 10px; }
|
||||
.tree-file { cursor: pointer; color: #2980b9; }
|
||||
.icon-btn { cursor: pointer; opacity: 0.6; transition: 0.2s; font-style: normal; }
|
||||
.icon-btn:hover { opacity: 1; transform: scale(1.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="https://raw.githubusercontent.com/containers/common/main/logos/podman-logo.svg" alt="Podman">
|
||||
<span>Podman MVP</span>
|
||||
</div>
|
||||
<div class="nav-item active" id="nav-dashboard" onclick="showView('dashboard')">Dashboard</div>
|
||||
<div class="nav-item" id="nav-containers" onclick="showView('containers')">Containers</div>
|
||||
<div class="nav-item" id="nav-pods" onclick="showView('pods')">Pods</div>
|
||||
<div class="nav-item" id="nav-workloads" onclick="showView('workloads')">Workload Studio</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="topbar">
|
||||
<span id="view-title" style="font-weight: bold; color: #34495e;">DASHBOARD</span>
|
||||
<button class="btn" onclick="refresh()">🔄 Ververs Data</button>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-view" class="view-container active">
|
||||
<div class="card">
|
||||
<div class="card-header">Beheerde Workloads</div>
|
||||
<div class="card-body">
|
||||
<table>
|
||||
<thead><tr><th>Naam</th><th>Status</th><th>IP</th><th>Acties</th></tr></thead>
|
||||
<tbody id="dashboard-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="containers-view" class="view-container">
|
||||
<div class="card">
|
||||
<div class="card-header">Live Containers</div>
|
||||
<div class="card-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Naam</th>
|
||||
<th>Status</th>
|
||||
<th>Pod</th>
|
||||
<th>IP</th>
|
||||
<th>Poorten</th>
|
||||
<th>Quick Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="live-container-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pods-view" class="view-container">
|
||||
<div class="card">
|
||||
<div class="card-header">Active Pods</div>
|
||||
<div class="card-body">
|
||||
<table>
|
||||
<thead><tr><th>Pod Naam</th><th>Status</th><th>Containers</th></tr></thead>
|
||||
<tbody id="pod-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workloads-view" class="view-container">
|
||||
<div class="card">
|
||||
<div class="card-header"><span>Bestandsbeheer</span><button class="btn" onclick="createNewMap()">📁 Nieuwe Hoofdmap</button></div>
|
||||
<div class="card-body studio-layout">
|
||||
<div id="file-tree" class="file-tree">Laden...</div>
|
||||
<div class="editor-container">
|
||||
<div id="editor-header" style="padding: 10px; background: #34495e; color: white; font-family: monospace; font-size: 0.8rem; border-radius: 4px 4px 0 0;">Selecteer een bestand...</div>
|
||||
<textarea id="edit-content"></textarea>
|
||||
<div style="margin-top:15px; display: flex; justify-content: flex-end;"><button class="btn btn-primary" onclick="saveFile()">💾 Bestand Opslaan</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal" class="modal-overlay" onclick="closeModal()">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<span id="modal-title">Info</span>
|
||||
<button class="btn" onclick="closeModal()">✖ Sluiten</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let editor;
|
||||
let currentPath = '';
|
||||
|
||||
window.onload = () => {
|
||||
editor = CodeMirror.fromTextArea(document.getElementById("edit-content"), {
|
||||
mode: "yaml", theme: "material-darker", lineNumbers: true, tabSize: 2, indentWithTabs: false
|
||||
});
|
||||
refresh();
|
||||
};
|
||||
|
||||
async function api(path, method = 'GET', body = null) {
|
||||
const options = { method, headers: body ? {'Content-Type': 'application/json'} : {} };
|
||||
if (body) options.body = JSON.stringify(body);
|
||||
const res = await fetch('/api' + path, options);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
// --- NAVIGATIE HERSTELD ---
|
||||
function showView(view) {
|
||||
document.querySelectorAll('.view-container').forEach(v => {
|
||||
v.style.display = 'none';
|
||||
v.classList.remove('active');
|
||||
});
|
||||
|
||||
const selected = document.getElementById(view + '-view');
|
||||
if (selected) {
|
||||
selected.style.display = 'block';
|
||||
selected.classList.add('active');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||||
const nav = document.getElementById('nav-' + view);
|
||||
if (nav) nav.classList.add('active');
|
||||
|
||||
document.getElementById('view-title').innerText = view.toUpperCase();
|
||||
|
||||
if(view === 'workloads') fetchTree();
|
||||
}
|
||||
|
||||
async function fetchDashboard() {
|
||||
const data = await api('/dashboard');
|
||||
document.getElementById('dashboard-list').innerHTML = data.map(w => `
|
||||
<tr><td><strong>${w.name}</strong></td>
|
||||
<td><span class="badge ${w.status === 'running' ? 'badge-running' : 'badge-stopped'}">${w.status}</span></td>
|
||||
<td>${w.ip || '-'}</td>
|
||||
<td><button class="btn" onclick="runAction('start', '${w.name}')">▶</button> <button class="btn" onclick="runAction('stop', '${w.name}')">⏹</button></td></tr>`).join('');
|
||||
}
|
||||
|
||||
// --- CONTAINERS MET POORTEN EN ACTIONS ---
|
||||
async function fetchContainers() {
|
||||
const data = await api('/containers');
|
||||
const list = document.getElementById('live-container-list');
|
||||
if (!data) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
const realContainers = data.filter(c => !c.IsInfra);
|
||||
|
||||
realContainers.forEach(c => {
|
||||
const name = c.Names[0].replace('/', '');
|
||||
const ip = c.Networks ? Object.values(c.Networks)[0]?.IPAddress || '-' : '-';
|
||||
|
||||
let portMapping = '-';
|
||||
if (c.Ports && c.Ports.length > 0) {
|
||||
portMapping = c.Ports.map(p => {
|
||||
const host = p.host_port;
|
||||
const container = p.container_port;
|
||||
return host ? `${host}:${container}` : `${container}/${p.protocol}`;
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
list.innerHTML += `
|
||||
<tr>
|
||||
<td><strong>${name}</strong></td>
|
||||
<td><span class="badge ${c.State==='running'?'badge-running':'badge-stopped'}">${c.State}</span></td>
|
||||
<td>${c.PodName || '-'}</td>
|
||||
<td>${ip}</td>
|
||||
<td style="font-family:monospace; font-size:0.8rem;">${portMapping}</td>
|
||||
<td>
|
||||
<button class="btn" title="Logs" onclick="showLogs('${name}')">📜</button>
|
||||
<button class="btn" title="Inspect" onclick="showInspect('${name}')">🔍</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
|
||||
async function showLogs(name) {
|
||||
document.getElementById('modal-title').innerText = `Logs: ${name}`;
|
||||
document.getElementById('modal-body').innerHTML = '<div class="log-container">Laden...</div>';
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
const data = await api(`/containers/logs/${name}`);
|
||||
const cleanLogs = data.logs.replace(/[\u0000-\u0008]/g, "");
|
||||
document.getElementById('modal-body').innerHTML = `<div class="log-container">${cleanLogs || 'Geen logs gevonden.'}</div>`;
|
||||
}
|
||||
|
||||
async function showInspect(name) {
|
||||
document.getElementById('modal-title').innerText = `Inspect: ${name}`;
|
||||
document.getElementById('modal-body').innerHTML = '<div class="inspect-container">Laden...</div>';
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
const data = await api(`/containers/inspect/${name}`);
|
||||
document.getElementById('modal-body').innerHTML = `<pre class="inspect-container">${JSON.stringify(data, null, 2)}</pre>`;
|
||||
}
|
||||
|
||||
function closeModal() { document.getElementById('modal').style.display = 'none'; }
|
||||
|
||||
async function fetchTree() {
|
||||
const data = await api('/files/tree');
|
||||
const tree = document.getElementById('file-tree');
|
||||
tree.innerHTML = '';
|
||||
data.forEach(folder => {
|
||||
const folderName = folder.path || "root";
|
||||
tree.innerHTML += `<div class="tree-item tree-folder"><span>📂 ${folderName}</span><div style="display:flex;gap:8px;"><i class="icon-btn" onclick="createFileIn('${folder.path}')">➕</i><i class="icon-btn" style="color:red" onclick="deleteItem('${folder.path}')">🗑</i></div></div>`;
|
||||
folder.files.forEach(f => {
|
||||
const full = folder.path ? `${folder.path}/${f}` : f;
|
||||
tree.innerHTML += `<div class="tree-item ml-4"><span class="tree-file" onclick="loadFile('${full}')">📄 ${f}</span><i class="icon-btn" style="color:red" onclick="deleteItem('${full}')">🗑</i></div>`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadFile(path) {
|
||||
currentPath = path;
|
||||
const data = await api(`/files/read?path=${encodeURIComponent(path)}`);
|
||||
editor.setValue(data.content);
|
||||
document.getElementById('editor-header').innerText = `BESTAND: ${path}`;
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if(!currentPath) return alert("Selecteer bestand!");
|
||||
await api(`/files/save?path=${encodeURIComponent(currentPath)}`, 'POST', {content: editor.getValue()});
|
||||
alert("Opgeslagen!");
|
||||
fetchTree();
|
||||
}
|
||||
|
||||
async function createFileIn(folderPath) {
|
||||
const name = prompt(`Naam in ${folderPath || 'root'}:`, "service.yaml");
|
||||
if(name) {
|
||||
const full = folderPath ? `${folderPath}/${name}` : name;
|
||||
const template = "version: '1.0'\nservices:\n ";
|
||||
await api(`/files/save?path=${encodeURIComponent(full)}`, 'POST', {content: template});
|
||||
fetchTree();
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewMap() {
|
||||
const path = prompt("Nieuwe hoofdmap:");
|
||||
if(path) { await api(`/files/mkdir?path=${encodeURIComponent(path)}`, 'POST'); fetchTree(); }
|
||||
}
|
||||
|
||||
async function deleteItem(path) {
|
||||
if(!path || !confirm(`Verwijderen: ${path}?`)) return;
|
||||
const res = await api(`/files/delete?path=${encodeURIComponent(path)}`, 'DELETE');
|
||||
if(res.status === 'deleted') fetchTree();
|
||||
else if(res.detail) alert(res.detail);
|
||||
}
|
||||
|
||||
async function fetchPods() {
|
||||
const data = await api('/pods');
|
||||
document.getElementById('pod-list').innerHTML = data.map(p => `<tr><td>${p.Name}</td><td><span class="badge ${p.Status==='Running'?'badge-running':'badge-stopped'}">${p.Status}</span></td><td>${p.Containers.length}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
async function runAction(action, name) {
|
||||
await api(`/actions/${action}/${name}`, 'POST');
|
||||
setTimeout(refresh, 1200);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetchDashboard();
|
||||
fetchContainers();
|
||||
fetchPods();
|
||||
if(document.getElementById('workloads-view').classList.contains('active')) fetchTree();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user