From ff38b74548ad6df97ac7252e3cc18ee60d397846 Mon Sep 17 00:00:00 2001 From: serversdwn Date: Mon, 19 Jan 2026 21:31:22 +0000 Subject: [PATCH] feat: added collapsible view in project data files. --- templates/partials/projects/ftp_browser.html | 273 ++++++++++++++--- .../partials/projects/unified_files.html | 279 ++++++++++-------- templates/partials/slm_live_view.html | 239 +++++++++++++-- 3 files changed, 593 insertions(+), 198 deletions(-) diff --git a/templates/partials/projects/ftp_browser.html b/templates/partials/projects/ftp_browser.html index 6d90e7e..d2499d4 100644 --- a/templates/partials/projects/ftp_browser.html +++ b/templates/partials/projects/ftp_browser.html @@ -185,49 +185,90 @@ async function loadFTPFiles(unitId, path) { return a.name.localeCompare(b.name); }); - // Render file list - let html = ''; - for (const file of sorted) { - const icon = file.is_dir - ? '' - : ''; - - const sizeStr = file.is_dir ? '' : formatFileSize(file.size); - - html += ` -
- ${icon} -
-
${file.name}
-
${file.modified}${sizeStr ? ' • ' + sizeStr : ''}
-
- ${file.is_dir ? ` -
- -
- ` : ` -
- -
- `} -
- `; + function escapeForAttribute(str) { + return String(str).replace(/'/g, "\\'").replace(/"/g, '"'); } + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + function getFileIcon(file) { + if (file.is_dir) { + return ''; + } else if (file.name.toLowerCase().endsWith('.csv')) { + return ''; + } else if (file.name.toLowerCase().match(/\.(txt|log)$/)) { + return ''; + } + return ''; + } + + // Render file list with collapsible folders + let html = '
'; + for (const file of sorted) { + const icon = getFileIcon(file); + const sizeStr = file.is_dir ? '' : formatFileSize(file.size); + const folderId = 'proj-folder-' + file.path.replace(/[^a-zA-Z0-9]/g, '-'); + + if (file.is_dir) { + // Collapsible folder row + html += ` +
+
+
+ + + + ${icon} + ${escapeHtml(file.name)} + +
+
+ + +
+
+ +
+ `; + } else { + html += ` +
+
+ ${icon} + ${escapeHtml(file.name)} +
+
+ + + +
+
+ `; + } + } + html += '
'; + fileListDiv.innerHTML = html; } catch (error) { fileListDiv.innerHTML = '
Error loading files: ' + error + '
'; @@ -235,6 +276,156 @@ async function loadFTPFiles(unitId, path) { } } +// Toggle folder expand/collapse and load contents for Project FTP browser +async function toggleFTPFolderProject(unitId, folderPath, folderId, headerElement) { + const contentDiv = document.getElementById(folderId); + const chevron = headerElement.querySelector('.folder-chevron'); + const statusSpan = headerElement.querySelector('.folder-status'); + + if (!contentDiv) return; + + const isExpanded = !contentDiv.classList.contains('hidden'); + + if (isExpanded) { + // Collapse + contentDiv.classList.add('hidden'); + chevron.style.transform = 'rotate(0deg)'; + } else { + // Expand and load contents if not already loaded + contentDiv.classList.remove('hidden'); + chevron.style.transform = 'rotate(90deg)'; + + // Check if already loaded + if (contentDiv.dataset.loaded === 'true') { + return; + } + + // Show loading state + contentDiv.innerHTML = '
Loading...
'; + + try { + const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=${encodeURIComponent(folderPath)}`); + const result = await response.json(); + + if (result.status !== 'ok' && !result.files) { + throw new Error(result.detail || 'Failed to list files'); + } + + const files = result.files || []; + + if (files.length === 0) { + contentDiv.innerHTML = '
Empty folder
'; + statusSpan.textContent = '(empty)'; + contentDiv.dataset.loaded = 'true'; + return; + } + + // Sort: directories first, then by name + files.sort((a, b) => { + if (a.is_dir && !b.is_dir) return -1; + if (!a.is_dir && b.is_dir) return 1; + return a.name.localeCompare(b.name); + }); + + // Update status with file count + const dirCount = files.filter(f => f.is_dir).length; + const fileCount = files.length - dirCount; + let statusText = []; + if (dirCount > 0) statusText.push(`${dirCount} folder${dirCount > 1 ? 's' : ''}`); + if (fileCount > 0) statusText.push(`${fileCount} file${fileCount > 1 ? 's' : ''}`); + statusSpan.textContent = `(${statusText.join(', ')})`; + + function escapeForAttribute(str) { + return String(str).replace(/'/g, "\\'").replace(/"/g, '"'); + } + + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + function getFileIcon(file) { + if (file.is_dir) { + return ''; + } else if (file.name.toLowerCase().endsWith('.csv')) { + return ''; + } else if (file.name.toLowerCase().match(/\.(txt|log)$/)) { + return ''; + } + return ''; + } + + let html = '
'; + + files.forEach(file => { + const fullPath = file.path || `${folderPath}/${file.name}`; + const icon = getFileIcon(file); + const sizeText = file.size ? formatFileSize(file.size) : ''; + const subFolderId = 'proj-folder-' + fullPath.replace(/[^a-zA-Z0-9]/g, '-'); + + if (file.is_dir) { + // Nested collapsible folder + html += ` +
+
+
+ + + + ${icon} + ${escapeHtml(file.name)} + +
+
+ +
+
+ +
+ `; + } else { + html += ` +
+
+ ${icon} + ${escapeHtml(file.name)} +
+
+ + +
+
+ `; + } + }); + + html += '
'; + contentDiv.innerHTML = html; + contentDiv.dataset.loaded = 'true'; + + } catch (error) { + console.error('Failed to load folder contents:', error); + contentDiv.innerHTML = `
Error: ${error.message}
`; + } + } +} + async function downloadFTPFile(unitId, remotePath, fileName) { const btn = event.target; btn.disabled = true; diff --git a/templates/partials/projects/unified_files.html b/templates/partials/projects/unified_files.html index 1bb8708..d0a55b4 100644 --- a/templates/partials/projects/unified_files.html +++ b/templates/partials/projects/unified_files.html @@ -1,4 +1,4 @@ - + {% if sessions %}
{% for session_data in sessions %} @@ -8,146 +8,154 @@ {% set files = session_data.files %} {% if files %} - -
-
-
- - - -
-
- {{ session.started_at.strftime('%Y-%m-%d %H:%M') if session.started_at else 'Unknown Date' }} -
-
- {% if unit %}{{ unit.id }}{% else %}Unknown Unit{% endif %} - {% if location %} @ {{ location.name }}{% endif %} - - {{ files|length }} file{{ 's' if files|length != 1 else '' }} + +
+ +
+
+
+ + + + + + + +
+
+ {{ session.started_at.strftime('%Y-%m-%d %H:%M') if session.started_at else 'Unknown Date' }} +
+
+ {% if unit %}{{ unit.id }}{% else %}Unknown Unit{% endif %} + {% if location %} @ {{ location.name }}{% endif %} + + {{ files|length }} file{{ 's' if files|length != 1 else '' }} +
-
-
- - {{ session.status or 'unknown' }} - +
+ + {{ session.status or 'unknown' }} + +
-
- -
-
- {% for file_data in files %} - {% set file = file_data.file %} - {% set exists = file_data.exists_on_disk %} - {% set metadata = file_data.metadata %} + +