From af895512ab41fa79a8cc0ce95c31896074d15989 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Thu, 25 Jul 2024 17:08:42 -0400 Subject: [PATCH] frontend: Unify general and cluster settings Accessing settings in Headlamp is confusing. This change adds the cluster settings to the sidebar along with the general and plugin settings. When we open a cluster, clicking the Settings (gear) icon at the top will now take us to the cluster settings in the home context. If there are multiple clusters set up, a cluster selector will appear at the top of the cluster settings page. Fixes: #1927 and #2037 Signed-off-by: Evangelos Skopelitis --- .../App/Settings/SettingsCluster.tsx | 429 +++++++++--------- ...idebar.HomeSidebarClosed.stories.storyshot | 48 +- .../Sidebar.HomeSidebarOpen.stories.storyshot | 48 +- .../src/components/Sidebar/prepareRoutes.ts | 12 +- 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/lib/router.tsx | 27 +- 10 files changed, 352 insertions(+), 222 deletions(-) diff --git a/frontend/src/components/App/Settings/SettingsCluster.tsx b/frontend/src/components/App/Settings/SettingsCluster.tsx index 5a978b42ddc..4b529f8454e 100644 --- a/frontend/src/components/App/Settings/SettingsCluster.tsx +++ b/frontend/src/components/App/Settings/SettingsCluster.tsx @@ -1,5 +1,5 @@ import { Icon, InlineIcon } from '@iconify/react'; -import { Box, Chip, IconButton, TextField } from '@mui/material'; +import { Box, Chip, FormControl, IconButton, MenuItem, Select, TextField } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ import { useCluster, useClustersConf } from '../../../lib/k8s'; import { deleteCluster, parseKubeConfig, renameCluster } from '../../../lib/k8s/apiProxy'; import { setConfig, setStatelessConfig } from '../../../redux/configSlice'; import { findKubeconfigByClusterName, updateStatelessClusterKubeconfig } from '../../../stateless/'; -import { Link, NameValueTable, SectionBox } from '../../common'; +import { NameValueTable, SectionBox } from '../../common'; import ConfirmButton from '../../common/ConfirmButton'; function isValidNamespaceFormat(namespace: string) { @@ -38,14 +38,15 @@ function isValidClusterNameFormat(name: string) { } export default function SettingsCluster() { - const cluster = useCluster(); const clusterConf = useClustersConf(); + const clusters = Object.values(clusterConf || {}).map(cluster => cluster.name); const { t } = useTranslation(['translation']); const [defaultNamespace, setDefaultNamespace] = React.useState('default'); const [userDefaultNamespace, setUserDefaultNamespace] = React.useState(''); const [newAllowedNamespace, setNewAllowedNamespace] = React.useState(''); const [clusterSettings, setClusterSettings] = React.useState(null); - const [newClusterName, setNewClusterName] = React.useState(cluster || ''); + const [newClusterName, setNewClusterName] = React.useState(useCluster() || ''); + const [cluster, setCluster] = React.useState(useCluster() || ''); const theme = useTheme(); const history = useHistory(); @@ -58,25 +59,23 @@ export default function SettingsCluster() { try { renameCluster(cluster || '', newClusterName, source) .then(async config => { - if (cluster) { - const kubeconfig = await findKubeconfigByClusterName(cluster); - if (kubeconfig !== null) { - await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster); - // Make another request for updated kubeconfig - const updatedKubeconfig = await findKubeconfigByClusterName(cluster); - if (updatedKubeconfig !== null) { - parseKubeConfig({ kubeconfig: updatedKubeconfig }) - .then((config: any) => { - storeNewClusterName(newClusterName); - dispatch(setStatelessConfig(config)); - }) - .catch((err: Error) => { - console.error('Error updating cluster name:', err.message); - }); - } - } else { - dispatch(setConfig(config)); + const kubeconfig = await findKubeconfigByClusterName(cluster); + if (kubeconfig !== null) { + await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster); + // Make another request for updated kubeconfig + const updatedKubeconfig = await findKubeconfigByClusterName(cluster); + if (updatedKubeconfig !== null) { + parseKubeConfig({ kubeconfig: updatedKubeconfig }) + .then((config: any) => { + storeNewClusterName(newClusterName); + dispatch(setStatelessConfig(config)); + }) + .catch((err: Error) => { + console.error('Error updating cluster name:', err.message); + }); } + } else { + dispatch(setConfig(config)); } history.push('/'); window.location.reload(); @@ -116,6 +115,12 @@ export default function SettingsCluster() { setClusterSettings(!!cluster ? helpers.loadClusterSettings(cluster || '') : null); }, [cluster]); + React.useEffect(() => { + if (clusters.length > 0 && !cluster) { + setCluster(clusters[0]); + } + }, [clusters, cluster]); + React.useEffect(() => { const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null; const clusterConfNs = clusterInfo?.meta_data?.namespace; @@ -163,10 +168,6 @@ export default function SettingsCluster() { return clusterSettings?.defaultNamespace !== userDefaultNamespace; } - if (!cluster) { - return null; - } - function storeNewAllowedNamespace(namespace: string) { setNewAllowedNamespace(''); setClusterSettings((settings: ClusterSettings | null) => { @@ -196,16 +197,11 @@ export default function SettingsCluster() { } function storeNewClusterName(name: string) { - let actualName = name; - if (name === cluster) { - actualName = ''; - setNewClusterName(actualName); - } - + setNewClusterName(name); setClusterSettings((settings: ClusterSettings | null) => { const newSettings = { ...(settings || {}) }; if (isValidClusterNameFormat(name)) { - newSettings.currentName = actualName; + newSettings.currentName = name; } return newSettings; }); @@ -221,201 +217,204 @@ export default function SettingsCluster() { const invalidClusterNameMessage = t( "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." ); + const isValidCluster = clusters.includes(cluster) && location.pathname.includes(cluster); return ( <> 1 - ? t('translation|Cluster Settings ({{ clusterName }})', { clusterName: cluster || '' }) + clusters.length < 2 || isValidCluster + ? t('translation|Cluster Settings ({{ clusterName }})', { + clusterName: cluster || '', + }) : t('translation|Cluster Settings') } backLink - headerProps={{ - actions: [ - - {t('translation|General Settings')} - , - ], - }} > - {helpers.isElectron() && ( - { - let value = event.target.value; - value = value.replace(' ', ''); - setNewClusterName(value); - }} - value={newClusterName} - placeholder={cluster} - error={!isValidCurrentName} - helperText={ - isValidCurrentName - ? t( - 'translation|The current name of cluster. You can define custom modified name.' - ) - : invalidClusterNameMessage - } - InputProps={{ - endAdornment: ( - - { - if (isValidCurrentName) { - handleUpdateClusterName(source); - } - }} - confirmTitle={t('translation|Change name')} - confirmDescription={t( - 'translation|Are you sure you want to change the name for "{{ clusterName }}"?', - { clusterName: cluster } - )} - disabled={!newClusterName || !isValidCurrentName} - > - {t('translation|Apply')} - - - ), - onKeyPress: event => { - if (event.key === 'Enter' && isValidCurrentName) { - handleUpdateClusterName(source); - } - }, - autoComplete: 'off', - sx: { maxWidth: 250 }, - }} - /> - ), - }, - ]} - /> + {clusters.length > 1 && !location.pathname.includes(cluster) && ( + + + )} - { - let value = event.target.value; - value = value.replace(' ', ''); - setUserDefaultNamespace(value); - }} - value={userDefaultNamespace} - placeholder={defaultNamespace} - error={!isValidDefaultNamespace} - helperText={ - isValidDefaultNamespace - ? t( - 'translation|The default namespace for e.g. when applying resources (when not specified directly).' - ) - : invalidNamespaceMessage - } - InputProps={{ - endAdornment: isEditingDefaultNamespace() ? ( - - ) : ( - - ), - sx: { maxWidth: 250 }, - }} - /> - ), - }, - { - name: t('translation|Allowed namespaces'), - value: ( - <> - { - let value = event.target.value; - value = value.replace(' ', ''); - setNewAllowedNamespace(value); - }} - placeholder="namespace" - error={!isValidNewAllowedNamespace} - value={newAllowedNamespace} - helperText={ - isValidNewAllowedNamespace - ? t( - 'translation|The list of namespaces you are allowed to access in this cluster.' - ) - : invalidNamespaceMessage - } - autoComplete="off" - inputProps={{ - form: { - autocomplete: 'off', - }, - }} - InputProps={{ - endAdornment: ( - { - storeNewAllowedNamespace(newAllowedNamespace); - }} - disabled={!newAllowedNamespace} - size="medium" - > - - - ), - onKeyPress: event => { - if (event.key === 'Enter') { - storeNewAllowedNamespace(newAllowedNamespace); + {!location.pathname.includes(cluster) || isValidCluster ? ( + <> + { + let value = event.target.value; + value = value.replace(' ', ''); + setNewClusterName(value); + }} + value={newClusterName} + placeholder={cluster} + error={!isValidCurrentName} + helperText={ + isValidCurrentName + ? t( + 'translation|The current name of cluster. You can define custom modified name.' + ) + : invalidClusterNameMessage + } + InputProps={{ + endAdornment: ( + + { + if (isValidCurrentName) { + handleUpdateClusterName(source); + } + }} + confirmTitle={t('translation|Change name')} + confirmDescription={t( + 'translation|Are you sure you want to change the name for "{{ clusterName }}"?', + { clusterName: cluster } + )} + disabled={!newClusterName || !isValidCurrentName} + > + {t('translation|Apply')} + + + ), + onKeyPress: event => { + if (event.key === 'Enter' && isValidCurrentName) { + handleUpdateClusterName(source); + } + }, + autoComplete: 'off', + sx: { maxWidth: 250 }, + }} + /> + ), + }, + ]} + /> + { + let value = event.target.value; + value = value.replace(' ', ''); + setUserDefaultNamespace(value); + }} + value={userDefaultNamespace} + placeholder={defaultNamespace} + error={!isValidDefaultNamespace} + helperText={ + isValidDefaultNamespace + ? t( + 'translation|The default namespace for e.g. when applying resources (when not specified directly).' + ) + : invalidNamespaceMessage + } + InputProps={{ + endAdornment: isEditingDefaultNamespace() ? ( + + ) : ( + + ), + sx: { maxWidth: 250 }, + }} + /> + ), + }, + { + name: t('translation|Allowed namespaces'), + value: ( + <> + { + let value = event.target.value; + value = value.replace(' ', ''); + setNewAllowedNamespace(value); + }} + placeholder="namespace" + error={!isValidNewAllowedNamespace} + value={newAllowedNamespace} + helperText={ + isValidNewAllowedNamespace + ? t( + 'translation|The list of namespaces you are allowed to access in this cluster.' + ) + : invalidNamespaceMessage } - }, - autoComplete: 'off', - sx: { maxWidth: 250 }, - }} - /> - *': { - margin: theme.spacing(0.5), - }, - marginTop: theme.spacing(1), - }} - aria-label={t('translation|Allowed namespaces')} - > - {((clusterSettings || {}).allowedNamespaces || []).map(namespace => ( - { - setClusterSettings(settings => { - const newSettings = { ...settings }; - newSettings.allowedNamespaces = newSettings.allowedNamespaces?.filter( - ns => ns !== namespace - ); - return newSettings; - }); + autoComplete="off" + inputProps={{ + form: { + autocomplete: 'off', + }, + }} + InputProps={{ + endAdornment: ( + { + storeNewAllowedNamespace(newAllowedNamespace); + }} + disabled={!newAllowedNamespace} + size="medium" + > + + + ), + onKeyPress: event => { + if (event.key === 'Enter') { + storeNewAllowedNamespace(newAllowedNamespace); + } + }, + autoComplete: 'off', + sx: { maxWidth: 250 }, }} /> - ))} - - - ), - }, - ]} - /> + + {((clusterSettings || {}).allowedNamespaces || []).map(namespace => ( + { + setClusterSettings(settings => { + const newSettings = { ...settings }; + newSettings.allowedNamespaces = + newSettings.allowedNamespaces?.filter(ns => ns !== namespace); + return newSettings; + }); + }} + /> + ))} + + + ), + }, + ]} + /> + + ) : ( + + {t( + 'translation|Cluster {{ clusterName }} does not exist. Please select a valid cluster.', + { + clusterName: cluster, + } + )} + + )} {removableCluster && helpers.isElectron() && ( diff --git a/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot b/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot index 3f4b03c9929..3b05d255c75 100644 --- a/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot +++ b/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot @@ -64,7 +64,7 @@ > @@ -94,6 +94,29 @@ diff --git a/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarOpen.stories.storyshot b/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarOpen.stories.storyshot index 2423c4ede41..68cb635cbde 100644 --- a/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarOpen.stories.storyshot +++ b/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarOpen.stories.storyshot @@ -78,7 +78,7 @@ > @@ -115,6 +115,29 @@ diff --git a/frontend/src/components/Sidebar/prepareRoutes.ts b/frontend/src/components/Sidebar/prepareRoutes.ts index 53804a152a7..06bc3602534 100644 --- a/frontend/src/components/Sidebar/prepareRoutes.ts +++ b/frontend/src/components/Sidebar/prepareRoutes.ts @@ -35,13 +35,23 @@ function prepareRoutes( name: 'settings', icon: 'mdi:cog', label: t('translation|Settings'), - url: '/settings', + url: '/settings/general', subList: [ + { + name: 'general', + label: t('translation|General'), + url: '/settings/general', + }, { name: 'plugins', label: t('translation|Plugins'), url: '/settings/plugins', }, + { + name: 'settingsCluster', + label: t('glossary|Cluster'), + url: '/settings/cluster', + }, ], }, ]; diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index f63653e0c3a..204d7c6c477 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -71,6 +71,7 @@ "The default namespace for e.g. when applying resources (when not specified directly).": "Der Standard-Namespace z. B. für die Anwendung von Ressourcen (wenn nicht anders angegeben).", "Allowed namespaces": "Erlaubte Namespaces", "The list of namespaces you are allowed to access in this cluster.": "Liste der Namespaces, auf die Sie in diesem Cluster zugreifen dürfen.", + "Cluster {{ clusterName }} does not exist. Please select a valid cluster.": "", "Remove Cluster": "Cluster entfernen", "Server": "Server", "light theme": "helles Design", @@ -370,6 +371,7 @@ "Expand sidebar": "Seitenleiste erweitern", "Main Navigation": "Hauptnavigation", "Navigation Tabs": "Navigationstabs", + "General": "", "Collapse Sidebar": "Seitenleiste einklappen", "Navigation": "Navigation", "Cluster version upgraded to {{ gitVersion }}": "Cluster-Version auf {{ gitVersion }} aktualisiert", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 231f890de42..d62fa68b97e 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -71,6 +71,7 @@ "The default namespace for e.g. when applying resources (when not specified directly).": "The default namespace for e.g. when applying resources (when not specified directly).", "Allowed namespaces": "Allowed namespaces", "The list of namespaces you are allowed to access in this cluster.": "The list of namespaces you are allowed to access in this cluster.", + "Cluster {{ clusterName }} does not exist. Please select a valid cluster.": "Cluster {{ clusterName }} does not exist. Please select a valid cluster.", "Remove Cluster": "Remove Cluster", "Server": "Server", "light theme": "light theme", @@ -370,6 +371,7 @@ "Expand sidebar": "Expand sidebar", "Main Navigation": "Main Navigation", "Navigation Tabs": "Navigation Tabs", + "General": "General", "Collapse Sidebar": "Collapse Sidebar", "Navigation": "Navigation", "Cluster version upgraded to {{ gitVersion }}": "Cluster version upgraded to {{ gitVersion }}", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index 9a21f394781..5526728c719 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -71,6 +71,7 @@ "The default namespace for e.g. when applying resources (when not specified directly).": "El espacio de nombre por defecto para, por ejemplo, cuando se aplican recursos (cuando no especificado directamente).", "Allowed namespaces": "Espacios de nombre permitidos", "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.", + "Cluster {{ clusterName }} does not exist. Please select a valid cluster.": "", "Remove Cluster": "Eliminar cluster", "Server": "Servidor", "light theme": "tema claro", @@ -371,6 +372,7 @@ "Expand sidebar": "Expandir barra lateral", "Main Navigation": "Navegación principal", "Navigation Tabs": "Pestañas de navegación", + "General": "", "Collapse Sidebar": "Contraer barra lateral", "Navigation": "Navegación", "Cluster version upgraded to {{ gitVersion }}": "Versión del cluster actualizada a {{ gitVersion }}", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 0c90477df8b..7db9d4fc40b 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -71,6 +71,7 @@ "The default namespace for e.g. when applying resources (when not specified directly).": "L'espace de noms par défaut, par exemple lors de l'application de ressources (lorsqu'il n'est pas spécifié directement).", "Allowed namespaces": "Espaces de noms autorisés", "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.", + "Cluster {{ clusterName }} does not exist. Please select a valid cluster.": "", "Remove Cluster": "Supprimer le cluster", "Server": "Serveur", "light theme": "thème clair", @@ -371,6 +372,7 @@ "Expand sidebar": "Développer la barre latérale", "Main Navigation": "Navigation principale", "Navigation Tabs": "Onglets de navigation", + "General": "", "Collapse Sidebar": "Réduire la barre latérale", "Navigation": "Navigation", "Cluster version upgraded to {{ gitVersion }}": "Version du cluster mise à jour vers {{ gitVersion }}", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 9a3590fa627..9c351e50e01 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -71,6 +71,7 @@ "The default namespace for e.g. when applying resources (when not specified directly).": "O namespace por defeito, por exemplo, quando se aplicam recursos (quando não especificado directamente).", "Allowed namespaces": "Namespaces permitidos", "The list of namespaces you are allowed to access in this cluster.": "A lista de namespaces que tem permissão para aceder neste cluster.", + "Cluster {{ clusterName }} does not exist. Please select a valid cluster.": "", "Remove Cluster": "Remover Cluster", "Server": "Servidor", "light theme": "tema claro", @@ -371,6 +372,7 @@ "Expand sidebar": "Expandir barra lateral", "Main Navigation": "Navegação Principal", "Navigation Tabs": "Separadores de Navegação", + "General": "", "Collapse Sidebar": "Encolher Barra Lateral", "Navigation": "Navegação", "Cluster version upgraded to {{ gitVersion }}": "Versão do cluster actualizada para {{ gitVersion }}", diff --git a/frontend/src/lib/router.tsx b/frontend/src/lib/router.tsx index 279cff2fecb..c4e619a0ef4 100644 --- a/frontend/src/lib/router.tsx +++ b/frontend/src/lib/router.tsx @@ -669,11 +669,11 @@ const defaultRoutes: { ), }, settings: { - path: '/settings', + path: '/settings/general', exact: true, name: 'Settings', sidebar: { - item: 'settings', + item: 'general', sidebar: DefaultSidebars.HOME, }, useClusterURL: false, @@ -684,7 +684,6 @@ const defaultRoutes: { ), }, - settingsClusters: { path: '/settings/clusters', exact: true, @@ -702,7 +701,27 @@ const defaultRoutes: { path: '/settings', exact: true, name: 'Cluster Settings', - sidebar: 'settingsCluster', + sidebar: { + item: 'settingsCluster', + sidebar: DefaultSidebars.HOME, + }, + useClusterURL: true, + noAuthRequired: true, + component: () => ( + + + + ), + }, + settingsClusterHomeContext: { + path: '/settings/cluster', + exact: true, + name: 'Cluster Settings', + sidebar: { + item: 'settingsCluster', + sidebar: DefaultSidebars.HOME, + }, + useClusterURL: false, noAuthRequired: true, component: () => (