Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useRef, useState } from "react"; | |
| import { toast, Toaster } from "sonner"; | |
| import { useTranslation } from "react-i18next"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogHeader, | |
| DialogTitle, | |
| DialogDescription, | |
| } from "@/components/ui/dialog"; | |
| import { DatabaseIcon, DownloadIcon, UploadIcon, Loader2 } from "lucide-react"; | |
| import { Card, CardContent } from "@/components/ui/card"; | |
| import { cn } from "@/lib/utils"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| interface DatabaseBackupProps { | |
| open: boolean; | |
| onClose: () => void; | |
| token?: string; | |
| } | |
| export default function DatabaseBackup({ | |
| open, | |
| onClose, | |
| token, | |
| }: DatabaseBackupProps) { | |
| const { t } = useTranslation("common"); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const [isExporting, setIsExporting] = useState(false); | |
| const [isImporting, setIsImporting] = useState(false); | |
| const handleExport = async () => { | |
| if (!token) { | |
| toast.error(t("auth.unauthorized")); | |
| return; | |
| } | |
| setIsExporting(true); | |
| try { | |
| const response = await fetch("/api/v1/panel/database/export", { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(t("backup.export.error")); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = `openwebui_monitor_backup_${ | |
| new Date().toISOString().split("T")[0] | |
| }.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| toast.success(t("backup.export.success")); | |
| } catch (error) { | |
| console.error(t("backup.export.error"), error); | |
| toast.error(t("backup.export.error")); | |
| } finally { | |
| setIsExporting(false); | |
| } | |
| }; | |
| const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0]; | |
| if (!file) return; | |
| if (!token) { | |
| toast.error(t("auth.unauthorized")); | |
| return; | |
| } | |
| setIsImporting(true); | |
| try { | |
| const content = await file.text(); | |
| const data = JSON.parse(content); | |
| const response = await fetch("/api/v1/panel/database/import", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| body: JSON.stringify(data), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(t("backup.import.error")); | |
| } | |
| const result = await response.json(); | |
| if (result.success) { | |
| toast.success(t("backup.import.success")); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ""; | |
| } | |
| } else { | |
| throw new Error(result.error || t("backup.import.error")); | |
| } | |
| } catch (err) { | |
| console.error(t("backup.import.error"), err); | |
| toast.error( | |
| err instanceof Error ? err.message : t("backup.import.error") | |
| ); | |
| } finally { | |
| setIsImporting(false); | |
| } | |
| }; | |
| return ( | |
| <AnimatePresence> | |
| <Toaster | |
| richColors | |
| position="top-center" | |
| theme="light" | |
| expand | |
| duration={1500} | |
| /> | |
| {open && ( | |
| <Dialog open={open} onOpenChange={onClose}> | |
| <DialogContent className="w-[calc(100%-2rem)] !max-w-[400px] rounded-lg backdrop-blur-lg bg-white/90 border border-white/20 shadow-xl md:px-6"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: 20 }} | |
| > | |
| <DialogHeader> | |
| <div className="flex items-center gap-2"> | |
| <motion.div | |
| initial={{ scale: 0.8, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| transition={{ delay: 0.1 }} | |
| className="flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-full bg-primary/10" | |
| > | |
| <DatabaseIcon className="h-4 w-4 sm:h-5 sm:w-5 text-primary" /> | |
| </motion.div> | |
| <DialogTitle className="text-base sm:text-lg"> | |
| {t("backup.title")} | |
| </DialogTitle> | |
| </div> | |
| <DialogDescription className="pt-2 text-sm"> | |
| {t("backup.description")} | |
| </DialogDescription> | |
| </DialogHeader> | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.2 }} | |
| className="grid grid-cols-1 gap-3 sm:gap-4 py-3 sm:py-4" | |
| > | |
| <Card | |
| className={cn( | |
| "cursor-pointer transition-all duration-300 hover:bg-accent/5", | |
| "group relative overflow-hidden backdrop-blur-sm bg-white/50 border-white/20", | |
| "hover:shadow-lg hover:scale-[1.02] transform-gpu" | |
| )} | |
| onClick={handleExport} | |
| > | |
| <CardContent className="flex items-center gap-3 sm:gap-4 p-4 sm:p-6"> | |
| <div className="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors duration-300"> | |
| {isExporting ? ( | |
| <Loader2 className="h-5 w-5 sm:h-6 sm:w-6 text-primary animate-spin" /> | |
| ) : ( | |
| <DownloadIcon className="h-5 w-5 sm:h-6 sm:w-6 text-primary" /> | |
| )} | |
| </div> | |
| <div className="space-y-0.5 sm:space-y-1"> | |
| <h3 className="font-medium leading-none text-sm sm:text-base group-hover:text-primary transition-colors duration-300"> | |
| {t("backup.export.title")} | |
| </h3> | |
| <p className="text-xs sm:text-sm text-muted-foreground group-hover:text-primary/80 transition-colors duration-300"> | |
| {t("backup.export.description")} | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card | |
| className={cn( | |
| "cursor-pointer transition-all duration-300 hover:bg-accent/5", | |
| "group relative overflow-hidden backdrop-blur-sm bg-white/50 border-white/20", | |
| "hover:shadow-lg hover:scale-[1.02] transform-gpu" | |
| )} | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".json" | |
| onChange={handleImport} | |
| className="hidden" | |
| /> | |
| <CardContent className="flex items-center gap-3 sm:gap-4 p-4 sm:p-6"> | |
| <div className="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors duration-300"> | |
| {isImporting ? ( | |
| <Loader2 className="h-5 w-5 sm:h-6 sm:w-6 text-primary animate-spin" /> | |
| ) : ( | |
| <UploadIcon className="h-5 w-5 sm:h-6 sm:w-6 text-primary" /> | |
| )} | |
| </div> | |
| <div className="space-y-0.5 sm:space-y-1"> | |
| <h3 className="font-medium leading-none text-sm sm:text-base group-hover:text-primary transition-colors duration-300"> | |
| {t("backup.import.title")} | |
| </h3> | |
| <p className="text-xs sm:text-sm text-muted-foreground group-hover:text-primary/80 transition-colors duration-300"> | |
| {t("backup.import.description")} | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </motion.div> | |
| </motion.div> | |
| </DialogContent> | |
| </Dialog> | |
| )} | |
| </AnimatePresence> | |
| ); | |
| } | |