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..2076c4e3 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" @@ -77,7 +79,8 @@ func appendOptionalManagedResourceTargets(resourceTargets []managedResourceTarge func dedupeManagedResources(items []interface{}) []interface{} { allResources := make([]interface{}, 0, len(items)) - seenResourceKeys := make(map[string]struct{}) + seenUIDs := make(map[string]struct{}, len(items)) + seenFallbackKeys := make(map[string]struct{}, len(items)) for _, item := range items { itemMap, ok := item.(map[string]interface{}) @@ -86,22 +89,27 @@ 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{}{} + } else { + // Use name/namespace as fallback key when no UID + name, _ := metadata["name"].(string) + namespace, _ := metadata["namespace"].(string) + fallbackKey := fmt.Sprintf("%s/%s", namespace, name) + if _, seen := seenFallbackKeys[fallbackKey]; seen { + continue + } + seenFallbackKeys[fallbackKey] = struct{}{} } - seenResourceKeys[resourceKey] = struct{}{} allResources = append(allResources, itemMap) } @@ -109,11 +117,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 +137,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 +180,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 +201,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 +276,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 +311,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 +353,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 +361,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 +389,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/ContextSelector.jsx b/src/presentation/components/layout/ContextSelector.jsx index e7d5a5db..7df1b313 100644 --- a/src/presentation/components/layout/ContextSelector.jsx +++ b/src/presentation/components/layout/ContextSelector.jsx @@ -2,19 +2,13 @@ import { Box, Text, HStack, - Icon, } from '@chakra-ui/react'; -import { FiAlertCircle } from 'react-icons/fi'; import { useAppContext } from '../../providers/AppProvider.jsx'; import { Dropdown } from '../common/Dropdown.jsx'; import { getBackgroundColor, getTextColor } from '../../utils/theme.js'; export const ContextSelector = () => { - const { contexts, selectedContext, setSelectedContext, contextErrors, colorMode } = useAppContext(); - - const handleSelect = async (contextName) => { - await setSelectedContext(contextName); - }; + const { contexts, selectedContext, setSelectedContext, colorMode } = useAppContext(); if (contexts.length === 0) { return ( @@ -35,7 +29,7 @@ export const ContextSelector = () => { const contextName = typeof selectedContext === 'string' ? selectedContext : selectedContext?.name || selectedContext; const options = contexts.map(context => { const name = typeof context === 'string' ? context : context.name || context; - const hasError = contextErrors[name]; + const hasError = false; return { value: name, label: name, @@ -48,7 +42,7 @@ export const ContextSelector = () => { w="100%" placeholder="Select context" value={contextName} - onChange={handleSelect} + onChange={setSelectedContext} options={options} /> ); diff --git a/src/presentation/components/layout/ContextSidebar.jsx b/src/presentation/components/layout/ContextSidebar.jsx index 83cbd764..470bd3f4 100644 --- a/src/presentation/components/layout/ContextSidebar.jsx +++ b/src/presentation/components/layout/ContextSidebar.jsx @@ -11,17 +11,13 @@ import { useAppContext } from '../../providers/AppProvider.jsx'; import { getBorderColor, getBackgroundColor, getTextColor, getAccentColor, getStatusColor } from '../../utils/theme.js'; export const ContextSidebar = () => { - const { contexts, selectedContext, setSelectedContext, contextErrors, colorMode, isInClusterMode } = useAppContext(); + const { contexts, selectedContext, setSelectedContext, colorMode, isInClusterMode } = useAppContext(); const navigate = useNavigate(); if (isInClusterMode) { return null; } - const handleContextClick = async (contextName) => { - await setSelectedContext(contextName); - }; - const getFirstLetter = (name) => { if (!name) return '?'; return name.charAt(0).toUpperCase(); @@ -76,12 +72,12 @@ export const ContextSidebar = () => { {contexts.map((context) => { const name = typeof context === 'string' ? context : context.name || context; const isSelected = contextName === name; - const hasError = contextErrors[name]; + const hasError = false; return ( handleContextClick(name)} + onClick={() => setSelectedContext(name)} w="100%" p={0} borderRadius="lg" @@ -93,11 +89,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/ContextManagement.jsx b/src/presentation/pages/ContextManagement.jsx index ebff837a..8b403db9 100644 --- a/src/presentation/pages/ContextManagement.jsx +++ b/src/presentation/pages/ContextManagement.jsx @@ -15,7 +15,7 @@ import { Dialog } from '../components/common/Dialog.jsx'; import { useAppContext } from '../providers/AppProvider.jsx'; export const ContextManagement = () => { - const { contexts, kubernetesRepository, getKubernetesContextsUseCase, selectedContext, contextErrors, colorMode } = useAppContext(); + const { contexts, kubernetesRepository, getKubernetesContextsUseCase, selectedContext, colorMode } = useAppContext(); const [kubeConfigText, setKubeConfigText] = useState(''); const [loading, setLoading] = useState(false); const [deletingContext, setDeletingContext] = useState(null); @@ -199,7 +199,7 @@ export const ContextManagement = () => { {contexts.map((contextName) => { const name = typeof contextName === 'string' ? contextName : contextName?.name || contextName; const isSelected = selectedContext === name; - const hasError = contextErrors[name]; + const hasError = false; const getFirstLetter = (n) => n ? n.charAt(0).toUpperCase() : '?'; return ( 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..5580eac3 100644 --- a/src/presentation/providers/AppProvider.jsx +++ b/src/presentation/providers/AppProvider.jsx @@ -37,7 +37,6 @@ export const AppProvider = ({ children }) => { const [authChecked, setAuthChecked] = useState(false); const [authMode, setAuthMode] = useState(null); const [serverError, setServerError] = useState(null); - const [contextErrors, setContextErrors] = useState({}); const [workingContexts, setWorkingContexts] = useState(() => { try { const saved = localStorage.getItem('workingContexts'); @@ -162,12 +161,9 @@ export const AppProvider = ({ children }) => { localStorage.setItem('workingContexts', JSON.stringify(updated)); return updated; }); - } else { - await tryFindWorkingContext(contextsList, contextNames); } } catch (error) { - console.warn(`Context ${contextToSet} is not working, trying others...`); - await tryFindWorkingContext(contextsList, contextNames); + console.warn(`Context ${contextToSet} is not working...`); } } } @@ -177,27 +173,7 @@ export const AppProvider = ({ children }) => { } }; - const tryFindWorkingContext = async (contextsList, contextNames) => { - for (const contextName of contextNames) { - try { - const isValid = await kubernetesRepository.isConnected(contextName); - if (isValid) { - await kubernetesRepository.setContext(contextName); - setSelectedContext(contextName); - localStorage.setItem('lastUsedContext', contextName); - setWorkingContexts(prev => { - const updated = prev.includes(contextName) ? prev : [...prev, contextName]; - localStorage.setItem('workingContexts', JSON.stringify(updated)); - return updated; - }); - return; - } - } catch (error) { - continue; - } - } - }; - + if (contexts.length === 0 && user) { loadContexts(); } @@ -220,120 +196,10 @@ export const AppProvider = ({ children }) => { }; }, [user, getKubernetesContextsUseCase]); - useEffect(() => { - const checkContextConnection = async () => { - if (!selectedContext || !user) return; - - const contextNameStr = typeof selectedContext === 'string' ? selectedContext : selectedContext?.name || selectedContext; - if (!contextNameStr) return; - - try { - const isConnected = await kubernetesRepository.isConnected(contextNameStr); - - if (!isConnected) { - 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; - }); - } else { - setContextErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[contextNameStr]; - return newErrors; - }); - setWorkingContexts(prev => { - if (!prev.includes(contextNameStr)) { - const updated = [...prev, contextNameStr]; - localStorage.setItem('workingContexts', JSON.stringify(updated)); - return updated; - } - return prev; - }); - } - } catch (error) { - 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; - }); - } - }; - - checkContextConnection(); - }, [selectedContext, user, kubernetesRepository]); - - const handleContextChange = async (contextName) => { - try { - const contextNameStr = typeof contextName === 'string' ? contextName : contextName?.name || contextName; - await kubernetesRepository.setContext(contextNameStr); - setSelectedContext(contextNameStr); - localStorage.setItem('lastUsedContext', contextNameStr); - - const isConnected = await kubernetesRepository.isConnected(contextNameStr); - if (isConnected) { - setWorkingContexts(prev => { - if (!prev.includes(contextNameStr)) { - const updated = [...prev, contextNameStr]; - localStorage.setItem('workingContexts', JSON.stringify(updated)); - return updated; - } - return prev; - }); - setContextErrors(prev => { - const newErrors = { ...prev }; - delete newErrors[contextNameStr]; - return newErrors; - }); - } 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; - }); - } - } 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; - }); - } - }; - - const handleLogin = async (credentials) => { - const result = await authService.login(credentials); - setUser(result.user); - return result; - }; - - const handleRegister = async (data) => { - const result = await authService.register(data); - setUser(result.user); - return result; - }; - - const handleLogout = async () => { - await authService.logout(); - setUser(null); + const handleContextChange = (contextName) => { + const contextNameStr = typeof contextName === 'string' ? contextName : contextName?.name || contextName; + setSelectedContext(contextNameStr); + localStorage.setItem('lastUsedContext', contextNameStr); }; const handleColorModeChange = (mode) => { @@ -365,46 +231,54 @@ export const AppProvider = ({ children }) => { localStorage.setItem('savedSearches', JSON.stringify(updated)); }; - const handleLoadSearch = (searchQuery) => { - }; - const handleDeleteSearch = (searchId) => { const updated = savedSearches.filter(s => s.id !== searchId); setSavedSearches(updated); localStorage.setItem('savedSearches', JSON.stringify(updated)); }; + const handleLogin = async (credentials) => { + const result = await authService.login(credentials); + setUser(result.user); + return result; + }; + + const handleRegister = async (data) => { + const result = await authService.register(data); + setUser(result.user); + return result; + }; + + const handleLogout = async () => { + await authService.logout(); + setUser(null); + }; + const value = useMemo(() => { - const contextName = typeof selectedContext === 'string' ? selectedContext : selectedContext?.name || selectedContext; - const selectedContextError = contextName ? contextErrors[contextName] : null; - return { - kubernetesRepository, - getDashboardDataUseCase, - getKubernetesContextsUseCase, - authService, - userService, - selectedContext, - contexts, - authMode, - setSelectedContext: handleContextChange, - user, - authChecked, - serverError, - contextErrors, - selectedContextError, - login: handleLogin, - register: handleRegister, - logout: handleLogout, - colorMode, - setColorMode: handleColorModeChange, - savedSearches, - saveSearch: handleSaveSearch, - loadSearch: handleLoadSearch, - deleteSearch: handleDeleteSearch, - isInClusterMode, + kubernetesRepository, + getDashboardDataUseCase, + getKubernetesContextsUseCase, + authService, + userService, + selectedContext, + contexts, + authMode, + setSelectedContext: handleContextChange, + user, + authChecked, + serverError, + login: handleLogin, + register: handleRegister, + logout: handleLogout, + colorMode, + setColorMode: handleColorModeChange, + savedSearches, + saveSearch: handleSaveSearch, + deleteSearch: handleDeleteSearch, + isInClusterMode, }; - }, [kubernetesRepository, getDashboardDataUseCase, getKubernetesContextsUseCase, authService, userService, selectedContext, contexts, user, authChecked, serverError, contextErrors, colorMode, savedSearches, isInClusterMode,authMode]); + }, [kubernetesRepository, getDashboardDataUseCase, getKubernetesContextsUseCase, authService, userService, selectedContext, contexts, user, authChecked, serverError, colorMode, savedSearches, isInClusterMode, authMode]); return (