feat: database management - auto/USB backup toggles, folder browser, cron jobs
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Folder, FolderOpen, ChevronLeft, Loader2 } from "lucide-react";
|
||||
|
||||
interface BrowseResult {
|
||||
current: string;
|
||||
parent: string | null;
|
||||
dirs: { name: string; path: string }[];
|
||||
}
|
||||
|
||||
interface FolderBrowserModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (path: string) => void;
|
||||
}
|
||||
|
||||
export function FolderBrowserModal({ open, onClose, onSelect }: FolderBrowserModalProps) {
|
||||
const [browsePath, setBrowsePath] = useState("/");
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
|
||||
const { data, isLoading, isError } = useQuery<BrowseResult>({
|
||||
queryKey: ["/db/browse", browsePath],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest(
|
||||
"GET",
|
||||
`/api/database-management/browse?path=${encodeURIComponent(browsePath)}`
|
||||
);
|
||||
if (!res.ok) throw new Error((await res.json()).error);
|
||||
return res.json();
|
||||
},
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
setSelected(null);
|
||||
setBrowsePath(path);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selected) {
|
||||
onSelect(selected);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select Folder</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Current path breadcrumb */}
|
||||
<div className="text-xs text-gray-500 bg-gray-50 rounded px-3 py-2 font-mono break-all">
|
||||
{data?.current ?? browsePath}
|
||||
</div>
|
||||
|
||||
{/* Back button */}
|
||||
{data?.parent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start text-gray-600"
|
||||
onClick={() => handleNavigate(data.parent!)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Directory list */}
|
||||
<div className="border rounded-md overflow-y-auto max-h-64">
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-8 text-gray-400">
|
||||
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<p className="text-sm text-red-500 p-4">Cannot read this directory.</p>
|
||||
)}
|
||||
{!isLoading && !isError && data?.dirs.length === 0 && (
|
||||
<p className="text-sm text-gray-400 p-4">No sub-folders here.</p>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!isError &&
|
||||
data?.dirs.map((dir) => (
|
||||
<button
|
||||
key={dir.path}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 transition-colors ${
|
||||
selected === dir.path ? "bg-blue-50 text-blue-700 font-medium" : "text-gray-700"
|
||||
}`}
|
||||
onClick={() => setSelected(dir.path)}
|
||||
onDoubleClick={() => handleNavigate(dir.path)}
|
||||
>
|
||||
{selected === dir.path ? (
|
||||
<FolderOpen className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
) : (
|
||||
<Folder className="h-4 w-4 shrink-0 text-yellow-500" />
|
||||
)}
|
||||
{dir.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400">
|
||||
Single-click to select · Double-click to open
|
||||
</p>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={!selected}>
|
||||
Select Folder
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user