import React, { useState } from 'react'; import { Button } from "@patternfly/react-core/dist/esm/components/Button"; import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form"; import { Modal } from "@patternfly/react-core/dist/esm/components/Modal"; import { Radio } from "@patternfly/react-core/dist/esm/components/Radio"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput"; import * as dockerNames from 'docker-names'; import { FormHelper } from 'cockpit-components-form-helper.jsx'; import { DynamicListForm } from 'cockpit-components-dynamic-list.jsx'; import { ErrorNotification } from './Notification.jsx'; import { PublishPort, validatePublishPort } from './PublishPort.jsx'; import { Volume } from './Volume.jsx'; import * as client from './client.js'; import * as utils from './util.js'; import cockpit from 'cockpit'; import { useDialogs } from "dialogs.jsx"; const _ = cockpit.gettext; const systemOwner = "system"; export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvailable }) => { const { version, selinuxAvailable } = utils.usePodmanInfo(); const [podName, setPodName] = useState(dockerNames.getRandomName()); const [publish, setPublish] = useState([]); const [volumes, setVolumes] = useState([]); const [owner, setOwner] = useState(systemServiceAvailable ? systemOwner : user); const [dialogError, setDialogError] = useState(null); const [dialogErrorDetail, setDialogErrorDetail] = useState(null); const [validationFailed, setValidationFailed] = useState({}); const Dialogs = useDialogs(); const getCreateConfig = () => { const createConfig = {}; if (podName) createConfig.name = podName; if (publish.length > 0) createConfig.portmappings = publish .filter(port => port?.containerPort) .map(port => { const pm = { container_port: parseInt(port.containerPort), protocol: port.protocol }; if (port.hostPort !== null) pm.host_port = parseInt(port.hostPort); if (port.IP !== null) pm.host_ip = port.IP; return pm; }); if (volumes.length > 0) { createConfig.mounts = volumes .filter(volume => volume?.hostPath && volume?.containerPath) .map(volume => { const record = { source: volume.hostPath, destination: volume.containerPath, type: "bind" }; record.options = []; if (volume.mode) record.options.push(volume.mode); if (volume.selinux) record.options.push(volume.selinux); return record; }); } return createConfig; }; /* Updates a validation object of the whole dynamic list's form (e.g. the whole port-mapping form) * * Arguments * - key: [publish/volumes/env] - Specifies the validation of which dynamic form of the Image run dialog is being updated * - value: An array of validation errors of the form. Each item of the array represents a row of the dynamic list. * Index needs to corellate with a row number */ const dynamicListOnValidationChange = (key, value) => { setValidationFailed(prevState => { prevState[key] = value; if (prevState[key].every(a => a === undefined)) delete prevState[key]; return prevState; }); }; const createPod = (isSystem, createConfig) => { client.createPod(isSystem, createConfig) .then(() => Dialogs.close()) .catch(ex => { setDialogError(_("Pod failed to be created")); setDialogErrorDetail(cockpit.format("$0: $1", ex.reason, ex.message)); }); }; const onCreateClicked = () => { if (!validateForm()) return; const createConfig = getCreateConfig(); createPod(owner === systemOwner, createConfig); }; const isFormInvalid = validationFailed => { const groupHasError = row => row && Object.values(row) .filter(val => val) // Filter out empty/undefined properties .length > 0; // If one field has error, the whole group (dynamicList) is invalid // If at least one group is invalid, then the whole form is invalid return validationFailed.publish?.some(groupHasError) || !!validationFailed.podName; }; const validatePodName = value => { if (!utils.is_valid_container_name(value)) return _("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -)."); }; const validateForm = () => { const newValidationFailed = { }; const publishValidation = publish.map(a => { if (a === undefined) return undefined; return { IP: validatePublishPort(a.IP, "IP"), hostPort: validatePublishPort(a.hostPort, "hostPort"), containerPort: validatePublishPort(a.containerPort, "containerPort"), }; }); if (publishValidation.some(entry => entry && Object.keys(entry).length > 0)) newValidationFailed.publish = publishValidation.filter(entry => entry !== undefined); const podNameValidation = validatePodName(podName); if (podNameValidation) newValidationFailed.containerName = podNameValidation; setValidationFailed(newValidationFailed); return !isFormInvalid(newValidationFailed); }; const defaultBody = (
{dialogError && } { utils.validationClear(validationFailed, "podName", (value) => setValidationFailed(value)); utils.validationDebounce(() => { const delta = validatePodName(value); if (delta) setValidationFailed(prevState => { return { ...prevState, podName: delta } }); }); setPodName(value); }} /> { userServiceAvailable && systemServiceAvailable && setOwner(systemOwner)} /> setOwner(user)} /> } dynamicListOnValidationChange('publish', value)} onChange={value => setPublish(value)} default={{ IP: null, containerPort: null, hostPort: null, protocol: 'tcp' }} itemcomponent={ } /> {version.localeCompare("4", undefined, { numeric: true, sensitivity: 'base' }) >= 0 && setVolumes(value)} default={{ containerPath: null, hostPath: null, mode: 'rw' }} options={{ selinuxAvailable }} itemcomponent={ } /> } ); return ( } > {defaultBody} ); };