From 2df94782f3742901477077a86a384d9940480cfe Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Fri, 10 Nov 2023 12:17:42 +0100 Subject: [PATCH] Add create pod validation for name/port mapping Recently the Image Create modal implemented validation for Volume/Port mapping. Re-use the same changes in Podman except for Volumes as there can be some improvements done there. --- src/PodCreateModal.jsx | 93 +++++++++++++++++++++++++++++++++--------- test/check-application | 12 ++++++ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/PodCreateModal.jsx b/src/PodCreateModal.jsx index 83aead5d4..aaf30ce0e 100644 --- a/src/PodCreateModal.jsx +++ b/src/PodCreateModal.jsx @@ -9,7 +9,7 @@ import * as dockerNames from 'docker-names'; import { FormHelper } from 'cockpit-components-form-helper.jsx'; import { DynamicListForm } from 'DynamicListForm.jsx'; import { ErrorNotification } from './Notification.jsx'; -import { PublishPort } from './PublishPort.jsx'; +import { PublishPort, validatePublishPort } from './PublishPort.jsx'; import { Volume } from './Volume.jsx'; import * as client from './client.js'; import * as utils from './util.js'; @@ -23,12 +23,12 @@ const systemOwner = "system"; export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvailable }) => { const { version, selinuxAvailable } = utils.usePodmanInfo(); const [podName, setPodName] = useState(dockerNames.getRandomName()); - const [nameError, setNameError] = useState(null); 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 = () => { @@ -66,6 +66,22 @@ export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvaila 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 = (value, key) => { + 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()) @@ -76,33 +92,68 @@ export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvaila }; const onCreateClicked = () => { + if (!validateForm()) + return; const createConfig = getCreateConfig(); createPod(owner === systemOwner, createConfig); }; - const onValueChanged = (key, value) => { - if (key === "podName") { - setPodName(value); - } - if (utils.is_valid_container_name(value)) { - setNameError(null); - } else { - setNameError(_("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -).")); - } + const isFormInvalid = validationFailed => { + const groupHasError = 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 => { + return { + IP: validatePublishPort(a.IP, "IP"), + hostPort: validatePublishPort(a.hostPort, "hostPort"), + containerPort: validatePublishPort(a.containerPort, "containerPort"), + }; + }); + if (publishValidation.some(entry => Object.keys(entry).length > 0)) + newValidationFailed.publish = publishValidation; + + const podNameValidation = validatePodName(podName); + + if (podNameValidation) + newValidationFailed.containerName = podNameValidation; + + setValidationFailed(newValidationFailed); + return !isFormInvalid(newValidationFailed); }; const defaultBody = (
{dialogError && } - + onValueChanged('podName', value)} /> - + className="pod-name" + placeholder={_("Pod name")} + value={podName} + validated={validationFailed.podName ? "error" : "default"} + onChange={(_, value) => { + 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 && @@ -123,6 +174,8 @@ export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvaila formclass='publish-port-form' label={_("Port mapping")} actionLabel={_("Add port mapping")} + validationFailed={validationFailed.publish} + onValidationChange={value => dynamicListOnValidationChange(value, "publish")} onChange={value => setPublish(value)} default={{ IP: null, containerPort: null, hostPort: null, protocol: 'tcp' }} itemcomponent={ } /> @@ -150,7 +203,7 @@ export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvaila title={_("Create pod")} footer={<>