feat (ui): netwerken en files verfraaid
This commit is contained in:
@@ -436,6 +436,7 @@ pre{
|
|||||||
.sidebar .navLabel { display: none; }
|
.sidebar .navLabel { display: none; }
|
||||||
.sidebar .tab { justify-content: center; }
|
.sidebar .tab { justify-content: center; }
|
||||||
}
|
}
|
||||||
|
/* Files tree (Portainer-ish) */
|
||||||
.file-folder-row{
|
.file-folder-row{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
@@ -443,16 +444,64 @@ pre{
|
|||||||
gap:10px;
|
gap:10px;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
user-select:none;
|
user-select:none;
|
||||||
|
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(36,52,95,.75);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(17,26,46,.35);
|
||||||
|
transition: background .12s ease, border-color .12s ease, transform .06s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-folder-row:hover{
|
||||||
|
background: rgba(96,165,250,.08);
|
||||||
|
border-color: rgba(96,165,250,.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-folder-row:active{
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-folder-left{
|
.file-folder-left{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
gap:10px;
|
gap:10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-toggle{
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(36,52,95,.8);
|
||||||
|
background: rgba(8,12,25,.35);
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .9;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-folder-left span:last-child{
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-folder-files{
|
.file-folder-files{
|
||||||
margin-left: 18px;
|
margin-left: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 1px dashed rgba(36,52,95,.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File rows inside folders (werkt ook met inline styles die je nu al hebt) */
|
||||||
|
.file-folder-files > div{
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-folder-files > div:hover{
|
||||||
|
background: rgba(96,165,250,.06);
|
||||||
}
|
}
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -637,3 +686,125 @@ pre{
|
|||||||
.mapDetailList{ margin: 8px 0 0 0; padding-left: 18px; }
|
.mapDetailList{ margin: 8px 0 0 0; padding-left: 18px; }
|
||||||
.mapDetailList li{ margin: 4px 0; }
|
.mapDetailList li{ margin: 4px 0; }
|
||||||
.mapDetailLink{ cursor: pointer; text-decoration: underline; }
|
.mapDetailLink{ cursor: pointer; text-decoration: underline; }
|
||||||
|
|
||||||
|
/* ===== Netwerken kaart split layout ===== */
|
||||||
|
.mapSplit{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: 1fr 360px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapMain{ min-width: 0; }
|
||||||
|
.mapSide{ min-width: 0; }
|
||||||
|
|
||||||
|
@media (max-width: 1100px){
|
||||||
|
.mapSplit{ grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Legenda horizontaal ===== */
|
||||||
|
.mapLegend{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:18px;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendTitle{
|
||||||
|
font-weight:600;
|
||||||
|
margin-right:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendRow{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Legenda horizontaal + compact (override) ===== */
|
||||||
|
.mapLegend{
|
||||||
|
display:flex !important;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: none; /* was 360px */
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendTitle{
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendRow{
|
||||||
|
margin: 0; /* haalt verticale spacing weg */
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap; /* voorkomt rare afbreking */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Legenda badge stijl ===== */
|
||||||
|
|
||||||
|
.mapLegend{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:12px;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendTitle{
|
||||||
|
font-weight:800;
|
||||||
|
margin-right:6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendRow{
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:8px;
|
||||||
|
padding:6px 10px;
|
||||||
|
border:1px solid rgba(255,255,255,0.12);
|
||||||
|
border-radius:20px;
|
||||||
|
background:rgba(255,255,255,0.03);
|
||||||
|
font-size:13px;
|
||||||
|
white-space:nowrap;
|
||||||
|
transition:all .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapLegend .legendRow:hover{
|
||||||
|
border-color:rgba(255,255,255,0.25);
|
||||||
|
background:rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Files tree: actions only on hover */
|
||||||
|
.file-folder-row .file-folder-actions{
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-folder-row:hover .file-folder-actions{
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-folder-meta{
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:8px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-badge{
|
||||||
|
display:inline-flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:6px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(36,52,95,.8);
|
||||||
|
background: rgba(8,12,25,.35);
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .92;
|
||||||
|
}
|
||||||
|
|||||||
@@ -653,11 +653,11 @@
|
|||||||
.attr('class', d => `graphNode ${d.type}`);
|
.attr('class', d => `graphNode ${d.type}`);
|
||||||
|
|
||||||
node.append('circle')
|
node.append('circle')
|
||||||
.attr('r', d => d.type === 'network' ? 10 : 8);
|
.attr('r', d => d.type === 'network' ? 14 : 9);
|
||||||
|
|
||||||
node.append('text')
|
node.append('text')
|
||||||
.attr('class', 'graphLabel')
|
.attr('class', 'graphLabel')
|
||||||
.attr('x', 12)
|
.attr('x', d => d.type === 'network' ? 18 : 12)
|
||||||
.attr('y', 4)
|
.attr('y', 4)
|
||||||
.text(d => d.label || d.key);
|
.text(d => d.label || d.key);
|
||||||
|
|
||||||
|
|||||||
+126
-26
@@ -174,16 +174,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mapSplit">
|
||||||
|
<div class="mapMain">
|
||||||
<div id="networksMapHost" class="mapHost">
|
<div id="networksMapHost" class="mapHost">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mapSide">
|
||||||
<div id="networksDetailPanel" class="mapDetail" style="display:none;">
|
<div id="networksDetailPanel" class="mapDetail" style="display:none;">
|
||||||
<div class="mapDetailHeader">
|
<div class="mapDetailHeader">
|
||||||
<button class="btn small ghost" type="button" id="networksMapBackBtn">← Terug</button>
|
<button class="btn small ghost" type="button" id="networksMapBackBtn">← Terug</button>
|
||||||
<div class="mapDetailTitle" id="networksDetailTitle">Netwerk</div>
|
<div class="mapDetailTitle" id="networksDetailTitle">Netwerk</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mapDetailBody" id="networksDetailBody"></div>
|
<div class="mapDetailBody" id="networksDetailBody"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="networksMapLegend" class="mapLegend" style="display:none;">
|
<div id="networksMapLegend" class="mapLegend" style="display:none;">
|
||||||
<div class="legendTitle">Legenda</div>
|
<div class="legendTitle">Legenda</div>
|
||||||
<div class="legendRow"><span class="legendSwatch net"></span> Netwerk</div>
|
<div class="legendRow"><span class="legendSwatch net"></span> Netwerk</div>
|
||||||
@@ -1022,46 +1029,139 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render simpel: folders met files eronder
|
// Bouw een geneste folder-tree uit de "platte" API response.
|
||||||
const parts = [];
|
const folderByPath = new Map();
|
||||||
for (const folder of scoped) {
|
for (const f of scoped) {
|
||||||
const apiFolderPath = (folder.path || '').replace(/^\/+/, '');
|
const apiPath = (f.path || '').replace(/^\/+/, '');
|
||||||
const uiFolderPath = filesToUiPath(apiFolderPath); // zonder systemd/
|
folderByPath.set(apiPath, f);
|
||||||
const folderLabel = uiFolderPath || 'root';
|
}
|
||||||
const folderKey = apiFolderPath; // unieke key (met systemd/..)
|
|
||||||
const collapsed = _isFolderCollapsed(folderKey);
|
|
||||||
|
|
||||||
parts.push(`
|
function getOrCreateChild(parent, name) {
|
||||||
<div class="mono file-folder-row" data-folder="${esc(folderKey)}" style="margin:8px 0 6px 0; font-weight:600;">
|
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="file-folder-left">
|
||||||
<span class="folder-toggle">${collapsed ? '▶' : '▼'}</span>
|
<span class="folder-toggle">${collapsed ? '▶' : '▼'}</span>
|
||||||
<span>📂 ${esc(folderLabel)}</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>
|
||||||
<span class="flex" onclick="event.stopPropagation();">
|
|
||||||
<button class="btn small ok" title="Nieuw bestand in ${esc(folderLabel)}" onclick="filesNewFileInFolder(decodeURIComponent('${encodeURIComponent(uiFolderPath)}'))">+</button>
|
|
||||||
<button class="btn small bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder(decodeURIComponent('${encodeURIComponent(uiFolderPath)}'))">🗑️</button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const files = folder.files || [];
|
out.push(`<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">`);
|
||||||
if (!files.length) {
|
|
||||||
parts.push(`<div class="muted" style="margin-left:18px;">(leeg)</div>`);
|
for (const name of childNames) {
|
||||||
continue;
|
out.push(renderNode(node.children.get(name), level + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(`<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">`);
|
for (const f of sortedFiles) {
|
||||||
|
const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f;
|
||||||
for (const f of files) {
|
out.push(`
|
||||||
const fullUi = uiFolderPath ? `${uiFolderPath}/${f}` : f;
|
<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)">
|
||||||
parts.push(`
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px dashed rgba(36,52,95,.35)">
|
|
||||||
<span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
|
<span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(`</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.innerHTML = parts.join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user