c411c5873e
Backend: - Add WORDPRESS to AppRuntime enum - Add wordpressDockerfile() using wordpress:6-php8.3-apache base image with custom theme/plugin/wp-content merge support - Add WordPress-specific K8s env vars (WORDPRESS_DB_HOST, WORDPRESS_DB_USER, WORDPRESS_DB_PASSWORD, WORDPRESS_DB_NAME, WORDPRESS_TABLE_PREFIX) - Create wp-content PersistentVolumeClaim (2Gi) for WordPress deployments - Mount wp-content PVC in deployment container at /var/www/html/wp-content Frontend: - Add 'wordpress' to runtime type unions (Application, CreateApplicationDto) - Add WordPress runtime card in deploy page (port 80, blue icon) - Auto-select MySQL database when WordPress is chosen, disable other DB options - Show Persian hint 'وردپرس به MySQL نیاز دارد' when WordPress selected - Update all runtime icon colors across dashboard, apps, admin/apps pages to show blue-600 for WordPress
161 lines
6.4 KiB
TypeScript
161 lines
6.4 KiB
TypeScript
'use client';
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import Link from 'next/link';
|
|
import type { ReactNode } from 'react';
|
|
import api from '@/lib/api';
|
|
import { useAuthStore } from '@/lib/store';
|
|
import type { Application } from '@/types';
|
|
import { Rocket, Package, Circle, Hexagon } from 'lucide-react';
|
|
|
|
const statusColors: Record<string, string> = {
|
|
running: 'bg-emerald-100 text-emerald-700',
|
|
pending: 'bg-amber-100 text-amber-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-600',
|
|
};
|
|
|
|
const statusIcons: Record<string, ReactNode> = {
|
|
running: <Circle className="w-3 h-3 fill-emerald-500 text-emerald-500" />,
|
|
pending: <Circle className="w-3 h-3 fill-amber-500 text-amber-500" />,
|
|
building: <Circle className="w-3 h-3 fill-blue-500 text-blue-500" />,
|
|
deploying: <Circle className="w-3 h-3 fill-blue-500 text-blue-500" />,
|
|
failed: <Circle className="w-3 h-3 fill-red-500 text-red-500" />,
|
|
build_failed: <Circle className="w-3 h-3 fill-red-500 text-red-500" />,
|
|
stopped: <Circle className="w-3 h-3 fill-gray-400 text-gray-400" />,
|
|
};
|
|
|
|
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'),
|
|
);
|
|
const failedApps = apps.filter(
|
|
(a) => a.deployments?.some((d) => d.status === 'failed' || d.status === 'build_failed'),
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Welcome */}
|
|
<div>
|
|
<h1 className="page-title">
|
|
Welcome back, {user?.firstName}!
|
|
</h1>
|
|
<p className="page-subtitle">
|
|
Here's an overview of your applications.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Apps</div>
|
|
<div className="stat-value text-gray-900">{isLoading ? '—' : apps.length}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Running</div>
|
|
<div className="stat-value text-emerald-600">{isLoading ? '—' : runningApps.length}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Failed</div>
|
|
<div className="stat-value text-red-600">{isLoading ? '—' : failedApps.length}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Deploys</div>
|
|
<div className="stat-value text-primary-600">
|
|
{isLoading ? '—' : apps.reduce((sum, a) => sum + (a.deployments?.length || 0), 0)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Applications */}
|
|
<div>
|
|
<div className="page-header mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Recent Applications</h2>
|
|
<Link href="/dashboard/deploy" className="btn-primary text-sm">
|
|
<Rocket className="w-4 h-4 mr-1 inline" /> New Application
|
|
</Link>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="space-y-3">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="card flex items-center gap-4">
|
|
<div className="skeleton w-11 h-11 rounded-xl" />
|
|
<div className="flex-1 space-y-2">
|
|
<div className="skeleton h-4 w-40" />
|
|
<div className="skeleton h-3 w-64" />
|
|
</div>
|
|
<div className="skeleton h-6 w-16 rounded-full" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : apps.length === 0 ? (
|
|
<div className="card text-center py-16">
|
|
<Package className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
|
<p className="text-gray-600 text-lg font-medium">No applications yet</p>
|
|
<p className="text-gray-400 mt-1 text-sm">
|
|
Deploy your first application to get started.
|
|
</p>
|
|
<Link href="/dashboard/deploy" className="btn-primary mt-6 inline-flex items-center gap-1">
|
|
<Rocket className="w-4 h-4" /> Deploy your first app
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-3">
|
|
{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 flex items-center justify-between group"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-11 h-11 rounded-xl bg-primary-50 flex items-center justify-center shrink-0">
|
|
<Hexagon className={`w-5 h-5 ${app.runtime === 'nodejs' ? 'text-green-600' : app.runtime === 'wordpress' ? 'text-blue-600' : 'text-orange-500'}`} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<h3 className="font-semibold text-gray-900 group-hover:text-primary-700 transition-colors truncate">
|
|
{app.name}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 truncate">
|
|
{app.runtime} · {app.replicas} replica{app.replicas > 1 ? 's' : ''}
|
|
{app.databaseType !== 'none' && ` · ${app.databaseType}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{statusIcons[status]}
|
|
<span className={`badge ${statusColors[status] || 'badge-gray'}`}>
|
|
{status}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
{apps.length > 5 && (
|
|
<Link
|
|
href="/dashboard/apps"
|
|
className="text-center text-sm text-primary-600 hover:text-primary-700 font-medium py-3"
|
|
>
|
|
View all {apps.length} applications →
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|