let cmEditor = null; function filesCurrentTheme() { const t = document.documentElement.getAttribute('data-theme'); return (t === 'light') ? 'light' : 'dark'; } function filesCodeMirrorTheme() { return filesCurrentTheme() === 'light' ? 'default' : 'material-darker'; } function filesSetEditorTheme(themeName) { if (!cmEditor) return; const cmTheme = (themeName === 'light') ? 'default' : 'material-darker'; cmEditor.setOption('theme', cmTheme); cmEditor.refresh(); } window.filesSetEditorTheme = filesSetEditorTheme; 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: filesCodeMirrorTheme() }); 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(`
${collapsed ? '▶' : '▼'} 📂 ${esc(label)} 📁 ${childNames.length} 📄 ${sortedFiles.length}
`); out.push(`
`); 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(`
📄 ${esc(f)}
`); } if (!childNames.length && !sortedFiles.length) { out.push(`
(leeg)
`); } out.push(`
`); 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(`
${collapsed ? '▶' : '▼'} 📂 root
${(rootFolder.files || []).slice().sort((a,b)=>a.localeCompare(b)).map(f => { const fullUi = f; return `
📄 ${esc(f)}
`; }).join('')}
`); } 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); } }