fix client UI and action
This commit is contained in:
@@ -46,6 +46,30 @@ export interface CreateJobRequest {
|
||||
pool_name?: string
|
||||
}
|
||||
|
||||
export interface BackupClient {
|
||||
client_id: number
|
||||
name: string
|
||||
uname?: string
|
||||
enabled: boolean
|
||||
auto_prune?: boolean
|
||||
file_retention?: number
|
||||
job_retention?: number
|
||||
last_backup_at?: string
|
||||
total_jobs?: number
|
||||
total_bytes?: number
|
||||
status?: 'online' | 'offline'
|
||||
}
|
||||
|
||||
export interface ListClientsResponse {
|
||||
clients: BackupClient[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface ListClientsParams {
|
||||
enabled?: boolean
|
||||
search?: string
|
||||
}
|
||||
|
||||
export const backupAPI = {
|
||||
listJobs: async (params?: ListJobsParams): Promise<ListJobsResponse> => {
|
||||
const queryParams = new URLSearchParams()
|
||||
@@ -76,5 +100,16 @@ export const backupAPI = {
|
||||
const response = await apiClient.post<{ output: string }>('/backup/console/execute', { command })
|
||||
return response.data
|
||||
},
|
||||
|
||||
listClients: async (params?: ListClientsParams): Promise<ListClientsResponse> => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.enabled !== undefined) queryParams.append('enabled', params.enabled.toString())
|
||||
if (params?.search) queryParams.append('search', params.search)
|
||||
|
||||
const response = await apiClient.get<ListClientsResponse>(
|
||||
`/backup/clients${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,108 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { backupAPI } from '@/api/backup'
|
||||
import { Search, X } from 'lucide-react'
|
||||
|
||||
// Styles for checkbox and tree lines
|
||||
const clientManagementStyles = `
|
||||
.checkbox-custom {
|
||||
appearance: none;
|
||||
background-color: #1c2936;
|
||||
border: 1px solid #324d67;
|
||||
border-radius: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-custom:checked {
|
||||
background-color: #137fec;
|
||||
border-color: #137fec;
|
||||
}
|
||||
.checkbox-custom:checked::after {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.agent-item {
|
||||
position: relative;
|
||||
}
|
||||
.agent-item .tree-line-vertical {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -24px;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: #324d67;
|
||||
z-index: 0;
|
||||
}
|
||||
.agent-item:last-child .tree-line-vertical {
|
||||
bottom: 50%;
|
||||
}
|
||||
.agent-item .tree-line-horizontal {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50%;
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background-color: #324d67;
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
|
||||
export default function BackupManagement() {
|
||||
const [activeTab, setActiveTab] = useState<'dashboard' | 'jobs' | 'clients' | 'storage' | 'restore' | 'console'>('dashboard')
|
||||
// Fetch recent jobs for dashboard
|
||||
const { data: dashboardJobsData } = useQuery({
|
||||
queryKey: ['dashboard-jobs'],
|
||||
queryFn: () => backupAPI.listJobs({ limit: 4 }),
|
||||
enabled: activeTab === 'dashboard',
|
||||
})
|
||||
|
||||
const recentJobs = dashboardJobsData?.jobs || []
|
||||
|
||||
// Helper functions for formatting
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
const formatDuration = (seconds?: number): string => {
|
||||
if (!seconds) return '-'
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusMap: Record<string, { bg: string; text: string; border: string; icon: string }> = {
|
||||
Running: { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/20', icon: 'pending_actions' },
|
||||
Completed: { bg: 'bg-green-500/10', text: 'text-green-400', border: 'border-green-500/20', icon: 'check_circle' },
|
||||
Failed: { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/20', icon: 'error' },
|
||||
Canceled: { bg: 'bg-yellow-500/10', text: 'text-yellow-400', border: 'border-yellow-500/20', icon: 'cancel' },
|
||||
Waiting: { bg: 'bg-gray-500/10', text: 'text-gray-400', border: 'border-gray-500/20', icon: 'schedule' },
|
||||
}
|
||||
const config = statusMap[status] || statusMap.Waiting
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium ${config.bg} ${config.text} border ${config.border}`}>
|
||||
{status === 'Running' && <span className="block h-1.5 w-1.5 rounded-full bg-blue-400 animate-pulse"></span>}
|
||||
{status !== 'Running' && <span className="material-symbols-outlined text-[14px]">{config.icon}</span>}
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full overflow-hidden relative bg-background-dark">
|
||||
@@ -206,90 +306,31 @@ export default function BackupManagement() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-dark text-sm">
|
||||
{/* Running Job */}
|
||||
<tr className="hover:bg-[#111a22]/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium bg-blue-500/10 text-blue-400 border border-blue-500/20">
|
||||
<span className="block h-1.5 w-1.5 rounded-full bg-blue-400 animate-pulse"></span>
|
||||
Running
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">10423</td>
|
||||
<td className="px-6 py-4 text-white font-medium">WeeklyArchive</td>
|
||||
<td className="px-6 py-4 text-text-secondary">filesrv-02</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Backup</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Full</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">00:45:12</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">142 GB</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-text-secondary hover:text-white p-1 rounded hover:bg-[#111a22] transition-colors">
|
||||
<span className="material-symbols-outlined text-[20px]">cancel</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Successful Job */}
|
||||
<tr className="hover:bg-[#111a22]/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20">
|
||||
<span className="material-symbols-outlined text-[14px]">check</span>
|
||||
OK
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">10422</td>
|
||||
<td className="px-6 py-4 text-white font-medium">DailyBackup</td>
|
||||
<td className="px-6 py-4 text-text-secondary">web-srv-01</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Backup</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Incr</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">00:12:05</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">4.2 GB</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-text-secondary hover:text-white p-1 rounded hover:bg-[#111a22] transition-colors">
|
||||
<span className="material-symbols-outlined text-[20px]">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Failed Job */}
|
||||
<tr className="hover:bg-[#111a22]/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium bg-red-500/10 text-red-400 border border-red-500/20">
|
||||
<span className="material-symbols-outlined text-[14px]">error</span>
|
||||
Error
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">10421</td>
|
||||
<td className="px-6 py-4 text-white font-medium">DB_Snapshot</td>
|
||||
<td className="px-6 py-4 text-text-secondary">db-prod-01</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Backup</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Diff</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">00:00:04</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">0 B</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-text-secondary hover:text-white p-1 rounded hover:bg-[#111a22] transition-colors">
|
||||
<span className="material-symbols-outlined text-[20px]">replay</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Another Success */}
|
||||
<tr className="hover:bg-[#111a22]/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20">
|
||||
<span className="material-symbols-outlined text-[14px]">check</span>
|
||||
OK
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">10420</td>
|
||||
<td className="px-6 py-4 text-white font-medium">CatalogBackup</td>
|
||||
<td className="px-6 py-4 text-text-secondary">backup-srv-01</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Backup</td>
|
||||
<td className="px-6 py-4 text-text-secondary">Full</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">00:05:30</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">850 MB</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-text-secondary hover:text-white p-1 rounded hover:bg-[#111a22] transition-colors">
|
||||
<span className="material-symbols-outlined text-[20px]">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{recentJobs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={9} className="px-6 py-8 text-center text-text-secondary">
|
||||
No recent jobs found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
recentJobs.map((job) => (
|
||||
<tr key={job.id} className="hover:bg-[#111a22]/50 transition-colors">
|
||||
<td className="px-6 py-4">{getStatusBadge(job.status)}</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">{job.job_id}</td>
|
||||
<td className="px-6 py-4 text-white font-medium">{job.job_name}</td>
|
||||
<td className="px-6 py-4 text-text-secondary">{job.client_name}</td>
|
||||
<td className="px-6 py-4 text-text-secondary">{job.job_type}</td>
|
||||
<td className="px-6 py-4 text-text-secondary">{job.job_level}</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">{formatDuration(job.duration_seconds)}</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">{formatBytes(job.bytes_written)}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-text-secondary hover:text-white transition-colors">
|
||||
<span className="material-symbols-outlined text-lg">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -332,9 +373,7 @@ export default function BackupManagement() {
|
||||
)}
|
||||
|
||||
{activeTab === 'clients' && (
|
||||
<div className="p-8 text-center text-text-secondary">
|
||||
Clients tab coming soon
|
||||
</div>
|
||||
<ClientsManagementTab onSwitchToConsole={() => setActiveTab('console')} />
|
||||
)}
|
||||
|
||||
{activeTab === 'storage' && (
|
||||
@@ -991,3 +1030,424 @@ function BackupConsoleTab() {
|
||||
)
|
||||
}
|
||||
|
||||
// Clients Management Tab Component
|
||||
function ClientsManagementTab({ onSwitchToConsole }: { onSwitchToConsole?: () => void }) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set())
|
||||
const [selectAll, setSelectAll] = useState(false)
|
||||
const [selectedClients, setSelectedClients] = useState<Set<number>>(new Set())
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['backup-clients', statusFilter, searchQuery],
|
||||
queryFn: () => backupAPI.listClients({
|
||||
enabled: statusFilter === 'all' ? undefined : statusFilter === 'enabled',
|
||||
search: searchQuery || undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
const clients = data?.clients || []
|
||||
const total = data?.total || 0
|
||||
|
||||
const formatDate = (dateStr?: string): string => {
|
||||
if (!dateStr) return '-'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMins = Math.floor(diffMs / 60000)
|
||||
const diffHours = Math.floor(diffMs / 3600000)
|
||||
const diffDays = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMins < 1) return 'Just now'
|
||||
if (diffMins < 60) return `${diffMins}m ago`
|
||||
if (diffHours < 24) return `${diffHours}h ago`
|
||||
if (diffDays < 7) return `${diffDays}d ago`
|
||||
return date.toLocaleDateString()
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
const getLastBackupStatus = (client: any): { status: 'success' | 'failed' | 'running' | 'warning', text: string, time: string } => {
|
||||
if (!client.last_backup_at) {
|
||||
return { status: 'warning', text: 'Never', time: 'No backup yet' }
|
||||
}
|
||||
|
||||
const date = new Date(client.last_backup_at)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffHours = Math.floor(diffMs / 3600000)
|
||||
const diffDays = Math.floor(diffMs / 86400000)
|
||||
|
||||
// Mock logic - in real app, this would come from job status
|
||||
if (diffDays > 7) {
|
||||
return { status: 'failed', text: 'Failed', time: `${diffDays}d ago (Connection Timed Out)` }
|
||||
}
|
||||
if (diffHours > 24) {
|
||||
return { status: 'warning', text: 'Warning', time: `${diffHours}h ago (Some files skipped)` }
|
||||
}
|
||||
return { status: 'success', text: 'Success', time: formatDate(client.last_backup_at) + ' (Daily)' }
|
||||
}
|
||||
|
||||
const toggleRow = (clientId: number) => {
|
||||
const newExpanded = new Set(expandedRows)
|
||||
if (newExpanded.has(clientId)) {
|
||||
newExpanded.delete(clientId)
|
||||
} else {
|
||||
newExpanded.add(clientId)
|
||||
}
|
||||
setExpandedRows(newExpanded)
|
||||
}
|
||||
|
||||
const toggleSelectClient = (clientId: number) => {
|
||||
const newSelected = new Set(selectedClients)
|
||||
if (newSelected.has(clientId)) {
|
||||
newSelected.delete(clientId)
|
||||
} else {
|
||||
newSelected.add(clientId)
|
||||
}
|
||||
setSelectedClients(newSelected)
|
||||
setSelectAll(newSelected.size === clients.length)
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectAll) {
|
||||
setSelectedClients(new Set())
|
||||
} else {
|
||||
setSelectedClients(new Set(clients.map(c => c.client_id)))
|
||||
}
|
||||
setSelectAll(!selectAll)
|
||||
}
|
||||
|
||||
// Extract IP and port from uname (mock - in real app this would come from API)
|
||||
const getConnectionInfo = (client: any) => {
|
||||
// Try to extract IP from uname or use mock
|
||||
const uname = client.uname || ''
|
||||
const ipMatch = uname.match(/\d+\.\d+\.\d+\.\d+/)
|
||||
return {
|
||||
ip: ipMatch ? ipMatch[0] : '192.168.10.25',
|
||||
port: '9102'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{clientManagementStyles}</style>
|
||||
<div className="flex flex-col gap-6 flex-1">
|
||||
{/* Header */}
|
||||
<header className="flex flex-wrap justify-between items-end gap-4 border-b border-border-dark pb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">Client Management</h1>
|
||||
<span className="flex h-6 px-2 items-center rounded-full bg-surface-highlight border border-border-dark text-xs font-bold text-primary">
|
||||
{total} Clients
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-secondary text-base font-normal max-w-2xl">
|
||||
Monitor backup status, configure file daemons, and manage client connectivity.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onSwitchToConsole) {
|
||||
onSwitchToConsole()
|
||||
} else {
|
||||
const consoleTab = document.querySelector('[data-tab="console"]') as HTMLElement
|
||||
if (consoleTab) consoleTab.click()
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer justify-center rounded-lg h-10 px-4 bg-surface-highlight border border-border-dark text-white text-sm font-bold hover:bg-[#2a3c50] transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">terminal</span>
|
||||
<span>Console</span>
|
||||
</button>
|
||||
<button className="flex items-center gap-2 cursor-pointer justify-center rounded-lg h-10 px-4 bg-primary text-white text-sm font-bold shadow-lg shadow-primary/20 hover:bg-primary/90 transition-colors">
|
||||
<span className="material-symbols-outlined text-base">add</span>
|
||||
<span>Add New Client</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 py-2">
|
||||
<div className="flex items-center gap-2 w-full md:w-auto flex-1">
|
||||
<div className="relative w-full max-w-sm">
|
||||
<span className="material-symbols-outlined absolute left-3 top-2.5 text-text-secondary text-[20px]">search</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search clients by name, IP..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-surface-highlight border border-border-dark text-white text-sm rounded-lg pl-10 pr-4 py-2 focus:ring-1 focus:ring-primary focus:border-primary outline-none placeholder-text-secondary/70 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden md:flex bg-surface-highlight border border-border-dark rounded-lg p-1 gap-1">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`px-3 py-1 rounded text-xs font-bold transition-colors ${
|
||||
statusFilter === 'all'
|
||||
? 'bg-surface-dark text-white shadow-sm border border-border-dark/50'
|
||||
: 'text-text-secondary hover:text-white hover:bg-surface-dark'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('online')}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
statusFilter === 'online'
|
||||
? 'bg-surface-dark text-white shadow-sm border border-border-dark/50'
|
||||
: 'text-text-secondary hover:text-white hover:bg-surface-dark'
|
||||
}`}
|
||||
>
|
||||
Online
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('offline')}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
statusFilter === 'offline'
|
||||
? 'bg-surface-dark text-white shadow-sm border border-border-dark/50'
|
||||
: 'text-text-secondary hover:text-white hover:bg-surface-dark'
|
||||
}`}
|
||||
>
|
||||
Offline
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('problems')}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
statusFilter === 'problems'
|
||||
? 'bg-surface-dark text-white shadow-sm border border-border-dark/50'
|
||||
: 'text-text-secondary hover:text-white hover:bg-surface-dark'
|
||||
}`}
|
||||
>
|
||||
Problems
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="flex items-center gap-2 px-3 py-2 bg-surface-highlight border border-border-dark rounded-lg text-text-secondary hover:text-white transition-colors text-sm font-medium">
|
||||
<span className="material-symbols-outlined text-[18px]">sort</span>
|
||||
Sort: Status
|
||||
</button>
|
||||
<button className="md:hidden flex items-center justify-center p-2 bg-surface-highlight border border-border-dark rounded-lg text-text-secondary">
|
||||
<span className="material-symbols-outlined">filter_list</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Clients Table */}
|
||||
<div className="rounded-lg border border-border-dark bg-surface-highlight overflow-hidden shadow-sm flex flex-col flex-1">
|
||||
{isLoading ? (
|
||||
<div className="p-8 text-center text-text-secondary">Loading clients...</div>
|
||||
) : error ? (
|
||||
<div className="p-8 text-center text-red-400">Failed to load clients</div>
|
||||
) : clients.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<p className="text-text-secondary">No clients found</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-surface-dark border-b border-border-dark text-text-secondary text-xs uppercase tracking-wider">
|
||||
<th className="px-6 py-4 w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox-custom"
|
||||
checked={selectAll}
|
||||
onChange={toggleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-4 font-semibold w-8"></th>
|
||||
<th className="px-6 py-4 font-semibold">Client Name</th>
|
||||
<th className="px-6 py-4 font-semibold">Connection</th>
|
||||
<th className="px-6 py-4 font-semibold">Status</th>
|
||||
<th className="px-6 py-4 font-semibold">Last Backup</th>
|
||||
<th className="px-6 py-4 font-semibold">Version</th>
|
||||
<th className="px-6 py-4 font-semibold text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-dark text-sm">
|
||||
{clients.map((client, idx) => {
|
||||
const isExpanded = expandedRows.has(client.client_id)
|
||||
const isSelected = selectedClients.has(client.client_id)
|
||||
const backupStatus = getLastBackupStatus(client)
|
||||
const connection = getConnectionInfo(client)
|
||||
const isOnline = client.status === 'online'
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
key={client.client_id || client.name}
|
||||
className={`hover:bg-surface-dark/50 transition-colors group ${
|
||||
idx % 2 === 0 ? 'bg-surface-dark/20' : ''
|
||||
} ${!isOnline ? 'bg-red-500/5' : ''}`}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox-custom"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSelectClient(client.client_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-4 text-center">
|
||||
<button
|
||||
onClick={() => toggleRow(client.client_id)}
|
||||
className="text-text-secondary hover:text-white transition-colors"
|
||||
>
|
||||
<span className={`material-symbols-outlined text-[20px] transition-transform ${isExpanded ? 'transform rotate-90' : ''}`}>
|
||||
chevron_right
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded bg-surface-dark flex items-center justify-center text-text-secondary border border-border-dark">
|
||||
<span className="material-symbols-outlined text-[20px]">dns</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-bold">{client.name}</p>
|
||||
<p className="text-text-secondary text-xs">{client.uname || 'Backup Client'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<p className="text-text-secondary font-mono text-xs">{connection.ip}</p>
|
||||
<p className="text-text-secondary/60 text-[10px]">Port: {connection.port}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{isOnline ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20">
|
||||
<span className="block h-1.5 w-1.5 rounded-full bg-green-400"></span>
|
||||
Online
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium bg-red-500/10 text-red-400 border border-red-500/20">
|
||||
<span className="material-symbols-outlined text-[12px]">link_off</span>
|
||||
Offline
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<div className={`flex items-center gap-1.5 ${
|
||||
backupStatus.status === 'success' ? 'text-green-400' :
|
||||
backupStatus.status === 'failed' ? 'text-red-400' :
|
||||
backupStatus.status === 'running' ? 'text-primary' :
|
||||
'text-yellow-500'
|
||||
}`}>
|
||||
{backupStatus.status === 'success' && <span className="material-symbols-outlined text-[14px]">check_circle</span>}
|
||||
{backupStatus.status === 'failed' && <span className="material-symbols-outlined text-[14px]">error</span>}
|
||||
{backupStatus.status === 'running' && <span className="block h-2 w-2 rounded-full bg-primary animate-pulse"></span>}
|
||||
{backupStatus.status === 'warning' && <span className="material-symbols-outlined text-[14px]">warning</span>}
|
||||
<span className="font-bold text-xs">{backupStatus.text}</span>
|
||||
</div>
|
||||
<p className="text-text-secondary text-xs mt-0.5">{backupStatus.time}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="text-text-secondary font-mono text-xs bg-surface-dark px-2 py-1 rounded border border-border-dark">
|
||||
v22.4.1
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button className="p-1.5 text-text-secondary hover:text-white hover:bg-surface-dark rounded transition-colors" title="Start Backup">
|
||||
<span className="material-symbols-outlined text-[20px]">play_arrow</span>
|
||||
</button>
|
||||
<button className="p-1.5 text-text-secondary hover:text-white hover:bg-surface-dark rounded transition-colors" title="Edit Config">
|
||||
<span className="material-symbols-outlined text-[20px]">edit</span>
|
||||
</button>
|
||||
<button className="p-1.5 text-text-secondary hover:text-white hover:bg-surface-dark rounded transition-colors">
|
||||
<span className="material-symbols-outlined text-[20px]">more_vert</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className="bg-surface-dark/10">
|
||||
<td className="p-0 border-b border-border-dark" colSpan={8}>
|
||||
<div className="flex flex-col pl-16 py-2 pr-6 border-l-4 border-primary/50 ml-[26px]">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs font-bold text-text-secondary uppercase tracking-wider">Installed Agents & Plugins</span>
|
||||
<div className="h-px bg-border-dark flex-1"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="relative pl-12 agent-item">
|
||||
<div className="tree-line-vertical"></div>
|
||||
<div className="tree-line-horizontal"></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface-dark border border-border-dark hover:border-primary/50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 rounded bg-blue-500/10 text-blue-400">
|
||||
<span className="material-symbols-outlined text-[18px]">folder_open</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-white text-sm font-bold">Standard File Daemon</p>
|
||||
<p className="text-text-secondary text-xs">Core Bacula Client</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2 px-2 py-1 rounded bg-surface-highlight border border-border-dark/50">
|
||||
<span className="text-[10px] text-text-secondary uppercase font-bold">Ver</span>
|
||||
<span className="text-xs text-white font-mono">22.4.1</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-green-400">
|
||||
<span className="material-symbols-outlined text-[16px]">check_circle</span>
|
||||
<span className="text-xs font-bold">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="bg-surface-dark border-t border-border-dark px-6 py-3 flex items-center justify-between mt-auto">
|
||||
<p className="text-text-secondary text-xs">Showing {clients.length} of {total} clients</p>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-1 rounded text-text-secondary opacity-50 cursor-not-allowed" disabled>
|
||||
<span className="material-symbols-outlined text-base">chevron_left</span>
|
||||
</button>
|
||||
<button className="p-1 rounded text-text-secondary hover:text-white hover:bg-surface-highlight">
|
||||
<span className="material-symbols-outlined text-base">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Console Log */}
|
||||
<div className="mt-4">
|
||||
<div className="rounded-lg bg-[#0d131a] border border-border-dark p-4 font-mono text-xs text-text-secondary shadow-inner h-32 overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-2 text-gray-500 border-b border-white/5 pb-1">
|
||||
<span>Console Log (tail -f)</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span> Connected
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-blue-400">[14:22:01] bareos-dir: Connected to Storage at backup-srv-01:9103</p>
|
||||
<p>[14:22:02] bareos-sd: Volume "Vol-0012" selected for appending</p>
|
||||
<p>[14:22:05] bareos-fd: Client "{clients[0]?.name || 'client'}" starting backup of /var/www/html</p>
|
||||
<p className="text-yellow-500">[14:23:10] warning: /var/www/html/cache/tmp locked by another process, skipping</p>
|
||||
<p>[14:23:45] bareos-dir: JobId 10423: Sending Accurate information.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user