// Provenance: amitknigam84/lensops (fixed). // repo: amitknigam84/lensops // commit: fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84 // parent: 9b87ecc3a44a7fe2a5185cca1fe52441647d5748 // commit_url: https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84 // cve: // license: MIT // function: setErrorMessage // relpath: components/KubernetesManagement.tsx // provenance: agent // mechanism: missing_auth_guard }: KubernetesManagementProps) { const { token } = useAuth() const [kubeconfig, setKubeconfig] = useState('') // Helper to get auth headers const getAuthHeaders = () => { const headers: HeadersInit = { 'Content-Type': 'application/json' } if (token) { headers['Authorization'] = `Bearer ${token}` } return headers } const [isConnected, setIsConnected] = useState(false) const [isConnecting, setIsConnecting] = useState(false) const [isValidating, setIsValidating] = useState(false) const [isResolving, setIsResolving] = useState(false) const [validationResult, setValidationResult] = useState(null) const [resolutionResult, setResolutionResult] = useState<{ resolved: boolean fixes: string[] remainingIssues: string[] resolvedKubeconfig: string hasChanges: boolean } | null>(null) const [deployments, setDeployments] = useState([]) const [services, setServices] = useState([]) const [pods, setPods] = useState([]) const [nodes, setNodes] = useState([]) const [configmaps, setConfigmaps] = useState([]) const [secrets, setSecrets] = useState([]) const [events, setEvents] = useState([]) const [cronjobs, setCronjobs] = useState([]) const [jobs, setJobs] = useState([]) const [ingresses, setIngresses] = useState([]) const [pvcs, setPvcs] = useState([]) const [namespaces, setNamespaces] = useState([]) const [selectedNamespace, setSelectedNamespace] = useState('') const [deployedNamespace, setDeployedNamespace] = useState(null) const [isLoading, setIsLoading] = useState(false) const [errorMessage, setErrorMessage] = useState(null) const [selectedDeployments, setSelectedDeployments] = useState>(new Set()) const [imageUpdates, setImageUpdates] = useState>(new Map()) const [isUpdating, setIsUpdating] = useState(false) const [updateMode, setUpdateMode] = useState<'bulk' | 'selective'>('selective') const [showDeployments, setShowDeployments] = useState(true) const [showServices, setShowServices] = useState(true) const [showPods, setShowPods] = useState(true) const [showNamespaces, setShowNamespaces] = useState(true) const [expandedPodIssues, setExpandedPodIssues] = useState(null) const [expandedDeploymentIssues, setExpandedDeploymentIssues] = useState(null) const [activeResourceTab, setActiveResourceTab] = useState<'overview' | 'deployments' | 'pods' | 'services' | 'configmaps' | 'secrets' | 'events' | 'cronjobs' | 'jobs' | 'ingresses' | 'pvcs' | 'flow-tracing' | 'databases' | 'download-images' | 'image-change-report' | 'mongodb-wizard' | 'adminer-wizard'>('overview') const [selectedResource, setSelectedResource] = useState<{ type: 'deployment' | 'service' | 'pod' | 'configmap' | 'secret' | 'cronjob' | 'job' | 'ingress' | 'pvc' | null namespace: string name: string } | null>(null) const [selectedEvent, setSelectedEvent] = useState(null) const [actionModal, setActionModal] = useState<{ type: 'scale' | 'restart' | 'delete' | null resourceType: string namespace: string name: string currentReplicas?: number } | null>(null) const [isActioning, setIsActioning] = useState(false) const [scaleReplicas, setScaleReplicas] = useState(1) const [selectedLogPod, setSelectedLogPod] = useState<{ namespace: string name: string container?: string } | null>(null) const [describePod, setDescribePod] = useState<{ namespace: string; name: string } | null>(null) const [describePodContent, setDescribePodContent] = useState(null) const [describePodLoading, setDescribePodLoading] = useState(false) const [describePodError, setDescribePodError] = useState(null) const [showImageComparison, setShowImageComparison] = useState(false) const [showBulkUpdateWizard, setShowBulkUpdateWizard] = useState(false) const [bulkUpdateSelections, setBulkUpdateSelections] = useState>(new Set()) const [bulkUpdateImage, setBulkUpdateImage] = useState('') const [bulkUpdateStep, setBulkUpdateStep] = useState<'select' | 'preview'>('select') const [portForwardModal, setPortForwardModal] = useState<{ service: Service; targetPort: number; localPort: number } | null>(null) const [portForwardCopied, setPortForwardCopied] = useState(false) const [showCreateSecretModal, setShowCreateSecretModal] = useState(false) const [createSecretName, setCreateSecretName] = useState('') const [createSecretData, setCreateSecretData] = useState<{ key: string; value: string }[]>([{ key: '', value: '' }]) const [createSecretSaving, setCreateSecretSaving] = useState(false) const [createSecretError, setCreateSecretError] = useState(null) const [bulkUpdateTagOnly, setBulkUpdateTagOnly] = useState(false) // Container-wise update state const [bulkUpdateActiveTab, setBulkUpdateActiveTab] = useState('') const [bulkUpdateContainerImages, setBulkUpdateContainerImages] = useState>(new Map()) const [bulkUpdateContainerTagOnly, setBulkUpdateContainerTagOnly] = useState>(new Map()) const [kubeconfigs, setKubeconfigs] = useState>([]) const [imageChangeReportDate, setImageChangeReportDate] = useState(() => { const now = new Date() return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}` }) const [expandedImageHistoryDeployments, setExpandedImageHistoryDeployments] = useState>(new Set()) const [imageChangeReportData, setImageChangeReportData] = useState<{ date: string byDeployment: Array<{ namespace: string; deployment: string; changeCount: number; images?: string[] }> totalChanges: number recentInNamespace?: Array<{ date: string; deployment: string; container?: string; image?: string; oldImage?: string; source?: string; user?: string; at: string }> currentDeployments?: Array<{ deployment: string namespace: string images: Array<{ container: string; image: string }> lastUpdated?: string | null changeCount?: number imageFlow?: Array<{ createdAt: string; images: Array<{ container: string; image: string }>; revision?: string | number }> }> } | null>(null) const [imageChangeReportLoading, setImageChangeReportLoading] = useState(false) const [showKubeconfigManager, setShowKubeconfigManager] = useState(false) const [newKubeconfigName, setNewKubeconfigName] = useState('') const [newKubeconfigContent, setNewKubeconfigContent] = useState('') const [isSavingKubeconfig, setIsSavingKubeconfig] = useState(false) // Auto-refresh state const [autoRefresh, setAutoRefresh] = useState(false) const [refreshInterval, setRefreshInterval] = useState(10) // seconds const [lastRefreshed, setLastRefreshed] = useState(null) // Overview, Search, Notifications state const [auditLogs, setAuditLogs] = useState([]) const [showSearchModal, setShowSearchModal] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [showNotificationPanel, setShowNotificationPanel] = useState(false) // Pod selection for bulk delete const [selectedPods, setSelectedPods] = useState>(new Set()) const [isDeletingPods, setIsDeletingPods] = useState(false) // Deleted deployments history for restore const [deletedDeployments, setDeletedDeployments] = useState>([]) const [showDeletedDeployments, setShowDeletedDeployments] = useState(false) // Load kubeconfig from manager settings or MongoDB (MongoDB is source of truth) useEffect(() => { // Priority: manager's personal kubeconfig > MongoDB active kubeconfig if (managerKubeconfig) { setKubeconfig(managerKubeconfig) } // MongoDB kubeconfigs will be loaded via loadSavedKubeconfigs }, [managerKubeconfig]) // When a feature is disabled, switch away from its tab useEffect(() => { if (activeResourceTab === 'image-change-report' && !enableImageChangeReport) { setActiveResourceTab('overview') } if (activeResourceTab === 'databases' && !enableDatabase) { setActiveResourceTab('overview') } if (activeResourceTab === 'mongodb-wizard' && !enableMongoDBWizard) { setActiveResourceTab('overview') } if (activeResourceTab === 'adminer-wizard' && !enableAdminerWizard) { setActiveResourceTab('overview') } }, [enableImageChangeReport, enableDatabase, enableMongoDBWizard, enableAdminerWizard, activeResourceTab]) // Kubeconfig management removed - using ServiceAccount for cluster access // No need to load saved kubeconfigs anymore const loadSavedKubeconfigs = async () => { // This function is kept for compatibility but does nothing // Kubeconfig management has been removed in favor of ServiceAccount console.log('ℹ️ Kubeconfig management removed - using ServiceAccount for cluster access') } const handleSaveKubeconfig = async () => { if (!newKubeconfigName.trim() || !newKubeconfigContent.trim()) { setErrorMessage('Name and kubeconfig content are required') return } setIsSavingKubeconfig(true) setErrorMessage(null) try { const response = await fetch(`${apiUrl}/k8s/kubeconfigs`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ name: newKubeconfigName.trim(), kubeconfig: newKubeconfigContent.trim(), isActive: false }) }) const data = await response.json() if (data.success) { await loadSavedKubeconfigs() setNewKubeconfigName('') setNewKubeconfigContent('') setShowKubeconfigManager(false) setErrorMessage('✅ Kubeconfig saved successfully!') setTimeout(() => setErrorMessage(null), 3000) } else { setErrorMessage(data.error || 'Failed to save kubeconfig') } } catch (error: any) { setErrorMessage('Failed to save kubeconfig: ' + error.message) } finally { setIsSavingKubeconfig(false) } } const handleActivateKubeconfig = async (id: string) => { setIsConnecting(true) setErrorMessage(null) try { const response = await fetch(`${apiUrl}/k8s/kubeconfigs/${id}/activate`, { method: 'POST', headers: getAuthHeaders() }) const data = await response.json() if (data.success) { setIsConnected(true) setKubeconfig(data.kubeconfig.kubeconfig) await loadSavedKubeconfigs() setErrorMessage('✅ Successfully connected to cluster. Loading namespaces...') try { await loadNamespaces(true) await loadData() } catch (err) { console.error('Error loading namespaces:', err) } setTimeout(() => { setErrorMessage('✅ Successfully connected and loaded namespaces') setTimeout(() => setErrorMessage(null), 3000) }, 1000) } else { setErrorMessage(data.error || 'Failed to activate kubeconfig') setIsConnected(false) } } catch (error: any) { setErrorMessage('Failed to activate kubeconfig: ' + error.message) setIsConnected(false) } finally { setIsConnecting(false) } } const handleDeleteKubeconfig = async (id: string) => { if (!confirm('Are you sure you want to delete this kubeconfig?')) { return } try { const response = await fetch(`${apiUrl}/k8s/kubeconfigs/${id}`, { method: 'DELETE', headers: getAuthHeaders() }) const data = await response.json() if (data.success) { await loadSavedKubeconfigs() // If deleted kubeconfig was active, clear connection const wasActive = kubeconfigs.find(kc => kc.id === id)?.isActive if (wasActive) { setIsConnected(false) setKubeconfig('') } } } catch (error: any) { setErrorMessage('Failed to delete kubeconfig: ' + error.message) } } // Clear validation/resolution results when kubeconfig changes useEffect(() => { if (validationResult) { setValidationResult(null) } if (resolutionResult) { setResolutionResult(null) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [kubeconfig]) // Set initial active tab when bulk update selections change useEffect(() => { if (showBulkUpdateWizard && bulkUpdateSelections.size > 0 && !bulkUpdateActiveTab) { // Check if there are main containers or sidecar containers let hasMainContainers = false let hasSidecarContainers = false deployments.forEach((deployment) => { const key = `${deployment.namespace}/${deployment.name}` if (bulkUpdateSelections.has(key)) { deployment.containers.forEach((container) => { const isSidecar = container.name.toLowerCase().includes('sidecar') if (isSidecar) { hasSidecarContainers = true } else { hasMainContainers = true } }) } }) // Set to 'main' if main containers exist, otherwise 'sidecar' if sidecar containers exist if (hasMainContainers) { setBulkUpdateActiveTab('main') } else if (hasSidecarContainers) { setBulkUpdateActiveTab('sidecar') } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [showBulkUpdateWizard, bulkUpdateSelections.size]) const handleResolve = async () => { if (!kubeconfig.trim()) { setErrorMessage('Please paste or upload a kubeconfig file') setResolutionResult(null) return } setIsResolving(true) setErrorMessage(null) setResolutionResult(null) try { const response = await fetch(`${apiUrl}/k8s/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ kubeconfig }), }) const data = await response.json() if (data.success) { setResolutionResult({ resolved: data.resolved, fixes: data.fixes || [], remainingIssues: data.remainingIssues || [], resolvedKubeconfig: data.resolvedKubeconfig, hasChanges: data.hasChanges, }) if (data.resolved && data.hasChanges) { setErrorMessage( `✅ Resolved ${data.fixes.length} issue(s). Review the changes below and click "Apply Fixes" to update.` ) } else if (data.resolved) { setErrorMessage('✅ No issues found to resolve') } else { setErrorMessage('⚠️ Some issues could not be auto-resolved') } // Auto-validate the resolved config if (data.resolvedKubeconfig) { const validateResponse = await fetch(`${apiUrl}/k8s/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ kubeconfig: data.resolvedKubeconfig }), }) const validateData = await validateResponse.json() if (validateData.valid) { setValidationResult({ valid: true, info: validateData.info, }) } else { setValidationResult({ valid: false, issues: validateData.issues, suggestions: validateData.suggestions, info: validateData.info, }) } } } else { setErrorMessage(data.message || 'Failed to resolve kubeconfig issues') } } catch (error: any) { setErrorMessage(error.message || 'Failed to resolve kubeconfig') } finally { setIsResolving(false) } } const handleApplyFixes = () => { if (resolutionResult && resolutionResult.resolvedKubeconfig) { setKubeconfig(resolutionResult.resolvedKubeconfig) setResolutionResult(null) setErrorMessage('✅ Fixed kubeconfig applied. You can now validate or connect.') setTimeout(() => setErrorMessage(null), 5000) } } const handleValidate = async () => { if (!kubeconfig.trim()) { setErrorMessage('Please paste or upload a kubeconfig file') setValidationResult(null) return } setIsValidating(true) setErrorMessage(null) setValidationResult(null) try { const response = await fetch(`${apiUrl}/k8s/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ kubeconfig }), }) const data = await response.json() if (data.valid) { setValidationResult({ valid: true, info: data.info, }) setErrorMessage('✅ Kubeconfig is valid and ready to connect') setTimeout(() => setErrorMessage(null), 5000) } else { setValidationResult({ valid: false, issues: data.issues || [], suggestions: data.suggestions || [], info: data.info, }) } } catch (error: any) { setErrorMessage(error.message || 'Failed to validate kubeconfig') setValidationResult({ valid: false, issues: ['Validation request failed'], suggestions: ['Check your connection to the API server'], }) } finally { setIsValidating(false) } } const handleConnect = async () => { if (!kubeconfig.trim()) { setErrorMessage('Please paste or upload a kubeconfig file') return } setIsConnecting(true) setErrorMessage(null) try { const response = await fetch(`${apiUrl}/k8s/connect`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ kubeconfig }), }) const data = await response.json() if (response.ok && data.success) { setIsConnected(true) setValidationResult({ valid: true, info: data.info, }) // Save kubeconfig to MongoDB for retention (MongoDB is source of truth) try { const saveResponse = await fetch(`${apiUrl}/k8s/kubeconfigs`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ name: `Kubeconfig ${new Date().toLocaleString()}`, kubeconfig: kubeconfig, isActive: true // Set as active since user just connected with it }) }) const saveData = await saveResponse.json() if (saveData.success) { console.log('✅ Kubeconfig saved to MongoDB for retention') // Reload kubeconfigs list to show the newly saved one await loadSavedKubeconfigs() // Clear localStorage since MongoDB is now the source of truth storage.removeLocal(STORAGE_KEYS.KUBECONFIG) } } catch (saveError) { console.error('Failed to save kubeconfig to MongoDB:', saveError) // Fallback to localStorage only if MongoDB save fails storage.setLocal(STORAGE_KEYS.KUBECONFIG, kubeconfig) } setErrorMessage('✅ Successfully connected to cluster. Kubeconfig saved to MongoDB. Loading namespaces...') // Load namespaces immediately after connection (skip connection check since we just connected) try { await loadNamespaces(true) // Always load data after namespaces are loaded (defaults to 'all' to show all resources) await loadData() } catch (err) { console.error('Error loading namespaces after connection:', err) setErrorMessage('✅ Connected, but failed to load namespaces. Click "Load Namespaces" to retry.') } setTimeout(() => { if (namespaces.length > 0) { setErrorMessage('✅ Successfully connected and loaded namespaces') } else { setErrorMessage('✅ Connected. Click "Load Namespaces" to fetch namespaces.') } setTimeout(() => setErrorMessage(null), 5000) }, 1000) } else { // Handle validation errors or connection errors if (data.issues && data.suggestions) { setValidationResult({ valid: data.valid || false, issues: data.issues, suggestions: data.suggestions, info: data.info, }) } setErrorMessage(data.message || 'Failed to connect to cluster') setIsConnected(false) } } catch (error: any) { setErrorMessage(error.message || 'Failed to connect to cluster') setIsConnected(false) } finally { setIsConnecting(false) } } const loadNamespaces = async (skipConnectionCheck = false) => { if (!skipConnectionCheck && !isConnected) { setErrorMessage('Please connect to a Kubernetes cluster first') return } setIsLoading(true) setErrorMessage(null) try { console.log('📋 Loading namespaces from:', `${apiUrl}/k8s/namespaces`) const response = await fetch(`${apiUrl}/k8s/namespaces`, { headers: token ? { 'Authorization': `Bearer ${token}` } : {}, }) if (!response.ok) { const errorData = await response.json().catch(() => ({ message: 'Unknown error' })) throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`) } const data = await response.json() console.log('📋 Namespaces response:', data) if (data.success && data.namespaces) { setNamespaces(data.namespaces) setIsConnected(true) // ✅ Mark as connected after successfully loading namespaces // Always use deployed namespace (from ServiceAccount) const namespaceToUse = data.podNamespace || (data.namespaces.length > 0 ? data.namespaces[0].name : null); if (namespaceToUse) { setDeployedNamespace(namespaceToUse) setSelectedNamespace(namespaceToUse) console.log('📌 Using deployed namespace:', namespaceToUse) // Automatically load resources for the deployed namespace console.log('📦 Loading resources from deployed namespace:', namespaceToUse) setTimeout(() => { loadData() }, 100) } else { setErrorMessage('⚠️ No namespace found. Please check POD_NAMESPACE configuration.') } } else { setErrorMessage(data.message || data.error || 'Failed to load namespaces') } } catch (error: any) { console.error('❌ Failed to load namespaces:', error) setErrorMessage(error.message || 'Failed to load namespaces. Please check your connection and try again.') } finally { setIsLoading(false) } } const loadNodes = async () => { if (!isConnected) return try { const response = await fetch(`${apiUrl}/k8s/nodes`) const data = await response.json() if (response.ok && data?.nodes) { setNodes(data.nodes) } else { console.warn('⚠️ Failed to load nodes:', data?.message || data?.error) setNodes([]) } } catch (error: any) { console.error('❌ Failed to load nodes:', error) setNodes([]) } } const loadImageChangeReport = async () => { if (!apiUrl) return const namespaceToUse = deployedNamespace || selectedNamespace if (!namespaceToUse || namespaceToUse === 'all') { setErrorMessage('No namespace available. Connect to the cluster so the report is scoped to your deployed namespace.') return } setImageChangeReportLoading(true) setImageChangeReportData(null) setErrorMessage(null) try { const params = new URLSearchParams({ date: imageChangeReportDate, namespace: namespaceToUse }) const response = await fetch(`${apiUrl}/k8s/image-change-report?${params}`) const data = await response.json() if (response.ok && data.success) { setImageChangeReportData({ date: data.date, byDeployment: data.byDeployment || [], totalChanges: data.totalChanges ?? 0, recentInNamespace: data.recentInNamespace || [], currentDeployments: data.currentDeployments || [], }) } else { setErrorMessage(data.message || 'Failed to load image change report') } } catch (err: any) { setErrorMessage(err.message || 'Failed to load image change report') } finally { setImageChangeReportLoading(false) } } const loadData = async () => { if (!isConnected) { console.log('⚠️ Not connected, skipping loadData') return } // Always use deployed namespace (from ServiceAccount) const namespaceToUse = deployedNamespace || selectedNamespace if (!namespaceToUse || namespaceToUse === 'all') { console.log('⚠️ No namespace available, waiting for namespace detection...') // If we have apiUrl but no namespace yet, try to load namespaces if (apiUrl && !deployedNamespace) { loadNamespaces(true) } // Don't clear existing data if we're just waiting for namespace // This prevents "no resources" flash when namespace is temporarily unavailable return } setIsLoading(true) setErrorMessage(null) // Don't clear data here - let the API responses determine what to show // This prevents "no resources" flash during refresh try { // Always use deployed namespace const namespaceParam = namespaceToUse console.log('📦 Loading resources from deployed namespace:', namespaceParam) const [deploymentsRes, servicesRes, podsRes, configmapsRes, secretsRes, eventsRes, cronjobsRes, jobsRes, ingressesRes, pvcsRes] = await Promise.all([ fetch(`${apiUrl}/k8s/deployments?namespace=${namespaceParam}`), fetch(`${apiUrl}/k8s/services?namespace=${namespaceParam}`), fetch(`${apiUrl}/k8s/pods?namespace=${namespaceParam}`), fetch(`${apiUrl}/k8s/configmaps?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ ConfigMaps fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, configmaps: [], error: err.message }) }; }), fetch(`${apiUrl}/k8s/secrets?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ Secrets fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, secrets: [], error: err.message }) }; }), fetch(`${apiUrl}/k8s/events?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ Events fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, events: [], error: err.message }) }; }), fetch(`${apiUrl}/k8s/cronjobs?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ CronJobs fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, cronjobs: [], error: err.message }) }; }), fetch(`${apiUrl}/k8s/jobs?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ Jobs fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, jobs: [], error: err.message }) }; }), fetch(`${apiUrl}/k8s/ingresses?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ Ingresses fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, ingresses: [], error: err.message }) }; }), fetch(`${apiUrl}/k8s/pvcs?namespace=${namespaceParam}`).catch((err) => { console.warn('⚠️ PVCs fetch failed:', err); return { ok: false, status: 500, json: async () => ({ success: false, pvcs: [], error: err.message }) }; }), ]) const deploymentsData = await deploymentsRes.json() const servicesData = await servicesRes.json() const podsData = await podsRes.json() console.log('📦 Deployments response:', deploymentsData) console.log('📦 Services response:', servicesData) console.log('📦 Pods response:', podsData) if (deploymentsData.success) { console.log('✅ Setting deployments:', deploymentsData.deployments?.length || 0) setDeployments(deploymentsData.deployments || []) } else { console.error('❌ Deployments API error:', deploymentsData.error || deploymentsData.message) // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues // This prevents "no resources" flash during refresh when there are transient errors if (deploymentsRes.status >= 400 && deploymentsRes.status < 500) { setDeployments([]) } else { console.warn('⚠️ Keeping existing deployments due to server/network error') } } if (servicesData.success) { console.log('✅ Setting services:', servicesData.services?.length || 0) setServices(servicesData.services || []) } else { console.error('❌ Services API error:', servicesData.error || servicesData.message) // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues if (servicesRes.status >= 400 && servicesRes.status < 500) { setServices([]) } else { console.warn('⚠️ Keeping existing services due to server/network error') } } if (podsData.success) { const podsArray = podsData.pods || [] // Remove duplicates based on namespace + name const uniquePods = podsArray.reduce((acc: Map, pod: Pod) => { const key = `${pod.namespace}/${pod.name}` if (!acc.has(key)) { acc.set(key, pod) } else { console.warn(`⚠️ Duplicate pod found: ${key}`) } return acc }, new Map()) const deduplicatedPods: Pod[] = Array.from(uniquePods.values()) // Filter by namespace if a specific namespace is selected (not 'all') const filteredPods: Pod[] = selectedNamespace && selectedNamespace !== 'all' ? deduplicatedPods.filter((pod: Pod) => pod.namespace === selectedNamespace) : deduplicatedPods console.log('✅ Setting pods:', { original: podsArray.length, afterDedup: deduplicatedPods.length, afterFilter: filteredPods.length, namespace: namespaceParam, selectedNamespace: selectedNamespace }) if (podsArray.length !== filteredPods.length) { console.warn(`⚠️ Pod count mismatch: API returned ${podsArray.length}, displaying ${filteredPods.length}`) } setPods(filteredPods) } else { console.error('❌ Pods API error:', podsData.error || podsData.message) // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues if (podsRes.status >= 400 && podsRes.status < 500) { setPods([]) } else { console.warn('⚠️ Keeping existing pods due to server/network error') } } const configmapsData = await configmapsRes.json() if (configmapsData.success) { console.log('✅ Setting configmaps:', configmapsData.configmaps?.length || 0) setConfigmaps(configmapsData.configmaps || []) } else { console.error('❌ ConfigMaps API error:', configmapsData.error || configmapsData.message) setConfigmaps([]) } const secretsData = await secretsRes.json() if (secretsData.success) { setSecrets(secretsData.secrets || []) } else { setSecrets([]) } const eventsData = await eventsRes.json() if (eventsData.success) { console.log('✅ Setting events:', eventsData.events?.length || 0) setEvents(eventsData.events || []) } else { console.error('❌ Events API error:', eventsData.error || eventsData.message) setEvents([]) } // Handle CronJobs try { const cronjobsData = await cronjobsRes.json() if (cronjobsData.success) { console.log('✅ Setting cronjobs:', cronjobsData.cronjobs?.length || 0) setCronjobs(cronjobsData.cronjobs || []) } else { console.error('❌ CronJobs API error:', cronjobsData.error || cronjobsData.message, cronjobsData.details) // Don't show error if it's just that there are no cronjobs (404 or empty list) if (cronjobsRes.status !== 404 && !cronjobsData.message?.includes('not found')) { console.warn('⚠️ CronJobs fetch warning:', cronjobsData.message) } setCronjobs([]) } } catch (cronjobsError: any) { console.error('❌ Failed to parse CronJobs response:', cronjobsError) setCronjobs([]) } // Handle Jobs try { const jobsData = await jobsRes.json() if (jobsData.success) { console.log('✅ Setting jobs:', jobsData.jobs?.length || 0) setJobs(jobsData.jobs || []) } else { console.error('❌ Jobs API error:', jobsData.error || jobsData.message, jobsData.details) // Don't show error if it's just that there are no jobs (404 or empty list) if (jobsRes.status !== 404 && !jobsData.message?.includes('not found')) { console.warn('⚠️ Jobs fetch warning:', jobsData.message) } setJobs([]) } } catch (jobsError: any) { console.error('❌ Failed to parse Jobs response:', jobsError) setJobs([]) } // Handle Ingresses try { const ingressesData = await ingressesRes.json() if (ingressesData.success) { setIngresses(ingressesData.ingresses || []) } else { setIngresses([]) } } catch (ingressesError: any) { console.error('❌ Failed to parse Ingresses response:', ingressesError) setIngresses([]) } // Handle PVCs try { const pvcsData = await pvcsRes.json() if (pvcsData.success) { setPvcs(pvcsData.pvcs || []) } else { setPvcs([]) } } catch (pvcsError: any) { console.error('❌ Failed to parse PVCs response:', pvcsError) setPvcs([]) } // Check for HTTP errors if (!deploymentsRes.ok) { console.error('❌ Deployments HTTP error:', deploymentsRes.status, deploymentsRes.statusText) setErrorMessage(`Failed to fetch deployments: ${deploymentsRes.status} ${deploymentsRes.statusText}`) } if (!servicesRes.ok) { console.error('❌ Services HTTP error:', servicesRes.status, servicesRes.statusText) setErrorMessage(`Failed to fetch services: ${servicesRes.status} ${servicesRes.statusText}`) } if (!podsRes.ok) { console.error('❌ Pods HTTP error:', podsRes.status, podsRes.statusText) setErrorMessage(`Failed to fetch pods: ${podsRes.status} ${podsRes.statusText}`) } } catch (error: any) { setErrorMessage(error.message || 'Failed to load cluster data') } finally { setIsLoading(false) setLastRefreshed(new Date()) } } // Automatically load namespaces and resources on mount when apiUrl is available useEffect(() => { if (apiUrl && !isConnected && !isConnecting) { console.log('🚀 Auto-connecting to Kubernetes cluster via ServiceAccount...') loadNamespaces(true) // Skip connection check since we're auto-connecting } }, [apiUrl]) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (isConnected && selectedNamespace) { console.log('🔄 Namespace changed to:', selectedNamespace, '- Loading resources...') // Clear previous data when namespace changes setDeployments([]) setPods([]) setNodes([]) setServices([]) setConfigmaps([]) setEvents([]) setCronjobs([]) setJobs([]) // Load new data for the selected namespace loadData() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedNamespace, isConnected]) useEffect(() => { if (isConnected) { loadNodes() } else { setNodes([]) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isConnected]) // Auto-refresh effect useEffect(() => { if (!autoRefresh || !isConnected) return const interval = setInterval(async () => { console.log('🔄 Auto-refreshing data...') const currentTab = activeResourceTab await loadData() setActiveResourceTab(currentTab) setLastRefreshed(new Date()) }, refreshInterval * 1000) return () => clearInterval(interval) // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoRefresh, refreshInterval, isConnected, deployedNamespace, selectedNamespace]) const handleImageChange = ( namespace: string, deploymentName: string, containerName: string, newImage: string, originalImage: string ) => { const key = `${namespace}/${deploymentName}/${containerName}` const newUpdates = new Map(imageUpdates) if (newImage === originalImage) { newUpdates.delete(key) } else { newUpdates.set(key, { namespace: namespace, deployment: deploymentName, container: containerName, image: newImage, originalImage, }) } setImageUpdates(newUpdates) } // Helper function to extract base image (without tag) const getBaseImage = (image: string): string => { // Remove tag if present (format: image:tag) if (image.includes(':')) { // Check if it's a port number (registry:port/image:tag) const parts = image.split(':') if (parts.length > 2) { // Has port, get everything before the last colon const lastColonIndex = image.lastIndexOf(':') return image.substring(0, lastColonIndex) } else { // Simple format: image:tag return parts[0] } } // Remove digest if present (format: image@sha256:...) if (image.includes('@')) { return image.split('@')[0] } return image } // Helper function to apply tag-only update const applyTagOnlyUpdate = (originalImage: string, newTag: string): string => { const baseImage = getBaseImage(originalImage) return `${baseImage}:${newTag}` } const handleBulkImageUpdate = (newImage: string) => { const newUpdates = new Map() deployments.forEach((deployment) => { deployment.containers.forEach((container) => { const key = `${selectedNamespace}/${deployment.name}/${container.name}` newUpdates.set(key, { namespace: selectedNamespace, deployment: deployment.name, container: container.name, image: newImage, originalImage: container.image, }) }) }) setImageUpdates(newUpdates) } const handleSaveSingleImage = async (namespace: string, deployment: string, container: string, newImage: string, originalImage: string) => { if (newImage === originalImage) { setErrorMessage('Image unchanged') setTimeout(() => setErrorMessage(null), 2000) return } if (!newImage || newImage.trim() === '') { setErrorMessage('Please enter a valid image name') setTimeout(() => setErrorMessage(null), 2000) return } // Validate required fields const trimmedNamespace = namespace?.trim() const trimmedDeployment = deployment?.trim() const trimmedContainer = container?.trim() const trimmedImage = newImage.trim() if (!trimmedNamespace || !trimmedDeployment || !trimmedContainer) { setErrorMessage('Missing required fields: namespace, deployment, or container name') setTimeout(() => setErrorMessage(null), 3000) return } // Ensure deployment name doesn't contain namespace (some APIs return it as namespace/name) let deploymentName = trimmedDeployment if (trimmedDeployment.includes('/')) { const parts = trimmedDeployment.split('/').filter(p => p && p.trim() !== '') deploymentName = parts.length > 0 ? parts[parts.length - 1] : trimmedDeployment console.log('Extracted deployment name:', { original: trimmedDeployment, extracted: deploymentName, parts }) } // Final validation after extraction if (!deploymentName || deploymentName.trim() === '') { setErrorMessage('Invalid deployment name after processing') setTimeout(() => setErrorMessage(null), 3000) return } setIsUpdating(true) setErrorMessage(null) try { const updatePayload = { namespace: String(trimmedNamespace), deployment: String(deploymentName), container: String(trimmedContainer), image: String(trimmedImage) } console.log('Saving image update:', { namespace: trimmedNamespace, deployment: deploymentName, container: trimmedContainer, image: trimmedImage, originalDeployment: deployment, payload: updatePayload }) const response = await fetch(`${apiUrl}/k8s/update-images`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ updates: [updatePayload], source: 'ui-single' }), }) // Check if response is ok before parsing JSON if (!response.ok) { const errorText = await response.text() let errorData try { errorData = JSON.parse(errorText) } catch { errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } } throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) } const data = await response.json() if (data.success || (data.updated && data.updated > 0)) { setErrorMessage(`✅ Successfully updated ${container} image in ${deployment}`) // Remove from pending updates const key = `${namespace}/${deployment}/${container}` const newUpdates = new Map(imageUpdates) newUpdates.delete(key) setImageUpdates(newUpdates) await loadData() setTimeout(() => setErrorMessage(null), 5000) } else { // Check if there are errors in the response const errorMsg = data.errors && data.errors.length > 0 ? data.errors[0].error || data.errors[0].message : data.error || data.message const errorDetails = data.errors && data.errors.length > 0 && data.errors[0].deployment ? ` (${data.errors[0].deployment}/${data.errors[0].container})` : '' setErrorMessage( `❌ Failed to update image${errorDetails}: ${errorMsg || 'Unknown error'}` ) setTimeout(() => setErrorMessage(null), 10000) } } catch (error: any) { console.error('Error updating image:', error) setErrorMessage(`❌ Failed to update image: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) setTimeout(() => setErrorMessage(null), 10000) } finally { setIsUpdating(false) } } const handleUpdateImages = async () => { console.log('🔄 handleUpdateImages called') console.log('📦 imageUpdates size:', imageUpdates.size) console.log('📦 imageUpdates:', Array.from(imageUpdates.entries())) if (imageUpdates.size === 0) { setErrorMessage('No image updates to apply') return } setIsUpdating(true) setErrorMessage(null) try { const updates = Array.from(imageUpdates.values()) console.log('📋 All updates:', updates) // If selective mode, only update selected deployments let updatesToApply = updates if (updateMode === 'selective') { console.log('🎯 Selective mode - selectedDeployments:', Array.from(selectedDeployments)) updatesToApply = updates.filter((update) => { const key = `${update.namespace}/${update.deployment}` return selectedDeployments.has(key) }) console.log('✅ Filtered updates to apply:', updatesToApply) } if (updatesToApply.length === 0) { console.log('⚠️ No deployments selected') setErrorMessage('Please select deployments to update or switch to bulk mode') setIsUpdating(false) return } console.log('🚀 Making API call to update images:', updatesToApply) const response = await fetch(`${apiUrl}/k8s/update-images`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ updates: updatesToApply, source: updateMode === 'selective' ? 'ui-selective' : 'ui-bulk' }), }) console.log('📡 API response status:', response.status) // Check if response is ok before parsing JSON if (!response.ok) { const errorText = await response.text() let errorData try { errorData = JSON.parse(errorText) } catch { errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } } throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) } const data = await response.json() if (data.success) { // All updates succeeded setErrorMessage(`✅ Successfully updated ${data.updated || updatesToApply.length} container(s)`) setImageUpdates(new Map()) setSelectedDeployments(new Set()) await loadData() setTimeout(() => setErrorMessage(null), 5000) } else if (data.updated && data.updated > 0) { // Partial success - some succeeded, some failed const errorDetails = data.errors && data.errors.length > 0 ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') : 'See details above' setErrorMessage( `⚠️ Partial update: ${data.updated} succeeded, ${data.failed || 0} failed. ${errorDetails}` ) // Clear successful updates from the pending list if (data.results && Array.isArray(data.results)) { const newUpdates = new Map(imageUpdates) data.results.forEach((result: any) => { const key = `${result.namespace}/${result.deployment}/${result.container}` newUpdates.delete(key) }) setImageUpdates(newUpdates) } await loadData() setTimeout(() => setErrorMessage(null), 8000) } else { // All updates failed const errorMsg = data.errors && data.errors.length > 0 ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') : data.error || data.message || 'Unknown error' setErrorMessage(`❌ Failed to update images: ${errorMsg}`) setTimeout(() => setErrorMessage(null), 10000) } } catch (error: any) { console.error('Error updating images:', error) setErrorMessage(`❌ Failed to update images: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) setTimeout(() => setErrorMessage(null), 10000) } finally { setIsUpdating(false) } } const toggleDeploymentSelection = (namespace: string, deploymentName: string) => { const key = `${namespace}/${deploymentName}` const newSelected = new Set(selectedDeployments) if (newSelected.has(key)) { newSelected.delete(key) } else { newSelected.add(key) } setSelectedDeployments(newSelected) } // Bulk Update Wizard Functions const handleSelectAllBulkUpdate = () => { if (bulkUpdateSelections.size === deployments.length) { setBulkUpdateSelections(new Set()) } else { const allKeys = new Set(deployments.map(d => `${d.namespace}/${d.name}`)) setBulkUpdateSelections(allKeys) } } const toggleBulkUpdateSelection = (namespace: string, name: string) => { const key = `${namespace}/${name}` const newSelected = new Set(bulkUpdateSelections) if (newSelected.has(key)) { newSelected.delete(key) } else { newSelected.add(key) } setBulkUpdateSelections(newSelected) } const handleBulkUpdateApply = async () => { if (bulkUpdateSelections.size === 0) { setErrorMessage('Please select at least one deployment') return } if (!bulkUpdateActiveTab || (bulkUpdateActiveTab !== 'main' && bulkUpdateActiveTab !== 'sidecar')) { setErrorMessage('Please select a container tab (Main or Sidecar)') return } const containerImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' const containerTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false if (!containerImage.trim()) { setErrorMessage('Please enter a new ' + (containerTagOnly ? 'tag' : 'image') + ' for ' + bulkUpdateActiveTab + ' containers') return } setIsUpdating(true) setErrorMessage(null) try { // Build updates for all selected deployments, but only for containers matching the active tab type const updates: ImageUpdate[] = [] const isSidecarTab = bulkUpdateActiveTab === 'sidecar' deployments.forEach((deployment) => { const key = `${deployment.namespace}/${deployment.name}` if (bulkUpdateSelections.has(key)) { deployment.containers.forEach((container) => { // Only update containers matching the active tab type (sidecar vs main) const isContainerSidecar = container.name.toLowerCase().includes('sidecar') if (isSidecarTab === isContainerSidecar) { // If tag-only mode, apply the tag to the base image const finalImage = containerTagOnly ? applyTagOnlyUpdate(container.image, containerImage.trim()) : containerImage.trim() updates.push({ namespace: deployment.namespace, deployment: deployment.name, container: container.name, image: finalImage, originalImage: container.image, }) } }) } }) const response = await fetch(`${apiUrl}/k8s/update-images`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ updates, source: 'ui-bulk' }), }) // Check if response is ok before parsing JSON if (!response.ok) { const errorText = await response.text() let errorData try { errorData = JSON.parse(errorText) } catch { errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } } throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) } const data = await response.json() if (data.success) { // All updates succeeded setErrorMessage(`✅ Successfully updated ${data.updated || updates.length} container(s)`) setShowBulkUpdateWizard(false) setBulkUpdateSelections(new Set()) setBulkUpdateImage('') setBulkUpdateTagOnly(false) setBulkUpdateActiveTab('') setBulkUpdateContainerImages(new Map()) setBulkUpdateContainerTagOnly(new Map()) setBulkUpdateStep('select') await loadData() setTimeout(() => setErrorMessage(null), 5000) } else if (data.updated && data.updated > 0) { // Partial success - some succeeded, some failed const errorDetails = data.errors && data.errors.length > 0 ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') : 'See details above' setErrorMessage( `⚠️ Partial update: ${data.updated} succeeded, ${data.failed || 0} failed. ${errorDetails}` ) setShowBulkUpdateWizard(false) setBulkUpdateSelections(new Set()) setBulkUpdateImage('') setBulkUpdateTagOnly(false) setBulkUpdateActiveTab('') setBulkUpdateContainerImages(new Map()) setBulkUpdateContainerTagOnly(new Map()) setBulkUpdateStep('select') await loadData() setTimeout(() => setErrorMessage(null), 8000) } else { // All updates failed const errorMsg = data.errors && data.errors.length > 0 ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') : data.error || data.message || 'Unknown error' setErrorMessage(`❌ Failed to update images: ${errorMsg}`) setTimeout(() => setErrorMessage(null), 10000) } } catch (error: any) { console.error('Error updating images:', error) setErrorMessage(`❌ Failed to update images: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) setTimeout(() => setErrorMessage(null), 10000) } finally { setIsUpdating(false) } } const handleScaleDeployment = async (namespace: string, name: string, replicas: number) => { setIsActioning(true) setErrorMessage(null) try { const response = await fetch(`${apiUrl}/k8s/deployments/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/scale`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ replicas }), }) const data = await response.json() if (data.success) { setErrorMessage(`✅ ${data.message}`) setTimeout(() => setErrorMessage(null), 5000) setActionModal(null) // Reload deployments to reflect the change await loadData() } else { setErrorMessage(`❌ ${data.error || data.message || 'Failed to scale deployment'}`) } } catch (error: any) { setErrorMessage(`❌ Failed to scale deployment: ${error.message}`) } finally { setIsActioning(false) } } const handleRestartDeployment = async (namespace: string, name: string) => { setIsActioning(true) setErrorMessage(null) try { const response = await fetch(`${apiUrl}/k8s/deployments/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/restart`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }) const data = await response.json() if (data.success) { setErrorMessage(`✅ ${data.message}`) setTimeout(() => setErrorMessage(null), 5000) setActionModal(null) // Reload deployments to reflect the change await loadData() } else { setErrorMessage(`❌ ${data.error || data.message || 'Failed to restart deployment'}`) } } catch (error: any) { setErrorMessage(`❌ Failed to restart deployment: ${error.message}`) } finally { setIsActioning(false) } } const handleDeleteResource = async (resourceType: string, namespace: string, name: string) => { setIsActioning(true) setErrorMessage(null) try { // If deleting a deployment, save its spec first for potential restore if (resourceType === 'deployments') { const deployment = deployments.find(d => d.name === name && d.namespace === namespace) if (deployment) { setDeletedDeployments(prev => [...prev, { name: deployment.name, namespace: deployment.namespace, deletedAt: new Date().toISOString(), spec: deployment }]) } } const response = await fetch(`${apiUrl}/k8s/${resourceType}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, }) const data = await response.json() if (data.success) { setErrorMessage(`✅ ${data.message}`) setTimeout(() => setErrorMessage(null), 5000) setActionModal(null) // Reload data to reflect the deletion await loadData() } else { setErrorMessage(`❌ ${data.error || data.message || 'Failed to delete resource'}`) } } catch (error: any) { setErrorMessage(`❌ Failed to delete resource: ${error.message}`) } finally { setIsActioning(false) } } // Handle pod selection toggle const handlePodSelect = (podKey: string) => { setSelectedPods(prev => { const newSet = new Set(prev) if (newSet.has(podKey)) { newSet.delete(podKey) } else { newSet.add(podKey) } return newSet }) } // Handle select all pods const handleSelectAllPods = () => { if (selectedPods.size === pods.length) { setSelectedPods(new Set()) } else { setSelectedPods(new Set(pods.map(p => `${p.namespace}/${p.name}`))) } } // Handle bulk delete pods const handleBulkDeletePods = async () => { if (selectedPods.size === 0) return if (!confirm(`Are you sure you want to delete ${selectedPods.size} pod(s)? This action cannot be undone.`)) { return } setIsDeletingPods(true) setErrorMessage(null) let successCount = 0 let failCount = 0 for (const podKey of selectedPods) { const [namespace, name] = podKey.split('/') try { const response = await fetch(`${apiUrl}/k8s/pods/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }) const data = await response.json() if (data.success) { successCount++ } else { failCount++ } } catch { failCount++ } } setSelectedPods(new Set()) setIsDeletingPods(false) if (failCount === 0) { setErrorMessage(`✅ Successfully deleted ${successCount} pod(s)`) } else { setErrorMessage(`⚠️ Deleted ${successCount} pod(s), failed to delete ${failCount} pod(s)`) } setTimeout(() => setErrorMessage(null), 5000) await loadData() } // Handle restore deleted deployment const handleRestoreDeployment = async (deletedDeployment: { name: string; namespace: string; deletedAt: string; spec: any }) => { if (!confirm(`Are you sure you want to restore deployment "${deletedDeployment.name}" in namespace "${deletedDeployment.namespace}"?`)) { return } setIsActioning(true) setErrorMessage(null) try { // Create deployment from saved spec const response = await fetch(`${apiUrl}/k8s/deployments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: deletedDeployment.namespace, name: deletedDeployment.name, spec: deletedDeployment.spec }), }) const data = await response.json() if (data.success) { // Remove from deleted deployments list setDeletedDeployments(prev => prev.filter(d => !(d.name === deletedDeployment.name && d.namespace === deletedDeployment.namespace && d.deletedAt === deletedDeployment.deletedAt) )) setErrorMessage(`✅ Successfully restored deployment "${deletedDeployment.name}"`) setTimeout(() => setErrorMessage(null), 5000) await loadData() } else { setErrorMessage(`❌ Failed to restore deployment: ${data.error || data.message}`) } } catch (error: any) { setErrorMessage(`❌ Failed to restore deployment: ${error.message}`) } finally { setIsActioning(false) } } // Handle clear deleted deployment from history const handleClearDeletedDeployment = (deletedDeployment: { name: string; namespace: string; deletedAt: string; spec: any }) => { setDeletedDeployments(prev => prev.filter(d => !(d.name === deletedDeployment.name && d.namespace === deletedDeployment.namespace && d.deletedAt === deletedDeployment.deletedAt) )) } // Auto-connect on component mount (using ServiceAccount) useEffect(() => { if (apiUrl && !isConnected) { loadNamespaces(true); } }, [apiUrl]); // Get docker image tag from portal deployment for build version const portalDeployment = deployments.find(d => d.name.toLowerCase().includes('portal')) const dockerImageTag = portalDeployment && portalDeployment.containers?.length > 0 ? portalDeployment.containers[0].image.split(':')[1] || 'latest' : (deployments.length > 0 && deployments[0].containers?.length > 0 ? deployments[0].containers[0].image.split(':')[1] || 'latest' : 'latest') // ─── Helper: time ago ─── const timeAgo = (dateStr: string) => { if (!dateStr) return '-' const now = new Date() const date = new Date(dateStr) const seconds = Math.floor((now.getTime() - date.getTime()) / 1000) if (seconds < 0) return 'just now' if (seconds < 60) return `${seconds}s ago` const minutes = Math.floor(seconds / 60) if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) return `${days}d ago` } // ─── Fetch audit logs for overview ─── useEffect(() => { if (activeResourceTab === 'overview' && isConnected && apiUrl) { fetch(`${apiUrl}/audit/logs?limit=10`) .then(res => res.json()) .then(data => { if (data.success) setAuditLogs(data.logs || []) }) .catch(err => console.warn('Failed to fetch audit logs:', err)) } }, [activeResourceTab, isConnected, apiUrl]) // ─── Fetch pod for Describe modal ─── useEffect(() => { if (!describePod || !apiUrl) { setDescribePodContent(null) setDescribePodError(null) return } let cancelled = false setDescribePodLoading(true) setDescribePodError(null) setDescribePodContent(null) fetch(`${apiUrl}/k8s/pods/${encodeURIComponent(describePod.namespace)}/${encodeURIComponent(describePod.name)}`, { headers: getAuthHeaders(), }) .then(res => res.json()) .then(data => { if (cancelled) return if (data.success && data.pod) { setDescribePodContent(JSON.stringify(data.pod, null, 2)) } else { setDescribePodError(data.message || 'Failed to load pod') } }) .catch(err => { if (!cancelled) setDescribePodError(err.message || 'Failed to fetch pod') }) .finally(() => { if (!cancelled) setDescribePodLoading(false) }) return () => { cancelled = true } }, [describePod, apiUrl]) // ─── Global search keyboard shortcut (Cmd+K / Ctrl+K) ─── useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() setShowSearchModal(true) setSearchQuery('') } if (e.key === 'Escape' && showSearchModal) { setShowSearchModal(false) } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [showSearchModal]) // ─── Search results computation ─── const searchResults = useMemo(() => { if (!searchQuery.trim()) return [] const q = searchQuery.toLowerCase() const results: Array<{ type: string; name: string; namespace: string; tabKey: string; resourceType: string }> = [] deployments.filter(d => d.name.toLowerCase().includes(q)).forEach(d => results.push({ type: 'Deployment', name: d.name, namespace: d.namespace, tabKey: 'deployments', resourceType: 'deployment' })) pods.filter(p => p.name.toLowerCase().includes(q)).forEach(p => results.push({ type: 'Pod', name: p.name, namespace: p.namespace, tabKey: 'pods', resourceType: 'pod' })) services.filter(s => s.name.toLowerCase().includes(q)).forEach(s => results.push({ type: 'Service', name: s.name, namespace: s.namespace, tabKey: 'services', resourceType: 'service' })) configmaps.filter(c => c.name?.toLowerCase().includes(q)).forEach(c => results.push({ type: 'ConfigMap', name: c.name, namespace: c.namespace, tabKey: 'configmaps', resourceType: 'configmap' })) secrets.filter(s => s.name?.toLowerCase().includes(q)).forEach(s => results.push({ type: 'Secret', name: s.name, namespace: s.namespace, tabKey: 'secrets', resourceType: 'secret' })) cronjobs.filter(c => c.name.toLowerCase().includes(q)).forEach(c => results.push({ type: 'CronJob', name: c.name, namespace: c.namespace, tabKey: 'cronjobs', resourceType: 'cronjob' })) jobs.filter(j => j.name.toLowerCase().includes(q)).forEach(j => results.push({ type: 'Job', name: j.name, namespace: j.namespace, tabKey: 'jobs', resourceType: 'job' })) ingresses.filter(i => i.name.toLowerCase().includes(q)).forEach(i => results.push({ type: 'Ingress', name: i.name, namespace: i.namespace, tabKey: 'ingresses', resourceType: 'ingress' })) pvcs.filter(p => p.name.toLowerCase().includes(q)).forEach(p => results.push({ type: 'PVC', name: p.name, namespace: p.namespace, tabKey: 'pvcs', resourceType: 'pvc' })) // Nodes nodes.filter((n: any) => (n.name || n.metadata?.name || '').toLowerCase().includes(q)).forEach((n: any) => results.push({ type: 'Node', name: n.name || n.metadata?.name, namespace: '', tabKey: 'overview', resourceType: 'node' })) // Events events.filter((e: any) => (e.message || '').toLowerCase().includes(q) || (e.reason || '').toLowerCase().includes(q) || (e.involvedObject?.name || '').toLowerCase().includes(q)).forEach((e: any) => results.push({ type: 'Event', name: `${e.involvedObject?.kind || ''}/${e.involvedObject?.name || ''} — ${e.reason || ''}`, namespace: e.namespace || '', tabKey: 'events', resourceType: 'event' })) // Databases from localStorage (connected database services) try { const savedDbs = localStorage.getItem('database_connections') if (savedDbs) { const dbConns = JSON.parse(savedDbs) as Array<{ name: string; type: string; host?: string; namespace?: string }> dbConns.filter(db => (db.name || '').toLowerCase().includes(q) || (db.type || '').toLowerCase().includes(q) || (db.host || '').toLowerCase().includes(q)).forEach(db => results.push({ type: 'Database', name: `${db.name} (${db.type})`, namespace: db.namespace || '', tabKey: 'databases', resourceType: 'database' })) } } catch {} return results.slice(0, 50) }, [searchQuery, deployments, pods, services, configmaps, secrets, cronjobs, jobs, ingresses, pvcs, nodes, events]) // ─── Notifications computation ─── const notifications = useMemo(() => { const notifs: Array<{ type: 'error' | 'warning'; message: string; resource: string; namespace: string; timestamp: string }> = [] // Pods with CrashLoopBackOff or OOMKilled pods.forEach(pod => { if (pod.issues && Array.isArray(pod.issues)) { pod.issues.forEach((issue: any) => { if (issue.reason === 'CrashLoopBackOff' || issue.reason === 'OOMKilled') { notifs.push({ type: 'error', message: `${issue.reason}: ${issue.message || pod.name}`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) } }) } }) // Pods with any issues (hasIssues flag from API) pods.forEach(pod => { if (pod.hasIssues && (!pod.issues || pod.issues.length === 0)) { notifs.push({ type: 'warning', message: `Pod ${pod.name} has issues (status: ${pod.status})`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) } }) // Pods not in Running/Succeeded state pods.forEach(pod => { if (pod.status && pod.status !== 'Running' && pod.status !== 'Succeeded') { // Avoid duplicate if already caught by issues above const alreadyNotified = notifs.some(n => n.resource === pod.name && n.namespace === pod.namespace) if (!alreadyNotified) { notifs.push({ type: 'warning', message: `Pod ${pod.name} is ${pod.status}`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || pod.creationTimestamp || '' }) } } }) // Deployments with Stalled / ReplicaFailure / Unavailable / Degraded deployments.forEach(dep => { if (dep.deploymentStatus === 'Stalled' || dep.deploymentStatus === 'ReplicaFailure' || dep.deploymentStatus === 'Unavailable' || dep.deploymentStatus === 'Degraded') { notifs.push({ type: 'error', message: `Deployment ${dep.name} is ${dep.deploymentStatus}`, resource: dep.name, namespace: dep.namespace, timestamp: dep.creationTimestamp }) } }) // Warning events — check both lastTimestamp and eventTime, use 2-hour window const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000) events.forEach((event: any) => { if (event.type === 'Warning') { const eventTs = event.lastTimestamp || event.eventTime || event.firstTimestamp if (eventTs && new Date(eventTs) > twoHoursAgo) { notifs.push({ type: 'warning', message: `${event.reason}: ${(event.message || '').slice(0, 80)}`, resource: `${event.involvedObject?.kind || ''}/${event.involvedObject?.name || ''}`, namespace: event.namespace || '', timestamp: eventTs }) } } }) // Pods with high restart count (API returns "restarts" field, not "restartCount") pods.forEach(pod => { const restarts = (pod as any).restarts ?? (pod as any).restartCount ?? 0 if (restarts > 5) { // Avoid duplicate const alreadyNotified = notifs.some(n => n.resource === pod.name && n.namespace === pod.namespace && n.message.includes('restarts')) if (!alreadyNotified) { notifs.push({ type: 'warning', message: `Pod ${pod.name} has ${restarts} restarts`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) } } }) return notifs.slice(0, 50) }, [pods, deployments, events]) return (
{/* Left Sidebar - Fixed Dark Theme */}
{/* Logo/Brand Section */}
Klarity
{/* Project/Namespace Selector */}
NAMESPACE
{selectedNamespace || 'Connecting...'}
{/* Navigation Menu */} {/* Bottom Section */}
Build version: {dockerImageTag}
{/* Main Content Area - Full width with left margin for fixed sidebar */}
{/* Connection Banner - Only when not connected */} {!isConnected && (
Connecting to Kubernetes cluster via ServiceAccount...
)} {/* Main Content */} {isConnected && (
{/* Header Banner - no overflow-hidden so notification dropdown is not clipped */}

Welcome to Klarity

Experience seamless Kubernetes management with powerful observability and control.

{/* Search Button */} {/* Notification Bell */}
{showNotificationPanel && (

Notifications ({notifications.length})

{notifications.length === 0 ? (
All clear! No issues detected.
) : notifications.map((notif, idx) => { const parts = (notif.resource || '').split('/') const kind = parts.length > 1 ? parts[0] : 'Pod' const name = parts.length > 1 ? parts[1] : notif.resource const tabMap: Record = { Pod: 'pods', Deployment: 'deployments', Service: 'services' } const typeMap: Record = { Pod: 'pod', Deployment: 'deployment', Service: 'service' } const tab = tabMap[kind] || 'pods' const resType = typeMap[kind] || 'pod' return (
{ setShowNotificationPanel(false) setActiveResourceTab(tab as any) if (name) setSelectedResource({ type: resType, namespace: notif.namespace || selectedNamespace, name }) }} >

{notif.message}

{notif.resource} · {timeAgo(notif.timestamp)}

) })}
)}
{/* Tabs Navigation */}
{/* Databases tab hidden */} {enableMongoDBWizard && ( )} {enableAdminerWizard && ( )} {enableImageChangeReport && ( )}
{/* Content Area */}
{/* Quick Stats Cards + Resource Summary - hidden on Flow Tracing tab and all DB tabs */} {activeResourceTab !== 'flow-tracing' && activeResourceTab !== 'databases' && activeResourceTab !== 'mongodb-wizard' && activeResourceTab !== 'adminer-wizard' && ( <>

Cluster Resources

Connected
{autoRefresh && ( )}
{/* Compact Resource Summary Cards */}
setActiveResourceTab('deployments')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'deployments' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
Deployments
{deployments.length}
setActiveResourceTab('pods')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'pods' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
Pods
{pods.length}
setActiveResourceTab('services')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'services' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
Services
{services.length}
setActiveResourceTab('cronjobs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'cronjobs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
CronJobs
{cronjobs.length}
setActiveResourceTab('jobs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'jobs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
Jobs
{jobs.length}
setActiveResourceTab('events')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'events' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
Events
{events.length}
setActiveResourceTab('ingresses')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'ingresses' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
Ingresses
{ingresses.length}
setActiveResourceTab('pvcs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'pvcs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}>
PVCs
{pvcs.length}
)} {/* ═══ OVERVIEW TAB ═══ */} {activeResourceTab === 'overview' && (
{/* Health Summary Cards - clickable, numbers centered */}
setActiveResourceTab('deployments')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300">
Deployments
{deployments.length}
{deployments.filter(d => d.deploymentStatus === 'Ready').length} healthy {deployments.filter(d => d.hasIssues).length > 0 && {deployments.filter(d => d.hasIssues).length} issues}
setActiveResourceTab('pods')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300">
Pods
{pods.filter((p: any) => p.status === 'Running' && p.ready !== false).length} /{pods.length}
Running
setActiveResourceTab('services')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300">
Services
{services.length}
Active in namespace
setActiveResourceTab('events')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300">
Warning Events
{events.filter((e: any) => e.type === 'Warning').length}
From cluster events
{/* Deployment Status Distribution Bar */}

Deployment Health

{(() => { const total = deployments.length || 1 const ready = deployments.filter(d => d.deploymentStatus === 'Ready').length const progressing = deployments.filter(d => d.deploymentStatus === 'Progressing' || d.deploymentStatus === 'Updating').length const stalled = deployments.filter(d => d.deploymentStatus === 'Stalled' || d.deploymentStatus === 'Degraded').length const failed = deployments.filter(d => d.deploymentStatus === 'Unavailable' || d.deploymentStatus === 'ReplicaFailure').length const other = total - ready - progressing - stalled - failed return (
{ready > 0 &&
} {progressing > 0 &&
} {stalled > 0 &&
} {failed > 0 &&
} {other > 0 &&
}
Ready: {ready} {progressing > 0 && Progressing: {progressing}} {stalled > 0 && Stalled: {stalled}} {failed > 0 && Failed: {failed}} {other > 0 && Other: {other}}
) })()}
{/* Unhealthy Resources */} {(() => { const unhealthyPods = pods.filter((p: any) => p.hasIssues || p.status !== 'Running' || p.ready === false) if (unhealthyPods.length === 0) return (

All pods are healthy and running.

) return (

⚠ Unhealthy Pods ({unhealthyPods.length})

{unhealthyPods.map((pod: any) => ( { setActiveResourceTab('pods'); setSelectedResource({ type: 'pod', namespace: pod.namespace, name: pod.name }) }}> ))}
Pod Status Reason Restarts Age
{pod.name} {pod.status} {pod.issues?.[0]?.reason || (pod.ready === false ? 'Not Ready' : '-')} {pod.restartCount ?? 0} {timeAgo(pod.creationTimestamp || pod.startTime || '')}
) })()} {/* Recent Warning Events */} {(() => { const warningEvents = events.filter((e: any) => e.type === 'Warning') if (warningEvents.length === 0) return null const kindToTab: Record = { Pod: 'pods', Deployment: 'deployments', Service: 'services', ReplicaSet: 'deployments', Node: 'overview' } const kindToType: Record = { Pod: 'pod', Deployment: 'deployment', Service: 'service', ReplicaSet: 'deployment', Node: null } return (

Recent Warning Events

{warningEvents.map((event: any, idx: number) => { const kind = event.involvedObject?.kind || '' const name = event.involvedObject?.name || '' const tab = kindToTab[kind] const resType = kindToType[kind] const ns = event.involvedObject?.namespace || event.metadata?.namespace || selectedNamespace || '' return ( { if (tab) setActiveResourceTab(tab as any) if (resType && name) setSelectedResource({ type: resType, namespace: ns || selectedNamespace, name }) if (tab === 'overview' && kind === 'Node') setSelectedResource(null) }} > ) })}
When Resource Reason Message
{timeAgo(event.lastTimestamp || event.eventTime || '')} {event.involvedObject?.kind}/{event.involvedObject?.name} {event.reason} {event.message}
) })()} {/* Recent Activity from Audit Logs */}

Recent Activity

{auditLogs.length === 0 ? (
No recent activity recorded.
) : (
{auditLogs.map((log: any, idx: number) => { const resourceType = (log.resourceType || '').toLowerCase() const tabMap: Record = { deployments: 'deployments', pods: 'pods', services: 'services', configmaps: 'configmaps', secrets: 'secrets', cronjobs: 'cronjobs', jobs: 'jobs', ingresses: 'ingresses', pvcs: 'pvcs', namespaces: 'overview' } const typeMap: Record = { deployments: 'deployment', pods: 'pod', services: 'service', configmaps: 'configmap', secrets: 'secret', cronjobs: 'cronjob', jobs: 'job', ingresses: 'ingress', pvcs: 'pvc', namespaces: null } const tab = tabMap[resourceType] const resType = typeMap[resourceType] const ns = log.namespace || selectedNamespace || '' return (
{ if (tab) setActiveResourceTab(tab as any) if (resType && log.resourceName) setSelectedResource({ type: resType, namespace: ns, name: log.resourceName }) }} >
{log.user || 'system'} {log.action} {log.resourceType}/{log.resourceName}
{timeAgo(log.timestamp)}
) })}
)}
)} {/* Bulk Update Controls - Show for deployments */} {activeResourceTab === 'deployments' && deployments.length > 0 && (
Update Mode:
{updateMode === 'bulk' && ( )}
{imageUpdates.size > 0 && (
{imageUpdates.size} image(s) pending update
)}
)} {/* Resource Details Section */} {/* Detailed Resource View */}
{/* Deployments Tab Content */} {activeResourceTab === 'deployments' && (
{/* Deleted Deployments Restore Section - only when deployment delete enabled */} {enableDeploymentDelete && deletedDeployments.length > 0 && (
{showDeletedDeployments && (
Recently Deleted Deployments (Session only - will be lost on page refresh)
{deletedDeployments.map((deleted, idx) => (
{deleted.name}
Namespace: {deleted.namespace} | Deleted: {new Date(deleted.deletedAt).toLocaleString()}
))}
)}
)} {isLoading ? (

Loading deployments...

) : deployments.length === 0 ? (

No deployments found

{selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}

) : (
{updateMode === 'selective' && ( )} {selectedNamespace === 'all' && ( )} {deployments.map((deployment) => (deployment.containers || []).map((container, idx) => { const key = `${deployment.namespace}/${deployment.name}/${container.name}` const update = imageUpdates.get(key) const isSelected = selectedDeployments.has( `${deployment.namespace}/${deployment.name}` ) return ( {updateMode === 'selective' && idx === 0 && ( )} {selectedNamespace === 'all' && idx === 0 && ( )} {idx === 0 && ( )} {idx === 0 && ( )} ) }) )}
0 && deployments.every((d) => selectedDeployments.has(`${d.namespace}/${d.name}`) ) } onChange={(e) => { if (e.target.checked) { setSelectedDeployments( new Set( deployments.map((d) => `${d.namespace}/${d.name}`) ) ) } else { setSelectedDeployments(new Set()) } }} className="w-4 h-4 rounded border-gray-300 bg-white text-blue-600 focus:ring-blue-500" /> Namespace Deployment Container Current Image New Image Actions
toggleDeploymentSelection(deployment.namespace, deployment.name) } className="w-4 h-4 rounded border-gray-300 bg-white text-blue-600" /> {deployment.namespace}
{deployment.name}
{/* Status indicator based on deploymentStatus */} {deployment.deploymentStatus === 'Ready' ? ( Ready ) : deployment.deploymentStatus === 'Progressing' || deployment.deploymentStatus === 'Updating' ? ( {deployment.deploymentStatus} ) : deployment.deploymentStatus === 'Stalled' ? ( Stalled ) : deployment.deploymentStatus === 'ReplicaFailure' || deployment.deploymentStatus === 'Unavailable' ? ( {deployment.deploymentStatus === 'ReplicaFailure' ? 'Failed' : 'Unavailable'} ) : deployment.deploymentStatus === 'Degraded' ? ( Degraded ) : deployment.deploymentStatus === 'ScaledToZero' ? ( Scaled to 0 ) : deployment.readyReplicas === deployment.replicas && deployment.replicas > 0 ? ( Ready ) : deployment.readyReplicas > 0 ? ( Partial ) : ( Not Ready )} {deployment.readyReplicas}/{deployment.replicas}
{/* Warning triangle for deployments with issues */} {deployment.hasIssues && (
{expandedDeploymentIssues === `${deployment.namespace}-${deployment.name}` && (
Deployment Issues ({deployment.issues?.length || 0})
{deployment.statusReason && (
{deployment.statusReason}
{deployment.statusMessage && (
{deployment.statusMessage.length > 200 ? `${deployment.statusMessage.substring(0, 200)}...` : deployment.statusMessage}
)}
)} {deployment.issues && deployment.issues.length > 0 && deployment.issues.map((issue, issueIdx) => (
{issue.conditionType}
{issue.reason}
{issue.message && (
{issue.message.length > 150 ? `${issue.message.substring(0, 150)}...` : issue.message}
)}
))}
{/* Show deployment conditions */} {deployment.conditions && deployment.conditions.some(c => c.status === 'False') && (
Conditions:
{deployment.conditions.filter(c => c.status === 'False').map((cond, condIdx) => (
{cond.type}: {cond.reason || 'Unknown'}
))}
)}
)}
)}
{container.name} {container.image}
handleImageChange( deployment.namespace, deployment.name, container.name, e.target.value, container.image ) } placeholder={container.image} title={update ? update.image : container.image} className="flex-1 min-w-0 px-2 py-1.5 bg-white border border-gray-300 rounded text-[11px] font-mono text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" /> {update && update.image !== container.image && ( )}
{enableDeploymentDelete && ( )}
)} {imageUpdates.size > 0 && (
{imageUpdates.size} container image(s) pending update
)}
)} {/* Pods Tab Content */} {activeResourceTab === 'pods' && (
{/* Bulk Actions Bar */} {selectedPods.size > 0 && (
{selectedPods.size} pod(s) selected
)} {isLoading ? (

Loading pods...

) : pods.length === 0 ? (

No pods found

{selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}

) : (
{selectedNamespace === 'all' && ( )} {pods.map((pod) => { const podKey = `${pod.namespace}/${pod.name}` return ( setSelectedResource({ type: 'pod', namespace: pod.namespace, name: pod.name })} > {selectedNamespace === 'all' && ( )} ) })}
0} onChange={handleSelectAllPods} className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" title="Select all pods" /> Namespace Pod Name Status Node Pod IP Containers Restarts Actions
e.stopPropagation()}> handlePodSelect(podKey)} className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" /> {pod.namespace}
{pod.name} {pod.hasIssues && (
{expandedPodIssues === `${pod.namespace}-${pod.name}` && (
Pod Issues ({pod.issues?.length || 0})
{pod.issues && pod.issues.length > 0 ? ( pod.issues.map((issue, idx) => (
{issue.type === 'initContainer' ? 'init' : issue.type}
{issue.reason} {issue.exitCode !== undefined && ( (exit: {issue.exitCode}) )}
{issue.containerName && (
Container: {issue.containerName}
)} {issue.conditionType && (
Condition: {issue.conditionType}
)} {issue.message && (
{issue.message.length > 150 ? `${issue.message.substring(0, 150)}...` : issue.message}
)}
)) ) : (
Pod has issues but no specific details available. Check container status and conditions.
)}
{/* Show pod conditions if available */} {pod.conditions && pod.conditions.some(c => c.status === 'False') && (
Conditions:
{pod.conditions.filter(c => c.status === 'False').map((cond, idx) => (
{cond.type}: {cond.reason || 'Unknown'}
))}
)}
)}
)}
{pod.status} {pod.nodeName || '-'} {pod.podIP || '-'}
{(pod.containers || []).map((container, idx) => (
{container.name} {container.image.split('/').pop()}
))}
{pod.restartCount || 0}
e.stopPropagation()}>
)}
)} {/* Services Tab Content */} {activeResourceTab === 'services' && (
{isLoading ? (

Loading services...

) : services.length === 0 ? (
No services found in namespace "{selectedNamespace}"
) : (
{services.map((service) => ( ))}
Service Name Type Cluster IP Ports Actions
setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} > {service.name} setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} > {service.type} setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} > {service.clusterIP || '-'} setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} > {(service.ports || []).map((port, idx) => (
{port.port}/{port.protocol}
))}
e.stopPropagation()}>
{(service.ports && service.ports.length > 0) ? ( ) : ( null )}
)}
)} {/* ConfigMaps Tab Content */} {activeResourceTab === 'configmaps' && (
{isLoading ? (

Loading configmaps...

) : configmaps.length === 0 ? (
No configmaps found in namespace "{selectedNamespace}"
) : (
{configmaps.map((cm) => ( setSelectedResource({ type: 'configmap', namespace: cm.namespace, name: cm.name })} > ))}
ConfigMap Name Data Keys Created
{cm.name}
{Object.keys(cm.data || {}).map((key) => ( {key} ))}
{cm.creationTimestamp ? new Date(cm.creationTimestamp).toLocaleDateString() : '-'}
)}
)} {/* Secrets Tab Content */} {activeResourceTab === 'secrets' && (
{isLoading ? (

Loading secrets...

) : secrets.length === 0 ? (
No secrets found in namespace "{selectedNamespace}"
) : (
{secrets.map((s) => ( setSelectedResource({ type: 'secret', namespace: s.namespace, name: s.name })} > ))}
Secret Name Type Keys Created
{s.name} {s.type || 'Opaque'}
{(s.keys || []).map((key: string) => ( {key} ))}
{s.age ? new Date(s.age).toLocaleDateString() : '-'}
)}
)} {/* Events Tab Content */} {activeResourceTab === 'events' && (
{isLoading ? (

Loading events...

) : events.length === 0 ? (
No events found in namespace "{selectedNamespace}"
) : (
{events.map((event) => ( setSelectedEvent(event)} > ))}
Object Type Reason Message Count Last Seen
{event.involvedObject?.kind}/{event.involvedObject?.name} {event.type} {event.reason} {event.message} {event.count || 0} {event.lastTimestamp ? new Date(event.lastTimestamp).toLocaleString() : '-'}
)}
)} {/* CronJobs Tab Content */} {activeResourceTab === 'cronjobs' && (
{isLoading ? (

Loading cronjobs...

) : cronjobs.length === 0 ? (
No cronjobs found in namespace "{selectedNamespace}"
) : (
{cronjobs.map((cronjob) => ( setSelectedResource({ type: 'cronjob', namespace: cronjob.namespace, name: cronjob.name })} > ))}
Name Schedule Suspend Active Last Schedule Containers Created Actions
{cronjob.name} {cronjob.schedule || '-'} {cronjob.suspend ? 'Yes' : 'No'} {cronjob.active} {cronjob.lastScheduleTime ? new Date(cronjob.lastScheduleTime).toLocaleString() : '-'}
{(cronjob.containers || []).map((container) => ( {container.name}: {container.image.split(':')[0]} ))}
{cronjob.creationTimestamp ? new Date(cronjob.creationTimestamp).toLocaleDateString() : '-'}
)}
)} {/* Jobs Tab Content */} {activeResourceTab === 'jobs' && (
{isLoading ? (

Loading jobs...

) : jobs.length === 0 ? (
No jobs found in namespace "{selectedNamespace}"
) : (
{jobs.map((job) => { const jobAge = job.startTime ? new Date(job.startTime) : null const ageString = jobAge ? `${Math.floor((Date.now() - jobAge.getTime()) / (1000 * 60))}m` : '-' return ( setSelectedResource({ type: 'job', namespace: job.namespace, name: job.name })} > ) })}
Name Completions Succeeded Failed Active Age Containers Actions
{job.name} {job.succeeded}/{job.completions} {job.succeeded} {job.failed > 0 ? ( {job.failed} ) : ( 0 )} {job.active > 0 ? ( {job.active} ) : ( 0 )} {ageString}
{(job.containers || []).map((container) => ( {container.name}: {container.image.split(':')[0]} ))}
)}
)}
{/* Flow Tracing Tab Content */} {activeResourceTab === 'flow-tracing' && (
)} {/* Databases Tab Content - only when feature enabled */} {enableDatabase && activeResourceTab === 'databases' && (
)} {/* MongoDB Compass Tab Content */} {enableMongoDBWizard && activeResourceTab === 'mongodb-wizard' && (
)} {/* Adminer Tab Content */} {enableAdminerWizard && activeResourceTab === 'adminer-wizard' && (
)} {/* ═══ INGRESSES TAB ═══ */} {activeResourceTab === 'ingresses' && (
{isLoading ? (

Loading ingresses...

) : ingresses.length === 0 ? (
No ingresses found in namespace "{selectedNamespace}"
) : (
{ingresses.map((ing) => ( ))}
Name Class Hosts Paths Backend TLS Age
{ing.name} {ing.ingressClassName || —} {ing.rules.map((r, i) =>
{r.host}
)}
{ing.rules.flatMap((r, ri) => r.paths.map((p, pi) => ( {p.path} )))}
{ing.rules.flatMap((r, ri) => r.paths.map((p, pi) => ( p.backend?.serviceName ?
{p.backend.serviceName}:{p.backend.servicePort}
: null )))}
{ing.tls && ing.tls.length > 0 ? ( 🔒 TLS ) : ( — )} {ing.creationTimestamp ? timeAgo(ing.creationTimestamp) : '-'}
)}
)} {/* ═══ PVCs TAB ═══ */} {activeResourceTab === 'pvcs' && (
{isLoading ? (

Loading persistent volume claims...

) : pvcs.length === 0 ? (
No PVCs found in namespace "{selectedNamespace}"
) : (
{pvcs.map((pvc) => ( ))}
Name Status Storage Class Capacity Access Modes Volume Age
{pvc.name} {pvc.status} {pvc.storageClassName || —} {pvc.capacity || '—'}
{pvc.accessModes.map((mode) => ( {mode} ))}
{pvc.volumeName || '—'} {pvc.creationTimestamp ? timeAgo(pvc.creationTimestamp) : '-'}
)}
)} {/* Image change report: per-deployment image change count + which images (from cluster) - only when feature enabled */} {enableImageChangeReport && activeResourceTab === 'image-change-report' && (

Image change report

Per deployment: how many times the image was changed (count) and which images (flow from start to current). Changes made via Selective or Bulk Update in Deployments are reflected here. Download CSV available.

{(deployedNamespace || selectedNamespace) && (deployedNamespace || selectedNamespace) !== 'all' ? ( <>
Namespace: {deployedNamespace || selectedNamespace}
{imageChangeReportData && ( <> {/* Current deployment images from cluster - always shown when we have data */} {imageChangeReportData.currentDeployments && imageChangeReportData.currentDeployments.length > 0 && (
Current deployment images in namespace {deployedNamespace || selectedNamespace} (from cluster — change count & flow from ReplicaSet history)
{imageChangeReportData.currentDeployments.map((row, i) => ( ))}
Service (Deployment) Image changes Current image list Flow (start → current)
{row.deployment} {row.changeCount ?? 0}
    {row.images?.map((img, j) => (
  • {img.container ? `${img.container}: ${img.image}` : img.image}
  • ))}
{row.imageFlow && row.imageFlow.length > 0 ? (
{row.imageFlow.map((rev, idx) => ( {idx > 0 && →} im.image).join(', ')}> Rev{rev.revision ?? idx + 1} ({rev.createdAt ? new Date(rev.createdAt).toISOString().slice(0, 10) : '-'}): {rev.images?.map((im) => im.image?.split(':').pop() || im.image).join(', ')} ))}
) : ( — )}
)} {/* Recent Image Changes - Detailed History by Deployment (collapsible) */} {imageChangeReportData.recentInNamespace && imageChangeReportData.recentInNamespace.length > 0 && (() => { const recent = imageChangeReportData.recentInNamespace const byDeployment = recent.reduce((acc: Record, entry) => { const key = entry.deployment || 'unknown' if (!acc[key]) acc[key] = [] acc[key].push(entry) return acc }, {}) const deploymentNames = Object.keys(byDeployment) const toggleExpanded = (key: string) => { setExpandedImageHistoryDeployments(prev => { const next = new Set(prev) if (next.has(key)) next.delete(key) else next.add(key) return next }) } return (
Recent image changes — by deployment ({recent.length} entries)
{deploymentNames.map((depName) => { const entries = byDeployment[depName] const count = entries.length const isExpanded = expandedImageHistoryDeployments.has(depName) return (
{isExpanded && (
{entries.map((entry, i) => { const sourceLabel = entry.source === 'ui-single' ? 'UI - Single Edit' : entry.source === 'ui-bulk' ? 'UI - Bulk Update' : entry.source === 'ui-selective' ? 'UI - Selective' : entry.source === 'api' ? 'API / Terminal' : entry.source || 'Unknown' const sourceBg = entry.source === 'ui-single' ? 'bg-green-100 text-green-800' : entry.source === 'ui-bulk' ? 'bg-purple-100 text-purple-800' : entry.source === 'ui-selective' ? 'bg-blue-100 text-blue-800' : entry.source === 'api' ? 'bg-orange-100 text-orange-800' : 'bg-gray-100 text-gray-700' return ( ) })}
When Deployment Container Old Image → New Image Source Changed By
{entry.at ? new Date(entry.at).toLocaleDateString() : entry.date}
{entry.at ? new Date(entry.at).toLocaleTimeString() : ''}
{entry.deployment} {entry.container || '—'}
{entry.oldImage ? ( <> {entry.oldImage.length > 40 ? '...' + entry.oldImage.slice(-37) : entry.oldImage} → {(entry.image || '').length > 40 ? '...' + (entry.image || '').slice(-37) : entry.image} ) : ( {(entry.image || '').length > 50 ? '...' + (entry.image || '').slice(-47) : entry.image} )}
{sourceLabel} {entry.user || 'anonymous'}
)}
) })}
) })()} {/* No audit data message */} {imageChangeReportData.recentInNamespace && imageChangeReportData.recentInNamespace.length === 0 && (
No image change history found in audit logs. Changes made via the UI will appear here after you update images.
)} )} ) : (
Connect to the cluster to see the image change report for your deployed namespace.
)}
)} {/* Download Images - Auto Excel Download */} {activeResourceTab === 'download-images' && (() => { // Auto-trigger Excel download when this tab is selected const downloadImagesExcel = () => { // Find max containers in any deployment const maxContainers = Math.max(...deployments.map(d => d.containers.length), 1) // Build header with Container 1, Container 2, Container 3 columns const header = ['S.No', 'Deployment', 'Namespace'] for (let i = 1; i <= maxContainers; i++) { header.push(`Container ${i} Name`, `Container ${i} Image`, `Container ${i} Tag`) } const rows: string[][] = [header] let sno = 1 deployments.forEach(d => { const row: string[] = [ String(sno++), d.name, d.namespace ] // Add each container in separate columns for (let i = 0; i < maxContainers; i++) { if (d.containers[i]) { const imageParts = d.containers[i].image.split(':') const imageName = imageParts[0] const imageTag = imageParts[1] || 'latest' row.push(d.containers[i].name, imageName, imageTag) } else { row.push('', '', '') } } rows.push(row) }) // Convert to CSV format (Excel compatible) const csvContent = rows.map(row => row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',') ).join('\n') // Create and download file const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') const url = URL.createObjectURL(blob) link.setAttribute('href', url) link.setAttribute('download', `${selectedNamespace}-images-${new Date().toISOString().split('T')[0]}.csv`) link.style.visibility = 'hidden' document.body.appendChild(link) link.click() document.body.removeChild(link) // Switch back to deployments tab after download setTimeout(() => setActiveResourceTab('deployments'), 100) } // Trigger download immediately setTimeout(downloadImagesExcel, 100) return (

Downloading Images List...

Exporting {deployments.reduce((acc, d) => acc + d.containers.length, 0)} container images from {selectedNamespace} namespace

) })()}
)}
{/* Old Pods Table - Removed (now in tabbed pane) */} {false && isConnected && selectedNamespace && showPods && (

Pods ({pods.length}) {selectedNamespace === 'all' ? '- All Namespaces' : `- Namespace: ${selectedNamespace}`}

{isLoading ? (

Loading pods...

) : pods.length === 0 ? (
No pods found {selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}
) : (
{selectedNamespace === 'all' && ( )} {pods.map((pod) => ( {selectedNamespace === 'all' && ( )} ))}
Namespace Pod Name Status Node Pod IP Containers Restarts Actions
{pod.namespace}
{pod.name} {pod.hasIssues && (
{expandedPodIssues === `${pod.namespace}-${pod.name}` && (
Pod Issues ({pod.issues?.length || 0})
{pod.issues && pod.issues.length > 0 ? ( pod.issues.map((issue, idx) => (
{issue.type === 'initContainer' ? 'init' : issue.type}
{issue.reason} {issue.exitCode !== undefined && ( (exit: {issue.exitCode}) )}
{issue.containerName && (
Container: {issue.containerName}
)} {issue.conditionType && (
Condition: {issue.conditionType}
)} {issue.message && (
{issue.message.length > 150 ? `${issue.message.substring(0, 150)}...` : issue.message}
)}
)) ) : (
Pod has issues but no specific details available. Check container status and conditions.
)}
{/* Show pod conditions if available */} {pod.conditions && pod.conditions.some(c => c.status === 'False') && (
Conditions:
{pod.conditions.filter(c => c.status === 'False').map((cond, idx) => (
{cond.type}: {cond.reason || 'Unknown'}
))}
)}
)}
)}
{pod.status} {pod.nodeName || '-'} {pod.podIP || '-'}
{(pod.containers || []).map((container, idx) => (
{container.name}:{' '} {container.image} {container.ready !== undefined && ( {container.ready ? '✓' : '✗'} )}
))}
{pod.restartCount || 0}
)}
)} {/* Services Table with Edit */} {/* Old Services Table - Removed (now in tabbed pane) */} {false && isConnected && selectedNamespace && showServices && (

Services ({services.length}) {selectedNamespace === 'all' ? '- All Namespaces' : `- Namespace: ${selectedNamespace}`}

{isLoading ? (

Loading services...

) : services.length === 0 ? (
No services found {selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}
) : (
{selectedNamespace === 'all' && ( )} {services.map((service) => ( {selectedNamespace === 'all' && ( )} ))}
Namespace Service Type Cluster IP Ports Selector
{service.namespace} {service.name} {service.type} {service.clusterIP || '-'} {(service.ports || []).map((port, idx) => (
{port.port}:{port.targetPort}/{port.protocol} {port.name && ` (${port.name})`}
))}
{Object.entries(service.selector).map(([key, value]) => ( {key}={value} ))}
)}
)} {/* Port Forward Modal (OpenLens-style) */} {portForwardModal && (
setPortForwardModal(null)} />

Port forward

{portForwardModal.service.name} · {portForwardModal.service.namespace}

setPortForwardModal({ ...portForwardModal, localPort: Number(e.target.value) || portForwardModal.targetPort })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" />
Open in browser (no kubectl)

Or run in terminal

kubectl port-forward -n {portForwardModal.service.namespace} svc/{portForwardModal.service.name} {portForwardModal.localPort}:{portForwardModal.targetPort}
)} {/* Resource Detail Modal */} setSelectedResource(null)} resourceType={selectedResource?.type || null} namespace={selectedResource?.namespace || ''} name={selectedResource?.name || ''} apiUrl={apiUrl} onResourceDeleted={loadData} /> {/* Describe Pod Modal */} {describePod && (
setDescribePod(null)} aria-hidden />

Describe Pod: {describePod.name} ({describePod.namespace})

{describePodLoading && (
)} {describePodError && (

{describePodError}

)} {!describePodLoading && !describePodError && describePodContent && (
                    {describePodContent}
                  
)}
)} {/* Create Secret Modal */} {showCreateSecretModal && (
setShowCreateSecretModal(false)} />

Create Secret

Namespace: {selectedNamespace} (key-value pairs are stored as base64)

setCreateSecretName(e.target.value)} placeholder="my-secret" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-amber-500 focus:border-amber-500" />
{createSecretData.map((row, idx) => (
{ const next = [...createSecretData] next[idx] = { ...next[idx], key: e.target.value } setCreateSecretData(next) }} placeholder="key" className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono" /> { const next = [...createSecretData] next[idx] = { ...next[idx], value: e.target.value } setCreateSecretData(next) }} placeholder="value" className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono" />
))}
{createSecretError && (
{createSecretError}
)}
)} {selectedLogPod && ( setSelectedLogPod(null)} podName={selectedLogPod.name} namespace={selectedLogPod.namespace} container={selectedLogPod.container} apiUrl={apiUrl} /> )} {/* Event Detail Modal */} {selectedEvent && (

Event Details

{selectedEvent.involvedObject?.kind}/{selectedEvent.involvedObject?.name}

{selectedEvent.type}

{selectedEvent.reason}

{selectedEvent.count || 1}

{selectedEvent.source?.component || '-'} / {selectedEvent.source?.host || '-'}

{selectedEvent.message}

{selectedEvent.firstTimestamp ? new Date(selectedEvent.firstTimestamp).toLocaleString() : '-'}

{selectedEvent.lastTimestamp ? new Date(selectedEvent.lastTimestamp).toLocaleString() : '-'}

Kind: {selectedEvent.involvedObject?.kind}

Name: {selectedEvent.involvedObject?.name}

Namespace: {selectedEvent.involvedObject?.namespace}

{selectedEvent.involvedObject?.uid && (

UID: {selectedEvent.involvedObject.uid}

)}
{selectedEvent.name && (

{selectedEvent.name}

)}
)} {/* Bulk Update Image Wizard Modal */} {showBulkUpdateWizard && (

Bulk Image Update Wizard

{bulkUpdateStep === 'select' ? 'Step 1: Select deployments to update' : 'Step 2: Review and apply updates'}

{bulkUpdateStep === 'select' ? (
{bulkUpdateSelections.size} deployment(s) selected
{deployments.map((deployment) => { const key = `${deployment.namespace}/${deployment.name}` const isSelected = bulkUpdateSelections.has(key) return ( ) })}
Deployment Containers Current Images
toggleBulkUpdateSelection(deployment.namespace, deployment.name)} className="w-4 h-4" />
{deployment.name}
{deployment.namespace}
{(deployment.containers || []).map((container, idx) => (
{container.name}
))}
{(deployment.containers || []).map((container, idx) => (
{container.image}
))}
{/* Container Tabs Section - Only show if deployments are selected */} {bulkUpdateSelections.size > 0 && (() => { // Group all containers by type (sidecar vs main) const mainContainers: Array<{ deployment: Deployment container: { name: string; image: string } }> = [] const sidecarContainers: Array<{ deployment: Deployment container: { name: string; image: string } }> = [] deployments.forEach((deployment) => { const key = `${deployment.namespace}/${deployment.name}` if (bulkUpdateSelections.has(key)) { deployment.containers.forEach((container) => { const isSidecar = container.name.toLowerCase().includes('sidecar') if (isSidecar) { sidecarContainers.push({ deployment, container }) } else { mainContainers.push({ deployment, container }) } }) } }) // Set initial active tab if not set if (!bulkUpdateActiveTab) { if (mainContainers.length > 0) { setBulkUpdateActiveTab('main') } else if (sidecarContainers.length > 0) { setBulkUpdateActiveTab('sidecar') } } const isActiveSidecar = bulkUpdateActiveTab === 'sidecar' const activeContainers = isActiveSidecar ? sidecarContainers : mainContainers const activeImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' const activeTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false return (
{/* Container Type Tabs */}
{mainContainers.length > 0 && ( )} {sidecarContainers.length > 0 && ( )}
{/* Selected Container Type's Deployments Table */} {activeContainers.length > 0 && (
{isActiveSidecar ? 'Sidecar' : 'Main'} Containers ({activeContainers.length} total)
{activeContainers.map((item, idx) => ( ))}
Deployment Namespace Container Name Current Image
{item.deployment.name}
{item.deployment.namespace}
{item.container.name}
{item.container.image}
)} {/* Image Input Section - Different for sidecar vs main containers */}
{ const newMap = new Map(bulkUpdateContainerTagOnly) newMap.set(bulkUpdateActiveTab, e.target.checked) setBulkUpdateContainerTagOnly(newMap) }} className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
{ const newMap = new Map(bulkUpdateContainerImages) newMap.set(bulkUpdateActiveTab, e.target.value) setBulkUpdateContainerImages(newMap) }} placeholder={activeTagOnly ? "e.g., 1.12.0-rc5 or latest" : "e.g., nginx:1.25 or myregistry.io/app:v2.0"} className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm" /> {activeTagOnly && (

Only the tag portion will be updated. Base image will remain unchanged.

)}
) })()}
) : (
{(() => { // Group all containers by type (sidecar vs main) const mainContainers: Array<{ deployment: Deployment container: { name: string; image: string } }> = [] const sidecarContainers: Array<{ deployment: Deployment container: { name: string; image: string } }> = [] deployments.forEach((deployment) => { const key = `${deployment.namespace}/${deployment.name}` if (bulkUpdateSelections.has(key)) { deployment.containers.forEach((container) => { const isSidecar = container.name.toLowerCase().includes('sidecar') if (isSidecar) { sidecarContainers.push({ deployment, container }) } else { mainContainers.push({ deployment, container }) } }) } }) const isActiveSidecar = bulkUpdateActiveTab === 'sidecar' const activeContainers = isActiveSidecar ? sidecarContainers : mainContainers const activeImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' const activeTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false return ( <> {/* Container Type Tabs */}
{mainContainers.length > 0 && ( )} {sidecarContainers.length > 0 && ( )}

Update Summary

• {bulkUpdateSelections.size} deployment(s) selected

• Container Type: {isActiveSidecar ? 'Sidecar' : 'Main'}

• Containers to update: {activeContainers.length}

• New {activeTagOnly ? 'tag' : 'image'}: {activeImage}

{activeContainers.map((item, idx) => { const newImage = activeTagOnly ? applyTagOnlyUpdate(item.container.image, activeImage.trim()) : activeImage.trim() return ( ) })}
Deployment Namespace Container Name Current Image New Image
{item.deployment.name} {item.deployment.namespace} {item.container.name} {item.container.image} {newImage}
) })()}
)}
{bulkUpdateStep === 'select' ? ( ) : ( )}
)} {/* Image comparison modal */} {showImageComparison && (
setShowImageComparison(false)} />

Image update review

Compare current vs new images before applying updates

{imageUpdates.size === 0 ? (

No pending image updates.

) : ( {Array.from(imageUpdates.values()).map((update, idx) => ( ))}
Namespace Deployment Container Current Image New Image
{update.namespace} {update.deployment} {update.container} {update.originalImage} {update.image}
)}
{imageUpdates.size} change(s) ready to apply
)} {/* Action Confirmation Modals */} {actionModal && (
setActionModal(null)}>

{actionModal.type === 'scale' && 'Scale Deployment'} {actionModal.type === 'restart' && 'Restart Deployment'} {actionModal.type === 'delete' && 'Delete Resource'}

{actionModal.type === 'scale' && (

Scale deployment {actionModal.name} in namespace {actionModal.namespace}

setScaleReplicas(Math.max(0, parseInt(e.target.value) || 0))} className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center focus:outline-none focus:ring-2 focus:ring-blue-500" /> replicas
{actionModal.currentReplicas !== undefined && (

Current: {actionModal.currentReplicas} replicas

)}
)} {actionModal.type === 'restart' && (

Are you sure you want to restart deployment {actionModal.name} in namespace {actionModal.namespace}?

This will trigger a rolling restart of all pods in the deployment.

)} {actionModal.type === 'delete' && (

Are you sure you want to delete {actionModal.resourceType} {actionModal.name} in namespace {actionModal.namespace}?

This action cannot be undone!

)}
)} {/* ═══ GLOBAL SEARCH MODAL (Cmd+K) ═══ */} {showSearchModal && (
setShowSearchModal(false)} />
setSearchQuery(e.target.value)} placeholder="Search deployments, pods, services, databases, events..." className="flex-1 text-sm text-gray-900 placeholder-gray-400 bg-transparent outline-none" /> ESC
{searchQuery.trim() === '' ? (
Type to search across all resource types...
) : searchResults.length === 0 ? (
No results for "{searchQuery}"
) : (
{searchResults.map((result, idx) => ( ))}
)}
)}
) }