From 4c5b01748b22afe091465118a58a2d2c3834cbb6 Mon Sep 17 00:00:00 2001 From: Vincent T Date: Wed, 13 Nov 2024 17:30:11 -0500 Subject: [PATCH 1/5] app: home: Add delete button for clusters Signed-off-by: Vincent T --- backend/cmd/headlamp.go | 39 ++++++++++- frontend/src/components/App/Home/index.tsx | 65 +++++++++++-------- .../src/components/common/ConfirmDialog.tsx | 52 +++++++++++++-- frontend/src/i18n/locales/de/translation.json | 9 ++- frontend/src/i18n/locales/en/translation.json | 9 ++- frontend/src/i18n/locales/es/translation.json | 9 ++- frontend/src/i18n/locales/fr/translation.json | 9 ++- frontend/src/i18n/locales/pt/translation.json | 9 ++- frontend/src/lib/k8s/api/v1/clusterApi.ts | 24 ++++--- 9 files changed, 164 insertions(+), 61 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 87cb557c5e..7b68b4896a 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) { @@ -1384,6 +1396,30 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { return } + removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true" + + if removeKubeConfig { + // delete context from actual default kubecofig file + 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 + } + + // Use kubeConfigFile to remove the context from the default kubeconfig file + 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 + } + } + kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile() if err != nil { logger.Log(logger.LevelError, map[string]string{"cluster": name}, @@ -1396,8 +1432,7 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { logger.Log(logger.LevelInfo, map[string]string{ "cluster": name, "kubeConfigPersistenceFile": kubeConfigPersistenceFile, - }, - nil, "Removing cluster from kubeconfig") + }, nil, "Removing cluster from kubeconfig") err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile) if err != nil { diff --git a/frontend/src/components/App/Home/index.tsx b/frontend/src/components/App/Home/index.tsx index abed5454a9..198c318444 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.tsx b/frontend/src/components/common/ConfirmDialog.tsx index 3ba49b0571..c8abbe073a 100644 --- a/frontend/src/components/common/ConfirmDialog.tsx +++ b/frontend/src/components/common/ConfirmDialog.tsx @@ -1,15 +1,18 @@ +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: string; - description: ReactNode; + description: string | React.ReactNode; + checkboxDescription?: string; onConfirm: () => void; handleClose: () => void; } @@ -17,12 +20,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 +47,46 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
{title} {description} + {props.checkboxDescription && ( + + + {props.checkboxDescription} + + + + )} - - + {props.checkboxDescription ? ( + + ) : ( + + )}
diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index 025012a3ce..b61f4744e4 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", @@ -144,6 +146,7 @@ "Last Seen": "Zuletzt gesehen", "Offline": "Offline", "Lost connection to the cluster.": "", + "I Agree": "", "No": "Nein", "Yes": "Ja", "Create {{ name }}": "", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 7874af5189..bb0dd0d001 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", @@ -144,6 +146,7 @@ "Last Seen": "Last Seen", "Offline": "Offline", "Lost connection to the cluster.": "Lost connection to the cluster.", + "I Agree": "I Agree", "No": "No", "Yes": "Yes", "Create {{ name }}": "Create {{ name }}", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index a70637833b..45a9e959ac 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", @@ -144,6 +146,7 @@ "Last Seen": "Últi. ocurrencia", "Offline": "Desconectado", "Lost connection to the cluster.": "", + "I Agree": "", "No": "No", "Yes": "Sí", "Create {{ name }}": "", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 61134299e9..1d73234f55 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", @@ -144,6 +146,7 @@ "Last Seen": "Dernière vue", "Offline": "Hors ligne", "Lost connection to the cluster.": "", + "I Agree": "", "No": "Non", "Yes": "Oui", "Create {{ name }}": "", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 47caf5a133..340a3d0519 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", @@ -144,6 +146,7 @@ "Last Seen": "Visto últ. vez", "Offline": "Desconectado", "Lost connection to the cluster.": "", + "I Agree": "", "No": "Não", "Yes": "Sim", "Create {{ name }}": "", diff --git a/frontend/src/lib/k8s/api/v1/clusterApi.ts b/frontend/src/lib/k8s/api/v1/clusterApi.ts index 374a2d59bd..20053ac161 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); } /** From 8666a779e809c0779659801c77f4d7579b4d5023 Mon Sep 17 00:00:00 2001 From: Vincent T Date: Thu, 5 Dec 2024 14:47:05 -0500 Subject: [PATCH 2/5] backend: headlamp.go: Make delete cluster shorter Signed-off-by: Vincent T --- backend/cmd/headlamp.go | 153 ++++++++++++++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 21 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 7b68b4896a..0a219bc25a 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -1377,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 { @@ -1399,7 +1519,6 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true" if removeKubeConfig { - // delete context from actual default kubecofig file kubeConfigFile, err := defaultKubeConfigFile() if err != nil { logger.Log(logger.LevelError, map[string]string{"cluster": name}, @@ -1409,7 +1528,6 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { return } - // Use kubeConfigFile to remove the context from the default kubeconfig file err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile) if err != nil { logger.Log(logger.LevelError, map[string]string{"cluster": name}, @@ -1420,27 +1538,20 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { } } - 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) - - 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}, From a6f0e8e7e7d86d6ab3c26500d9e235bd6dc48301 Mon Sep 17 00:00:00 2001 From: Vincent T Date: Thu, 12 Dec 2024 15:17:14 -0500 Subject: [PATCH 3/5] frontend: Add i18n translations Signed-off-by: Vincent T --- frontend/src/i18n/locales/de/translation.json | 2 +- frontend/src/i18n/locales/en/translation.json | 2 +- frontend/src/i18n/locales/es/translation.json | 2 +- frontend/src/i18n/locales/fr/translation.json | 2 +- frontend/src/i18n/locales/pt/translation.json | 2 +- frontend/src/i18n/locales/zh-tw/translation.json | 9 ++++++--- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index b61f4744e4..b5e3d687a2 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -146,8 +146,8 @@ "Last Seen": "Zuletzt gesehen", "Offline": "Offline", "Lost connection to the cluster.": "", - "I Agree": "", "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 bb0dd0d001..e59c1c901f 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -146,8 +146,8 @@ "Last Seen": "Last Seen", "Offline": "Offline", "Lost connection to the cluster.": "Lost connection to the cluster.", - "I Agree": "I Agree", "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 45a9e959ac..eca647d939 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -146,8 +146,8 @@ "Last Seen": "Últi. ocurrencia", "Offline": "Desconectado", "Lost connection to the cluster.": "", - "I Agree": "", "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 1d73234f55..3f0cf19cae 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -146,8 +146,8 @@ "Last Seen": "Dernière vue", "Offline": "Hors ligne", "Lost connection to the cluster.": "", - "I Agree": "", "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 340a3d0519..1af9afbb9b 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -146,8 +146,8 @@ "Last Seen": "Visto últ. vez", "Offline": "Desconectado", "Lost connection to the cluster.": "", - "I Agree": "", "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 f97c5dee99..d897be5b1a 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": "切換全螢幕", From c9d803a7a64cd5835e19ef7aa5024a855e93cfaa Mon Sep 17 00:00:00 2001 From: Vincent T Date: Mon, 6 Jan 2025 15:58:57 -0500 Subject: [PATCH 4/5] frontend: ConfirmDialog: Add update to confirm dialog story Signed-off-by: Vincent T --- .../common/ConfirmDialog.stories.tsx | 16 +++ .../src/components/common/ConfirmDialog.tsx | 10 ++ ...onfirmDialogWithCheckbox.stories.storyshot | 125 ++++++++++++++++++ ...DialogWithCheckboxClosed.stories.storyshot | 5 + 4 files changed, 156 insertions(+) create mode 100644 frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckbox.stories.storyshot create mode 100644 frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckboxClosed.stories.storyshot diff --git a/frontend/src/components/common/ConfirmDialog.stories.tsx b/frontend/src/components/common/ConfirmDialog.stories.tsx index 6afb12831d..6ef459c452 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 c8abbe073a..70cecffb86 100644 --- a/frontend/src/components/common/ConfirmDialog.tsx +++ b/frontend/src/components/common/ConfirmDialog.tsx @@ -10,8 +10,18 @@ import { useTranslation } from 'react-i18next'; import { DialogTitle } from './Dialog'; export interface ConfirmDialogProps extends MuiDialogProps { + /** + * Title of the dialog box + */ title: string; + /** + * 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; 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 0000000000..97972a00de --- /dev/null +++ b/frontend/src/components/common/__snapshots__/ConfirmDialog.ConfirmDialogWithCheckbox.stories.storyshot @@ -0,0 +1,125 @@ + +