- 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.
132 lines
7.1 KiB
JavaScript
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>
|
|
);
|
|
}
|