u
This commit is contained in:
@@ -9,6 +9,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Table from '@mui/material/Table';
|
||||
@@ -18,15 +19,18 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import BackupOutlinedIcon from '@mui/icons-material/BackupOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||
import CloudQueueOutlinedIcon from '@mui/icons-material/CloudQueueOutlined';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
@@ -34,7 +38,6 @@ import backupAutomat from '../../automats/backupAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
BORDER,
|
||||
CHROMA,
|
||||
DARK_1,
|
||||
DARK_2,
|
||||
DARK_4,
|
||||
@@ -79,19 +82,19 @@ function formatDuration(ms) {
|
||||
export default class BackupSettingsPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { status, error, runningBackup, backupStatus, backups } = backupAutomat.state;
|
||||
const { status, error, backupStatus, backups, vfsFilesByBackup, vfsLoadingByBackup } = backupAutomat.state;
|
||||
this.state = {
|
||||
status,
|
||||
error,
|
||||
runningBackup,
|
||||
backupStatus,
|
||||
backups,
|
||||
vfsFilesByBackup: vfsFilesByBackup || {},
|
||||
vfsLoadingByBackup: vfsLoadingByBackup || {},
|
||||
tierTab: 'hourly', // 'hourly' | 'daily' | 'monthly'
|
||||
expandedItemName: null,
|
||||
};
|
||||
|
||||
this.handleRefresh = this.handleRefresh.bind(this);
|
||||
this.handleRunBackup = this.handleRunBackup.bind(this);
|
||||
this.handleTierChange = this.handleTierChange.bind(this);
|
||||
this.handleToggleExpand = this.handleToggleExpand.bind(this);
|
||||
}
|
||||
@@ -100,9 +103,10 @@ export default class BackupSettingsPanel extends Component {
|
||||
this.unsub = backupAutomat.subscribe(this, (state) => ({
|
||||
status: state.status,
|
||||
error: state.error,
|
||||
runningBackup: state.runningBackup,
|
||||
backupStatus: state.backupStatus,
|
||||
backups: state.backups,
|
||||
vfsFilesByBackup: state.vfsFilesByBackup,
|
||||
vfsLoadingByBackup: state.vfsLoadingByBackup,
|
||||
}));
|
||||
|
||||
if (backupAutomat.state.status === 'idle') {
|
||||
@@ -137,23 +141,6 @@ export default class BackupSettingsPanel extends Component {
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region handleRunBackup
|
||||
// MARK: calls POST /backup/run via backupAutomat.actions.runBackup from ./api/index.js
|
||||
async handleRunBackup() {
|
||||
if (this.state.runningBackup || this.state.backupStatus?.isRunning) return;
|
||||
|
||||
try {
|
||||
const result = await backupAutomat.actions.runBackup();
|
||||
const sizeStr = result?.totalSizeBytes != null ? ` (${formatBytes(result.totalSizeBytes)})` : '';
|
||||
enqueueSnackbar(`Manuelles Backup erfolgreich erstellt${sizeStr}.`, {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
handleTierChange(_event, newValue) {
|
||||
this.setState({ tierTab: newValue, expandedItemName: null });
|
||||
}
|
||||
@@ -165,7 +152,16 @@ export default class BackupSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, error, runningBackup, backupStatus, backups, tierTab, expandedItemName } = this.state;
|
||||
const {
|
||||
status,
|
||||
error,
|
||||
backupStatus,
|
||||
backups,
|
||||
vfsFilesByBackup,
|
||||
vfsLoadingByBackup,
|
||||
tierTab,
|
||||
expandedItemName,
|
||||
} = this.state;
|
||||
|
||||
if (status === 'loading' && !backupStatus) {
|
||||
return (
|
||||
@@ -196,7 +192,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
const isExecuting = Boolean(runningBackup || backupStatus?.isRunning);
|
||||
const isRunning = Boolean(backupStatus?.isRunning);
|
||||
const retention = backupStatus?.retention ?? { hourlyMax: 24, dailyMax: 7, monthly: 'Monatserster' };
|
||||
const counts = backups?.counts ?? { hourly: 0, daily: 0, monthly: 0 };
|
||||
const hourlyList = backups?.hourly || [];
|
||||
@@ -219,7 +215,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
Datensicherung & Backups
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Automatische Sicherung von Datenbank, virtuellen Dateisystemen (VFS) und Dokumentenspeicher.
|
||||
Automatische Sicherung von Datenbank, virtuellem Dateisystem (VFS) und Dokumentenspeicher.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1.5} sx={{ alignItems: 'center' }}>
|
||||
@@ -228,20 +224,10 @@ export default class BackupSettingsPanel extends Component {
|
||||
size="small"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={this.handleRefresh}
|
||||
disabled={isExecuting}
|
||||
disabled={isRunning}
|
||||
>
|
||||
Aktualisieren
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={isExecuting ? <CircularProgress size={16} color="inherit" /> : <PlayArrowOutlinedIcon />}
|
||||
onClick={this.handleRunBackup}
|
||||
disabled={isExecuting}
|
||||
sx={{ bgcolor: PRIMARY, '&:hover': { bgcolor: CHROMA.PETROL.dark } }}
|
||||
>
|
||||
{isExecuting ? 'Sicherung läuft...' : 'Jetzt sichern'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -265,7 +251,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
color={backupStatus?.enabled ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
{isExecuting && (
|
||||
{isRunning && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<CircularProgress size={12} color="inherit" />}
|
||||
@@ -410,7 +396,7 @@ export default class BackupSettingsPanel extends Component {
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer sx={{ maxHeight: 360 }}>
|
||||
<TableContainer sx={{ maxHeight: 480 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -418,22 +404,24 @@ export default class BackupSettingsPanel extends Component {
|
||||
<TableCell sx={{ fontWeight: 600 }}>Zeitpunkt</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Archivname</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Größe</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Typ</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>VFS-Dateien</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{activeList.map((item) => {
|
||||
const isExpanded = expandedItemName === item.name;
|
||||
const manifest = item.manifest;
|
||||
const isManual = Boolean(manifest?.manual);
|
||||
const vfsFiles = vfsFilesByBackup[item.name];
|
||||
const vfsLoading = Boolean(vfsLoadingByBackup[item.name]);
|
||||
|
||||
return (
|
||||
<BackupTableRow
|
||||
key={item.name}
|
||||
item={item}
|
||||
isExpanded={isExpanded}
|
||||
isManual={isManual}
|
||||
manifest={manifest}
|
||||
vfsFiles={vfsFiles}
|
||||
vfsLoading={vfsLoading}
|
||||
onToggleExpand={() => this.handleToggleExpand(item.name)}
|
||||
/>
|
||||
);
|
||||
@@ -448,120 +436,314 @@ export default class BackupSettingsPanel extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
function BackupTableRow({ item, isExpanded, isManual, manifest, onToggleExpand }) {
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
hover
|
||||
onClick={onToggleExpand}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
bgcolor: isExpanded ? LIGHT_3 : 'inherit',
|
||||
'& > *': { borderBottom: isExpanded ? 'none !important' : undefined },
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{ py: 1 }}>
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onToggleExpand(); }}>
|
||||
{isExpanded ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, fontWeight: 500 }}>
|
||||
{formatDate(item.timestamp)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, color: DARK_2, fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
{item.name}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, fontWeight: 600 }}>
|
||||
{formatBytes(item.totalSizeBytes)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={isManual ? 'Manuell' : 'Automatisch'}
|
||||
color={isManual ? 'primary' : 'default'}
|
||||
variant="outlined"
|
||||
sx={{ height: 22, fontSize: '0.75rem' }}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ bgcolor: LIGHT_3 }}>
|
||||
<TableCell colSpan={5} sx={{ py: 0, px: 3 }}>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Typography variant="caption" fontWeight={700} color="text.secondary" sx={{ textTransform: 'uppercase' }}>
|
||||
Backup-Details & Manifest
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' },
|
||||
gap: 1.5,
|
||||
mt: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: WHITE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Datenbank
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.database
|
||||
? `${formatBytes(manifest.files.database.sizeBytes)} (${manifest.files.database.backedUp ? 'OK' : manifest.files.database.reason || 'Übersprungen'})`
|
||||
: '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
VFS (Dateien & Belege)
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.vfs ? formatBytes(manifest.files.vfs.sizeBytes) : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Dokumentenspeicher (Storage)
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.storage ? formatBytes(manifest.files.storage.sizeBytes) : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{manifest?.durationMs != null && (
|
||||
class BackupTableRow extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
vfsFilter: '',
|
||||
recoveringPath: null,
|
||||
vfsExplorerOpen: false,
|
||||
};
|
||||
|
||||
this.handleToggleVfsExplorer = this.handleToggleVfsExplorer.bind(this);
|
||||
this.handleRecoverFile = this.handleRecoverFile.bind(this);
|
||||
}
|
||||
|
||||
// #region handleToggleVfsExplorer
|
||||
// MARK: calls backupAutomat.actions.listBackupVfs from ./api/index.js
|
||||
async handleToggleVfsExplorer(e) {
|
||||
e?.stopPropagation?.();
|
||||
const nextOpen = !this.state.vfsExplorerOpen;
|
||||
this.setState({ vfsExplorerOpen: nextOpen });
|
||||
|
||||
if (nextOpen && !this.props.vfsFiles && !this.props.vfsLoading) {
|
||||
try {
|
||||
await backupAutomat.actions.listBackupVfs(this.props.item.name);
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region handleRecoverFile
|
||||
// MARK: calls POST /backup/:id/vfs/recover via backupAutomat.actions.recoverBackupVfs from ./api/index.js
|
||||
async handleRecoverFile(e, path) {
|
||||
e?.stopPropagation?.();
|
||||
if (this.state.recoveringPath) return;
|
||||
|
||||
this.setState({ recoveringPath: path });
|
||||
try {
|
||||
await backupAutomat.actions.recoverBackupVfs(this.props.item.name, path);
|
||||
enqueueSnackbar(`Datei "${path}" erfolgreich ins VFS wiederhergestellt.`, {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
} finally {
|
||||
this.setState({ recoveringPath: null });
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
render() {
|
||||
const { item, isExpanded, manifest, vfsFiles, vfsLoading, onToggleExpand } = this.props;
|
||||
const { vfsFilter, recoveringPath, vfsExplorerOpen } = this.state;
|
||||
|
||||
const filteredFiles = Array.isArray(vfsFiles)
|
||||
? vfsFilter.trim()
|
||||
? vfsFiles.filter((f) => f.name.toLowerCase().includes(vfsFilter.toLowerCase()))
|
||||
: vfsFiles
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
hover
|
||||
onClick={onToggleExpand}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
bgcolor: isExpanded ? LIGHT_3 : 'inherit',
|
||||
'& > *': { borderBottom: isExpanded ? 'none !important' : undefined },
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{ py: 1 }}>
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onToggleExpand(); }}>
|
||||
{isExpanded ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, fontWeight: 500 }}>
|
||||
{formatDate(item.timestamp)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, color: DARK_2, fontFamily: 'monospace', fontSize: '0.8rem' }}>
|
||||
{item.name}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1, fontWeight: 600 }}>
|
||||
{formatBytes(item.totalSizeBytes)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<FolderOutlinedIcon />}
|
||||
label={manifest?.files?.vfs ? formatBytes(manifest.files.vfs.sizeBytes) : '—'}
|
||||
variant="outlined"
|
||||
sx={{ height: 22, fontSize: '0.75rem' }}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ bgcolor: LIGHT_3 }}>
|
||||
<TableCell colSpan={5} sx={{ py: 0, px: 3 }}>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Typography variant="caption" fontWeight={700} color="text.secondary" sx={{ textTransform: 'uppercase' }}>
|
||||
Backup-Details & Manifest
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' },
|
||||
gap: 1.5,
|
||||
mt: 1,
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: WHITE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Dauer
|
||||
Datenbank
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{formatDuration(manifest.durationMs)}
|
||||
{manifest?.files?.database
|
||||
? `${formatBytes(manifest.files.database.sizeBytes)} (${manifest.files.database.backedUp ? 'OK' : manifest.files.database.reason || 'Übersprungen'})`
|
||||
: '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{manifest?.dbProvider && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Datenbank-Provider
|
||||
VFS (Dateien & Belege)
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest.dbProvider.toUpperCase()}
|
||||
{manifest?.files?.vfs ? formatBytes(manifest.files.vfs.sizeBytes) : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ gridColumn: { sm: 'span 3' } }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Speicherpfad
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: DARK_2, wordBreak: 'break-all' }}>
|
||||
{item.path}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Dokumentenspeicher (Storage)
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest?.files?.storage ? formatBytes(manifest.files.storage.sizeBytes) : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{manifest?.durationMs != null && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Dauer
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{formatDuration(manifest.durationMs)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{manifest?.dbProvider && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Datenbank-Provider
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{manifest.dbProvider.toUpperCase()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ gridColumn: { sm: 'span 3' } }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Speicherpfad
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: DARK_2, wordBreak: 'break-all' }}>
|
||||
{item.path}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* VFS File Explorer & Restore Section */}
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
p: 1.5,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: WHITE,
|
||||
border: `1px solid ${BORDER}`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FolderOutlinedIcon sx={{ color: PRIMARY, fontSize: 20 }} />
|
||||
<Typography variant="subtitle2" fontWeight={700} sx={{ color: DARK_4 }}>
|
||||
Archivierte VFS-Dateien & Wiederherstellung
|
||||
</Typography>
|
||||
{Array.isArray(vfsFiles) && (
|
||||
<Chip size="small" label={`${vfsFiles.length} Dateien`} variant="outlined" sx={{ height: 20, fontSize: '0.75rem' }} />
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={vfsLoading ? <CircularProgress size={14} color="inherit" /> : vfsExplorerOpen ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
onClick={this.handleToggleVfsExplorer}
|
||||
disabled={vfsLoading}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
{vfsExplorerOpen ? 'Dateien verbergen' : 'VFS-Dateien durchsuchen'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Collapse in={vfsExplorerOpen} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
{vfsLoading ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', py: 3, gap: 1.5 }}>
|
||||
<CircularProgress size={20} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
VFS-Archiv wird gelesen...
|
||||
</Typography>
|
||||
</Box>
|
||||
) : !vfsFiles || vfsFiles.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 2, textAlign: 'center' }}>
|
||||
Keine VFS-Dateien in dieser Sicherung für das aktuelle Unternehmen gefunden.
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={1.5}>
|
||||
{vfsFiles.length > 3 && (
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Dateien filtern..."
|
||||
value={vfsFilter}
|
||||
onChange={(e) => this.setState({ vfsFilter: e.target.value })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" sx={{ color: DARK_1 }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ maxWidth: 320 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TableContainer sx={{ maxHeight: 280, border: `1px solid ${BORDER}`, borderRadius: 1 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Dateipfad im VFS</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Größe</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Typ</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Änderungsdatum</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Aktion</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredFiles.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
|
||||
Keine Dateien entsprechen dem Suchbegriff.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredFiles.map((f) => {
|
||||
const isRecovering = recoveringPath === f.name;
|
||||
|
||||
return (
|
||||
<TableRow key={f.name} hover>
|
||||
<TableCell sx={{ py: 0.75 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<InsertDriveFileOutlinedIcon fontSize="small" sx={{ color: DARK_1 }} />
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontSize: '0.8rem', wordBreak: 'break-all' }}>
|
||||
{f.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.75, whiteSpace: 'nowrap' }}>
|
||||
{formatBytes(f.size)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.75, color: 'text.secondary', fontSize: '0.75rem' }}>
|
||||
{f.mime || '—'}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.75, color: 'text.secondary', fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
|
||||
{formatDate(f.mtime)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 0.75 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={isRecovering ? <CircularProgress size={12} color="inherit" /> : <SettingsBackupRestoreIcon fontSize="small" />}
|
||||
onClick={(e) => this.handleRecoverFile(e, f.name)}
|
||||
disabled={Boolean(recoveringPath)}
|
||||
sx={{ textTransform: 'none', py: 0.25, px: 1, minHeight: 26, fontSize: '0.75rem' }}
|
||||
>
|
||||
{isRecovering ? 'Wiederherstellen...' : 'Wiederherstellen'}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user