LCOV - code coverage report
Current view: top level - src - ImageDeleteModal.jsx Coverage Total Hit
Test: cockpit-podman Lines: 91.8 % 97 89
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 { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
       5              : import { List, ListItem } from '@patternfly/react-core/dist/esm/components/List';
       6              : import {
       7              :     Modal
       8              : } from '@patternfly/react-core/dist/esm/deprecated/components/Modal';
       9              : import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack";
      10              : import { useDialogs } from "dialogs.jsx";
      11              : 
      12              : import cockpit from 'cockpit';
      13              : 
      14              : import ForceRemoveModal from './ForceRemoveModal.jsx';
      15              : import * as client from './client.js';
      16              : 
      17           43 : const _ = cockpit.gettext;
      18              : 
      19            2 : function sortTags(a, b) {
      20            2 :     if (a.endsWith(":latest"))
      21            2 :         return -1;
      22            1 :     if (b.endsWith(":latest"))
      23            1 :         return 1;
      24            1 :     return a.localeCompare(b);
      25            2 : }
      26              : 
      27            2 : export const ImageDeleteModal = ({ con, imageWillDelete, onAddNotification }) => {
      28            2 :     const Dialogs = useDialogs();
      29            1 :     const repoTags = imageWillDelete.RepoTags ? imageWillDelete.RepoTags : [];
      30            2 :     const isIntermediateImage = repoTags.length === 0;
      31              : 
      32            2 :     const [tags, setTags] = useState(repoTags.sort(sortTags).reduce((acc, item, i) => {
      33            2 :         acc[item] = (i === 0);
      34            2 :         return acc;
      35            2 :     }, {}));
      36              : 
      37            2 :     const checkedTags = Object.keys(tags).sort(sortTags)
      38            2 :             .filter(x => tags[x]);
      39              : 
      40            2 :     const onValueChanged = (item, value) => {
      41            2 :         setTags(prevState => ({
      42            2 :             ...prevState,
      43            2 :             [item]: value,
      44            2 :         }));
      45            2 :     };
      46              : 
      47            2 :     const handleRemoveImage = (tags, all, close_dialog = true) => {
      48            2 :         const handleForceRemoveImage = () => {
      49            2 :             Dialogs.close();
      50            2 :             return client.delImage(con, imageWillDelete.Id, true)
      51            0 :                     .catch(ex => {
      52            0 :                         const error = cockpit.format(_("Failed to force remove image $0"), imageWillDelete.RepoTags[0]);
      53            0 :                         onAddNotification({ type: 'danger', error, errorDetail: ex.message });
      54            0 :                     });
      55            2 :         };
      56              : 
      57            2 :         if (close_dialog) {
      58            2 :             Dialogs.close();
      59            2 :         }
      60              : 
      61            2 :         if (all) {
      62            2 :             client.delImage(con, imageWillDelete.Id, false)
      63            2 :                     .catch(ex => {
      64            1 :                         Dialogs.show(<ForceRemoveModal name={isIntermediateImage ? _("intermediate image") : repoTags[0]}
      65            2 :                                                        handleForceRemove={handleForceRemoveImage}
      66            2 :                                                        reason={ex.message} />);
      67            2 :                     });
      68            1 :         } else {
      69              :             // Call another untag once previous one resolved. Calling all at once can result in undefined behavior
      70            1 :             const tag = tags.shift();
      71            1 :             const i = tag.lastIndexOf(":");
      72            1 :             client.untagImage(con, imageWillDelete.Id, tag.substring(0, i), tag.substring(i + 1, tag.length))
      73            1 :                     .then(() => {
      74            1 :                         if (tags.length > 0)
      75            1 :                             handleRemoveImage(tags, all, false);
      76            1 :                     })
      77            0 :                     .catch(ex => {
      78            0 :                         const error = cockpit.format(_("Failed to remove image $0"), tag);
      79            0 :                         onAddNotification({ type: 'danger', error, errorDetail: ex.message });
      80            0 :                     });
      81            1 :         }
      82            2 :     };
      83              : 
      84            1 :     const imageName = repoTags[0]?.split(":")[0].split("/").at(-1) ?? _("intermediate");
      85              : 
      86            2 :     let isAllSelected = null;
      87            2 :     if (checkedTags.length === repoTags.length)
      88            2 :         isAllSelected = true;
      89            2 :     else if (checkedTags.length === 0)
      90            1 :         isAllSelected = false;
      91              : 
      92            2 :     return (
      93            2 :         <Modal isOpen
      94            2 :                  position="top" variant="medium"
      95            2 :                  titleIconVariant="warning"
      96            2 :                  onClose={Dialogs.close}
      97            2 :                  title={cockpit.format(_("Delete $0 image?"), imageName)}
      98            2 :                  footer={<>
      99            2 :                      <Button id="btn-img-delete" variant="danger" isDisabled={!isIntermediateImage && checkedTags.length === 0}
     100            2 :                              onClick={() => handleRemoveImage(checkedTags, checkedTags.length === repoTags.length)}>
     101            1 :                          {isIntermediateImage ? _("Delete image") : _("Delete tagged images")}
     102            2 :                      </Button>
     103            2 :                      <Button variant="link" onClick={Dialogs.close}>{_("Cancel")}</Button>
     104            2 :                  </>}
     105              :         >
     106            2 :             <Stack hasGutter>
     107            2 :                 { repoTags.length > 1 && <StackItem>{_("Multiple tags exist for this image. Select the tagged images to delete.")}</StackItem> }
     108            2 :                 <StackItem isFilled>
     109            2 :                     {repoTags.length > 1 && <Checkbox isChecked={isAllSelected} id='delete-all' label={_("All")} aria-label='All'
     110            2 :                         onChange={(_event, checked) => repoTags.forEach(item => onValueChanged(item, checked))}
     111            2 :                         body={
     112            2 :                             repoTags.map(x => (
     113            2 :                                 <Checkbox isChecked={checkedTags.indexOf(x) > -1}
     114            2 :                                             id={"delete-" + x}
     115            2 :                                             aria-label={x}
     116            2 :                                             key={x}
     117            2 :                                             label={x}
     118            2 :                                             onChange={(_event, checked) => onValueChanged(x, checked)} />
     119            2 :                             ))
     120            2 :                         } />}
     121            2 :                     {repoTags.length === 1 && <List><ListItem>{repoTags[0]}</ListItem></List>}
     122            2 :                 </StackItem>
     123            2 :             </Stack>
     124            2 :         </Modal>
     125              :     );
     126            2 : };
        

Generated by: LCOV version 2.0-1