From 8ce6cfe21c3d8ac79c6757461b56f7161b30a261 Mon Sep 17 00:00:00 2001 From: farodin91 Date: Wed, 27 Nov 2024 22:53:46 +0100 Subject: [PATCH] frontend: refactor settings cluster Signed-off-by: farodin91 --- .../App/Settings/AllowedNamespaces.tsx | 109 +++++ .../App/Settings/CustomClusterName.tsx | 149 +++++++ .../App/Settings/DefaultNamespace.tsx | 131 ++++++ .../App/Settings/SettingsCluster.tsx | 402 +++--------------- 4 files changed, 437 insertions(+), 354 deletions(-) create mode 100644 frontend/src/components/App/Settings/AllowedNamespaces.tsx create mode 100644 frontend/src/components/App/Settings/CustomClusterName.tsx create mode 100644 frontend/src/components/App/Settings/DefaultNamespace.tsx diff --git a/frontend/src/components/App/Settings/AllowedNamespaces.tsx b/frontend/src/components/App/Settings/AllowedNamespaces.tsx new file mode 100644 index 0000000000..119c0e7132 --- /dev/null +++ b/frontend/src/components/App/Settings/AllowedNamespaces.tsx @@ -0,0 +1,109 @@ +import { InlineIcon } from '@iconify/react'; +import { Box, Chip, IconButton, TextField, useTheme } from '@mui/material'; +import { Dispatch, SetStateAction, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ClusterSettings } from '../../../helpers'; +import { isValidNamespaceFormat } from './DefaultNamespace'; + +interface AllowedNamespacesProps { + clusterSettings: ClusterSettings | null; + setClusterSettings: Dispatch>; +} + +export default function AllowedNamespaces(props: AllowedNamespacesProps) { + const { clusterSettings, setClusterSettings } = props; + const { t } = useTranslation(['translation']); + const [newAllowedNamespace, setNewAllowedNamespace] = useState(''); + const theme = useTheme(); + + function storeNewAllowedNamespace(namespace: string) { + setNewAllowedNamespace(''); + setClusterSettings((settings: ClusterSettings | null) => { + const newSettings = { ...(settings || {}) }; + newSettings.allowedNamespaces = newSettings.allowedNamespaces || []; + newSettings.allowedNamespaces.push(namespace); + // Sort and avoid duplicates + newSettings.allowedNamespaces = [...new Set(newSettings.allowedNamespaces)].sort(); + return newSettings; + }); + } + const isValidNewAllowedNamespace = isValidNamespaceFormat(newAllowedNamespace); + const invalidNamespaceMessage = t( + "translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." + ); + return ( + <> + { + 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" + aria-label={t('translation|Add namespace')} + > + + + ), + onKeyPress: event => { + if (event.key === 'Enter') { + storeNewAllowedNamespace(newAllowedNamespace); + } + }, + 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; + }); + }} + /> + ))} + + + ); +} diff --git a/frontend/src/components/App/Settings/CustomClusterName.tsx b/frontend/src/components/App/Settings/CustomClusterName.tsx new file mode 100644 index 0000000000..bc35bc195d --- /dev/null +++ b/frontend/src/components/App/Settings/CustomClusterName.tsx @@ -0,0 +1,149 @@ +import { Box, TextField } from '@mui/material'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; +import helpers, { ClusterSettings } from '../../../helpers'; +import { parseKubeConfig, renameCluster } from '../../../lib/k8s/apiProxy'; +import { setConfig, setStatelessConfig } from '../../../redux/configSlice'; +import { findKubeconfigByClusterName, updateStatelessClusterKubeconfig } from '../../../stateless/'; +import { ConfirmButton } from '../../common'; + +function isValidClusterNameFormat(name: string) { + // We allow empty isValidClusterNameFormat just because that's the default value in our case. + if (!name) { + return true; + } + + // Validates that the namespace is a valid DNS-1123 label and returns a boolean. + // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names + const regex = new RegExp('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); + return regex.test(name); +} + +interface CustomClusterNameProps { + currentCluster?: string; + clusterSettings: ClusterSettings | null; + source: string; + setClusterSettings: Dispatch>; +} + +export default function CustomClusterName(props: CustomClusterNameProps) { + const { currentCluster = '', clusterSettings, source, setClusterSettings } = props; + const { t } = useTranslation(['translation']); + const [newClusterName, setNewClusterName] = useState(currentCluster || ''); + const isValidCurrentName = isValidClusterNameFormat(newClusterName); + const history = useHistory(); + const dispatch = useDispatch(); + + useEffect(() => { + if (clusterSettings?.currentName !== currentCluster) { + setNewClusterName(clusterSettings?.currentName || ''); + } + + // Avoid re-initializing settings as {} just because the cluster is not yet set. + if (clusterSettings !== null) { + helpers.storeClusterSettings(currentCluster || '', clusterSettings); + } + }, [currentCluster, clusterSettings]); + + const handleUpdateClusterName = (source: string) => { + try { + renameCluster(currentCluster || '', newClusterName, source) + .then(async config => { + if (currentCluster) { + const kubeconfig = await findKubeconfigByClusterName(currentCluster); + if (kubeconfig !== null) { + await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, currentCluster); + // Make another request for updated kubeconfig + const updatedKubeconfig = await findKubeconfigByClusterName(currentCluster); + 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(); + }) + .catch((err: Error) => { + console.error('Error updating cluster name:', err.message); + }); + } catch (error) { + console.error('Error updating cluster name:', error); + } + }; + + function storeNewClusterName(name: string) { + let actualName = name; + if (name === currentCluster) { + actualName = ''; + setNewClusterName(actualName); + } + + setClusterSettings((settings: ClusterSettings | null) => { + const newSettings = { ...(settings || {}) }; + if (isValidClusterNameFormat(name)) { + newSettings.currentName = actualName; + } + return newSettings; + }); + } + + const invalidClusterNameMessage = t( + "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." + ); + return ( + { + let value = event.target.value; + value = value.replace(' ', ''); + setNewClusterName(value); + }} + value={newClusterName} + placeholder={currentCluster} + 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: currentCluster } + )} + disabled={!newClusterName || !isValidCurrentName} + > + {t('translation|Apply')} + + + ), + onKeyPress: event => { + if (event.key === 'Enter' && isValidCurrentName) { + handleUpdateClusterName(source); + } + }, + autoComplete: 'off', + sx: { maxWidth: 250 }, + }} + /> + ); +} diff --git a/frontend/src/components/App/Settings/DefaultNamespace.tsx b/frontend/src/components/App/Settings/DefaultNamespace.tsx new file mode 100644 index 0000000000..2c4568de09 --- /dev/null +++ b/frontend/src/components/App/Settings/DefaultNamespace.tsx @@ -0,0 +1,131 @@ +import { Icon } from '@iconify/react'; +import { TextField, useTheme } from '@mui/material'; +import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import helpers, { ClusterSettings } from '../../../helpers'; +import { ConfigState } from '../../../redux/configSlice'; + +export function isValidNamespaceFormat(namespace: string) { + // We allow empty strings just because that's the default value in our case. + if (!namespace) { + return true; + } + + // Validates that the namespace is a valid DNS-1123 label and returns a boolean. + // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names + const regex = new RegExp('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); + return regex.test(namespace); +} + +interface DefaultNamespaceProps { + currentCluster?: string; + clusterSettings: ClusterSettings | null; + clusterConf: ConfigState['allClusters']; + setClusterSettings: Dispatch>; + clusterFromURLRef: MutableRefObject; +} + +export default function DefaultNamespace(props: DefaultNamespaceProps) { + const { + currentCluster = '', + clusterSettings, + clusterConf, + setClusterSettings, + clusterFromURLRef, + } = props; + const { t } = useTranslation(['translation']); + const [defaultNamespace, setDefaultNamespace] = useState('default'); + const [userDefaultNamespace, setUserDefaultNamespace] = useState(''); + const theme = useTheme(); + + useEffect(() => { + const clusterInfo = (clusterConf && clusterConf[currentCluster || '']) || null; + const clusterConfNs = clusterInfo?.meta_data?.namespace; + if (!!clusterConfNs && clusterConfNs !== defaultNamespace) { + setDefaultNamespace(clusterConfNs); + } + }, [currentCluster, clusterConf]); + + useEffect(() => { + if (clusterSettings?.defaultNamespace !== userDefaultNamespace) { + setUserDefaultNamespace(clusterSettings?.defaultNamespace || ''); + } + + // Avoid re-initializing settings as {} just because the cluster is not yet set. + if (clusterSettings !== null) { + helpers.storeClusterSettings(currentCluster || '', clusterSettings); + } + }, [currentCluster, clusterSettings]); + + useEffect(() => { + let timeoutHandle: NodeJS.Timeout | null = null; + + if (isEditingDefaultNamespace()) { + // We store the namespace after a timeout. + timeoutHandle = setTimeout(() => { + if (isValidNamespaceFormat(userDefaultNamespace)) { + storeNewDefaultNamespace(userDefaultNamespace); + } + }, 1000); + } + + return () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + clusterFromURLRef.current = ''; + } + }; + }, [userDefaultNamespace]); + + function isEditingDefaultNamespace() { + return clusterSettings?.defaultNamespace !== userDefaultNamespace; + } + + function storeNewDefaultNamespace(namespace: string) { + let actualNamespace = namespace; + if (namespace === defaultNamespace) { + actualNamespace = ''; + setUserDefaultNamespace(actualNamespace); + } + + setClusterSettings((settings: ClusterSettings | null) => { + const newSettings = { ...(settings || {}) }; + if (isValidNamespaceFormat(namespace)) { + newSettings.defaultNamespace = actualNamespace; + } + return newSettings; + }); + } + + const isValidDefaultNamespace = isValidNamespaceFormat(userDefaultNamespace); + const invalidNamespaceMessage = t( + "translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." + ); + return ( + { + 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 }, + }} + /> + ); +} diff --git a/frontend/src/components/App/Settings/SettingsCluster.tsx b/frontend/src/components/App/Settings/SettingsCluster.tsx index 67eaafa629..dcb0acca36 100644 --- a/frontend/src/components/App/Settings/SettingsCluster.tsx +++ b/frontend/src/components/App/Settings/SettingsCluster.tsx @@ -1,14 +1,4 @@ -import { Icon, InlineIcon } from '@iconify/react'; -import { - Box, - Chip, - FormControl, - IconButton, - MenuItem, - Select, - TextField, - Typography, -} from '@mui/material'; +import { Box, FormControl, MenuItem, Select, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,36 +6,14 @@ import { useDispatch } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; import helpers, { ClusterSettings } from '../../../helpers'; 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, Loader, NameValueTable, SectionBox } from '../../common'; +import { deleteCluster } from '../../../lib/k8s/apiProxy'; +import { setConfig } from '../../../redux/configSlice'; +import { Link, Loader, NameValueTable, NameValueTableRow, SectionBox } from '../../common'; import ConfirmButton from '../../common/ConfirmButton'; import Empty from '../../common/EmptyContent'; - -function isValidNamespaceFormat(namespace: string) { - // We allow empty strings just because that's the default value in our case. - if (!namespace) { - return true; - } - - // Validates that the namespace is a valid DNS-1123 label and returns a boolean. - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names - const regex = new RegExp('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); - return regex.test(namespace); -} - -function isValidClusterNameFormat(name: string) { - // We allow empty isValidClusterNameFormat just because that's the default value in our case. - if (!name) { - return true; - } - - // Validates that the namespace is a valid DNS-1123 label and returns a boolean. - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names - const regex = new RegExp('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); - return regex.test(name); -} +import AllowedNamespaces from './AllowedNamespaces'; +import CustomClusterName from './CustomClusterName'; +import DefaultNamespace from './DefaultNamespace'; interface ClusterSelectorProps { currentCluster?: string; @@ -83,13 +51,9 @@ export default function SettingsCluster() { 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 [cluster, setCluster] = React.useState(useCluster() || ''); const clusterFromURLRef = React.useRef(''); - const [newClusterName, setNewClusterName] = React.useState(cluster || ''); const theme = useTheme(); const history = useHistory(); @@ -99,41 +63,6 @@ export default function SettingsCluster() { const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null; const source = clusterInfo?.meta_data?.source || ''; - const handleUpdateClusterName = (source: string) => { - 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)); - } - } - history.push('/'); - window.location.reload(); - }) - .catch((err: Error) => { - console.error('Error updating cluster name:', err.message); - }); - } catch (error) { - console.error('Error updating cluster name:', error); - } - }; - const removeCluster = () => { deleteCluster(cluster || '') .then(config => { @@ -161,49 +90,6 @@ export default function SettingsCluster() { setClusterSettings(!!cluster ? helpers.loadClusterSettings(cluster || '') : null); }, [cluster]); - React.useEffect(() => { - const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null; - const clusterConfNs = clusterInfo?.meta_data?.namespace; - if (!!clusterConfNs && clusterConfNs !== defaultNamespace) { - setDefaultNamespace(clusterConfNs); - } - }, [cluster, clusterConf]); - - React.useEffect(() => { - if (clusterSettings?.defaultNamespace !== userDefaultNamespace) { - setUserDefaultNamespace(clusterSettings?.defaultNamespace || ''); - } - - if (clusterSettings?.currentName !== cluster) { - setNewClusterName(clusterSettings?.currentName || ''); - } - - // Avoid re-initializing settings as {} just because the cluster is not yet set. - if (clusterSettings !== null) { - helpers.storeClusterSettings(cluster || '', clusterSettings); - } - }, [cluster, clusterSettings]); - - React.useEffect(() => { - let timeoutHandle: NodeJS.Timeout | null = null; - - if (isEditingDefaultNamespace()) { - // We store the namespace after a timeout. - timeoutHandle = setTimeout(() => { - if (isValidNamespaceFormat(userDefaultNamespace)) { - storeNewDefaultNamespace(userDefaultNamespace); - } - }, 1000); - } - - return () => { - if (timeoutHandle) { - clearTimeout(timeoutHandle); - clusterFromURLRef.current = ''; - } - }; - }, [userDefaultNamespace]); - React.useEffect(() => { const clusterFromUrl = new URLSearchParams(location.search).get('c'); clusterFromURLRef.current = clusterFromUrl || ''; @@ -217,65 +103,6 @@ export default function SettingsCluster() { } }, [location.search, clusters]); - function isEditingDefaultNamespace() { - return clusterSettings?.defaultNamespace !== userDefaultNamespace; - } - - function storeNewAllowedNamespace(namespace: string) { - setNewAllowedNamespace(''); - setClusterSettings((settings: ClusterSettings | null) => { - const newSettings = { ...(settings || {}) }; - newSettings.allowedNamespaces = newSettings.allowedNamespaces || []; - newSettings.allowedNamespaces.push(namespace); - // Sort and avoid duplicates - newSettings.allowedNamespaces = [...new Set(newSettings.allowedNamespaces)].sort(); - return newSettings; - }); - } - - function storeNewDefaultNamespace(namespace: string) { - let actualNamespace = namespace; - if (namespace === defaultNamespace) { - actualNamespace = ''; - setUserDefaultNamespace(actualNamespace); - } - - setClusterSettings((settings: ClusterSettings | null) => { - const newSettings = { ...(settings || {}) }; - if (isValidNamespaceFormat(namespace)) { - newSettings.defaultNamespace = actualNamespace; - } - return newSettings; - }); - } - - function storeNewClusterName(name: string) { - let actualName = name; - if (name === cluster) { - actualName = ''; - setNewClusterName(actualName); - } - - setClusterSettings((settings: ClusterSettings | null) => { - const newSettings = { ...(settings || {}) }; - if (isValidClusterNameFormat(name)) { - newSettings.currentName = actualName; - } - return newSettings; - }); - } - - const isValidDefaultNamespace = isValidNamespaceFormat(userDefaultNamespace); - const isValidCurrentName = isValidClusterNameFormat(newClusterName); - const isValidNewAllowedNamespace = isValidNamespaceFormat(newAllowedNamespace); - const invalidNamespaceMessage = t( - "translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." - ); - - const invalidClusterNameMessage = t( - "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." - ); - // If we don't have yet a cluster name from the URL, we are still loading. if (!clusterFromURLRef.current) { return ; @@ -313,6 +140,46 @@ export default function SettingsCluster() { ); } + let prefixRows: NameValueTableRow[] = []; + if (helpers.isElectron()) { + prefixRows = [ + { + name: t('translation|Name'), + value: ( + + ), + }, + ]; + } + + const rows = [ + { + name: t('translation|Default namespace'), + value: ( + + ), + }, + { + name: t('translation|Allowed namespaces'), + value: ( + + ), + }, + ]; return ( <> @@ -332,180 +199,7 @@ export default function SettingsCluster() { {t('translation|Go to cluster')} - {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 }, - }} - /> - ), - }, - ]} - /> - )} - { - 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" - aria-label={t('translation|Add namespace')} - > - - - ), - onKeyPress: event => { - if (event.key === 'Enter') { - storeNewAllowedNamespace(newAllowedNamespace); - } - }, - 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; - }); - }} - /> - ))} - - - ), - }, - ]} - /> + {removableCluster && helpers.isElectron() && (