feat: comprehensive UI polish with responsive design, animations, and modern styling
- Rewrite globals.css with professional utility classes (btn-primary/secondary/danger/ghost, input-field, card/card-hover, badge system, stat-card, skeleton, page-header, animations) - Responsive dashboard layout with mobile hamburger menu, slide-out sidebar, backdrop overlay - Polish login/register pages with gradient backgrounds, slide-up animations, loading spinners - Dashboard home with stat cards grid (2-col mobile, 4-col desktop), skeleton loading, empty states - Apps list with desktop table + mobile card views, proper badges - Deploy wizard with responsive step indicator (checkmarks, progress line), mobile-friendly forms - App detail page with responsive header/action buttons, improved logs tabs, resource scaling - Admin users page with desktop table + mobile cards, skeleton loading - Admin clusters page with responsive layout, animated resource panel - Admin pools page with responsive form grids, improved pool cards - Consistent rounded-2xl cards, rounded-xl buttons/inputs, active:scale feedback throughout
This commit is contained in:
@@ -25,9 +25,9 @@ function ResourcePanel({ clusterId }: { clusterId: string }) {
|
||||
const memUsedPct = memCap > 0 ? ((memCap - memAlloc) / memCap * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 space-y-4">
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 space-y-4 animate-fade-in">
|
||||
{/* Summary cards */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="bg-blue-50 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-blue-700">{data.nodeCount}</div>
|
||||
<div className="text-xs text-blue-600">Nodes</div>
|
||||
@@ -47,7 +47,7 @@ function ResourcePanel({ clusterId }: { clusterId: string }) {
|
||||
</div>
|
||||
|
||||
{/* CPU & Memory bars */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-600">CPU Reserved</span>
|
||||
@@ -83,7 +83,7 @@ function ResourcePanel({ clusterId }: { clusterId: string }) {
|
||||
{/* Nodes table */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-2">Nodes</h4>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<div className="overflow-x-auto -mx-2 px-2 rounded-xl border border-gray-200">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
@@ -194,21 +194,24 @@ export default function AdminClustersPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Cluster Management</h1>
|
||||
<button onClick={() => setShowForm(!showForm)} className="btn-primary">
|
||||
{showForm ? 'Cancel' : '+ Add Cluster'}
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">Cluster Management</h1>
|
||||
<p className="page-subtitle">{clusters.length} cluster{clusters.length !== 1 ? 's' : ''} registered</p>
|
||||
</div>
|
||||
<button onClick={() => setShowForm(!showForm)} className={showForm ? 'btn-ghost' : 'btn-primary'}>
|
||||
{showForm ? '✕ Cancel' : '+ Add Cluster'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<div className="card space-y-4">
|
||||
<h2 className="text-lg font-semibold">Register New Cluster</h2>
|
||||
<div className="card space-y-4 animate-slide-up">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Register New Cluster</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
The system will verify the Kubernetes connection before registering. Only clusters with valid kubeconfig will be marked as active.
|
||||
The system will verify the Kubernetes connection before registering.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input className="input-field" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
@@ -266,62 +269,71 @@ export default function AdminClustersPage() {
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="card text-center py-12 text-gray-500">Loading clusters...</div>
|
||||
<div className="space-y-3">
|
||||
{[1,2].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-8 w-24 rounded-lg" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : clusters.length === 0 ? (
|
||||
<div className="card text-center py-12">
|
||||
<p className="text-gray-500">No clusters registered yet.</p>
|
||||
<div className="card text-center py-16">
|
||||
<div className="text-5xl mb-4">🖥️</div>
|
||||
<p className="text-gray-600 font-medium">No clusters registered yet.</p>
|
||||
<p className="text-gray-400 text-sm mt-1">Add a Kubernetes cluster to start deploying applications.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{clusters.map((cluster) => (
|
||||
<div key={cluster.id} className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
cluster.status === 'active' ? 'bg-green-100' : 'bg-red-100'
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||
<div className={`w-11 h-11 rounded-xl flex items-center justify-center shrink-0 ${
|
||||
cluster.status === 'active' ? 'bg-emerald-50' : 'bg-red-50'
|
||||
}`}>
|
||||
{cluster.status === 'active' ? '✅' : '❌'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-semibold text-gray-900">{cluster.name}</h3>
|
||||
{cluster.isDefault && (
|
||||
<span className="px-2 py-0.5 bg-primary-100 text-primary-700 text-xs rounded-full font-medium">Default</span>
|
||||
<span className="badge badge-blue">Default</span>
|
||||
)}
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
cluster.status === 'active' ? 'bg-green-100 text-green-700'
|
||||
: cluster.status === 'maintenance' ? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
<span className={`badge ${
|
||||
cluster.status === 'active' ? 'badge-green'
|
||||
: cluster.status === 'maintenance' ? 'badge-yellow'
|
||||
: 'badge-red'
|
||||
}`}>
|
||||
{cluster.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{cluster.provider || 'Unknown'} · {cluster.region || 'N/A'} · {cluster.apiServer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
||||
<button
|
||||
onClick={() => toggleResources(cluster.id)}
|
||||
className={`text-sm px-3 py-1.5 rounded-md transition-colors ${
|
||||
expandedResources.has(cluster.id)
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'bg-purple-50 text-purple-600 hover:bg-purple-100'
|
||||
}`}
|
||||
className={`btn-ghost text-sm ${expandedResources.has(cluster.id) ? 'bg-purple-50 text-purple-700' : ''}`}
|
||||
>
|
||||
📊 Resources
|
||||
</button>
|
||||
<button
|
||||
onClick={() => testMutation.mutate(cluster.id)}
|
||||
disabled={testingId === cluster.id}
|
||||
className="text-sm px-3 py-1.5 bg-blue-50 text-blue-700 rounded-md hover:bg-blue-100 transition-colors disabled:opacity-50"
|
||||
className="btn-ghost text-sm disabled:opacity-50"
|
||||
>
|
||||
{testingId === cluster.id ? '🔄 Testing...' : '🔌 Test Connection'}
|
||||
{testingId === cluster.id ? '⏳ Testing...' : '🔌 Test'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (confirm('Remove this cluster?')) deleteMutation.mutate(cluster.id); }}
|
||||
className="text-sm text-red-600 hover:text-red-800"
|
||||
className="text-sm text-red-600 hover:text-red-800 font-medium"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user