Files
Infra-Management/resources/js/Pages/Backup/Index.jsx
I-Fun c649f055d8 feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js
- Added main application entry point (app.jsx) and bootstrap file for React setup.
- Created Wiki index page (Index.jsx) for documentation and file management.
- Implemented utility functions for formatting and status handling in utils.js.
- Set up Blade template (app.blade.php) for Inertia.js integration.
- Defined API routes for metrics and terminal access in api.php.
- Established console commands for scheduled tasks in console.php.
- Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php.
- Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments.
- Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js.
- Set up Vite for asset bundling and development server in vite.config.js.
2026-04-07 20:29:32 +07:00

115 lines
6.2 KiB
JavaScript

import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { HardDrive, Plus, Download, RotateCcw, Calendar, CheckCircle, XCircle, Clock } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { formatBytes, timeAgo } from '@/lib/utils';
export default function BackupIndex({ backups, policies, stats }) {
const backupColumns = [
{ header: 'Target', accessor: 'target_name', render: row => (
<div>
<p className="text-sm text-white font-medium">{row.target_name}</p>
<p className="text-xs text-white/30 capitalize">{row.target_type}</p>
</div>
)},
{ header: 'Size', accessor: 'size_bytes', render: row => (
<span className="text-sm text-white/70 font-mono">{row.size_bytes ? formatBytes(row.size_bytes) : '—'}</span>
)},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: 'Created', accessor: 'created_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.created_at)}</span>
)},
{ header: 'Expires', accessor: 'expires_at', render: row => (
row.expires_at
? <span className={`text-xs ${new Date(row.expires_at) < new Date() ? 'text-red-400' : 'text-white/40'}`}>{timeAgo(row.expires_at)}</span>
: <span className="text-xs text-white/20">Never</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex gap-1" onClick={e => e.stopPropagation()}>
{row.status === 'success' && (
<>
<Button size="sm" variant="ghost" icon={Download} title="Download" />
<Button size="sm" variant="ghost" icon={RotateCcw} title="Restore"
onClick={() => confirm('Restore from this backup?') && router.post(`/backups/${row.id}/restore`)} />
</>
)}
<Button size="sm" variant="ghost" icon={XCircle} className="hover:text-red-400"
onClick={() => confirm('Delete this backup?') && router.delete(`/backups/${row.id}`)} />
</div>
)},
];
const policyColumns = [
{ header: 'Policy', accessor: 'name', render: row => (
<div>
<p className="text-sm text-white font-medium">{row.name}</p>
<p className="text-xs text-white/30 capitalize">{row.target_type} · {row.schedule}</p>
</div>
)},
{ header: 'Retention', accessor: 'retention_value', render: row => (
<span className="text-sm text-white/70">{row.retention_value} {row.retention_type === 'count' ? 'backups' : 'days'}</span>
)},
{ header: 'Storage', accessor: 'storage_type', render: row => (
<span className="text-xs px-2 py-0.5 rounded bg-white/5 text-white/60 uppercase">{row.storage_type}</span>
)},
{ header: 'Status', accessor: 'is_active', render: row => (
<StatusBadge status={row.is_active ? 'active' : 'stopped'} />
)},
];
return (
<AppLayout title="Backup & DR">
<Head title="Backup & DR" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Backup & Disaster Recovery</h2>
<p className="text-sm text-white/40">Manage backups, schedules, and restore points</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" icon={Calendar}>Schedules</Button>
<Button icon={Plus}>Create Policy</Button>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Total Backups" value={stats?.total ?? 0} icon={HardDrive} color="cyan" />
<StatCard title="Successful" value={stats?.success ?? 0} icon={CheckCircle} color="green" />
<StatCard title="Failed" value={stats?.failed ?? 0} icon={XCircle} color="red" />
<StatCard title="Storage Used" value={stats?.total_size ?? '0 B'} icon={HardDrive} color="purple" />
</div>
{/* DR Status */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{[
{ label: 'RTO', value: '< 4h', desc: 'Recovery Time Objective', color: 'text-green-400' },
{ label: 'RPO', value: '< 1h', desc: 'Recovery Point Objective', color: 'text-cyan-400' },
{ label: 'Last DR Test', value: 'N/A', desc: 'Last tested', color: 'text-white/40' },
].map(d => (
<GlassCard key={d.label} className="p-5 text-center">
<p className="text-xs text-white/40 uppercase tracking-wider mb-1">{d.label}</p>
<p className={`text-3xl font-bold ${d.color}`}>{d.value}</p>
<p className="text-xs text-white/30 mt-1">{d.desc}</p>
</GlassCard>
))}
</div>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-4">Backup Policies</h3>
<DataTable columns={policyColumns} data={policies ?? []}
emptyMessage="No backup policies. Create one to start protecting your infrastructure." />
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-4">Backup History</h3>
<DataTable columns={backupColumns} data={backups?.data ?? []}
emptyMessage="No backups found." />
</GlassCard>
</div>
</AppLayout>
);
}