LCOV - code coverage report
Current view: top level - src - PodCreateModal.jsx Coverage Total Hit
Test: cockpit-podman Lines: 95.2 % 168 160
Test Date: 2025-05-21 17:35:03

            Line data    Source code
       1           43 : import React, { useState } from 'react';
       2              : 
       3              : import { Button } from "@patternfly/react-core/dist/esm/components/Button";
       4              : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
       5              : import { Radio } from "@patternfly/react-core/dist/esm/components/Radio";
       6              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
       7              : import {
       8              :     Modal
       9              : } from '@patternfly/react-core/dist/esm/deprecated/components/Modal';
      10              : import { FormHelper } from 'cockpit-components-form-helper.jsx';
      11              : import { useDialogs } from "dialogs.jsx";
      12           43 : import * as dockerNames from 'docker-names';
      13              : 
      14              : import cockpit from 'cockpit';
      15              : import { DynamicListForm } from 'cockpit-components-dynamic-list.jsx';
      16              : 
      17              : import { ErrorNotification } from './Notification.jsx';
      18              : import { PublishPort, validatePublishPort } from './PublishPort.jsx';
      19              : import { Volume } from './Volume.jsx';
      20              : import * as client from './client.js';
      21              : import * as utils from './util.js';
      22              : 
      23           43 : const _ = cockpit.gettext;
      24              : 
      25            2 : export const PodCreateModal = ({ users }) => {
      26            2 :     const { version, selinuxAvailable } = utils.usePodmanInfo();
      27            2 :     const [podName, setPodName] = useState(dockerNames.getRandomName());
      28            2 :     const [publish, setPublish] = useState([]);
      29            2 :     const [volumes, setVolumes] = useState([]);
      30            2 :     const [owner, setOwner] = useState(users[0]);
      31            2 :     const [inProgress, setInProgress] = useState(false);
      32            2 :     const [dialogError, setDialogError] = useState(null);
      33            2 :     const [dialogErrorDetail, setDialogErrorDetail] = useState(null);
      34            2 :     const [validationFailed, setValidationFailed] = useState({});
      35            2 :     const Dialogs = useDialogs();
      36              : 
      37            2 :     const getCreateConfig = () => {
      38            2 :         const createConfig = {};
      39              : 
      40            2 :         if (podName)
      41            2 :             createConfig.name = podName;
      42              : 
      43            2 :         if (publish.length > 0)
      44            2 :             createConfig.portmappings = publish
      45            2 :                     .filter(port => port?.containerPort)
      46            2 :                     .map(port => {
      47            2 :                         const pm = { container_port: parseInt(port.containerPort), protocol: port.protocol };
      48            2 :                         if (port.hostPort !== null)
      49            2 :                             pm.host_port = parseInt(port.hostPort);
      50            2 :                         if (port.IP !== null)
      51            2 :                             pm.host_ip = port.IP;
      52            2 :                         return pm;
      53            2 :                     });
      54              : 
      55            2 :         if (volumes.length > 0) {
      56            2 :             createConfig.mounts = volumes
      57            2 :                     .filter(volume => volume?.hostPath && volume?.containerPath)
      58            2 :                     .map(volume => {
      59            2 :                         const record = { source: volume.hostPath, destination: volume.containerPath, type: "bind" };
      60            2 :                         record.options = [];
      61            2 :                         if (volume.mode)
      62            2 :                             record.options.push(volume.mode);
      63            2 :                         if (volume.selinux)
      64            2 :                             record.options.push(volume.selinux);
      65            2 :                         return record;
      66            2 :                     });
      67            2 :         }
      68              : 
      69            2 :         return createConfig;
      70            2 :     };
      71              : 
      72              :     /* Updates a validation object of the whole dynamic list's form (e.g. the whole port-mapping form)
      73              :     *
      74              :     * Arguments
      75              :     *   - key: [publish/volumes/env] - Specifies the validation of which dynamic form of the Image run dialog is being updated
      76              :     *   - value: An array of validation errors of the form. Each item of the array represents a row of the dynamic list.
      77              :     *            Index needs to correlate with a row number
      78              :     */
      79            2 :     const dynamicListOnValidationChange = (key, value) => {
      80            2 :         setValidationFailed(prevState => {
      81            2 :             const newState = Object.assign({}, prevState, { [key]: value });
      82            0 :             if (newState[key].every(a => a === undefined))
      83            2 :                 delete newState[key];
      84            2 :             return newState;
      85            2 :         });
      86            2 :     };
      87              : 
      88            2 :     const onCreateClicked = () => {
      89            2 :         if (!validateForm())
      90            2 :             return;
      91            2 :         setInProgress(true);
      92            2 :         client.createPod(owner.con, getCreateConfig())
      93            2 :                 .then(Dialogs.close)
      94            0 :                 .catch(ex => {
      95            0 :                     setInProgress(false);
      96            0 :                     setDialogError(_("Pod failed to be created"));
      97            0 :                     setDialogErrorDetail(cockpit.format("$0: $1", ex.reason, ex.message));
      98            0 :                 });
      99            2 :     };
     100              : 
     101            2 :     const isFormInvalid = validationFailed => {
     102            2 :         function publishGroupHasError(row, idx) {
     103              :             // We always ignore errors for empty slots in
     104              :             // publish. Errors for these slots might show up when the
     105              :             // debounced validation runs after a row has been removed.
     106            2 :             if (!row || !publish[idx])
     107            2 :                 return false;
     108              : 
     109            2 :             return Object.values(row)
     110            2 :                     .filter(val => val) // Filter out empty/undefined properties
     111            2 :                     .length > 0; // If one field has error, the whole group (dynamicList) is invalid
     112            2 :         }
     113              : 
     114              :         // If at least one group is invalid, then the whole form is invalid
     115            2 :         return validationFailed.publish?.some(publishGroupHasError) ||
     116            2 :             !!validationFailed.podName;
     117            2 :     };
     118              : 
     119            2 :     const validatePodName = value => {
     120            2 :         if (!utils.is_valid_container_name(value))
     121            2 :             return _("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -).");
     122            2 :     };
     123              : 
     124            2 :     const validateForm = () => {
     125            2 :         const newValidationFailed = { };
     126              : 
     127            2 :         const publishValidation = publish.map(a => {
     128            2 :             if (a === undefined)
     129            2 :                 return undefined;
     130              : 
     131            2 :             return {
     132            2 :                 IP: validatePublishPort(a.IP, "IP"),
     133            2 :                 hostPort: validatePublishPort(a.hostPort, "hostPort"),
     134            2 :                 containerPort: validatePublishPort(a.containerPort, "containerPort"),
     135            2 :             };
     136            2 :         });
     137            2 :         if (publishValidation.some(entry => entry && Object.keys(entry).length > 0))
     138            2 :             newValidationFailed.publish = publishValidation;
     139              : 
     140            2 :         const podNameValidation = validatePodName(podName);
     141              : 
     142            2 :         if (podNameValidation)
     143            0 :             newValidationFailed.containerName = podNameValidation;
     144              : 
     145            2 :         setValidationFailed(newValidationFailed);
     146            2 :         return !isFormInvalid(newValidationFailed);
     147            2 :     };
     148              : 
     149            2 :     const defaultBody = (
     150            2 :         <Form>
     151            0 :             {dialogError && <ErrorNotification errorMessage={dialogError} errorDetail={dialogErrorDetail} />}
     152            2 :             <FormGroup id="pod-name-group" fieldId='create-pod-dialog-name' label={_("Name")} className="ct-m-horizontal">
     153            2 :                 <TextInput id='create-pod-dialog-name'
     154            2 :                            className="pod-name"
     155            2 :                            placeholder={_("Pod name")}
     156            2 :                            value={podName}
     157            2 :                            validated={validationFailed.podName ? "error" : "default"}
     158            2 :                            onChange={(_, value) => {
     159            2 :                                utils.validationClear(validationFailed, "podName", (value) => setValidationFailed(value));
     160            2 :                                utils.validationDebounce(() => {
     161            2 :                                    const delta = validatePodName(value);
     162            2 :                                    if (delta)
     163            2 :                                        setValidationFailed(prevState => { return { ...prevState, podName: delta } });
     164            2 :                                });
     165            2 :                                setPodName(value);
     166            2 :                            }} />
     167            2 :                 <FormHelper fieldId="create-pod-dialog-name" helperTextInvalid={validationFailed?.podName} />
     168            2 :             </FormGroup>
     169            2 :             { users.length > 1 &&
     170            1 :                 <FormGroup isInline hasNoPaddingTop fieldId='create-pod-dialog-owner' label={_("Owner")} className="ct-m-horizontal">
     171            1 :                     { users.map(user => (
     172            1 :                         <Radio key={user.name}
     173            1 :                             value={user.name}
     174            1 :                             label={user.uid === 0 ? _("System") : cockpit.format("$0 $1", _("User:"), user.name)}
     175            1 :                             id={"create-pod-dialog-owner-" + user.name }
     176            1 :                             isChecked={owner === user}
     177            1 :                             onChange={() => setOwner(user)} />))
     178              :                     }
     179            1 :                 </FormGroup>
     180              :             }
     181            2 :             <DynamicListForm id='create-pod-dialog-publish'
     182            2 :                         emptyStateString={_("No ports exposed")}
     183            2 :                         formclass='publish-port-form'
     184            2 :                         label={_("Port mapping")}
     185            2 :                         actionLabel={_("Add port mapping")}
     186            2 :                         validationFailed={validationFailed.publish}
     187            2 :                         onValidationChange={value => dynamicListOnValidationChange('publish', value)}
     188            2 :                         onChange={value => setPublish(value)}
     189            2 :                         default={{ IP: null, containerPort: null, hostPort: null, protocol: 'tcp' }}
     190            2 :                         itemcomponent={PublishPort} />
     191              : 
     192            2 :             {version.localeCompare("4", undefined, { numeric: true, sensitivity: 'base' }) >= 0 &&
     193            2 :                 <DynamicListForm id='create-pod-dialog-volume'
     194            2 :                             emptyStateString={_("No volumes specified")}
     195            2 :                             formclass='volume-form'
     196            2 :                             label={_("Volumes")}
     197            2 :                             actionLabel={_("Add volume")}
     198            2 :                             onChange={value => setVolumes(value)}
     199            2 :                             default={{ containerPath: null, hostPath: null, mode: 'rw' }}
     200            2 :                             options={{ selinuxAvailable }}
     201            2 :                             itemcomponent={Volume} />
     202              :             }
     203              : 
     204            2 :         </Form>
     205              :     );
     206              : 
     207            2 :     return (
     208            2 :         <Modal isOpen
     209            2 :                 position="top" variant="medium"
     210            2 :                 onClose={Dialogs.close}
     211            2 :                 onEscapePress={Dialogs.close}
     212            2 :                 title={_("Create pod")}
     213            2 :                 footer={<>
     214            2 :                     <Button variant='primary' id="create-pod-create-btn" onClick={onCreateClicked}
     215            2 :                             isLoading={inProgress}
     216            2 :                             isDisabled={isFormInvalid(validationFailed) || inProgress}>
     217            2 :                         {_("Create")}
     218            2 :                     </Button>
     219            2 :                     <Button variant='link' className='btn-cancel' isDisabled={inProgress} onClick={Dialogs.close}>
     220            2 :                         {_("Cancel")}
     221            2 :                     </Button>
     222            2 :                 </>}
     223              :         >
     224            2 :             {defaultBody}
     225            2 :         </Modal>
     226              :     );
     227            2 : };
        

Generated by: LCOV version 2.0-1