This commit is contained in:
Ybehrooz
2025-08-30 20:07:27 +03:30
parent 24e362cdba
commit 4b15476de6

View File

@@ -1,136 +1,232 @@
import { useState } from 'react'
import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import { Download, Feather, Trash2 } from 'lucide-react'
import { Download, Feather, Trash2, Loader2, Cpu } from 'lucide-react'
import type { FormEvent } from 'react'
import { fromTheme } from 'tailwind-merge'
interface Cluster {
id: string
Name: string
ClusterID: string
Status: string
Version: string
HealthCheck: string
ControlPlane: string
PlatformVersion: string
Alert: string
EndPoint: string
Cpu : string
Memory: string
clusterId: string
status: string
version: string
alerts: string
endpoint: string
}
export default function CreateCluster() {
const [clusters, setClusters] = useState<Cluster[]>(() => {
fetch('http://localhost:8082/clusters', {
const [clusters, setClusters] = useState<Cluster[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [showModal, setShowModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [clusterToDelete, setClusterToDelete] = useState<string>('')
const [formData, setFormData] = useState({
clusterName: '',
namespace: '',
controlPlane: 'k8s',
PlatformVersion: 'v1.31.6 (recommended)',
Cpu: 1,
Memory: 1024
})
const pollingIntervalRef = useRef<number | null>(null)
// Function to fetch clusters
const fetchClusters = async () => {
try {
const response = await fetch('http://localhost:8082/clusters', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
}).then(async (res) => {
if (res.ok) {
const data = await res.json()
setClusters(data.clusters || [])
return data.clusters || []
//localStorage.setItem('clusters', JSON.stringify(data.clusters || []))
} else {
const data = await res.json()
console.error(data.message || 'Failed to fetch clusters')
}
}).catch(() => {
console.error('Failed to fetch clusters')
})
const saved = localStorage.getItem('clusters')
if (saved) {
return JSON.parse(saved)
}
return []
})
const [showModal, setShowModal] = useState(false)
const [formData, setFormData] = useState({
if (response.ok) {
const data = await response.json()
setClusters(data || [])
localStorage.setItem('clusters', JSON.stringify(data || []))
// Check if any cluster is still progressing
const hasProgressingClusters = data.some((cluster: Cluster) => cluster.Status === 'Progressing' || cluster.Status === '' || cluster.Status === 'Missing' || cluster.Status === 'Pendding')
// Start or stop polling based on cluster status
if (hasProgressingClusters) {
startPolling()
} else {
stopPolling()
}
} else {
const data = await response.json()
console.error(data.message || 'Failed to fetch clusters')
}
} catch (error) {
console.error('Failed to fetch clusters', error)
}
}
// Start polling for clusters with Progressing status
const startPolling = () => {
if (pollingIntervalRef.current) {
return // Already polling
}
pollingIntervalRef.current = setInterval(() => {
fetchClusters()
}, 3000) // Poll every 3 seconds
}
// Stop polling
const stopPolling = () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current)
pollingIntervalRef.current = null
}
}
// Load clusters on component mount
useEffect(() => {
fetchClusters()
// Cleanup polling on component unmount
return () => {
stopPolling()
}
}, [])
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
setIsLoading(true)
const newCluster = {
Name: formData.clusterName,
ClusterID: Math.random().toString(36).substr(2, 8),
ControlPlane: formData.controlPlane,
Status: 'Progressing',
Cpu: formData.Cpu.toString(),
Memory: formData.Memory.toString(),
PlatformVersion: formData.PlatformVersion.split(' ')[0],
HealthCheck: '',
Alert: '',
EndPoint: ''
}
try {
const response = await fetch('http://localhost:8082/createcluster', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('auth:token') || ''
},
body: JSON.stringify(newCluster),
})
if (response.ok) {
const data = await response.json()
console.log('Cluster created successfully:', data)
// Close modal and reset form
setShowModal(false)
setFormData({
clusterName: '',
namespace: '',
controlPlane: 'Kubernetes (k8s)',
kubernetesVersion: 'v1.31.6 (recommended)',
PlatformVersion: 'v1.31.6 (recommended)',
Cpu: 1,
Memory: 1024
})
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
const newCluster: Cluster = {
Name: formData.clusterName,
clusterId: Math.random().toString(36).substr(2, 8),
status: 'Creating',
ControlPlane: formData.controlPlane,
Cpu: formData.Cpu.toString(),
Memory: formData.Memory.toString(),
PlatformVersion: formData.kubernetesVersion.split(' ')[0]
// Refresh cluster list and start polling
await fetchClusters()
} else {
const data = await response.json()
console.error('Failed to create cluster:', data.message)
// You can add error handling here (show toast notification, etc.)
}
} catch (error) {
console.error('Error creating cluster:', error)
// You can add error handling here (show toast notification, etc.)
} finally {
setIsLoading(false)
}
}
fetch('http://localhost:8082/createcluster', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('auth:token') || '' },
body: JSON.stringify(newCluster),
}).then(async (res) => {
if (res.ok) {
const data = await res.json()
console.log(data)
} else {
const data = await res.json()
console.log(data)
// setError(data.message || 'Login failed')
}
}).catch(() => {
//setError('Login failed')
const downloadKubeconfig = async (clusterName: string) => {
try {
const response = await fetch(`http://localhost:8082/connect?Name=${encodeURIComponent(clusterName)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
})
// const updatedClusters = [...clusters, newCluster]
// setClusters(updatedClusters)
// localStorage.setItem('clusters', JSON.stringify(updatedClusters))
// setShowModal(false)
// setFormData({
// clusterName: '',
// namespace: '',
// controlPlane: 'Kubernetes (k8s)',
// kubernetesVersion: 'v1.31.6 (recommended)',
// Cpu: 1,
// Memory: 2048
// })
}
const downloadKubeconfig = (clusterId: string) => {
const kubeconfig = `apiVersion: v1
kind: Config
clusters:
- name: ${clusterId}
cluster:
server: https://${clusterId}.example.com
contexts:
- name: ${clusterId}
context:
cluster: ${clusterId}
user: admin
current-context: ${clusterId}
users:
- name: admin
user:
token: your-token-here`
if (response.ok) {
const kubeconfig = await response.text()
const blob = new Blob([kubeconfig], { type: 'text/yaml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `kubeconfig-${clusterId}.yaml`
a.download = `kubeconfig-${clusterName}.yaml`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} else {
console.error('Failed to download kubeconfig')
// You can add error handling here (show toast notification, etc.)
}
} catch (error) {
console.error('Error downloading kubeconfig:', error)
// You can add error handling here (show toast notification, etc.)
}
}
const deleteCluster = (clusterId: string) => {
const updatedClusters = clusters.filter(cluster => cluster.clusterId !== clusterId)
setClusters(updatedClusters)
localStorage.setItem('clusters', JSON.stringify(updatedClusters))
const confirmDeleteCluster = (clusterName: string) => {
setClusterToDelete(clusterName)
setShowDeleteModal(true)
}
const deleteCluster = async () => {
if (!clusterToDelete) return
setIsDeleting(true)
setShowDeleteModal(false)
try {
const response = await fetch(`http://localhost:8082/deletecluster?Name=${encodeURIComponent(clusterToDelete)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
})
if (response.ok) {
console.log('Cluster deleted successfully')
// Refresh the cluster list to reflect the deletion and start polling
pollingIntervalRef.current = setInterval(() => {
fetchClusters()
}, 3000) // Poll every 3 seconds
} else {
const data = await response.json()
console.error('Failed to delete cluster:', data.message)
// You can add error handling here (show toast notification, etc.)
}
} catch (error) {
console.error('Error deleting cluster:', error)
// You can add error handling here (show toast notification, etc.)
} finally {
setIsDeleting(false)
setClusterToDelete('')
}
}
return (
@@ -161,44 +257,47 @@ users:
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cluster ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Alerts</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Health Check</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Alert</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Endpoint</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{clusters.map((cluster) => (
<tr key={cluster.id} className="hover:bg-gray-50">
<tr key={cluster.ClusterID} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<Link
to={`/app/clusters/${cluster.id}`}
to={`/app/clusters/${cluster.ClusterID}`}
className="text-sm font-medium text-blue-600 hover:text-blue-900"
>
{cluster.Name}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.clusterId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.ClusterID}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
cluster.status === 'Healthy' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
cluster.Status === 'Healthy' ? 'bg-green-100 text-green-800' :
cluster.Status === 'Progressing' ? 'bg-blue-100 text-blue-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{cluster.status}
{cluster.Status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.version}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.alerts}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.endpoint}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.Version}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.HealthCheck}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.Alert}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.EndPoint}</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center space-x-2">
<button
onClick={() => downloadKubeconfig(cluster.clusterId)}
onClick={() => downloadKubeconfig(cluster.Name)}
className="p-2 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded-md transition-colors"
title="Download kubeconfig"
>
<Download size={16} />
</button>
<button
onClick={() => deleteCluster(cluster.clusterId)}
onClick={() => confirmDeleteCluster(cluster.Name)}
className="p-2 text-red-600 hover:text-red-900 hover:bg-red-50 rounded-md transition-colors"
title="Delete cluster"
>
@@ -216,7 +315,16 @@ users:
{/* Create Cluster Modal */}
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto relative">
{/* Loading Overlay */}
{isLoading && (
<div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-10 rounded-lg">
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
<p className="text-sm text-gray-600">Creating cluster...</p>
</div>
</div>
)}
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold">Create Cluster</h2>
</div>
@@ -234,8 +342,9 @@ users:
type="text"
value={formData.clusterName}
onChange={(e) => setFormData({...formData, clusterName: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
required
disabled={isLoading}
/>
</div>
@@ -253,7 +362,8 @@ users:
<select
value={formData.controlPlane}
onChange={(e) => setFormData({...formData, controlPlane: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
>
<option>Kubernetes (k8s)</option>
</select>
@@ -263,9 +373,10 @@ users:
Kubernetes Version
</label>
<select
value={formData.kubernetesVersion}
onChange={(e) => setFormData({...formData, kubernetesVersion: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={formData.PlatformVersion}
onChange={(e) => setFormData({...formData, PlatformVersion: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
>
<option>v1.31.6 (recommended)</option>
<option>v1.30.0</option>
@@ -289,7 +400,8 @@ users:
max="8"
value={formData.Cpu}
onChange={(e) => setFormData({...formData, Cpu: parseInt(e.target.value)})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
/>
</div>
<div>
@@ -303,7 +415,8 @@ users:
step="1024"
value={formData.Memory}
onChange={(e) => setFormData({...formData, Memory: parseInt(e.target.value)})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
/>
</div>
</div>
@@ -314,21 +427,75 @@ users:
<button
type="button"
onClick={() => setShowModal(false)}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300"
className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading}
>
Cancel
</button>
<button
type="submit"
className="px-6 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600"
disabled={isLoading}
className="px-6 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Create Cluster
{isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
'Create Cluster'
)}
</button>
</div>
</form>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold">Delete Cluster</h2>
</div>
<div className="p-6">
<p className="text-gray-600 mb-6">
Are you sure you want to delete the cluster <span className="font-semibold">"{clusterToDelete}"</span>?
This action cannot be undone.
</p>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => {
setShowDeleteModal(false)
setClusterToDelete('')
}}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300"
>
Cancel
</button>
<button
type="button"
onClick={deleteCluster}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
>
Delete Cluster
</button>
</div>
</div>
</div>
</div>
)}
{/* Global Loading Overlay for Delete */}
{isDeleting && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-8 flex flex-col items-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-red-600" />
<p className="text-sm text-gray-600">Deleting cluster...</p>
</div>
</div>
)}
</div>
)
}