Files
vclusterfront/src/pages/Pods.tsx
2025-09-27 17:02:34 +03:30

791 lines
30 KiB
TypeScript

import { useEffect, useState, useRef } from "react"
interface Pods {
Name: string
Namespace: string
Status: string
Restart: number
Age: string
Ready: string
Restarts: string
Ip: string
Node: string
Image: string
}
interface Namespace {
Name: string
Status: string
Age: string
}
export default function Pods() {
const [pods, setPods] = useState<Pods[]>([])
const [isLoadingPods, setIsLoadingPods] = useState<boolean>(false)
const [namespaces, setNamespaces] = useState<Namespace[]>([])
const [selectedNamespace, setSelectedNamespace] = useState<string>('')
const [clusterName, setClusterName] = useState<string>('')
const [isLogsModalOpen, setIsLogsModalOpen] = useState<boolean>(false)
const [selectedPod, setSelectedPod] = useState<Pods | null>(null)
const [podLogs, setPodLogs] = useState<string>('')
const [isLoadingLogs, setIsLoadingLogs] = useState<boolean>(false)
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false)
const [selectedPodForManifest, setSelectedPodForManifest] = useState<Pods | null>(null)
const [podManifest, setPodManifest] = useState<string>('')
const [isLoadingManifest, setIsLoadingManifest] = useState<boolean>(false)
const [isCreateSidebarOpen, setIsCreateSidebarOpen] = useState<boolean>(false)
const [createPodManifest, setCreatePodManifest] = useState<string>('')
const [isCreatingPod, setIsCreatingPod] = useState<boolean>(false)
const [showDeletePodModal, setShowDeletePodModal] = useState<boolean>(false)
const [podToDelete, setPodToDelete] = useState<Pods | null>(null)
const [isDeletingPod, setIsDeletingPod] = useState<boolean>(false)
// Refs to track request states more reliably
const isCreatingPodRef = useRef<boolean>(false)
const isLoadingLogsRef = useRef<boolean>(false)
const isLoadingManifestRef = useRef<boolean>(false)
const deletePollIntervalRef = useRef<number | null>(null)
// Encode a string to Base64 with UTF-8 safety
const encodeBase64Utf8 = (input: string): string => {
try {
return btoa(unescape(encodeURIComponent(input)))
} catch (_err) {
// Fallback using TextEncoder for very large inputs
const bytes = new TextEncoder().encode(input)
let binary = ''
const chunkSize = 0x8000 // 32KB chunks to avoid call stack limits
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize)
binary += String.fromCharCode.apply(null, Array.from(chunk))
}
return btoa(binary)
}
}
const fetchNamespaces = async () => {
if (!clusterName) return
try {
const response = await fetch(`http://localhost:8082/cluster_namespaces?Name=${encodeURIComponent(clusterName)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
})
if (response.ok) {
const data = await response.json()
setNamespaces(data || [])
// Set default namespace to first one if available
if (data && data.length > 0 && !selectedNamespace) {
setSelectedNamespace(data[0].Name)
}
} else {
const data = await response.json()
console.error(data.message || 'Failed to fetch namespaces')
}
} catch (error) {
console.error('Failed to fetch namespaces', error)
}
}
const fetchPods = async (namespace?: string) => {
if (!clusterName) return
try {
setIsLoadingPods(true)
const url = namespace
? `http://localhost:8082/cluster_pods?Name=${encodeURIComponent(clusterName)}&Namespace=${encodeURIComponent(namespace)}`
: `http://localhost:8082/cluster_pods?Name=${encodeURIComponent(clusterName)}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
})
if (response.ok) {
const data = await response.json()
setPods(data || [])
} else {
const data = await response.json()
console.error(data.message || 'Failed to fetch pods')
}
} catch (error) {
console.error('Failed to fetch pods', error)
} finally {
setIsLoadingPods(false)
}
}
const fetchLogs = async (pod: Pods) => {
// Prevent duplicate requests using ref
if (isLoadingLogsRef.current) {
console.log('Logs already loading, ignoring duplicate request')
return
}
isLoadingLogsRef.current = true
setIsLoadingLogs(true)
try {
const response = await fetch('http://localhost:8082/pod_logs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
body: JSON.stringify({
Clustername: clusterName,
Namespace: pod.Namespace,
Podname: pod.Name
})
})
if (response.ok) {
const data = await response.json()
setPodLogs(data)
} else {
const data = await response.json()
console.error(data.message || 'Failed to fetch pod logs')
setPodLogs('Failed to fetch logs: ' + (data.message || 'Unknown error'))
}
} catch (error) {
console.error('Failed to fetch pod logs', error)
setPodLogs('Failed to fetch logs: ' + error)
} finally {
isLoadingLogsRef.current = false
setIsLoadingLogs(false)
}
}
const handleNamespaceChange = (namespace: string) => {
setSelectedNamespace(namespace)
fetchPods(namespace)
}
const handleLogsClick = (pod: Pods) => {
// Prevent duplicate clicks using ref
if (isLoadingLogsRef.current) return
setSelectedPod(pod)
setIsLogsModalOpen(true)
fetchLogs(pod)
}
const fetchManifest = async (pod: Pods) => {
// Prevent duplicate requests using ref
if (isLoadingManifestRef.current) {
console.log('Manifest already loading, ignoring duplicate request')
return
}
isLoadingManifestRef.current = true
setIsLoadingManifest(true)
try {
const response = await fetch('http://localhost:8082/pod_manifest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
body: JSON.stringify({
Clustername: clusterName,
Namespace: pod.Namespace,
Podname: pod.Name
})
})
if (response.ok) {
const data = await response.json()
// If the response is already a string (YAML), use it directly
// Otherwise, format it as JSON
if (typeof data === 'string') {
// Convert \n to actual line breaks for better formatting
setPodManifest(data.replace(/\\n/g, '\n'))
} else {
setPodManifest(JSON.stringify(data, null, 2))
}
} else {
const data = await response.json()
console.error(data.message || 'Failed to fetch pod manifest')
setPodManifest('Failed to fetch manifest: ' + (data.message || 'Unknown error'))
}
} catch (error) {
console.error('Failed to fetch pod manifest', error)
setPodManifest('Failed to fetch manifest: ' + error)
} finally {
isLoadingManifestRef.current = false
setIsLoadingManifest(false)
}
}
const closeLogsModal = () => {
setIsLogsModalOpen(false)
setSelectedPod(null)
setPodLogs('')
}
const handleViewClick = (pod: Pods) => {
// Prevent duplicate clicks using ref
if (isLoadingManifestRef.current) return
setSelectedPodForManifest(pod)
setIsSidebarOpen(true)
fetchManifest(pod)
}
const closeSidebar = () => {
setIsSidebarOpen(false)
setSelectedPodForManifest(null)
setPodManifest('')
}
const confirmDeletePod = (pod: Pods) => {
setPodToDelete(pod)
setShowDeletePodModal(true)
}
const closeDeletePodModal = () => {
setShowDeletePodModal(false)
setPodToDelete(null)
}
const deletePod = async () => {
if (!podToDelete || !clusterName) return
setIsDeletingPod(true)
try {
const url = `http://localhost:8082/pod_delete?Name=${encodeURIComponent(clusterName)}&Namespace=${encodeURIComponent(podToDelete.Namespace)}&Pod=${encodeURIComponent(podToDelete.Name)}`
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
}
})
if (response.ok) {
closeDeletePodModal()
await fetchPods(selectedNamespace)
// Begin polling every 3s until the pod is gone (or timeout)
startPollingUntilPodDeleted(podToDelete.Name, podToDelete.Namespace)
} else {
const data = await response.json()
console.error(data.message || 'Failed to delete pod')
alert('Failed to delete pod: ' + (data.message || 'Unknown error'))
}
} catch (error) {
console.error('Failed to delete pod', error)
alert('Failed to delete pod: ' + error)
} finally {
setIsDeletingPod(false)
}
}
const startPollingUntilPodDeleted = (podName: string, namespace: string) => {
// Clear any existing polling
if (deletePollIntervalRef.current) {
clearInterval(deletePollIntervalRef.current)
deletePollIntervalRef.current = null
}
let attempts = 0
const maxAttempts = 40 // ~2 minutes
const poll = async () => {
attempts += 1
try {
// Build URL to ensure we check the same namespace
const url = `http://localhost:8082/cluster_pods?Name=${encodeURIComponent(clusterName)}&Namespace=${encodeURIComponent(namespace)}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
}
})
if (response.ok) {
const data = await response.json()
// Update UI with the latest list
setPods(data || [])
const stillThere = Array.isArray(data) && data.some((p: Pods) => p.Name === podName)
if (!stillThere) {
stopDeletePolling()
}
} else {
// Non-OK: still continue polling, but stop if too many attempts
if (attempts >= maxAttempts) stopDeletePolling()
}
} catch (_err) {
if (attempts >= maxAttempts) stopDeletePolling()
}
if (attempts >= maxAttempts) {
stopDeletePolling()
}
}
// Start interval
deletePollIntervalRef.current = window.setInterval(poll, 3000)
// Also run first poll immediately
void poll()
}
const stopDeletePolling = () => {
if (deletePollIntervalRef.current) {
clearInterval(deletePollIntervalRef.current)
deletePollIntervalRef.current = null
}
}
const copyManifest = () => {
navigator.clipboard.writeText(podManifest).then(() => {
// You could add a toast notification here
console.log('Manifest copied to clipboard')
}).catch(err => {
console.error('Failed to copy manifest: ', err)
})
}
const createPod = async () => {
if (!createPodManifest.trim()) {
alert('Please enter a pod manifest')
return
}
// Prevent duplicate requests using ref
if (isCreatingPodRef.current) {
console.log('Pod creation already in progress, ignoring duplicate request')
return
}
isCreatingPodRef.current = true
setIsCreatingPod(true)
try {
const encodedManifest = encodeBase64Utf8(createPodManifest)
const response = await fetch('http://localhost:8082/pod_create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `${localStorage.getItem('auth:token') || ''}`
},
body: JSON.stringify({
Clustername: clusterName,
Manifest: encodedManifest
})
})
if (response.ok) {
const data = await response.json()
console.log('Pod created successfully:', data)
// Close sidebar and refresh pods list
closeCreateSidebar()
fetchPods(selectedNamespace)
alert('Pod created successfully!')
} else {
const data = await response.json()
console.error(data.message || 'Failed to create pod')
alert('Failed to create pod: ' + (data.message || 'Unknown error'))
}
} catch (error) {
console.error('Failed to create pod', error)
alert('Failed to create pod: ' + error)
} finally {
isCreatingPodRef.current = false
setIsCreatingPod(false)
}
}
const handleCreatePodClick = () => {
setIsCreateSidebarOpen(true)
setCreatePodManifest('')
}
const closeCreateSidebar = () => {
setIsCreateSidebarOpen(false)
setCreatePodManifest('')
}
useEffect(() => {
// Get cluster name from localStorage
const storedClusterName = localStorage.getItem('selectedCluster')
if (storedClusterName) {
setClusterName(storedClusterName)
}
}, [])
useEffect(() => {
if (clusterName) {
fetchNamespaces()
}
}, [clusterName])
useEffect(() => {
if (selectedNamespace && clusterName) {
fetchPods(selectedNamespace)
}
}, [selectedNamespace, clusterName])
// Cleanup polling on unmount
useEffect(() => {
return () => {
if (deletePollIntervalRef.current) {
clearInterval(deletePollIntervalRef.current)
deletePollIntervalRef.current = null
}
}
}, [])
return (
<div className={`space-y-6 transition-all duration-300 ${isSidebarOpen || isCreateSidebarOpen ? 'mr-96' : ''}`}>
<div>
<h1 className="text-2xl font-semibold">Pods</h1>
<p className="text-sm text-gray-600">Monitor and manage application pods.</p>
{clusterName && (
<p className="text-sm text-blue-600 mt-1">Cluster: <span className="font-medium">{clusterName}</span></p>
)}
</div>
{!clusterName && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">No Cluster Selected</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>Please select a cluster from the Create Cluster page to view its pods.</p>
</div>
</div>
</div>
</div>
)}
{clusterName && (
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<h2 className="text-lg font-medium">Pod List</h2>
<div className="flex items-center space-x-2">
<label htmlFor="namespace-select" className="text-sm font-medium text-gray-700">
Namespace:
</label>
<select
id="namespace-select"
value={selectedNamespace}
onChange={(e) => handleNamespaceChange(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">All Namespaces</option>
{namespaces.map((ns) => (
<option key={ns.Name} value={ns.Name}>
{ns.Name}
</option>
))}
</select>
</div>
</div>
<button
onClick={handleCreatePodClick}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
>
Create Pod
</button>
</div>
</div>
<div className="overflow-x-auto">
{isLoadingPods ? (
<div className="p-6 text-sm text-gray-600">Loading pods...</div>
) : (
<table className="min-w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Namespace</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ready</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">Restarts</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Node</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</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">
{pods.map((pod) => (
<tr key={pod.Name} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{pod.Name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.Namespace}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.Ready}</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 bg-green-100 text-green-800">
{pod.Status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.Restarts}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.Age}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 font-mono">{pod.Ip}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.Node}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{pod.Image}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => handleViewClick(pod)}
className="text-blue-600 hover:text-blue-900 mr-3"
>
View
</button>
<button
onClick={() => handleLogsClick(pod)}
className="text-orange-600 hover:text-orange-900 mr-3"
>
Logs
</button>
<button
onClick={() => confirmDeletePod(pod)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)}
{/* Logs Modal */}
{isLogsModalOpen && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div className="mt-3">
{/* Modal Header */}
<div className="flex items-center justify-between pb-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Pod Logs - {selectedPod?.Name}
</h3>
<button
onClick={closeLogsModal}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Modal Body */}
<div className="mt-4">
<div className="mb-4 text-sm text-gray-600">
<span className="font-medium">Namespace:</span> {selectedPod?.Namespace} |
<span className="font-medium ml-2">Pod:</span> {selectedPod?.Name} |
<span className="font-medium ml-2">Status:</span> {selectedPod?.Status}
</div>
<div className="bg-gray-900 rounded-lg p-4 h-96 overflow-y-auto">
{isLoadingLogs ? (
<div className="flex items-center justify-center h-full">
<div className="text-white">Loading logs...</div>
</div>
) : (
<pre className="text-green-400 text-sm font-mono whitespace-pre-wrap">
{podLogs}
</pre>
)}
</div>
</div>
{/* Modal Footer */}
<div className="flex justify-end pt-4 border-t border-gray-200">
<button
onClick={closeLogsModal}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 text-sm"
>
Close
</button>
</div>
</div>
</div>
</div>
)}
{/* Sidebar for Pod Manifest */}
{isSidebarOpen && (
<div className="fixed top-0 right-0 h-screen w-96 bg-white border-l border-gray-200 shadow-lg z-40 flex flex-col">
{/* Sidebar Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-gray-50 flex-shrink-0">
<h3 className="text-lg font-medium text-gray-900">
Pod Manifest
</h3>
<div className="flex items-center space-x-2">
<button
onClick={copyManifest}
className="text-gray-400 hover:text-gray-600"
title="Copy manifest"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
<button
onClick={closeSidebar}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Pod Info */}
<div className="p-4 border-b border-gray-200 bg-gray-50 flex-shrink-0">
<div className="text-sm text-gray-600 space-y-1">
<div><span className="font-medium">Name:</span> {selectedPodForManifest?.Name}</div>
<div><span className="font-medium">Namespace:</span> {selectedPodForManifest?.Namespace}</div>
<div><span className="font-medium">Status:</span> {selectedPodForManifest?.Status}</div>
<div><span className="font-medium">Node:</span> {selectedPodForManifest?.Node}</div>
</div>
</div>
{/* Manifest Content - Scrollable Area */}
<div className="flex-1 min-h-0 bg-gray-900">
{isLoadingManifest ? (
<div className="flex items-center justify-center h-full">
<div className="text-white">Loading manifest...</div>
</div>
) : (
<div className="h-full overflow-y-auto overflow-x-auto">
<pre className="p-4 text-xs font-mono text-green-400 whitespace-pre-wrap leading-relaxed">
{podManifest}
</pre>
</div>
)}
</div>
{/* Sidebar Footer */}
<div className="p-4 border-t border-gray-200 bg-gray-50 flex-shrink-0">
<button
onClick={closeSidebar}
className="w-full px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 text-sm"
>
Close
</button>
</div>
</div>
)}
{/* Create Pod Sidebar */}
{isCreateSidebarOpen && (
<div className="fixed top-0 right-0 h-screen w-96 bg-white border-l border-gray-200 shadow-lg z-40 flex flex-col">
{/* Sidebar Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-gray-50 flex-shrink-0">
<h3 className="text-lg font-medium text-gray-900">
Create Pod
</h3>
<button
onClick={closeCreateSidebar}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Instructions */}
<div className="p-4 border-b border-gray-200 bg-blue-50 flex-shrink-0">
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">Instructions:</p>
<p>Paste your pod manifest (YAML format) in the text area below and click Create to deploy the pod.</p>
</div>
</div>
{/* Manifest Editor */}
<div className="flex-1 min-h-0 bg-gray-900 flex flex-col">
<div className="flex-1 overflow-hidden">
<textarea
value={createPodManifest}
onChange={(e) => setCreatePodManifest(e.target.value)}
placeholder="Paste your pod manifest here (YAML format)..."
className="w-full h-full p-4 text-xs font-mono text-green-400 bg-gray-900 border-none resize-none focus:outline-none placeholder-gray-500"
style={{ fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace' }}
/>
</div>
</div>
{/* Sidebar Footer */}
<div className="p-4 border-t border-gray-200 bg-gray-50 flex-shrink-0">
<div className="flex space-x-3">
<button
onClick={closeCreateSidebar}
className="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 text-sm"
>
Cancel
</button>
<button
onClick={createPod}
disabled={isCreatingPod || !createPodManifest.trim()}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
>
{isCreatingPod ? 'Creating...' : 'Create Pod'}
</button>
</div>
</div>
</div>
)}
{/* Delete Pod Confirmation Modal */}
{showDeletePodModal && (
<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 Pod</h2>
</div>
<div className="p-6">
<p className="text-gray-700 mb-4">
Are you sure you want to delete pod
<span className="font-semibold"> {podToDelete?.Name}</span> in namespace
<span className="font-semibold"> {podToDelete?.Namespace}</span>?
</p>
<p className="text-sm text-gray-500">This action cannot be undone.</p>
</div>
<div className="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
<button
onClick={closeDeletePodModal}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 disabled:opacity-50"
disabled={isDeletingPod}
>
Cancel
</button>
<button
onClick={deletePod}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50"
disabled={isDeletingPod}
>
{isDeletingPod ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
</div>
)
}