120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
'use client';
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import Link from 'next/link';
|
|
import api from '@/lib/api';
|
|
import { useAuthStore } from '@/lib/store';
|
|
import type { Application } from '@/types';
|
|
|
|
const statusColors: Record<string, string> = {
|
|
running: 'bg-green-100 text-green-700',
|
|
pending: 'bg-yellow-100 text-yellow-700',
|
|
building: 'bg-blue-100 text-blue-700',
|
|
deploying: 'bg-blue-100 text-blue-700',
|
|
failed: 'bg-red-100 text-red-700',
|
|
build_failed: 'bg-red-100 text-red-700',
|
|
stopped: 'bg-gray-100 text-gray-700',
|
|
};
|
|
|
|
export default function DashboardPage() {
|
|
const user = useAuthStore((s) => s.user);
|
|
|
|
const { data: apps = [], isLoading } = useQuery<Application[]>({
|
|
queryKey: ['applications'],
|
|
queryFn: () => api.get('/applications').then((r) => r.data),
|
|
});
|
|
|
|
const runningApps = apps.filter(
|
|
(a) => a.deployments?.some((d) => d.status === 'running'),
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
Welcome back, {user?.firstName}! 👋
|
|
</h1>
|
|
<p className="mt-1 text-gray-500">
|
|
Here's an overview of your applications.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="card">
|
|
<p className="text-sm font-medium text-gray-500">Total Apps</p>
|
|
<p className="mt-2 text-3xl font-bold text-gray-900">{apps.length}</p>
|
|
</div>
|
|
<div className="card">
|
|
<p className="text-sm font-medium text-gray-500">Running</p>
|
|
<p className="mt-2 text-3xl font-bold text-green-600">{runningApps.length}</p>
|
|
</div>
|
|
<div className="card">
|
|
<p className="text-sm font-medium text-gray-500">Deployments (7d)</p>
|
|
<p className="mt-2 text-3xl font-bold text-primary-600">
|
|
{apps.reduce((sum, a) => sum + (a.deployments?.length || 0), 0)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Applications */}
|
|
<div>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Recent Applications</h2>
|
|
<Link href="/dashboard/deploy" className="btn-primary text-sm">
|
|
+ New Application
|
|
</Link>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="card text-center py-12 text-gray-500">Loading...</div>
|
|
) : apps.length === 0 ? (
|
|
<div className="card text-center py-12">
|
|
<p className="text-gray-500 text-lg">No applications yet</p>
|
|
<p className="text-gray-400 mt-2">
|
|
Deploy your first application to get started.
|
|
</p>
|
|
<Link href="/dashboard/deploy" className="btn-primary mt-4 inline-block">
|
|
Deploy your first app
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{apps.slice(0, 5).map((app) => {
|
|
const latestDeploy = app.deployments?.[0];
|
|
const status = latestDeploy?.status || 'pending';
|
|
return (
|
|
<Link
|
|
key={app.id}
|
|
href={`/dashboard/apps/${app.id}`}
|
|
className="card hover:border-primary-300 transition-colors flex items-center justify-between"
|
|
>
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-10 h-10 rounded-lg bg-primary-100 flex items-center justify-center text-lg">
|
|
{app.runtime === 'nodejs' ? '🟩' : '🟧'}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">{app.name}</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{app.runtime} · {app.replicas} replica{app.replicas > 1 ? 's' : ''}
|
|
{app.databaseType !== 'none' && ` · ${app.databaseType}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span
|
|
className={`px-3 py-1 rounded-full text-xs font-medium ${
|
|
statusColors[status] || 'bg-gray-100 text-gray-600'
|
|
}`}
|
|
>
|
|
{status}
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|