diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 87cb557c5ea..0a219bc25a6 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -234,6 +234,18 @@ func serveWithNoCacheHeader(fs http.Handler) http.HandlerFunc { } } +// defaultKubeConfigFile returns the default path to the kubeconfig file. +func defaultKubeConfigFile() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %v", err) + } + + kubeConfigFile := filepath.Join(homeDir, ".kube", "config") + + return kubeConfigFile, nil +} + // defaultKubeConfigPersistenceDir returns the default directory to store kubeconfig // files of clusters that are loaded in Headlamp. func defaultKubeConfigPersistenceDir() (string, error) { @@ -1365,6 +1377,126 @@ func (c *HeadlampConfig) addContextsToStore(contexts []kubeconfig.Context, setup return setupErrors } +// collectMultiConfigPaths looks at the default dynamic directory +// (e.g. ~/.config/Headlamp/kubeconfigs) and returns any files found there. +// This is called from the 'else' block in deleteCluster(). +func (c *HeadlampConfig) collectMultiConfigPaths() ([]string, error) { + dynamicDir, err := defaultKubeConfigPersistenceDir() + if err != nil { + return nil, fmt.Errorf("getting default kubeconfig persistence dir: %w", err) + } + + entries, err := os.ReadDir(dynamicDir) + if err != nil { + return nil, fmt.Errorf("reading dynamic kubeconfig directory: %w", err) + } + + var configPaths []string //nolint:prealloc + + for _, entry := range entries { + // Optionally skip directories or non-kubeconfig files, if needed. + if entry.IsDir() { + continue + } + + filePath := filepath.Join(dynamicDir, entry.Name()) + + configPaths = append(configPaths, filePath) + } + + return configPaths, nil +} + +func removeContextFromDefaultKubeConfig( + w http.ResponseWriter, + contextName string, + configPaths ...string, +) error { + // If no specific paths passed, fallback to the default. + if len(configPaths) == 0 { + discoveredPath, err := defaultKubeConfigPersistenceFile() + if err != nil { + logger.Log( + logger.LevelError, + map[string]string{"cluster": contextName}, + err, + "getting default kubeconfig persistence file", + ) + http.Error(w, "getting default kubeconfig persistence file", http.StatusInternalServerError) + + return err + } + + configPaths = []string{discoveredPath} + } + + // Hand off to a small helper function that handles multi-file iteration. + return removeContextFromConfigs(w, contextName, configPaths) +} + +// removeContextFromConfigs does the real iteration over the configPaths. +func removeContextFromConfigs(w http.ResponseWriter, contextName string, configPaths []string) error { + var removed bool + + for _, filePath := range configPaths { + logger.Log( + logger.LevelInfo, + map[string]string{ + "cluster": contextName, + "kubeConfigPersistenceFile": filePath, + }, + nil, + "Trying to remove context from kubeconfig", + ) + + err := kubeconfig.RemoveContextFromFile(contextName, filePath) + if err == nil { + removed = true + + logger.Log(logger.LevelInfo, + map[string]string{"cluster": contextName, "file": filePath}, + nil, "Removed context from kubeconfig", + ) + + break + } + + if strings.Contains(err.Error(), "context not found") { + logger.Log(logger.LevelInfo, + map[string]string{"cluster": contextName, "file": filePath}, + nil, "Context not in this file; checking next.", + ) + + continue + } + + logger.Log(logger.LevelError, + map[string]string{"cluster": contextName}, + err, "removing cluster from kubeconfig", + ) + + http.Error(w, "removing cluster from kubeconfig", http.StatusInternalServerError) + + return err + } + + if !removed { + e := fmt.Errorf("context %q not found in any provided kubeconfig file(s)", contextName) + + logger.Log( + logger.LevelError, + map[string]string{"cluster": contextName}, + e, + "context not found in any file", + ) + http.Error(w, e.Error(), http.StatusBadRequest) + + return e + } + + return nil +} + // deleteCluster deletes the cluster from the store and updates the kubeconfig file. func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { if err := checkHeadlampBackendToken(w, r); err != nil { @@ -1384,28 +1516,42 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { return } - kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile() - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": name}, - err, "getting default kubeconfig persistence file") - http.Error(w, "getting default kubeconfig persistence file", http.StatusInternalServerError) + removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true" - return + if removeKubeConfig { + kubeConfigFile, err := defaultKubeConfigFile() + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": name}, + err, "failed to get default kubeconfig file path") + http.Error(w, "failed to get default kubeconfig file path", http.StatusInternalServerError) + + return + } + + err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": name}, + err, "removing context from default kubeconfig file") + http.Error(w, "removing context from default kubeconfig file", http.StatusInternalServerError) + + return + } } - logger.Log(logger.LevelInfo, map[string]string{ - "cluster": name, - "kubeConfigPersistenceFile": kubeConfigPersistenceFile, - }, - nil, "Removing cluster from kubeconfig") + if !removeKubeConfig { + configPathsList, pathErr := c.collectMultiConfigPaths() + if pathErr != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": name}, + pathErr, "collecting multi config paths") + http.Error(w, "collecting multi config paths", http.StatusInternalServerError) - err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile) - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": name}, - err, "removing cluster from kubeconfig") - http.Error(w, "removing cluster from kubeconfig", http.StatusInternalServerError) + return + } - return + if err := removeContextFromDefaultKubeConfig(w, name, configPathsList...); err != nil { + // removeContextFromDefaultKubeConfig writes any needed http.Error if it fails + return + } } logger.Log(logger.LevelInfo, map[string]string{"cluster": name, "proxy": name}, diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 792732fd23f..f2e092ba32a 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -665,6 +665,34 @@ func TestRenameCluster(t *testing.T) { } } +func TestRemoveContextFromDefaultKubeConfig(t *testing.T) { + // 1) Create a temp directory for our test kubeconfig + tmpDir := t.TempDir() + mockConfigFile := filepath.Join(tmpDir, "config") + + // 2) Copy "kubeconfig_remove" (which includes 'kubedelta') into that file + testDataPath := filepath.Join("headlamp_testdata", "kubeconfig_remove") + testData, err := os.ReadFile(testDataPath) + require.NoError(t, err, "failed to read test data for 'kubeconfig_remove'") + + err = os.WriteFile(mockConfigFile, testData, 0o600) + require.NoError(t, err, "failed to write test kubeconfig") + + // 3) We need a fake http.ResponseWriter + w := httptest.NewRecorder() + + // 4) Call removeContextFromDefaultKubeConfig with our mock path as the third param + err = removeContextFromDefaultKubeConfig(w, "kubedelta", mockConfigFile) + require.NoError(t, err, "removeContextFromDefaultKubeConfig should succeed") + + // 5) Verify 'kubedelta' is removed from the file + updatedData, err := os.ReadFile(mockConfigFile) + require.NoError(t, err, "failed to read updated kubeconfig") + + require.NotContains(t, string(updatedData), "kubedelta", + "Expected 'kubedelta' context to be removed from kubeconfig") +} + func TestFileExists(t *testing.T) { // Test for existing file assert.True(t, fileExists("./headlamp_testdata/kubeconfig"), diff --git a/backend/cmd/headlamp_testdata/kubeconfig_remove b/backend/cmd/headlamp_testdata/kubeconfig_remove new file mode 100644 index 00000000000..a86d9146368 --- /dev/null +++ b/backend/cmd/headlamp_testdata/kubeconfig_remove @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: Config +current-context: minikubetest +preferences: {} +clusters: + - cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeU1EZ3lOakV4TURRMU0xb1hEVE15TURneU16RXhNRFExTTFvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTnk3Ci9kREMxV0w3TXNSWGV2Z2tUQXkzcFZHMVVLa1VQeXd4cS9ETHBPdmRzQmloQjZoVmN1bWNZUTkzYUxLbERzSXMKR0Q0QUJkUFM4cEFPMzhMb3RBWWVDeDIwcDFPem9LYVMvVkp6ZlJKQWVUSStCY3dzRjh2U1VXYU0reWZ4STBPUgpnalE0OVR0eUppYURyS2tzbnd4R3Y0K0U3aWFhZUVPMG55U01EcnpON1RvYkVyb1pObHRzNkdMN2tpTDB0TG5ZCkorNnNtSHlhSGh6WThaR0JZMFdWUXpzNENFMnJ0Q1k5eTV4N2F3bUlDUWE2anBXVFVQazNqa0RMcU93bEQyRmMKcHNkeXI4a1Z3UUhTUUVnRkg2Yzgwdnp3Ny9RSUVDdGRYNlZRRnE1bzYzOWlvc3hQcXVKV3ZtMGVjdkx5dC81cApxNXZpNzMxWThEb0VDMjFtS2NzQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJNFFkU2FSRFVodi8wWjk0ZzV5RmlVdWlMZHBNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQ3pWWUpBUzQ1UFBOSFVSaDJKWQpKWDFycmFMdGNTbzVuNG1DVy9oeE5YdHpCMlIzWkhPU0hnNmF2R3JNeFY4ZlpCdmtBdEJFaUYzM2JvRThzZzVhCjhhWHRFTjR5bzlZQ2FZc2ZXK2tNNlZDRUdtVWd5bm13aXltYTBzSW5USlZ1R3ZVbDVucVhjUHJJdW9OTVVrTUwKdCsrckxCb0NwY2xrN09VSTA0dXZvanpxc2hsQ0JiMURSOXRwT0s0Kys0UGdPait6OXZ0N3g0dzhMYlhvQmtvegozOEJyVEoyQ3NqbU0xS2ZqZXlpNWdHVmFjeE9YSXRjbXprNzRpQzZ0SjdqVm1MVmNacEc5ZElvcFk5WTBaTkQ0CmQzZjlmOGdCWkJzaXA0a3gxMmFxMlJ5dzFYNGVOaFY2dW5OaCtHVHNhNlFDSlJ0Zk9FK1Q4Njd2ZHlPbjZMb2wKYWQ4PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + extensions: + - extension: + last-update: Mon, 26 Dec 2022 20:33:03 IST + provider: minikubetest.sigs.k8s.io + version: v1.28.0 + name: cluster_info + server: https://127.0.0.1:60279 + name: minikubetest +contexts: + - context: + cluster: minikubetest + namespace: default + user: minikubetest + extensions: + - extension: + last-update: Mon, 26 Dec 2022 20:33:03 IST + provider: minikubetest.sigs.k8s.io + version: v1.28.0 + name: context_info + - extension: + creationTimestamp: null + customName: minikubetestworks + name: headlamp_info + name: minikubetest + - context: + cluster: minikubetest + namespace: default + user: kubedelta + name: kubedelta +users: + - name: minikubetest + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJekNDQWd1Z0F3SUJBZ0lJZlJpZk1qZWl1eFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBNE1qWXhNVEEwTlROYUZ3MHlOREF4TURJeE1ESXhNelZhTURZeApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sc3dHUVlEVlFRREV4SmtiMk5yWlhJdFptOXlMV1JsCmMydDBiM0F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzRLUmwrS0lsN0NJYVgKbzIwYjdBOVEvaURDbUN6dWFXMSs3WEJyelhiZHNmNkNaRzhVMWZYWTdVWXl3bXVhYkZldUFrUHRBT1hyWVg0YQpMTGZtWTRvdkZYc1RQWmtPUktJeWRFUmNnLy9hOStPd3d2c1ZCUUp4NFplbUtrN1NzaFYxcjl3WGVqVnJIUkFOCm5xQ3JIQVhFNHA5bmFKZHNkTXIyQWdDa0VIK01tTFNqTExNL1lWcnExdmJpRWRtUVFSWHduVnFwcmNyRXBIQzUKWWJJenl4cVZRWWZIZVdWc2N0SUxFeVdPMFQwMS9tYkZ2RVY4QW9BL3phekIycjF3Y0VaeUNSRXFXbExrS2RXTwpNYmU1WnlwMDNhQzlBOSs4cThQNFBEOUNnVXlrcVovN0xydGlja2k0TVBsK2VmaGFlUk9YSEJMSURuQmplTHJkCmJGdHpaOVhKQWdNQkFBR2pWakJVTUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU1CZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZJNFFkU2FSRFVodi8wWjk0ZzV5RmlVdQppTGRwTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBRjNvaTFvNVlNM1UvOWxPRElhaUpmaGllNzdieG1pN3NwCitiL0NEOGRCTXhIdWpPVnBSaTFNaHRJa2U3N2U1RVVuZEFGRzYvQTQwK3c2TGtCYXJFUEl5R2daRlBvZkttcSsKRGlIMGxPZHBYY0hFd3laTjhWSmdRd0JKUkhKcDhBc0p3TGFYWGplU1FQdmZyeHhLdUFGenRzeXNaYlBMUkxoYQpjeXZmeDNwTE91ZVJ4MDJqQVZUUlNJUGNPZEV4SERPa0FGWFFCdDV4TFo2eGFKTU1VQjZXNUYwcVpPelFuVUZsCk80QUNNOEhnOEdKc2xqLzFqZnpZaGlneWdwL2psQ0Jkd1Izb2c1ZXFqaC9ZRzlxWHVsU2Z0WUNhMURaOEp2QnAKaGRSYzZxOVM0ZFdtRW9zMmkxTDA1WUs3ZFBaQk5JVHRLNkVzQS9CRCs0VlVWRHczZldkNQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdUNrWmZpaUpld2lHbDZOdEcrd1BVUDRnd3BnczdtbHRmdTF3YTgxMjNiSCtnbVJ2CkZOWDEyTzFHTXNKcm1teFhyZ0pEN1FEbDYyRitHaXkzNW1PS0x4VjdFejJaRGtTaU1uUkVYSVAvMnZmanNNTDcKRlFVQ2NlR1hwaXBPMHJJVmRhL2NGM28xYXgwUURaNmdxeHdGeE9LZloyaVhiSFRLOWdJQXBCQi9qSmkwb3l5egpQMkZhNnRiMjRoSFprRUVWOEoxYXFhM0t4S1J3dVdHeU04c2FsVUdIeDNsbGJITFNDeE1sanRFOU5mNW14YnhGCmZBS0FQODJzd2RxOWNIQkdjZ2tSS2xwUzVDblZqakczdVdjcWROMmd2UVBmdkt2RCtEdy9Rb0ZNcEttZit5NjcKWW5KSXVERDVmbm40V25rVGx4d1N5QTV3WTNpNjNXeGJjMmZWeVFJREFRQUJBb0lCQUdYMWUwTzV0Y1FFU0dBVAovd2lDZlVoZUtrMFNhMjNqdU5lWkpiREpwSkhCUmlOeTczMGRxR3Rka292djBCdEMrSmhDY05ENnVsRERQVW5JCmtGaGhxOU85bE5KbVBDTUdKTGJDWUViSVhoTWhRMUpONFMwV0JQQi84Ykh4b29wTVJrMU4vQkNUZkplOUUzSTIKN01WUFVuSmE5ZDRPcmlkQjBreTVkeGxlZVAraGFvT2NTejJGamhXbDEycnlqbm1ad0draU1BdnhHazBaejFkZgpxZ0QyUE5CRHYzMTdtMkJxYjdkcENaTmZsSi90MGtqQ3hGbm81UmRsUUl5NDhSeml4LytaV1ZSeWlsaVFjL2srCnY4UzRTWGljZjBDK1RzV0orZzBNU3NoaGs1SWV4OURzTmR1bmJrSTcxMDNoR2ZOcGZYbGlRVlZHTlZ2eHdNd2kKenV4eE9nRUNnWUVBeGFYbU9GSUhkbm1tU0tMbEp4KzhBc3pPcmdTNDFKM2RMemtUZUZhWU5STHY0Z1AyK05SQwpQcXAzYVRCRmNjNWFMTDFXcG1ZRkdGSkthejRUbm05dlRKN0Nhell6K1RZRWc4OWlidGJESmhFSlFaSGtRaDQxCnJXRlBHTERWL1ZVSk5kaStSRk9TU2xMVDVGY2gvS1NkdFFRazhkbStUempiWi94a29ZOVpKbmtDZ1lFQTdvZlQKRnQ0MytQY0w5T0ZoTGZjYnB2SUJkeDdzWlAyK09NVzF3eG92TnhTbElSelBSQi9QbXNsai9hUzV2VnFWOVlVdgp0YjVFaUM3cUVYN2JVS0lZT1hMY1c3N3ozcmxpODdMRW5CWkNDemFhdHk3Nmw1U2lneU16VDY3MS9DdjNSSVJYCkw5citoQ2ZUUThPVHBUVDR6R3ZhRWJrQldBcnRmeVZOTDhkVGxkRUNnWUF1TE44SFEyckk3QXpFSllKaHpKRXgKR2tZaTg2bDJ5dGJVNUlHKytJUWd5aWJPNTl3NEwrYTJHejlBak8xOGRCZ3ZJYUR2eVIvaG1jQVhJKzZUY2pkUApjRHU5cm5FZ0JOV3pNYTB1ZGZBcm9ZbEhEMTJEY09sYmMwTjJZa0hzS0lTNVZzVEUwNzEycmJraFBKWU5IeXhWCkVQM01udkZPTXR0WGhPakJzZXJEQ1FLQmdEOCtBOW1zVVdyUkZYcDN4eXhJdUN3clBmZzNXclhzRU9NOGlGU1MKUExKOTVzcEF1VE4ydTdSdWNQUnZHRS84Rklaa0thSW1NRVZyS3VRNG5pMWl6TWx1aXI1SWdxQXF4dkdXRkVyTwpHL1NkSmFncjdJVUVBNUtCWXJsZHlocHlEYjA4MldEMnowUjZ5cWpNMGZpYmN0dkFQTEUyUEFUNzRMdzFSNkhEClY0WUJBb0dCQUo4bUdVNWJnejBQUnJZL1hoUERER2tIa0Z3SHVocHhjL2tieWNweVVTZ2NDR2dsczFXTTdKOFUKQ05MV2pQc0pnSTNxeTJSN0xxaGxmNHJRK1orLy80WHdyNW1NYWpqSEVGZE93Z2xqbFZRdEljNWM4a2U3MzM1SwprbCtxblpJbUcwRy83R1hUZHhSOW9mQXBBZVFkT3pJTFY0K1YzZm5DRGNYMnBHWjZOOTJUCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + - name: kubedelta + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJekNDQWd1Z0F3SUJBZ0lJZlJpZk1qZWl1eFV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBNE1qWXhNVEEwTlROYUZ3MHlOREF4TURJeE1ESXhNelZhTURZeApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sc3dHUVlEVlFRREV4SmtiMk5yWlhJdFptOXlMV1JsCmMydDBiM0F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzRLUmwrS0lsN0NJYVgKbzIwYjdBOVEvaURDbUN6dWFXMSs3WEJyelhiZHNmNkNaRzhVMWZYWTdVWXl3bXVhYkZldUFrUHRBT1hyWVg0YQpMTGZtWTRvdkZYc1RQWmtPUktJeWRFUmNnLy9hOStPd3d2c1ZCUUp4NFplbUtrN1NzaFYxcjl3WGVqVnJIUkFOCm5xQ3JIQVhFNHA5bmFKZHNkTXIyQWdDa0VIK01tTFNqTExNL1lWcnExdmJpRWRtUVFSWHduVnFwcmNyRXBIQzUKWWJJenl4cVZRWWZIZVdWc2N0SUxFeVdPMFQwMS9tYkZ2RVY4QW9BL3phekIycjF3Y0VaeUNSRXFXbExrS2RXTwpNYmU1WnlwMDNhQzlBOSs4cThQNFBEOUNnVXlrcVovN0xydGlja2k0TVBsK2VmaGFlUk9YSEJMSURuQmplTHJkCmJGdHpaOVhKQWdNQkFBR2pWakJVTUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU1CZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZJNFFkU2FSRFVodi8wWjk0ZzV5RmlVdQppTGRwTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBRjNvaTFvNVlNM1UvOWxPRElhaUpmaGllNzdieG1pN3NwCitiL0NEOGRCTXhIdWpPVnBSaTFNaHRJa2U3N2U1RVVuZEFGRzYvQTQwK3c2TGtCYXJFUEl5R2daRlBvZkttcSsKRGlIMGxPZHBYY0hFd3laTjhWSmdRd0JKUkhKcDhBc0p3TGFYWGplU1FQdmZyeHhLdUFGenRzeXNaYlBMUkxoYQpjeXZmeDNwTE91ZVJ4MDJqQVZUUlNJUGNPZEV4SERPa0FGWFFCdDV4TFo2eGFKTU1VQjZXNUYwcVpPelFuVUZsCk80QUNNOEhnOEdKc2xqLzFqZnpZaGlneWdwL2psQ0Jkd1Izb2c1ZXFqaC9ZRzlxWHVsU2Z0WUNhMURaOEp2QnAKaGRSYzZxOVM0ZFdtRW9zMmkxTDA1WUs3ZFBaQk5JVHRLNkVzQS9CRCs0VlVWRHczZldkNQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdUNrWmZpaUpld2lHbDZOdEcrd1BVUDRnd3BnczdtbHRmdTF3YTgxMjNiSCtnbVJ2CkZOWDEyTzFHTXNKcm1teFhyZ0pEN1FEbDYyRitHaXkzNW1PS0x4VjdFejJaRGtTaU1uUkVYSVAvMnZmanNNTDcKRlFVQ2NlR1hwaXBPMHJJVmRhL2NGM28xYXgwUURaNmdxeHdGeE9LZloyaVhiSFRLOWdJQXBCQi9qSmkwb3l5egpQMkZhNnRiMjRoSFprRUVWOEoxYXFhM0t4S1J3dVdHeU04c2FsVUdIeDNsbGJITFNDeE1sanRFOU5mNW14YnhGCmZBS0FQODJzd2RxOWNIQkdjZ2tSS2xwUzVDblZqakczdVdjcWROMmd2UVBmdkt2RCtEdy9Rb0ZNcEttZit5NjcKWW5KSXVERDVmbm40V25rVGx4d1N5QTV3WTNpNjNXeGJjMmZWeVFJREFRQUJBb0lCQUdYMWUwTzV0Y1FFU0dBVAovd2lDZlVoZUtrMFNhMjNqdU5lWkpiREpwSkhCUmlOeTczMGRxR3Rka292djBCdEMrSmhDY05ENnVsRERQVW5JCmtGaGhxOU85bE5KbVBDTUdKTGJDWUViSVhoTWhRMUpONFMwV0JQQi84Ykh4b29wTVJrMU4vQkNUZkplOUUzSTIKN01WUFVuSmE5ZDRPcmlkQjBreTVkeGxlZVAraGFvT2NTejJGamhXbDEycnlqbm1ad0draU1BdnhHazBaejFkZgpxZ0QyUE5CRHYzMTdtMkJxYjdkcENaTmZsSi90MGtqQ3hGbm81UmRsUUl5NDhSeml4LytaV1ZSeWlsaVFjL2srCnY4UzRTWGljZjBDK1RzV0orZzBNU3NoaGs1SWV4OURzTmR1bmJrSTcxMDNoR2ZOcGZYbGlRVlZHTlZ2eHdNd2kKenV4eE9nRUNnWUVBeGFYbU9GSUhkbm1tU0tMbEp4KzhBc3pPcmdTNDFKM2RMemtUZUZhWU5STHY0Z1AyK05SQwpQcXAzYVRCRmNjNWFMTDFXcG1ZRkdGSkthejRUbm05dlRKN0Nhell6K1RZRWc4OWlidGJESmhFSlFaSGtRaDQxCnJXRlBHTERWL1ZVSk5kaStSRk9TU2xMVDVGY2gvS1NkdFFRazhkbStUempiWi94a29ZOVpKbmtDZ1lFQTdvZlQKRnQ0MytQY0w5T0ZoTGZjYnB2SUJkeDdzWlAyK09NVzF3eG92TnhTbElSelBSQi9QbXNsai9hUzV2VnFWOVlVdgp0YjVFaUM3cUVYN2JVS0lZT1hMY1c3N3ozcmxpODdMRW5CWkNDemFhdHk3Nmw1U2lneU16VDY3MS9DdjNSSVJYCkw5citoQ2ZUUThPVHBUVDR6R3ZhRWJrQldBcnRmeVZOTDhkVGxkRUNnWUF1TE44SFEyckk3QXpFSllKaHpKRXgKR2tZaTg2bDJ5dGJVNUlHKytJUWd5aWJPNTl3NEwrYTJHejlBak8xOGRCZ3ZJYUR2eVIvaG1jQVhJKzZUY2pkUApjRHU5cm5FZ0JOV3pNYTB1ZGZBcm9ZbEhEMTJEY09sYmMwTjJZa0hzS0lTNVZzVEUwNzEycmJraFBKWU5IeXhWCkVQM01udkZPTXR0WGhPakJzZXJEQ1FLQmdEOCtBOW1zVVdyUkZYcDN4eXhJdUN3clBmZzNXclhzRU9NOGlGU1MKUExKOTVzcEF1VE4ydTdSdWNQUnZHRS84Rklaa0thSW1NRVZyS3VRNG5pMWl6TWx1aXI1SWdxQXF4dkdXRkVyTwpHL1NkSmFncjdJVUVBNUtCWXJsZHlocHlEYjA4MldEMnowUjZ5cWpNMGZpYmN0dkFQTEUyUEFUNzRMdzFSNkhEClY0WUJBb0dCQUo4bUdVNWJnejBQUnJZL1hoUERER2tIa0Z3SHVocHhjL2tieWNweVVTZ2NDR2dsczFXTTdKOFUKQ05MV2pQc0pnSTNxeTJSN0xxaGxmNHJRK1orLy80WHdyNW1NYWpqSEVGZE93Z2xqbFZRdEljNWM4a2U3MzM1SwprbCtxblpJbUcwRy83R1hUZHhSOW9mQXBBZVFkT3pJTFY0K1YzZm5DRGNYMnBHWjZOOTJUCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/frontend/src/components/App/Home/index.tsx b/frontend/src/components/App/Home/index.tsx index abed5454a91..198c318444d 100644 --- a/frontend/src/components/App/Home/index.tsx +++ b/frontend/src/components/App/Home/index.tsx @@ -25,6 +25,24 @@ import { ConfirmDialog } from '../../common'; import ResourceTable from '../../common/Resource/ResourceTable'; import RecentClusters from './RecentClusters'; +/** + * Gets the origin of a cluster. + * + * @param cluster + * @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file. + */ +function getOrigin(cluster: Cluster, t: any): string { + if (cluster?.meta_data?.source === 'kubeconfig') { + const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config'; + return `Kubeconfig: ${kubeconfigPath}`; + } else if (cluster.meta_data?.source === 'dynamic_cluster') { + return t('translation|Plugin'); + } else if (cluster.meta_data?.source === 'in_cluster') { + return t('translation|In-cluster'); + } + return 'Unknown'; +} + function ContextMenu({ cluster }: { cluster: Cluster }) { const { t } = useTranslation(['translation']); const history = useHistory(); @@ -33,8 +51,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { const menuId = useId('context-menu'); const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false); - function removeCluster(cluster: Cluster) { - deleteCluster(cluster.name || '') + function removeCluster(cluster: Cluster, removeKubeconfig?: boolean) { + deleteCluster(cluster.name || '', removeKubeconfig) .then(config => { dispatch(setConfig(config)); }) @@ -91,7 +109,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { > {t('translation|Settings')} - {helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && ( + + {helpers.isElectron() && ( { setOpenConfirmDialog(true); @@ -105,18 +124,28 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { setOpenConfirmDialog(false)} + handleClose={() => { + setOpenConfirmDialog(false); + }} onConfirm={() => { setOpenConfirmDialog(false); - removeCluster(cluster); + if (cluster.meta_data?.source !== 'dynamic_cluster') { + removeCluster(cluster, true); + } else { + removeCluster(cluster); + } }} title={t('translation|Delete Cluster')} description={t( - 'translation|Are you sure you want to remove the cluster "{{ clusterName }}"?', + 'translation|This action will delete cluster "{{ clusterName }}" from {{ source }}.', { clusterName: cluster.name, + source: getOrigin(cluster, t), } )} + checkboxDescription={ + cluster.meta_data?.source !== 'dynamic_cluster' ? t('Delete from kubeconfig') : '' + } /> > ); @@ -238,24 +267,6 @@ function HomeComponent(props: HomeComponentProps) { .sort(); } - /** - * Gets the origin of a cluster. - * - * @param cluster - * @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file. - */ - function getOrigin(cluster: Cluster): string { - if (cluster.meta_data?.source === 'kubeconfig') { - const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config'; - return `Kubeconfig: ${kubeconfigPath}`; - } else if (cluster.meta_data?.source === 'dynamic_cluster') { - return t('translation|Plugin'); - } else if (cluster.meta_data?.source === 'in_cluster') { - return t('translation|In-cluster'); - } - return 'Unknown'; - } - const memoizedComponent = React.useMemo( () => ( @@ -286,10 +297,8 @@ function HomeComponent(props: HomeComponentProps) { }, { label: t('Origin'), - getValue: cluster => getOrigin(cluster), - render: ({ name }) => ( - {getOrigin(clusters[name])} - ), + getValue: cluster => getOrigin(cluster, t), + render: cluster => {getOrigin(cluster, t)}, }, { label: t('Status'), diff --git a/frontend/src/components/common/ConfirmDialog.stories.tsx b/frontend/src/components/common/ConfirmDialog.stories.tsx index 6afb12831d3..6ef459c452e 100644 --- a/frontend/src/components/common/ConfirmDialog.stories.tsx +++ b/frontend/src/components/common/ConfirmDialog.stories.tsx @@ -26,3 +26,19 @@ ConfirmDialogClosed.args = { title: 'A fine title', description: 'A really good description.', }; + +export const ConfirmDialogWithCheckbox = Template.bind({}); +ConfirmDialogWithCheckbox.args = { + open: true, + title: 'A fine title', + description: 'A really good description, only now we are using a checkbox.', + checkboxDescription: 'Click the checkbox.', +}; + +export const ConfirmDialogWithCheckboxClosed = Template.bind({}); +ConfirmDialogWithCheckboxClosed.args = { + open: false, + title: 'A fine title', + description: 'A really good description, only now we are using a checkbox.', + checkboxDescription: 'Click the checkbox.', +}; diff --git a/frontend/src/components/common/ConfirmDialog.tsx b/frontend/src/components/common/ConfirmDialog.tsx index 3ba49b0571a..70cecffb861 100644 --- a/frontend/src/components/common/ConfirmDialog.tsx +++ b/frontend/src/components/common/ConfirmDialog.tsx @@ -1,15 +1,28 @@ +import { Checkbox } from '@mui/material'; +import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import MuiDialog, { DialogProps as MuiDialogProps } from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; -import React, { ReactNode } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { DialogTitle } from './Dialog'; export interface ConfirmDialogProps extends MuiDialogProps { + /** + * Title of the dialog box + */ title: string; - description: ReactNode; + /** + * Description of the dialog box + */ + description: string | React.ReactNode; + /* + * Description of the checkbox + * Note: If this is provided, an additional description will be rendered under the original and will require the checkbox to continue action. + */ + checkboxDescription?: string; onConfirm: () => void; handleClose: () => void; } @@ -17,12 +30,22 @@ export interface ConfirmDialogProps extends MuiDialogProps { export function ConfirmDialog(props: ConfirmDialogProps) { const { onConfirm, open, handleClose, title, description } = props; const { t } = useTranslation(); + const [checkedChoice, setcheckedChoice] = React.useState(false); function onConfirmationClicked() { handleClose(); onConfirm(); } + function closeDialog() { + setcheckedChoice(false); + handleClose(); + } + + function handleChoiceToggle() { + setcheckedChoice(!checkedChoice); + } + const focusedRef = React.useCallback((node: HTMLElement) => { if (node !== null) { node.setAttribute('tabindex', '-1'); @@ -34,21 +57,46 @@ export function ConfirmDialog(props: ConfirmDialogProps) { {title} {description} + {props.checkboxDescription && ( + + + {props.checkboxDescription} + + + + )} - + { + closeDialog(); + }} + color="primary" + > {t('No')} - - {t('Yes')} - + {props.checkboxDescription ? ( + + {t('I Agree')} + + ) : ( + + {t('Yes')} + + )} diff --git a/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckbox.stories.storyshot b/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckbox.stories.storyshot new file mode 100644 index 00000000000..97972a00de5 --- /dev/null +++ b/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckbox.stories.storyshot @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + A fine title + + + + + + + A really good description, only now we are using a checkbox. + + + + Click the checkbox. + + + + + + + + + + + + + No + + + + I Agree + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckboxClosed.stories.storyshot b/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckboxClosed.stories.storyshot new file mode 100644 index 00000000000..db35fe54c1b --- /dev/null +++ b/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckboxClosed.stories.storyshot @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index 025012a3cef..b5e3d687a26 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -9,15 +9,16 @@ "Cancel": "Abbrechen", "Authenticate": "Authentifizieren Sie", "Error authenticating": "Fehler beim Authentifizieren", + "Plugin": "", + "In-cluster": "", "Actions": "Aktionen", "View": "Ansicht", "Settings": "Einstellungen", "Delete": "Löschen", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Sind Sie sicher, dass Sie den Cluster \"{{ clusterName }}\" entfernen möchten?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Aktiv", - "Plugin": "", - "In-cluster": "", "Home": "Startseite", "All Clusters": "Alle Cluster", "Name": "Name", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Cluster-Einstellungen ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Cluster entfernen", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Sind Sie sicher, dass Sie den Cluster \"{{ clusterName }}\" entfernen möchten?", "Server": "Server", "light theme": "helles Design", "dark theme": "dunkles Design", @@ -145,6 +147,7 @@ "Offline": "Offline", "Lost connection to the cluster.": "", "No": "Nein", + "I Agree": "", "Yes": "Ja", "Create {{ name }}": "", "Toggle fullscreen": "Vollbild ein/aus", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 7874af5189b..e59c1c901f5 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancel", "Authenticate": "Authenticate", "Error authenticating": "Error authenticating", + "Plugin": "Plugin", + "In-cluster": "In-cluster", "Actions": "Actions", "View": "View", "Settings": "Settings", "Delete": "Delete", "Delete Cluster": "Delete Cluster", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Are you sure you want to remove the cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.", + "Delete from kubeconfig": "Delete from kubeconfig", "Active": "Active", - "Plugin": "Plugin", - "In-cluster": "In-cluster", "Home": "Home", "All Clusters": "All Clusters", "Name": "Name", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Cluster Settings ({{ clusterName }})", "Go to cluster": "Go to cluster", "Remove Cluster": "Remove Cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Are you sure you want to remove the cluster \"{{ clusterName }}\"?", "Server": "Server", "light theme": "light theme", "dark theme": "dark theme", @@ -145,6 +147,7 @@ "Offline": "Offline", "Lost connection to the cluster.": "Lost connection to the cluster.", "No": "No", + "I Agree": "I Agree", "Yes": "Yes", "Create {{ name }}": "Create {{ name }}", "Toggle fullscreen": "Toggle fullscreen", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index a70637833b1..eca647d9392 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancelar", "Authenticate": "Autenticar", "Error authenticating": "Error al autenticarse", + "Plugin": "", + "In-cluster": "", "Actions": "Acciones", "View": "Ver", "Settings": "Definiciones", "Delete": "Borrar", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "¿Está seguro de que desea eliminar el cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Activo", - "Plugin": "", - "In-cluster": "", "Home": "Inicio", "All Clusters": "Todos los Clusters", "Name": "Nombre", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Configuración del cluster ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Eliminar cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "¿Está seguro de que desea eliminar el cluster \"{{ clusterName }}\"?", "Server": "Servidor", "light theme": "tema claro", "dark theme": "tema oscuro", @@ -145,6 +147,7 @@ "Offline": "Desconectado", "Lost connection to the cluster.": "", "No": "No", + "I Agree": "", "Yes": "Sí", "Create {{ name }}": "", "Toggle fullscreen": "Alternar pantalla completa", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 61134299e9d..3f0cf19cae3 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancel", "Authenticate": "Authentifier", "Error authenticating": "Erreur d'authentification", + "Plugin": "", + "In-cluster": "", "Actions": "Actions", "View": "Vue", "Settings": "Paramètres", "Delete": "Supprimer", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Voulez-vous vraiment supprimer le cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Actif", - "Plugin": "", - "In-cluster": "", "Home": "Accueil", "All Clusters": "Tous les clusters", "Name": "Nom", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Paramètres du cluster ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Supprimer le cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Voulez-vous vraiment supprimer le cluster \"{{ clusterName }}\"?", "Server": "Serveur", "light theme": "thème clair", "dark theme": "thème sombre", @@ -145,6 +147,7 @@ "Offline": "Hors ligne", "Lost connection to the cluster.": "", "No": "Non", + "I Agree": "", "Yes": "Oui", "Create {{ name }}": "", "Toggle fullscreen": "Basculer en mode plein écran", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 47caf5a1331..1af9afbb9b5 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancelar", "Authenticate": "Autenticar", "Error authenticating": "Erro ao autenticar", + "Plugin": "", + "In-cluster": "", "Actions": "Acções", "View": "Ver", "Settings": "Definições", "Delete": "Apagar", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Tem a certeza que quer remover o cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Activo", - "Plugin": "", - "In-cluster": "", "Home": "Início", "All Clusters": "Todos os Clusters", "Name": "Nome", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Definições do cluster ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Remover Cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Tem a certeza que quer remover o cluster \"{{ clusterName }}\"?", "Server": "Servidor", "light theme": "tema claro", "dark theme": "tema escuro", @@ -145,6 +147,7 @@ "Offline": "Desconectado", "Lost connection to the cluster.": "", "No": "Não", + "I Agree": "", "Yes": "Sim", "Create {{ name }}": "", "Toggle fullscreen": "Alternar ecrã inteiro", diff --git a/frontend/src/i18n/locales/zh-tw/translation.json b/frontend/src/i18n/locales/zh-tw/translation.json index f97c5dee99a..d897be5b1a2 100644 --- a/frontend/src/i18n/locales/zh-tw/translation.json +++ b/frontend/src/i18n/locales/zh-tw/translation.json @@ -9,15 +9,16 @@ "Cancel": "取消", "Authenticate": "驗證", "Error authenticating": "驗證錯誤", + "Plugin": "插件", + "In-cluster": "叢集內", "Actions": "操作", "View": "查看", "Settings": "設置", "Delete": "刪除", "Delete Cluster": "刪除叢集", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "您確定要移除叢集 \"{{ clusterName }}\" 嗎?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "活躍", - "Plugin": "外掛", - "In-cluster": "叢集內", "Home": "首頁", "All Clusters": "所有叢集", "Name": "名稱", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "叢集設置 ({{ clusterName }})", "Go to cluster": "前往叢集", "Remove Cluster": "移除叢集", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "您確定要移除叢集 \"{{ clusterName }}\" 嗎?", "Server": "伺服器", "light theme": "淺色主題", "dark theme": "深色主題", @@ -145,6 +147,7 @@ "Offline": "離線", "Lost connection to the cluster.": "與叢集的連線遺失。", "No": "否", + "I Agree": "", "Yes": "是", "Create {{ name }}": "新增 {{ name }}", "Toggle fullscreen": "切換全螢幕", diff --git a/frontend/src/lib/k8s/api/v1/clusterApi.ts b/frontend/src/lib/k8s/api/v1/clusterApi.ts index 374a2d59bd3..20053ac1614 100644 --- a/frontend/src/lib/k8s/api/v1/clusterApi.ts +++ b/frontend/src/lib/k8s/api/v1/clusterApi.ts @@ -75,10 +75,17 @@ export async function setCluster(clusterReq: ClusterRequest) { ); } -// @todo: needs documenting. - +/** + * The deleteCluster function sends a DELETE request to the backend to delete a cluster. + * + * If the removeKubeConfig parameter is true, it will also remove the cluster from the kubeconfig. + * + * @param cluster The name of the cluster to delete. + * @param removeKubeConfig Whether to remove the cluster from the kubeconfig. Defaults to false/unused. + */ export async function deleteCluster( - cluster: string + cluster: string, + removeKubeConfig?: boolean ): Promise<{ clusters: ConfigState['clusters'] }> { if (cluster) { const kubeconfig = await findKubeconfigByClusterName(cluster); @@ -89,12 +96,11 @@ export async function deleteCluster( } } - return request( - `/cluster/${cluster}`, - { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, - false, - false - ); + const url = removeKubeConfig + ? `/cluster/${cluster}?removeKubeConfig=true` + : `/cluster/${cluster}`; + + return request(url, { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, false, false); } /**
+ A really good description, only now we are using a checkbox. +
+ Click the checkbox. +