UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

248 lines (247 loc) 12.4 kB
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;