|
|
@@ -46,6 +46,7 @@ import {
|
|
|
ChevronRight,
|
|
|
Settings,
|
|
|
User,
|
|
|
+ ClipboardList,
|
|
|
} from 'lucide-react';
|
|
|
import { api } from '../api/client';
|
|
|
import { openInSlicer, type SlicerType } from '../utils/slicer';
|
|
|
@@ -2064,7 +2065,7 @@ function ArchiveListRow({
|
|
|
}
|
|
|
|
|
|
type SortOption = 'date-desc' | 'date-asc' | 'name-asc' | 'name-desc' | 'size-desc' | 'size-asc';
|
|
|
-type ViewMode = 'grid' | 'list' | 'calendar';
|
|
|
+type ViewMode = 'grid' | 'list' | 'calendar' | 'log';
|
|
|
type Collection = 'all' | 'recent' | 'this-week' | 'this-month' | 'favorites' | 'failed' | 'duplicates';
|
|
|
|
|
|
const collections: { id: Collection; label: string; icon: React.ReactNode }[] = [
|
|
|
@@ -2133,6 +2134,29 @@ export function ArchivesPage() {
|
|
|
const [showTagManagement, setShowTagManagement] = useState(false);
|
|
|
const [highlightedArchiveId, setHighlightedArchiveId] = useState<number | null>(null);
|
|
|
|
|
|
+ // Log view state
|
|
|
+ const [logFilterUser, setLogFilterUser] = useState<string | null>(() =>
|
|
|
+ localStorage.getItem('logFilterUser') || null
|
|
|
+ );
|
|
|
+ const [logFilterStatus, setLogFilterStatus] = useState<string | null>(() =>
|
|
|
+ localStorage.getItem('logFilterStatus')
|
|
|
+ );
|
|
|
+ const [logFilterDateFrom, setLogFilterDateFrom] = useState(() =>
|
|
|
+ localStorage.getItem('logFilterDateFrom') || ''
|
|
|
+ );
|
|
|
+ const [logFilterDateTo, setLogFilterDateTo] = useState(() =>
|
|
|
+ localStorage.getItem('logFilterDateTo') || ''
|
|
|
+ );
|
|
|
+ const [logOffset, setLogOffset] = useState(() => {
|
|
|
+ const saved = localStorage.getItem('logOffset');
|
|
|
+ return saved ? Number(saved) : 0;
|
|
|
+ });
|
|
|
+ const [showClearLogConfirm, setShowClearLogConfirm] = useState(false);
|
|
|
+ const [logPageSize, setLogPageSize] = useState(() => {
|
|
|
+ const saved = localStorage.getItem('logPageSize');
|
|
|
+ return saved ? Number(saved) : 25;
|
|
|
+ });
|
|
|
+
|
|
|
// Clear highlight after 5 seconds and scroll to highlighted element
|
|
|
useEffect(() => {
|
|
|
if (highlightedArchiveId) {
|
|
|
@@ -2173,6 +2197,27 @@ export function ArchivesPage() {
|
|
|
queryFn: api.getSettings,
|
|
|
});
|
|
|
|
|
|
+ const { data: users } = useQuery({
|
|
|
+ queryKey: ['users'],
|
|
|
+ queryFn: api.getUsers,
|
|
|
+ enabled: viewMode === 'log',
|
|
|
+ });
|
|
|
+
|
|
|
+ const { data: printLogData, isLoading: isLogLoading } = useQuery({
|
|
|
+ queryKey: ['print-log', filterPrinter, logFilterUser, logFilterStatus, logFilterDateFrom, logFilterDateTo, search, logOffset, logPageSize],
|
|
|
+ queryFn: () => api.getPrintLog({
|
|
|
+ search: search || undefined,
|
|
|
+ printerId: filterPrinter || undefined,
|
|
|
+ username: logFilterUser || undefined,
|
|
|
+ status: logFilterStatus || undefined,
|
|
|
+ dateFrom: logFilterDateFrom || undefined,
|
|
|
+ dateTo: logFilterDateTo || undefined,
|
|
|
+ limit: logPageSize,
|
|
|
+ offset: logOffset,
|
|
|
+ }),
|
|
|
+ enabled: viewMode === 'log',
|
|
|
+ });
|
|
|
+
|
|
|
const timeFormat: TimeFormat = settings?.time_format || 'system';
|
|
|
const preferredSlicer: SlicerType = settings?.preferred_slicer || 'bambu_studio';
|
|
|
|
|
|
@@ -2191,6 +2236,18 @@ export function ArchivesPage() {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
+ const clearLogMutation = useMutation({
|
|
|
+ mutationFn: () => api.clearPrintLog(),
|
|
|
+ onSuccess: (data) => {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['print-log'] });
|
|
|
+ setLogOffset(0);
|
|
|
+ showToast(t('archives.log.cleared', { count: data.deleted }));
|
|
|
+ },
|
|
|
+ onError: () => {
|
|
|
+ showToast(t('archives.log.clearFailed'), 'error');
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
// Persist all filters to localStorage
|
|
|
useEffect(() => {
|
|
|
if (filterPrinter !== null) {
|
|
|
@@ -2248,6 +2305,47 @@ export function ArchivesPage() {
|
|
|
localStorage.setItem('archiveCollection', collection);
|
|
|
}, [collection]);
|
|
|
|
|
|
+ // Persist log view filters
|
|
|
+ useEffect(() => {
|
|
|
+ if (logFilterUser) {
|
|
|
+ localStorage.setItem('logFilterUser', logFilterUser);
|
|
|
+ } else {
|
|
|
+ localStorage.removeItem('logFilterUser');
|
|
|
+ }
|
|
|
+ }, [logFilterUser]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (logFilterStatus) {
|
|
|
+ localStorage.setItem('logFilterStatus', logFilterStatus);
|
|
|
+ } else {
|
|
|
+ localStorage.removeItem('logFilterStatus');
|
|
|
+ }
|
|
|
+ }, [logFilterStatus]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (logFilterDateFrom) {
|
|
|
+ localStorage.setItem('logFilterDateFrom', logFilterDateFrom);
|
|
|
+ } else {
|
|
|
+ localStorage.removeItem('logFilterDateFrom');
|
|
|
+ }
|
|
|
+ }, [logFilterDateFrom]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (logFilterDateTo) {
|
|
|
+ localStorage.setItem('logFilterDateTo', logFilterDateTo);
|
|
|
+ } else {
|
|
|
+ localStorage.removeItem('logFilterDateTo');
|
|
|
+ }
|
|
|
+ }, [logFilterDateTo]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ localStorage.setItem('logOffset', logOffset.toString());
|
|
|
+ }, [logOffset]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ localStorage.setItem('logPageSize', logPageSize.toString());
|
|
|
+ }, [logPageSize]);
|
|
|
+
|
|
|
const printerMap = new Map(printers?.map((p) => [p.id, p.name]) || []);
|
|
|
|
|
|
// Extract unique materials and colors from archives
|
|
|
@@ -2672,8 +2770,40 @@ export function ArchivesPage() {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {/* Filters */}
|
|
|
- <Card className="mb-6">
|
|
|
+ {/* View mode toggle — always visible */}
|
|
|
+ <div className="flex items-center border border-bambu-dark-tertiary rounded-lg overflow-hidden flex-shrink-0 w-fit mb-4">
|
|
|
+ <button
|
|
|
+ className={`p-2 ${viewMode === 'grid' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
+ onClick={() => setViewMode('grid')}
|
|
|
+ title={t('archives.gridView')}
|
|
|
+ >
|
|
|
+ <LayoutGrid className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ className={`p-2 ${viewMode === 'list' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
+ onClick={() => setViewMode('list')}
|
|
|
+ title={t('archives.listView')}
|
|
|
+ >
|
|
|
+ <List className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ className={`p-2 ${viewMode === 'calendar' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
+ onClick={() => setViewMode('calendar')}
|
|
|
+ title={t('archives.calendarView')}
|
|
|
+ >
|
|
|
+ <CalendarDays className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ className={`p-2 ${viewMode === 'log' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
+ onClick={() => setViewMode('log')}
|
|
|
+ title={t('archives.logView')}
|
|
|
+ >
|
|
|
+ <ClipboardList className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Filters (hidden in log view which has its own filters) */}
|
|
|
+ {viewMode !== 'log' && <Card className="mb-6">
|
|
|
<CardContent className="py-4">
|
|
|
<div className="flex flex-col md:flex-row gap-3 md:gap-4 md:items-center md:flex-wrap">
|
|
|
{/* Search - full width on mobile */}
|
|
|
@@ -2799,29 +2929,6 @@ export function ArchivesPage() {
|
|
|
<option value="size-asc">{t('archives.sortSmallest')}</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
- <div className="flex items-center border border-bambu-dark-tertiary rounded-lg overflow-hidden flex-shrink-0">
|
|
|
- <button
|
|
|
- className={`p-2 ${viewMode === 'grid' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
- onClick={() => setViewMode('grid')}
|
|
|
- title={t('archives.gridView')}
|
|
|
- >
|
|
|
- <LayoutGrid className="w-4 h-4" />
|
|
|
- </button>
|
|
|
- <button
|
|
|
- className={`p-2 ${viewMode === 'list' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
- onClick={() => setViewMode('list')}
|
|
|
- title={t('archives.listView')}
|
|
|
- >
|
|
|
- <List className="w-4 h-4" />
|
|
|
- </button>
|
|
|
- <button
|
|
|
- className={`p-2 ${viewMode === 'calendar' ? 'bg-bambu-green text-white' : 'bg-bambu-dark text-bambu-gray hover:text-white'}`}
|
|
|
- onClick={() => setViewMode('calendar')}
|
|
|
- title={t('archives.calendarView')}
|
|
|
- >
|
|
|
- <CalendarDays className="w-4 h-4" />
|
|
|
- </button>
|
|
|
- </div>
|
|
|
</div>
|
|
|
{hasTopFilters && (
|
|
|
<Button
|
|
|
@@ -2879,7 +2986,7 @@ export function ArchivesPage() {
|
|
|
</div>
|
|
|
)}
|
|
|
</CardContent>
|
|
|
- </Card>
|
|
|
+ </Card>}
|
|
|
|
|
|
{/* Pending Uploads Panel (visible when in queue mode with pending files) */}
|
|
|
<PendingUploadsPanel />
|
|
|
@@ -2958,6 +3065,211 @@ export function ArchivesPage() {
|
|
|
))}
|
|
|
</div>
|
|
|
</Card>
|
|
|
+ ) : viewMode === 'log' ? (
|
|
|
+ <div className="space-y-4">
|
|
|
+ {/* Log filters */}
|
|
|
+ <Card>
|
|
|
+ <CardContent className="py-3">
|
|
|
+ <div className="flex flex-col md:flex-row gap-3 md:items-center md:flex-wrap">
|
|
|
+ {/* Search */}
|
|
|
+ <div className="flex-1 relative md:min-w-[200px]">
|
|
|
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray" />
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ placeholder={t('archives.searchPlaceholder')}
|
|
|
+ className="w-full pl-10 pr-4 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
|
|
+ value={search}
|
|
|
+ onChange={(e) => { setSearch(e.target.value); setLogOffset(0); }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {/* Printer filter */}
|
|
|
+ <select
|
|
|
+ className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
|
|
+ value={filterPrinter || ''}
|
|
|
+ onChange={(e) => { setFilterPrinter(e.target.value ? Number(e.target.value) : null); setLogOffset(0); }}
|
|
|
+ >
|
|
|
+ <option value="">{t('archives.log.allPrinters')}</option>
|
|
|
+ {printers?.map((p) => (
|
|
|
+ <option key={p.id} value={p.id}>{p.name}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ {/* User filter */}
|
|
|
+ <select
|
|
|
+ className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
|
|
+ value={logFilterUser || ''}
|
|
|
+ onChange={(e) => { setLogFilterUser(e.target.value || null); setLogOffset(0); }}
|
|
|
+ >
|
|
|
+ <option value="">{t('archives.log.allUsers')}</option>
|
|
|
+ {users?.map((u) => (
|
|
|
+ <option key={u.id} value={u.username}>{u.username}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ {/* Status filter */}
|
|
|
+ <select
|
|
|
+ className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
|
|
+ value={logFilterStatus || ''}
|
|
|
+ onChange={(e) => { setLogFilterStatus(e.target.value || null); setLogOffset(0); }}
|
|
|
+ >
|
|
|
+ <option value="">{t('archives.log.allStatuses')}</option>
|
|
|
+ <option value="completed">{t('archives.status.completed')}</option>
|
|
|
+ <option value="failed">{t('archives.status.failed')}</option>
|
|
|
+ <option value="stopped">{t('archives.status.stopped')}</option>
|
|
|
+ <option value="cancelled">{t('archives.log.cancelled')}</option>
|
|
|
+ <option value="skipped">{t('archives.log.skipped')}</option>
|
|
|
+ </select>
|
|
|
+ {/* Date range */}
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <label className="text-sm text-bambu-gray">{t('archives.log.dateFrom')}</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
|
|
+ value={logFilterDateFrom}
|
|
|
+ onChange={(e) => { setLogFilterDateFrom(e.target.value); setLogOffset(0); }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <label className="text-sm text-bambu-gray">{t('archives.log.dateTo')}</label>
|
|
|
+ <input
|
|
|
+ type="date"
|
|
|
+ className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none text-sm"
|
|
|
+ value={logFilterDateTo}
|
|
|
+ onChange={(e) => { setLogFilterDateTo(e.target.value); setLogOffset(0); }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {/* Clear log button */}
|
|
|
+ <div className="ml-auto">
|
|
|
+ <Button
|
|
|
+ variant="danger"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => setShowClearLogConfirm(true)}
|
|
|
+ disabled={!hasPermission('archives:delete_all') || clearLogMutation.isPending}
|
|
|
+ >
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
+ {t('archives.log.clearLog')}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Log table */}
|
|
|
+ <Card>
|
|
|
+ {isLogLoading ? (
|
|
|
+ <div className="flex items-center justify-center py-12">
|
|
|
+ <Loader2 className="w-6 h-6 animate-spin text-bambu-green" />
|
|
|
+ </div>
|
|
|
+ ) : !printLogData?.items.length ? (
|
|
|
+ <div className="text-center py-12 text-bambu-gray">
|
|
|
+ {t('archives.log.noEntries')}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <table className="w-full text-sm">
|
|
|
+ <thead>
|
|
|
+ <tr className="border-b border-bambu-dark-tertiary text-bambu-gray text-left">
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.date')}</th>
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.printName')}</th>
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.printer')}</th>
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.user')}</th>
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.status')}</th>
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.duration')}</th>
|
|
|
+ <th className="px-4 py-3 font-medium">{t('archives.log.filament')}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody className="divide-y divide-bambu-dark-tertiary">
|
|
|
+ {printLogData.items.map((entry) => (
|
|
|
+ <tr key={entry.id} className="hover:bg-bambu-dark-secondary/50">
|
|
|
+ <td className="px-4 py-3 text-white whitespace-nowrap">
|
|
|
+ {formatDateTime(entry.started_at || entry.created_at, timeFormat)}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-3">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ {entry.thumbnail_path && (
|
|
|
+ <img
|
|
|
+ src={api.getPrintLogThumbnail(entry.id)}
|
|
|
+ alt=""
|
|
|
+ className="w-8 h-8 rounded object-cover flex-shrink-0"
|
|
|
+ onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <span className="text-white truncate max-w-[200px]">
|
|
|
+ {entry.print_name || '—'}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-3 text-bambu-gray-light">{entry.printer_name || '—'}</td>
|
|
|
+ <td className="px-4 py-3 text-bambu-gray-light">{entry.created_by_username || '—'}</td>
|
|
|
+ <td className="px-4 py-3">
|
|
|
+ <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
|
+ entry.status === 'completed' ? 'bg-green-500/20 text-green-400' :
|
|
|
+ entry.status === 'failed' ? 'bg-red-500/20 text-red-400' :
|
|
|
+ entry.status === 'stopped' ? 'bg-yellow-500/20 text-yellow-400' :
|
|
|
+ entry.status === 'cancelled' ? 'bg-orange-500/20 text-orange-400' :
|
|
|
+ entry.status === 'skipped' ? 'bg-blue-500/20 text-blue-400' :
|
|
|
+ 'bg-gray-500/20 text-gray-400'
|
|
|
+ }`}>
|
|
|
+ {entry.status}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-3 text-bambu-gray-light whitespace-nowrap">
|
|
|
+ {entry.duration_seconds ? formatDuration(entry.duration_seconds) : '—'}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-3">
|
|
|
+ <div className="flex items-center gap-1.5">
|
|
|
+ {entry.filament_color && (
|
|
|
+ <span
|
|
|
+ className="w-3 h-3 rounded-full border border-white/20 flex-shrink-0"
|
|
|
+ style={{ backgroundColor: entry.filament_color.startsWith('#') ? entry.filament_color : undefined }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <span className="text-bambu-gray-light text-xs">
|
|
|
+ {entry.filament_type || '—'}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ {/* Pagination */}
|
|
|
+ <div className="flex items-center justify-between px-4 py-3 border-t border-bambu-dark-tertiary flex-wrap gap-2">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <span className="text-sm text-bambu-gray">
|
|
|
+ {t('archives.log.showing', { count: Math.min(logOffset + logPageSize, printLogData.total), total: printLogData.total })}
|
|
|
+ </span>
|
|
|
+ <div className="flex items-center gap-1.5">
|
|
|
+ <label className="text-xs text-bambu-gray">{t('archives.log.rowsPerPage')}</label>
|
|
|
+ <select
|
|
|
+ className="px-2 py-1 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-xs focus:border-bambu-green focus:outline-none"
|
|
|
+ value={logPageSize}
|
|
|
+ onChange={(e) => { setLogPageSize(Number(e.target.value)); setLogOffset(0); }}
|
|
|
+ >
|
|
|
+ <option value={10}>10</option>
|
|
|
+ <option value={25}>25</option>
|
|
|
+ <option value={50}>50</option>
|
|
|
+ <option value={100}>100</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm text-bambu-gray">
|
|
|
+ {t('archives.log.page')} {Math.floor(logOffset / logPageSize) + 1} / {Math.max(1, Math.ceil(printLogData.total / logPageSize))}
|
|
|
+ </span>
|
|
|
+ <Button variant="secondary" size="sm" onClick={() => setLogOffset(Math.max(0, logOffset - logPageSize))} disabled={logOffset === 0}>
|
|
|
+ <ChevronLeft className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ <Button variant="secondary" size="sm" onClick={() => setLogOffset(logOffset + logPageSize)} disabled={logOffset + logPageSize >= printLogData.total}>
|
|
|
+ <ChevronRight className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ </div>
|
|
|
) : null}
|
|
|
|
|
|
{/* Upload Modal */}
|
|
|
@@ -3019,6 +3331,21 @@ export function ArchivesPage() {
|
|
|
{showTagManagement && (
|
|
|
<TagManagementModal onClose={() => setShowTagManagement(false)} />
|
|
|
)}
|
|
|
+
|
|
|
+ {/* Clear Log Confirmation */}
|
|
|
+ {showClearLogConfirm && (
|
|
|
+ <ConfirmModal
|
|
|
+ title={t('archives.log.clearLogTitle')}
|
|
|
+ message={t('archives.log.clearLogConfirm')}
|
|
|
+ confirmText={t('archives.log.clearLogButton')}
|
|
|
+ variant="danger"
|
|
|
+ onConfirm={() => {
|
|
|
+ clearLogMutation.mutate();
|
|
|
+ setShowClearLogConfirm(false);
|
|
|
+ }}
|
|
|
+ onCancel={() => setShowClearLogConfirm(false)}
|
|
|
+ />
|
|
|
+ )}
|
|
|
</div>
|
|
|
);
|
|
|
}
|