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:
150
resources/js/Pages/Cloud/Index.jsx
Normal file
150
resources/js/Pages/Cloud/Index.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import AppLayout from '@/Layouts/AppLayout';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Cloud, Plus, RefreshCw, Server, HardDrive, Database, Globe } 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 { timeAgo } from '@/lib/utils';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
|
||||
const PROVIDERS = {
|
||||
aws: { label: 'Amazon Web Services', color: '#FF9900', icon: '☁' },
|
||||
alibaba: { label: 'Alibaba Cloud', color: '#FF6A00', icon: '☁' },
|
||||
tencent: { label: 'Tencent Cloud', color: '#00A3FF', icon: '☁' },
|
||||
gcp: { label: 'Google Cloud', color: '#4285F4', icon: '☁' },
|
||||
azure: { label: 'Microsoft Azure', color: '#0078D4', icon: '☁' },
|
||||
digitalocean: { label: 'DigitalOcean', color: '#0080FF', icon: '☁' },
|
||||
vultr: { label: 'Vultr', color: '#007BFC', icon: '☁' },
|
||||
hetzner: { label: 'Hetzner', color: '#D50C2D', icon: '☁' },
|
||||
};
|
||||
|
||||
const RESOURCE_ICONS = { ec2: Server, s3: HardDrive, rds: Database, vpc: Globe };
|
||||
|
||||
export default function CloudIndex({ accounts, resources, stats }) {
|
||||
const [syncLoading, setSyncLoading] = useState(null);
|
||||
const [selectedAccount, setSelectedAccount] = useState(null);
|
||||
|
||||
const sync = (id) => {
|
||||
setSyncLoading(id);
|
||||
router.post(`/cloud/accounts/${id}/sync`, {}, { onFinish: () => setSyncLoading(null) });
|
||||
};
|
||||
|
||||
const accountColumns = [
|
||||
{ header: 'Account', accessor: 'name', render: row => {
|
||||
const p = PROVIDERS[row.provider] ?? {};
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-base font-bold"
|
||||
style={{ color: p.color }}>{row.provider?.toUpperCase().slice(0, 2)}</div>
|
||||
<div>
|
||||
<p className="font-medium text-white">{row.name}</p>
|
||||
<p className="text-xs" style={{ color: p.color ?? '#fff' }}>{p.label ?? row.provider}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}},
|
||||
{ header: 'Account ID', accessor: 'account_id', render: row => (
|
||||
<span className="font-mono text-xs text-white/50">{row.account_id ?? '—'}</span>
|
||||
)},
|
||||
{ header: 'Region', accessor: 'region', render: row => (
|
||||
<span className="text-xs text-white/60">{row.region ?? '—'}</span>
|
||||
)},
|
||||
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
|
||||
{ header: 'Last Sync', accessor: 'last_sync_at', render: row => (
|
||||
<span className="text-xs text-white/40">{timeAgo(row.last_sync_at)}</span>
|
||||
)},
|
||||
{ header: '', key: 'actions', render: row => (
|
||||
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
|
||||
<button onClick={() => sync(row.id)} disabled={syncLoading === row.id}
|
||||
className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-cyan-400 transition-colors">
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${syncLoading === row.id ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
)},
|
||||
];
|
||||
|
||||
const resourceColumns = [
|
||||
{ header: 'Resource', accessor: 'name', render: row => {
|
||||
const Icon = RESOURCE_ICONS[row.resource_type] ?? Cloud;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="w-4 h-4 text-white/40" />
|
||||
<div>
|
||||
<p className="text-sm text-white">{row.name}</p>
|
||||
<p className="font-mono text-xs text-white/30">{row.resource_id}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}},
|
||||
{ header: 'Type', accessor: 'resource_type', render: row => (
|
||||
<span className="font-mono text-xs px-2 py-0.5 rounded bg-white/5 text-white/60">{row.resource_type}</span>
|
||||
)},
|
||||
{ header: 'Region', accessor: 'region', render: row => (
|
||||
<span className="text-xs text-white/60">{row.region}</span>
|
||||
)},
|
||||
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
|
||||
{ header: 'Cost/mo', accessor: 'monthly_cost', render: row => (
|
||||
row.monthly_cost ? <span className="text-xs text-green-400 font-mono">${row.monthly_cost}</span> : <span className="text-xs text-white/20">—</span>
|
||||
)},
|
||||
];
|
||||
|
||||
return (
|
||||
<AppLayout title="Cloud">
|
||||
<Head title="Cloud Management" />
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Cloud Management</h2>
|
||||
<p className="text-sm text-white/40">AWS, Alibaba, Tencent, GCP, Azure and more</p>
|
||||
</div>
|
||||
<a href="/cloud/accounts/create">
|
||||
<Button icon={Plus}>Add Account</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Provider cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{Object.entries(PROVIDERS).slice(0, 4).map(([key, p]) => {
|
||||
const count = accounts?.filter(a => a.provider === key).length ?? 0;
|
||||
return (
|
||||
<GlassCard key={key} className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-2xl font-bold" style={{ color: p.color }}>
|
||||
{key.toUpperCase().slice(0, 2)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white font-medium">{key.toUpperCase()}</p>
|
||||
<p className="text-xs text-white/40">{count} account{count !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Accounts */}
|
||||
<GlassCard className="p-5">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">Cloud Accounts</h3>
|
||||
<DataTable
|
||||
columns={accountColumns}
|
||||
data={accounts ?? []}
|
||||
onRowClick={row => router.visit(`/cloud/accounts/${row.id}`)}
|
||||
emptyMessage="No cloud accounts. Add your first account to get started."
|
||||
/>
|
||||
</GlassCard>
|
||||
|
||||
{/* Resources */}
|
||||
<GlassCard className="p-5">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">Cloud Resources</h3>
|
||||
<DataTable
|
||||
columns={resourceColumns}
|
||||
data={resources?.data ?? []}
|
||||
emptyMessage="No resources synced yet. Sync a cloud account to see resources."
|
||||
/>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user