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 : };
|