diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 139e8b0..02ff4f4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,9 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Layout } from './components/Layout'; +import { Dashboard } from './pages/Dashboard'; +import { Backups } from './pages/Backups'; +import { Settings } from './pages/Settings'; const queryClient = new QueryClient({ defaultOptions: { @@ -11,42 +14,15 @@ const queryClient = new QueryClient({ }, }); -function DashboardPage() { - return ( -
-

Dashboard

-

Welcome to the Backup Tool dashboard.

-
- ); -} - -function BackupsPage() { - return ( -
-

Backups

-

Manage your backup jobs here.

-
- ); -} - -function SettingsPage() { - return ( -
-

Settings

-

Configure your backup sources and preferences.

-
- ); -} - function App() { return ( - } /> - } /> - } /> + } /> + } /> + } /> diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index c4ceb7e..3038927 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -51,6 +51,7 @@ export const jobsApi = { getById: (id: string) => apiClient.get(`/jobs/${id}`), create: (sourceId: string) => apiClient.post('/jobs', { source_id: sourceId }), + delete: (id: string) => apiClient.delete(`/jobs/${id}`), getLogs: (id: string) => apiClient.get(`/jobs/${id}/logs`), }; diff --git a/frontend/src/pages/Backups.tsx b/frontend/src/pages/Backups.tsx new file mode 100644 index 0000000..f19a298 --- /dev/null +++ b/frontend/src/pages/Backups.tsx @@ -0,0 +1,238 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Play, Trash2, Plus } from 'lucide-react'; +import { jobsApi, sourcesApi } from '../api/client'; +import type { BackupJob, BackupSource } from '../api/client'; + +function StatusBadge({ status }: { status: string }) { + const styles = { + pending: 'bg-yellow-100 text-yellow-800', + running: 'bg-blue-100 text-blue-800', + completed: 'bg-green-100 text-green-800', + failed: 'bg-red-100 text-red-800', + }; + return ( + + {status} + + ); +} + +function CreateJobModal({ + onClose, +}: { + onClose: () => void; +}) { + const queryClient = useQueryClient(); + const [sourceId, setSourceId] = useState(''); + + const { data: sources } = useQuery({ + queryKey: ['sources'], + queryFn: () => sourcesApi.getAll().then((res) => res.data), + }); + + const createMutation = useMutation({ + mutationFn: (sid: string) => jobsApi.create(sid), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + onClose(); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (sourceId) { + createMutation.mutate(sourceId); + } + }; + + return ( +
+
+
+

+ Create Backup Job +

+
+
+
+ + +
+
+ + +
+
+
+
+ ); +} + +export function Backups() { + const [showModal, setShowModal] = useState(false); + const queryClient = useQueryClient(); + + const { data: jobs, isLoading } = useQuery({ + queryKey: ['jobs'], + queryFn: () => jobsApi.getAll().then((res) => res.data), + }); + + const { data: sources } = useQuery({ + queryKey: ['sources'], + queryFn: () => sourcesApi.getAll().then((res) => res.data), + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => jobsApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + }, + }); + + const runMutation = useMutation({ + mutationFn: (id: string) => jobsApi.create(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + }, + }); + + const getSourceName = (sourceId: string) => { + const source = sources?.find((s: BackupSource) => s.id === sourceId); + return source?.name || sourceId.slice(0, 8); + }; + + return ( +
+
+

Backups

+ +
+ +
+ + + + + + + + + + + + {isLoading && ( + + + + )} + {jobs?.length === 0 && !isLoading && ( + + + + )} + {jobs?.map((job: BackupJob) => ( + + + + + + + + ))} + +
+ Job ID + + Source + + Status + + Started + + Actions +
+ Loading... +
+ No backup jobs yet. Create one to get started. +
+ {job.id.slice(0, 8)} + + {getSourceName(job.source_id)} + + + + {job.started_at + ? new Date(job.started_at).toLocaleString() + : 'Not started'} + +
+ + +
+
+
+ + {showModal && setShowModal(false)} />} +
+ ); +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..ffcd5de --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,141 @@ +import { useQuery } from '@tanstack/react-query'; +import { Activity, Archive, AlertTriangle, CheckCircle } from 'lucide-react'; +import { dashboardApi } from '../api/client'; + +function StatCard({ + title, + value, + icon: Icon, + color, +}: { + title: string; + value: number; + icon: React.ElementType; + color: string; +}) { + return ( +
+
+
+

{title}

+

{value}

+
+
+ +
+
+
+ ); +} + +function StatusBadge({ status }: { status: string }) { + const styles = { + pending: 'bg-yellow-100 text-yellow-800', + running: 'bg-blue-100 text-blue-800', + completed: 'bg-green-100 text-green-800', + failed: 'bg-red-100 text-red-800', + }; + return ( + + {status} + + ); +} + +export function Dashboard() { + const { data: stats } = useQuery({ + queryKey: ['dashboard-stats'], + queryFn: () => dashboardApi.getStats().then((res) => res.data), + }); + + const { data: recentJobs } = useQuery({ + queryKey: ['recent-jobs'], + queryFn: () => dashboardApi.getRecentJobs(5).then((res) => res.data), + }); + + const activeJobs = stats?.pending_jobs || 0; + const recentFailures = stats?.failed_jobs || 0; + + return ( +
+

Dashboard

+ +
+ + + + +
+ +
+
+

+ Recent Activity +

+
+
+ {recentJobs?.length === 0 && ( +
+ No recent activity +
+ )} + {recentJobs?.map((job) => ( +
+
+
+
+

+ Job {job.id.slice(0, 8)} +

+

+ {job.created_at + ? new Date(job.created_at).toLocaleString() + : 'Unknown'} +

+
+
+ +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..5076a7f --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,205 @@ +import { useState } from 'react'; + +const tabs = [ + { id: 'general', label: 'General' }, + { id: 'notifications', label: 'Notifications' }, + { id: 'security', label: 'Security' }, + { id: 'logs', label: 'Logs' }, +]; + +function GeneralSettings() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +} + +function NotificationSettings() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +} + +function SecuritySettings() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +} + +function LogSettings() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +} + +export function Settings() { + const [activeTab, setActiveTab] = useState('general'); + + const tabContent = { + general: , + notifications: , + security: , + logs: , + }; + + return ( +
+

Settings

+ +
+
+ +
+
{tabContent[activeTab as keyof typeof tabContent]}
+
+
+ ); +}