Skip to content

Commit

Permalink
Merge pull request #2615 from farodin91/refactor-settings-cluster
Browse files Browse the repository at this point in the history
frontend: refactor settings cluster
  • Loading branch information
joaquimrocha authored Dec 4, 2024
2 parents f5dc9fd + 8ce6cfe commit 39d8f72
Show file tree
Hide file tree
Showing 4 changed files with 437 additions and 354 deletions.
109 changes: 109 additions & 0 deletions frontend/src/components/App/Settings/AllowedNamespaces.tsx
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 frontend/src/components/App/Settings/CustomClusterName.tsx
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 frontend/src/components/App/Settings/DefaultNamespace.tsx
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 },
}}
/>
);
}
Loading

0 comments on commit 39d8f72

Please sign in to comment.