Files
Infra-Management/resources/js/Pages/Container/Index.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

132 lines
7.1 KiB
JavaScript

import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Box, Play, Square, RotateCcw, Trash2, Terminal, BarChart2, Plus } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { useState } from 'react';
import { cn } from '@/lib/utils';
export default function ContainerIndex({ containers, endpoints }) {
const [actionLoading, setActionLoading] = useState(null);
const [selectedEndpoint, setSelectedEndpoint] = useState(null);
const action = (containerId, act) => {
setActionLoading(`${containerId}-${act}`);
router.post(`/containers/${containerId}/action`, { action: act }, {
onFinish: () => setActionLoading(null),
});
};
const filtered = selectedEndpoint
? (containers.data ?? []).filter(c => c.endpoint_id === selectedEndpoint)
: (containers.data ?? []);
const columns = [
{ header: 'Container', accessor: 'name', render: row => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-500/10 border border-blue-500/20 flex items-center justify-center">
<Box className="w-4 h-4 text-blue-400" />
</div>
<div>
<p className="font-medium text-white font-mono text-sm">{row.name}</p>
<p className="text-xs text-white/30 truncate max-w-48">{row.image}</p>
</div>
</div>
)},
{ header: 'Status', accessor: 'state', render: row => <StatusBadge status={row.state} /> },
{ header: 'Ports', key: 'ports', render: row => (
<div className="flex flex-wrap gap-1">
{(row.ports ?? []).slice(0, 3).map((p, i) => (
<span key={i} className="font-mono text-xs px-1.5 py-0.5 rounded bg-white/5 text-white/50">
{p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}` : p.PrivatePort}
</span>
))}
{(row.ports ?? []).length > 3 && <span className="text-xs text-white/30">+{row.ports.length - 3}</span>}
</div>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
{row.state === 'running' ? (
<>
<button onClick={() => action(row.id, 'stop')} disabled={actionLoading?.startsWith(row.id)}
className="p-1.5 rounded-lg hover:bg-red-500/20 text-white/40 hover:text-red-400 transition-colors" title="Stop">
<Square className="w-3.5 h-3.5" />
</button>
<button onClick={() => action(row.id, 'restart')} disabled={actionLoading?.startsWith(row.id)}
className="p-1.5 rounded-lg hover:bg-yellow-500/20 text-white/40 hover:text-yellow-400 transition-colors" title="Restart">
<RotateCcw className="w-3.5 h-3.5" />
</button>
</>
) : (
<button onClick={() => action(row.id, 'start')} disabled={actionLoading?.startsWith(row.id)}
className="p-1.5 rounded-lg hover:bg-green-500/20 text-white/40 hover:text-green-400 transition-colors" title="Start">
<Play className="w-3.5 h-3.5" />
</button>
)}
<a href={`/containers/${row.id}/logs`} className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-white transition-colors" title="Logs">
<Terminal className="w-3.5 h-3.5" />
</a>
<a href={`/containers/${row.id}/stats`} className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-white transition-colors" title="Stats">
<BarChart2 className="w-3.5 h-3.5" />
</a>
</div>
)},
];
const running = (containers.data ?? []).filter(c => c.state === 'running').length;
const stopped = (containers.data ?? []).filter(c => c.state !== 'running').length;
return (
<AppLayout title="Containers">
<Head title="Containers" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Containers</h2>
<p className="text-sm text-white/40">Manage Docker containers across all endpoints</p>
</div>
<div className="flex gap-2">
<a href="/stacks"><Button variant="secondary">Stacks</Button></a>
<a href="/stacks/create"><Button icon={Plus}>Deploy Stack</Button></a>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Endpoints" value={endpoints?.length ?? 0} color="cyan" />
<StatCard title="Total" value={containers.total ?? 0} color="blue" />
<StatCard title="Running" value={running} color="green" />
<StatCard title="Stopped" value={stopped} color="red" />
</div>
{/* Endpoint filter tabs */}
{endpoints?.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<button onClick={() => setSelectedEndpoint(null)}
className={cn('px-3 py-1.5 rounded-lg text-xs font-medium border transition-all',
!selectedEndpoint ? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400' : 'border-white/10 text-white/40 hover:text-white hover:border-white/20'
)}>All Endpoints</button>
{endpoints.map(e => (
<button key={e.id} onClick={() => setSelectedEndpoint(e.id)}
className={cn('px-3 py-1.5 rounded-lg text-xs font-medium border transition-all',
selectedEndpoint === e.id ? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400' : 'border-white/10 text-white/40 hover:text-white hover:border-white/20'
)}>{e.name}</button>
))}
</div>
)}
<GlassCard className="p-5">
<DataTable
columns={columns}
data={filtered}
onRowClick={row => router.visit(`/containers/${row.id}`)}
emptyMessage="No containers found. Add a Docker endpoint to get started."
/>
</GlassCard>
</div>
</AppLayout>
);
}