From 3a7f9a69d7a3033b5425d20f60dba25cc7b105f2 Mon Sep 17 00:00:00 2001 From: mheidar1 Date: Fri, 8 May 2026 18:18:32 +0200 Subject: [PATCH] fix(cache): Fix cching issue and cache readability from MRDs and MRAPs Signed-off-by: mheidar1 --- .../services/kubernetes_context.go | 42 ++++- .../services/kubernetes_managed.go | 166 ++++++++++-------- ...anagedResourceActivationPoliciesUseCase.js | 8 +- .../GetManagedResourceDefinitionsUseCase.js | 8 +- .../components/layout/ContextSidebar.jsx | 8 +- src/presentation/components/layout/Layout.jsx | 111 +++++++++--- src/presentation/hooks/useContextData.js | 41 +++++ src/presentation/pages/Providers.jsx | 24 ++- src/presentation/providers/AppProvider.jsx | 85 +++++---- 9 files changed, 337 insertions(+), 156 deletions(-) create mode 100644 src/presentation/hooks/useContextData.js diff --git a/crossview-go-server/services/kubernetes_context.go b/crossview-go-server/services/kubernetes_context.go index 461cb3cc..d714e8ef 100644 --- a/crossview-go-server/services/kubernetes_context.go +++ b/crossview-go-server/services/kubernetes_context.go @@ -30,6 +30,36 @@ func (k *KubernetesService) getKubeConfigPath() string { return filepath.Join(homeDir, ".kube", "config") } +func (k *KubernetesService) normalizeKubeConfigPaths() error { + kubeConfigPath := k.getKubeConfigPath() + kubeConfigDir := filepath.Dir(kubeConfigPath) + + for clusterName, cluster := range k.kubeConfig.Clusters { + if cluster != nil && cluster.CertificateAuthority != "" { + certPath := cluster.CertificateAuthority + if !filepath.IsAbs(certPath) { + absPath := filepath.Join(kubeConfigDir, certPath) + cluster.CertificateAuthority = absPath + k.kubeConfig.Clusters[clusterName] = cluster + } + } + } + + for authName, authInfo := range k.kubeConfig.AuthInfos { + if authInfo != nil { + if authInfo.ClientCertificate != "" && !filepath.IsAbs(authInfo.ClientCertificate) { + authInfo.ClientCertificate = filepath.Join(kubeConfigDir, authInfo.ClientCertificate) + } + if authInfo.ClientKey != "" && !filepath.IsAbs(authInfo.ClientKey) { + authInfo.ClientKey = filepath.Join(kubeConfigDir, authInfo.ClientKey) + } + k.kubeConfig.AuthInfos[authName] = authInfo + } + } + + return nil +} + func (k *KubernetesService) loadKubeConfig() error { kubeConfigPath := k.getKubeConfigPath() if kubeConfigPath == "" { @@ -46,12 +76,15 @@ func (k *KubernetesService) loadKubeConfig() error { } k.kubeConfig = config + if err := k.normalizeKubeConfigPaths(); err != nil { + return err + } return nil } func (k *KubernetesService) isInCluster() bool { serviceAccountPath := "/var/run/secrets/kubernetes.io/serviceaccount" - return fileExists(serviceAccountPath) && + return fileExists(serviceAccountPath) && fileExists(filepath.Join(serviceAccountPath, "token")) && fileExists(filepath.Join(serviceAccountPath, "ca.crt")) } @@ -120,7 +153,7 @@ func (k *KubernetesService) SetContext(ctxName string) error { k.clientset = clientset k.dynamicClient = nil delete(k.failedContexts, targetContext) - + // Clear managed resources cache when context changes k.managedResourcesCache = make(map[string]map[string]interface{}) k.managedResourcesCacheTime = make(map[string]time.Time) @@ -135,7 +168,7 @@ func (k *KubernetesService) SetContext(ctxName string) error { func (k *KubernetesService) IsConnected(ctxName string) (bool, error) { originalContext := k.GetCurrentContext() - + if err := k.SetContext(ctxName); err != nil { return false, err } @@ -155,7 +188,7 @@ func (k *KubernetesService) IsConnected(ctxName string) (bool, error) { if originalContext != "" && originalContext != ctxName { k.SetContext(originalContext) } - + if err != nil { return false, err } @@ -281,4 +314,3 @@ func (k *KubernetesService) RemoveContext(ctxName string) error { return nil } - diff --git a/crossview-go-server/services/kubernetes_managed.go b/crossview-go-server/services/kubernetes_managed.go index c811674b..b1bb8280 100644 --- a/crossview-go-server/services/kubernetes_managed.go +++ b/crossview-go-server/services/kubernetes_managed.go @@ -6,6 +6,8 @@ import ( "sync" "time" + "crossview-go-server/lib" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -67,17 +69,13 @@ func buildManagedResourceTargetsFromMRDs(mrdList []map[string]interface{}) []man func appendOptionalManagedResourceTargets(resourceTargets []managedResourceTarget) []managedResourceTarget { return append(resourceTargets, managedResourceTarget{apiVersion: "pkg.crossplane.io/v1", kind: "ManagedResourceDefinition", plural: "managedresourcedefinitions"}, - managedResourceTarget{apiVersion: "pkg.crossplane.io/v1beta1", kind: "ManagedResourceDefinition", plural: "managedresourcedefinitions"}, - managedResourceTarget{apiVersion: "pkg.crossplane.io/v1alpha1", kind: "ManagedResourceDefinition", plural: "managedresourcedefinitions"}, managedResourceTarget{apiVersion: "pkg.crossplane.io/v1", kind: "ManagedResourceActivationPolicy", plural: "managedresourceactivationpolicies"}, - managedResourceTarget{apiVersion: "pkg.crossplane.io/v1beta1", kind: "ManagedResourceActivationPolicy", plural: "managedresourceactivationpolicies"}, - managedResourceTarget{apiVersion: "pkg.crossplane.io/v1alpha1", kind: "ManagedResourceActivationPolicy", plural: "managedresourceactivationpolicies"}, ) } func dedupeManagedResources(items []interface{}) []interface{} { allResources := make([]interface{}, 0, len(items)) - seenResourceKeys := make(map[string]struct{}) + seenUIDs := make(map[string]struct{}, len(items)) for _, item := range items { itemMap, ok := item.(map[string]interface{}) @@ -86,22 +84,18 @@ func dedupeManagedResources(items []interface{}) []interface{} { } metadata, _ := itemMap["metadata"].(map[string]interface{}) - uid, _ := metadata["uid"].(string) - name, _ := metadata["name"].(string) - namespace, _ := metadata["namespace"].(string) - apiVersion, _ := itemMap["apiVersion"].(string) - kind, _ := itemMap["kind"].(string) - - resourceKey := uid - if resourceKey == "" { - resourceKey = fmt.Sprintf("%s|%s|%s|%s", apiVersion, kind, namespace, name) + if metadata == nil { + continue } - if _, exists := seenResourceKeys[resourceKey]; exists { - continue + uid, _ := metadata["uid"].(string) + if uid != "" { + if _, seen := seenUIDs[uid]; seen { + continue + } + seenUIDs[uid] = struct{}{} } - seenResourceKeys[resourceKey] = struct{}{} allResources = append(allResources, itemMap) } @@ -109,11 +103,18 @@ func dedupeManagedResources(items []interface{}) []interface{} { } func (k *KubernetesService) fetchManagedResourceTarget(contextName string, target managedResourceTarget) ([]interface{}, error) { - continueToken := "" allItems := make([]interface{}, 0) + continueToken := "" + pageSize := int64(1000) + maxItems := int64(5000) + itemCount := int64(0) for { - result, err := k.GetResources(target.apiVersion, target.kind, "", contextName, target.plural, nil, continueToken) + if itemCount >= maxItems { + break + } + + result, err := k.GetResources(target.apiVersion, target.kind, "", contextName, target.plural, &pageSize, continueToken) if err != nil { return nil, err } @@ -122,13 +123,13 @@ func (k *KubernetesService) fetchManagedResourceTarget(contextName string, targe if items != nil { for _, item := range items { if itemMap, ok := item.(map[string]interface{}); ok { - itemMapCopy := make(map[string]interface{}) - for key, val := range itemMap { - itemMapCopy[key] = val + itemMap["apiVersion"] = target.apiVersion + itemMap["kind"] = target.kind + allItems = append(allItems, itemMap) + itemCount++ + if itemCount >= maxItems { + break } - itemMapCopy["apiVersion"] = target.apiVersion - itemMapCopy["kind"] = target.kind - allItems = append(allItems, itemMapCopy) } } } @@ -165,14 +166,9 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh if cacheTime, timeExists := k.managedResourcesCacheTime[contextName]; timeExists { if time.Since(cacheTime) < k.managedResourcesCacheTTL { k.logger.Infof("Returning cached managed resources for context: %s", contextName) - // Create a copy with fromCache: true - result := make(map[string]interface{}) - for key, value := range cachedResult { - result[key] = value - } - result["fromCache"] = true + cachedResult["fromCache"] = true k.mu.RUnlock() - return result, nil + return cachedResult, nil } } } @@ -191,24 +187,61 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh return nil, fmt.Errorf("failed to create dynamic client: %w", err) } - providersResult, err := k.GetResources("pkg.crossplane.io/v1", "Provider", "", contextName, "", nil, "") - if err != nil { - return nil, fmt.Errorf("failed to get providers: %w", err) + var providers, revisions []interface{} + var provErr, revErr error + var provWg sync.WaitGroup + provWg.Add(2) + + go func() { + defer provWg.Done() + providersResult, err := k.GetResources("pkg.crossplane.io/v1", "Provider", "", contextName, "", nil, "") + if err != nil { + provErr = err + return + } + providers, _ = providersResult["items"].([]interface{}) + }() + + go func() { + defer provWg.Done() + revisionsResult, err := k.GetResources("pkg.crossplane.io/v1", "ProviderRevision", "", contextName, "", nil, "") + if err != nil { + revErr = err + return + } + revisions, _ = revisionsResult["items"].([]interface{}) + }() + + provWg.Wait() + if provErr != nil { + return nil, fmt.Errorf("failed to get providers: %w", provErr) + } + if revErr != nil { + return nil, fmt.Errorf("failed to get provider revisions: %w", revErr) } - providers, _ := providersResult["items"].([]interface{}) if providers == nil { providers = []interface{}{} } - - revisionsResult, err := k.GetResources("pkg.crossplane.io/v1", "ProviderRevision", "", contextName, "", nil, "") - if err != nil { - return nil, fmt.Errorf("failed to get provider revisions: %w", err) - } - revisions, _ := revisionsResult["items"].([]interface{}) if revisions == nil { revisions = []interface{}{} } + providerNameMap := make(map[string]bool) + for _, prov := range providers { + provMap, _ := prov.(map[string]interface{}) + if provMap == nil { + continue + } + provMetadata, _ := provMap["metadata"].(map[string]interface{}) + if provMetadata == nil { + continue + } + provName, _ := provMetadata["name"].(string) + if provName != "" { + providerNameMap[provName] = true + } + } + revisionToProvider := make(map[string]string) for _, rev := range revisions { revMap, _ := rev.(map[string]interface{}) @@ -229,22 +262,9 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh ownerKind, _ := owner["kind"].(string) ownerAPIVersion, _ := owner["apiVersion"].(string) ownerName, _ := owner["name"].(string) - if ownerKind == "Provider" && ownerAPIVersion == "pkg.crossplane.io/v1" { - for _, prov := range providers { - provMap, _ := prov.(map[string]interface{}) - if provMap == nil { - continue - } - provMetadata, _ := provMap["metadata"].(map[string]interface{}) - if provMetadata == nil { - continue - } - provName, _ := provMetadata["name"].(string) - if provName == ownerName { - revisionToProvider[revName] = ownerName - break - } - } + if ownerKind == "Provider" && ownerAPIVersion == "pkg.crossplane.io/v1" && providerNameMap[ownerName] { + revisionToProvider[revName] = ownerName + break } } } @@ -277,22 +297,8 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh ownerName, _ := owner["name"].(string) var providerName string - if ownerKind == "Provider" && ownerAPIVersion == "pkg.crossplane.io/v1" { - for _, prov := range providers { - provMap, _ := prov.(map[string]interface{}) - if provMap == nil { - continue - } - provMetadata, _ := provMap["metadata"].(map[string]interface{}) - if provMetadata == nil { - continue - } - provName, _ := provMetadata["name"].(string) - if provName == ownerName { - providerName = ownerName - break - } - } + if ownerKind == "Provider" && ownerAPIVersion == "pkg.crossplane.io/v1" && providerNameMap[ownerName] { + providerName = ownerName } else if ownerKind == "ProviderRevision" && ownerAPIVersion == "pkg.crossplane.io/v1" { providerName = revisionToProvider[ownerName] } @@ -333,6 +339,7 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh resourceTargets := appendOptionalManagedResourceTargets(buildManagedResourceTargetsFromMRDs(mrdList)) + semaphore := make(chan struct{}, 10) resourceChan := make(chan resourceResult, len(resourceTargets)) var wg sync.WaitGroup @@ -340,12 +347,18 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh wg.Add(1) go func(target managedResourceTarget) { defer wg.Done() + semaphore <- struct{}{} + defer func() { <-semaphore }() items, err := k.fetchManagedResourceTarget(contextName, target) if err != nil { - resourceChan <- resourceResult{items: nil, err: err} + if !lib.IsMissingKubernetesResourceError(err) { + resourceChan <- resourceResult{items: nil, err: err} + } return } - resourceChan <- resourceResult{items: items, err: nil} + if items != nil && len(items) > 0 { + resourceChan <- resourceResult{items: items, err: nil} + } }(target) } @@ -362,7 +375,6 @@ func (k *KubernetesService) GetManagedResources(contextName string, forceRefresh } allResources = dedupeManagedResources(allResources) - // Cache the results result := map[string]interface{}{ "items": allResources, "fromCache": false, diff --git a/src/domain/usecases/GetManagedResourceActivationPoliciesUseCase.js b/src/domain/usecases/GetManagedResourceActivationPoliciesUseCase.js index 20b5f383..aa67d67c 100644 --- a/src/domain/usecases/GetManagedResourceActivationPoliciesUseCase.js +++ b/src/domain/usecases/GetManagedResourceActivationPoliciesUseCase.js @@ -5,10 +5,10 @@ export class GetManagedResourceActivationPoliciesUseCase { async execute(context = null) { try { - const apiVersion = 'apiextensions.crossplane.io/v1alpha1'; - const kind = 'ManagedResourceActivationPolicy'; - const mrapsResult = await this.kubernetesRepository.getResources(apiVersion, kind, null, context); - const mraps = mrapsResult.items || mrapsResult; // Support both new format and legacy array format + const managedResourcesResult = await this.kubernetesRepository.getManagedResources(context); + const allResources = managedResourcesResult.items || []; + + const mraps = allResources.filter(item => item.kind === 'ManagedResourceActivationPolicy'); const mrapsArray = Array.isArray(mraps) ? mraps : []; return mrapsArray.map(mrap => ({ diff --git a/src/domain/usecases/GetManagedResourceDefinitionsUseCase.js b/src/domain/usecases/GetManagedResourceDefinitionsUseCase.js index ae8a8f5c..f08f0f51 100644 --- a/src/domain/usecases/GetManagedResourceDefinitionsUseCase.js +++ b/src/domain/usecases/GetManagedResourceDefinitionsUseCase.js @@ -5,10 +5,10 @@ export class GetManagedResourceDefinitionsUseCase { async execute(context = null) { try { - const apiVersion = 'apiextensions.crossplane.io/v1alpha1'; - const kind = 'ManagedResourceDefinition'; - const mrdsResult = await this.kubernetesRepository.getResources(apiVersion, kind, null, context); - const mrds = mrdsResult.items || mrdsResult; // Support both new format and legacy array format + const managedResourcesResult = await this.kubernetesRepository.getManagedResources(context); + const allResources = managedResourcesResult.items || []; + + const mrds = allResources.filter(item => item.kind === 'ManagedResourceDefinition'); const mrdsArray = Array.isArray(mrds) ? mrds : []; return mrdsArray.map(mrd => { diff --git a/src/presentation/components/layout/ContextSidebar.jsx b/src/presentation/components/layout/ContextSidebar.jsx index 83cbd764..16f3c1f9 100644 --- a/src/presentation/components/layout/ContextSidebar.jsx +++ b/src/presentation/components/layout/ContextSidebar.jsx @@ -93,11 +93,17 @@ export const ContextSidebar = () => { color={isSelected ? getTextColor(colorMode, 'inverse') : getTextColor(colorMode, 'primary')} _hover={{ bg: isSelected ? getAccentColor('blue', 'medium') : getBackgroundColor(colorMode, 'tertiary'), - _dark: { bg: isSelected ? getAccentColor('blue', 'dark') : getBackgroundColor('dark', 'tertiary') } + border: '1px solid', + borderColor: getAccentColor('blue', 'light'), + _dark: { + bg: isSelected ? getAccentColor('blue', 'dark') : getBackgroundColor('dark', 'tertiary'), + borderColor: getAccentColor('blue', 'primary') + } }} position="relative" transition="all 0.2s" h="44px" + cursor="default" title={name} > diff --git a/src/presentation/components/layout/Layout.jsx b/src/presentation/components/layout/Layout.jsx index f3908714..d7d18613 100644 --- a/src/presentation/components/layout/Layout.jsx +++ b/src/presentation/components/layout/Layout.jsx @@ -1,5 +1,5 @@ -import { Box, Text, HStack, Icon } from '@chakra-ui/react'; -import { FiAlertCircle } from 'react-icons/fi'; +import { Box, Text, HStack, Icon, Button, VStack } from '@chakra-ui/react'; +import { FiAlertCircle, FiChevronRight } from 'react-icons/fi'; import { useState, useEffect } from 'react'; import { Sidebar } from './Sidebar.jsx'; import { ContextSidebar } from './ContextSidebar.jsx'; @@ -11,7 +11,7 @@ import { useOnWatchResources } from '../../providers/OnWatchResourcesProvider.js import { getBackgroundColor } from '../../utils/theme.js'; export const Layout = ({ children }) => { - const { colorMode, selectedContextError, selectedContext, isInClusterMode } = useAppContext(); + const { colorMode, selectedContextError, selectedContext, isInClusterMode, contexts, setSelectedContext } = useAppContext(); const { watchedResources, isCollapsed: isOnWatchCollapsed, notifications, removeNotification } = useOnWatchResources(); const [sidebarWidth, setSidebarWidth] = useState(280); const [contextSidebarWidth, setContextSidebarWidth] = useState(60); @@ -22,6 +22,20 @@ export const Layout = ({ children }) => { }); const onWatchWidth = watchedResources.length > 0 && !isOnWatchCollapsed ? 400 : 0; + const getWorkingContexts = () => { + try { + const workingCtxs = JSON.parse(localStorage.getItem('workingContexts') || '[]'); + const contextNames = contexts.map(ctx => typeof ctx === 'string' ? ctx : ctx?.name || ctx); + return contextNames.filter(ctx => workingCtxs.includes(ctx) && ctx !== selectedContext); + } catch { + return []; + } + }; + + const workingContexts = getWorkingContexts(); + const hasContextError = selectedContextError && selectedContext; + const showMainSidebar = !hasContextError; + const handleSidebarToggle = (collapsed, width) => { setSidebarWidth(width || (collapsed ? 60 : 280)); }; @@ -51,7 +65,70 @@ export const Layout = ({ children }) => { }, [isInClusterMode]); const bgColor = getBackgroundColor(colorMode, 'html'); - const totalLeftWidth = sidebarWidth + (isInClusterMode ? 0 : contextSidebarWidth); + const totalLeftWidth = showMainSidebar ? sidebarWidth + (isInClusterMode ? 0 : contextSidebarWidth) : 0; + + if (hasContextError) { + return ( + + + + + + + + + + Context Connection Error + + + {selectedContextError} + + + + {workingContexts.length > 0 && ( + + + Switch to an available context: + + + {workingContexts.map((ctx) => ( + + ))} + + + )} + + + + + + ); + } return ( @@ -67,31 +144,7 @@ export const Layout = ({ children }) => { >
- {selectedContextError && selectedContext && ( - - - - - - - Context Connection Error - - - {selectedContextError} - - - - - - )} - + {children} diff --git a/src/presentation/hooks/useContextData.js b/src/presentation/hooks/useContextData.js new file mode 100644 index 00000000..08a801a7 --- /dev/null +++ b/src/presentation/hooks/useContextData.js @@ -0,0 +1,41 @@ +import { useEffect, useRef, useCallback } from 'react'; + +export const useContextData = (dependencies = []) => { + const abortControllerRef = useRef(null); + const isMountedRef = useRef(true); + + const executeWithAbort = useCallback(async (asyncFn) => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + try { + const result = await asyncFn(signal); + if (!signal.aborted && isMountedRef.current) { + return result; + } + return null; + } catch (err) { + if (signal.aborted) { + return null; + } + throw err; + } + }, []); + + useEffect(() => { + isMountedRef.current = true; + + return () => { + isMountedRef.current = false; + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, dependencies); + + return { executeWithAbort, isMounted: isMountedRef }; +}; diff --git a/src/presentation/pages/Providers.jsx b/src/presentation/pages/Providers.jsx index ba8bf583..c24e7512 100644 --- a/src/presentation/pages/Providers.jsx +++ b/src/presentation/pages/Providers.jsx @@ -30,6 +30,7 @@ export const Providers = () => { const [searchQuery, setSearchQuery] = useState(''); const [useSplitView, setUseSplitView] = useState(false); const gridContainerRef = useRef(null); + const loadingContextRef = useRef(null); // Close resource detail when route changes useEffect(() => { @@ -38,26 +39,43 @@ export const Providers = () => { }, [location.pathname]); useEffect(() => { + let isMounted = true; + const loadProviders = async () => { if (!selectedContext) { setLoading(false); return; } + + const currentContext = selectedContext; + loadingContextRef.current = currentContext; + try { setLoading(true); setError(null); const contextName = typeof selectedContext === 'string' ? selectedContext : selectedContext.name || selectedContext; const useCase = new GetProvidersUseCase(kubernetesRepository); const data = await useCase.execute(contextName); - setProviders(data); + + if (isMounted && loadingContextRef.current === currentContext) { + setProviders(data); + } } catch (err) { - setError(err.message); + if (isMounted && loadingContextRef.current === currentContext) { + setError(err.message); + } } finally { - setLoading(false); + if (isMounted && loadingContextRef.current === currentContext) { + setLoading(false); + } } }; loadProviders(); + + return () => { + isMounted = false; + }; }, [selectedContext, kubernetesRepository]); useEffect(() => { diff --git a/src/presentation/providers/AppProvider.jsx b/src/presentation/providers/AppProvider.jsx index 79755d6d..d6472b31 100644 --- a/src/presentation/providers/AppProvider.jsx +++ b/src/presentation/providers/AppProvider.jsx @@ -240,6 +240,7 @@ export const AppProvider = ({ children }) => { localStorage.setItem('workingContexts', JSON.stringify(updated)); return updated; }); + tryNextWorkingContext(contextNameStr); } else { setContextErrors(prev => { const newErrors = { ...prev }; @@ -265,6 +266,7 @@ export const AppProvider = ({ children }) => { localStorage.setItem('workingContexts', JSON.stringify(updated)); return updated; }); + tryNextWorkingContext(contextNameStr); } }; @@ -274,48 +276,65 @@ export const AppProvider = ({ children }) => { const handleContextChange = async (contextName) => { try { const contextNameStr = typeof contextName === 'string' ? contextName : contextName?.name || contextName; - await kubernetesRepository.setContext(contextNameStr); + + try { + await kubernetesRepository.setContext(contextNameStr); + } catch (error) { + console.warn('Error setting context:', error); + } + setSelectedContext(contextNameStr); localStorage.setItem('lastUsedContext', contextNameStr); + setContextErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[contextNameStr]; + return newErrors; + }); - const isConnected = await kubernetesRepository.isConnected(contextNameStr); - if (isConnected) { - setWorkingContexts(prev => { - if (!prev.includes(contextNameStr)) { - const updated = [...prev, contextNameStr]; + kubernetesRepository.isConnected(contextNameStr).then(isConnected => { + if (isConnected) { + setWorkingContexts(prev => { + if (!prev.includes(contextNameStr)) { + const updated = [...prev, contextNameStr]; + localStorage.setItem('workingContexts', JSON.stringify(updated)); + return updated; + } + return prev; + }); + } else { + setContextErrors(prev => ({ + ...prev, + [contextNameStr]: 'Unable to connect to the Kubernetes cluster. Please check your connection settings.' + })); + setWorkingContexts(prev => { + const updated = prev.filter(ctx => ctx !== contextNameStr); localStorage.setItem('workingContexts', JSON.stringify(updated)); return updated; - } - return prev; - }); - setContextErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[contextNameStr]; - return newErrors; - }); - } else { + }); + tryNextWorkingContext(contextNameStr); + } + }).catch(error => { + console.error('Error checking context connection:', error); setContextErrors(prev => ({ ...prev, - [contextNameStr]: 'Unable to connect to the Kubernetes cluster. Please check your connection settings.' + [contextNameStr]: error.message || 'Unable to connect to the Kubernetes cluster.' })); - setWorkingContexts(prev => { - const updated = prev.filter(ctx => ctx !== contextNameStr); - localStorage.setItem('workingContexts', JSON.stringify(updated)); - return updated; - }); - } - } catch (error) { - console.error('Failed to set context:', error); - const contextNameStr = typeof contextName === 'string' ? contextName : contextName?.name || contextName; - setContextErrors(prev => ({ - ...prev, - [contextNameStr]: error.message || 'Failed to connect to the Kubernetes cluster.' - })); - setWorkingContexts(prev => { - const updated = prev.filter(ctx => ctx !== contextNameStr); - localStorage.setItem('workingContexts', JSON.stringify(updated)); - return updated; }); + } catch (error) { + console.error('Error switching context:', error); + } + }; + + const tryNextWorkingContext = (failedContext) => { + const contextNames = contexts.map(ctx => typeof ctx === 'string' ? ctx : ctx?.name || ctx); + const workingCtxList = JSON.parse(localStorage.getItem('workingContexts') || '[]'); + + const availableContexts = contextNames.filter(ctx => ctx !== failedContext && workingCtxList.includes(ctx)); + + if (availableContexts.length > 0) { + setTimeout(() => { + handleContextChange(availableContexts[0]); + }, 500); } };