Fix StorageDashboard 500 error and implement interactive folder tree for archive previews
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s
This commit is contained in:
parent
dbd3a868ed
commit
e79f2fbbd5
2 changed files with 69 additions and 9 deletions
|
|
@ -1152,6 +1152,8 @@ func (h *FSHandler) StorageDashboard(c *fiber.Ctx) error {
|
|||
"audio": {Category: "audio"},
|
||||
"documents": {Category: "documents"},
|
||||
"archives": {Category: "archives"},
|
||||
"code": {Category: "code"},
|
||||
"3d_models": {Category: "3d_models"},
|
||||
"other": {Category: "other"},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Download, File, Image as ImageIcon, Film, Music, FileText, Code, Loader2, Archive } from 'lucide-react';
|
||||
import { X, Download, File, Image as ImageIcon, Film, Music, FileText, Code, Loader2, Archive, Folder, ChevronLeft } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
|
|
@ -29,9 +29,44 @@ interface FilePreviewProps {
|
|||
export default function FilePreview({ file, onClose, downloadToken }: FilePreviewProps) {
|
||||
const [content, setContent] = useState<string | null>(null);
|
||||
const [archiveItems, setArchiveItems] = useState<{name: string, size: number, path: string}[] | null>(null);
|
||||
const [archivePath, setArchivePath] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const visibleItems = useMemo(() => {
|
||||
if (!archiveItems) return [];
|
||||
|
||||
const itemsMap = new Map<string, {name: string, isDir: boolean, size: number, path: string}>();
|
||||
|
||||
archiveItems.forEach(item => {
|
||||
// Normalize path (remove leading slash if any)
|
||||
const normalizedItemPath = item.path.replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
if (normalizedItemPath.startsWith(archivePath)) {
|
||||
const relativePath = normalizedItemPath.substring(archivePath.length);
|
||||
if (relativePath === '') return; // Skip the directory itself if present
|
||||
|
||||
const parts = relativePath.split('/');
|
||||
|
||||
if (parts.length === 1) {
|
||||
itemsMap.set(parts[0], { name: parts[0], isDir: false, size: item.size, path: normalizedItemPath });
|
||||
} else {
|
||||
const folderName = parts[0];
|
||||
if (!itemsMap.has(folderName)) {
|
||||
itemsMap.set(folderName, { name: folderName, isDir: true, size: 0, path: archivePath + folderName + '/' });
|
||||
}
|
||||
const folder = itemsMap.get(folderName)!;
|
||||
folder.size += item.size;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(itemsMap.values()).sort((a, b) => {
|
||||
if (a.isDir && !b.isDir) return -1;
|
||||
if (!a.isDir && b.isDir) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [archiveItems, archivePath]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose();
|
||||
|
|
@ -209,9 +244,25 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
|||
) : (
|
||||
<div className="w-full h-full flex flex-col rounded-xl overflow-hidden" style={{ backgroundColor: 'var(--color-bg-secondary)', border: '1px solid var(--color-border-subtle)' }}>
|
||||
<div className="px-4 py-3 border-b flex items-center justify-between" style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-tertiary)' }}>
|
||||
<span className="font-semibold" style={{ color: 'var(--color-text-primary)' }}>Archive Contents</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{archivePath !== '' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const parts = archivePath.split('/').filter(Boolean);
|
||||
parts.pop();
|
||||
setArchivePath(parts.length ? parts.join('/') + '/' : '');
|
||||
}}
|
||||
className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" style={{ color: 'var(--color-text-secondary)' }} />
|
||||
</button>
|
||||
)}
|
||||
<span className="font-semibold" style={{ color: 'var(--color-text-primary)' }}>
|
||||
{archivePath === '' ? 'Archive Contents' : archivePath}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm px-2 py-1 rounded-md" style={{ backgroundColor: 'var(--color-bg-elevated)', color: 'var(--color-text-secondary)' }}>
|
||||
{archiveItems?.length || 0} items
|
||||
{visibleItems.length} items
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
|
@ -223,10 +274,17 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-neutral-800">
|
||||
{archiveItems?.map((item, idx) => {
|
||||
const iconConfig = getIcon(getPreviewType({ name: item.name, mime_type: '' }));
|
||||
{visibleItems.map((item, idx) => {
|
||||
const iconConfig = item.isDir ? <Folder className="w-5 h-5" style={{ color: '#3b82f6' }} /> : getIcon(getPreviewType({ name: item.name, mime_type: '' }));
|
||||
return (
|
||||
<tr key={idx} className="hover:bg-white/5 transition-colors" style={{ color: 'var(--color-text-primary)' }}>
|
||||
<tr
|
||||
key={idx}
|
||||
className="hover:bg-white/5 transition-colors cursor-pointer"
|
||||
style={{ color: 'var(--color-text-primary)' }}
|
||||
onClick={() => {
|
||||
if (item.isDir) setArchivePath(item.path);
|
||||
}}
|
||||
>
|
||||
<td className="px-4 py-2 flex items-center gap-2">
|
||||
{iconConfig}
|
||||
<span className="truncate max-w-[60vw]" title={item.name}>{item.name}</span>
|
||||
|
|
@ -237,10 +295,10 @@ export default function FilePreview({ file, onClose, downloadToken }: FilePrevie
|
|||
</tr>
|
||||
);
|
||||
})}
|
||||
{archiveItems?.length === 0 && (
|
||||
{visibleItems.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={2} className="px-4 py-8 text-center" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
This archive is empty.
|
||||
This folder is empty.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
|
|||
Reference in a new issue