Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 73 additions & 20 deletions src/PodCreateModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

delete prevState[key];
return prevState;
});
};

const createPod = (isSystem, createConfig) => {
client.createPod(isSystem, createConfig)
.then(() => Dialogs.close())
Expand All @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details


setValidationFailed(newValidationFailed);
return !isFormInvalid(newValidationFailed);
};

const defaultBody = (
<Form>
{dialogError && <ErrorNotification errorMessage={dialogError} errorDetail={dialogErrorDetail} />}
<FormGroup fieldId='create-pod-dialog-name' label={_("Name")} className="ct-m-horizontal">
<FormGroup id="pod-name-group" fieldId='create-pod-dialog-name' label={_("Name")} className="ct-m-horizontal">
<TextInput id='create-pod-dialog-name'
className="pod-name"
placeholder={_("Pod name")}
value={podName}
validated={nameError ? "error" : "default"}
aria-label={nameError}
onChange={(_event, value) => onValueChanged('podName', value)} />
<FormHelper fieldId="create-pod-dialog-name" helperTextInvalid={nameError} />
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);
}} />
<FormHelper fieldId="create-pod-dialog-name" helperTextInvalid={validationFailed?.podName} />
</FormGroup>
{ userServiceAvailable && systemServiceAvailable &&
<FormGroup isInline hasNoPaddingTop fieldId='create-pod-dialog-owner' label={_("Owner")} className="ct-m-horizontal">
Expand All @@ -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={ <PublishPort />} />
Expand Down Expand Up @@ -150,7 +203,7 @@ export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvaila
title={_("Create pod")}
footer={<>
<Button variant='primary' id="create-pod-create-btn" onClick={() => onCreateClicked()}
isDisabled={nameError}>
isDisabled={isFormInvalid(validationFailed)}>
{_("Create")}
</Button>
<Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
Expand Down
12 changes: 12 additions & 0 deletions test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -2698,7 +2698,19 @@ class TestApplication(testlib.MachineCase):
self.login(auth)

b.click("#containers-containers-create-pod-btn")
b.set_input_text("#create-pod-dialog-name", "")
b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:disabled")
b.wait_in_text("#pod-name-group .pf-v5-c-helper-text__item-text", "Invalid characters")

b.set_input_text("#create-pod-dialog-name", pod_name)
b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:not(:disabled)")

b.click('.publish-port-form .btn-add')
b.set_input_text("#create-pod-dialog-publish-0-container-port-group input", "-1")
b.click(".pf-v5-c-modal-box__footer #create-pod-create-btn")
b.wait_in_text("#create-pod-dialog-publish-0-container-port-group .pf-v5-c-helper-text__item-text",
"1 to 65535")
b.click("#create-pod-dialog-publish-0-btn-close")

if auth:
b.wait_visible("#create-pod-dialog-owner-system:checked")
Expand Down