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.
This commit is contained in:
42
resources/js/Components/ui/Button.jsx
Normal file
42
resources/js/Components/ui/Button.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Button({ children, variant = 'primary', size = 'md', className, loading, icon: Icon, ...props }) {
|
||||
const variants = {
|
||||
primary: 'bg-cyan-500 hover:bg-cyan-400 text-black font-semibold shadow-[0_0_20px_rgba(0,212,255,0.3)] hover:shadow-[0_0_30px_rgba(0,212,255,0.5)]',
|
||||
secondary: 'bg-white/10 hover:bg-white/15 text-white border border-white/20',
|
||||
danger: 'bg-red-500/20 hover:bg-red-500/30 text-red-400 border border-red-500/30',
|
||||
ghost: 'hover:bg-white/5 text-white/70 hover:text-white',
|
||||
outline: 'border border-cyan-500/50 text-cyan-400 hover:bg-cyan-500/10',
|
||||
};
|
||||
const sizes = {
|
||||
sm: 'h-7 px-3 text-xs gap-1.5',
|
||||
md: 'h-9 px-4 text-sm gap-2',
|
||||
lg: 'h-11 px-6 text-base gap-2',
|
||||
icon: 'h-9 w-9 p-0',
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
disabled={loading || props.disabled}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : Icon ? (
|
||||
<Icon className="w-4 h-4" />
|
||||
) : null}
|
||||
{size !== 'icon' && children}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
137
resources/js/Components/ui/DataTable.jsx
Normal file
137
resources/js/Components/ui/DataTable.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ChevronUp, ChevronDown, Search, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function DataTable({ columns, data, onRowClick, emptyMessage = 'No data found', searchable = true, className }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortCol, setSortCol] = useState(null);
|
||||
const [sortDir, setSortDir] = useState('asc');
|
||||
const [page, setPage] = useState(1);
|
||||
const perPage = 15;
|
||||
|
||||
const filtered = search
|
||||
? data.filter(row => columns.some(col => {
|
||||
const val = col.accessor ? row[col.accessor] : '';
|
||||
return String(val ?? '').toLowerCase().includes(search.toLowerCase());
|
||||
}))
|
||||
: data;
|
||||
|
||||
const sorted = sortCol
|
||||
? [...filtered].sort((a, b) => {
|
||||
const va = a[sortCol] ?? '';
|
||||
const vb = b[sortCol] ?? '';
|
||||
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
|
||||
return sortDir === 'asc' ? cmp : -cmp;
|
||||
})
|
||||
: filtered;
|
||||
|
||||
const totalPages = Math.ceil(sorted.length / perPage);
|
||||
const paginated = sorted.slice((page - 1) * perPage, page * perPage);
|
||||
|
||||
const handleSort = (accessor) => {
|
||||
if (sortCol === accessor) {
|
||||
setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortCol(accessor);
|
||||
setSortDir('asc');
|
||||
}
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
{searchable && (
|
||||
<div className="relative w-72">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||||
className="w-full pl-9 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-cyan-500/50 focus:bg-white/[0.07] transition-all"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 bg-white/[0.02]">
|
||||
{columns.map(col => (
|
||||
<th key={col.key ?? col.accessor}
|
||||
className={cn(
|
||||
'text-left px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider whitespace-nowrap',
|
||||
col.accessor && 'cursor-pointer hover:text-white/70 select-none'
|
||||
)}
|
||||
onClick={() => col.accessor && handleSort(col.accessor)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{col.header}
|
||||
{col.accessor && sortCol === col.accessor && (
|
||||
sortDir === 'asc'
|
||||
? <ChevronUp className="w-3 h-3 text-cyan-400" />
|
||||
: <ChevronDown className="w-3 h-3 text-cyan-400" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<AnimatePresence mode="wait">
|
||||
{paginated.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center py-12 text-white/30">
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
paginated.map((row, i) => (
|
||||
<motion.tr
|
||||
key={row.id ?? i}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.02 }}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
className={cn(
|
||||
'border-b border-white/[0.05] last:border-0 transition-colors',
|
||||
onRowClick && 'cursor-pointer hover:bg-white/[0.03]'
|
||||
)}
|
||||
>
|
||||
{columns.map(col => (
|
||||
<td key={col.key ?? col.accessor} className="px-4 py-3 text-white/80">
|
||||
{col.render ? col.render(row) : row[col.accessor]}
|
||||
</td>
|
||||
))}
|
||||
</motion.tr>
|
||||
))
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between text-xs text-white/40">
|
||||
<span>{filtered.length} results</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setPage(1)} disabled={page === 1} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronsLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setPage(p => p - 1)} disabled={page === 1} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="px-3 text-white">Page {page} of {totalPages}</span>
|
||||
<button onClick={() => setPage(p => p + 1)} disabled={page === totalPages} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setPage(totalPages)} disabled={page === totalPages} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronsRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
resources/js/Components/ui/GlassCard.jsx
Normal file
59
resources/js/Components/ui/GlassCard.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function GlassCard({ children, className, hover = true, glow, ...props }) {
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={hover ? { y: -2, scale: 1.005 } : undefined}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-xl',
|
||||
'border border-white/10 bg-white/[0.03] backdrop-blur-xl',
|
||||
hover && 'cursor-pointer',
|
||||
glow === 'cyan' && 'hover:border-cyan-500/40 hover:shadow-[0_0_30px_rgba(0,212,255,0.1)]',
|
||||
glow === 'green' && 'hover:border-green-500/40 hover:shadow-[0_0_30px_rgba(0,255,136,0.1)]',
|
||||
glow === 'purple' && 'hover:border-purple-500/40 hover:shadow-[0_0_30px_rgba(157,78,221,0.1)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* Inner gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.02] to-transparent pointer-events-none" />
|
||||
<div className="relative z-10">{children}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatCard({ title, value, subtitle, icon: Icon, color = 'cyan', trend, className }) {
|
||||
const colors = {
|
||||
cyan: { text: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20' },
|
||||
green: { text: 'text-green-400', bg: 'bg-green-500/10', border: 'border-green-500/20' },
|
||||
purple: { text: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/20' },
|
||||
orange: { text: 'text-orange-400', bg: 'bg-orange-500/10', border: 'border-orange-500/20' },
|
||||
red: { text: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/20' },
|
||||
blue: { text: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/20' },
|
||||
};
|
||||
const c = colors[color] ?? colors.cyan;
|
||||
|
||||
return (
|
||||
<GlassCard className={cn('p-5', className)} glow={color === 'cyan' ? 'cyan' : undefined}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium text-white/50 uppercase tracking-wider mb-1">{title}</p>
|
||||
<p className={cn('text-3xl font-bold tracking-tight', c.text)}>{value}</p>
|
||||
{subtitle && <p className="text-xs text-white/40 mt-1">{subtitle}</p>}
|
||||
{trend !== undefined && (
|
||||
<p className={cn('text-xs mt-2', trend >= 0 ? 'text-green-400' : 'text-red-400')}>
|
||||
{trend >= 0 ? '↑' : '↓'} {Math.abs(trend)}% vs last hour
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{Icon && (
|
||||
<div className={cn('p-2.5 rounded-lg border', c.bg, c.border)}>
|
||||
<Icon className={cn('w-5 h-5', c.text)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
);
|
||||
}
|
||||
54
resources/js/Components/ui/Modal.jsx
Normal file
54
resources/js/Components/ui/Modal.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Modal({ isOpen, onClose, title, children, size = 'md', className }) {
|
||||
const sizes = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-2xl',
|
||||
lg: 'max-w-4xl',
|
||||
xl: 'max-w-6xl',
|
||||
full: 'max-w-[95vw]',
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-200" enterFrom="opacity-0" enterTo="opacity-100"
|
||||
leave="ease-in duration-150" leaveFrom="opacity-100" leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-200" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-150" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={cn(
|
||||
'w-full rounded-2xl border border-white/10 bg-[#0a1628] shadow-2xl',
|
||||
sizes[size],
|
||||
className
|
||||
)}>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<Dialog.Title className="text-base font-semibold text-white">{title}</Dialog.Title>
|
||||
<button onClick={onClose} className="p-1 rounded-lg hover:bg-white/10 text-white/50 hover:text-white transition-colors">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">{children}</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
40
resources/js/Components/ui/ProgressBar.jsx
Normal file
40
resources/js/Components/ui/ProgressBar.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function ProgressBar({ value, max = 100, className, showLabel = true, color, size = 'md' }) {
|
||||
const pct = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
|
||||
const autoColor = color ?? (
|
||||
pct >= 90 ? 'red' :
|
||||
pct >= 70 ? 'yellow' :
|
||||
pct >= 50 ? 'cyan' : 'green'
|
||||
);
|
||||
|
||||
const colors = {
|
||||
cyan: { bar: 'bg-cyan-400', glow: 'shadow-[0_0_10px_rgba(0,212,255,0.5)]' },
|
||||
green: { bar: 'bg-green-400', glow: 'shadow-[0_0_10px_rgba(0,255,136,0.5)]' },
|
||||
yellow: { bar: 'bg-yellow-400', glow: 'shadow-[0_0_10px_rgba(255,255,0,0.5)]' },
|
||||
red: { bar: 'bg-red-400', glow: 'shadow-[0_0_10px_rgba(255,45,85,0.5)]' },
|
||||
purple: { bar: 'bg-purple-400', glow: 'shadow-[0_0_10px_rgba(157,78,221,0.5)]' },
|
||||
};
|
||||
const c = colors[autoColor] ?? colors.cyan;
|
||||
const heights = { sm: 'h-1', md: 'h-1.5', lg: 'h-2' };
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-1', className)}>
|
||||
{showLabel && (
|
||||
<div className="flex justify-between text-xs text-white/50">
|
||||
<span>{Math.round(pct)}%</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('w-full bg-white/10 rounded-full overflow-hidden', heights[size])}>
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
className={cn('h-full rounded-full', c.bar, c.glow)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
resources/js/Components/ui/StatusBadge.jsx
Normal file
34
resources/js/Components/ui/StatusBadge.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { cn, getStatusDot } from '@/lib/utils';
|
||||
|
||||
export function StatusBadge({ status, className }) {
|
||||
const variants = {
|
||||
online: 'bg-green-500/15 text-green-400 border-green-500/30',
|
||||
running: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/30',
|
||||
active: 'bg-green-500/15 text-green-400 border-green-500/30',
|
||||
stopped: 'bg-slate-500/15 text-slate-400 border-slate-500/30',
|
||||
offline: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
error: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
warning: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30',
|
||||
unknown: 'bg-gray-500/15 text-gray-400 border-gray-500/30',
|
||||
paused: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30',
|
||||
exited: 'bg-slate-500/15 text-slate-400 border-slate-500/30',
|
||||
success: 'bg-green-500/15 text-green-400 border-green-500/30',
|
||||
failed: 'bg-red-500/15 text-red-400 border-red-500/30',
|
||||
pending: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
|
||||
queued: 'bg-purple-500/15 text-purple-400 border-purple-500/30',
|
||||
};
|
||||
|
||||
const variant = variants[status?.toLowerCase()] ?? variants.unknown;
|
||||
const dot = getStatusDot(status);
|
||||
|
||||
return (
|
||||
<span className={cn(
|
||||
'inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border',
|
||||
variant,
|
||||
className
|
||||
)}>
|
||||
<span className={cn('w-1.5 h-1.5 rounded-full', dot)} />
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user