mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 23:12:16 +00:00
feat(frontend): Add ability to list backups and create a backup on demand.
This commit is contained in:
committed by
Elisiário Couto
parent
222bb2ec64
commit
473f126d3e
@@ -73,11 +73,16 @@ export default function S3BackupConfigDrawer({
|
||||
service: "s3",
|
||||
config: config,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
console.log("S3 connection test successful");
|
||||
toast.success(
|
||||
"S3 connection test successful! Your configuration is working correctly.",
|
||||
);
|
||||
onSuccess: (response) => {
|
||||
if (response.success) {
|
||||
console.log("S3 connection test successful");
|
||||
toast.success(
|
||||
"S3 connection test successful! Your configuration is working correctly.",
|
||||
);
|
||||
} else {
|
||||
console.error("S3 connection test failed:", response.message);
|
||||
toast.error(response.message || "S3 connection test failed. Please verify your credentials and settings.");
|
||||
}
|
||||
},
|
||||
onError: (error: Error & { response?: { data?: { detail?: string } } }) => {
|
||||
console.error("Failed to test S3 connection:", error);
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
User,
|
||||
Filter,
|
||||
Cloud,
|
||||
Archive,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { apiClient } from "../lib/api";
|
||||
import { formatCurrency, formatDate } from "../lib/utils";
|
||||
import {
|
||||
@@ -43,6 +46,7 @@ import type {
|
||||
NotificationSettings,
|
||||
NotificationService,
|
||||
BackupSettings,
|
||||
BackupInfo,
|
||||
} from "../types/api";
|
||||
|
||||
// Helper function to get status indicator color and styles
|
||||
@@ -83,6 +87,7 @@ export default function Settings() {
|
||||
const [editingAccountId, setEditingAccountId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
|
||||
const [showBackups, setShowBackups] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -139,6 +144,17 @@ export default function Settings() {
|
||||
queryFn: apiClient.getBackupSettings,
|
||||
});
|
||||
|
||||
const {
|
||||
data: backups,
|
||||
isLoading: backupsLoading,
|
||||
error: backupsError,
|
||||
refetch: refetchBackups,
|
||||
} = useQuery<BackupInfo[]>({
|
||||
queryKey: ["backups"],
|
||||
queryFn: apiClient.listBackups,
|
||||
enabled: showBackups,
|
||||
});
|
||||
|
||||
// Account mutations
|
||||
const updateAccountMutation = useMutation({
|
||||
mutationFn: ({ id, display_name }: { id: string; display_name: string }) =>
|
||||
@@ -172,6 +188,26 @@ export default function Settings() {
|
||||
},
|
||||
});
|
||||
|
||||
// Backup mutations
|
||||
const createBackupMutation = useMutation({
|
||||
mutationFn: () => apiClient.performBackupOperation({ operation: "backup" }),
|
||||
onSuccess: (response) => {
|
||||
if (response.success) {
|
||||
toast.success(response.message || "Backup created successfully!");
|
||||
queryClient.invalidateQueries({ queryKey: ["backups"] });
|
||||
} else {
|
||||
toast.error(response.message || "Failed to create backup.");
|
||||
}
|
||||
},
|
||||
onError: (error: Error & { response?: { data?: { detail?: string } } }) => {
|
||||
console.error("Failed to create backup:", error);
|
||||
const message =
|
||||
error?.response?.data?.detail ||
|
||||
"Failed to create backup. Please check your S3 configuration.";
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
// Account handlers
|
||||
const handleEditStart = (account: Account) => {
|
||||
setEditingAccountId(account.id);
|
||||
@@ -203,6 +239,23 @@ export default function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
// Backup handlers
|
||||
const handleCreateBackup = () => {
|
||||
if (!backupSettings?.s3?.enabled) {
|
||||
toast.error("S3 backup is not enabled. Please configure and enable S3 backup first.");
|
||||
return;
|
||||
}
|
||||
createBackupMutation.mutate();
|
||||
};
|
||||
|
||||
const handleViewBackups = () => {
|
||||
if (!backupSettings?.s3?.enabled) {
|
||||
toast.error("S3 backup is not enabled. Please configure and enable S3 backup first.");
|
||||
return;
|
||||
}
|
||||
setShowBackups(true);
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
accountsLoading || settingsLoading || servicesLoading || backupLoading;
|
||||
const hasError =
|
||||
@@ -836,25 +889,82 @@ export default function Settings() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// TODO: Implement manual backup trigger
|
||||
console.log("Manual backup triggered");
|
||||
}}
|
||||
onClick={handleCreateBackup}
|
||||
disabled={createBackupMutation.isPending}
|
||||
>
|
||||
Create Backup Now
|
||||
{createBackupMutation.isPending ? (
|
||||
<>
|
||||
<Archive className="h-4 w-4 mr-2 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Archive className="h-4 w-4 mr-2" />
|
||||
Create Backup Now
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// TODO: Implement backup list view
|
||||
console.log("View backups");
|
||||
}}
|
||||
onClick={handleViewBackups}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
View Backups
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup List Modal/View */}
|
||||
{showBackups && (
|
||||
<div className="mt-6 p-4 border rounded-lg bg-background">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h5 className="font-medium">Available Backups</h5>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setShowBackups(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{backupsLoading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading backups...</p>
|
||||
) : backupsError ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-destructive">Failed to load backups</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => refetchBackups()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
) : !backups || backups.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No backups found</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{backups.map((backup, index) => (
|
||||
<div
|
||||
key={backup.key || index}
|
||||
className="flex items-center justify-between p-3 border rounded bg-muted/50"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{backup.key}</p>
|
||||
<div className="flex items-center space-x-4 text-xs text-muted-foreground mt-1">
|
||||
<span>Modified: {formatDate(backup.last_modified)}</span>
|
||||
<span>Size: {(backup.size / 1024 / 1024).toFixed(2)} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -296,8 +296,9 @@ export const apiClient = {
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
testBackupConnection: async (test: BackupTest): Promise<void> => {
|
||||
await api.post("/backup/test", test);
|
||||
testBackupConnection: async (test: BackupTest): Promise<ApiResponse<{ connected?: boolean }>> => {
|
||||
const response = await api.post<ApiResponse<{ connected?: boolean }>>("/backup/test", test);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
listBackups: async (): Promise<BackupInfo[]> => {
|
||||
@@ -305,8 +306,9 @@ export const apiClient = {
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
performBackupOperation: async (operation: BackupOperation): Promise<void> => {
|
||||
await api.post("/backup/operation", operation);
|
||||
performBackupOperation: async (operation: BackupOperation): Promise<ApiResponse<{ operation: string; completed: boolean }>> => {
|
||||
const response = await api.post<ApiResponse<{ operation: string; completed: boolean }>>("/backup/operation", operation);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,170 +8,170 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as TransactionsRouteImport } from "./routes/transactions";
|
||||
import { Route as SystemRouteImport } from "./routes/system";
|
||||
import { Route as SettingsRouteImport } from "./routes/settings";
|
||||
import { Route as NotificationsRouteImport } from "./routes/notifications";
|
||||
import { Route as BankConnectedRouteImport } from "./routes/bank-connected";
|
||||
import { Route as AnalyticsRouteImport } from "./routes/analytics";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as TransactionsRouteImport } from './routes/transactions'
|
||||
import { Route as SystemRouteImport } from './routes/system'
|
||||
import { Route as SettingsRouteImport } from './routes/settings'
|
||||
import { Route as NotificationsRouteImport } from './routes/notifications'
|
||||
import { Route as BankConnectedRouteImport } from './routes/bank-connected'
|
||||
import { Route as AnalyticsRouteImport } from './routes/analytics'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const TransactionsRoute = TransactionsRouteImport.update({
|
||||
id: "/transactions",
|
||||
path: "/transactions",
|
||||
id: '/transactions',
|
||||
path: '/transactions',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const SystemRoute = SystemRouteImport.update({
|
||||
id: "/system",
|
||||
path: "/system",
|
||||
id: '/system',
|
||||
path: '/system',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const SettingsRoute = SettingsRouteImport.update({
|
||||
id: "/settings",
|
||||
path: "/settings",
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const NotificationsRoute = NotificationsRouteImport.update({
|
||||
id: "/notifications",
|
||||
path: "/notifications",
|
||||
id: '/notifications',
|
||||
path: '/notifications',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const BankConnectedRoute = BankConnectedRouteImport.update({
|
||||
id: "/bank-connected",
|
||||
path: "/bank-connected",
|
||||
id: '/bank-connected',
|
||||
path: '/bank-connected',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const AnalyticsRoute = AnalyticsRouteImport.update({
|
||||
id: "/analytics",
|
||||
path: "/analytics",
|
||||
id: '/analytics',
|
||||
path: '/analytics',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/analytics": typeof AnalyticsRoute;
|
||||
"/bank-connected": typeof BankConnectedRoute;
|
||||
"/notifications": typeof NotificationsRoute;
|
||||
"/settings": typeof SettingsRoute;
|
||||
"/system": typeof SystemRoute;
|
||||
"/transactions": typeof TransactionsRoute;
|
||||
'/': typeof IndexRoute
|
||||
'/analytics': typeof AnalyticsRoute
|
||||
'/bank-connected': typeof BankConnectedRoute
|
||||
'/notifications': typeof NotificationsRoute
|
||||
'/settings': typeof SettingsRoute
|
||||
'/system': typeof SystemRoute
|
||||
'/transactions': typeof TransactionsRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/analytics": typeof AnalyticsRoute;
|
||||
"/bank-connected": typeof BankConnectedRoute;
|
||||
"/notifications": typeof NotificationsRoute;
|
||||
"/settings": typeof SettingsRoute;
|
||||
"/system": typeof SystemRoute;
|
||||
"/transactions": typeof TransactionsRoute;
|
||||
'/': typeof IndexRoute
|
||||
'/analytics': typeof AnalyticsRoute
|
||||
'/bank-connected': typeof BankConnectedRoute
|
||||
'/notifications': typeof NotificationsRoute
|
||||
'/settings': typeof SettingsRoute
|
||||
'/system': typeof SystemRoute
|
||||
'/transactions': typeof TransactionsRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/analytics": typeof AnalyticsRoute;
|
||||
"/bank-connected": typeof BankConnectedRoute;
|
||||
"/notifications": typeof NotificationsRoute;
|
||||
"/settings": typeof SettingsRoute;
|
||||
"/system": typeof SystemRoute;
|
||||
"/transactions": typeof TransactionsRoute;
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/analytics': typeof AnalyticsRoute
|
||||
'/bank-connected': typeof BankConnectedRoute
|
||||
'/notifications': typeof NotificationsRoute
|
||||
'/settings': typeof SettingsRoute
|
||||
'/system': typeof SystemRoute
|
||||
'/transactions': typeof TransactionsRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| "/"
|
||||
| "/analytics"
|
||||
| "/bank-connected"
|
||||
| "/notifications"
|
||||
| "/settings"
|
||||
| "/system"
|
||||
| "/transactions";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
| '/'
|
||||
| '/analytics'
|
||||
| '/bank-connected'
|
||||
| '/notifications'
|
||||
| '/settings'
|
||||
| '/system'
|
||||
| '/transactions'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| "/"
|
||||
| "/analytics"
|
||||
| "/bank-connected"
|
||||
| "/notifications"
|
||||
| "/settings"
|
||||
| "/system"
|
||||
| "/transactions";
|
||||
| '/'
|
||||
| '/analytics'
|
||||
| '/bank-connected'
|
||||
| '/notifications'
|
||||
| '/settings'
|
||||
| '/system'
|
||||
| '/transactions'
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/analytics"
|
||||
| "/bank-connected"
|
||||
| "/notifications"
|
||||
| "/settings"
|
||||
| "/system"
|
||||
| "/transactions";
|
||||
fileRoutesById: FileRoutesById;
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/analytics'
|
||||
| '/bank-connected'
|
||||
| '/notifications'
|
||||
| '/settings'
|
||||
| '/system'
|
||||
| '/transactions'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
AnalyticsRoute: typeof AnalyticsRoute;
|
||||
BankConnectedRoute: typeof BankConnectedRoute;
|
||||
NotificationsRoute: typeof NotificationsRoute;
|
||||
SettingsRoute: typeof SettingsRoute;
|
||||
SystemRoute: typeof SystemRoute;
|
||||
TransactionsRoute: typeof TransactionsRoute;
|
||||
IndexRoute: typeof IndexRoute
|
||||
AnalyticsRoute: typeof AnalyticsRoute
|
||||
BankConnectedRoute: typeof BankConnectedRoute
|
||||
NotificationsRoute: typeof NotificationsRoute
|
||||
SettingsRoute: typeof SettingsRoute
|
||||
SystemRoute: typeof SystemRoute
|
||||
TransactionsRoute: typeof TransactionsRoute
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
"/transactions": {
|
||||
id: "/transactions";
|
||||
path: "/transactions";
|
||||
fullPath: "/transactions";
|
||||
preLoaderRoute: typeof TransactionsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/system": {
|
||||
id: "/system";
|
||||
path: "/system";
|
||||
fullPath: "/system";
|
||||
preLoaderRoute: typeof SystemRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/settings": {
|
||||
id: "/settings";
|
||||
path: "/settings";
|
||||
fullPath: "/settings";
|
||||
preLoaderRoute: typeof SettingsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/notifications": {
|
||||
id: "/notifications";
|
||||
path: "/notifications";
|
||||
fullPath: "/notifications";
|
||||
preLoaderRoute: typeof NotificationsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/bank-connected": {
|
||||
id: "/bank-connected";
|
||||
path: "/bank-connected";
|
||||
fullPath: "/bank-connected";
|
||||
preLoaderRoute: typeof BankConnectedRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/analytics": {
|
||||
id: "/analytics";
|
||||
path: "/analytics";
|
||||
fullPath: "/analytics";
|
||||
preLoaderRoute: typeof AnalyticsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
'/transactions': {
|
||||
id: '/transactions'
|
||||
path: '/transactions'
|
||||
fullPath: '/transactions'
|
||||
preLoaderRoute: typeof TransactionsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/system': {
|
||||
id: '/system'
|
||||
path: '/system'
|
||||
fullPath: '/system'
|
||||
preLoaderRoute: typeof SystemRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/settings': {
|
||||
id: '/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/settings'
|
||||
preLoaderRoute: typeof SettingsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/notifications': {
|
||||
id: '/notifications'
|
||||
path: '/notifications'
|
||||
fullPath: '/notifications'
|
||||
preLoaderRoute: typeof NotificationsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/bank-connected': {
|
||||
id: '/bank-connected'
|
||||
path: '/bank-connected'
|
||||
fullPath: '/bank-connected'
|
||||
preLoaderRoute: typeof BankConnectedRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/analytics': {
|
||||
id: '/analytics'
|
||||
path: '/analytics'
|
||||
fullPath: '/analytics'
|
||||
preLoaderRoute: typeof AnalyticsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
SettingsRoute: SettingsRoute,
|
||||
SystemRoute: SystemRoute,
|
||||
TransactionsRoute: TransactionsRoute,
|
||||
};
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
Reference in New Issue
Block a user