340 lines
12 KiB
JavaScript
340 lines
12 KiB
JavaScript
let cmEditor = null;
|
|
|
|
function _isFolderCollapsed(folderKey) {
|
|
return localStorage.getItem('files_folder_collapsed:' + folderKey) !== '0';
|
|
// default collapsed = true
|
|
}
|
|
|
|
function _setFolderCollapsed(folderKey, v) {
|
|
localStorage.setItem('files_folder_collapsed:' + folderKey, v ? '1' : '0');
|
|
}
|
|
|
|
// =========================
|
|
// Files tab (systemd subtree)
|
|
// =========================
|
|
const FILES_ROOT = 'systemd'; // API-root binnen WORKLOADS_DIR
|
|
let filesCurrentUiPath = ''; // zonder "systemd/"
|
|
let filesCurrentApiPath = ''; // met "systemd/"
|
|
|
|
function cmModeForPath(uiPath) {
|
|
const p = (uiPath || '').toLowerCase();
|
|
if (p.endsWith('.yaml') || p.endsWith('.yml') || p.endsWith('.kube') || p.endsWith('.container')) return 'yaml';
|
|
if (p.endsWith('.json')) return 'application/json';
|
|
if (p.endsWith('.js')) return 'javascript';
|
|
return 'text/plain';
|
|
}
|
|
|
|
function filesToApiPath(uiPath) {
|
|
let p = (uiPath || '').trim().replace(/^\/+/, '');
|
|
if (!p) return FILES_ROOT;
|
|
if (p === FILES_ROOT || p.startsWith(FILES_ROOT + '/')) return p;
|
|
return `${FILES_ROOT}/${p}`;
|
|
}
|
|
|
|
function filesToUiPath(apiPath) {
|
|
const p = (apiPath || '').trim().replace(/^\/+/, '');
|
|
return p.replace(new RegExp('^' + FILES_ROOT + '/?'), '');
|
|
}
|
|
|
|
function filesSetCurrent(uiPath) {
|
|
filesCurrentUiPath = (uiPath || '').trim().replace(/^\/+/, '');
|
|
filesCurrentApiPath = filesToApiPath(filesCurrentUiPath);
|
|
document.getElementById('filesCurrent').textContent = filesCurrentUiPath || '-';
|
|
}
|
|
|
|
async function filesRefresh() {
|
|
// Files editor: CodeMirror init (alleen als textarea bestaat)
|
|
if (!cmEditor) {
|
|
const taFiles = document.getElementById('filesEditor');
|
|
if (taFiles && window.CodeMirror) {
|
|
cmEditor = CodeMirror.fromTextArea(taFiles, {
|
|
lineNumbers: true,
|
|
lineWrapping: true,
|
|
mode: 'text/plain',
|
|
theme: 'material-darker'
|
|
});
|
|
cmEditor.setSize('100%', 360);
|
|
}
|
|
}
|
|
|
|
const treeEl = document.getElementById('filesTree');
|
|
treeEl.textContent = 'Laden...';
|
|
|
|
const data = await api('/files/tree', 'GET');
|
|
|
|
// Filter alleen systemd subtree
|
|
const scoped = (data || []).filter(folder => {
|
|
const p = (folder.path || '').replace(/^\/+/, '');
|
|
return p === FILES_ROOT || p.startsWith(FILES_ROOT + '/');
|
|
});
|
|
|
|
if (!scoped.length) {
|
|
treeEl.textContent = 'Geen bestanden gevonden onder systemd.';
|
|
return;
|
|
}
|
|
|
|
// Bouw een geneste folder-tree uit de "platte" API response.
|
|
const folderByPath = new Map();
|
|
for (const f of scoped) {
|
|
const apiPath = (f.path || '').replace(/^\/+/, '');
|
|
folderByPath.set(apiPath, f);
|
|
}
|
|
|
|
function getOrCreateChild(parent, name) {
|
|
if (!parent.children.has(name)) {
|
|
const apiPath = parent.apiPath ? `${parent.apiPath}/${name}` : name;
|
|
parent.children.set(name, {
|
|
name,
|
|
apiPath,
|
|
uiPath: filesToUiPath(apiPath),
|
|
children: new Map(),
|
|
});
|
|
}
|
|
return parent.children.get(name);
|
|
}
|
|
|
|
const root = { name: FILES_ROOT, apiPath: FILES_ROOT, uiPath: '', children: new Map() };
|
|
|
|
// 1) Nodes aanmaken op basis van bekende folder paths
|
|
for (const apiPath of folderByPath.keys()) {
|
|
if (apiPath === FILES_ROOT) continue;
|
|
if (!apiPath.startsWith(FILES_ROOT + '/')) continue;
|
|
const rel = apiPath.slice((FILES_ROOT + '/').length);
|
|
const segs = rel.split('/').filter(Boolean);
|
|
let cur = root;
|
|
for (const s of segs) cur = getOrCreateChild(cur, s);
|
|
}
|
|
|
|
// 2) Nodes aanvullen op basis van dirs-lijsten (zodat lege tussenfolders ook verschijnen)
|
|
for (const [apiPath, folder] of folderByPath.entries()) {
|
|
if (apiPath !== FILES_ROOT && !apiPath.startsWith(FILES_ROOT + '/')) continue;
|
|
|
|
let base = root;
|
|
if (apiPath !== FILES_ROOT) {
|
|
const rel = apiPath.slice((FILES_ROOT + '/').length);
|
|
const segs = rel.split('/').filter(Boolean);
|
|
for (const s of segs) base = getOrCreateChild(base, s);
|
|
}
|
|
|
|
for (const d of (folder.dirs || [])) getOrCreateChild(base, d);
|
|
}
|
|
|
|
function renderNode(node, level) {
|
|
const folderKey = node.apiPath;
|
|
const collapsed = _isFolderCollapsed(folderKey);
|
|
const label = node.uiPath || 'root';
|
|
const indent = Math.max(0, level) * 14;
|
|
|
|
const folder = folderByPath.get(folderKey);
|
|
const files = (folder && folder.files) ? folder.files : [];
|
|
|
|
// subfolders (NU AL beschikbaar voor de badges)
|
|
const childNames = Array.from(node.children.keys()).sort((a,b) => a.localeCompare(b));
|
|
|
|
// files (NU AL beschikbaar voor de badges)
|
|
const sortedFiles = (files || []).slice().sort((a,b) => a.localeCompare(b));
|
|
|
|
const out = [];
|
|
out.push(`
|
|
<div class="mono file-folder-row" data-folder="${esc(folderKey)}" style="margin:${level === 0 ? '8px' : '6px'} 0 6px 0; padding-left:${indent}px; font-weight:600;">
|
|
<span class="file-folder-left">
|
|
<span class="folder-toggle">${collapsed ? '▶' : '▼'}</span>
|
|
<span>📂 ${esc(label)}</span>
|
|
</span>
|
|
<span class="file-folder-meta" onclick="event.stopPropagation();">
|
|
<span class="file-badge" title="Subfolders in deze map">📁 ${childNames.length}</span>
|
|
<span class="file-badge" title="Bestanden in deze map">📄 ${sortedFiles.length}</span>
|
|
|
|
<span class="flex file-folder-actions">
|
|
<button class="btn small ok" title="Nieuw bestand in ${esc(label)}" onclick="filesNewFileInFolder(decodeURIComponent('${encodeURIComponent(node.uiPath)}'))">+</button>
|
|
<button class="btn small bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder(decodeURIComponent('${encodeURIComponent(node.uiPath)}'))">🗑️</button>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
`);
|
|
|
|
out.push(`<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">`);
|
|
|
|
for (const name of childNames) {
|
|
out.push(renderNode(node.children.get(name), level + 1));
|
|
}
|
|
|
|
for (const f of sortedFiles) {
|
|
const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f;
|
|
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)">
|
|
<span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
if (!childNames.length && !sortedFiles.length) {
|
|
out.push(`<div class="muted" style="padding-left:${indent + 18}px;">(leeg)</div>`);
|
|
}
|
|
|
|
out.push(`</div>`);
|
|
return out.join('');
|
|
}
|
|
|
|
const parts = [];
|
|
const topNames = Array.from(root.children.keys()).sort((a,b) => a.localeCompare(b));
|
|
for (const n of topNames) parts.push(renderNode(root.children.get(n), 0));
|
|
|
|
// Files direct onder "systemd/" (root) tonen bovenaan
|
|
const rootFolder = folderByPath.get(FILES_ROOT);
|
|
if (rootFolder && (rootFolder.files || []).length) {
|
|
const folderKey = FILES_ROOT;
|
|
const collapsed = _isFolderCollapsed(folderKey);
|
|
parts.unshift(`
|
|
<div class="mono file-folder-row" data-folder="${esc(folderKey)}" style="margin:8px 0 6px 0; font-weight:600;">
|
|
<span class="file-folder-left">
|
|
<span class="folder-toggle">${collapsed ? '▶' : '▼'}</span>
|
|
<span>📂 root</span>
|
|
</span>
|
|
<span class="flex" onclick="event.stopPropagation();">
|
|
<button class="btn small ok" title="Nieuw bestand in root" onclick="filesNewFileInFolder('')">+</button>
|
|
</span>
|
|
</div>
|
|
<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 => {
|
|
const fullUi = f;
|
|
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)">
|
|
<span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
treeEl.innerHTML = parts.join('');
|
|
treeEl.onclick = (ev) => {
|
|
const row = ev.target.closest('.file-folder-row');
|
|
if (!row) return;
|
|
|
|
const folderKey = row.getAttribute('data-folder');
|
|
const isNowCollapsed = !_isFolderCollapsed(folderKey);
|
|
_setFolderCollapsed(folderKey, isNowCollapsed);
|
|
|
|
// pijltje updaten
|
|
const arrow = row.querySelector('.folder-toggle');
|
|
if (arrow) arrow.textContent = isNowCollapsed ? '▶' : '▼';
|
|
|
|
// files block tonen/verbergen
|
|
const filesBlock = treeEl.querySelector(`[data-folder-files="${CSS.escape(folderKey)}"]`);
|
|
if (filesBlock) filesBlock.style.display = isNowCollapsed ? 'none' : '';
|
|
};
|
|
}
|
|
|
|
async function filesOpen(uiPath) {
|
|
filesSetCurrent(uiPath);
|
|
|
|
const res = await api(`/files/read?path=${encodeURIComponent(filesCurrentApiPath)}`, 'GET');
|
|
|
|
const text = res.content || '';
|
|
if (cmEditor) {
|
|
cmEditor.setOption('mode', cmModeForPath(uiPath));
|
|
cmEditor.setValue(text);
|
|
cmEditor.refresh();
|
|
} else {
|
|
document.getElementById('filesEditor').value = text;
|
|
}
|
|
}
|
|
|
|
async function filesSave() {
|
|
if (!filesCurrentApiPath || filesCurrentApiPath === FILES_ROOT) {
|
|
return showModal('Files', 'Selecteer eerst een bestand.');
|
|
}
|
|
|
|
const content = cmEditor
|
|
? cmEditor.getValue()
|
|
: document.getElementById('filesEditor').value;
|
|
|
|
const res = await api(
|
|
`/files/save?path=${encodeURIComponent(filesCurrentApiPath)}`,
|
|
'POST',
|
|
{ content }
|
|
);
|
|
|
|
showModal('Opgeslagen', JSON.stringify(res, null, 2));
|
|
await filesRefresh();
|
|
}
|
|
|
|
async function filesDelete() {
|
|
if (!filesCurrentApiPath || filesCurrentApiPath === FILES_ROOT) {
|
|
return showModal('Files', 'Selecteer eerst een bestand om te verwijderen.');
|
|
}
|
|
if (!confirm(`Verwijderen: ${filesCurrentUiPath}?`)) return;
|
|
|
|
const res = await api(`/files/delete?path=${encodeURIComponent(filesCurrentApiPath)}`, 'DELETE');
|
|
showModal('Verwijderd', JSON.stringify(res, null, 2));
|
|
|
|
// reset current
|
|
filesSetCurrent('');
|
|
document.getElementById('filesEditor').value = '';
|
|
await filesRefresh();
|
|
}
|
|
|
|
async function filesNewFolder() {
|
|
const ui = prompt('Nieuwe map (onder systemd):\nVoorbeeld: mediaserver', '');
|
|
if (!ui) return;
|
|
|
|
const apiPath = filesToApiPath(ui);
|
|
const res = await api(`/files/mkdir?path=${encodeURIComponent(apiPath)}`, 'POST');
|
|
showModal('Map aangemaakt', JSON.stringify(res, null, 2));
|
|
await filesRefresh();
|
|
}
|
|
|
|
async function filesNewFile() {
|
|
const ui = prompt('Nieuw bestand (onder systemd):\nVoorbeeld: demo-web/demo-web.container', '');
|
|
if (!ui) return;
|
|
|
|
const apiPath = filesToApiPath(ui);
|
|
|
|
// altijd leeg (jouw keuze) -> leeg bestand
|
|
const res = await api(`/files/save?path=${encodeURIComponent(apiPath)}`, 'POST', { content: "" });
|
|
showModal('Bestand aangemaakt', JSON.stringify(res, null, 2));
|
|
|
|
// Open direct
|
|
filesSetCurrent(ui);
|
|
const editorEl = document.getElementById('filesEditor');
|
|
if (editorEl) editorEl.value = "";
|
|
await filesRefresh();
|
|
await filesOpen(ui);
|
|
}
|
|
|
|
async function filesNewFileInFolder(uiFolderPath) {
|
|
const base = (uiFolderPath || '').trim().replace(/^\/+/, '');
|
|
const name = prompt(`Nieuw bestand in "${base || 'root'}"\nBijv: test.yaml of demo.container`, '');
|
|
if (!name) return;
|
|
|
|
const uiFull = base ? `${base}/${name}` : name;
|
|
const apiPath = filesToApiPath(uiFull);
|
|
|
|
// altijd leeg (jouw keuze)
|
|
const res = await api(`/files/save?path=${encodeURIComponent(apiPath)}`, 'POST', { content: "" });
|
|
showModal('Bestand aangemaakt', JSON.stringify(res, null, 2));
|
|
|
|
await filesRefresh();
|
|
await filesOpen(uiFull);
|
|
}
|
|
|
|
async function filesDeleteFolder(uiFolderPath) {
|
|
const base = (uiFolderPath || '').trim().replace(/^\/+/, '');
|
|
if (!base) {
|
|
return showModal('Files', 'Root map verwijderen mag niet.');
|
|
}
|
|
if (!confirm(`Map verwijderen (alleen als leeg): ${base}?`)) return;
|
|
|
|
const apiPath = filesToApiPath(base);
|
|
|
|
try {
|
|
const res = await api(`/files/rmdir?path=${encodeURIComponent(apiPath)}`, 'DELETE');
|
|
showModal('Map verwijderd', JSON.stringify(res, null, 2));
|
|
await filesRefresh();
|
|
} catch (e) {
|
|
showModal('Kan map niet verwijderen', e.message);
|
|
}
|
|
}
|