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 { 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 type { FormEvent } from 'react'
import { fromTheme } from 'tailwind-merge'
interface Cluster { interface Cluster {
id: string
Name: string Name: string
ClusterID: string
Status: string
Version: string
HealthCheck: string
ControlPlane: string ControlPlane: string
PlatformVersion: string PlatformVersion: string
Cpu: string Alert: string
EndPoint: string
Cpu : string
Memory: string Memory: string
clusterId: string
status: string
version: string
alerts: string
endpoint: string
} }
export default function CreateCluster() { export default function CreateCluster() {
const [clusters, setClusters] = useState<Cluster[]>(() => { const [clusters, setClusters] = useState<Cluster[]>([])
fetch('http://localhost:8082/clusters', { const [isLoading, setIsLoading] = useState(false)
method: 'GET', const [isDeleting, setIsDeleting] = useState(false)
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 [showModal, setShowModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [clusterToDelete, setClusterToDelete] = useState<string>('')
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
clusterName: '', clusterName: '',
namespace: '', namespace: '',
controlPlane: 'Kubernetes (k8s)', controlPlane: 'k8s',
kubernetesVersion: 'v1.31.6 (recommended)', PlatformVersion: 'v1.31.6 (recommended)',
Cpu: 1, Cpu: 1,
Memory: 1024 Memory: 1024
}) })
const handleSubmit = (e: FormEvent) => { const pollingIntervalRef = useRef<number | null>(null)
e.preventDefault()
const newCluster: Cluster = { // Function to fetch clusters
Name: formData.clusterName, const fetchClusters = async () => {
clusterId: Math.random().toString(36).substr(2, 8), try {
status: 'Creating', const response = await fetch('http://localhost:8082/clusters', {
ControlPlane: formData.controlPlane, method: 'GET',
Cpu: formData.Cpu.toString(), headers: {
Memory: formData.Memory.toString(), 'Content-Type': 'application/json',
PlatformVersion: formData.kubernetesVersion.split(' ')[0] 'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
})
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
} }
fetch('http://localhost:8082/createcluster', { pollingIntervalRef.current = setInterval(() => {
method: 'POST', fetchClusters()
headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('auth:token') || '' }, }, 3000) // Poll every 3 seconds
body: JSON.stringify(newCluster), }
}).then(async (res) => {
if (res.ok) { // Stop polling
const data = await res.json() const stopPolling = () => {
console.log(data) 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)',
PlatformVersion: 'v1.31.6 (recommended)',
Cpu: 1,
Memory: 1024
})
// Refresh cluster list and start polling
await fetchClusters()
} else { } else {
const data = await res.json() const data = await response.json()
console.log(data) console.error('Failed to create cluster:', data.message)
// setError(data.message || 'Login failed') // You can add error handling here (show toast notification, etc.)
} }
}).catch(() => { } catch (error) {
//setError('Login failed') console.error('Error creating cluster:', error)
}) // You can add error handling here (show toast notification, etc.)
} finally {
// const updatedClusters = [...clusters, newCluster] setIsLoading(false)
// 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 downloadKubeconfig = async (clusterName: string) => {
const kubeconfig = `apiVersion: v1 try {
kind: Config const response = await fetch(`http://localhost:8082/connect?Name=${encodeURIComponent(clusterName)}`, {
clusters: method: 'GET',
- name: ${clusterId} headers: {
cluster: 'Content-Type': 'application/json',
server: https://${clusterId}.example.com 'Authorization': `${localStorage.getItem('auth:token') || ''}`
contexts: },
- name: ${clusterId} })
context:
cluster: ${clusterId}
user: admin
current-context: ${clusterId}
users:
- name: admin
user:
token: your-token-here`
const blob = new Blob([kubeconfig], { type: 'text/yaml' }) if (response.ok) {
const url = URL.createObjectURL(blob) const kubeconfig = await response.text()
const a = document.createElement('a')
a.href = url const blob = new Blob([kubeconfig], { type: 'text/yaml' })
a.download = `kubeconfig-${clusterId}.yaml` const url = URL.createObjectURL(blob)
document.body.appendChild(a) const a = document.createElement('a')
a.click() a.href = url
document.body.removeChild(a) a.download = `kubeconfig-${clusterName}.yaml`
URL.revokeObjectURL(url) 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 confirmDeleteCluster = (clusterName: string) => {
const updatedClusters = clusters.filter(cluster => cluster.clusterId !== clusterId) setClusterToDelete(clusterName)
setClusters(updatedClusters) setShowDeleteModal(true)
localStorage.setItem('clusters', JSON.stringify(updatedClusters)) }
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 ( return (
@@ -161,49 +257,52 @@ 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">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">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">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">Endpoint</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{clusters.map((cluster) => ( {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"> <td className="px-6 py-4 whitespace-nowrap">
<Link <Link
to={`/app/clusters/${cluster.id}`} to={`/app/clusters/${cluster.ClusterID}`}
className="text-sm font-medium text-blue-600 hover:text-blue-900" className="text-sm font-medium text-blue-600 hover:text-blue-900"
> >
{cluster.Name} {cluster.Name}
</Link> </Link>
</td> </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"> <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 ${ <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} }`}>
</span> {cluster.Status}
</td> </span>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cluster.version}</td> </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.Version}</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.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"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <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" className="p-2 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded-md transition-colors"
title="Download kubeconfig" title="Download kubeconfig"
> >
<Download size={16} /> <Download size={16} />
</button> </button>
<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" className="p-2 text-red-600 hover:text-red-900 hover:bg-red-50 rounded-md transition-colors"
title="Delete cluster" title="Delete cluster"
> >
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -216,7 +315,16 @@ users:
{/* Create Cluster Modal */} {/* Create Cluster Modal */}
{showModal && ( {showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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"> <div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold">Create Cluster</h2> <h2 className="text-xl font-semibold">Create Cluster</h2>
</div> </div>
@@ -234,8 +342,9 @@ users:
type="text" type="text"
value={formData.clusterName} value={formData.clusterName}
onChange={(e) => setFormData({...formData, clusterName: e.target.value})} 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 required
disabled={isLoading}
/> />
</div> </div>
@@ -253,7 +362,8 @@ users:
<select <select
value={formData.controlPlane} value={formData.controlPlane}
onChange={(e) => setFormData({...formData, controlPlane: e.target.value})} 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> <option>Kubernetes (k8s)</option>
</select> </select>
@@ -263,9 +373,10 @@ users:
Kubernetes Version Kubernetes Version
</label> </label>
<select <select
value={formData.kubernetesVersion} value={formData.PlatformVersion}
onChange={(e) => setFormData({...formData, kubernetesVersion: e.target.value})} 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" 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.31.6 (recommended)</option>
<option>v1.30.0</option> <option>v1.30.0</option>
@@ -289,7 +400,8 @@ users:
max="8" max="8"
value={formData.Cpu} value={formData.Cpu}
onChange={(e) => setFormData({...formData, Cpu: parseInt(e.target.value)})} 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>
<div> <div>
@@ -303,7 +415,8 @@ users:
step="1024" step="1024"
value={formData.Memory} value={formData.Memory}
onChange={(e) => setFormData({...formData, Memory: parseInt(e.target.value)})} 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>
</div> </div>
@@ -314,21 +427,75 @@ users:
<button <button
type="button" type="button"
onClick={() => setShowModal(false)} 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 Cancel
</button> </button>
<button <button
type="submit" 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> </button>
</div> </div>
</form> </form>
</div> </div>
</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> </div>
) )
} }