UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

155 lines (154 loc) 7.75 kB
import { jsx as _jsx, jsxs as _jsxs } 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 { Icon, InlineIcon } from '@iconify/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; import { alpha } from '@mui/system'; import * as yaml from 'js-yaml'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelectedClusters } from '../../../lib/k8s'; import Pod from '../../../lib/k8s/pod'; import { Activity } from '../../activity/Activity'; import CreatePodForm from '../../pod/CreatePodForm'; import ActionButton from '../ActionButton'; import EditorDialog from './EditorDialog'; export const RESOURCE_DEFINITIONS = { Pod: { class: Pod, form: CreatePodForm }, }; /** Parse a YAML string into a plain object. Returns `null` for non-empty * invalid input so the form state is not overwritten mid-edit. Blank input * yields `{}`. Used by {@link CreateActivityContent} to sync the form when * the user edits YAML directly in the editor panel. */ export function parseEditorObject(input) { if (!input.trim()) return {}; try { const parsed = yaml.load(input); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return parsed; } } catch { // Ignore parse errors while editing incomplete YAML. } return null; } /** Inner component rendered inside the Activity panel. Holds shared state so the * form can update the editor YAML in-place without relaunching the activity. */ function CreateActivityContent(props) { const { onClose } = props; const { t } = useTranslation(['translation', 'glossary']); const clusters = useSelectedClusters(); const [item, setItem] = React.useState({}); const [formResource, setFormResource] = React.useState({}); const [errorMessage, setErrorMessage] = React.useState(''); const [selectedResource, setSelectedResource] = React.useState(); const [targetCluster, setTargetCluster] = React.useState(clusters[0] || ''); function handleResourceChange(resource) { setSelectedResource(resource); if (resource && resource in RESOURCE_DEFINITIONS) { const baseObject = RESOURCE_DEFINITIONS[resource].class.getBaseObject(); setItem(baseObject); setFormResource(baseObject); } } React.useEffect(() => { if (clusters.length === 0) { setTargetCluster(''); } else if (!clusters.includes(targetCluster)) { setTargetCluster(clusters[0]); } }, [clusters, targetCluster]); /** Called by the resource form (e.g. CreatePodForm) when the user edits a * field. Updates both the form state and the editor YAML via `item`. */ const handleFormChange = (newItem) => { setFormResource(newItem); setItem(newItem); }; /** Called by EditorDialog when the user edits YAML directly. Parses the * text and syncs it back into the form; skips the update on invalid YAML * so incomplete edits don't wipe the form state. */ const handleEditorChanged = (newValue) => { setErrorMessage(''); const parsed = parseEditorObject(newValue); if (parsed !== null) { setFormResource(parsed); } }; /** Renders the resource-type picker and, once a type is selected, the * corresponding form component. Passed to EditorDialog as `formContent`. */ function renderFormContent() { return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, pt: 2, px: 2, height: '100%', overflowY: 'auto', }, children: [_jsxs(FormControl, { sx: { alignSelf: 'start', width: '80%', maxWidth: 480, marginLeft: 5, }, children: [_jsx(InputLabel, { id: "resource-picker-label", children: t('translation|Resource Type') }), _jsx(Select, { labelId: "resource-picker-label", value: selectedResource ?? '', label: t('translation|Resource Type'), onChange: e => handleResourceChange(e.target.value), children: Object.keys(RESOURCE_DEFINITIONS).map(rt => (_jsx(MenuItem, { value: rt, children: rt }, rt))) })] }), selectedResource && (() => { const FormComponent = RESOURCE_DEFINITIONS[selectedResource].form; return _jsx(FormComponent, { resource: formResource, onChange: handleFormChange }); })()] })); } return (_jsx(EditorDialog, { item: item, open: true, noDialog: true, onClose: onClose, setOpen: () => { }, saveLabel: t('translation|Apply'), errorMessage: errorMessage, onEditorChanged: handleEditorChanged, treatItemChangesAsEdits: true, title: t('translation|Create / Apply'), cluster: targetCluster, formContent: renderFormContent(), actions: clusters.length > 1 ? [ _jsxs(FormControl, { children: [_jsx(InputLabel, { id: "edit-dialog-cluster-target", children: t('glossary|Cluster') }), _jsx(Select, { labelId: "edit-dialog-cluster-target", id: "edit-dialog-cluster-target-select", value: targetCluster, onChange: event => { setTargetCluster(event.target.value); }, children: clusters.map(cluster => (_jsx(MenuItem, { value: cluster, children: cluster }, cluster))) })] }, "cluster-select"), ] : [] })); } export default function CreateButton(props) { const { isNarrow } = props; const { t } = useTranslation(['translation']); const openActivity = () => { const id = 'create-button'; Activity.launch({ id: id, title: t('translation|Create / Apply'), icon: _jsx(Icon, { icon: "mdi:pencil" }), content: _jsx(CreateActivityContent, { onClose: () => Activity.close(id) }), location: 'full', }); }; return (_jsx(React.Fragment, { children: isNarrow ? (_jsx(ActionButton, { description: t('translation|Create / Apply'), onClick: openActivity, icon: "mdi:plus-box", width: "48", iconButtonProps: { color: 'primary', sx: theme => ({ color: theme.palette.sidebar.color, }), } })) : (_jsx(Button, { onClick: openActivity, startIcon: _jsx(InlineIcon, { icon: "mdi:plus" }), color: "secondary", size: "large", sx: theme => ({ background: theme.palette.sidebar.actionBackground, color: theme.palette.getContrastText(theme.palette.sidebar.actionBackground), ':hover': { background: alpha(theme.palette.sidebar.actionBackground, 0.6), }, }), children: t('translation|Create') })) })); }