Skip to content

Commit

Permalink
frontend: Add UI to rename cluster
Browse files Browse the repository at this point in the history
This adds UI to rename cluster name in cluster settings component. This
does not update the values in kubeconfig/backend only for React state.

Signed-off-by: Kautilya Tripathi <ktripathi@microsoft.com>
  • Loading branch information
knrt10 committed May 14, 2024
1 parent 2523481 commit 930b20d
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 5 deletions.
12 changes: 8 additions & 4 deletions frontend/src/components/App/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,15 @@ interface HomeComponentProps {

function HomeComponent(props: HomeComponentProps) {
const { clusters } = props;
const customNameClusters = Object.values(clusters).map(c => ({
...c,
name: c.meta_data?.extensions?.headlamp_info?.customName || c.name,
}));
const { t } = useTranslation(['translation', 'glossary']);
const [versions, errors] = useClustersVersion(Object.values(clusters));
const [versions, errors] = useClustersVersion(Object.values(customNameClusters));
const filterFunc = useFilterFunc<Cluster>(['.name']);
const maxWarnings = 50;
const warningsMap = Event.useWarningList(Object.values(clusters).map(c => c.name));
const warningsMap = Event.useWarningList(Object.values(customNameClusters).map(c => c.name));

function renderWarningsText(clusterName: string) {
const numWarnings =
Expand All @@ -199,7 +203,7 @@ function HomeComponent(props: HomeComponentProps) {
return (
<PageGrid>
<SectionBox headerProps={{ headerStyle: 'main' }} title={t('Home')}>
<RecentClusters clusters={Object.values(clusters)} onButtonClick={() => {}} />
<RecentClusters clusters={Object.values(customNameClusters)} onButtonClick={() => {}} />
</SectionBox>
<SectionBox
title={
Expand Down Expand Up @@ -246,7 +250,7 @@ function HomeComponent(props: HomeComponentProps) {
),
},
]}
data={Object.values(clusters)}
data={Object.values(customNameClusters)}
id="headlamp-home-clusters"
/>
</SectionBox>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/App/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export default function Layout({}: LayoutProps) {
.then(config => {
const clustersToConfig: ConfigState['clusters'] = {};
config?.clusters.forEach((cluster: Cluster) => {
if (cluster.meta_data?.extensions?.headlamp_info?.customName) {
cluster.name = cluster.meta_data?.extensions?.headlamp_info?.customName;
}
clustersToConfig[cluster.name] = cluster;
});

Expand Down
114 changes: 113 additions & 1 deletion frontend/src/components/App/Settings/SettingsCluster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import helpers, { ClusterSettings } from '../../../helpers';
import { useCluster, useClustersConf } from '../../../lib/k8s';
import { deleteCluster } from '../../../lib/k8s/apiProxy';
import { deleteCluster, renameCluster } from '../../../lib/k8s/apiProxy';
import { setConfig } from '../../../redux/configSlice';
import { Link, NameValueTable, SectionBox } from '../../common';
import ConfirmButton from '../../common/ConfirmButton';
Expand All @@ -31,6 +31,18 @@ function isValidNamespaceFormat(namespace: string) {
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);
}

export default function SettingsCluster() {
const cluster = useCluster();
const clusterConf = useClustersConf();
Expand All @@ -39,12 +51,36 @@ export default function SettingsCluster() {
const [userDefaultNamespace, setUserDefaultNamespace] = React.useState('');
const [newAllowedNamespace, setNewAllowedNamespace] = React.useState('');
const [clusterSettings, setClusterSettings] = React.useState<ClusterSettings | null>(null);
const [newClusterName, setNewClusterName] = React.useState(cluster || '');

const classes = useStyles();
const theme = useTheme();

const history = useHistory();
const dispatch = useDispatch();

const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null;
const source = clusterInfo?.meta_data?.source || '';

const handleUpdateClusterName = (source: string) => {
try {
storeNewClusterName(newClusterName);
renameCluster(cluster || '', newClusterName, source)
.then(config => {
dispatch(setConfig(config));
history.push('/');
window.location.reload();
})
.catch((err: Error) => {
if (err.message === 'Not Found') {
// TODO: create notification with error message
}
});
} catch (error) {
console.error('Error updating cluster name:', error);
}
};

const removeCluster = () => {
deleteCluster(cluster || '')
.then(config => {
Expand Down Expand Up @@ -85,6 +121,10 @@ export default function SettingsCluster() {
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);
Expand Down Expand Up @@ -147,12 +187,33 @@ export default function SettingsCluster() {
});
}

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."
);

return (
<>
<SectionBox
Expand Down Expand Up @@ -289,6 +350,57 @@ export default function SettingsCluster() {
</>
),
},
{
name: t('translation|Current name'),
value: (
<TextField
onChange={event => {
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: (
<Box pt={2} textAlign="right">
<ConfirmButton
color="secondary"
onConfirm={() => {
if (isValidCurrentName) {
handleUpdateClusterName(source);
}
}}
confirmTitle={t('translation|Change name')}
confirmDescription={t(
'translation|This will add an extension field in your kubeconfig file for "{{ clusterName }}. Are you sure"?',
{ clusterName: cluster }
)}
disabled={!newClusterName || !isValidCurrentName}
>
{t('translation|Change name')}
</ConfirmButton>
</Box>
),
onKeyPress: event => {
if (event.key === 'Enter' && isValidCurrentName) {
handleUpdateClusterName(source);
}
},
autoComplete: 'off',
className: classes.input,
}}
/>
),
},
]}
/>
</SectionBox>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ function getProductName(): string | undefined {
export interface ClusterSettings {
defaultNamespace?: string;
allowedNamespaces?: string[];
currentName?: string;
}

function storeClusterSettings(clusterName: string, settings: ClusterSettings) {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@
"Number of rows for tables": "Zeilen pro Tabelle",
"Timezone to display for dates": "Zeitzone",
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Namespaces dürfen nur alphanumerische Kleinbuchstaben oder \"-\" enthalten und müssen mit einem alphanumerischen Zeichen beginnen und enden.",
"Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "",
"Cluster Settings ({{ clusterName }})": "Cluster-Einstellungen ({{ clusterName }})",
"Cluster Settings": "Cluster-Einstellungen",
"Default namespace": "Standard-Namespace",
"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.",
"Current name": "",
"The current name of cluster. You can define custom modified name.": "",
"Change name": "",
"This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "",
"Remove Cluster": "Cluster entfernen",
"Server": "Server",
"light theme": "helles Design",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@
"Number of rows for tables": "Number of rows for tables",
"Timezone to display for dates": "Timezone to display for dates",
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.",
"Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.",
"Cluster Settings ({{ clusterName }})": "Cluster Settings ({{ clusterName }})",
"Cluster Settings": "Cluster Settings",
"Default namespace": "Default namespace",
"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.",
"Current name": "Current name",
"The current name of cluster. You can define custom modified name.": "The current name of cluster. You can define custom modified name.",
"Change name": "Change name",
"This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?",
"Remove Cluster": "Remove Cluster",
"Server": "Server",
"light theme": "light theme",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@
"Number of rows for tables": "Núm. de líneas para las tablas",
"Timezone to display for dates": "Huso horario para mostrar en fechas",
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Los espacios de nombre deben contener solo caracteres alfanuméricos en minúsculas o '-', y deben comenzar y terminar con un carácter alfanumérico.",
"Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "",
"Cluster Settings ({{ clusterName }})": "Configuración del cluster ({{ clusterName }})",
"Cluster Settings": "Configuración del cluster",
"Default namespace": "Espacio de nombre por defecto",
"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.",
"Current name": "",
"The current name of cluster. You can define custom modified name.": "",
"Change name": "",
"This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "",
"Remove Cluster": "Eliminar cluster",
"Server": "Servidor",
"light theme": "tema claro",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/i18n/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@
"Number of rows for tables": "Nombre de lignes pour les tableaux",
"Timezone to display for dates": "Fuseau horaire à afficher pour les dates",
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Les espaces de noms ne doivent contenir que des caractères alphanumériques minuscules ou '-', et doivent commencer et se terminer par un caractère alphanumérique.",
"Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "",
"Cluster Settings ({{ clusterName }})": "Paramètres du cluster ({{ clusterName }})",
"Cluster Settings": "Paramètres du cluster",
"Default namespace": "Espace de noms par défaut",
"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.",
"Current name": "",
"The current name of cluster. You can define custom modified name.": "",
"Change name": "",
"This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "",
"Remove Cluster": "Supprimer le cluster",
"Server": "Serveur",
"light theme": "thème clair",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/i18n/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@
"Number of rows for tables": "Núm. de linhas para as tabelas",
"Timezone to display for dates": "Fuso horário para mostrar em datas",
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Os namespaces devem conter apenas caracteres alfanuméricos minúsculos ou '-', e devem começar e terminar com um carácter alfanumérico.",
"Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "",
"Cluster Settings ({{ clusterName }})": "Definições do cluster ({{ clusterName }})",
"Cluster Settings": "Definições do cluster",
"Default namespace": "Namespace padrão",
"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.",
"Current name": "",
"The current name of cluster. You can define custom modified name.": "",
"Change name": "",
"This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "",
"Remove Cluster": "Remover Cluster",
"Server": "Servidor",
"light theme": "tema claro",
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/lib/k8s/apiProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,32 @@ export async function deleteCluster(cluster: string) {
);
}

/**
* renameCluster sends call to backend to update a field in kubeconfig which
* is the custom name of the cluster used by the user.
* @param cluster
*/
export async function renameCluster(cluster: string, newClusterName: string, source: string) {
if (cluster) {
const kubeconfig = await findKubeconfigByClusterName(cluster);
if (kubeconfig !== null) {
// @TODO: Update kubeconfig in indexDB
return window.location.reload();
}
}

return request(
`/cluster/${cluster}`,
{
method: 'PUT',
headers: { ...getHeadlampAPIHeaders() },
body: JSON.stringify({ newClusterName, source }),
},
false,
false
);
}

// @todo: Move startPortForward, stopPortForward, and getPortForwardStatus to a portForward.ts

// @todo: the return type is missing for the following functions.
Expand Down

0 comments on commit 930b20d

Please sign in to comment.