latest
This commit is contained in:
@@ -0,0 +1,876 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Grid,
|
||||
Alert,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Paper,
|
||||
TextField,
|
||||
Pagination,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CloudUpload as UploadIcon,
|
||||
Folder as FolderIcon,
|
||||
FolderOpen as FolderOpenIcon,
|
||||
Description as FileIcon,
|
||||
Delete as DeleteIcon,
|
||||
Refresh as RefreshIcon,
|
||||
PlayArrow as ProcessIcon,
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon,
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
import { supabase } from '../../supabaseClient';
|
||||
import {
|
||||
pickDirectory,
|
||||
processDirectoryFiles,
|
||||
calculateDirectoryStats,
|
||||
formatFileSize,
|
||||
isDirectoryPickerSupported,
|
||||
FileWithPath
|
||||
} from '../../utils/folderPicker';
|
||||
|
||||
interface Cabinet {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface FileRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
mime_type?: string;
|
||||
is_directory?: boolean;
|
||||
size_bytes?: number;
|
||||
processing_status?: string;
|
||||
relative_path?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
interface PaginationInfo {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_count: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
interface FileListResponse {
|
||||
files: FileRecord[];
|
||||
pagination: PaginationInfo;
|
||||
filters: {
|
||||
search?: string;
|
||||
sort_by: string;
|
||||
sort_order: string;
|
||||
include_directories: boolean;
|
||||
parent_directory_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UploadProgress {
|
||||
path: string;
|
||||
size: number;
|
||||
status: 'queued' | 'uploading' | 'done' | 'error';
|
||||
progress: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const SimpleUploadTest: React.FC = () => {
|
||||
// State management
|
||||
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
|
||||
const [selectedCabinet, setSelectedCabinet] = useState<string>('');
|
||||
const [files, setFiles] = useState<FileRecord[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Pagination and filtering state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortBy, setSortBy] = useState('created_at');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
|
||||
const [showUploadDialog, setShowUploadDialog] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<FileWithPath[]>([]);
|
||||
const [directoryStats, setDirectoryStats] = useState<any>(null);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error' | 'info', text: string } | null>(null);
|
||||
const [uploadType, setUploadType] = useState<'old' | 'new'>('new');
|
||||
|
||||
// Refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dirInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || 'http://127.0.0.1:8080';
|
||||
|
||||
const apiFetch = useCallback(async (url: string, init?: { method?: string; body?: FormData | string; headers?: Record<string, string> }) => {
|
||||
const session = await supabase.auth.getSession();
|
||||
const token = session?.data?.session?.access_token;
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No authentication token available');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...((init?.headers as Record<string, string>) || {})
|
||||
};
|
||||
|
||||
const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`;
|
||||
const res = await fetch(fullUrl, { ...init, headers });
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
throw new Error(`HTTP ${res.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}, [API_BASE]);
|
||||
|
||||
// Load cabinets and files
|
||||
const loadCabinets = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await apiFetch('/database/cabinets');
|
||||
const all = [...(data.owned || []), ...(data.shared || [])];
|
||||
setCabinets(all);
|
||||
if (all.length && !selectedCabinet) {
|
||||
setSelectedCabinet(all[0].id);
|
||||
}
|
||||
setMessage({ type: 'success', text: `Loaded ${all.length} cabinets` });
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to load cabinets:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
setMessage({ type: 'error', text: `Failed to load cabinets: ${errorMessage}` });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedCabinet, apiFetch]);
|
||||
|
||||
const loadFiles = useCallback(async (cabinetId: string, page: number = currentPage) => {
|
||||
if (!cabinetId) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Build query parameters for pagination, search, and sorting
|
||||
const params = new URLSearchParams({
|
||||
cabinet_id: cabinetId,
|
||||
page: page.toString(),
|
||||
per_page: itemsPerPage.toString(),
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
include_directories: 'true'
|
||||
});
|
||||
|
||||
if (searchTerm) {
|
||||
params.append('search', searchTerm);
|
||||
}
|
||||
|
||||
// Use the new simple upload endpoint for listing files with pagination
|
||||
const data: FileListResponse = await apiFetch(`/simple-upload/files?${params.toString()}`);
|
||||
|
||||
setFiles(data.files || []);
|
||||
setPagination(data.pagination);
|
||||
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `Loaded ${data.files?.length || 0} files (${data.pagination.total_count} total)`
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to load files:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
setMessage({ type: 'error', text: `Failed to load files: ${errorMessage}` });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, itemsPerPage, sortBy, sortOrder, searchTerm, apiFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCabinets();
|
||||
}, [loadCabinets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCabinet) {
|
||||
setCurrentPage(1); // Reset to first page when cabinet changes
|
||||
loadFiles(selectedCabinet, 1);
|
||||
}
|
||||
}, [selectedCabinet, loadFiles]);
|
||||
|
||||
// Reload files when pagination/filtering parameters change
|
||||
useEffect(() => {
|
||||
if (selectedCabinet) {
|
||||
loadFiles(selectedCabinet, currentPage);
|
||||
}
|
||||
}, [selectedCabinet, loadFiles, currentPage]);
|
||||
|
||||
// Search with debouncing
|
||||
useEffect(() => {
|
||||
if (selectedCabinet) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
loadFiles(selectedCabinet, 1);
|
||||
}, 500); // 500ms debounce
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [searchTerm, selectedCabinet, loadFiles]);
|
||||
|
||||
// Single file upload
|
||||
const handleSingleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || !selectedCabinet) return;
|
||||
|
||||
const file = e.target.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('cabinet_id', selectedCabinet);
|
||||
formData.append('path', file.name);
|
||||
formData.append('scope', 'teacher');
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Choose endpoint based on upload type
|
||||
const endpoint = uploadType === 'new' ? '/simple-upload/files/upload' : '/database/files/upload';
|
||||
|
||||
const result = await apiFetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('Upload result:', result);
|
||||
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `File uploaded successfully using ${uploadType === 'new' ? 'NEW' : 'OLD'} endpoint: ${file.name}`
|
||||
});
|
||||
|
||||
await loadFiles(selectedCabinet);
|
||||
e.target.value = '';
|
||||
} catch (error: unknown) {
|
||||
console.error('Upload failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
setMessage({ type: 'error', text: `Upload failed: ${errorMessage}` });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Directory upload handling
|
||||
const handleDirectoryPicker = async () => {
|
||||
try {
|
||||
const files = await pickDirectory();
|
||||
prepareDirectoryUpload(files);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
if (errorMessage === 'fallback-input') {
|
||||
dirInputRef.current?.click();
|
||||
} else if (errorMessage === 'user-cancelled') {
|
||||
setMessage({ type: 'info', text: 'Directory selection cancelled' });
|
||||
} else {
|
||||
console.error('Directory picker error:', error);
|
||||
setMessage({ type: 'error', text: 'Failed to pick directory. Trying fallback method...' });
|
||||
dirInputRef.current?.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFallbackDirectorySelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files) return;
|
||||
const files = processDirectoryFiles(e.target.files);
|
||||
prepareDirectoryUpload(files);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const prepareDirectoryUpload = (files: FileWithPath[]) => {
|
||||
if (files.length === 0) {
|
||||
setMessage({ type: 'error', text: 'No files selected' });
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFiles(files);
|
||||
setDirectoryStats(calculateDirectoryStats(files));
|
||||
|
||||
const progress: UploadProgress[] = files.map(file => ({
|
||||
path: file.relativePath,
|
||||
size: file.size,
|
||||
status: 'queued',
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
setUploadProgress(progress);
|
||||
setShowUploadDialog(true);
|
||||
};
|
||||
|
||||
const startDirectoryUpload = async () => {
|
||||
if (!selectedCabinet || selectedFiles.length === 0) return;
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
const firstFilePath = selectedFiles[0].relativePath;
|
||||
const directoryName = firstFilePath.split('/')[0] || 'uploaded-folder';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('cabinet_id', selectedCabinet);
|
||||
formData.append('scope', 'teacher');
|
||||
formData.append('directory_name', directoryName);
|
||||
|
||||
selectedFiles.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
const relativePaths = selectedFiles.map(f => f.relativePath);
|
||||
formData.append('file_paths', JSON.stringify(relativePaths));
|
||||
|
||||
const result = await apiFetch('/simple-upload/files/upload-directory', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('Directory upload result:', result);
|
||||
|
||||
setUploadProgress(prev => prev.map(item => ({
|
||||
...item,
|
||||
status: 'done',
|
||||
progress: 100
|
||||
})));
|
||||
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `Directory uploaded successfully: ${directoryName} (${selectedFiles.length} files)`
|
||||
});
|
||||
|
||||
await loadFiles(selectedCabinet);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowUploadDialog(false);
|
||||
setIsUploading(false);
|
||||
setSelectedFiles([]);
|
||||
setUploadProgress([]);
|
||||
}, 2000);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('Directory upload failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
setMessage({ type: 'error', text: `Directory upload failed: ${errorMessage}` });
|
||||
|
||||
setUploadProgress(prev => prev.map(item => ({
|
||||
...item,
|
||||
status: 'error',
|
||||
error: String(error)
|
||||
})));
|
||||
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete file
|
||||
const handleDelete = async (fileId: string) => {
|
||||
try {
|
||||
await apiFetch(`/simple-upload/files/${fileId}`, { method: 'DELETE' });
|
||||
setMessage({ type: 'success', text: 'File deleted successfully' });
|
||||
await loadFiles(selectedCabinet);
|
||||
} catch (error: unknown) {
|
||||
console.error('Delete failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
setMessage({ type: 'error', text: `Delete failed: ${errorMessage}` });
|
||||
}
|
||||
};
|
||||
|
||||
// Manual processing trigger
|
||||
const handleManualProcessing = async (fileId: string) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('processing_type', 'basic');
|
||||
|
||||
const result = await apiFetch(`/simple-upload/files/${fileId}/process-manual`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('Manual processing result:', result);
|
||||
setMessage({ type: 'info', text: 'Manual processing triggered (not yet implemented)' });
|
||||
} catch (error: unknown) {
|
||||
console.error('Manual processing failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
setMessage({ type: 'error', text: `Manual processing failed: ${errorMessage}` });
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'uploaded': return 'primary';
|
||||
case 'processing': return 'warning';
|
||||
case 'completed': return 'success';
|
||||
case 'failed': return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, maxWidth: 1200, mx: 'auto' }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
🧪 Simple Upload Test Page
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
<Typography variant="body2">
|
||||
This page tests the NEW simple upload system (no auto-processing) vs the OLD system (with auto-processing).
|
||||
Use this to verify that files upload without triggering Docling bundles, Tika runs, etc.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
{message && (
|
||||
<Alert severity={message.type} sx={{ mb: 2 }} onClose={() => setMessage(null)}>
|
||||
{message.text}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* Upload Controls */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Upload Controls"
|
||||
avatar={<UploadIcon />}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Cabinet</InputLabel>
|
||||
<Select
|
||||
value={selectedCabinet}
|
||||
label="Cabinet"
|
||||
onChange={(e) => setSelectedCabinet(e.target.value)}
|
||||
>
|
||||
{cabinets.map(cabinet => (
|
||||
<MenuItem key={cabinet.id} value={cabinet.id}>
|
||||
{cabinet.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||
<InputLabel>Upload Type</InputLabel>
|
||||
<Select
|
||||
value={uploadType}
|
||||
label="Upload Type"
|
||||
onChange={(e) => setUploadType(e.target.value as 'old' | 'new')}
|
||||
>
|
||||
<MenuItem value="new">🆕 NEW (Simple, No Auto-Processing)</MenuItem>
|
||||
<MenuItem value="old">🔄 OLD (Auto-Processing)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleSingleUpload}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={!selectedCabinet || loading}
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
|
||||
<input
|
||||
ref={dirInputRef}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
{...({ webkitdirectory: '' } as any)}
|
||||
multiple
|
||||
onChange={handleFallbackDirectorySelect}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<FolderOpenIcon />}
|
||||
onClick={handleDirectoryPicker}
|
||||
disabled={!selectedCabinet || loading}
|
||||
>
|
||||
Upload Directory
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={() => {
|
||||
setCurrentPage(1);
|
||||
setSearchTerm('');
|
||||
loadFiles(selectedCabinet, 1);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{isDirectoryPickerSupported() ? (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
✅ Modern directory picker supported (Chromium browser)
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="info" sx={{ mt: 1 }}>
|
||||
ℹ️ Using fallback directory picker (webkitdirectory)
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* System Info */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="System Info"
|
||||
avatar={<InfoIcon />}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Current Endpoints:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
• NEW: <code>/simple-upload/files/upload</code> (no auto-processing)
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
• OLD: <code>/database/files/upload</code> (auto-processing disabled for testing)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Storage Buckets:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Both systems now use: <code>cc.users</code> (teacher scope)
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Files will be stored in the same bucket for consistency
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Selected Cabinet:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{selectedCabinet || 'None selected'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Upload Mode:</strong>
|
||||
</Typography>
|
||||
<Chip
|
||||
label={uploadType === 'new' ? 'NEW (Simple)' : 'OLD (Auto-Processing)'}
|
||||
color={uploadType === 'new' ? 'success' : 'warning'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Files in Cabinet:</strong>
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{pagination ? `${pagination.total_count} total (${files.length} on page ${pagination.page})` : files.length}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<strong>Pagination:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{itemsPerPage} per page • Sort by {sortBy} ({sortOrder})
|
||||
</Typography>
|
||||
{searchTerm && (
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Searching: "{searchTerm}"
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* File List */}
|
||||
<Grid item xs={12}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FileIcon />
|
||||
<Typography variant="h6">
|
||||
Files {pagination && `(${pagination.total_count} total)`}
|
||||
</Typography>
|
||||
</Box>
|
||||
{pagination && (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Page {pagination.page} of {pagination.total_pages}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
{/* Search and Filter Controls */}
|
||||
<Box sx={{ mb: 2, display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search files"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
sx={{ minWidth: 200 }}
|
||||
/>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<InputLabel>Sort by</InputLabel>
|
||||
<Select
|
||||
value={sortBy}
|
||||
label="Sort by"
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
>
|
||||
<MenuItem value="name">Name</MenuItem>
|
||||
<MenuItem value="created_at">Date Created</MenuItem>
|
||||
<MenuItem value="size_bytes">Size</MenuItem>
|
||||
<MenuItem value="processing_status">Status</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||||
<InputLabel>Order</InputLabel>
|
||||
<Select
|
||||
value={sortOrder}
|
||||
label="Order"
|
||||
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}
|
||||
>
|
||||
<MenuItem value="asc">Ascending</MenuItem>
|
||||
<MenuItem value="desc">Descending</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||||
<InputLabel>Per page</InputLabel>
|
||||
<Select
|
||||
value={itemsPerPage}
|
||||
label="Per page"
|
||||
onChange={(e) => {
|
||||
setItemsPerPage(Number(e.target.value));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
<MenuItem value={5}>5</MenuItem>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={20}>20</MenuItem>
|
||||
<MenuItem value={50}>50</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* File List with Fixed Height */}
|
||||
<Box sx={{
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
height: 400, // Fixed height
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{loading ? (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<LinearProgress />
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
Loading files...
|
||||
</Typography>
|
||||
</Box>
|
||||
) : files.length === 0 ? (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{searchTerm ? 'No files found matching your search.' : 'No files found. Upload some files to test!'}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{files.map((file, index) => (
|
||||
<React.Fragment key={file.id}>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<Box>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleManualProcessing(file.id)}
|
||||
title="Trigger manual processing"
|
||||
>
|
||||
<ProcessIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleDelete(file.id)}
|
||||
title="Delete file"
|
||||
color="error"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{file.is_directory ? <FolderIcon /> : <FileIcon />}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Typography variant="body2" sx={{ wordBreak: 'break-all' }}>
|
||||
{file.name}
|
||||
</Typography>
|
||||
{file.is_directory && <Chip label="Directory" size="small" />}
|
||||
<Chip
|
||||
label={file.processing_status || 'unknown'}
|
||||
size="small"
|
||||
color={getStatusColor(file.processing_status)}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{file.size_bytes ? formatFileSize(file.size_bytes) : 'Unknown size'} • {file.mime_type || 'Unknown type'}
|
||||
</Typography>
|
||||
{file.relative_path && (
|
||||
<Typography variant="caption" color="textSecondary" display="block">
|
||||
Path: {file.relative_path}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < files.length - 1 && <Divider />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{pagination && pagination.total_pages > 1 && (
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Pagination
|
||||
count={pagination.total_pages}
|
||||
page={pagination.page}
|
||||
onChange={(event, value) => setCurrentPage(value)}
|
||||
color="primary"
|
||||
showFirstButton
|
||||
showLastButton
|
||||
/>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Showing {pagination.offset + 1}-{Math.min(pagination.offset + pagination.per_page, pagination.total_count)} of {pagination.total_count} files
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Directory Upload Dialog */}
|
||||
<Dialog open={showUploadDialog} onClose={() => !isUploading && setShowUploadDialog(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<FolderOpenIcon />
|
||||
Directory Upload Progress
|
||||
{isUploading && <LinearProgress sx={{ flexGrow: 1, ml: 2 }} />}
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{directoryStats && (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>{directoryStats.fileCount} files</strong> in{' '}
|
||||
<strong>{directoryStats.directoryCount} folders</strong><br/>
|
||||
Total size: <strong>{directoryStats.formattedSize}</strong>
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2, maxHeight: 300, overflow: 'auto' }}>
|
||||
{uploadProgress.map((item, i) => (
|
||||
<Box key={i} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', py: 0.5 }}>
|
||||
<Typography variant="body2" sx={{ flex: 1, mr: 2 }}>
|
||||
{item.path}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mr: 2, minWidth: 80 }}>
|
||||
{formatFileSize(item.size)}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={item.status}
|
||||
size="small"
|
||||
color={
|
||||
item.status === 'done' ? 'success' :
|
||||
item.status === 'error' ? 'error' :
|
||||
item.status === 'uploading' ? 'primary' : 'default'
|
||||
}
|
||||
icon={
|
||||
item.status === 'done' ? <SuccessIcon /> :
|
||||
item.status === 'error' ? <ErrorIcon /> : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowUploadDialog(false)} disabled={isUploading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={startDirectoryUpload}
|
||||
variant="contained"
|
||||
disabled={isUploading || selectedFiles.length === 0}
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Start Upload'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleUploadTest;
|
||||
Reference in New Issue
Block a user