Files
podman-mvp/webui/html/index2.html
T

330 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>