mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 20:42:39 +00:00
feat(frontend): Add S3 backup UI and complete backup functionality
Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
This commit is contained in:
committed by
Elisiário Couto
parent
7f2a4634c5
commit
0122913052
248
frontend/src/components/S3BackupConfigDrawer.tsx
Normal file
248
frontend/src/components/S3BackupConfigDrawer.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Cloud, TestTube } from "lucide-react";
|
||||
import { apiClient } from "../lib/api";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { EditButton } from "./ui/edit-button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "./ui/drawer";
|
||||
import type { BackupSettings, S3Config } from "../types/api";
|
||||
|
||||
interface S3BackupConfigDrawerProps {
|
||||
settings?: BackupSettings;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function S3BackupConfigDrawer({
|
||||
settings,
|
||||
trigger,
|
||||
}: S3BackupConfigDrawerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [config, setConfig] = useState<S3Config>({
|
||||
access_key_id: "",
|
||||
secret_access_key: "",
|
||||
bucket_name: "",
|
||||
region: "us-east-1",
|
||||
endpoint_url: "",
|
||||
path_style: false,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.s3) {
|
||||
setConfig({ ...settings.s3 });
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (s3Config: S3Config) =>
|
||||
apiClient.updateBackupSettings({
|
||||
s3: s3Config,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["backupSettings"] });
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update S3 backup configuration:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const testMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
apiClient.testBackupConnection({
|
||||
service: "s3",
|
||||
config: config,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
console.log("S3 connection test successful");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to test S3 connection:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
updateMutation.mutate(config);
|
||||
};
|
||||
|
||||
const handleTest = () => {
|
||||
testMutation.mutate();
|
||||
};
|
||||
|
||||
const isConfigValid =
|
||||
config.access_key_id.trim().length > 0 &&
|
||||
config.secret_access_key.trim().length > 0 &&
|
||||
config.bucket_name.trim().length > 0;
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
{trigger || <EditButton />}
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle className="flex items-center space-x-2">
|
||||
<Cloud className="h-5 w-5 text-primary" />
|
||||
<span>S3 Backup Configuration</span>
|
||||
</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Configure S3 settings for automatic database backups
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="px-4 space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enabled"
|
||||
checked={config.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setConfig({ ...config, enabled: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="enabled">Enable S3 backups</Label>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="access_key_id">Access Key ID</Label>
|
||||
<Input
|
||||
id="access_key_id"
|
||||
type="text"
|
||||
value={config.access_key_id}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, access_key_id: e.target.value })
|
||||
}
|
||||
placeholder="Your AWS Access Key ID"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secret_access_key">Secret Access Key</Label>
|
||||
<Input
|
||||
id="secret_access_key"
|
||||
type="password"
|
||||
value={config.secret_access_key}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, secret_access_key: e.target.value })
|
||||
}
|
||||
placeholder="Your AWS Secret Access Key"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bucket_name">Bucket Name</Label>
|
||||
<Input
|
||||
id="bucket_name"
|
||||
type="text"
|
||||
value={config.bucket_name}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, bucket_name: e.target.value })
|
||||
}
|
||||
placeholder="my-backup-bucket"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="region">Region</Label>
|
||||
<Input
|
||||
id="region"
|
||||
type="text"
|
||||
value={config.region}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, region: e.target.value })
|
||||
}
|
||||
placeholder="us-east-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endpoint_url">
|
||||
Custom Endpoint URL (Optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="endpoint_url"
|
||||
type="url"
|
||||
value={config.endpoint_url || ""}
|
||||
onChange={(e) =>
|
||||
setConfig({ ...config, endpoint_url: e.target.value })
|
||||
}
|
||||
placeholder="https://custom-s3-endpoint.com"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
For S3-compatible services like MinIO or DigitalOcean Spaces
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="path_style"
|
||||
checked={config.path_style}
|
||||
onCheckedChange={(checked) =>
|
||||
setConfig({ ...config, path_style: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="path_style">Use path-style addressing</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enable for older S3 implementations or certain S3-compatible services
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DrawerFooter className="px-0">
|
||||
<div className="flex space-x-2">
|
||||
<Button type="submit" disabled={updateMutation.isPending || !config.enabled}>
|
||||
{updateMutation.isPending ? "Saving..." : "Save Configuration"}
|
||||
</Button>
|
||||
{config.enabled && isConfigValid && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTest}
|
||||
disabled={testMutation.isPending}
|
||||
>
|
||||
{testMutation.isPending ? (
|
||||
<>
|
||||
<TestTube className="h-4 w-4 mr-2 animate-spin" />
|
||||
Testing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TestTube className="h-4 w-4 mr-2" />
|
||||
Test
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</form>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Trash2,
|
||||
User,
|
||||
Filter,
|
||||
Cloud,
|
||||
} from "lucide-react";
|
||||
import { apiClient } from "../lib/api";
|
||||
import { formatCurrency, formatDate } from "../lib/utils";
|
||||
@@ -35,11 +36,13 @@ import NotificationFiltersDrawer from "./NotificationFiltersDrawer";
|
||||
import DiscordConfigDrawer from "./DiscordConfigDrawer";
|
||||
import TelegramConfigDrawer from "./TelegramConfigDrawer";
|
||||
import AddBankAccountDrawer from "./AddBankAccountDrawer";
|
||||
import S3BackupConfigDrawer from "./S3BackupConfigDrawer";
|
||||
import type {
|
||||
Account,
|
||||
Balance,
|
||||
NotificationSettings,
|
||||
NotificationService,
|
||||
BackupSettings,
|
||||
} from "../types/api";
|
||||
|
||||
// Helper function to get status indicator color and styles
|
||||
@@ -125,6 +128,17 @@ export default function Settings() {
|
||||
queryFn: apiClient.getBankConnectionsStatus,
|
||||
});
|
||||
|
||||
// Backup queries
|
||||
const {
|
||||
data: backupSettings,
|
||||
isLoading: backupLoading,
|
||||
error: backupError,
|
||||
refetch: refetchBackup,
|
||||
} = useQuery<BackupSettings>({
|
||||
queryKey: ["backupSettings"],
|
||||
queryFn: apiClient.getBackupSettings,
|
||||
});
|
||||
|
||||
// Account mutations
|
||||
const updateAccountMutation = useMutation({
|
||||
mutationFn: ({ id, display_name }: { id: string; display_name: string }) =>
|
||||
@@ -189,8 +203,8 @@ export default function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = accountsLoading || settingsLoading || servicesLoading;
|
||||
const hasError = accountsError || settingsError || servicesError;
|
||||
const isLoading = accountsLoading || settingsLoading || servicesLoading || backupLoading;
|
||||
const hasError = accountsError || settingsError || servicesError || backupError;
|
||||
|
||||
if (isLoading) {
|
||||
return <AccountsSkeleton />;
|
||||
@@ -211,6 +225,7 @@ export default function Settings() {
|
||||
refetchAccounts();
|
||||
refetchSettings();
|
||||
refetchServices();
|
||||
refetchBackup();
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -226,7 +241,7 @@ export default function Settings() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultValue="accounts" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="accounts" className="flex items-center space-x-2">
|
||||
<User className="h-4 w-4" />
|
||||
<span>Accounts</span>
|
||||
@@ -238,6 +253,10 @@ export default function Settings() {
|
||||
<Bell className="h-4 w-4" />
|
||||
<span>Notifications</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="backup" className="flex items-center space-x-2">
|
||||
<Cloud className="h-4 w-4" />
|
||||
<span>Backup</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="accounts" className="space-y-6">
|
||||
@@ -728,6 +747,107 @@ export default function Settings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="backup" className="space-y-6">
|
||||
{/* S3 Backup Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Cloud className="h-5 w-5 text-primary" />
|
||||
<span>S3 Backup Configuration</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure automatic database backups to Amazon S3 or S3-compatible storage
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{!backupSettings?.s3 ? (
|
||||
<div className="text-center py-8">
|
||||
<Cloud className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
No S3 backup configured
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Set up S3 backup to automatically backup your database to the cloud.
|
||||
</p>
|
||||
<S3BackupConfigDrawer settings={backupSettings} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-muted rounded-full">
|
||||
<Cloud className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<h4 className="text-lg font-medium text-foreground">
|
||||
S3 Backup
|
||||
</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
backupSettings.s3.enabled
|
||||
? 'bg-green-500'
|
||||
: 'bg-muted-foreground'
|
||||
}`} />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{backupSettings.s3.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-medium">Bucket:</span> {backupSettings.s3.bucket_name}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-medium">Region:</span> {backupSettings.s3.region}
|
||||
</p>
|
||||
{backupSettings.s3.endpoint_url && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-medium">Endpoint:</span> {backupSettings.s3.endpoint_url}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<S3BackupConfigDrawer settings={backupSettings} />
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<h5 className="font-medium mb-2">Backup Information</h5>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Database backups are stored in the "leggen_backups/" folder in your S3 bucket.
|
||||
Backups include the complete SQLite database file.
|
||||
</p>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// TODO: Implement manual backup trigger
|
||||
console.log("Manual backup triggered");
|
||||
}}
|
||||
>
|
||||
Create Backup Now
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// TODO: Implement backup list view
|
||||
console.log("View backups");
|
||||
}}
|
||||
>
|
||||
View Backups
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,10 @@ import type {
|
||||
BankConnectionStatus,
|
||||
BankRequisition,
|
||||
Country,
|
||||
BackupSettings,
|
||||
BackupTest,
|
||||
BackupInfo,
|
||||
BackupOperation,
|
||||
} from "../types/api";
|
||||
|
||||
// Use VITE_API_URL for development, relative URLs for production
|
||||
@@ -274,6 +278,37 @@ export const apiClient = {
|
||||
const response = await api.get<ApiResponse<Country[]>>("/banks/countries");
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// Backup endpoints
|
||||
getBackupSettings: async (): Promise<BackupSettings> => {
|
||||
const response = await api.get<ApiResponse<BackupSettings>>(
|
||||
"/backup/settings",
|
||||
);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
updateBackupSettings: async (
|
||||
settings: BackupSettings,
|
||||
): Promise<BackupSettings> => {
|
||||
const response = await api.put<ApiResponse<BackupSettings>>(
|
||||
"/backup/settings",
|
||||
settings,
|
||||
);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
testBackupConnection: async (test: BackupTest): Promise<void> => {
|
||||
await api.post("/backup/test", test);
|
||||
},
|
||||
|
||||
listBackups: async (): Promise<BackupInfo[]> => {
|
||||
const response = await api.get<ApiResponse<BackupInfo[]>>("/backup/list");
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
performBackupOperation: async (operation: BackupOperation): Promise<void> => {
|
||||
await api.post("/backup/operation", operation);
|
||||
},
|
||||
};
|
||||
|
||||
export default apiClient;
|
||||
|
||||
@@ -277,3 +277,34 @@ export interface Country {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Backup types
|
||||
export interface S3Config {
|
||||
access_key_id: string;
|
||||
secret_access_key: string;
|
||||
bucket_name: string;
|
||||
region: string;
|
||||
endpoint_url?: string;
|
||||
path_style: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface BackupSettings {
|
||||
s3?: S3Config;
|
||||
}
|
||||
|
||||
export interface BackupTest {
|
||||
service: string;
|
||||
config: S3Config;
|
||||
}
|
||||
|
||||
export interface BackupInfo {
|
||||
key: string;
|
||||
last_modified: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface BackupOperation {
|
||||
operation: string;
|
||||
backup_key?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user