@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
248 lines (247 loc) • 12.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import Grid from '@mui/material/Grid';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import { styled } from '@mui/system';
import * as yaml from 'js-yaml';
import React, { useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useClustersConf } from '../../lib/k8s';
import { setCluster } from '../../lib/k8s/apiProxy';
import { setStatelessConfig } from '../../redux/configSlice';
import { DialogTitle } from '../common/Dialog';
import Loader from '../common/Loader';
import { ClusterDialog } from './Chooser';
function configWithSelectedClusters(config, selectedClusters) {
const newConfig = {
clusters: [],
users: [],
contexts: [],
currentContext: '',
};
// We use a map to avoid duplicates since many contexts can point to the same cluster/user.
const clusters = {};
const users = {};
selectedClusters.forEach(clusterName => {
const context = config.contexts.find(c => c.name === clusterName);
if (!context) {
return;
}
const cluster = config.clusters.find(c => c.name === context.context.cluster);
if (!cluster) {
return;
}
clusters[cluster.name] = cluster;
// Optionally add the user.
const user = config.users?.find(c => c.name === context.context.user);
if (!!user) {
users[user.name] = user;
}
newConfig.contexts.push(context);
});
newConfig.clusters = Object.values(clusters);
newConfig.users = Object.values(users);
return newConfig;
}
const DropZoneBox = styled(Box)({
border: 1,
borderRadius: 1,
borderWidth: 2,
borderColor: 'rgba(0, 0, 0)',
borderStyle: 'dashed',
padding: '20px',
margin: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
'&:hover': {
borderColor: 'rgba(0, 0, 0, 0.5)',
},
'&:focus-within': {
borderColor: 'rgba(0, 0, 0, 0.5)',
},
});
const WideButton = styled(Button)({
width: '100%',
maxWidth: '300px',
});
var Step;
(function (Step) {
Step[Step["LoadKubeConfig"] = 0] = "LoadKubeConfig";
Step[Step["SelectClusters"] = 1] = "SelectClusters";
Step[Step["ValidateKubeConfig"] = 2] = "ValidateKubeConfig";
Step[Step["ConfigureClusters"] = 3] = "ConfigureClusters";
Step[Step["Success"] = 4] = "Success";
})(Step || (Step = {}));
function KubeConfigLoader() {
const history = useHistory();
const [state, setState] = useState(Step.LoadKubeConfig);
const [error, setError] = React.useState('');
const [fileContent, setFileContent] = useState({
clusters: [],
users: [],
contexts: [],
currentContext: '',
});
const [selectedClusters, setSelectedClusters] = useState([]);
const configuredClusters = useClustersConf(); // Get already configured clusters
useEffect(() => {
if (fileContent.contexts.length > 0) {
setSelectedClusters(fileContent.contexts.map(context => context.name));
setState(Step.SelectClusters);
}
return () => { };
}, [fileContent]);
useEffect(() => {
if (state === Step.ValidateKubeConfig) {
const alreadyConfiguredClusters = selectedClusters.filter(clusterName => configuredClusters && configuredClusters[clusterName]);
if (alreadyConfiguredClusters.length > 0) {
setError(t('translation|Duplicate cluster: {{ clusterNames }} in the list. Please edit the context name.', {
clusterNames: alreadyConfiguredClusters.join(', '),
}));
setState(Step.SelectClusters);
}
else {
setState(Step.ConfigureClusters);
}
}
if (state === Step.ConfigureClusters) {
function loadClusters() {
const selectedClusterConfig = configWithSelectedClusters(fileContent, selectedClusters);
setCluster({ kubeconfig: btoa(yaml.dump(selectedClusterConfig)) })
.then(res => {
if (res?.clusters?.length > 0) {
dispatch(setStatelessConfig(res));
}
setState(Step.Success);
})
.catch(e => {
console.debug('Error setting up clusters from kubeconfig:', e);
setError(t('translation|Error setting up clusters, please load a valid kubeconfig file'));
setState(Step.SelectClusters);
});
}
loadClusters();
}
return () => { };
}, [state]);
const dispatch = useDispatch();
const { t } = useTranslation(['translation']);
const onDrop = (acceptedFiles) => {
setError('');
const reader = new FileReader();
reader.onerror = () => setError(t("translation|Couldn't read kubeconfig file"));
reader.onload = () => {
try {
const data = String.fromCharCode.apply(null, [
...new Uint8Array(reader.result),
]);
const doc = yaml.load(data);
if (!doc.clusters) {
throw new Error(t('translation|No clusters found!'));
}
if (!doc.contexts) {
throw new Error(t('translation|No contexts found!'));
}
setFileContent(doc);
}
catch (err) {
setError(t(`translation|Invalid kubeconfig file: {{ errorMessage }}`, {
errorMessage: err.message,
}));
return;
}
};
reader.readAsArrayBuffer(acceptedFiles[0]);
};
const { getRootProps, getInputProps, open } = useDropzone({
onDrop: onDrop,
multiple: false,
});
function handleCheckboxChange(event) {
if (!event.target.checked) {
// remove from selected clusters
setSelectedClusters(selectedClusters => selectedClusters.filter(cluster => cluster !== event.target.name));
}
else {
// add to selected clusters
setSelectedClusters(selectedClusters => [...selectedClusters, event.target.name]);
}
}
function renderSwitch() {
switch (state) {
case Step.LoadKubeConfig:
return (_jsxs(Box, { children: [_jsx(DropZoneBox, { border: 1, borderColor: "secondary.main", ...getRootProps(), children: _jsxs(FormControl, { children: [_jsx("input", { ...getInputProps() }), _jsx(Tooltip, { title: t('translation|Drag & drop or choose kubeconfig file here'), placement: "top", children: _jsx(Button, { variant: "contained", onClick: () => open, startIcon: _jsx(InlineIcon, { icon: "mdi:upload", width: 32 }), children: t('translation|Choose file') }) })] }) }), _jsx(Box, { style: { display: 'flex', justifyContent: 'center' }, children: _jsx(WideButton, { onClick: () => history.goBack(), children: t('translation|Back') }) })] }));
case Step.SelectClusters:
return (_jsxs(Box, { style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
textAlign: 'center',
alignItems: 'center',
}, children: [_jsx(Typography, { children: t('translation|Select clusters') }), fileContent.clusters ? (_jsx(_Fragment, { children: _jsxs(Box, { sx: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
justifyContent: 'center',
padding: '15px',
width: '100%',
maxWidth: '300px',
}, children: [_jsx(FormControl, { sx: {
overflowY: 'auto',
height: '150px',
paddingLeft: '10px',
paddingRight: '10px',
width: '100%',
}, children: fileContent.contexts.map(context => {
return (_jsx(FormControlLabel, { control: _jsx(Checkbox, { value: context.name, name: context.name, onChange: handleCheckboxChange, color: "primary", checked: selectedClusters.includes(context.name) }), label: context.name }, context.name));
}) }), _jsxs(Grid, { container: true, direction: "column", spacing: 2, justifyContent: "center", alignItems: "stretch", children: [_jsx(Grid, { item: true, children: _jsx(WideButton, { variant: "contained", color: "primary", onClick: () => {
setState(Step.ValidateKubeConfig);
}, disabled: selectedClusters.length === 0, children: t('translation|Next') }) }), _jsx(Grid, { item: true, children: _jsx(WideButton, { onClick: () => {
setError('');
setState(Step.LoadKubeConfig);
}, children: t('translation|Back') }) })] })] }) })) : null] }));
case Step.ValidateKubeConfig:
return (_jsxs(Box, { style: { textAlign: 'center' }, children: [_jsx(Typography, { children: t('translation|Validating selected clusters') }), _jsx(Loader, { title: t('translation|Validating selected clusters') })] }));
case Step.ConfigureClusters:
return (_jsxs(Box, { style: { textAlign: 'center' }, children: [_jsx(Typography, { children: t('translation|Setting up clusters') }), _jsx(Loader, { title: t('translation|Setting up clusters') })] }));
case Step.Success:
return (_jsxs(Box, { sx: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
textAlign: 'center',
alignItems: 'center',
}, children: [_jsx(Box, { style: { padding: '32px' }, children: _jsx(Typography, { children: t('translation|Clusters successfully set up!') }) }), _jsx(WideButton, { variant: "contained", onClick: () => history.replace('/'), children: t('translation|Finish') })] }));
}
}
return (_jsxs(ClusterDialog, { showInfoButton: false,
// Disable backdrop clicking.
onClose: () => { }, useCover: true, children: [_jsx(DialogTitle, { children: t('translation|Load from KubeConfig') }), error && error !== '' ? (_jsx(Box, { style: { backgroundColor: 'red', textAlign: 'center', padding: '4px' }, children: error })) : null, _jsx(Box, { children: renderSwitch() })] }));
}
export default KubeConfigLoader;