init
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import type { Cluster } from '@/types';
|
||||
|
||||
export default function AdminClustersPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [testingId, setTestingId] = useState<string | null>(null);
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
apiServer: '',
|
||||
kubeconfig: '',
|
||||
region: '',
|
||||
provider: '',
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
const { data: clusters = [], isLoading } = useQuery<Cluster[]>({
|
||||
queryKey: ['admin-clusters'],
|
||||
queryFn: () => api.get('/clusters').then((r) => r.data),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: typeof form) => api.post('/clusters', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-clusters'] });
|
||||
toast.success('Cluster added & connection verified ✓');
|
||||
setShowForm(false);
|
||||
setForm({ name: '', description: '', apiServer: '', kubeconfig: '', region: '', provider: '', isDefault: false });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
const message = err?.response?.data?.message || 'Failed to add cluster';
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const testMutation = useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
setTestingId(id);
|
||||
return api.post(`/clusters/${id}/test`);
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-clusters'] });
|
||||
const data = res.data;
|
||||
if (data.connected) {
|
||||
toast.success(`Connection OK — Kubernetes ${data.version}`);
|
||||
} else {
|
||||
toast.error(`Connection failed: ${data.error}`);
|
||||
}
|
||||
setTestingId(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to test connection');
|
||||
setTestingId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/clusters/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-clusters'] });
|
||||
toast.success('Cluster removed');
|
||||
},
|
||||
});
|
||||
|
||||
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'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<div className="card space-y-4">
|
||||
<h2 className="text-lg font-semibold">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.
|
||||
</p>
|
||||
<div className="grid 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 })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">API Server URL</label>
|
||||
<input className="input-field" placeholder="https://k8s-api:6443" value={form.apiServer} onChange={(e) => setForm({ ...form, apiServer: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Region</label>
|
||||
<input className="input-field" placeholder="us-east-1" value={form.region} onChange={(e) => setForm({ ...form, region: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Provider</label>
|
||||
<select className="input-field" value={form.provider} onChange={(e) => setForm({ ...form, provider: e.target.value })}>
|
||||
<option value="">Select provider</option>
|
||||
<option value="aws">AWS (EKS)</option>
|
||||
<option value="gcp">GCP (GKE)</option>
|
||||
<option value="azure">Azure (AKS)</option>
|
||||
<option value="bare-metal">Bare Metal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<input className="input-field" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kubeconfig (YAML)</label>
|
||||
<textarea
|
||||
className="input-field font-mono text-xs"
|
||||
rows={8}
|
||||
placeholder="Paste your kubeconfig here..."
|
||||
value={form.kubeconfig}
|
||||
onChange={(e) => setForm({ ...form, kubeconfig: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isDefault"
|
||||
checked={form.isDefault}
|
||||
onChange={(e) => setForm({ ...form, isDefault: e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="isDefault" className="text-sm text-gray-700">Set as default cluster</label>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => createMutation.mutate(form)}
|
||||
disabled={!form.name || !form.apiServer || !form.kubeconfig || createMutation.isPending}
|
||||
className="btn-primary"
|
||||
>
|
||||
{createMutation.isPending ? '🔄 Verifying connection & adding...' : 'Add Cluster'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="card text-center py-12 text-gray-500">Loading clusters...</div>
|
||||
) : clusters.length === 0 ? (
|
||||
<div className="card text-center py-12">
|
||||
<p className="text-gray-500">No clusters registered yet.</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'
|
||||
}`}>
|
||||
{cluster.status === 'active' ? '✅' : '❌'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<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={`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'
|
||||
}`}>
|
||||
{cluster.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{cluster.provider || 'Unknown'} · {cluster.region || 'N/A'} · {cluster.apiServer}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<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"
|
||||
>
|
||||
{testingId === cluster.id ? '🔄 Testing...' : '🔌 Test Connection'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (confirm('Remove this cluster?')) deleteMutation.mutate(cluster.id); }}
|
||||
className="text-sm text-red-600 hover:text-red-800"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user