439 lines
14 KiB
TypeScript
439 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Database, Trash2, RotateCcw, Save, Clock } from "lucide-react";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import { fr } from "date-fns/locale/fr";
|
|
import { toast } from "sonner";
|
|
|
|
interface Backup {
|
|
id: string;
|
|
filename: string;
|
|
size: number;
|
|
createdAt: Date;
|
|
}
|
|
|
|
interface BackupSettings {
|
|
enabled: boolean;
|
|
frequency: "hourly" | "daily" | "weekly" | "monthly";
|
|
lastBackup?: string;
|
|
nextBackup?: string;
|
|
}
|
|
|
|
export function BackupCard() {
|
|
const [backups, setBackups] = useState<Backup[]>([]);
|
|
const [settings, setSettings] = useState<BackupSettings>({
|
|
enabled: true,
|
|
frequency: "hourly",
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
const [creating, setCreating] = useState(false);
|
|
const [restoring, setRestoring] = useState<string | null>(null);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
const [backupsRes, settingsRes] = await Promise.all([
|
|
fetch("/api/backups"),
|
|
fetch("/api/backups/settings"),
|
|
]);
|
|
|
|
const backupsData = await backupsRes.json();
|
|
const settingsData = await settingsRes.json();
|
|
|
|
if (backupsData.success) {
|
|
setBackups(
|
|
backupsData.data.map(
|
|
(b: {
|
|
id: string;
|
|
filename: string;
|
|
size: number;
|
|
createdAt: string;
|
|
}) => ({
|
|
...b,
|
|
createdAt: new Date(b.createdAt),
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (settingsData.success) {
|
|
setSettings(settingsData.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading backup data:", error);
|
|
toast.error("Erreur lors du chargement des données");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const handleCreateBackup = async () => {
|
|
setCreating(true);
|
|
try {
|
|
const response = await fetch("/api/backups", {
|
|
method: "POST",
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
if (data.data.skipped) {
|
|
toast.info(
|
|
"Aucun changement détecté. La dernière sauvegarde a été mise à jour.",
|
|
);
|
|
} else {
|
|
toast.success("Sauvegarde créée avec succès");
|
|
}
|
|
await loadData();
|
|
} else {
|
|
toast.error(data.error || "Erreur lors de la création");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error creating backup:", error);
|
|
toast.error("Erreur lors de la création de la sauvegarde");
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteBackup = async (id: string) => {
|
|
try {
|
|
const response = await fetch(`/api/backups/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
toast.success("Sauvegarde supprimée");
|
|
await loadData();
|
|
} else {
|
|
toast.error(data.error || "Erreur lors de la suppression");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting backup:", error);
|
|
toast.error("Erreur lors de la suppression");
|
|
}
|
|
};
|
|
|
|
const handleRestoreBackup = async (id: string) => {
|
|
setRestoring(id);
|
|
try {
|
|
const response = await fetch(`/api/backups/${id}/restore`, {
|
|
method: "POST",
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
toast.success(
|
|
"Sauvegarde restaurée avec succès. Rechargement de la page...",
|
|
);
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 2000);
|
|
} else {
|
|
toast.error(data.error || "Erreur lors de la restauration");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error restoring backup:", error);
|
|
toast.error("Erreur lors de la restauration");
|
|
} finally {
|
|
setRestoring(null);
|
|
}
|
|
};
|
|
|
|
const handleSettingsChange = async (updates: Partial<BackupSettings>) => {
|
|
const newSettings = { ...settings, ...updates };
|
|
setSettings(newSettings);
|
|
|
|
try {
|
|
const response = await fetch("/api/backups/settings", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(updates),
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
setSettings(data.data);
|
|
toast.success("Paramètres mis à jour");
|
|
} else {
|
|
toast.error("Erreur lors de la mise à jour");
|
|
await loadData(); // Revert on error
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating settings:", error);
|
|
toast.error("Erreur lors de la mise à jour");
|
|
await loadData(); // Revert on error
|
|
}
|
|
};
|
|
|
|
const formatFileSize = (bytes: number): string => {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="w-5 h-5" />
|
|
Sauvegardes automatiques
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">Chargement...</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="w-5 h-5" />
|
|
Sauvegardes automatiques
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Configurez les sauvegardes périodiques de votre base de données
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Settings */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="backup-enabled">Sauvegardes automatiques</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activez les sauvegardes périodiques
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="backup-enabled"
|
|
checked={settings.enabled}
|
|
onCheckedChange={(checked) =>
|
|
handleSettingsChange({ enabled: checked })
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{settings.enabled && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="backup-frequency">Fréquence</Label>
|
|
<Select
|
|
value={settings.frequency}
|
|
onValueChange={(
|
|
value: "hourly" | "daily" | "weekly" | "monthly",
|
|
) => handleSettingsChange({ frequency: value })}
|
|
>
|
|
<SelectTrigger id="backup-frequency">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="hourly">Horaire</SelectItem>
|
|
<SelectItem value="daily">Quotidienne</SelectItem>
|
|
<SelectItem value="weekly">Hebdomadaire</SelectItem>
|
|
<SelectItem value="monthly">Mensuelle</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
{settings.enabled && settings.lastBackup && (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Clock className="w-4 h-4" />
|
|
<span>
|
|
Dernière sauvegarde:{" "}
|
|
{formatDistanceToNow(new Date(settings.lastBackup), {
|
|
addSuffix: true,
|
|
locale: fr,
|
|
})}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{settings.enabled && settings.nextBackup && (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Clock className="w-4 h-4" />
|
|
<span>
|
|
Prochaine sauvegarde:{" "}
|
|
{formatDistanceToNow(new Date(settings.nextBackup), {
|
|
addSuffix: true,
|
|
locale: fr,
|
|
})}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Manual backup */}
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={handleCreateBackup}
|
|
disabled={creating}
|
|
variant="outline"
|
|
className="flex-1"
|
|
>
|
|
<Save className="w-4 h-4 mr-2" />
|
|
{creating ? "Création..." : "Créer une sauvegarde"}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Backups list */}
|
|
{backups.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label>Sauvegardes ({backups.length}/10)</Label>
|
|
<div className="border rounded-lg">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Date</TableHead>
|
|
<TableHead>Taille</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{backups.map((backup) => (
|
|
<TableRow key={backup.id}>
|
|
<TableCell>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">
|
|
{backup.createdAt.toLocaleDateString("fr-FR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatDistanceToNow(backup.createdAt, {
|
|
addSuffix: true,
|
|
locale: fr,
|
|
})}
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{formatFileSize(backup.size)}</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={restoring === backup.id}
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Restaurer cette sauvegarde ?
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Cette action va remplacer votre base de
|
|
données actuelle par cette sauvegarde. Une
|
|
sauvegarde de sécurité sera créée avant la
|
|
restauration.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleRestoreBackup(backup.id)}
|
|
>
|
|
Restaurer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Supprimer cette sauvegarde ?
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Cette action est irréversible. La sauvegarde
|
|
sera définitivement supprimée.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleDeleteBackup(backup.id)}
|
|
>
|
|
Supprimer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{backups.length === 0 && (
|
|
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
Aucune sauvegarde pour le moment
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|