working on storage dashboard
This commit is contained in:
@@ -1,190 +1,609 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import apiClient from '@/api/client'
|
||||
import { monitoringApi } from '@/api/monitoring'
|
||||
import { storageApi } from '@/api/storage'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Activity, Database, AlertTriangle, HardDrive } from 'lucide-react'
|
||||
import { formatBytes } from '@/lib/format'
|
||||
import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
Clock,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
TrendingDown,
|
||||
CheckCircle2,
|
||||
AlertTriangle
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
|
||||
export default function Dashboard() {
|
||||
const [activeTab, setActiveTab] = useState<'jobs' | 'logs' | 'alerts'>('jobs')
|
||||
const [networkDataPoints, setNetworkDataPoints] = useState<Array<{ time: string; inbound: number; outbound: number }>>([])
|
||||
const refreshInterval = 5
|
||||
|
||||
const { data: health } = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/health')
|
||||
return response.data
|
||||
},
|
||||
refetchInterval: refreshInterval * 1000,
|
||||
})
|
||||
|
||||
const { data: metrics } = useQuery({
|
||||
queryKey: ['metrics'],
|
||||
queryFn: monitoringApi.getMetrics,
|
||||
refetchInterval: refreshInterval * 1000,
|
||||
})
|
||||
|
||||
const { data: alerts } = useQuery({
|
||||
queryKey: ['alerts', 'dashboard'],
|
||||
queryFn: () => monitoringApi.listAlerts({ is_acknowledged: false, limit: 5 }),
|
||||
queryFn: () => monitoringApi.listAlerts({ is_acknowledged: false, limit: 10 }),
|
||||
refetchInterval: refreshInterval * 1000,
|
||||
})
|
||||
|
||||
const { data: repositories } = useQuery({
|
||||
const { data: repositories = [] } = useQuery({
|
||||
queryKey: ['storage', 'repositories'],
|
||||
queryFn: storageApi.listRepositories,
|
||||
})
|
||||
|
||||
const unacknowledgedAlerts = alerts?.alerts?.length || 0
|
||||
const totalRepos = repositories?.length || 0
|
||||
const totalStorage = repositories?.reduce((sum, repo) => sum + repo.size_bytes, 0) || 0
|
||||
const usedStorage = repositories?.reduce((sum, repo) => sum + repo.used_bytes, 0) || 0
|
||||
// Calculate uptime (mock for now, would come from metrics)
|
||||
const uptime = metrics?.system?.uptime_seconds || 0
|
||||
const days = Math.floor(uptime / 86400)
|
||||
const hours = Math.floor((uptime % 86400) / 3600)
|
||||
const minutes = Math.floor((uptime % 3600) / 60)
|
||||
|
||||
// Mock active jobs (would come from tasks API)
|
||||
const activeJobs = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Daily Backup: VM-Cluster-01',
|
||||
type: 'Replication',
|
||||
progress: 45,
|
||||
speed: '145 MB/s',
|
||||
status: 'running',
|
||||
eta: '1h 12m',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'ZFS Scrub: Pool-01',
|
||||
type: 'Maintenance',
|
||||
progress: 78,
|
||||
speed: '1.2 GB/s',
|
||||
status: 'running',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock system logs
|
||||
const systemLogs = [
|
||||
{ time: '10:45:22', level: 'INFO', source: 'systemd', message: 'Started User Manager for UID 1000.' },
|
||||
{ time: '10:45:15', level: 'WARN', source: 'smartd', message: 'Device: /dev/ada5, SMART Usage Attribute: 194 Temperature_Celsius changed from 38 to 41' },
|
||||
{ time: '10:44:58', level: 'INFO', source: 'kernel', message: 'ix0: link state changed to UP' },
|
||||
{ time: '10:42:10', level: 'INFO', source: 'zfs', message: 'zfs_arc_reclaim_thread: reclaiming 157286400 bytes ...' },
|
||||
]
|
||||
|
||||
const totalStorage = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.size_bytes || 0), 0) : 0
|
||||
const usedStorage = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.used_bytes || 0), 0) : 0
|
||||
const storagePercent = totalStorage > 0 ? (usedStorage / totalStorage) * 100 : 0
|
||||
|
||||
// Initialize network data
|
||||
useEffect(() => {
|
||||
// Generate initial 30 data points
|
||||
const initialData = []
|
||||
const now = Date.now()
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const time = new Date(now - i * 5000)
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0')
|
||||
const seconds = time.getSeconds().toString().padStart(2, '0')
|
||||
|
||||
const baseInbound = 800 + Math.random() * 400
|
||||
const baseOutbound = 400 + Math.random() * 200
|
||||
|
||||
initialData.push({
|
||||
time: `${minutes}:${seconds}`,
|
||||
inbound: Math.round(baseInbound),
|
||||
outbound: Math.round(baseOutbound),
|
||||
})
|
||||
}
|
||||
setNetworkDataPoints(initialData)
|
||||
|
||||
// Update data every 5 seconds
|
||||
const interval = setInterval(() => {
|
||||
setNetworkDataPoints((prev) => {
|
||||
const now = new Date()
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||
|
||||
const baseInbound = 800 + Math.random() * 400
|
||||
const baseOutbound = 400 + Math.random() * 200
|
||||
|
||||
const newPoint = {
|
||||
time: `${minutes}:${seconds}`,
|
||||
inbound: Math.round(baseInbound),
|
||||
outbound: Math.round(baseOutbound),
|
||||
}
|
||||
|
||||
// Keep only last 30 points
|
||||
const updated = [...prev.slice(1), newPoint]
|
||||
return updated
|
||||
})
|
||||
}, 5000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Calculate current and peak throughput
|
||||
const currentThroughput = useMemo(() => {
|
||||
if (networkDataPoints.length === 0) return { inbound: 0, outbound: 0, total: 0 }
|
||||
const last = networkDataPoints[networkDataPoints.length - 1]
|
||||
return {
|
||||
inbound: last.inbound,
|
||||
outbound: last.outbound,
|
||||
total: last.inbound + last.outbound,
|
||||
}
|
||||
}, [networkDataPoints])
|
||||
|
||||
const peakThroughput = useMemo(() => {
|
||||
if (networkDataPoints.length === 0) return 0
|
||||
return Math.max(...networkDataPoints.map((d) => d.inbound + d.outbound))
|
||||
}, [networkDataPoints])
|
||||
|
||||
const systemStatus = health?.status === 'healthy' ? 'System Healthy' : 'System Degraded'
|
||||
const isHealthy = health?.status === 'healthy'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Overview of your Calypso backup appliance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* System Health */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Activity className="h-5 w-5 mr-2" />
|
||||
System Health
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{health && (
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={`h-4 w-4 rounded-full ${
|
||||
health.status === 'healthy'
|
||||
? 'bg-green-500'
|
||||
: health.status === 'degraded'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-lg font-semibold capitalize">{health.status}</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600">Service: {health.service}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Storage Repositories</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalRepos}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatBytes(usedStorage)} / {formatBytes(totalStorage)} used
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Alerts</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{unacknowledgedAlerts}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{unacknowledgedAlerts === 0 ? 'All clear' : 'Requires attention'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{metrics && (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">iSCSI Targets</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{metrics.scst.total_targets}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{metrics.scst.active_sessions} active sessions
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Tasks</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{metrics.tasks.running_tasks}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{metrics.tasks.pending_tasks} pending
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
<CardDescription>Common operations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Link to="/storage" className="w-full">
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<Database className="h-4 w-4 mr-2" />
|
||||
Manage Storage
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/alerts" className="w-full">
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<AlertTriangle className="h-4 w-4 mr-2" />
|
||||
View Alerts
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Alerts */}
|
||||
{alerts && alerts.alerts.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Alerts</CardTitle>
|
||||
<CardDescription>Latest unacknowledged alerts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{alerts.alerts.slice(0, 3).map((alert) => (
|
||||
<div key={alert.id} className="text-sm">
|
||||
<p className="font-medium">{alert.title}</p>
|
||||
<p className="text-xs text-gray-500">{alert.message}</p>
|
||||
</div>
|
||||
))}
|
||||
{alerts.alerts.length > 3 && (
|
||||
<Link to="/alerts">
|
||||
<Button variant="link" className="p-0 h-auto">
|
||||
View all alerts →
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="min-h-screen bg-background-dark text-white overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="flex-none px-6 py-5 border-b border-border-dark bg-background-dark/95 backdrop-blur z-10">
|
||||
<div className="flex flex-wrap justify-between items-end gap-3 max-w-[1600px] mx-auto">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-white text-3xl font-black tracking-tight">System Monitor</h2>
|
||||
<p className="text-text-secondary text-sm">Real-time telemetry, storage health, and system event logs</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-card-dark rounded-lg border border-border-dark">
|
||||
<span className="relative flex h-2 w-2">
|
||||
{isHealthy && (
|
||||
<>
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||
</>
|
||||
)}
|
||||
{!isHealthy && (
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-yellow-500"></span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`text-xs font-medium ${isHealthy ? 'text-emerald-400' : 'text-yellow-400'}`}>
|
||||
{systemStatus}
|
||||
</span>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 h-10 px-4 bg-card-dark hover:bg-[#233648] border border-border-dark text-white text-sm font-bold rounded-lg transition-colors">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<span>Refresh: {refreshInterval}s</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-6">
|
||||
<div className="flex flex-col gap-6 max-w-[1600px] mx-auto pb-10">
|
||||
{/* Top Stats Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* CPU */}
|
||||
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-text-secondary text-sm font-medium">CPU Load</p>
|
||||
<Cpu className="text-text-secondary w-5 h-5" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<div className="flex items-end gap-3 mt-1">
|
||||
<p className="text-white text-3xl font-bold">
|
||||
{metrics?.system?.cpu_usage_percent?.toFixed(0) || 0}%
|
||||
</p>
|
||||
<span className="text-emerald-500 text-sm font-medium mb-1 flex items-center">
|
||||
<TrendingDown className="w-4 h-4 mr-1" />
|
||||
2%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-[#233648] rounded-full mt-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all"
|
||||
style={{ width: `${metrics?.system?.cpu_usage_percent || 0}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RAM */}
|
||||
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-text-secondary text-sm font-medium">RAM Usage</p>
|
||||
<MemoryStick className="text-text-secondary w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex items-end gap-3 mt-1">
|
||||
<p className="text-white text-3xl font-bold">
|
||||
{formatBytes(metrics?.system?.memory_used_bytes || 0, 1)}
|
||||
</p>
|
||||
<span className="text-text-secondary text-xs mb-2">
|
||||
/ {formatBytes(metrics?.system?.memory_total_bytes || 0, 1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-[#233648] rounded-full mt-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-emerald-500 rounded-full transition-all"
|
||||
style={{ width: `${metrics?.system?.memory_usage_percent || 0}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Storage Health */}
|
||||
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-text-secondary text-sm font-medium">Storage Status</p>
|
||||
<CheckCircle2 className="text-emerald-500 w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex items-end gap-3 mt-1">
|
||||
<p className="text-white text-3xl font-bold">Online</p>
|
||||
<span className="text-text-secondary text-sm font-medium mb-1">No Errors</span>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-3">
|
||||
<div className="h-1.5 flex-1 bg-emerald-500 rounded-l-full"></div>
|
||||
<div className="h-1.5 flex-1 bg-emerald-500"></div>
|
||||
<div className="h-1.5 flex-1 bg-emerald-500"></div>
|
||||
<div className="h-1.5 flex-1 bg-emerald-500 rounded-r-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Uptime */}
|
||||
<div className="flex flex-col gap-2 rounded-xl p-5 border border-border-dark bg-card-dark">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-text-secondary text-sm font-medium">System Uptime</p>
|
||||
<Clock className="text-text-secondary w-5 h-5" />
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<p className="text-white text-3xl font-bold">
|
||||
{days}d {hours}h {minutes}m
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-text-secondary text-xs mt-3">Last reboot: Manual Patching</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Section: Charts & Storage */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
{/* Charts Column (2/3) */}
|
||||
<div className="xl:col-span-2 flex flex-col gap-6">
|
||||
{/* Network Chart */}
|
||||
<div className="bg-card-dark border border-border-dark rounded-xl p-6 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h3 className="text-white text-lg font-bold">Network Throughput</h3>
|
||||
<p className="text-text-secondary text-sm">Inbound vs Outbound (eth0)</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white text-2xl font-bold leading-tight">
|
||||
{(currentThroughput.total / 1000).toFixed(1)} Gbps
|
||||
</p>
|
||||
<p className="text-emerald-500 text-sm">Peak: {(peakThroughput / 1000).toFixed(1)} Gbps</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[200px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={networkDataPoints} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#324d67" opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="#92adc9"
|
||||
style={{ fontSize: '11px' }}
|
||||
tick={{ fill: '#92adc9' }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#92adc9"
|
||||
style={{ fontSize: '11px' }}
|
||||
tick={{ fill: '#92adc9' }}
|
||||
label={{ value: 'Mbps', angle: -90, position: 'insideLeft', fill: '#92adc9', style: { fontSize: '11px' } }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1a2632',
|
||||
border: '1px solid #324d67',
|
||||
borderRadius: '8px',
|
||||
color: '#fff',
|
||||
}}
|
||||
labelStyle={{ color: '#92adc9', fontSize: '12px' }}
|
||||
itemStyle={{ color: '#fff', fontSize: '12px' }}
|
||||
formatter={(value: number) => [`${value} Mbps`, '']}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '12px', color: '#92adc9' }}
|
||||
iconType="line"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="inbound"
|
||||
stroke="#137fec"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Inbound"
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="outbound"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Outbound"
|
||||
activeDot={{ r: 4 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Storage Chart */}
|
||||
<div className="bg-card-dark border border-border-dark rounded-xl p-6 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h3 className="text-white text-lg font-bold">Storage Capacity</h3>
|
||||
<p className="text-text-secondary text-sm">Repository usage</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white text-2xl font-bold leading-tight">
|
||||
{storagePercent.toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-text-secondary text-sm">Target: <90%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[150px] w-full relative">
|
||||
<div className="w-full h-full bg-[#111a22] rounded flex items-center justify-center">
|
||||
<div className="w-full px-4">
|
||||
<div className="w-full bg-[#233648] h-2 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="bg-primary h-full rounded-full transition-all"
|
||||
style={{ width: `${storagePercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Storage Info Column (1/3) */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="bg-card-dark border border-border-dark rounded-xl p-6 h-full shadow-sm flex flex-col">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-white text-lg font-bold">Storage Overview</h3>
|
||||
<span className="bg-[#233648] text-white text-xs px-2 py-1 rounded border border-border-dark">
|
||||
{repositories?.length || 0} Repos
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border-dark">
|
||||
<div className="flex justify-between text-sm text-text-secondary">
|
||||
<span>Total Capacity</span>
|
||||
<span className="text-white font-bold">{formatBytes(totalStorage)}</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#233648] h-2 rounded-full mt-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary h-full transition-all"
|
||||
style={{ width: `${storagePercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-text-secondary mt-1">
|
||||
<span>Used: {formatBytes(usedStorage)}</span>
|
||||
<span>Free: {formatBytes(totalStorage - usedStorage)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Tabs & Logs */}
|
||||
<div className="bg-card-dark border border-border-dark rounded-xl shadow-sm overflow-hidden flex flex-col h-[400px]">
|
||||
{/* Tabs Header */}
|
||||
<div className="flex border-b border-border-dark bg-[#161f29]">
|
||||
<button
|
||||
onClick={() => setActiveTab('jobs')}
|
||||
className={`px-6 py-4 text-sm font-bold transition-colors ${
|
||||
activeTab === 'jobs'
|
||||
? 'text-primary border-b-2 border-primary bg-card-dark'
|
||||
: 'text-text-secondary hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Active Jobs{' '}
|
||||
{activeJobs.length > 0 && (
|
||||
<span className="ml-2 bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs">
|
||||
{activeJobs.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('logs')}
|
||||
className={`px-6 py-4 text-sm font-medium transition-colors ${
|
||||
activeTab === 'logs'
|
||||
? 'text-primary border-b-2 border-primary bg-card-dark'
|
||||
: 'text-text-secondary hover:text-white'
|
||||
}`}
|
||||
>
|
||||
System Logs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('alerts')}
|
||||
className={`px-6 py-4 text-sm font-medium transition-colors ${
|
||||
activeTab === 'alerts'
|
||||
? 'text-primary border-b-2 border-primary bg-card-dark'
|
||||
: 'text-text-secondary hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Alerts History
|
||||
</button>
|
||||
<div className="flex-1 flex justify-end items-center px-4">
|
||||
<div className="relative">
|
||||
<Activity className="absolute left-2 top-1.5 text-text-secondary w-4 h-4" />
|
||||
<input
|
||||
className="bg-[#111a22] border border-border-dark rounded-md py-1 pl-8 pr-3 text-sm text-white focus:outline-none focus:border-primary w-48 transition-all"
|
||||
placeholder="Search logs..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
{activeTab === 'jobs' && (
|
||||
<div className="p-0">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="bg-[#1a2632] text-xs uppercase text-text-secondary font-medium sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="px-6 py-3 border-b border-border-dark">Job Name</th>
|
||||
<th className="px-6 py-3 border-b border-border-dark">Type</th>
|
||||
<th className="px-6 py-3 border-b border-border-dark w-1/3">Progress</th>
|
||||
<th className="px-6 py-3 border-b border-border-dark">Speed</th>
|
||||
<th className="px-6 py-3 border-b border-border-dark">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm divide-y divide-border-dark">
|
||||
{activeJobs.map((job) => (
|
||||
<tr key={job.id} className="group hover:bg-[#233648] transition-colors">
|
||||
<td className="px-6 py-4 font-medium text-white">{job.name}</td>
|
||||
<td className="px-6 py-4 text-text-secondary">{job.type}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-full bg-[#111a22] rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary h-full rounded-full relative overflow-hidden"
|
||||
style={{ width: `${job.progress}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white/20 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-white">{job.progress}%</span>
|
||||
</div>
|
||||
{job.eta && (
|
||||
<p className="text-[10px] text-text-secondary mt-1">ETA: {job.eta}</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-text-secondary font-mono">{job.speed}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-primary/20 text-primary">
|
||||
Running
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'logs' && (
|
||||
<>
|
||||
<div className="px-6 py-2 bg-[#161f29] border-y border-border-dark flex items-center justify-between">
|
||||
<h4 className="text-xs uppercase text-text-secondary font-bold tracking-wider">
|
||||
Recent System Events
|
||||
</h4>
|
||||
<button className="text-xs text-primary hover:text-white transition-colors">
|
||||
View All Logs
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar bg-[#111a22]">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<tbody className="text-sm font-mono divide-y divide-border-dark/50">
|
||||
{systemLogs.map((log, idx) => (
|
||||
<tr key={idx} className="group hover:bg-[#233648] transition-colors">
|
||||
<td className="px-6 py-2 text-text-secondary w-32 whitespace-nowrap">
|
||||
{log.time}
|
||||
</td>
|
||||
<td className="px-6 py-2 w-24">
|
||||
<span
|
||||
className={
|
||||
log.level === 'INFO'
|
||||
? 'text-emerald-500'
|
||||
: log.level === 'WARN'
|
||||
? 'text-yellow-500'
|
||||
: 'text-red-500'
|
||||
}
|
||||
>
|
||||
{log.level}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-2 w-32 text-white">{log.source}</td>
|
||||
<td className="px-6 py-2 text-text-secondary truncate max-w-lg">
|
||||
{log.message}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'alerts' && (
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar bg-[#111a22] p-6">
|
||||
{alerts?.alerts && alerts.alerts.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{alerts.alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className="bg-[#1a2632] border border-border-dark rounded-lg p-4 hover:bg-[#233648] transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<AlertTriangle
|
||||
className={`w-4 h-4 ${
|
||||
alert.severity === 'critical'
|
||||
? 'text-red-500'
|
||||
: alert.severity === 'warning'
|
||||
? 'text-yellow-500'
|
||||
: 'text-blue-500'
|
||||
}`}
|
||||
/>
|
||||
<h4 className="text-white font-medium">{alert.title}</h4>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
alert.severity === 'critical'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: alert.severity === 'warning'
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
}`}
|
||||
>
|
||||
{alert.severity}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-secondary text-sm">{alert.message}</p>
|
||||
<p className="text-text-secondary text-xs mt-2 font-mono">
|
||||
{new Date(alert.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-500 mx-auto mb-4" />
|
||||
<p className="text-text-secondary">No alerts</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user