Files
I-Fun c649f055d8 feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js
- Added main application entry point (app.jsx) and bootstrap file for React setup.
- Created Wiki index page (Index.jsx) for documentation and file management.
- Implemented utility functions for formatting and status handling in utils.js.
- Set up Blade template (app.blade.php) for Inertia.js integration.
- Defined API routes for metrics and terminal access in api.php.
- Established console commands for scheduled tasks in console.php.
- Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php.
- Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments.
- Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js.
- Set up Vite for asset bundling and development server in vite.config.js.
2026-04-07 20:29:32 +07:00

239 lines
16 KiB
JavaScript

import AppLayout from '@/Layouts/AppLayout';
import { Head } from '@inertiajs/react';
import { motion } from 'framer-motion';
import {
Server, Monitor, Box, Cloud, Shield, AlertTriangle,
Activity, TrendingUp, Cpu, MemoryStick, HardDrive, Wifi
} from 'lucide-react';
import { StatCard, GlassCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { ProgressBar } from '@/Components/ui/ProgressBar';
import { timeAgo, getStatusDot } from '@/lib/utils';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip,
ResponsiveContainer, PieChart, Pie, Cell
} from 'recharts';
const FADE_UP = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } };
const CONTAINER = {
hidden: {},
show: { transition: { staggerChildren: 0.05 } }
};
// Mock chart data - replace with real metrics
const cpuData = Array.from({ length: 20 }, (_, i) => ({
t: `${i}m`, cpu: Math.round(20 + Math.random() * 50), mem: Math.round(40 + Math.random() * 40)
}));
const PROVIDER_COLORS = { aws: '#FF9900', alibaba: '#FF6A00', tencent: '#00A3FF', gcp: '#4285F4', azure: '#0078D4' };
export default function Dashboard({ stats, hypervisors, endpoints, cloud_accounts, recent_alerts }) {
return (
<AppLayout title="Dashboard">
<Head title="Dashboard" />
<motion.div variants={CONTAINER} initial="hidden" animate="show" className="space-y-6">
{/* ── Header ──────────────────────────────────────── */}
<motion.div variants={FADE_UP} className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Infrastructure Overview</h2>
<p className="text-sm text-white/40 mt-0.5">Real-time view across all platforms</p>
</div>
<div className="flex items-center gap-2 text-xs text-white/30">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
Live
</div>
</motion.div>
{/* ── Top stats ────────────────────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-3">
<StatCard title="Hypervisors" value={stats.hypervisors} subtitle={`${stats.hypervisors_online} online`} icon={Server} color="cyan" />
<StatCard title="Virtual Machines" value={stats.vms} subtitle={`${stats.vms_running} running`} icon={Monitor} color="green" />
<StatCard title="Containers" value={stats.containers} subtitle={`${stats.containers_running} running`} icon={Box} color="purple" />
<StatCard title="Cloud Accounts" value={stats.cloud_accounts} subtitle="Connected" icon={Cloud} color="blue" />
<StatCard title="Critical Alerts" value={stats.critical_alerts} subtitle={`${stats.warning_alerts} warnings`} icon={AlertTriangle} color={stats.critical_alerts > 0 ? 'red' : 'green'} />
<StatCard title="Security Score" value={`${stats.security_score}%`} subtitle="Avg across infra" icon={Shield} color={stats.security_score >= 80 ? 'green' : stats.security_score >= 60 ? 'yellow' : 'red'} />
</motion.div>
{/* ── Charts Row ───────────────────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* CPU / Memory chart */}
<GlassCard className="p-5 lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-semibold text-white">Resource Utilization</h3>
<p className="text-xs text-white/30">Avg CPU & Memory — last 20 min</p>
</div>
<div className="flex items-center gap-4 text-xs">
<span className="flex items-center gap-1.5 text-cyan-400"><span className="w-2 h-0.5 bg-cyan-400 rounded" />CPU</span>
<span className="flex items-center gap-1.5 text-purple-400"><span className="w-2 h-0.5 bg-purple-400 rounded" />Memory</span>
</div>
</div>
<ResponsiveContainer width="100%" height={180}>
<AreaChart data={cpuData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="cpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#00d4ff" stopOpacity={0.3} />
<stop offset="95%" stopColor="#00d4ff" stopOpacity={0} />
</linearGradient>
<linearGradient id="mem" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#9d4edd" stopOpacity={0.3} />
<stop offset="95%" stopColor="#9d4edd" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="t" tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} domain={[0, 100]} />
<Tooltip
contentStyle={{ backgroundColor: '#0f1f35', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '12px' }}
labelStyle={{ color: 'rgba(255,255,255,0.6)' }}
/>
<Area type="monotone" dataKey="cpu" stroke="#00d4ff" strokeWidth={2} fill="url(#cpu)" />
<Area type="monotone" dataKey="mem" stroke="#9d4edd" strokeWidth={2} fill="url(#mem)" />
</AreaChart>
</ResponsiveContainer>
</GlassCard>
{/* VM Status Donut */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-1">VM Status</h3>
<p className="text-xs text-white/30 mb-4">Across all hypervisors</p>
<ResponsiveContainer width="100%" height={140}>
<PieChart>
<Pie data={[
{ name: 'Running', value: stats.vms_running },
{ name: 'Stopped', value: stats.vms_stopped },
{ name: 'Other', value: Math.max(0, stats.vms - stats.vms_running - stats.vms_stopped) },
]}
cx="50%" cy="50%" innerRadius={45} outerRadius={65}
paddingAngle={3} dataKey="value"
>
<Cell fill="#00d4ff" />
<Cell fill="#334155" />
<Cell fill="#9d4edd" />
</Pie>
<Tooltip contentStyle={{ backgroundColor: '#0f1f35', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '12px' }} />
</PieChart>
</ResponsiveContainer>
<div className="space-y-1.5 mt-2">
{[{ label: 'Running', val: stats.vms_running, color: 'bg-cyan-400' },
{ label: 'Stopped', val: stats.vms_stopped, color: 'bg-slate-600' }].map(s => (
<div key={s.label} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2"><span className={`w-2 h-2 rounded-full ${s.color}`} /><span className="text-white/50">{s.label}</span></div>
<span className="text-white font-medium">{s.val}</span>
</div>
))}
</div>
</GlassCard>
</motion.div>
{/* ── Hypervisors & Endpoints ───────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Hypervisors */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Hypervisors</h3>
<a href="/hypervisors" className="text-xs text-cyan-400 hover:text-cyan-300">View all →</a>
</div>
<div className="space-y-2">
{hypervisors?.slice(0, 6).map(h => (
<a key={h.id} href={`/hypervisors/${h.id}`}
className="flex items-center gap-3 p-3 rounded-lg bg-white/[0.02] hover:bg-white/[0.05] border border-white/[0.05] transition-colors group"
>
<span className={`w-2 h-2 rounded-full ${getStatusDot(h.status)} shrink-0`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-white truncate group-hover:text-cyan-300 transition-colors">{h.name}</p>
<p className="text-xs text-white/30 capitalize">{h.type} · {timeAgo(h.last_sync_at)}</p>
</div>
<StatusBadge status={h.status} />
</a>
))}
{(!hypervisors || hypervisors.length === 0) && (
<p className="text-sm text-white/30 text-center py-6">No hypervisors configured</p>
)}
</div>
</GlassCard>
{/* Docker Endpoints */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Docker Endpoints</h3>
<a href="/containers" className="text-xs text-cyan-400 hover:text-cyan-300">View all →</a>
</div>
<div className="space-y-2">
{endpoints?.slice(0, 6).map(e => (
<div key={e.id} className="flex items-center gap-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.05]">
<span className={`w-2 h-2 rounded-full ${getStatusDot(e.status)} shrink-0`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-white truncate">{e.name}</p>
<p className="text-xs text-white/30 capitalize">{e.type}</p>
</div>
<StatusBadge status={e.status} />
</div>
))}
{(!endpoints || endpoints.length === 0) && (
<p className="text-sm text-white/30 text-center py-6">No Docker endpoints</p>
)}
</div>
</GlassCard>
</motion.div>
{/* ── Cloud & Alerts ────────────────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Cloud accounts */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Cloud Accounts</h3>
<a href="/cloud" className="text-xs text-cyan-400 hover:text-cyan-300">Manage →</a>
</div>
<div className="grid grid-cols-2 gap-2">
{cloud_accounts?.map(acc => (
<div key={acc.id} className="flex items-center gap-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.05]">
<div className="w-8 h-8 rounded-lg bg-white/10 flex items-center justify-center text-xs font-bold"
style={{ color: PROVIDER_COLORS[acc.provider] ?? '#fff' }}>
{acc.provider?.toUpperCase().slice(0, 2)}
</div>
<div className="min-w-0">
<p className="text-xs font-medium text-white truncate">{acc.name}</p>
<StatusBadge status={acc.status} className="mt-0.5 text-[10px]" />
</div>
</div>
))}
{(!cloud_accounts || cloud_accounts.length === 0) && (
<p className="col-span-2 text-sm text-white/30 text-center py-6">No cloud accounts</p>
)}
</div>
</GlassCard>
{/* Recent Alerts */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Active Alerts</h3>
<a href="/monitoring/alerts" className="text-xs text-cyan-400 hover:text-cyan-300">View all →</a>
</div>
<div className="space-y-2">
{recent_alerts?.map(alert => (
<div key={alert.id} className="flex items-start gap-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.05]">
<AlertTriangle className={`w-4 h-4 mt-0.5 shrink-0 ${alert.severity === 'critical' ? 'text-red-400' : 'text-yellow-400'}`} />
<div className="min-w-0 flex-1">
<p className="text-sm text-white truncate">{alert.title}</p>
<p className="text-xs text-white/30">{alert.target_name} · {timeAgo(alert.created_at)}</p>
</div>
<StatusBadge status={alert.severity} />
</div>
))}
{(!recent_alerts || recent_alerts.length === 0) && (
<div className="flex flex-col items-center py-6 gap-2">
<Shield className="w-8 h-8 text-green-400/50" />
<p className="text-sm text-white/30">All clear — no active alerts</p>
</div>
)}
</div>
</GlassCard>
</motion.div>
</motion.div>
</AppLayout>
);
}