@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
155 lines (154 loc) • 7.75 kB
JavaScript
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') })) }));
}