Skip to content

Commit a248958

Browse files
committed
app: home: Add delete button for clusters
Signed-off-by: Vincent T <[email protected]>
1 parent 82eb285 commit a248958

File tree

9 files changed

+145
-48
lines changed

9 files changed

+145
-48
lines changed

backend/cmd/headlamp.go

+32-2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,17 @@ func serveWithNoCacheHeader(fs http.Handler) http.HandlerFunc {
234234
}
235235
}
236236

237+
// defaultKubeConfigFile returns the default path to the kubeconfig file.
238+
func defaultKubeConfigFile() (string, error) {
239+
homeDir, err := os.UserHomeDir()
240+
if err != nil {
241+
return "", fmt.Errorf("failed to get user home directory: %v", err)
242+
}
243+
244+
kubeConfigFile := filepath.Join(homeDir, ".kube", "config")
245+
return kubeConfigFile, nil
246+
}
247+
237248
// defaultKubeConfigPersistenceDir returns the default directory to store kubeconfig
238249
// files of clusters that are loaded in Headlamp.
239250
func defaultKubeConfigPersistenceDir() (string, error) {
@@ -1384,6 +1395,26 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
13841395
return
13851396
}
13861397

1398+
removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true"
1399+
1400+
if removeKubeConfig {
1401+
// delete context from actual deafult kubecofig file
1402+
kubeConfigFile, err := defaultKubeConfigFile()
1403+
if err != nil {
1404+
logger.Log(logger.LevelError, map[string]string{"cluster": name}, err, "failed to get default kubeconfig file path")
1405+
http.Error(w, "failed to get default kubeconfig file path", http.StatusInternalServerError)
1406+
return
1407+
}
1408+
1409+
// Use kubeConfigFile to remove the context from the default kubeconfig file
1410+
err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile)
1411+
if err != nil {
1412+
logger.Log(logger.LevelError, map[string]string{"cluster": name}, err, "removing context from default kubeconfig file")
1413+
http.Error(w, "removing context from default kubeconfig file", http.StatusInternalServerError)
1414+
return
1415+
}
1416+
}
1417+
13871418
kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile()
13881419
if err != nil {
13891420
logger.Log(logger.LevelError, map[string]string{"cluster": name},
@@ -1396,8 +1427,7 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
13961427
logger.Log(logger.LevelInfo, map[string]string{
13971428
"cluster": name,
13981429
"kubeConfigPersistenceFile": kubeConfigPersistenceFile,
1399-
},
1400-
nil, "Removing cluster from kubeconfig")
1430+
}, nil, "Removing cluster from kubeconfig")
14011431

14021432
err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile)
14031433
if err != nil {

frontend/src/components/App/Home/index.tsx

+32-23
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ import { ConfirmDialog } from '../../common';
2525
import ResourceTable from '../../common/Resource/ResourceTable';
2626
import RecentClusters from './RecentClusters';
2727

28+
/**
29+
* Gets the origin of a cluster.
30+
*
31+
* @param cluster
32+
* @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file.
33+
*/
34+
function getOrigin(cluster: Cluster): string {
35+
if (cluster.meta_data?.source === 'kubeconfig') {
36+
const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config';
37+
return `Kubeconfig: ${kubeconfigPath}`;
38+
} else if (cluster.meta_data?.source === 'dynamic_cluster') {
39+
return t('translation|Plugin');
40+
} else if (cluster.meta_data?.source === 'in_cluster') {
41+
return t('translation|In-cluster');
42+
}
43+
return 'Unknown';
44+
}
45+
2846
function ContextMenu({ cluster }: { cluster: Cluster }) {
2947
const { t } = useTranslation(['translation']);
3048
const history = useHistory();
@@ -33,8 +51,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
3351
const menuId = useId('context-menu');
3452
const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false);
3553

36-
function removeCluster(cluster: Cluster) {
37-
deleteCluster(cluster.name || '')
54+
function removeCluster(cluster: Cluster, removeKubeconfig?: boolean) {
55+
deleteCluster(cluster.name || '', removeKubeconfig)
3856
.then(config => {
3957
dispatch(setConfig(config));
4058
})
@@ -92,7 +110,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
92110
>
93111
<ListItemText>{t('translation|Settings')}</ListItemText>
94112
</MenuItem>
95-
{helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && (
113+
114+
{helpers.isElectron() && (
96115
<MenuItem
97116
onClick={() => {
98117
setOpenConfirmDialog(true);
@@ -109,15 +128,23 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
109128
handleClose={() => setOpenConfirmDialog(false)}
110129
onConfirm={() => {
111130
setOpenConfirmDialog(false);
112-
removeCluster(cluster);
131+
if (cluster.meta_data?.source !== 'dynamic_cluster') {
132+
removeCluster(cluster, true);
133+
} else {
134+
removeCluster(cluster);
135+
}
113136
}}
114137
title={t('translation|Delete Cluster')}
115138
description={t(
116-
'translation|Are you sure you want to remove the cluster "{{ clusterName }}"?',
139+
'translation|This action will delete cluster "{{ clusterName }}" from {{ source }}.',
117140
{
118141
clusterName: cluster.name,
142+
source: getOrigin(cluster),
119143
}
120144
)}
145+
checkboxDescription={
146+
cluster.meta_data?.source !== 'dynamic_cluster' ? t('Delete from kubeconfig') : ''
147+
}
121148
/>
122149
</>
123150
);
@@ -239,24 +266,6 @@ function HomeComponent(props: HomeComponentProps) {
239266
.sort();
240267
}
241268

242-
/**
243-
* Gets the origin of a cluster.
244-
*
245-
* @param cluster
246-
* @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file.
247-
*/
248-
function getOrigin(cluster: Cluster): string {
249-
if (cluster.meta_data?.source === 'kubeconfig') {
250-
const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config';
251-
return `Kubeconfig: ${kubeconfigPath}`;
252-
} else if (cluster.meta_data?.source === 'dynamic_cluster') {
253-
return t('translation|Plugin');
254-
} else if (cluster.meta_data?.source === 'in_cluster') {
255-
return t('translation|In-cluster');
256-
}
257-
return 'Unknown';
258-
}
259-
260269
const memoizedComponent = React.useMemo(
261270
() => (
262271
<PageGrid>

frontend/src/components/common/ConfirmDialog.tsx

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Checkbox } from '@mui/material';
2+
import Box from '@mui/material/Box';
13
import Button from '@mui/material/Button';
24
import MuiDialog, { DialogProps as MuiDialogProps } from '@mui/material/Dialog';
35
import DialogActions from '@mui/material/DialogActions';
@@ -9,7 +11,8 @@ import { DialogTitle } from './Dialog';
911

1012
export interface ConfirmDialogProps extends MuiDialogProps {
1113
title: string;
12-
description: string;
14+
description: string | JSX.Element;
15+
checkboxDescription?: string;
1316
onConfirm: () => void;
1417
handleClose: () => void;
1518
}
@@ -18,6 +21,8 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
1821
const { onConfirm, open, handleClose, title, description } = props;
1922
const { t } = useTranslation();
2023

24+
const [checkboxClicked, setCheckboxClicked] = React.useState(false);
25+
2126
function onConfirmationClicked() {
2227
handleClose();
2328
onConfirm();
@@ -30,6 +35,44 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
3035
}
3136
}, []);
3237

38+
if (props.checkboxDescription) {
39+
return (
40+
<div>
41+
<MuiDialog
42+
open={open}
43+
onClose={handleClose}
44+
aria-labelledby="alert-dialog-title"
45+
aria-describedby="alert-dialog-description"
46+
>
47+
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
48+
<DialogContent ref={focusedRef}>
49+
<DialogContentText id="alert-dialog-description">{description}</DialogContentText>
50+
<Box
51+
sx={{
52+
display: 'flex',
53+
alignItems: 'center',
54+
marginTop: '10px',
55+
}}
56+
>
57+
<DialogContentText id="alert-dialog-description">
58+
{props.checkboxDescription}
59+
</DialogContentText>
60+
<Checkbox onChange={() => setCheckboxClicked(!checkboxClicked)} />
61+
</Box>
62+
</DialogContent>
63+
<DialogActions>
64+
<Button onClick={handleClose} color="primary">
65+
{t('Cancel')}
66+
</Button>
67+
<Button disabled={!checkboxClicked} onClick={onConfirmationClicked} color="primary">
68+
{t('I Agree')}
69+
</Button>
70+
</DialogActions>
71+
</MuiDialog>
72+
</div>
73+
);
74+
}
75+
3376
return (
3477
<div>
3578
<MuiDialog

frontend/src/i18n/locales/de/translation.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
"Cancel": "Abbrechen",
1010
"Authenticate": "Authentifizieren Sie",
1111
"Error authenticating": "Fehler beim Authentifizieren",
12+
"Plugin": "",
13+
"In-cluster": "",
1214
"Actions": "Aktionen",
1315
"View": "Ansicht",
1416
"Settings": "Einstellungen",
1517
"Delete": "Löschen",
1618
"Delete Cluster": "",
17-
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Sind Sie sicher, dass Sie den Cluster \"{{ clusterName }}\" entfernen möchten?",
19+
"This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "",
20+
"Delete from kubeconfig": "",
1821
"Active": "Aktiv",
19-
"Plugin": "",
20-
"In-cluster": "",
2122
"Home": "Startseite",
2223
"All Clusters": "Alle Cluster",
2324
"Name": "Name",
@@ -81,6 +82,7 @@
8182
"The list of namespaces you are allowed to access in this cluster.": "Liste der Namespaces, auf die Sie in diesem Cluster zugreifen dürfen.",
8283
"Add namespace": "",
8384
"Remove Cluster": "Cluster entfernen",
85+
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Sind Sie sicher, dass Sie den Cluster \"{{ clusterName }}\" entfernen möchten?",
8486
"Server": "Server",
8587
"light theme": "helles Design",
8688
"dark theme": "dunkles Design",
@@ -144,6 +146,7 @@
144146
"Last Seen": "Zuletzt gesehen",
145147
"Offline": "Offline",
146148
"Lost connection to the cluster.": "",
149+
"I Agree": "",
147150
"No": "Nein",
148151
"Yes": "Ja",
149152
"Toggle fullscreen": "Vollbild ein/aus",

frontend/src/i18n/locales/en/translation.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
"Cancel": "Cancel",
1010
"Authenticate": "Authenticate",
1111
"Error authenticating": "Error authenticating",
12+
"Plugin": "Plugin",
13+
"In-cluster": "In-cluster",
1214
"Actions": "Actions",
1315
"View": "View",
1416
"Settings": "Settings",
1517
"Delete": "Delete",
1618
"Delete Cluster": "Delete Cluster",
17-
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Are you sure you want to remove the cluster \"{{ clusterName }}\"?",
19+
"This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.",
20+
"Delete from kubeconfig": "Delete from kubeconfig",
1821
"Active": "Active",
19-
"Plugin": "Plugin",
20-
"In-cluster": "In-cluster",
2122
"Home": "Home",
2223
"All Clusters": "All Clusters",
2324
"Name": "Name",
@@ -81,6 +82,7 @@
8182
"The list of namespaces you are allowed to access in this cluster.": "The list of namespaces you are allowed to access in this cluster.",
8283
"Add namespace": "Add namespace",
8384
"Remove Cluster": "Remove Cluster",
85+
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Are you sure you want to remove the cluster \"{{ clusterName }}\"?",
8486
"Server": "Server",
8587
"light theme": "light theme",
8688
"dark theme": "dark theme",
@@ -144,6 +146,7 @@
144146
"Last Seen": "Last Seen",
145147
"Offline": "Offline",
146148
"Lost connection to the cluster.": "Lost connection to the cluster.",
149+
"I Agree": "I Agree",
147150
"No": "No",
148151
"Yes": "Yes",
149152
"Toggle fullscreen": "Toggle fullscreen",

frontend/src/i18n/locales/es/translation.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
"Cancel": "Cancelar",
1010
"Authenticate": "Autenticar",
1111
"Error authenticating": "Error al autenticarse",
12+
"Plugin": "",
13+
"In-cluster": "",
1214
"Actions": "Acciones",
1315
"View": "Ver",
1416
"Settings": "Definiciones",
1517
"Delete": "Borrar",
1618
"Delete Cluster": "",
17-
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "¿Está seguro de que desea eliminar el cluster \"{{ clusterName }}\"?",
19+
"This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "",
20+
"Delete from kubeconfig": "",
1821
"Active": "Activo",
19-
"Plugin": "",
20-
"In-cluster": "",
2122
"Home": "Inicio",
2223
"All Clusters": "Todos los Clusters",
2324
"Name": "Nombre",
@@ -81,6 +82,7 @@
8182
"The list of namespaces you are allowed to access in this cluster.": "La lista de espacios de nombre a los que tiene permiso para acceder en este cluster.",
8283
"Add namespace": "",
8384
"Remove Cluster": "Eliminar cluster",
85+
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "¿Está seguro de que desea eliminar el cluster \"{{ clusterName }}\"?",
8486
"Server": "Servidor",
8587
"light theme": "tema claro",
8688
"dark theme": "tema oscuro",
@@ -144,6 +146,7 @@
144146
"Last Seen": "Últi. ocurrencia",
145147
"Offline": "Desconectado",
146148
"Lost connection to the cluster.": "",
149+
"I Agree": "",
147150
"No": "No",
148151
"Yes": "",
149152
"Toggle fullscreen": "Alternar pantalla completa",

frontend/src/i18n/locales/fr/translation.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
"Cancel": "Cancel",
1010
"Authenticate": "Authentifier",
1111
"Error authenticating": "Erreur d'authentification",
12+
"Plugin": "",
13+
"In-cluster": "",
1214
"Actions": "Actions",
1315
"View": "Vue",
1416
"Settings": "Paramètres",
1517
"Delete": "Supprimer",
1618
"Delete Cluster": "",
17-
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Voulez-vous vraiment supprimer le cluster \"{{ clusterName }}\"?",
19+
"This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "",
20+
"Delete from kubeconfig": "",
1821
"Active": "Actif",
19-
"Plugin": "",
20-
"In-cluster": "",
2122
"Home": "Accueil",
2223
"All Clusters": "Tous les clusters",
2324
"Name": "Nom",
@@ -81,6 +82,7 @@
8182
"The list of namespaces you are allowed to access in this cluster.": "La liste des espaces de noms que vous pouvez accéder dans ce cluster.",
8283
"Add namespace": "",
8384
"Remove Cluster": "Supprimer le cluster",
85+
"Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Voulez-vous vraiment supprimer le cluster \"{{ clusterName }}\"?",
8486
"Server": "Serveur",
8587
"light theme": "thème clair",
8688
"dark theme": "thème sombre",
@@ -144,6 +146,7 @@
144146
"Last Seen": "Dernière vue",
145147
"Offline": "Hors ligne",
146148
"Lost connection to the cluster.": "",
149+
"I Agree": "",
147150
"No": "Non",
148151
"Yes": "Oui",
149152
"Toggle fullscreen": "Basculer en mode plein écran",

0 commit comments

Comments
 (0)