-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2615 from farodin91/refactor-settings-cluster
frontend: refactor settings cluster
- Loading branch information
Showing
4 changed files
with
437 additions
and
354 deletions.
There are no files selected for viewing
109 changes: 109 additions & 0 deletions
109
frontend/src/components/App/Settings/AllowedNamespaces.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SetStateAction<ClusterSettings | null>>; | ||
} | ||
|
||
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 ( | ||
<> | ||
<TextField | ||
onChange={event => { | ||
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: ( | ||
<IconButton | ||
onClick={() => { | ||
storeNewAllowedNamespace(newAllowedNamespace); | ||
}} | ||
disabled={!newAllowedNamespace} | ||
size="medium" | ||
aria-label={t('translation|Add namespace')} | ||
> | ||
<InlineIcon icon="mdi:plus-circle" /> | ||
</IconButton> | ||
), | ||
onKeyPress: event => { | ||
if (event.key === 'Enter') { | ||
storeNewAllowedNamespace(newAllowedNamespace); | ||
} | ||
}, | ||
autoComplete: 'off', | ||
sx: { maxWidth: 250 }, | ||
}} | ||
/> | ||
<Box | ||
sx={{ | ||
display: 'flex', | ||
flexWrap: 'wrap', | ||
'& > *': { | ||
margin: theme.spacing(0.5), | ||
}, | ||
marginTop: theme.spacing(1), | ||
}} | ||
aria-label={t('translation|Allowed namespaces')} | ||
> | ||
{((clusterSettings || {}).allowedNamespaces || []).map(namespace => ( | ||
<Chip | ||
key={namespace} | ||
label={namespace} | ||
size="small" | ||
clickable={false} | ||
onDelete={() => { | ||
setClusterSettings(settings => { | ||
const newSettings = { ...settings }; | ||
newSettings.allowedNamespaces = newSettings.allowedNamespaces?.filter( | ||
ns => ns !== namespace | ||
); | ||
return newSettings; | ||
}); | ||
}} | ||
/> | ||
))} | ||
</Box> | ||
</> | ||
); | ||
} |
149 changes: 149 additions & 0 deletions
149
frontend/src/components/App/Settings/CustomClusterName.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SetStateAction<ClusterSettings | null>>; | ||
} | ||
|
||
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 ( | ||
<TextField | ||
onChange={event => { | ||
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: ( | ||
<Box pt={2} textAlign="right"> | ||
<ConfirmButton | ||
onConfirm={() => { | ||
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')} | ||
</ConfirmButton> | ||
</Box> | ||
), | ||
onKeyPress: event => { | ||
if (event.key === 'Enter' && isValidCurrentName) { | ||
handleUpdateClusterName(source); | ||
} | ||
}, | ||
autoComplete: 'off', | ||
sx: { maxWidth: 250 }, | ||
}} | ||
/> | ||
); | ||
} |
131 changes: 131 additions & 0 deletions
131
frontend/src/components/App/Settings/DefaultNamespace.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SetStateAction<ClusterSettings | null>>; | ||
clusterFromURLRef: MutableRefObject<string>; | ||
} | ||
|
||
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 ( | ||
<TextField | ||
onChange={event => { | ||
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() ? ( | ||
<Icon width={24} color={theme.palette.text.secondary} icon="mdi:progress-check" /> | ||
) : ( | ||
<Icon width={24} icon="mdi:check-bold" /> | ||
), | ||
sx: { maxWidth: 250 }, | ||
}} | ||
/> | ||
); | ||
} |
Oops, something went wrong.