Skip to content

Commit a8e45bb

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

File tree

9 files changed

+155
-58
lines changed

9 files changed

+155
-58
lines changed

backend/cmd/headlamp.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,18 @@ 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+
246+
return kubeConfigFile, nil
247+
}
248+
237249
// defaultKubeConfigPersistenceDir returns the default directory to store kubeconfig
238250
// files of clusters that are loaded in Headlamp.
239251
func defaultKubeConfigPersistenceDir() (string, error) {
@@ -1384,6 +1396,30 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
13841396
return
13851397
}
13861398

1399+
removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true"
1400+
1401+
if removeKubeConfig {
1402+
// delete context from actual default kubecofig file
1403+
kubeConfigFile, err := defaultKubeConfigFile()
1404+
if err != nil {
1405+
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1406+
err, "failed to get default kubeconfig file path")
1407+
http.Error(w, "failed to get default kubeconfig file path", http.StatusInternalServerError)
1408+
1409+
return
1410+
}
1411+
1412+
// Use kubeConfigFile to remove the context from the default kubeconfig file
1413+
err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile)
1414+
if err != nil {
1415+
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1416+
err, "removing context from default kubeconfig file")
1417+
http.Error(w, "removing context from default kubeconfig file", http.StatusInternalServerError)
1418+
1419+
return
1420+
}
1421+
}
1422+
13871423
kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile()
13881424
if err != nil {
13891425
logger.Log(logger.LevelError, map[string]string{"cluster": name},
@@ -1396,8 +1432,7 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
13961432
logger.Log(logger.LevelInfo, map[string]string{
13971433
"cluster": name,
13981434
"kubeConfigPersistenceFile": kubeConfigPersistenceFile,
1399-
},
1400-
nil, "Removing cluster from kubeconfig")
1435+
}, nil, "Removing cluster from kubeconfig")
14011436

14021437
err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile)
14031438
if err != nil {

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

+37-28
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, t: any): 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
})
@@ -91,7 +109,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
91109
>
92110
<ListItemText>{t('translation|Settings')}</ListItemText>
93111
</MenuItem>
94-
{helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && (
112+
113+
{helpers.isElectron() && (
95114
<MenuItem
96115
onClick={() => {
97116
setOpenConfirmDialog(true);
@@ -105,18 +124,28 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
105124

106125
<ConfirmDialog
107126
open={openConfirmDialog}
108-
handleClose={() => setOpenConfirmDialog(false)}
127+
handleClose={() => {
128+
setOpenConfirmDialog(false);
129+
}}
109130
onConfirm={() => {
110131
setOpenConfirmDialog(false);
111-
removeCluster(cluster);
132+
if (cluster.meta_data?.source !== 'dynamic_cluster') {
133+
removeCluster(cluster, true);
134+
} else {
135+
removeCluster(cluster);
136+
}
112137
}}
113138
title={t('translation|Delete Cluster')}
114139
description={t(
115-
'translation|Are you sure you want to remove the cluster "{{ clusterName }}"?',
140+
'translation|This action will delete cluster "{{ clusterName }}" from {{ source }}.',
116141
{
117142
clusterName: cluster.name,
143+
source: getOrigin(cluster, t),
118144
}
119145
)}
146+
checkboxDescription={
147+
cluster.meta_data?.source !== 'dynamic_cluster' ? t('Delete from kubeconfig') : ''
148+
}
120149
/>
121150
</>
122151
);
@@ -238,24 +267,6 @@ function HomeComponent(props: HomeComponentProps) {
238267
.sort();
239268
}
240269

241-
/**
242-
* Gets the origin of a cluster.
243-
*
244-
* @param cluster
245-
* @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file.
246-
*/
247-
function getOrigin(cluster: Cluster): string {
248-
if (cluster.meta_data?.source === 'kubeconfig') {
249-
const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config';
250-
return `Kubeconfig: ${kubeconfigPath}`;
251-
} else if (cluster.meta_data?.source === 'dynamic_cluster') {
252-
return t('translation|Plugin');
253-
} else if (cluster.meta_data?.source === 'in_cluster') {
254-
return t('translation|In-cluster');
255-
}
256-
return 'Unknown';
257-
}
258-
259270
const memoizedComponent = React.useMemo(
260271
() => (
261272
<PageGrid>
@@ -286,10 +297,8 @@ function HomeComponent(props: HomeComponentProps) {
286297
},
287298
{
288299
label: t('Origin'),
289-
getValue: cluster => getOrigin(cluster),
290-
render: ({ name }) => (
291-
<Typography variant="body2">{getOrigin(clusters[name])}</Typography>
292-
),
300+
getValue: cluster => getOrigin(cluster, t),
301+
render: cluster => <Typography variant="body2">{getOrigin(cluster, t)}</Typography>,
293302
},
294303
{
295304
label: t('Status'),

frontend/src/components/common/ConfirmDialog.tsx

+44-6
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,20 +11,31 @@ import { DialogTitle } from './Dialog';
911

1012
export interface ConfirmDialogProps extends MuiDialogProps {
1113
title: string;
12-
description: string;
14+
description: string | React.ReactNode;
15+
checkboxDescription?: string;
1316
onConfirm: () => void;
1417
handleClose: () => void;
1518
}
1619

1720
export function ConfirmDialog(props: ConfirmDialogProps) {
1821
const { onConfirm, open, handleClose, title, description } = props;
1922
const { t } = useTranslation();
23+
const [checkedChoice, setcheckedChoice] = React.useState(false);
2024

2125
function onConfirmationClicked() {
2226
handleClose();
2327
onConfirm();
2428
}
2529

30+
function closeDialog() {
31+
setcheckedChoice(false);
32+
handleClose();
33+
}
34+
35+
function handleChoiceToggle() {
36+
setcheckedChoice(!checkedChoice);
37+
}
38+
2639
const focusedRef = React.useCallback((node: HTMLElement) => {
2740
if (node !== null) {
2841
node.setAttribute('tabindex', '-1');
@@ -34,21 +47,46 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
3447
<div>
3548
<MuiDialog
3649
open={open}
37-
onClose={handleClose}
50+
onClose={closeDialog}
3851
aria-labelledby="alert-dialog-title"
3952
aria-describedby="alert-dialog-description"
4053
>
4154
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
4255
<DialogContent ref={focusedRef}>
4356
<DialogContentText id="alert-dialog-description">{description}</DialogContentText>
57+
{props.checkboxDescription && (
58+
<Box
59+
sx={{
60+
display: 'flex',
61+
alignItems: 'center',
62+
marginTop: '10px',
63+
}}
64+
>
65+
<DialogContentText id="alert-dialog-description">
66+
{props.checkboxDescription}
67+
</DialogContentText>
68+
<Checkbox checked={checkedChoice} onChange={handleChoiceToggle} />
69+
</Box>
70+
)}
4471
</DialogContent>
4572
<DialogActions>
46-
<Button onClick={handleClose} color="primary">
73+
<Button
74+
onClick={() => {
75+
closeDialog();
76+
}}
77+
color="primary"
78+
>
4779
{t('No')}
4880
</Button>
49-
<Button onClick={onConfirmationClicked} color="primary">
50-
{t('Yes')}
51-
</Button>
81+
{props.checkboxDescription ? (
82+
<Button disabled={!checkedChoice} onClick={onConfirmationClicked} color="primary">
83+
{t('I Agree')}
84+
</Button>
85+
) : (
86+
<Button onClick={onConfirmationClicked} color="primary">
87+
{t('Yes')}
88+
</Button>
89+
)}
5290
</DialogActions>
5391
</MuiDialog>
5492
</div>

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
"Cluster Settings ({{ clusterName }})": "Cluster-Einstellungen ({{ clusterName }})",
8283
"Go to cluster": "",
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
"Cluster Settings ({{ clusterName }})": "Cluster Settings ({{ clusterName }})",
8283
"Go to cluster": "Go to cluster",
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
"Cluster Settings ({{ clusterName }})": "Configuración del cluster ({{ clusterName }})",
8283
"Go to cluster": "",
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",

0 commit comments

Comments
 (0)