Files
Infra-Management/resources/js/Layouts/AppLayout.jsx
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

254 lines
13 KiB
JavaScript

import { useState, useEffect } from 'react';
import { Link, usePage } from '@inertiajs/react';
import { motion, AnimatePresence } from 'framer-motion';
import {
LayoutDashboard, Server, Monitor, Layers, Box, Boxes, Cloud,
Shield, HardDrive, Activity, KeyRound, BookOpen,
Users, Settings, ChevronLeft, ChevronRight, Bell, Search,
LogOut, User, Menu, X, Zap, ChevronDown
} from 'lucide-react';
import { cn, getStatusDot } from '@/lib/utils';
import { Button } from '@/Components/ui/Button';
const ICONS = {
LayoutDashboard, Server, Monitor, Layers, Box, Boxes, Cloud,
Shield, HardDrive, Activity, KeyRound, BookOpen, Users, Settings
};
function NavItem({ item, collapsed, active }) {
const Icon = ICONS[item.icon] ?? Server;
return (
<Link
href={item.href}
className={cn(
'group relative flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200',
active
? 'bg-cyan-500/15 text-cyan-400 border border-cyan-500/30'
: 'text-white/50 hover:text-white hover:bg-white/5'
)}
>
{active && (
<motion.div
layoutId="nav-active"
className="absolute inset-0 rounded-xl bg-cyan-500/10 border border-cyan-500/20"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<Icon className={cn('w-4 h-4 shrink-0 relative z-10', active && 'text-cyan-400')} />
{!collapsed && (
<span className="relative z-10 truncate">{item.name}</span>
)}
{collapsed && (
<div className="absolute left-full ml-3 px-2 py-1 rounded-lg bg-[#0f1f35] border border-white/10 text-xs text-white whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-50">
{item.name}
</div>
)}
</Link>
);
}
export default function AppLayout({ children, title }) {
const { auth, navigation, flash, app } = usePage().props;
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [notifOpen, setNotifOpen] = useState(false);
const currentPath = window.location.pathname;
useEffect(() => {
setMobileOpen(false);
}, [currentPath]);
const isActive = (href) => {
if (href === '/') return currentPath === '/';
return currentPath.startsWith(href);
};
const topNav = navigation?.filter(n => !['acl', 'admin'].includes(n.key)) ?? [];
const adminNav = navigation?.filter(n => ['acl', 'admin'].includes(n.key)) ?? [];
return (
<div className="flex h-screen bg-[#050a0e] overflow-hidden">
{/* ── Sidebar ─────────────────────────────────────────── */}
<motion.aside
animate={{ width: collapsed ? 64 : 240 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className={cn(
'hidden lg:flex flex-col border-r border-white/[0.06] bg-[#07111d]',
'relative shrink-0 overflow-hidden'
)}
>
{/* Logo */}
<div className="flex items-center gap-3 h-16 px-4 border-b border-white/[0.06]">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cyan-500 to-purple-600 flex items-center justify-center shrink-0">
<Zap className="w-4 h-4 text-white" />
</div>
{!collapsed && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<p className="text-sm font-bold text-white leading-tight">{app?.name ?? 'Infra Manager'}</p>
<p className="text-[10px] text-white/30">v{app?.version ?? '1.0.0'}</p>
</motion.div>
)}
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto overflow-x-hidden custom-scroll px-2 py-4 space-y-0.5">
{topNav.map(item => (
<NavItem key={item.key} item={item} collapsed={collapsed} active={isActive(item.href)} />
))}
{adminNav.length > 0 && (
<>
<div className={cn('my-3 border-t border-white/[0.06]', collapsed && 'mx-2')} />
{!collapsed && <p className="px-3 text-[10px] text-white/25 uppercase tracking-wider mb-1">Admin</p>}
{adminNav.map(item => (
<NavItem key={item.key} item={item} collapsed={collapsed} active={isActive(item.href)} />
))}
</>
)}
</nav>
{/* Collapse toggle */}
<button
onClick={() => setCollapsed(c => !c)}
className="absolute top-1/2 -right-3 z-20 w-6 h-6 rounded-full bg-[#0f1f35] border border-white/20 flex items-center justify-center hover:bg-white/10 transition-colors text-white/50 hover:text-white"
>
{collapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronLeft className="w-3 h-3" />}
</button>
{/* User */}
<div className={cn('border-t border-white/[0.06] p-3', collapsed ? 'px-2' : '')}>
<Link
href="/admin"
className="flex items-center gap-3 rounded-xl p-2 hover:bg-white/5 transition-colors group"
>
<img
src={auth?.user?.avatar}
alt={auth?.user?.name}
className="w-8 h-8 rounded-full ring-1 ring-white/20 shrink-0"
/>
{!collapsed && (
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-white truncate">{auth?.user?.name}</p>
<p className="text-[10px] text-white/30 truncate">{auth?.user?.roles?.[0]}</p>
</div>
)}
</Link>
<form method="POST" action="/logout" className="mt-1">
<input type="hidden" name="_token" value={document.querySelector('meta[name=csrf-token]')?.content} />
<button
type="submit"
className={cn(
'w-full flex items-center gap-3 px-2 py-2 rounded-xl text-white/40 hover:text-red-400 hover:bg-red-500/10 transition-all text-xs',
collapsed && 'justify-center'
)}
>
<LogOut className="w-3.5 h-3.5 shrink-0" />
{!collapsed && 'Sign out'}
</button>
</form>
</div>
</motion.aside>
{/* ── Mobile Sidebar ──────────────────────────────────── */}
<AnimatePresence>
{mobileOpen && (
<>
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 z-30 bg-black/60 backdrop-blur-sm lg:hidden"
onClick={() => setMobileOpen(false)}
/>
<motion.aside
initial={{ x: -280 }} animate={{ x: 0 }} exit={{ x: -280 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className="fixed left-0 top-0 bottom-0 z-40 w-64 bg-[#07111d] border-r border-white/[0.06] flex flex-col lg:hidden"
>
<div className="flex items-center justify-between h-16 px-4 border-b border-white/[0.06]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cyan-500 to-purple-600 flex items-center justify-center">
<Zap className="w-4 h-4 text-white" />
</div>
<p className="text-sm font-bold text-white">{app?.name ?? 'Infra Manager'}</p>
</div>
<button onClick={() => setMobileOpen(false)} className="text-white/50 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<nav className="flex-1 overflow-y-auto custom-scroll px-2 py-4 space-y-0.5">
{navigation?.map(item => (
<NavItem key={item.key} item={item} collapsed={false} active={isActive(item.href)} />
))}
</nav>
</motion.aside>
</>
)}
</AnimatePresence>
{/* ── Main content ────────────────────────────────────── */}
<div className="flex flex-col flex-1 min-w-0 overflow-hidden">
{/* Top Bar */}
<header className="h-16 border-b border-white/[0.06] bg-[#07111d]/80 backdrop-blur-xl flex items-center gap-4 px-4 lg:px-6 shrink-0">
<button onClick={() => setMobileOpen(true)} className="lg:hidden text-white/50 hover:text-white">
<Menu className="w-5 h-5" />
</button>
{title && (
<h1 className="text-sm font-semibold text-white/90 hidden sm:block">{title}</h1>
)}
<div className="flex-1" />
{/* Notifications */}
<button
onClick={() => setNotifOpen(n => !n)}
className="relative p-2 rounded-lg hover:bg-white/5 text-white/50 hover:text-white transition-colors"
>
<Bell className="w-4 h-4" />
{flash?.error && (
<span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-red-400" />
)}
</button>
{/* App env badge */}
{app?.env !== 'production' && (
<span className="hidden sm:block text-[10px] px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-400 border border-yellow-500/30 font-medium">
{app?.env?.toUpperCase()}
</span>
)}
</header>
{/* Flash messages */}
<AnimatePresence>
{(flash?.success || flash?.error || flash?.warning) && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={cn(
'mx-4 lg:mx-6 mt-4 px-4 py-3 rounded-xl border text-sm flex items-center gap-3',
flash?.success && 'bg-green-500/15 border-green-500/30 text-green-300',
flash?.error && 'bg-red-500/15 border-red-500/30 text-red-300',
flash?.warning && 'bg-yellow-500/15 border-yellow-500/30 text-yellow-300'
)}
>
<span>{flash?.success || flash?.error || flash?.warning}</span>
</motion.div>
)}
</AnimatePresence>
{/* Page Content */}
<main className="flex-1 overflow-y-auto custom-scroll">
<div className="p-4 lg:p-6">
{children}
</div>
</main>
</div>
</div>
);
}