791 lines
30 KiB
TypeScript
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>
|
|
)
|
|
}
|